Big restructuring by moving to python

This commit is contained in:
budtmo
2023-05-09 19:34:44 +02:00
parent abcdf3f4d1
commit 6767970307
448 changed files with 2493 additions and 5679 deletions
View File
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env python3
import subprocess
from typing import Union
import click
import logging
import os
from enum import Enum
from src.application import Application
from src.device import DeviceType
from src.device.emulator import Emulator
from src.device.geny_aws import GenyAWS
from src.device.geny_saas import GenySAAS
from src.helper import convert_str_to_bool, get_env_value_or_raise
from src.constants import ENV
from src.logger import log
log.init()
logger = logging.getLogger("App")
def get_device(given_input: str) -> Union[Emulator, GenyAWS, GenySAAS, None]:
"""
Get Device object based on given input
:param given_input: device in string
:return: Platform object
"""
input_lower = given_input.lower()
if input_lower == DeviceType.EMULATOR.value.lower():
emu_av = get_env_value_or_raise(ENV.EMULATOR_ANDROID_VERSION)
emu_img_type = get_env_value_or_raise(ENV.EMULATOR_IMG_TYPE)
emu_sys_img = get_env_value_or_raise(ENV.EMULATOR_SYS_IMG)
emu_device = os.getenv(ENV.EMULATOR_DEVICE, "Nexus 5")
emu_data_partition = os.getenv(ENV.EMULATOR_DATA_PARTITION, "550m")
emu_additional_args = os.getenv(ENV.EMULATOR_ADDITIONAL_ARGS, "")
emu_name = os.getenv(ENV.EMULATOR_NAME, "{d}_{v}".format(
d=emu_device.replace(" ", "_").lower(), v=emu_av))
emu = Emulator(emu_name, emu_device, emu_av, emu_data_partition,
emu_additional_args, emu_img_type, emu_sys_img)
return emu
elif input_lower == DeviceType.GENY_AWS.value.lower():
return GenyAWS()
elif input_lower == DeviceType.GENY_SAAS.value.lower():
return GenySAAS()
else:
return None
@click.group(context_settings=dict(help_option_names=['-h', '--help']))
def cli():
pass
def start_appium() -> None:
if convert_str_to_bool(os.getenv(ENV.APPIUM)):
cmd = f"/usr/bin/appium"
app_appium = Application("Appium", cmd,
os.getenv(ENV.APPIUM_ADDITIONAL_ARGS, ""), False)
app_appium.start()
else:
logger.info("env APPIUM cannot be found, Appium is not started!")
def start_device() -> None:
given_pt = get_env_value_or_raise(ENV.DEVICE_TYPE)
selected_device = get_device(given_pt)
if selected_device is None:
raise RuntimeError(f"'{given_pt}' is invalid! Please check again!")
selected_device.create()
selected_device.start()
selected_device.wait_until_ready()
selected_device.reconfigure()
selected_device.keep_alive()
def start_display_screen() -> None:
cmd = "/usr/bin/Xvfb"
args = f"{os.getenv(ENV.DISPLAY)} " \
f"-screen {os.getenv(ENV.SCREEN_NUMBER)} " \
f"{os.getenv(ENV.SCREEN_WIDTH)}x" \
f"{os.getenv(ENV.SCREEN_HEIGHT)}x" \
f"{os.getenv(ENV.SCREEN_DEPTH)}"
d_screen = Application("d_screen", cmd, args, False)
d_screen.start()
def start_display_wm() -> None:
cmd = "/usr/bin/openbox-session"
d_wm = Application("d_wm", cmd)
d_wm.start()
def start_port_forwarder() -> None:
import socket
local_ip = socket.gethostbyname(socket.gethostname())
cmd = f"/usr/bin/socat tcp-listen:5554,bind={local_ip},fork tcp:127.0.0.1:5554 & " \
f"/usr/bin/socat tcp-listen:5555,bind={local_ip},fork tcp:127.0.0.1:5555"
pf = Application("port_forwarder", cmd)
pf.start()
def start_vnc_server() -> None:
cmd = "/usr/bin/x11vnc"
vnc_pass = os.getenv(ENV.VNC_PASSWORD)
if vnc_pass:
pass_path = os.path.join(os.getenv(ENV.WORK_PATH), ".vncpass")
subprocess.check_call(f"{cmd} -storepasswd {vnc_pass} {pass_path}", shell=True)
last_arg = f"-rfbauth {pass_path}"
else:
last_arg = "-nopw"
display = os.getenv(ENV.DISPLAY)
args = f"-display {display} -forever -shared {last_arg}"
vnc_server = Application("vnc_web", cmd, args, False)
vnc_server.start()
def start_vnc_web() -> None:
if convert_str_to_bool(os.getenv(ENV.WEB_VNC)):
vnc_port = get_env_value_or_raise(ENV.VNC_PORT)
vnc_web_port = get_env_value_or_raise(ENV.WEB_VNC_PORT)
cmd = "/opt/noVNC/utils/novnc_proxy"
args = f"--vnc localhost:{vnc_port} localhost:{vnc_web_port}"
vnc_web = Application("vnc_web", cmd, args, False)
vnc_web.start()
else:
logger.info("env WEB_VNC cannot be found, VNC_WEB is not started!")
@cli.command()
@click.argument("app", type=click.Choice([app.value for app in Application.App]))
def start(app):
selected_app = str(app).lower()
if selected_app == Application.App.APPIUM.value.lower():
start_appium()
elif selected_app == Application.App.DEVICE.value.lower():
start_device()
elif selected_app == Application.App.DISPLAY_SCREEN.value.lower():
start_display_screen()
elif selected_app == Application.App.DISPLAY_WM.value.lower():
start_display_wm()
elif selected_app == Application.App.PORT_FORWARDER.value.lower():
start_port_forwarder()
elif selected_app == Application.App.VNC_SERVER.value.lower():
start_vnc_server()
elif selected_app == Application.App.VNC_WEB.value.lower():
start_vnc_web()
else:
logger.error(f"application '{selected_app}' is not supported!")
class SharedComponent(Enum):
LOG = "log"
def shared_log() -> None:
if convert_str_to_bool(os.getenv(ENV.WEB_LOG)):
from http.server import BaseHTTPRequestHandler, HTTPServer
log_path = get_env_value_or_raise(ENV.LOG_PATH)
log_port = int(get_env_value_or_raise(ENV.WEB_LOG_PORT))
logger.info(f"Shared log is enabled! all logs can be found on port '{log_port}'")
class LogSharedHandler(BaseHTTPRequestHandler):
def do_GET(self):
# root path
if self.path == "/":
html = "<html><body>"
for f in os.listdir(log_path):
html += f"<p><a href=\"{f}\">{f}</a></p>"
html += "</body></html>"
self.send_response(200)
self.send_header("Content-type", "text/html")
self.end_headers()
self.wfile.write(html.encode())
# open each selected log file
else:
p = log_path + self.path
try:
with open(p, "rb") as file:
self.send_response(200)
self.send_header("Content-type", "text/plain")
self.end_headers()
self.wfile.write(file.read())
except FileNotFoundError:
self.send_error(404, "File not found")
httpd = HTTPServer(('0.0.0.0', log_port), LogSharedHandler)
httpd.serve_forever()
else:
logger.info(f"Shared log is disabled! nothing to do!")
@cli.command()
@click.argument("component", type=click.Choice([component.value for component in SharedComponent]))
def share(component):
selected_component = str(component).lower()
if selected_component == SharedComponent.LOG.value.lower():
shared_log()
else:
logger.error(f"component '{component}' is not supported!")
if __name__ == '__main__':
cli()
+35
View File
@@ -0,0 +1,35 @@
import logging
import subprocess
from enum import Enum
class Application:
class App(Enum):
APPIUM = "appium"
DEVICE = "device"
DISPLAY_SCREEN = "display_screen"
DISPLAY_WM = "display_wm"
PORT_FORWARDER = "port_forwarder"
VNC_SERVER = "vnc_server"
VNC_WEB = "vnc_web"
def __init__(self, name: str, command: str, additional_args: str = "", ui: bool = False) -> None:
self.logger = logging.getLogger(self.__class__.__name__)
self.name = name
self.command = command
self.additional_args = additional_args
self.ui = ui
def start(self) -> None:
if self.ui:
self.logger.info(f"{self.name} will be started with ui!")
subprocess.check_call(f"/usr/bin/xterm -T {self.name} -n {self.name} "
f"-e '{self.command} {self.additional_args}'", shell=True)
else:
self.logger.info(f"{self.name} will be started without ui!")
subprocess.check_call(f"{self.command} {self.additional_args}", shell=True)
def __repr__(self) -> str:
return "Application(name={n}, command={c}, args={args}, ui={ui})".format(
n=self.name, c=self.command, args=self.additional_args, ui=self.ui)
+6
View File
@@ -0,0 +1,6 @@
# Status
STATUS_CREATING = "CREATING"
STATUS_STARTING = "STARTING"
STATUS_BOOTING = "BOOTING"
STATUS_RECONFIGURING = "RECONFIGURING"
STATUS_READY = "READY"
+45
View File
@@ -0,0 +1,45 @@
# General
DOCKER_ANDROID_VERSION = "DOCKER_ANDROID_VERSION"
USER_BEHAVIOR_ANALYTICS = "USER_BEHAVIOR_ANALYTICS"
APPIUM = "APPIUM"
APPIUM_ADDITIONAL_ARGS = "APPIUM_ADDITIONAL_ARGS"
DISPLAY = "DISPLAY"
SCREEN_DEPTH = "SCREEN_DEPTH"
SCREEN_HEIGHT = "SCREEN_HEIGHT"
SCREEN_NUMBER = "SCREEN_NUMBER"
SCREEN_WIDTH = "SCREEN_WIDTH"
VNC_PASSWORD = "VNC_PASSWORD"
VNC_PORT = "VNC_PORT"
WEB_VNC_PORT = "WEB_VNC_PORT"
WEB_VNC = "WEB_VNC"
WORK_PATH = "WORK_PATH"
LOG_PATH = "LOG_PATH"
WEB_LOG_PORT = "WEB_LOG_PORT"
WEB_LOG = "WEB_LOG"
# Device
DEVICE_INTERVAL_WAITING = "DEVICE_INTERVAL_WAITING"
DEVICE_TYPE = "DEVICE_TYPE"
# Device (Emulator)
EMULATOR_ADDITIONAL_ARGS = "EMULATOR_ADDITIONAL_ARGS"
EMULATOR_ANDROID_VERSION = "EMULATOR_ANDROID_VERSION"
EMULATOR_DATA_PARTITION = "EMULATOR_DATA_PARTITION"
EMULATOR_DEVICE = "EMULATOR_DEVICE"
EMULATOR_IMG_TYPE = "EMULATOR_IMG_TYPE"
EMULATOR_NAME = "EMULATOR_NAME"
EMULATOR_NO_SKIN = "EMULATOR_NO_SKIN"
EMULATOR_SYS_IMG = "EMULATOR_SYS_IMG"
# Device (Genymotion - General)
GENYMOTION_TEMPLATE_PATH = "GENYMOTION_TEMPLATE_PATH"
# Device (Geny_SAAS)
GENY_SAAS_USER = "GENY_SAAS_USER"
GENY_SAAS_PASS = "GENY_SAAS_PASS"
GENY_SAAS_TEMPLATE_FILE_NAME = "saas.json"
# Device (Geny_AWS)
AWS_ACCESS_KEY_ID = "AWS_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY = "AWS_SECRET_ACCESS_KEY"
GENY_AWS_TEMPLATE_FILE_NAME = "aws.json"
+1
View File
@@ -0,0 +1 @@
UTF8 = "utf-8"
+172
View File
@@ -0,0 +1,172 @@
import json
import logging
import os
import platform
import requests
import signal
import time
from abc import ABC, abstractmethod
from enum import Enum
from src.helper import convert_str_to_bool, get_env_value_or_raise
from src.constants import DEVICE, ENV
class DeviceType(Enum):
EMULATOR = "emulator"
GENY_SAAS = "geny_saas"
GENY_AWS = "geny_aws"
class Device(ABC):
FORM_ID = "1FAIpQLSdrKWQdMh6Nt8v8NQdYvTIntohebAgqWCpXT3T9NofAoxcpkw"
FORM_USER = "user"
FORM_CITY = "city"
FORM_REGION = "region"
FORM_COUNTRY = "country"
FORM_APP_VERSION = "app_version"
FORM_APPIUM = "appium"
FORM_APPIUM_ADDITIONAL_ARGS = "appium_additional_args"
FORM_WEB_LOG = "web_log"
FORM_WEB_VNC = "web_vnc"
FORM_SCREEN_RESOLUTION = "screen_resolution"
FORM_DEVICE_TYPE = "device_type"
FORM_EMU_DEVICE = "emu_device"
FORM_EMU_ANDROID_VERSION = "emu_android_version"
FORM_EMU_NO_SKIN = "emu_no_skin"
FORM_EMU_DATA_PARTITION = "emu_data_partition"
FORM_EMU_ADDITIONAL_ARGS = "emu_additional_args"
def __init__(self) -> None:
self.logger = logging.getLogger(self.__class__.__name__)
self.device_type = None
self.interval_waiting = int(os.getenv(ENV.DEVICE_INTERVAL_WAITING, 2))
self.user_behavior_analytics = convert_str_to_bool(os.getenv(ENV.USER_BEHAVIOR_ANALYTICS, "true"))
self.form_field = {
Device.FORM_USER: "entry.108751316",
Device.FORM_CITY: "entry.2083022547",
Device.FORM_REGION: "entry.1083141079",
Device.FORM_COUNTRY: "entry.1946159560",
Device.FORM_APP_VERSION: "entry.818050927",
Device.FORM_APPIUM: "entry.181610571",
Device.FORM_APPIUM_ADDITIONAL_ARGS: "entry.727759656",
Device.FORM_WEB_LOG: "entry.1225589007",
Device.FORM_WEB_VNC: "entry.2055392048",
Device.FORM_SCREEN_RESOLUTION: "entry.709976626",
Device.FORM_DEVICE_TYPE: "entry.207096546",
Device.FORM_EMU_DEVICE: "entry.1960740382",
Device.FORM_EMU_ANDROID_VERSION: "entry.671872491",
Device.FORM_EMU_NO_SKIN: "entry.403556951",
Device.FORM_EMU_DATA_PARTITION: "entry.1052258875",
Device.FORM_EMU_ADDITIONAL_ARGS: "entry.57529972"
}
self.form_data = {}
signal.signal(signal.SIGTERM, self.tear_down)
def set_status(self, current_status) -> None:
bashrc_file = f"{os.getenv(ENV.WORK_PATH)}/device_status"
with open(bashrc_file, "w+") as bf:
bf.write(current_status)
# It won't work using docker exec
# os.environ[constants.ENV_DEVICE_STATUS] = current_status
def _prepare_analytics_payload(self) -> None:
self.form_data.update({
self.form_field[Device.FORM_USER]: f"{platform.platform()}_{platform.version().replace(' ', '_')}",
self.form_field[Device.FORM_APP_VERSION]: os.getenv(ENV.DOCKER_ANDROID_VERSION),
self.form_field[Device.FORM_DEVICE_TYPE]: self.device_type,
self.form_field[Device.FORM_WEB_VNC]: convert_str_to_bool(os.getenv(ENV.WEB_VNC)),
self.form_field[Device.FORM_WEB_LOG]: convert_str_to_bool(os.getenv(ENV.WEB_LOG)),
self.form_field[Device.FORM_APPIUM]: convert_str_to_bool(os.getenv(ENV.APPIUM))
})
try:
res = requests.get("https://ipinfo.io")
if res.ok:
json_res = res.json()
self.form_data.update({
self.form_field[Device.FORM_CITY]: json_res[Device.FORM_CITY],
self.form_field[Device.FORM_REGION]: json_res[Device.FORM_REGION],
self.form_field[Device.FORM_COUNTRY]: json_res[Device.FORM_COUNTRY]
})
except requests.exceptions.RequestException as rer:
self.logger.warning(rer)
pass
except KeyError as ke:
self.logger.warning(ke)
pass
def create(self) -> None:
if self.user_behavior_analytics:
self.logger.info("Sending user behavior analytics to improve the tool")
try:
form_url = f"https://docs.google.com/forms/d/e/{Device.FORM_ID}/formResponse"
self._prepare_analytics_payload()
requests.post(url=form_url, data=self.form_data)
except requests.exceptions.RequestException as rer:
self.logger.warning(rer)
pass
self.set_status(DEVICE.STATUS_CREATING)
def start(self) -> None:
self.set_status(DEVICE.STATUS_STARTING)
def wait_until_ready(self) -> None:
self.set_status(DEVICE.STATUS_BOOTING)
def reconfigure(self) -> None:
self.set_status(DEVICE.STATUS_RECONFIGURING)
def keep_alive(self) -> None:
self.set_status(DEVICE.STATUS_READY)
self.logger.warning(f"{self.device_type} process will be kept alive to be able to get sigterm signal...")
while True:
time.sleep(2)
@abstractmethod
def tear_down(self, *args) -> None:
pass
class Genymotion(Device):
def __init__(self) -> None:
super().__init__()
self.logger = logging.getLogger(self.__class__.__name__)
def get_data_from_template(self, filename: str) -> dict:
path_template_json = os.path.join(get_env_value_or_raise(ENV.GENYMOTION_TEMPLATE_PATH), filename)
data = {}
if os.path.isfile(path_template_json):
try:
self.logger.info(path_template_json)
with open(path_template_json, "r") as f:
data = json.load(f)
except FileNotFoundError as fnf:
self.shutdown_and_logout()
self.logger.error(f"File cannot be found: {fnf}")
except json.JSONDecodeError as jde:
self.shutdown_and_logout()
self.logger.error(f"Error Decoding Json: {jde}")
except Exception as e:
self.shutdown_and_logout()
self.logger.error(e)
else:
self.shutdown_and_logout()
raise RuntimeError(f"'{path_template_json}' cannot be found!")
return data
@abstractmethod
def login(self) -> None:
pass
def create(self) -> None:
super().create()
self.login()
@abstractmethod
def shutdown_and_logout(self) -> None:
pass
def tear_down(self, *args) -> None:
self.shutdown_and_logout()
+237
View File
@@ -0,0 +1,237 @@
import logging
import os
import subprocess
import time
from enum import Enum
from src.device import Device, DeviceType
from src.helper import convert_str_to_bool, get_env_value_or_raise, symlink_force
from src.constants import ENV, UTF8
class Emulator(Device):
DEVICE = (
"Nexus 4",
"Nexus 5",
"Nexus 7",
"Nexus One",
"Nexus S",
"Samsung Galaxy S6",
"Samsung Galaxy S7",
"Samsung Galaxy S7 Edge",
"Samsung Galaxy S8",
"Samsung Galaxy S9",
"Samsung Galaxy S10"
)
API_LEVEL = {
"9.0": "28",
"10.0": "29",
"11.0": "30",
"12.0": "32",
"13.0": "33"
}
adb_name_id = 5554
class ReadinessCheck(Enum):
BOOTED = "booted"
RUN_STATE = "in running state"
WELCOME_SCREEN = "in welcome screen"
POP_UP_WINDOW = "pop up window"
def __init__(self, name: str, device: str, android_version: str, data_partition: str,
additional_args: str, img_type: str, sys_img: str) -> None:
super().__init__()
self.logger = logging.getLogger(self.__class__.__name__)
self.adb_name = f"emulator-{Emulator.adb_name_id}"
self.device_type = DeviceType.EMULATOR.value
self.name = name
if device in self.DEVICE:
self.device = device
else:
raise RuntimeError(f"device '{device}' is not supported!")
if android_version in self.API_LEVEL.keys():
self.android_version = android_version
else:
raise RuntimeError(f"android version '{android_version}' is not supported!")
self.api_level = self.API_LEVEL[self.android_version]
self.data_partition = data_partition
self.additional_args = additional_args
self.img_type = img_type
self.sys_img = sys_img
workdir = get_env_value_or_raise(ENV.WORK_PATH)
self.path_device_profile_target = os.path.join(workdir, ".android", "devices.xml")
self.path_emulator = os.path.join(workdir, "emulator")
self.path_emulator_config = os.path.join(workdir, "emulator", "config.ini")
self.path_emulator_profiles = os.path.join(workdir, "docker-android", "mixins",
"configs", "devices", "profiles")
self.path_emulator_skins = os.path.join(workdir, "docker-android", "mixins",
"configs", "devices", "skins")
self.file_name = self.device.replace(" ", "_").lower()
self.no_skin = convert_str_to_bool(os.getenv(ENV.EMULATOR_NO_SKIN))
self.interval_after_booting = 15
Emulator.adb_name_id += 2
self.form_data.update({
self.form_field[Device.FORM_SCREEN_RESOLUTION]: f"{os.getenv(ENV.SCREEN_WIDTH)}x"
f"{os.getenv(ENV.SCREEN_HEIGHT)}x"
f"{os.getenv(ENV.SCREEN_DEPTH)}",
self.form_field[Device.FORM_EMU_DEVICE]: self.device,
self.form_field[Device.FORM_EMU_ANDROID_VERSION]: self.android_version,
self.form_field[Device.FORM_EMU_NO_SKIN]: self.no_skin,
self.form_field[Device.FORM_EMU_DATA_PARTITION]: self.data_partition,
self.form_field[Device.FORM_EMU_ADDITIONAL_ARGS]: self.additional_args
})
def is_initialized(self) -> bool:
import re
if os.path.exists(self.path_emulator_config):
self.logger.info("Config file exists")
with open(self.path_emulator_config, 'r') as f:
if any(re.match(r'hw\.device\.name ?= ?{}'.format(self.device), line) for line in f):
self.logger.info("Selected device is already created")
return True
else:
self.logger.info("Selected device is not created")
return False
self.logger.info("Config file does not exist")
return False
def _add_profile(self) -> None:
if "samsung" in self.device.lower():
path_device_profile_source = os.path.join(self.path_emulator_profiles,
"{fn}.xml".format(fn=self.file_name))
symlink_force(path_device_profile_source, self.path_device_profile_target)
self.logger.info("Samsung device profile is linked")
def _add_skin(self) -> None:
device_skin_path = os.path.join(
self.path_emulator_skins, "{fn}".format(fn=self.file_name))
with open(self.path_emulator_config, "a") as cf:
cf.write("hw.keyboard=yes\n")
cf.write("disk.dataPartition.size={dp}\n".format(dp=self.data_partition))
cf.write("skin.path={sp}\n".format(
sp="_no_skin" if self.no_skin else device_skin_path))
self.logger.info(f"Skin is added in: '{self.path_emulator_config}'")
def create(self) -> None:
super().create()
first_run = not self.is_initialized()
if first_run:
self.logger.info(f"Creating the {self.device_type}...")
self._add_profile()
creation_cmd = "avdmanager create avd -f -n {n} -b {it}/{si} " \
"-k 'system-images;android-{al};{it};{si}' " \
"-d {d} -p {pe}".format(n=self.name, it=self.img_type, si=self.sys_img,
al=self.api_level, d=self.device.replace(" ", "\ "),
pe=self.path_emulator)
self.logger.info(f"Command to create emulator: '{creation_cmd}'")
subprocess.check_call(creation_cmd, shell=True)
self._add_skin()
self.logger.info(f"{self.device_type} is created!")
def change_permission(self) -> None:
kvm_path = "/dev/kvm"
if os.path.exists(kvm_path):
cmds = (f"sudo chown 1300:1301 {kvm_path}",
"sudo sed -i '1d' /etc/passwd")
for c in cmds:
subprocess.check_call(c, shell=True)
self.logger.info("KVM permission is granted!")
else:
raise RuntimeError("/dev/kvm cannot be found!")
def deploy(self):
self.logger.info(f"Deploying the {self.device_type}")
basic_cmd = "emulator @{n}".format(n=self.name)
basic_args = "-gpu swiftshader_indirect -accel on -writable-system -verbose"
wipe_arg = "-wipe-data" if not self.is_initialized() else ""
start_cmd = f"{basic_cmd} {basic_args} {wipe_arg} {self.additional_args}"
self.logger.info(f"Command to run {self.device_type}: '{start_cmd}'")
subprocess.Popen(start_cmd.split())
def start(self) -> None:
super().start()
self.change_permission()
self.deploy()
def check_adb_command(self, readiness_check_type: ReadinessCheck, bash_command: str,
expected_keyword: str, max_attempts: int, interval_waiting_time: int,
adb_action: str = None) -> None:
success = False
for _ in range(1, max_attempts):
if success:
break
else:
try:
output = subprocess.check_output(
bash_command.split()).decode(UTF8)
if expected_keyword in str(output).lower():
if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
subprocess.check_call(adb_action, shell=True)
else:
self.logger.info(
f"{self.device_type} is {readiness_check_type.value}!")
success = True
else:
self.logger.info(f"[attempt: {_}] {self.device_type} is not {readiness_check_type.value}! "
f"will check again in {interval_waiting_time} seconds")
time.sleep(interval_waiting_time)
except subprocess.CalledProcessError:
self.logger.warning("command cannot be executed! will continue...")
time.sleep(2)
continue
else:
if readiness_check_type is self.ReadinessCheck.POP_UP_WINDOW:
self.logger.info(f"Pop up windows '{expected_keyword}' is not found!")
else:
raise RuntimeError(
f"{readiness_check_type.value} is checked {_} times!")
def wait_until_ready(self) -> None:
super().wait_until_ready()
booting_cmd = f"adb -s {self.adb_name} wait-for-device shell getprop sys.boot_completed"
focus_cmd = f"adb -s {self.adb_name} shell dumpsys window | grep -i mCurrentFocus"
self.check_adb_command(self.ReadinessCheck.BOOTED,
booting_cmd, "1", 60, self.interval_waiting)
time.sleep(self.interval_after_booting)
interval_pop_up = 0
max_attempt_pop_up = 3
pop_up_system_ui = "Not Responding: com.android.systemui"
system_ui_cmd = f"adb shell su root 'kill $(pidof com.android.systemui)'"
pop_up_key_enter = {
"Not Responding: com.google.android.gms",
"Not Responding: system",
"ConversationListActivity"
}
key_enter_cmd = "adb shell input keyevent KEYCODE_ENTER"
self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pop_up_system_ui,
max_attempt_pop_up, interval_pop_up, system_ui_cmd)
for pe in pop_up_key_enter:
self.check_adb_command(self.ReadinessCheck.POP_UP_WINDOW, focus_cmd, pe, max_attempt_pop_up,
interval_pop_up, key_enter_cmd)
self.check_adb_command(self.ReadinessCheck.WELCOME_SCREEN,
focus_cmd, "launcheractivity", 60, self.interval_waiting)
self.logger.info(f"{self.device_type} is ready to use")
def tear_down(self, *args) -> None:
self.logger.warning("Sigterm is detected! Nothing to do!")
def __repr__(self) -> str:
try:
return "Emulator(name={n}, device={d}, adb_name={an}, android_version={av}, api_level={al}, " \
"data_partition={dp}, additional_args={aa}, img_type={it}, sys_img={si}, " \
"path_device_profile_target={pdpt}, path_emulator={pe}, path_emulator_config={pec}, " \
"file={f})".format(n=self.name, d=self.device, an=self.adb_name, av=self.android_version,
al=self.api_level, dp=self.data_partition, aa=self.additional_args,
it=self.img_type, si=self.sys_img, pdpt=self.path_device_profile_target,
pe=self.path_emulator, pec=self.path_emulator_config, f=self.file_name)
except AttributeError as ae:
self.logger.error(ae)
return ""
+223
View File
@@ -0,0 +1,223 @@
import json
import logging
import os
import shutil
import subprocess
import time
from src.device import Genymotion, DeviceType
from src.helper import get_env_value_or_raise
from src.constants import ENV, UTF8
class GenyAWS(Genymotion):
port = 5555
def __init__(self) -> None:
super().__init__()
self.logger = logging.getLogger(self.__class__.__name__)
self.device_type = DeviceType.GENY_AWS.value
self.workdir = get_env_value_or_raise(ENV.WORK_PATH)
self.aws_credentials_path = os.path.join(self.workdir, ".aws")
self.remove_cred_at_the_end = False # for logout
self.geny_aws_template_path = os.path.join(self.workdir, "docker-android", "mixins",
"templates", "genymotion", "aws")
self.created_devices = {}
def login(self) -> None:
aws_credentials_file = os.path.join(self.aws_credentials_path, "credentials")
if os.path.exists(self.aws_credentials_path):
self.logger.info(".aws is found! It will be used as credentials")
else:
self.logger.info(".aws cannot be found! the template will be used!")
self.remove_cred_at_the_end = True
aws_credentials_template_path = os.path.join(self.geny_aws_template_path, ".aws")
shutil.move(aws_credentials_template_path, self.aws_credentials_path)
aws_key_id = get_env_value_or_raise(ENV.AWS_ACCESS_KEY_ID)
aws_secret_key = get_env_value_or_raise(ENV.AWS_SECRET_ACCESS_KEY)
replacements_cred = {
f"<{ENV.AWS_ACCESS_KEY_ID.lower()}>": aws_key_id,
f"<{ENV.AWS_SECRET_ACCESS_KEY.lower()}>": aws_secret_key
}
with open(aws_credentials_file, 'r+') as cred_file:
cred_file_contents = cred_file.read()
for old_str, new_str in replacements_cred.items():
cred_file_contents = cred_file_contents.replace(old_str, new_str)
cred_file.seek(0)
cred_file.write(cred_file_contents)
cred_file.truncate()
self.logger.info("aws credentials is set!")
def create_ssh_key(self) -> None:
subprocess.check_call('ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa -N ""', shell=True)
self.logger.info("ssh key is created!")
def create_tf_files(self) -> None:
try:
for item in self.get_data_from_template(ENV.GENY_AWS_TEMPLATE_FILE_NAME):
name = item["name"]
region = item["region"]
ami = item["ami"]
instance_type = item["instance_type"]
if "security_group" in item:
sg = item["security_group"]
tf_content = f'''
provider "aws" {{
alias = "provider_{name}"
region = "{region}"
}}
resource "aws_key_pair" "geny_key_{name}" {{
provider = aws.provider_{name}
public_key = file("~/.ssh/id_rsa.pub")
}}
resource "aws_instance" "geny_aws_{name}" {{
provider = aws.provider_{name}
ami = "{ami}"
instance_type = "{instance_type}"
vpc_security_group_ids = ["{sg}"]
key_name = aws_key_pair.geny_key_{name}.key_name
tags = {{
Name = "DockerAndroid-GenyAWS-{ami}.id}}"
}}
provisioner "remote-exec" {{
connection {{
type = "ssh"
user = "shell"
host = self.public_ip
private_key = file("~/.ssh/id_rsa")
}}
script = "/home/androidusr/docker-android/mixins/scripts/genymotion/aws/enable_adb.sh"
}}
}}
output "public_dns_{name}" {{
value = aws_instance.geny_aws_{name}.public_dns
}}
'''
else:
ingress_rules = json.dumps(item["ingress_rules"])
egress_rules = json.dumps(item["egress_rules"])
tf_content = f'''
locals {{
ingress_rules = {ingress_rules}
egress_rules = {egress_rules}
}}
provider "aws" {{
alias = "provider_{name}"
region = "{region}"
}}
resource "aws_security_group" "geny_sg_{name}" {{
provider = aws.provider_{name}
dynamic "ingress" {{
for_each = local.ingress_rules
content {{
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}}
}}
dynamic "egress" {{
for_each = local.egress_rules
content {{
from_port = egress.value.from_port
to_port = egress.value.to_port
protocol = egress.value.protocol
cidr_blocks = egress.value.cidr_blocks
}}
}}
}}
resource "aws_key_pair" "geny_key_{name}" {{
provider = aws.provider_{name}
public_key = file("~/.ssh/id_rsa.pub")
}}
resource "aws_instance" "geny_aws_{name}" {{
provider = aws.provider_{name}
ami = "{ami}"
instance_type = "{instance_type}"
vpc_security_group_ids = [aws_security_group.geny_sg_{name}.name]
key_name = aws_key_pair.geny_key_{name}.key_name
tags = {{
Name = "DockerAndroid-GenyAWS-{ami}.id}}"
}}
provisioner "remote-exec" {{
connection {{
type = "ssh"
user = "shell"
host = self.public_ip
private_key = file("~/.ssh/id_rsa")
}}
script = "/home/androidusr/docker-android/mixins/scripts/genymotion/aws/enable_adb.sh"
}}
}}
output "public_dns_{name}" {{
value = aws_instance.geny_aws_{name}.public_dns
}}
'''
tf_deployment_filename = f"{name}.tf"
self.created_devices[name] = GenyAWS.port
GenyAWS.port += 1
with open(tf_deployment_filename, "w") as df:
df.write(tf_content)
self.logger.info("Terraform files are created!")
except Exception as e:
self.logger.error(e)
self.shutdown_and_logout()
def deploy_tf(self) -> None:
try:
cmds = (
"terraform init",
"terraform plan",
"terraform apply -auto-approve"
)
for c in cmds:
subprocess.check_call(c, shell=True)
self.logger.info("Genymotion-Device(s) are deployed on AWS")
except subprocess.CalledProcessError as cpe:
self.logger.error(cpe)
self.shutdown_and_logout()
def connect_with_local_adb(self) -> None:
self.logger.info(f"created devices: {self.created_devices}")
try:
for d, p in self.created_devices.items():
dns_cmd = f"terraform output public_dns_{d}"
dns_ip = subprocess.check_output(dns_cmd.split()).decode(UTF8).replace('"', '')
tunnel_cmd = f"ssh -i ~/.ssh/id_rsa -o ServerAliveInterval=60 -o StrictHostKeyChecking=no -q -NL " \
f"{p}:localhost:5555 shell@{dns_ip}"
subprocess.Popen(tunnel_cmd.split())
time.sleep(10)
subprocess.check_call(f"adb connect localhost:{p} >/dev/null 2>&1", shell=True)
except Exception as e:
self.logger.error(e)
self.shutdown_and_logout()
def create(self) -> None:
super().create()
self.create_ssh_key()
self.create_tf_files()
self.deploy_tf()
self.connect_with_local_adb()
def shutdown_and_logout(self) -> None:
try:
subprocess.check_call("terraform destroy -auto-approve -lock=false", shell=True)
self.logger.info("device(s) is successfully removed!")
except subprocess.CalledProcessError as cpe:
self.logger.error(cpe)
finally:
if self.remove_cred_at_the_end:
subprocess.check_call(f"rm -rf {self.aws_credentials_path}", shell=True)
self.logger.info("successfully logged out!")
+72
View File
@@ -0,0 +1,72 @@
import logging
import subprocess
from src.device import Genymotion, DeviceType
from src.helper import get_env_value_or_raise
from src.constants import ENV, UTF8
class GenySAAS(Genymotion):
def __init__(self) -> None:
super().__init__()
self.logger = logging.getLogger(self.__class__.__name__)
self.device_type = DeviceType.GENY_SAAS.value
self.created_devices = []
def login(self) -> None:
user = get_env_value_or_raise(ENV.GENY_SAAS_USER)
password = get_env_value_or_raise(ENV.GENY_SAAS_PASS)
subprocess.check_call(f"gmsaas auth login {user} {password} > /dev/null 2>&1", shell=True)
self.logger.info("successfully logged in!")
def create(self) -> None:
super().create()
for item in self.get_data_from_template(ENV.GENY_SAAS_TEMPLATE_FILE_NAME):
name = ""
template = ""
local_port = ""
# implement like this because local_port param is not a must
for k, v in item.items():
if k.lower() == "name":
name = v
elif k.lower() == "template":
template = v
elif k.lower() == "local_port":
local_port = v
else:
self.logger.warning(f"'{k}' is not supported! Please check the documentation!")
if not name:
import uuid
name = str(uuid.uuid4())
if not template:
self.shutdown_and_logout()
raise RuntimeError(f"'template' is a must parameter and not given!")
else:
self.logger.info(f"name: {name}, template: {template}")
creation_cmd = f"gmsaas instances start {template} {name}"
try:
instance_id = subprocess.check_output(creation_cmd.split()).decode(UTF8).replace("\n", "")
created_device = {f"{name}": {instance_id}}
self.created_devices.append(created_device)
additional_args = ""
if local_port:
additional_args = f"--adb-serial-port {local_port}"
connect_cmd = f"gmsaas instances adbconnect {instance_id} {additional_args}"
subprocess.check_call(f"{connect_cmd}", shell=True)
except Exception as e:
self.shutdown_and_logout()
self.logger.error(e)
exit(1)
def shutdown_and_logout(self) -> None:
if bool(self.created_devices):
self.logger.info("Created device(s) will be removed!")
for d in self.created_devices:
for n, i in d.items():
subprocess.check_call(f"gmsaas instances stop {i}", shell=True)
self.logger.info(f"device '{n}' is successfully removed!")
subprocess.check_call("gmsaas auth logout", shell=True)
self.logger.info("successfully logged out!")
+56
View File
@@ -0,0 +1,56 @@
import logging
import os
logger = logging.getLogger("helper")
def convert_str_to_bool(given_str: str) -> bool:
"""
Convert String to Boolean value
:param given_str: given string
:return: converted string in Boolean
"""
if given_str:
if type(given_str) is str:
return given_str.lower() in ("yes", "true", "t", "1")
else:
raise AttributeError
else:
logger.info(f"'{given_str}' is empty!")
return False
def get_env_value_or_raise(env_key: str) -> str:
"""
Get value of necessary environment variable.
:param env_key: given environment variable
:return: env_value in String
"""
try:
env_value = os.getenv(env_key)
if not env_value:
raise RuntimeError(f"'{env_key}' is missing.")
elif env_value.isspace():
raise RuntimeError(f"'{env_key}' contains only white space.")
return env_value
except TypeError as t_err:
logger.error(t_err)
def symlink_force(source: str, target: str) -> None:
"""
Create Symbolic link
:param source: source file
:param target: target file
:return: None
"""
try:
os.symlink(source, target)
except FileNotFoundError as ffe_err:
logger.error(ffe_err)
except FileExistsError:
os.remove(target)
os.symlink(source, target)
+3
View File
@@ -0,0 +1,3 @@
import os
LOGGING_FILE = os.path.join(os.path.dirname(__file__), 'logging.conf')
+7
View File
@@ -0,0 +1,7 @@
import logging.config
from src.logger import LOGGING_FILE
def init():
logging.config.fileConfig(LOGGING_FILE)
+21
View File
@@ -0,0 +1,21 @@
[loggers]
keys=root
[handlers]
keys=console
[formatters]
keys=formatter
[logger_root]
level=INFO
handlers=console
[handler_console]
class=StreamHandler
formatter=formatter
args=(sys.stdout,)
[formatter_formatter]
format=%(asctime)s %(levelname)s %(name)s - %(message)s
datefmt=%Y-%m-%d %H:%M:%S
+9
View File
@@ -0,0 +1,9 @@
from unittest import TestCase
class BaseTest(TestCase):
def setUp(self) -> None:
pass
def tearDown(self) -> None:
pass
+21
View File
@@ -0,0 +1,21 @@
import os
from src.constants import ENV
from src.tests import BaseTest
class BaseDeviceTest(BaseTest):
DEVICE_ENVS = {
ENV.WORK_PATH: "/home/androidusr",
ENV.USER_BEHAVIOR_ANALYTICS: str(False),
ENV.EMULATOR_NO_SKIN: str(False)
}
def setUp(self) -> None:
for k, v in self.DEVICE_ENVS.items():
os.environ[k] = v
def tearDown(self) -> None:
for k in self.DEVICE_ENVS.keys():
if os.environ[k]:
del os.environ[k]
+8
View File
@@ -0,0 +1,8 @@
from src.device import Device
from src.tests.device import BaseDeviceTest
class TestDevice(BaseDeviceTest):
def test_create_device(self):
with self.assertRaises(TypeError):
Device()
+87
View File
@@ -0,0 +1,87 @@
import mock
from src.device.emulator import Emulator
from src.tests.device import BaseDeviceTest
class TestEmulator(BaseDeviceTest):
def setUp(self) -> None:
super().setUp()
self.name = "my_emu"
self.device = "Nexus 4"
self.a_version = "10.0"
self.d_partition = "550m"
self.additional_args = ""
self.i_type = "google_apis"
self.s_img = "x86"
self.emu = Emulator(self.name, self.device, self.a_version, self.d_partition,
self.additional_args, self.i_type, self.s_img)
def tearDown(self) -> None:
super().tearDown()
def test_adb_name(self):
my_emu = Emulator("my_other_emu", self.device, self.a_version, self.d_partition,
self.additional_args, self.i_type, self.s_img)
self.assertNotEqual(self.emu.adb_name, my_emu.adb_name)
def test_invalid_device(self):
with self.assertRaises(RuntimeError):
Emulator("my_other_emu", "unknown device", self.a_version, self.d_partition,
self.additional_args, self.i_type, self.s_img)
with self.assertRaises(RuntimeError):
Emulator("my_other_emu", "NEXUS 5", self.a_version, self.d_partition,
self.additional_args, self.i_type, self.s_img)
def test_invalid_android_version(self):
with self.assertRaises(RuntimeError):
Emulator("my_other_emu", self.device, "0.0", self.d_partition,
self.additional_args, self.i_type, self.s_img)
@mock.patch("os.path.exists", mock.MagicMock(return_value=False))
def test_initialisation_config_not_exist(self):
self.assertEqual(self.emu.is_initialized(), False)
@mock.patch("os.path.exists", mock.MagicMock(return_value=True))
@mock.patch("builtins.open", mock.mock_open(read_data=""))
def test_initialisation_device_not_exist(self):
self.assertEqual(self.emu.is_initialized(), False)
@mock.patch("os.path.exists", mock.MagicMock(return_value=True))
@mock.patch("builtins.open", mock.mock_open(read_data="hw.device.name=Nexus 4\n"))
def test_initialisation_device_exists(self):
self.assertEqual(self.emu.is_initialized(), True)
@mock.patch("src.device.Device.set_status")
@mock.patch("src.device.emulator.Emulator._add_profile")
@mock.patch("subprocess.check_call")
@mock.patch("src.device.emulator.Emulator._add_skin")
@mock.patch("src.device.emulator.Emulator.is_initialized", mock.MagicMock(return_value=False))
def test_create_device_not_exist(self, mocked_status, mocked_profile, mocked_subprocess, mocked_skin):
self.emu.create()
self.assertEqual(mocked_status.called, True)
self.assertEqual(mocked_profile.called, True)
self.assertEqual(mocked_subprocess.called, True)
self.assertEqual(mocked_skin.called, True)
@mock.patch("src.device.Device.set_status")
@mock.patch("src.device.emulator.Emulator._add_profile")
@mock.patch("subprocess.check_call")
@mock.patch("src.device.emulator.Emulator._add_skin")
@mock.patch("src.device.emulator.Emulator.is_initialized", mock.MagicMock(return_value=True))
def test_create_device_exists(self, mocked_status, mocked_profile, mocked_subprocess, mocked_skin):
self.emu.create()
self.assertEqual(mocked_status.called, False)
self.assertEqual(mocked_profile.called, False)
self.assertEqual(mocked_subprocess.called, False)
def test_check_adb_command(self):
with mock.patch("subprocess.check_output", mock.MagicMock(return_value="1".encode("utf-8"))):
self.emu.check_adb_command(
self.emu.ReadinessCheck.BOOTED, "mocked_command", "1", 3, 0)
def test_check_adb_command_out_of_attempts(self):
with mock.patch("subprocess.check_output", mock.MagicMock(return_value=" ".encode("utf-8"))):
with self.assertRaises(RuntimeError):
self.emu.check_adb_command(
self.emu.ReadinessCheck.BOOTED, "mocked_command", "1", 3, 0)
View File
+73
View File
@@ -0,0 +1,73 @@
import os
import mock
from src.helper import convert_str_to_bool, get_env_value_or_raise, symlink_force
from src.tests import BaseTest
class TestHelperMethods(BaseTest):
def test_boolean_converter_with_valid_str(self):
self.assertEqual(convert_str_to_bool("TRUE"), True)
self.assertEqual(convert_str_to_bool("true"), True)
self.assertEqual(convert_str_to_bool("T"), True)
self.assertEqual(convert_str_to_bool("Yes"), True)
self.assertEqual(convert_str_to_bool("1"), True)
self.assertEqual(convert_str_to_bool("False"), False)
self.assertEqual(convert_str_to_bool("f"), False)
self.assertEqual(convert_str_to_bool("0"), False)
def test_boolean_converter_with_empty(self):
self.assertEqual(convert_str_to_bool(None), False)
self.assertEqual(convert_str_to_bool(""), False)
def test_boolean_converter_with_invalid_str(self):
self.assertEqual(convert_str_to_bool(" "), False)
self.assertEqual(convert_str_to_bool("test"), False)
def test_boolean_converter_with_invalid_format(self):
with self.assertRaises(AttributeError):
convert_str_to_bool(True)
def test_get_env_value_from_valid_key(self):
env_key = "env_key01"
os.environ[env_key] = "env_value01"
get_env_value_or_raise(env_key)
del os.environ[env_key]
def test_get_env_value_with_empty_string(self):
with self.assertRaises(RuntimeError):
env_key = "env_key01"
os.environ[env_key] = " "
get_env_value_or_raise(env_key)
del os.environ[env_key]
def test_get_env_value_from_invalid_key(self):
with self.assertRaises(RuntimeError):
get_env_value_or_raise("env_key02")
def test_get_env_value_with_invalid_format(self):
with mock.patch("src.logger"):
get_env_value_or_raise(True)
def test_symlink(self):
s = os.path.join("source.txt")
t = os.path.join("target_file.txt")
open(s, "a").close()
symlink_force(s, t)
os.remove(s)
os.remove(t)
def test_symlink_already_exist(self):
s = os.path.join("source.txt")
t = os.path.join("target_file.txt")
open(s, "a").close()
open(t, "a").close()
symlink_force(s, t)
os.remove(s)
os.remove(t)
def test_symlink_file_not_exists(self):
s = os.path.join("source.txt")
t = os.path.join("target_file.txt")
symlink_force(s, t)
os.remove(t)