├── .gitignore ├── .idea ├── .name ├── AutomationTestSupervisor.iml ├── codeStyleSettings.xml ├── dictionaries │ └── F1sherKK.xml ├── inspectionProfiles │ └── Project_Default.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── Launcher.py ├── README.md ├── config_files_dir.json ├── error └── Exceptions.py ├── git_images ├── html_dashboard_example1.png ├── html_dashboard_example2.png └── test_package_example.png ├── log_generator ├── GeneratorModels.py ├── LogGenerator.py ├── __init__.py ├── styles │ ├── logcat.css │ └── summary.css └── utils │ ├── HtmlLogcatUtils.py │ ├── HtmlResultsUtils.py │ ├── HtmlSummaryUtils.py │ └── HtmlUtils.py ├── session ├── SessionDataStores.py ├── SessionGlobalLogger.py ├── SessionManagers.py ├── SessionModels.py └── SessionThreads.py ├── settings ├── GlobalConfig.py ├── Version.py ├── loader │ ├── ArgLoader.py │ ├── AvdSetLoader.py │ ├── LaunchPlanLoader.py │ ├── PathsLoader.py │ └── TestSetLoader.py └── manifest │ ├── models │ ├── AvdManifestModels.py │ ├── LaunchManifestModels.py │ ├── PathManifestModels.py │ └── TestManifestModels.py │ └── templates │ ├── avdManifest.json │ ├── launchManifest.json │ ├── pathManifest.json │ └── testManifest.json └── system ├── bin ├── AndroidBinaryFileControllers.py ├── AndroidShellCommandAssemblers.py ├── AndroidShellCommands.py └── SystemShellCommands.py ├── buffer └── AdbCallBuffer.py ├── console ├── Color.py ├── Printer.py └── ShellHelper.py ├── file └── FileUtils.py └── port └── PortManager.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # Pyc files 7 | *.pyc 8 | *.cpython-36.pyc 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # dotenv 87 | .env 88 | 89 | # virtualenv 90 | .venv 91 | venv/ 92 | ENV/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # Launcher output 101 | output/ 102 | 103 | # Project idea 104 | .idea/workspace.xml 105 | .idea/tasks.xml 106 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | AutomationTestSupervisor -------------------------------------------------------------------------------- /.idea/AutomationTestSupervisor.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/dictionaries/F1sherKK.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | aapt 5 | avdmanager 6 | badging 7 | cmds 8 | displayname 9 | filenames 10 | filepath 11 | gradle 12 | gradlew 13 | initdata 14 | logcat 15 | logcats 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Launcher.py: -------------------------------------------------------------------------------- 1 | from error.Exceptions import LauncherFlowInterruptedException 2 | 3 | from session.SessionDataStores import ( 4 | DeviceStore, 5 | ApkStore, 6 | TestStore 7 | ) 8 | from session import SessionGlobalLogger as session_logger 9 | from session.SessionManagers import ( 10 | CleanUpManager, 11 | DeviceManager, 12 | ApkManager, 13 | TestManager 14 | ) 15 | 16 | from settings import ( 17 | GlobalConfig, 18 | Version 19 | ) 20 | from settings.loader import ( 21 | PathsLoader, 22 | LaunchPlanLoader, 23 | AvdSetLoader, 24 | TestSetLoader 25 | ) 26 | 27 | from system.console import Printer 28 | from system.bin.AndroidBinaryFileControllers import ( 29 | AaptController, 30 | AdbController, 31 | AdbShellController, 32 | AdbPackageManagerController, 33 | AdbSettingsController, 34 | AdbLogCatController, 35 | AvdManagerController, 36 | EmulatorController, 37 | GradleController, 38 | InstrumentationRunnerController, 39 | ) 40 | 41 | from log_generator import LogGenerator 42 | 43 | 44 | class Launcher: 45 | def __init__(self): 46 | self.avd_set = None 47 | self.avd_schemas = None 48 | 49 | self.test_set = None 50 | self.test_list = None 51 | 52 | self.adb_controller = None 53 | self.adb_shell_controller = None 54 | self.adb_package_manager_controller = None 55 | self.adb_settings_controller = None 56 | self.adb_logcat_controller = None 57 | self.avdmanager_controller = None 58 | self.emulator_controller = None 59 | self.aapt_controller = None 60 | self.gradle_controller = None 61 | self.instrumentation_runner_controller = None 62 | 63 | self.log_store = None 64 | self.log_manager = None 65 | 66 | self.device_store = None 67 | self.device_manager = None 68 | 69 | self.apk_store = None 70 | self.apk_manager = None 71 | 72 | self.test_store = None 73 | self.test_manager = None 74 | 75 | self.clean_up_manager = None 76 | 77 | @staticmethod 78 | def _init_phase(): 79 | Printer.phase("INIT") 80 | session_logger.log_session_start_time() 81 | 82 | Printer.step("Launcher started working!") 83 | Version.info() 84 | Version.python_check() 85 | 86 | def _load_config_phase(self): 87 | Printer.phase("LOADING CONFIG") 88 | 89 | Printer.step("Preparing paths.") 90 | PathsLoader.init_paths() 91 | 92 | Printer.step("Preparing launch plan.") 93 | LaunchPlanLoader.init_launch_plan() 94 | 95 | Printer.step("Preparing avd settings.") 96 | self.avd_set, self.avd_schemas = AvdSetLoader.init_avd_settings() 97 | 98 | Printer.step("Preparing test settings.") 99 | self.test_set, self.test_list = TestSetLoader.init_test_settings() 100 | 101 | def _object_init_phase(self): 102 | Printer.phase("OBJECT GRAPH INITIALIZATION") 103 | 104 | Printer.step("Creating objects handling binary file communication.") 105 | self.adb_controller = AdbController() 106 | self.adb_shell_controller = AdbShellController() 107 | self.adb_package_manager_controller = AdbPackageManagerController() 108 | self.adb_settings_controller = AdbSettingsController() 109 | self.avdmanager_controller = AvdManagerController() 110 | self.adb_logcat_controller = AdbLogCatController() 111 | self.emulator_controller = EmulatorController() 112 | self.aapt_controller = AaptController() 113 | self.gradle_controller = GradleController() 114 | self.instrumentation_runner_controller = InstrumentationRunnerController() 115 | 116 | Printer.step("Creating objects controlling devices.") 117 | self.device_store = DeviceStore(self.adb_controller, 118 | self.adb_package_manager_controller, 119 | self.adb_settings_controller, 120 | self.avdmanager_controller, 121 | self.emulator_controller) 122 | 123 | self.device_manager = DeviceManager(self.device_store, 124 | self.adb_controller, 125 | self.adb_shell_controller, 126 | self.avdmanager_controller) 127 | 128 | Printer.step("Creating objects handling .*apk file related operations.") 129 | self.apk_store = ApkStore(self.aapt_controller) 130 | self.apk_manager = ApkManager(self.device_store, 131 | self.apk_store, 132 | self.gradle_controller, 133 | self.aapt_controller) 134 | 135 | Printer.step("Creating objects controlling test session.") 136 | self.test_store = TestStore() 137 | self.test_manager = TestManager(self.instrumentation_runner_controller, 138 | self.adb_controller, 139 | self.adb_shell_controller, 140 | self.adb_package_manager_controller, 141 | self.adb_logcat_controller, 142 | self.device_store, 143 | self.test_store) 144 | 145 | Printer.step("Creating objects handling clean up.") 146 | self.clean_up_manager = CleanUpManager(self.device_store, 147 | self.adb_controller, 148 | self.adb_shell_controller) 149 | 150 | def _pre_device_preparation_clean_up_phase(self): 151 | Printer.phase("PRE-DEVICE PREPARATION CLEAN UP") 152 | 153 | Printer.step("Clean-up of launcher output directories") 154 | self.clean_up_manager.prepare_output_directories() 155 | 156 | Printer.step("Restarting ADB server.") 157 | if GlobalConfig.SHOULD_RESTART_ADB: 158 | self.clean_up_manager.restart_adb() 159 | 160 | def _device_preparation_phase(self): 161 | Printer.phase("DEVICE PREPARATION") 162 | 163 | if GlobalConfig.SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION: 164 | Printer.step("Killing currently launched AVD.") 165 | session_logger.log_total_device_creation_start_time() 166 | 167 | self.device_manager.add_models_representing_outside_session_virtual_devices() 168 | if self.device_manager.is_any_avd_visible(): 169 | self.device_manager.kill_all_avd() 170 | self.device_manager.clear_models_representing_outside_session_virtual_devices() 171 | 172 | if self.avd_set.avd_list: 173 | Printer.step("Creating models for devices specified in AVD set.") 174 | self.device_manager.add_models_based_on_avd_schema(self.avd_set, self.avd_schemas) 175 | 176 | if GlobalConfig.SHOULD_RECREATE_EXISTING_AVD: 177 | Printer.step("Creating all requested AVD from scratch. Recreating existing ones.") 178 | self.device_manager.create_all_avd_and_recreate_existing() 179 | else: 180 | Printer.step("Creating all requested AVD from scratch. Reusing existing ones.") 181 | self.device_manager.create_all_avd_and_reuse_existing() 182 | 183 | session_logger.log_total_device_creation_end_time() 184 | else: 185 | Printer.step("Creating models for currently visible Android Devices and AVD.") 186 | self.device_manager.add_models_representing_outside_session_devices() 187 | self.device_manager.add_models_representing_outside_session_virtual_devices() 188 | 189 | if not self.device_manager.is_any_avd_visible(): 190 | quit() 191 | 192 | if GlobalConfig.IGNORED_DEVICE_LIST: 193 | Printer.step("Checking device ignore list") 194 | self.device_manager.clear_models_with_android_ids_in_ignore_list() 195 | 196 | def _device_launch_phase(self): 197 | Printer.phase("DEVICE LAUNCH") 198 | session_logger.log_total_device_launch_start_time() 199 | 200 | if GlobalConfig.SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION: 201 | if GlobalConfig.SHOULD_LAUNCH_AVD_SEQUENTIALLY: 202 | Printer.step("Launching AVD - sequentially.") 203 | self.device_manager.launch_all_avd_sequentially() 204 | else: 205 | Printer.step("Launching AVD - all at once.") 206 | self.device_manager.launch_all_avd_at_once() 207 | else: 208 | Printer.step("Using currently launched devices.") 209 | 210 | session_logger.log_total_device_launch_end_time() 211 | 212 | def _apk_preparation_phase(self): 213 | Printer.phase("APK PREPARATION") 214 | session_logger.log_total_apk_build_start_time() 215 | 216 | Printer.step("Preparing .*apk for test.") 217 | if GlobalConfig.SHOULD_BUILD_NEW_APK: 218 | apk = self.apk_manager.build_apk(self.test_set) 219 | else: 220 | apk = self.apk_manager.get_existing_apk(self.test_set) 221 | if apk is None: 222 | apk = self.apk_manager.build_apk(self.test_set) 223 | self.apk_manager.display_picked_apk_info() 224 | 225 | Printer.step("Scanning .*apk for helpful data.") 226 | self.apk_manager.set_instrumentation_runner_according_to(apk) 227 | 228 | session_logger.log_total_apk_build_end_time() 229 | 230 | def _apk_installation_phase(self): 231 | Printer.phase("APK INSTALLATION") 232 | session_logger.log_total_apk_install_start_time() 233 | 234 | Printer.step("Installing .*apk on devices included in test session.") 235 | apk = self.apk_manager.get_existing_apk(self.test_set) 236 | self.apk_manager.install_apk_on_devices(apk) 237 | 238 | session_logger.log_total_apk_install_end_time() 239 | 240 | def _pre_test_clean_up_phase(self): 241 | Printer.phase("PRE-TESTING CLEAN UP") 242 | 243 | if GlobalConfig.SHOULD_RECORD_TESTS: 244 | Printer.step("Preparing directory for recordings storage on test devices") 245 | self.clean_up_manager.prepare_device_directories() 246 | 247 | def _testing_phase(self): 248 | Printer.phase("TESTING") 249 | session_logger.log_total_test_start_time() 250 | 251 | Printer.step("Starting tests.") 252 | self.test_manager.run_tests(self.test_set, self.test_list) 253 | 254 | session_logger.log_total_test_end_time() 255 | 256 | def _flakiness_check_phase(self): 257 | if GlobalConfig.SHOULD_RERUN_FAILED_TESTS: 258 | Printer.phase("FLAKINESS CHECK") 259 | session_logger.log_total_rerun_start_time() 260 | 261 | Printer.step("Re-running failed tests.") 262 | self.test_manager.rerun_failed_tests() 263 | 264 | session_logger.log_total_rerun_end_time() 265 | 266 | def _finalization_phase(self): 267 | Printer.phase("FINALIZATION") 268 | 269 | Printer.step("Displaying saved files during test session.") 270 | session_logger.dump_saved_files_history() 271 | 272 | Printer.step("Session summary.") 273 | session_logger.log_session_end_time() 274 | session_logger.save_session_summary() 275 | session_logger.dump_session_summary() 276 | LogGenerator.generate_logs(self.test_set) 277 | 278 | if self.device_manager is not None and self.device_manager.is_any_avd_visible() \ 279 | and GlobalConfig.SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION: 280 | Printer.step("Killing AVD spawned for test session.") 281 | self.device_manager.kill_all_avd() 282 | self.device_manager.clear_models_based_on_avd_schema() 283 | 284 | def run(self): 285 | try: 286 | self._init_phase() 287 | self._load_config_phase() 288 | self._object_init_phase() 289 | self._pre_device_preparation_clean_up_phase() 290 | self._device_preparation_phase() 291 | self._device_launch_phase() 292 | self._apk_preparation_phase() 293 | self._apk_installation_phase() 294 | self._pre_test_clean_up_phase() 295 | self._testing_phase() 296 | self._flakiness_check_phase() 297 | except Exception as e: 298 | if isinstance(e, LauncherFlowInterruptedException): 299 | Printer.error(e.caller_tag, str(e)) 300 | quit() 301 | else: 302 | raise e 303 | finally: 304 | self._finalization_phase() 305 | 306 | 307 | if __name__ == "__main__": 308 | launcher = Launcher() 309 | launcher.run() 310 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutomationTestSupervisor 2 | AutomationTestSupervisor is a Python tool that is capable of creating, running Android Virtual Devices, building and installing .apk files on devices, running tests and generating logs. It’s strongest point is customizability as it allows you to easily manage which tests should be ran in session, devices to be used, paths on machine to be read and more. It allows to create run profiles which are set once and can be reused after. 3 | 4 | ## Related Articles 5 | - [Story behind AutomationTestSupervisor — our custom made tool for Android automation tests](https://medium.com/azimolabs/story-behind-automationtestsupervisor-our-custom-made-tool-for-android-automation-tests-180c74a5cbfb) 6 | 7 | ## Example Project 8 | We are aware of that ATS has many functions and features. Some people prefer to see code rather then read blocks of text. That's why we have prepared example Android application - **AzimoNote** and wrote few Espresso tests so you can pull it and launch ATS by yourself. 9 | 10 | In this example ATS is joined with **AzimoNote** in form of submodule. We use it the same way in Azimo project. It's quite convenient way as you can easily delegate ATS work to Jenkins or Gitlab. It is being downloaded along with Azimo repository and it's already pre-configured. You only need one CI command to launch it. 11 | 12 | We have provided **Quick Launch** for you to follow. 13 | - [Link to AzimoNote project](https://github.com/AzimoLabs/AzimoNote) 14 | 15 | ## Quick Launch 16 | Meeting minimum launch requirements step by step: 17 | 1. Make sure you have Python 3.6 or newer installed and available from terminal. (If you are MAC OS user we highly recommend [Homebrew](https://brew.sh/index_pl.html) -> `brew install python3`.) 18 | 2. Clone AutomationTestSupervisor project to you machine from https://github.com/AzimoLabs/AutomationTestSupervisor. 19 | 3. Navigate to AutomationTestSupervisor folder on your machine. 20 | 4. Launch ATS with following command: 21 | ``` 22 | python3 Launcher.py 23 | ``` 24 | 25 | ### Launch command parameters 26 | It is possible to add four parameters to ATS launch command: 27 | - pset < name of Path Module set > 28 | - lplan < name of Launch Module launch plan > 29 | - aset < name of AVD Module set > 30 | - tset < name of Test Module set > 31 | 32 | Parameters can be added in any order you like. If you omit some parameter ATS will try to look for set/plan with name `"default"` in manifest JSON files. 33 | 34 | In example: 35 | ``` 36 | python3 Launcher.py -tset deeplinkTests -aset 3AVD-API23 -pset JenkinsPaths 37 | ``` 38 | 39 | ## Module configurations 40 | AutomationTestSupervisor can be divided into four main modules where each of them has it’s own config manifest file. Modules are integrated which each other and forming linear pipeline with set of steps. By modifying manifest of each module you can adjust behaviour of each step. 41 | 42 | We wanted to make ATS project as self-contained as possible. That means we didn't wanted to force you to edit anything in the project. General idea is to just pull it and provide your own config files in right directory. In root of ATS project there is `config_files_dir.json` file which looks like this: 43 | ``` 44 | { 45 | "path_manifest_path": "../automationTestSupervisorConfig/pathManifest.json", 46 | "launch_manifest_path": "../automationTestSupervisorConfig/launchManifest.json", 47 | "test_manifest_path": "../automationTestSupervisorConfig/testManifest.json", 48 | "avd_manifest_path": "../automationTestSupervisorConfig/avdManifest.json" 49 | } 50 | ``` 51 | It points for directory where you should put config .json files -> [This is how it looks in test project](https://github.com/AzimoLabs/AzimoNote/tree/master/automation). 52 | 53 | [Templates for .json manifest files](https://github.com/AzimoLabs/AutomationTestSupervisor/tree/master/settings/manifest/templates) are located in AutomationTestSupervisor project itself. 54 | 55 | ### Path Module 56 | File pathManifest.json: 57 | ``` 58 | 59 | { 60 | "path_set_list": [ 61 | { 62 | "set_name": "default", 63 | "paths": [ 64 | { 65 | "path_name": "sdk_dir", 66 | "path_value": "", 67 | "path_description": "Path to Android Software Development kit. By default stored in ANDROID_HOME env." 68 | }, 69 | { 70 | "path_name": "avd_dir", 71 | "path_value": "", 72 | "path_description": "Location to .android folder where AVD images are stored. By default stored in ANDROID_SDK_HOME env." 73 | }, 74 | { 75 | "path_name": "output_dir", 76 | "path_value": "", 77 | "path_description": "Path to where test process logs will be saved. By default ./output/." 78 | }, 79 | { 80 | "path_name": "project_root_dir", 81 | "path_value": "", 82 | "path_description": "Path to root of application under test. (optional)" 83 | }, 84 | { 85 | "path_name": "apk_dir", 86 | "path_value": "", 87 | "path_description": "Directory where .apk files of application under test are stored." 88 | } 89 | ] 90 | } 91 | ] 92 | } 93 | ``` 94 | Overview: 95 | - You can provide both relative and absolute paths. ATS will convert relative paths to absolute paths anyway. 96 | - You can provide many set of paths (for different machines/developers) by adding another object `path_set_list` and giving it unique `set_name`. 97 | 98 | Parameters (explanations in `path_description`): 99 | - **`sdk_dir`** 100 | - **`avd_dir`** 101 | - **`output_dir`** 102 | - **`project_root_dir`** - If you want to allow ATS to build .apk files of your project then you need to provide path to it so ATS can access `./gradlew` file. 103 | - **`apk_dir`** - Directory where ATS will look for .apk files. If it won't find them and `project_root_dir` is properly set then there is a possibility to build them. 104 | 105 | ### Launch Module 106 | File: launchManifest.json 107 | ``` 108 | { 109 | "launch_plan_list": [ 110 | { 111 | "plan_name": "default", 112 | "general": { 113 | "adb_call_buffer_size": 3, 114 | "adb_call_buffer_delay_between_cmd": 2000 115 | }, 116 | "device_preparation_phase": { 117 | "avd_should_recreate_existing": true 118 | }, 119 | "apk_preparation_phase": { 120 | "build_new_apk": true 121 | }, 122 | "device_launching_phase": { 123 | "device_android_id_to_ignore": [], 124 | "avd_launch_sequentially": true, 125 | "avd_status_scan_interval_millis": 10000, 126 | "avd_wait_for_adb_boot_timeout_millis": 240000, 127 | "avd_wait_for_system_boot_timeout_millis": 120000, 128 | "device_before_launching_restart_adb": true 129 | }, 130 | "testing_phase": { 131 | "record_tests": true 132 | } 133 | } 134 | ] 135 | } 136 | ``` 137 | Overview: 138 | - You can create many launch plans by adding another object `launch_plan_list` and giving it unique `plan_name`. 139 | - Check how it's done in `AzimoNote` project -> [link](https://github.com/AzimoLabs/AzimoNote/blob/master/automation/automationTestSupervisorConfig/avdManifest.json) 140 | 141 | Parameters: 142 | - ADB Buffer - as ADB has problem with too frequent calls to it and tends to process information too slow and return errors. Unfortunately it's limitation of ADB and all you can do is to wait for some major improvement/update. So to avoid spamming it, every command to ADB is entering buffer of size **`adb_call_buffer_size`** and then it's released after **`adb_call_buffer_delay_between_cmd`** milliseconds. This will allow you to reduce burden on your ADB in case you used weaker machine or decided to run tons of AVD. 143 | - **`avd_should_recreate_existing`** - Boolean value, if AVD that you have picked for your session already exists on your machine you can either choose to leave them as they are or ask ATS to re-create them (to completely reset memory state of device for example) 144 | - **`build_new_apk`** - If ATS won't find your .apk in directory specified in PathModule it will attempt to build it if you wish for that. In some cases you are debugging your tests and try to run it many times to trigger some non-deterministic action. That's why you don't want to wait for .apk being built from scratch every time if you are not making any changes to your code. If you are 100% sure .apk is already built you can use this flag to create "speed-run" launch plan. 145 | - **`device_android_id_to_ignore`** - Simply enter Android Device Id that should be ignored. 146 | - **`avd_launch_sequentially`** - After you've requested list of AVD to launch you can either wait for their boot in parallel or "one by one". Though non-sequential launch is dangerous for your OS. What's limiting how many AVD can be launched at the same time is HAXM. If you run 10 AVD in exactly same moment (where each takes ~1GB of RAM) - even though limit of your HAXM might be 6GB of RAM - SDK won't stop you from doing that. It will perform 10 checks and each of them will say - there is 6GB of free RAM, go ahead. That way you can launch more AVD - over the limit of your HAXM and in worst case you will crash your OS with out of memory error. 147 | - **`avd_status_scan_interval_millis`** - When AVD is booting and ATS is waiting for it this parameter allows you to set interval (millis) in which ADB will check device state. 148 | - **`avd_wait_for_adb_boot_timeout_millis`** - Timeout for so called `ADB BOOT`. It's time from launching terminal command that should start AVD to moment where ADB will display AVD in `adb list` with status `device`. At that point system of AVD just started booting. 149 | - **`avd_wait_for_system_boot_timeout_millis`** - Wait for system parameters of AVD: `dev.bootcomplete`, `sys.boot_completed`, `init.svc.bootanim`. 150 | - **`device_before_launching_restart_adb`** - You can choose if ADB should be restarted before it starts working with AVD. 151 | - **`record_tests`** - You can choose if every test should be recorded. It will put more burden on your machine. 152 | 153 | ### AVD Module 154 | File: avdManifest.json 155 | ``` 156 | { 157 | "avd_schema_list": [ 158 | { 159 | "avd_name": "", 160 | "create_avd_package": "", 161 | "create_device": "", 162 | "create_avd_tag": "", 163 | "create_avd_abi": "", 164 | "create_avd_additional_options": "", 165 | "create_avd_hardware_config_filepath": "", 166 | "launch_avd_snapshot_filepath": "", 167 | "launch_avd_launch_binary_name": "", 168 | "launch_avd_additional_options": "" 169 | } 170 | ], 171 | "avd_set_list": [ 172 | { 173 | "set_name": "default", 174 | "avd_list": [ 175 | { 176 | "avd_name": "", 177 | "instances": -1 178 | } 179 | ], 180 | "avd_port_rules": { 181 | "assign_missing_ports": true, 182 | "search_range_min": 5556, 183 | "search_range_max": 5580, 184 | "ports_to_ignore": [], 185 | "ports_to_use": [] 186 | } 187 | } 188 | ] 189 | } 190 | ``` 191 | Overview: 192 | - You can create many AVD schemas in `avd_schema_list` which can be then used to create sets in `avd_set_list`. 193 | - Check how it's done in `AzimoNote` project -> [link](https://github.com/AzimoLabs/AzimoNote/blob/master/automation/automationTestSupervisorConfig/avdManifest.json) 194 | 195 | If you were user of our [fastlane-emulator-run-plugin](https://github.com/AzimoLabs/fastlane-plugin-automated-test-emulator-run) than you can notice that this way of created AVD for test session is actually very similar adopted mechanism with some extensions. 196 | 197 | AVD schema parameters: 198 | - **`avd_name`** - Name of your AVD, avoid using spaces, this field is necessary. 199 | - **`create_avd_package`** - Path to system image eg. "system-images;android-23;google_apis;x86_64". 200 | - **`create_device`** - Name of your device visible on `avdmanager list`. 201 | - **`create_avd_tag`** - The sys-img tag to use for the AVD. e.g. if you are using Google Apis then set it to "google_apis". 202 | - **`create_avd_abi`** - Abi for AVD e.g. "x86" or "x86_64" (https://developer.android.com/ndk/guides/abis.html). 203 | - **`create_avd_additional_options`** - If you think that you need something more you can just add your create parameters here (e.g. "--sdcard 128M". 204 | - **`create_avd_hardware_config_filepath`** - Path to config.ini file containing custom config for your AVD. After AVD is created this file will be copied into AVD location before it launches. 205 | - **`launch_avd_snapshot_filepath`** - Plugin might (if you set it) delete and re-create AVD before test start. That means all your permissions and settings will be lost on each emulator run. If you want to apply QEMU image with saved AVD state you can put path to it in this field. It will be applied by using `-wipe-data -initdata` command. 206 | - **`launch_avd_launch_binary_name`** -Ddepending on your CPU architecture you need to choose binary file which should launch your AVD (e.g. "emulator", "emulator64-arm"). 207 | - **`launch_avd_additional_options`** - If you need more customization add your parameters here (e.g. "-gpu on -no-boot-anim -no-window", https://developer.android.com/studio/run/emulator-commandline.html) 208 | 209 | AVD set parameters: 210 | - **`avd_list`** - List of .json objects where names of AVD schemas can be listed and quantity of emulator that should be launched from them. 211 | - **`assign_missing_ports`** - ATS will pick free ports automatically for you if this flag is set to true, otherwise it will use port list provided by you. 212 | - **`search_range_min`** - Value from which ATS starts too look for free ports. Google recommended to use ports 5556 - 5580. 213 | - **`search_range_max`** - Value from which ATS stops too look for free ports. 214 | - **`ports_to_ignore`** - In case you had port reserved for something you can ask ATS to omit it. 215 | - **`ports_to_use`** - Your own list of ports to use. Remember to pick only even ports as uneven ones are reserved for ADB instances cooperating with AVD instance (e.g. if you pick 5554 then 5555 will be used by ADB). 216 | 217 | ### Test Module 218 | File testManifest.json: 219 | ``` 220 | { 221 | "test_list": [ 222 | { 223 | "test_package_name": "", 224 | "test_packages": [], 225 | "test_classes": [], 226 | "test_cases": [] 227 | } 228 | ], 229 | "test_set_list": [ 230 | { 231 | "set_name": "default", 232 | "apk_name_part": "", 233 | "application_apk_assemble_task": "", 234 | "test_apk_assemble_task": "", 235 | "gradle_build_params": "", 236 | "shard": false, 237 | "set_package_names": [] 238 | } 239 | ] 240 | } 241 | ``` 242 | Overview: 243 | - You HAVE TO create list of your test packages in `test_list`. Package is a java-package to folder where .java files with @Test annotated code. 244 | - Check how it's done in `AzimoNote` project -> [link](https://github.com/AzimoLabs/AzimoNote/blob/master/automation/automationTestSupervisorConfig/testManifest.json) 245 | 246 | Example on our Azimo project: 247 | 248 | 249 | 250 | This is a package with 4 test containers: 251 | - SearchContact_AllContacts_Tests 252 | - SearchContact_AzimoInstant_Tests 253 | - SearchContact_MyRecipients_Tests 254 | - SearchContact_NewUser_Tests 255 | 256 | Which are inside package `com.azimo.sendmoney.instrumentation.azimoTestCases.bddTests.functional.SearchContact`. Example test package object could look like this (you pick `test_package_name` to your liking): 257 | ``` 258 | { 259 | "test_package_name": "SearchContactTests", 260 | "test_packages": [ 261 | "com.azimo.sendmoney.instrumentation.azimoTestCases.bddTests.functional.SearchContact" 262 | ], 263 | "test_classes": [], 264 | "test_cases": [] 265 | } 266 | ``` 267 | 268 | #### Important: 269 | Parameters `test_classes` and `test_cases` are not supported for now. 270 | 271 | Test set parameters: 272 | - **`apk_name_part`** - Part of .apk name. ATS will look for .apk files containing this name in specified in Path Module directory. From all .apk files that contains this "name part" it will pick those which has highest version code and both application and test .apk files exists. 273 | - **`application_apk_assemble_task`** - Gradle task for building application .apk. You can create ATS sets for different flavours. 274 | - **`test_apk_assemble_task`** - Gradle task for building test .apk corresponding to .apk in `application_apk_assemble_task`. 275 | - **`gradle_build_params`** - Gradle parameters which you can add to .apk build process. 276 | - **`shard`** - Boolean value. Based on this flag you can decide if tests will run in parallel on each device or will be divided into parts (called shards) where each device will receive only part of tests. 277 | - **`set_package_names`** - If you have finished filling up your `test_list` then you have many test package objects here, where each object has `test_package_name`. **`set_package_names`** is a list for those `test_package_name` values. 278 | 279 | ### More AutomationTestSupervisor features 280 | #### Logging 281 |

282 | 283 | 284 |

285 | a) HTML dashboard generation - ATS provides you with fully generated HTML website structure where you can preview your test session. It contains: 286 | - General test health rate. 287 | - List of failed tests. 288 | - List of module sets/plan you have used in this session. 289 | - Used .apk name, version, build time. 290 | - Device creation, boot, time, .apk install time. 291 | - Expandable list of test packages with test cases and their status. 292 | - Highlighted error of test. 293 | - Test duration. 294 | - Device on which test ran. 295 | - Link to LogCat from test session (another generated and pre-formatted HTML page). 296 | - Link to videos from test (if feature was used). 297 | 298 | b) Logs from tests in form of JSON files. 299 | 300 | c) Logs from emulators. 301 | 302 | #### Boosted sharding 303 | - TODO: Write description 304 | 305 | #### Secure .apk install 306 | - TODO: Write description 307 | 308 | #### .apk data scrapping 309 | - TODO: Write description 310 | 311 | #### AVD separated session 312 | - TODO: Write description 313 | 314 | ### Possible new features 315 | a) AutomationTestSupervisor 316 | - Support for ADB Orchistrator (HOT). 317 | - Possibility to re-run failed tests to flag flakiness. 318 | - 3x retry with small time interval on ADB commands as it tends to malfunctions (rarely but still). 319 | - Screenshots - This function was not implemented as it cannot be done from Python code. We could take random screenshots from terminal but best performance is when user can specify in test when screenshot should be taken. It requires some universal java class for taking screenshots and additionally supports for real-time screenshot pulling from devices. In general it's much work to do it properly. 320 | - Add list parameter to display currently available manifest configs. 321 | 322 | b) LauncherWizard - subtool 323 | - Wizard version of launcher that leads person which doesn't know how to start test with usage of single command. Wizard will show possible options and react to user answers. 324 | 325 | c) LauncherConfigCreator - subtool 326 | - Tool that will ask user to fill all necessary fields by question/answer format. After process is finished user will receive manifest file configs compatible with his machine. 327 | 328 | ### Problems to be solved 329 | a) Test recording is working but the way files are pulled from device is very imperfect. This is due to fact, even if you stop recording from terminal command ADB still takes few seconds to process file. If you pull video too fast then it will be corrupted and won't run. This is simply bug in ADB as it doesn't release video recording process in the moment when video is ready to use. We tested this feature on our test set (where some tests take even 6 minutes) and decided to wait 10 seconds before each file is pulled. This will cause huge problem if someone has set of very small and fast tests. What needs to be done to **implement proper synchronisation/wait for .mp4 to finish being written to** in [TestRecordingSavingThread](https://github.com/AzimoLabs/AutomationTestSupervisor/blob/master/session/SessionThreads.py). 330 | 331 | 332 | # Towards financial services available to all 333 | We’re working throughout the company to create faster, cheaper, and more available financial services all over the world, and here are some of the techniques that we’re utilizing. There’s still a long way ahead of us, and if you’d like to be part of that journey, check out our [careers page](https://bit.ly/3vajnu6). 334 | -------------------------------------------------------------------------------- /config_files_dir.json: -------------------------------------------------------------------------------- 1 | { 2 | "path_manifest_path": "../automationTestSupervisorConfig/pathManifest.json", 3 | "launch_manifest_path": "../automationTestSupervisorConfig/launchManifest.json", 4 | "test_manifest_path": "../automationTestSupervisorConfig/testManifest.json", 5 | "avd_manifest_path": "../automationTestSupervisorConfig/avdManifest.json" 6 | } -------------------------------------------------------------------------------- /error/Exceptions.py: -------------------------------------------------------------------------------- 1 | class LauncherFlowInterruptedException(Exception): 2 | def __init__(self, tag, message): 3 | super(Exception, self).__init__(message) 4 | self.caller_tag = tag 5 | -------------------------------------------------------------------------------- /git_images/html_dashboard_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzimoLabs/AutomationTestSupervisor/511be027254fd9c250f941b758be97b109c6ed78/git_images/html_dashboard_example1.png -------------------------------------------------------------------------------- /git_images/html_dashboard_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzimoLabs/AutomationTestSupervisor/511be027254fd9c250f941b758be97b109c6ed78/git_images/html_dashboard_example2.png -------------------------------------------------------------------------------- /git_images/test_package_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzimoLabs/AutomationTestSupervisor/511be027254fd9c250f941b758be97b109c6ed78/git_images/test_package_example.png -------------------------------------------------------------------------------- /log_generator/GeneratorModels.py: -------------------------------------------------------------------------------- 1 | class LogPackage: 2 | def __init__(self): 3 | self.name = None 4 | self.logs = None 5 | 6 | 7 | class Log: 8 | def __init__(self): 9 | self.name = None 10 | self.package_name = None 11 | self.device = None 12 | self.duration = None 13 | self.status = None 14 | self.rerun_count = None 15 | self.error = None 16 | self.error_type = None 17 | self.logcat = None 18 | self.recording = None 19 | -------------------------------------------------------------------------------- /log_generator/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AzimoLabs/AutomationTestSupervisor/511be027254fd9c250f941b758be97b109c6ed78/log_generator/__init__.py -------------------------------------------------------------------------------- /log_generator/styles/logcat.css: -------------------------------------------------------------------------------- 1 | .logcat-header { 2 | width: 100%; 3 | 4 | background-color: #0e2b59; 5 | color: #ffffff; 6 | 7 | font-family: "Verdana", Geneva, sans-serif; 8 | font-size: 14px; 9 | text-align: center; 10 | 11 | padding: 8px; 12 | } 13 | 14 | .logcat-table { 15 | width: 100%; 16 | } 17 | 18 | .logcat-debug-row { 19 | display: table-row; 20 | 21 | background-color: #ffffff; 22 | } 23 | 24 | .logcat-info-row { 25 | display: table-row; 26 | 27 | background-color: #d3f2ff; 28 | } 29 | 30 | .logcat-verbose-row { 31 | display: table-row; 32 | 33 | background-color: #ffffff; 34 | } 35 | 36 | .logcat-warning-row { 37 | display: table-row; 38 | 39 | background-color: #fff7d3; 40 | } 41 | 42 | .logcat-error-row { 43 | display: table-row; 44 | 45 | background-color: #ffd3d3; 46 | } 47 | 48 | .cell { 49 | display: table-cell; 50 | 51 | font-family: "Verdana", Geneva, sans-serif; 52 | font-weight: normal; 53 | font-size: 12px; 54 | 55 | padding: 3px 6px; 56 | } 57 | .date-cell { 58 | min-width: 45px; 59 | } 60 | 61 | .message-cell { 62 | text-align: left; 63 | 64 | word-wrap: break-word; 65 | word-break: break-all; 66 | 67 | padding-left: 12px; 68 | } 69 | 70 | .centered-cell { 71 | text-align: center 72 | } -------------------------------------------------------------------------------- /log_generator/styles/summary.css: -------------------------------------------------------------------------------- 1 | .summary-wrapper { 2 | width: 76%; 3 | min-width: 800px; 4 | 5 | margin: auto; 6 | padding: 10px; 7 | } 8 | 9 | .summary-header { 10 | width: 100%; 11 | 12 | background-color: #0e2b59; 13 | color: #ffffff; 14 | 15 | font-family: Verdana, Geneva, sans-serif; 16 | font-size: 18px; 17 | text-align: center; 18 | 19 | padding: 8px 0; 20 | } 21 | 22 | .summary-table { 23 | width: 100%; 24 | 25 | display: table; 26 | 27 | background-color: #f7faff; 28 | 29 | padding: 3px 0 0 0; 30 | } 31 | 32 | .summary-subtable { 33 | width: 100%; 34 | 35 | display: table; 36 | } 37 | 38 | .summary-table-row { 39 | width: 50%; 40 | 41 | display: table-row; 42 | } 43 | 44 | .summary-table-cell-separator { 45 | width: 100%; 46 | height: 3px; 47 | 48 | display: table-caption; 49 | 50 | background-color: #f7faff; 51 | } 52 | 53 | .summary-table-cell-subseparator { 54 | width: 100%; 55 | height: 1px; 56 | 57 | display: table-caption; 58 | 59 | background-color: #f7faff; 60 | } 61 | 62 | .summary-table-results-header { 63 | width: 100%; 64 | 65 | display: table-caption; 66 | 67 | background-color: #cbef7a; 68 | 69 | font-family: Verdana, Geneva, sans-serif; 70 | font-weight: bold; 71 | font-size: 16px; 72 | text-align: center; 73 | 74 | padding: 6px 0; 75 | } 76 | 77 | .summary-table-results-cell { 78 | width: 100%; 79 | height: 100px; 80 | 81 | display: table-caption; 82 | 83 | background-color: #ebffd3; 84 | color: #182301; 85 | 86 | font-family: Verdana, Geneva, sans-serif; 87 | font-weight: bold; 88 | font-size: 400%; 89 | text-align: center; 90 | vertical-align: middle; 91 | 92 | line-height: 100px; 93 | } 94 | 95 | .summary-table-results-passed-cell-left { 96 | width: 60%; 97 | 98 | display: table-cell; 99 | 100 | background-color: #dfffaa; 101 | 102 | font-family: Verdana, Geneva, sans-serif; 103 | font-size: 12px; 104 | text-align: left; 105 | 106 | padding: 6px 6px 6px 18px; 107 | } 108 | 109 | .summary-table-results-passed-cell-right { 110 | width: 40%; 111 | 112 | display: table-cell; 113 | 114 | background-color: #d6ff96; 115 | 116 | font-family: Verdana, Geneva, sans-serif; 117 | font-size: 12px; 118 | text-align: center; 119 | 120 | padding: 6px 6px 6px 18px; 121 | } 122 | 123 | .summary-table-results-failed-cell-left { 124 | width: 60%; 125 | 126 | display: table-cell; 127 | 128 | background-color: #ffafaf; 129 | 130 | font-family: Verdana, Geneva, sans-serif; 131 | font-size: 12px; 132 | text-align: left; 133 | 134 | padding: 6px 6px 6px 18px; 135 | } 136 | 137 | .summary-table-results-failed-cell-right { 138 | width: 40%; 139 | 140 | display: table-cell; 141 | 142 | background-color: #ff9696; 143 | 144 | font-family: Verdana, Geneva, sans-serif; 145 | font-size: 12px; 146 | text-align: center; 147 | 148 | padding: 6px 6px 6px 18px; 149 | } 150 | 151 | .summary-table-general-header { 152 | width: 100%; 153 | 154 | display: table-caption; 155 | 156 | background-color: #7aa7ef; 157 | 158 | font-family: Verdana, Geneva, sans-serif; 159 | font-weight: bold; 160 | font-size: 16px; 161 | text-align: center; 162 | 163 | padding: 6px 0; 164 | } 165 | 166 | .summary-table-general-cell-left { 167 | width: 60%; 168 | 169 | display: table-cell; 170 | 171 | background-color: #d1e2ff; 172 | 173 | font-family: Verdana, Geneva, sans-serif; 174 | font-size: 12px; 175 | text-align: left; 176 | 177 | padding: 6px 6px 6px 18px; 178 | } 179 | 180 | .summary-table-general-cell-right { 181 | width: 40%; 182 | 183 | display: table-cell; 184 | 185 | background-color: #c9daff; 186 | 187 | font-family: Verdana, Geneva, sans-serif; 188 | font-size: 12px; 189 | text-align: center; 190 | 191 | padding: 6px 6px 6px 18px; 192 | } 193 | 194 | .summary-table-apk-header { 195 | width: 100%; 196 | 197 | display: table-caption; 198 | 199 | background-color: #bc7aef; 200 | 201 | font-family: Verdana, Geneva, sans-serif; 202 | font-weight: bold; 203 | font-size: 16px; 204 | text-align: center; 205 | 206 | padding: 6px 0; 207 | } 208 | 209 | .summary-table-apk-subheader { 210 | width: 100%; 211 | 212 | display: table-caption; 213 | 214 | background-color: #d8a5ff; 215 | 216 | font-family: Verdana, Geneva, sans-serif; 217 | font-weight: bold; 218 | text-align: center; 219 | font-size: 14px; 220 | 221 | padding: 6px 0; 222 | } 223 | 224 | .summary-table-apk-cell-left { 225 | width: 60%; 226 | 227 | display: table-cell; 228 | 229 | background-color: #e8ccff; 230 | font-family: Verdana, Geneva, sans-serif; 231 | font-size: 12px; 232 | text-align: left; 233 | 234 | padding: 3px 3px 3px 15px; 235 | } 236 | 237 | .summary-table-apk-cell-right { 238 | width: 40%; 239 | 240 | display: table-cell; 241 | 242 | background-color: #e5c6ff; 243 | 244 | font-family: Verdana, Geneva, sans-serif; 245 | font-size: 12px; 246 | text-align: center; 247 | 248 | padding: 3px 3px 3px 15px; 249 | } 250 | 251 | .summary-table-devices-header { 252 | width: 100%; 253 | 254 | display: table-caption; 255 | 256 | background-color: #efe37a; 257 | 258 | font-family: Verdana, Geneva, sans-serif; 259 | font-weight: bold; 260 | font-size: 16px; 261 | text-align: center; 262 | 263 | padding: 6px 0; 264 | } 265 | 266 | .summary-table-devices-subheader { 267 | width: 100%; 268 | 269 | display: table-caption; 270 | 271 | background-color: #fff493; 272 | 273 | font-family: Verdana, Geneva, sans-serif; 274 | font-weight: bold; 275 | text-align: center; 276 | font-size: 14px; 277 | 278 | padding: 6px 0; 279 | } 280 | 281 | .summary-table-devices-cell-left { 282 | width: 60%; 283 | 284 | display: table-cell; 285 | 286 | background-color: #fff9c4; 287 | font-family: Verdana, Geneva, sans-serif; 288 | font-size: 12px; 289 | text-align: left; 290 | 291 | padding: 3px 3px 3px 15px; 292 | } 293 | 294 | .summary-table-devices-cell-right { 295 | width: 40%; 296 | 297 | display: table-cell; 298 | 299 | background-color: #fff5af; 300 | 301 | font-family: Verdana, Geneva, sans-serif; 302 | font-size: 12px; 303 | text-align: center; 304 | 305 | padding: 3px 3px 3px 15px; 306 | } 307 | 308 | .test-package-wrapper { 309 | } 310 | 311 | .package-header { 312 | width: 100%; 313 | 314 | background-color: #0e2b59; 315 | color: #ffffff; 316 | 317 | font-family: Verdana, Geneva, sans-serif; 318 | font-size: 12px; 319 | text-align: center; 320 | 321 | padding: 8px 0; 322 | } 323 | 324 | .passed-test-table { 325 | width: 100%; 326 | 327 | display: table; 328 | 329 | background-color: #ebffd3; 330 | 331 | padding: 5px 0; 332 | } 333 | 334 | .passed-test-header { 335 | width: 100%; 336 | 337 | display: table-caption; 338 | 339 | background-color: #b2e278; 340 | 341 | font-family: Verdana, Geneva, sans-serif; 342 | font-size: 12px; 343 | text-align: center; 344 | 345 | padding: 6px 0; 346 | } 347 | 348 | .passed-test-row { 349 | width: 100% 350 | ; 351 | display: table-row; 352 | } 353 | 354 | .passed-test-cell { 355 | width: 25%; 356 | 357 | display: table-cell; 358 | 359 | font-family: Verdana, Geneva, sans-serif; 360 | font-size: 12px; 361 | text-align: center; 362 | 363 | padding: 6px; 364 | } 365 | 366 | .failed-table-wrapper { 367 | width: 100%; 368 | 369 | background-color: #ffd3d3; 370 | } 371 | 372 | 373 | .failed-test-table { 374 | width: 100%; 375 | 376 | display: table; 377 | 378 | background-color: #ffd3d3; 379 | 380 | padding: 5px 0; 381 | } 382 | 383 | .failed-test-header { 384 | width: 100%; 385 | 386 | display: table-caption; 387 | 388 | background-color: #e27777; 389 | 390 | font-family: Verdana, Geneva, sans-serif; 391 | font-size: 12px; 392 | text-align: center; 393 | 394 | padding: 6px 0; 395 | } 396 | 397 | .failed-test-row { 398 | width: 100%; 399 | 400 | display: table-row; 401 | } 402 | 403 | .failed-test-cell { 404 | width: 25%; 405 | 406 | display: table-cell; 407 | 408 | font-family: Verdana, Geneva, sans-serif; 409 | font-size: 12px; 410 | text-align: center; 411 | 412 | padding: 6px; 413 | } 414 | 415 | .test-case-separator { 416 | width: 100%; 417 | height: 3px; 418 | 419 | display: table-caption; 420 | 421 | background-color: #f7faff; 422 | } 423 | 424 | .failed-test-error-table { 425 | width: 98%; 426 | 427 | display: table; 428 | 429 | margin: auto; 430 | padding: 6px; 431 | } 432 | 433 | .failed-test-error-dark { 434 | width: 100%; 435 | 436 | display: table-cell; 437 | 438 | background-color: #f2b5b5; 439 | 440 | font-family: Verdana, Geneva, sans-serif; 441 | font-size: 11px; 442 | text-align: left; 443 | 444 | white-space: pre-wrap; /* CSS3 */ 445 | white-space: -moz-pre-wrap; /* Firefox */ 446 | white-space: -pre-wrap; /* Opera <7 */ 447 | white-space: -o-pre-wrap; /* Opera 7 */ 448 | word-wrap: break-word; /* IE */ 449 | word-break: break-all; 450 | 451 | padding: 3px; 452 | } 453 | 454 | .failed-test-error-light { 455 | width: 100%; 456 | 457 | display: table-cell; 458 | 459 | background-color: #f2bfbf; 460 | 461 | font-family: Verdana, Geneva, sans-serif; 462 | font-size: 11px; 463 | text-align: left; 464 | 465 | white-space: pre-wrap; /* CSS3 */ 466 | white-space: -moz-pre-wrap; /* Firefox */ 467 | white-space: -pre-wrap; /* Opera <7 */ 468 | white-space: -o-pre-wrap; /* Opera 7 */ 469 | word-wrap: break-word; /* IE */ 470 | word-break: break-all; 471 | 472 | padding: 3px; 473 | } 474 | 475 | .summary-failed-test-list-table { 476 | width: 100%; 477 | 478 | background-color: #ffd3d3; 479 | } 480 | 481 | .summary-failed-test-list-subtable { 482 | width: 100%; 483 | 484 | display: table; 485 | 486 | background-color: #ffd3d3; 487 | 488 | padding: 0 0 0 0; 489 | } 490 | 491 | .summary-failed-test-row { 492 | width: 100%; 493 | 494 | display: table-row; 495 | } 496 | 497 | .failed-test-list-light { 498 | width: 100%; 499 | 500 | display: table-cell; 501 | 502 | background-color: #f2bfbf; 503 | 504 | font-family: Verdana, Geneva, sans-serif; 505 | font-size: 11px; 506 | text-align: left; 507 | 508 | white-space: pre-wrap; /* CSS3 */ 509 | white-space: -moz-pre-wrap; /* Firefox */ 510 | white-space: -pre-wrap; /* Opera <7 */ 511 | white-space: -o-pre-wrap; /* Opera 7 */ 512 | word-wrap: break-word; /* IE */ 513 | word-break: break-all; 514 | 515 | padding: 3px 0px 3px 18px; 516 | } 517 | 518 | .failed-test-list-dark { 519 | width: 100%; 520 | 521 | display: table-cell; 522 | 523 | background-color: #f2b5b5; 524 | 525 | font-family: Verdana, Geneva, sans-serif; 526 | font-size: 11px; 527 | text-align: left; 528 | 529 | white-space: pre-wrap; /* CSS3 */ 530 | white-space: -moz-pre-wrap; /* Firefox */ 531 | white-space: -pre-wrap; /* Opera <7 */ 532 | white-space: -o-pre-wrap; /* Opera 7 */ 533 | word-wrap: break-word; /* IE */ 534 | word-break: break-all; 535 | 536 | padding: 3px 0px 3px 18px; 537 | } 538 | 539 | .link { 540 | text-decoration: none; 541 | } 542 | 543 | .rerun-table-wrapper { 544 | width: 100%; 545 | 546 | background-color: #eafeff; 547 | } 548 | 549 | .rerun-test-table { 550 | width: 100%; 551 | 552 | display: table; 553 | 554 | background-color: #d3fff9; 555 | 556 | padding: 5px 0; 557 | } 558 | 559 | .rerun-test-header { 560 | width: 100%; 561 | 562 | display: table-caption; 563 | 564 | background-color: #77dce2; 565 | 566 | font-family: Verdana, Geneva, sans-serif; 567 | font-size: 12px; 568 | text-align: center; 569 | 570 | padding: 6px 0; 571 | } 572 | 573 | .rerun-test-row { 574 | width: 100%; 575 | 576 | display: table-row; 577 | } 578 | 579 | .rerun-test-cell { 580 | width: 25%; 581 | 582 | display: table-cell; 583 | 584 | font-family: Verdana, Geneva, sans-serif; 585 | font-size: 12px; 586 | text-align: center; 587 | 588 | padding: 6px; 589 | } 590 | 591 | .rerun-test-error-table { 592 | width: 98%; 593 | 594 | display: table; 595 | 596 | margin: auto; 597 | padding: 6px; 598 | } 599 | 600 | .rerun-test-error-dark { 601 | width: 100%; 602 | 603 | display: table-cell; 604 | 605 | background-color: #baecef; 606 | 607 | font-family: Verdana, Geneva, sans-serif; 608 | font-size: 11px; 609 | text-align: left; 610 | 611 | white-space: pre-wrap; /* CSS3 */ 612 | white-space: -moz-pre-wrap; /* Firefox */ 613 | white-space: -pre-wrap; /* Opera <7 */ 614 | white-space: -o-pre-wrap; /* Opera 7 */ 615 | word-wrap: break-word; /* IE */ 616 | word-break: break-all; 617 | 618 | padding: 3px; 619 | } 620 | 621 | .rerun-test-error-light { 622 | width: 100%; 623 | 624 | display: table-cell; 625 | 626 | background-color: ##d4f5f7; 627 | 628 | font-family: Verdana, Geneva, sans-serif; 629 | font-size: 11px; 630 | text-align: left; 631 | 632 | white-space: pre-wrap; /* CSS3 */ 633 | white-space: -moz-pre-wrap; /* Firefox */ 634 | white-space: -pre-wrap; /* Opera <7 */ 635 | white-space: -o-pre-wrap; /* Opera 7 */ 636 | word-wrap: break-word; /* IE */ 637 | word-break: break-all; 638 | 639 | padding: 3px; 640 | } 641 | 642 | -------------------------------------------------------------------------------- /log_generator/utils/HtmlLogcatUtils.py: -------------------------------------------------------------------------------- 1 | def add_logcat_header(text): 2 | return "
{}
".format(text) 3 | 4 | 5 | def start_logcat_table(): 6 | return "
" 7 | 8 | 9 | def add_debug_row(date, time, tag, level, message): 10 | row_content = "" 11 | row_content += "
" 12 | row_content += "
{}
".format(date) 13 | row_content += "
{}
".format(time) 14 | row_content += "
{}
".format(tag) 15 | row_content += "
{}
".format(level) 16 | row_content += "
{}
".format(message) 17 | row_content += "
" 18 | return row_content 19 | 20 | 21 | def add_verbose_row(date, time, tag, level, message): 22 | row_content = "" 23 | row_content += "
" 24 | row_content += "
{}
".format(date) 25 | row_content += "
{}
".format(time) 26 | row_content += "
{}
".format(tag) 27 | row_content += "
{}
".format(level) 28 | row_content += "
{}
".format(message) 29 | row_content += "
" 30 | return row_content 31 | 32 | 33 | def add_info_row(date, time, tag, level, message): 34 | row_content = "" 35 | row_content += "
" 36 | row_content += "
{}
".format(date) 37 | row_content += "
{}
".format(time) 38 | row_content += "
{}
".format(tag) 39 | row_content += "
{}
".format(level) 40 | row_content += "
{}
".format(message) 41 | row_content += "
" 42 | return row_content 43 | 44 | 45 | def add_warning_row(date, time, tag, level, message): 46 | row_content = "" 47 | row_content += "
" 48 | row_content += "
{}
".format(date) 49 | row_content += "
{}
".format(time) 50 | row_content += "
{}
".format(tag) 51 | row_content += "
{}
".format(level) 52 | row_content += "
{}
".format(message) 53 | row_content += "
" 54 | return row_content 55 | 56 | 57 | def add_error_row(date, time, tag, level, message): 58 | row_content = "" 59 | row_content += "
" 60 | row_content += "
{}
".format(date) 61 | row_content += "
{}
".format(time) 62 | row_content += "
{}
".format(tag) 63 | row_content += "
{}
".format(level) 64 | row_content += "
{}
".format(message) 65 | row_content += "
" 66 | return row_content 67 | 68 | 69 | def end_table(): 70 | return "
" 71 | -------------------------------------------------------------------------------- /log_generator/utils/HtmlResultsUtils.py: -------------------------------------------------------------------------------- 1 | def start_test_package_wrapper(toggleable_id): 2 | return "
".format(toggleable_id) 3 | 4 | 5 | def add_package_header(text): 6 | return "
{}
".format(text) 7 | 8 | 9 | def add_test_case_separator(): 10 | return "
" 11 | 12 | 13 | def start_passed_test_table(): 14 | return "
" 15 | 16 | 17 | def add_passed_test_header(text): 18 | return "
{}
".format(text) 19 | 20 | 21 | def start_passed_test_row(): 22 | return "
" 23 | 24 | 25 | def add_passed_test_cell(text): 26 | return "
{}
".format(text) 27 | 28 | 29 | def start_failed_table_wrapper(): 30 | return "
" 31 | 32 | 33 | def start_failed_test_table(): 34 | return "
" 35 | 36 | 37 | def start_failed_test_error_table(): 38 | return "
" 39 | 40 | 41 | def add_failed_test_header(text): 42 | return "
{}
".format(text) 43 | 44 | 45 | def start_failed_test_row(): 46 | return "
" 47 | 48 | 49 | def add_failed_test_cell(text): 50 | return "
{}
".format(text) 51 | 52 | 53 | def add_error_cell_light(text): 54 | return "
{}
".format(text) 55 | 56 | 57 | def add_error_cell_dark(text): 58 | return "
{}
".format(text) 59 | 60 | 61 | def start_rerun_table_wrapper(): 62 | return "
" 63 | 64 | 65 | def start_rerun_test_table(): 66 | return "
" 67 | 68 | 69 | def start_rerun_test_error_table(): 70 | return "
" 71 | 72 | 73 | def add_rerun_test_header(text): 74 | return "
{}
".format(text) 75 | 76 | 77 | def start_rerun_test_row(): 78 | return "
" 79 | 80 | 81 | def add_rerun_test_cell(text): 82 | return "
{}
".format(text) 83 | 84 | 85 | def add_rerun_error_cell_light(text): 86 | return "
{}
".format(text) 87 | 88 | 89 | def add_rerun_error_cell_dark(text): 90 | return "
{}
".format(text) 91 | 92 | 93 | def end_wrapper(): 94 | return "
" 95 | 96 | 97 | def end_row(): 98 | return "
" 99 | 100 | 101 | def end_table(): 102 | return "
" 103 | -------------------------------------------------------------------------------- /log_generator/utils/HtmlSummaryUtils.py: -------------------------------------------------------------------------------- 1 | def start_wrapper(): 2 | return "
" 3 | 4 | 5 | def add_summary_header(text): 6 | return "
{}
".format(text) 7 | 8 | 9 | def start_summary_table(): 10 | return "
" 11 | 12 | 13 | def start_summary_subtable(): 14 | return "
" 15 | 16 | 17 | def start_summary_table_row(): 18 | return "
" 19 | 20 | 21 | def add_cell_separator(): 22 | return "
" 23 | 24 | 25 | def add_cell_subseparator(): 26 | return "
" 27 | 28 | 29 | def add_summary_table_results_header(text): 30 | return "
{}
".format(text) 31 | 32 | 33 | def add_summary_table_results_cell(text): 34 | return "
{}
".format(text) 35 | 36 | 37 | def add_summary_table_results_passed_left(text): 38 | return "
{}
".format(text) 39 | 40 | 41 | def add_summary_table_results_passed_right(text): 42 | return "
{}
".format(text) 43 | 44 | 45 | def add_summary_table_results_failed_left(text): 46 | return "
{}
".format(text) 47 | 48 | 49 | def add_summary_table_results_failed_right(text): 50 | return "
{}
".format(text) 51 | 52 | 53 | def add_summary_table_general_header(text): 54 | return "
{}
".format(text) 55 | 56 | 57 | def add_summary_table_general_cell_left(text): 58 | return "
{}
".format(text) 59 | 60 | 61 | def add_summary_table_general_cell_right(text): 62 | return "
{}
".format(text) 63 | 64 | 65 | def add_summary_table_apk_header(text): 66 | return "
{}
".format(text) 67 | 68 | 69 | def add_summary_table_apk_subheader(text): 70 | return "
{}
".format(text) 71 | 72 | 73 | def add_summary_table_apk_cell_left(text): 74 | return "
{}
".format(text) 75 | 76 | 77 | def add_summary_table_apk_cell_right(text): 78 | return "
{}
".format(text) 79 | 80 | 81 | def add_summary_table_devices_header(text): 82 | return "
{}
".format(text) 83 | 84 | 85 | def add_summary_table_devices_subheader(text): 86 | return "
{}
".format(text) 87 | 88 | 89 | def add_summary_table_devices_cell_left(text): 90 | return "
{}
".format(text) 91 | 92 | 93 | def add_summary_table_devices_cell_right(text): 94 | return "
{}
".format(text) 95 | 96 | 97 | def start_summary_failed_test_list_table(): 98 | return "
" 99 | 100 | 101 | def start_summary_failed_test_list_subtable(): 102 | return "
" 103 | 104 | 105 | def start_summary_failed_test_row(): 106 | return "
" 107 | 108 | 109 | def add_summary_failed_test_row_cell_light(text): 110 | return "
{}
".format(text) 111 | 112 | 113 | def add_summary_failed_test_row_cell_dark(text): 114 | return "
{}
".format(text) 115 | 116 | 117 | def end_row(): 118 | return "
" 119 | 120 | 121 | def end_table(): 122 | return "
" 123 | 124 | 125 | def end_wrapper(): 126 | return "
" 127 | -------------------------------------------------------------------------------- /log_generator/utils/HtmlUtils.py: -------------------------------------------------------------------------------- 1 | def start_html(): 2 | return "" 3 | 4 | 5 | def end_html(): 6 | return "" 7 | 8 | 9 | def start_head(): 10 | return "" 11 | 12 | 13 | def end_head(): 14 | return "" 15 | 16 | 17 | def link_css(css_dir): 18 | return "".format(css_dir) 19 | 20 | 21 | def start_body(): 22 | return "" 23 | 24 | 25 | def end_body(): 26 | return "" 27 | 28 | 29 | def create_link_to_file(link_destination, text): 30 | return "{}".format(link_destination, text) 31 | 32 | 33 | def start_script(): 34 | return "" 39 | 40 | 41 | def add_toggle_visibility_function_for_package_with_fails(): 42 | return """ 43 | function toggle_visibility_for_package_with_fails(id) { 44 | var e = document.getElementById(id); 45 | 46 | if (e.style.display === 'none') 47 | e.style.display = 'block'; 48 | else 49 | e.style.display = 'none'; 50 | } 51 | """ 52 | 53 | 54 | def add_toggle_visibility_function_for_clean_package(): 55 | return """ 56 | function toggle_visibility_for_clean_package(id) { 57 | var e = document.getElementById(id); 58 | 59 | if (e.style.display === 'block') 60 | e.style.display = 'none'; 61 | else 62 | e.style.display = 'block'; 63 | } 64 | """ 65 | 66 | 67 | def wrap_in_toggle_visibility_on_click_for_package_with_fails(object_to_be_wrapped, click_id): 68 | return ("{}").format(click_id, object_to_be_wrapped) 70 | 71 | 72 | def wrap_in_toggle_visibility_on_click_for_clean_package(object_to_be_wrapped, click_id): 73 | return ("{" 74 | + "}").format(click_id, object_to_be_wrapped) 75 | 76 | -------------------------------------------------------------------------------- /session/SessionDataStores.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import re 3 | import os 4 | 5 | from settings import GlobalConfig 6 | 7 | from session.SessionModels import ( 8 | OutsideSessionVirtualDevice, 9 | OutsideSessionDevice, 10 | SessionVirtualDevice, 11 | ApkCandidate, 12 | ) 13 | 14 | from system.file import FileUtils 15 | from system.port import PortManager 16 | from system.console import ( 17 | Printer, 18 | Color 19 | ) 20 | 21 | 22 | class ApkStore: 23 | TAG = "ApkStore:" 24 | 25 | def __init__(self, aapt_controller): 26 | self.aapt_controller = aapt_controller 27 | self.apk_candidates = list() 28 | self.usable_apk_candidate = None 29 | self._create_apk_dir_if_not_exists() 30 | 31 | def _create_apk_dir_if_not_exists(self): 32 | if FileUtils.dir_exists(GlobalConfig.APK_DIR): 33 | Printer.system_message(self.TAG, "Directory " + Color.GREEN + GlobalConfig.APK_DIR + Color.BLUE 34 | + " was found.") 35 | else: 36 | Printer.system_message(self.TAG, "Directory " + Color.GREEN + GlobalConfig.APK_DIR + Color.BLUE 37 | + " not found. Creating...") 38 | FileUtils.create_dir(GlobalConfig.APK_DIR) 39 | 40 | def provide_apk(self, test_set): 41 | self._find_candidates(test_set) 42 | self.usable_apk_candidate = self._get_usable_apk_candidate_for_latest_version() 43 | return self.usable_apk_candidate 44 | 45 | def _find_candidates(self, test_set): 46 | name_part = test_set.apk_name_part.replace(".apk", "") 47 | Printer.system_message(self.TAG, 48 | "Checking " + Color.GREEN + GlobalConfig.APK_DIR + Color.BLUE + " directory for .*apk" + 49 | " list with names containing " + Color.GREEN + name_part + Color.BLUE + ":") 50 | 51 | app_apk_list = self.get_list_with_application_apk(name_part, GlobalConfig.APK_DIR) 52 | test_apk_list = self.get_list_with_test_apk(name_part, GlobalConfig.APK_DIR) 53 | 54 | path = 0 55 | name = 1 56 | if app_apk_list: 57 | for apk in app_apk_list: 58 | apk_filename = apk[name] 59 | apk_filepath = FileUtils.clean_folder_only_dir( 60 | FileUtils.add_ending_slash(apk[path])) + apk[name] 61 | 62 | apk_test_filename = "" 63 | apk_test_filepath = "" 64 | for test_apk in test_apk_list: 65 | if apk_filename.replace(".apk", "") in test_apk[name] and "-androidTest" in test_apk[name]: 66 | apk_test_filename = test_apk[name] 67 | apk_test_filepath = FileUtils.clean_folder_only_dir( 68 | FileUtils.add_ending_slash(test_apk[path])) + test_apk[name] 69 | 70 | dump = self.aapt_controller.dump_badging(apk_filepath) 71 | version_code = re.findall("versionCode='(.+?)'", dump) 72 | version_code = int(version_code[0]) 73 | 74 | self.apk_candidates.append(ApkCandidate(apk_filename, 75 | apk_filepath, 76 | apk_test_filename, 77 | apk_test_filepath, 78 | version_code)) 79 | else: 80 | Printer.system_message(self.TAG, " * No .apk* files found.") 81 | 82 | def display_candidates(self): 83 | candidate_no = 0 84 | for apk_info in self.apk_candidates: 85 | candidate_no += 1 86 | Printer.system_message(self.TAG, " * Candidate no." + str(candidate_no) + " " 87 | + (Color.GREEN + "('can be used in test')" if apk_info.is_usable() 88 | else Color.RED + "('cannot be used in test - missing fields')")) 89 | Printer.system_message(self.TAG, 90 | " Apk candidate name: " + Color.GREEN + str(apk_info.apk_name) + Color.END) 91 | Printer.system_message(self.TAG, 92 | " Apk candidate path: " + Color.GREEN + str(apk_info.apk_path) + Color.END) 93 | Printer.system_message(self.TAG, 94 | " Related test apk: " + Color.GREEN + str(apk_info.test_apk_name) + Color.END) 95 | Printer.system_message(self.TAG, " Related test apk path: " + Color.GREEN + str(apk_info.test_apk_path) 96 | + Color.END) 97 | Printer.system_message(self.TAG, " Version: " + Color.GREEN + str(apk_info.apk_version) + Color.END) 98 | 99 | def _get_usable_apk_candidate_for_latest_version(self): 100 | latest_apk_info = None 101 | latest_ver = -1 102 | for apk_info in self.apk_candidates: 103 | if apk_info.is_usable() and apk_info.apk_version > latest_ver: 104 | latest_apk_info = apk_info 105 | latest_ver = apk_info.apk_version 106 | return latest_apk_info 107 | 108 | @staticmethod 109 | def get_list_with_application_apk(apk_name_part_cleaned, apk_dir): 110 | app_apk_files = list() 111 | for path, subdirs, files in os.walk(apk_dir): 112 | for filename in files: 113 | if apk_name_part_cleaned in filename and ".apk" in filename and "androidTest" not in filename: 114 | app_apk_files.append((path, filename)) 115 | 116 | return app_apk_files 117 | 118 | @staticmethod 119 | def get_list_with_test_apk(apk_name_part_cleaned, apk_dir): 120 | test_apk_files = list() 121 | for path, subdirs, files in os.walk(apk_dir): 122 | for filename in files: 123 | if apk_name_part_cleaned in filename and ".apk" in filename and "androidTest" in filename: 124 | test_apk_files.append((path, filename)) 125 | 126 | return test_apk_files 127 | 128 | 129 | class DeviceStore: 130 | TAG = "DeviceStore:" 131 | 132 | def __init__(self, 133 | adb_controller, 134 | adb_package_manager_controller, 135 | adb_settings_controller, 136 | avdmanager_controller, 137 | emulator_controller): 138 | 139 | self.adb_controller = adb_controller 140 | self.adb_package_manager_controller = adb_package_manager_controller 141 | self.adb_settings_controller = adb_settings_controller 142 | self.avdmanager_controller = avdmanager_controller 143 | self.emulator_controller = emulator_controller 144 | 145 | self.outside_session_virtual_devices = list() 146 | self.outside_session_devices = list() 147 | self.session_devices = list() 148 | 149 | def prepare_outside_session_devices(self): 150 | currently_visible_devices = self._get_visible_devices() 151 | 152 | for device_name, status in currently_visible_devices.items(): 153 | if "emulator" not in device_name: 154 | outside_session_device = OutsideSessionDevice(device_name, 155 | status, 156 | self.adb_controller, 157 | self.adb_package_manager_controller, 158 | self.adb_settings_controller) 159 | Printer.system_message(self.TAG, "Android Device model representing device with name " 160 | + Color.GREEN + device_name + Color.BLUE + " was added to test run.") 161 | self.outside_session_devices.append(outside_session_device) 162 | 163 | if not any(isinstance(device, OutsideSessionDevice) for device in self.outside_session_devices): 164 | Printer.system_message(self.TAG, "No Android Devices connected to PC were found.") 165 | 166 | def prepare_outside_session_virtual_devices(self): 167 | currently_visible_devices = self._get_visible_devices() 168 | 169 | for device_name, status in currently_visible_devices.items(): 170 | if "emulator" in device_name: 171 | outside_session_virtual_device = OutsideSessionVirtualDevice(device_name, 172 | status, 173 | self.adb_controller, 174 | self.adb_package_manager_controller, 175 | self.adb_settings_controller) 176 | Printer.system_message(self.TAG, "AVD model representing device with name " 177 | + Color.GREEN + device_name + Color.BLUE + " was added to test run.") 178 | self.outside_session_virtual_devices.append(outside_session_virtual_device) 179 | 180 | if not any(isinstance(device, OutsideSessionVirtualDevice) for device in self.outside_session_virtual_devices): 181 | Printer.system_message(self.TAG, "No currently launched AVD were found.") 182 | 183 | def prepare_session_devices(self, avd_set, avd_schemas): 184 | avd_ports = PortManager.get_open_ports(avd_set) 185 | for avd in avd_set.avd_list: 186 | instances_of_schema = avd.instances 187 | for i in range(instances_of_schema): 188 | avd_schema = copy.deepcopy(avd_schemas[avd.avd_name]) 189 | avd_schema.avd_name = avd_schema.avd_name + "-" + str(i) 190 | port = avd_ports.pop(0) 191 | 192 | log_file = FileUtils.clean_path(GlobalConfig.OUTPUT_AVD_LOG_DIR + avd_schema.avd_name + ".txt") 193 | FileUtils.create_file(GlobalConfig.OUTPUT_AVD_LOG_DIR, avd_schema.avd_name, "txt") 194 | Printer.system_message(self.TAG, "Created file " + Color.GREEN + log_file + Color.BLUE + ".") 195 | 196 | session_device = SessionVirtualDevice(avd_schema, 197 | port, 198 | log_file, 199 | self.avdmanager_controller, 200 | self.emulator_controller, 201 | self.adb_controller, 202 | self.adb_package_manager_controller, 203 | self.adb_settings_controller) 204 | self.session_devices.append(session_device) 205 | Printer.system_message(self.TAG, "Android Virtual Device model was created according to schema " 206 | + Color.GREEN + avd_schema.avd_name + Color.BLUE + 207 | ". Instance number: " + str(i) + ". Assigned to port: " + str(port) + ".") 208 | 209 | def _get_visible_devices(self): 210 | currently_visible_devices = dict() 211 | 212 | adb_devices_output = self.adb_controller.devices() 213 | adb_devices_lines = [line for line in adb_devices_output.splitlines() if len(line.split()) == 2] 214 | 215 | for line in adb_devices_lines: 216 | device_name = line.split()[0] 217 | device_status = line.split()[1] 218 | 219 | # edge case 220 | if device_name == "*": 221 | continue 222 | 223 | currently_visible_devices.update({device_name: device_status}) 224 | 225 | return currently_visible_devices 226 | 227 | def get_devices(self): 228 | return self.session_devices + self.outside_session_devices + self.outside_session_virtual_devices 229 | 230 | def update_model_statuses(self): 231 | currently_visible_devices = self._get_visible_devices() 232 | 233 | for session_device in self.session_devices: 234 | session_device.status = "not-launched" 235 | 236 | for outside_session_device in self.outside_session_devices: 237 | outside_session_device.status = "not-launched" 238 | 239 | for outside_session_virtual_device in self.outside_session_virtual_devices: 240 | outside_session_virtual_device.status = "not-launched" 241 | 242 | for device_name, status in currently_visible_devices.items(): 243 | for session_device in self.session_devices: 244 | if session_device.adb_name == device_name: 245 | session_device.status = status 246 | 247 | for outside_session_device in self.outside_session_devices: 248 | if outside_session_device.adb_name == device_name: 249 | outside_session_device.status = status 250 | 251 | for outside_session_virtual_device in self.outside_session_virtual_devices: 252 | if outside_session_virtual_device.adb_name == device_name: 253 | outside_session_virtual_device.status = status 254 | 255 | def clear_outside_session_virtual_device_models(self): 256 | self.outside_session_virtual_devices.clear() 257 | 258 | def clear_outside_session_device_models(self): 259 | self.outside_session_devices.clear() 260 | 261 | def clear_session_avd_models(self): 262 | self.session_devices.clear() 263 | 264 | def remove_device_from_session(self, device): 265 | if device in self.outside_session_virtual_devices: 266 | self.outside_session_virtual_devices.remove(device) 267 | elif device in self.outside_session_devices: 268 | self.outside_session_devices.remove(device) 269 | elif device in self.session_devices: 270 | self.session_devices.remove(device) 271 | Printer.system_message(self.TAG, "Device with name " 272 | + Color.GREEN + device.adb_name + Color.BLUE + " was removed from session.") 273 | 274 | 275 | class TestStore: 276 | TAG = "TestStore:" 277 | 278 | def __init__(self): 279 | self.packages_to_run = list() 280 | self.test_statuses = list() 281 | self.test_logcats = list() 282 | 283 | # TODO split into init and getter 284 | def get_packages(self, test_set, test_list): 285 | for package_name in test_set.set_package_names: 286 | for test_package in test_list[package_name].test_packages: 287 | if test_package not in self.packages_to_run: 288 | self.packages_to_run.append(test_package) 289 | return self.packages_to_run 290 | 291 | def store_test_status(self, test_statuses): 292 | for status in test_statuses: 293 | self.test_statuses.append(status) 294 | 295 | def store_test_logcat(self, test_logcats): 296 | for logcat in test_logcats: 297 | self.test_logcats.append(logcat) 298 | 299 | def test_contain_count(self, test_name): 300 | return len([t for t in self.get_test_statuses() if t.test_name == test_name]) 301 | 302 | def test_logcat_contain_count(self, test_name): 303 | return len([t for t in self.get_test_logcats() if t.test_name == test_name]) 304 | 305 | def get_test_statuses(self): 306 | return self.test_statuses 307 | 308 | def get_test_logcats(self): 309 | return self.test_logcats 310 | -------------------------------------------------------------------------------- /session/SessionModels.py: -------------------------------------------------------------------------------- 1 | from system.console import Color 2 | 3 | from error.Exceptions import LauncherFlowInterruptedException 4 | 5 | 6 | class _BasicDevice: 7 | def __init__(self, adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller): 8 | self.adb_controller = adb_controller 9 | self.adb_package_manager_controller = adb_package_manager_controller 10 | self.adb_settings_controller = adb_settings_controller 11 | self.adb_name = adb_name 12 | self.android_id = None 13 | self.status = status 14 | 15 | def install_apk(self, apk_file): 16 | return self.adb_controller.install_apk(self.adb_name, apk_file) 17 | 18 | def list_packages(self): 19 | return self.adb_package_manager_controller 20 | 21 | def get_installed_packages(self): 22 | return self.adb_package_manager_controller.get_installed_packages(self.adb_name) 23 | 24 | def uninstall_package(self, package_name): 25 | return self.adb_package_manager_controller.uninstall_package(self.adb_name, package_name) 26 | 27 | def get_android_id(self): 28 | if self.android_id is None: 29 | self.android_id = self.adb_settings_controller.get_device_android_id(self.adb_name).strip() 30 | return self.android_id 31 | 32 | 33 | class _BasicVirtualDevice(_BasicDevice): 34 | def __init__(self, adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller): 35 | super().__init__(adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller) 36 | 37 | def get_property(self, device_property): 38 | return self.adb_controller.get_property(self.adb_name, device_property) 39 | 40 | def kill(self): 41 | return self.adb_controller.kill_device(self.adb_name) 42 | 43 | 44 | class OutsideSessionDevice(_BasicDevice): 45 | def __init__(self, adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller): 46 | super().__init__(adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller) 47 | 48 | 49 | class OutsideSessionVirtualDevice(_BasicVirtualDevice): 50 | def __init__(self, adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller): 51 | super().__init__(adb_name, status, adb_controller, adb_package_manager_controller, adb_settings_controller) 52 | 53 | 54 | class SessionVirtualDevice(_BasicVirtualDevice): 55 | TAG = "SessionVirtualDevice:" 56 | 57 | def __init__(self, 58 | avd_schema, port, log_file, avdmanager_controller, emulator_controller, adb_controller, 59 | adb_package_manager_controller, adb_settings_controller): 60 | 61 | super().__init__("emulator-" + str(port), "not-launched", adb_controller, adb_package_manager_controller, 62 | adb_settings_controller) 63 | 64 | self.avdmanager_controller = avdmanager_controller 65 | self.emulator_controller = emulator_controller 66 | self.avd_schema = avd_schema 67 | self._check_avd_schema() 68 | 69 | self.port = port 70 | self.log_file = log_file 71 | 72 | def _check_avd_schema(self): 73 | if self.avd_schema.avd_name == "": 74 | message = "One AVD schema doesn't have name set." 75 | raise LauncherFlowInterruptedException(self.TAG, message) 76 | 77 | if self.avd_schema.create_avd_package == "": 78 | message = "Parameter 'create_avd_package' in AVD schema {} cannot be empty." 79 | message = message.format(self.avd_schema.avd_name) 80 | raise LauncherFlowInterruptedException(self.TAG, message) 81 | 82 | if self.avd_schema.launch_avd_launch_binary_name == "": 83 | message = "Parameter 'launch_avd_launch_binary_name' in AVD schema {} cannot be empty." 84 | message = message.format(self.avd_schema.avd_name) 85 | raise LauncherFlowInterruptedException(self.TAG, message) 86 | 87 | def create(self): 88 | return self.avdmanager_controller.create_avd(self.avd_schema) 89 | 90 | def delete(self): 91 | return self.avdmanager_controller.delete_avd(self.avd_schema) 92 | 93 | def launch(self): 94 | return self.emulator_controller.launch_avd(self.avd_schema, self.port, self.log_file) 95 | 96 | def apply_config_ini(self): 97 | return self.emulator_controller.apply_config_to_avd(self.avd_schema) 98 | 99 | 100 | class ApkCandidate: 101 | MISSING_VALUE = Color.RED + "missing" + Color.END 102 | 103 | def __init__(self, apk_name, apk_path, test_apk_name, test_apk_path, apk_version): 104 | self.apk_name = self._set_field(apk_name) 105 | self.apk_path = self._set_field(apk_path) 106 | self.test_apk_name = self._set_field(test_apk_name) 107 | self.test_apk_path = self._set_field(test_apk_path) 108 | self.apk_version = apk_version 109 | 110 | def is_usable(self): 111 | return self._is_field_filled(self.apk_name) \ 112 | and self._is_field_filled(self.test_apk_path) \ 113 | and self._is_field_filled(self.test_apk_name) \ 114 | and self._is_field_filled(self.test_apk_path) \ 115 | and self.apk_version != -1 116 | 117 | def __str__(self): 118 | return "Apk('apk_name: " + self.apk_name + "', " \ 119 | + "'apk_path: " + self.apk_path + "', " \ 120 | + "'test_apk_name: " + self.test_apk_name + "', " \ 121 | + "'test_apk_path: " + self.test_apk_path + "', " \ 122 | + "'version_code: " + str(self.apk_version) + "')" 123 | 124 | def _is_field_filled(self, field): 125 | return field is not None and field != self.MISSING_VALUE 126 | 127 | def _set_field(self, field): 128 | return field if field is not None and field != "" else self.MISSING_VALUE 129 | 130 | 131 | class SessionSummary: 132 | def __init__(self): 133 | self.device_summaries = None 134 | self.time_summary = None 135 | self.apk_summary = None 136 | self.test_summary = None 137 | 138 | 139 | class SessionDeviceSummary: 140 | def __init__(self): 141 | self.device_name = None 142 | 143 | self.creation_start_time = None 144 | self.creation_end_time = None 145 | self.creation_time = None 146 | 147 | self.launch_start_time = None 148 | self.launch_end_time = None 149 | self.launch_time = None 150 | 151 | self.apk_install_start_time = None 152 | self.apk_install_end_time = None 153 | self.apk_install_time = None 154 | 155 | self.test_apk_install_start_time = None 156 | self.test_apk_install_end_time = None 157 | self.test_apk_install_time = None 158 | 159 | 160 | class SessionTimeSummary: 161 | def __init__(self): 162 | self.total_device_creation_start_time = None 163 | self.total_device_creation_end_time = None 164 | self.total_device_creation_time = None 165 | 166 | self.total_device_launch_start_time = None 167 | self.total_device_launch_end_time = None 168 | self.total_device_launch_time = None 169 | 170 | self.total_apk_build_start_time = None 171 | self.total_apk_build_end_time = None 172 | self.total_apk_build_time = None 173 | 174 | self.total_apk_install_start_time = None 175 | self.total_apk_install_end_time = None 176 | self.total_apk_install_time = None 177 | 178 | self.total_test_start_time = None 179 | self.total_test_end_time = None 180 | self.total_test_time = None 181 | 182 | self.total_rerun_start_time = None 183 | self.total_rerun_end_time = None 184 | self.total_rerun_time = None 185 | 186 | self.total_session_start_time = None 187 | self.total_session_end_time = None 188 | self.total_session_time = None 189 | 190 | 191 | class SessionApkSummary: 192 | def __init__(self): 193 | self.apk = None 194 | self.test_apk = None 195 | self.version_code = None 196 | 197 | self.apk_build_start_time = None 198 | self.apk_build_end_time = None 199 | self.apk_build_time = None 200 | 201 | self.test_apk_build_start_time = None 202 | self.test_apk_build_end_time = None 203 | self.test_apk_build_time = None 204 | 205 | 206 | class SessionTestSummary: 207 | def __init__(self): 208 | self.test_number = None 209 | self.test_passed = None 210 | self.test_failed = None 211 | self.health_rate = None 212 | 213 | 214 | class SessionFlakinessCheckSummary: 215 | def __init__(self): 216 | self.suspects = dict() 217 | 218 | 219 | class TestSummary: 220 | def __init__(self): 221 | self.test_name = None 222 | self.test_container = None 223 | self.test_full_package = None 224 | self.test_status = None 225 | self.device = None 226 | self.test_start_time = None 227 | self.test_end_time = None 228 | self.rerun_count = 0 229 | self.error_messages = list() 230 | 231 | 232 | class TestLogCat: 233 | def __init__(self): 234 | self.test_name = None 235 | self.test_container = None 236 | self.test_full_package = None 237 | self.rerun_count = 0 238 | self.lines = list() 239 | 240 | 241 | class TestLogCatLine: 242 | def __init__(self): 243 | self.date = None 244 | self.time = None 245 | self.level = None 246 | self.tag = None 247 | self.text = None 248 | -------------------------------------------------------------------------------- /session/SessionThreads.py: -------------------------------------------------------------------------------- 1 | import time 2 | import re 3 | import os 4 | import subprocess 5 | import threading 6 | 7 | from error.Exceptions import LauncherFlowInterruptedException 8 | 9 | from settings import GlobalConfig 10 | 11 | from session.SessionModels import ( 12 | TestLogCat, 13 | TestLogCatLine, 14 | TestSummary 15 | ) 16 | 17 | from system.file import FileUtils 18 | from system.console import ShellHelper 19 | from system.console import ( 20 | Printer, 21 | Color 22 | ) 23 | 24 | 25 | class TestSummarySavingThread(threading.Thread): 26 | TAG = "TestSummarySavingThread" 27 | 28 | TEST_SUMMARY_APPENDIX = "test_summary.json" 29 | 30 | def __init__(self, device, test_summary_list): 31 | super().__init__() 32 | self.device = device 33 | self.test_summary_list = test_summary_list 34 | self.created_files = None 35 | 36 | def run(self): 37 | self.created_files = list() 38 | 39 | for test_summary in self.test_summary_list: 40 | test_summary_json_dict = vars(test_summary) 41 | 42 | filename = test_summary_json_dict["test_name"] + "_" + self.device.adb_name + "_" \ 43 | + self.TEST_SUMMARY_APPENDIX 44 | 45 | if test_summary.rerun_count > 0: 46 | filename = filename.replace(".json", "_rerun_no_{}.json".format(test_summary.rerun_count)) 47 | 48 | created_file_path = FileUtils.save_json_dict_to_json( 49 | GlobalConfig.OUTPUT_TEST_LOG_DIR, test_summary_json_dict, filename) 50 | 51 | self.created_files.append(created_file_path) 52 | 53 | def is_finished(self): 54 | return self.created_files is not None and len(self.created_files) == len(self.test_summary_list) \ 55 | and all(os.path.isfile(file) for file in self.created_files) 56 | 57 | 58 | class TestLogcatSavingThread(threading.Thread): 59 | TAG = "TestLogcatSavingThread" 60 | 61 | TEST_LOGCAT_APPENDIX = "logcat.json" 62 | 63 | def __init__(self, device, test_logcat_list): 64 | super().__init__() 65 | self.device = device 66 | self.test_logcat_list = test_logcat_list 67 | self.created_files = None 68 | 69 | def run(self): 70 | self.created_files = list() 71 | 72 | for logcat in self.test_logcat_list: 73 | logcat_lines_json_dict = list() 74 | for logcat_line in logcat.lines: 75 | logcat_lines_json_dict.append(vars(logcat_line)) 76 | logcat.lines = logcat_lines_json_dict 77 | 78 | logcat_json_dict = vars(logcat) 79 | 80 | filename = logcat_json_dict["test_name"] + "_" + self.device.adb_name + "_" + self.TEST_LOGCAT_APPENDIX 81 | 82 | if logcat.rerun_count > 0: 83 | filename = filename.replace(".json", "_rerun_no_{}.json".format(logcat.rerun_count)) 84 | 85 | created_file_path = FileUtils.save_json_dict_to_json( 86 | GlobalConfig.OUTPUT_TEST_LOGCAT_DIR, logcat_json_dict, filename) 87 | 88 | self.created_files.append(created_file_path) 89 | 90 | def is_finished(self): 91 | return self.created_files is not None and len(self.created_files) == len(self.test_logcat_list) \ 92 | and all(os.path.isfile(file) for file in self.created_files) 93 | 94 | 95 | class TestRecordingSavingThread(threading.Thread): 96 | TAG = "TestRecordingSavingThread" 97 | 98 | def __init__(self, device): 99 | super().__init__() 100 | self.device = device 101 | 102 | self.recordings = list() 103 | self.recording_pull_cmds = dict() 104 | self.recording_clear_cmds = dict() 105 | 106 | self.should_finish = False 107 | 108 | def run(self): 109 | while True: 110 | if self.recordings and self._all_recordings_has_commands(): 111 | recording = self.recordings.pop() 112 | 113 | # TODO Implement proper synchronisation/wait for .mp4 to finish being written to 114 | time.sleep(10) 115 | 116 | ShellHelper.execute_shell(self.recording_pull_cmds.get(recording), False, False) 117 | ShellHelper.execute_shell(self.recording_clear_cmds.get(recording), False, False) 118 | 119 | if self.should_finish: 120 | break 121 | 122 | def _all_recordings_has_commands(self): 123 | all_recordings_have_commands = True 124 | for recording in self.recordings: 125 | pull_cmd = self.recording_pull_cmds.get(recording) 126 | remove_cmd = self.recording_clear_cmds.get(recording) 127 | 128 | if pull_cmd is None or remove_cmd is None: 129 | all_recordings_have_commands = False 130 | break 131 | 132 | return all_recordings_have_commands 133 | 134 | def add_recordings(self, recording_list): 135 | self.recordings.extend(recording_list) 136 | 137 | def add_pull_recording_cmds(self, pull_recording_cmds): 138 | for cmd in pull_recording_cmds: 139 | for recording_name in self.recordings: 140 | if recording_name in cmd: 141 | self.recording_pull_cmds.update({recording_name: cmd}) 142 | 143 | def add_clear_recordings_cmd(self, clear_recording_cmds): 144 | for cmd in clear_recording_cmds: 145 | for recording_name in self.recordings: 146 | if recording_name in cmd: 147 | self.recording_clear_cmds.update({recording_name: cmd}) 148 | 149 | def kill_processes(self): 150 | self.should_finish = True 151 | 152 | 153 | class TestRecordingThread(threading.Thread): 154 | TAG = "TestRecordingThread:" 155 | 156 | def __init__(self, start_recording_cmd, recording_name, device): 157 | super().__init__() 158 | self.start_recording_cmd = start_recording_cmd 159 | self.recording_name = recording_name 160 | 161 | self.device = device 162 | self.TAG = self.TAG.replace("device_adb_name", device.adb_name) 163 | 164 | self.device = list() 165 | self.process = None 166 | 167 | def run(self): 168 | with subprocess.Popen(self.start_recording_cmd + self.recording_name, shell=True, stdout=subprocess.PIPE, 169 | bufsize=1, universal_newlines=True) as p: 170 | self.process = p 171 | 172 | def kill_processes(self): 173 | if self.process is not None and hasattr(self.process, "kill"): 174 | self.process.kill() 175 | 176 | 177 | class TestLogCatMonitorThread(threading.Thread): 178 | TAG = "TestLogCatMonitorThread:" 179 | 180 | TEST_STARTED = "TestRunner: started:" 181 | TEST_FINISHED = "TestRunner: finished:" 182 | 183 | LOG_LEVELS = ["D", "I", "W", "V", "E"] 184 | 185 | DATE_INDEX = 0 186 | TIME_INDEX = 1 187 | PID_INDEX = 2 188 | LEVEL_INDEX = 4 189 | TAG_INDEX = 5 190 | 191 | def __init__(self, device, device_commands_dict, should_record_screen): 192 | super().__init__() 193 | self.monitor_logcat_cmd = device_commands_dict["monitor_logcat_cmd"] 194 | self.flush_logcat_cmd = device_commands_dict["flush_logcat_cmd"] 195 | self.record_screen_cmd = device_commands_dict["record_screen_cmd"] 196 | 197 | self.device = device 198 | self.TAG = self.TAG.replace("device_adb_name", device.adb_name) 199 | self.should_record_screen = should_record_screen 200 | 201 | self.logs = list() 202 | self.recordings = list() 203 | 204 | self.logcat_process = None 205 | self.screen_recording_thread = None 206 | 207 | def run(self): 208 | ShellHelper.execute_shell(self.flush_logcat_cmd, False, False) 209 | with subprocess.Popen(self.monitor_logcat_cmd, shell=True, stdout=subprocess.PIPE, bufsize=1, 210 | universal_newlines=True, encoding="utf-8", errors="ignore") as p: 211 | self.logcat_process = p 212 | 213 | current_log = None 214 | current_process_pid = None 215 | current_recording_name = None 216 | 217 | for line in p.stdout: 218 | line_cleaned = line.encode("utf-8", "ignore").decode("utf-8").strip() 219 | line_parts = line_cleaned.split() 220 | 221 | if len(line_parts) <= 5: 222 | continue 223 | 224 | if self.TEST_STARTED in line and current_log is None and current_process_pid is None: 225 | current_log = TestLogCat() 226 | 227 | current_process_pid = line_parts[self.PID_INDEX] 228 | 229 | test_name = re.findall("TestRunner: started:(.+)\(", line_cleaned) 230 | current_log.test_name = test_name[0].strip() 231 | 232 | full_test_package = re.findall("\((.+)\)", line_cleaned) 233 | package_parts = full_test_package[0].split(".") 234 | current_log.test_container = package_parts.pop().strip() 235 | current_log.test_full_package = full_test_package[0].strip() + "." + test_name[0].strip() 236 | if self.should_record_screen: 237 | self._restart_recording(current_log.test_name) 238 | if current_recording_name is not None: 239 | self.recordings.append(current_recording_name) 240 | current_recording_name = self.screen_recording_thread.recording_name 241 | 242 | if current_log is not None: 243 | if line_parts[self.PID_INDEX] == current_process_pid: 244 | logcat_line = TestLogCatLine() 245 | 246 | date = line_parts[self.DATE_INDEX] 247 | logcat_line.date = date 248 | 249 | time_hour = line_parts[self.TIME_INDEX] 250 | logcat_line.time = time_hour 251 | 252 | level = line_parts[self.LEVEL_INDEX] 253 | logcat_line.level = level 254 | 255 | tag = line_parts[self.TAG_INDEX] 256 | if len(tag) > 0 and tag[len(tag) - 1] == ":": 257 | tag = tag[:-1] 258 | logcat_line.tag = tag 259 | 260 | string_pos = line_cleaned.find(tag) 261 | length_tag = len(tag) 262 | text = line_cleaned[(string_pos + length_tag):].strip() 263 | if text.startswith(":"): 264 | text = text[1:] 265 | text = text.strip() 266 | logcat_line.text = text 267 | 268 | current_log.lines.append(logcat_line) 269 | 270 | if self.TEST_FINISHED in line: 271 | self.logs.append(current_log) 272 | 273 | if self.should_record_screen: 274 | self._stop_recording() 275 | self.recordings.append(current_recording_name) 276 | 277 | current_log = None 278 | current_process_pid = None 279 | current_recording_name = None 280 | 281 | def _stop_recording(self): 282 | if self.screen_recording_thread is not None: 283 | self.screen_recording_thread.kill_processes() 284 | self.screen_recording_thread = None 285 | 286 | def _restart_recording(self, test_name): 287 | if self.screen_recording_thread is None or not self.screen_recording_thread.is_alive(): 288 | recording_name = test_name + "-" + str(int(round(time.time() * 1000))) + ".mp4" 289 | self.screen_recording_thread = TestRecordingThread(self.record_screen_cmd, recording_name, self.device) 290 | self.screen_recording_thread.start() 291 | 292 | def kill_processes(self): 293 | if self.screen_recording_thread is not None: 294 | self.screen_recording_thread.kill_processes() 295 | 296 | if self.logcat_process is not None and hasattr(self.logcat_process, "kill"): 297 | self.logcat_process.kill() 298 | 299 | 300 | class TestThread(threading.Thread): 301 | TAG = "TestThread:" 302 | 303 | TEST_STATUS_SUCCESS = "success" 304 | TEST_STATUS_FAILURE = "failure" 305 | 306 | TEST_ENDED_WITH_SUCCESS_0 = "INSTRUMENTATION_STATUS_CODE: 0" 307 | TEST_ENDED_WITH_FAILURE = "INSTRUMENTATION_STATUS_CODE: -2" 308 | 309 | TEST_NAME = "INSTRUMENTATION_STATUS: test=" 310 | TEST_PACKAGE = "INSTRUMENTATION_STATUS: class=" 311 | TEST_OUTPUT_STACK_STARTED = "INSTRUMENTATION_STATUS: stack=" 312 | TEST_CURRENT_TEST_NUMBER = "INSTRUMENTATION_STATUS: current" 313 | 314 | def __init__(self, launch_tests_cmd, device): 315 | super().__init__() 316 | self.launch_tests_cmd = launch_tests_cmd 317 | 318 | self.device = device 319 | self.TAG = self.TAG.replace("device_adb_name", device.adb_name) 320 | 321 | self.logs = list() 322 | 323 | self.test_process = None 324 | 325 | def run(self): 326 | Printer.console_highlighted(self.TAG, "Executing shell command: ", self.launch_tests_cmd) 327 | with subprocess.Popen(self.launch_tests_cmd, shell=True, stdout=subprocess.PIPE, bufsize=1, 328 | universal_newlines=True, encoding="utf-8", errors="ignore") as p: 329 | self.test_process = p 330 | 331 | is_using_post_api27_flow = None 332 | reading_stack_in_progress = False 333 | current_log = None 334 | stack = None 335 | 336 | for line in p.stdout: 337 | line_cleaned = line.encode("utf-8", "ignore").decode("utf-8") 338 | 339 | if self.TEST_NAME in line_cleaned and is_using_post_api27_flow is None: 340 | is_using_post_api27_flow = False 341 | elif self.TEST_PACKAGE in line_cleaned and is_using_post_api27_flow is None: 342 | is_using_post_api27_flow = True 343 | elif is_using_post_api27_flow is None: 344 | continue 345 | 346 | if is_using_post_api27_flow: 347 | current_log, stack, reading_stack_in_progress = self._post_api27_logging_flow( 348 | line_cleaned, current_log, stack, reading_stack_in_progress) 349 | else: 350 | current_log, stack, reading_stack_in_progress = self._pre_api27_logging_flow( 351 | line_cleaned, current_log, stack, reading_stack_in_progress) 352 | 353 | def _post_api27_logging_flow(self, line_cleaned, current_log, stack, reading_stack_in_progress): 354 | if self.TEST_PACKAGE in line_cleaned and current_log is None: 355 | current_log = TestSummary() 356 | 357 | line_cleaned = line_cleaned.replace(self.TEST_PACKAGE, "").strip() 358 | package_parts = line_cleaned.split(".") 359 | current_log.test_container = package_parts.pop() 360 | current_log.test_full_package = line_cleaned + "." 361 | 362 | if self.TEST_OUTPUT_STACK_STARTED in line_cleaned: 363 | stack = "" 364 | reading_stack_in_progress = True 365 | 366 | if self.TEST_CURRENT_TEST_NUMBER in line_cleaned: 367 | if stack is not None: 368 | current_log.error_messages.append(stack) 369 | reading_stack_in_progress = False 370 | stack = None 371 | 372 | if self.TEST_ENDED_WITH_SUCCESS_0 in line_cleaned: 373 | current_log.test_end_time = int(round(time.time() * 1000)) 374 | current_log.test_status = self.TEST_STATUS_SUCCESS 375 | self.logs.append(current_log) 376 | current_log = None 377 | 378 | if self.TEST_ENDED_WITH_FAILURE in line_cleaned: 379 | current_log.test_end_time = int(round(time.time() * 1000)) 380 | current_log.test_status = self.TEST_STATUS_FAILURE 381 | self.logs.append(current_log) 382 | current_log = None 383 | 384 | if self.TEST_NAME in line_cleaned: 385 | current_log.test_name = line_cleaned.replace(self.TEST_NAME, "").strip() 386 | current_log.test_start_time = int(round(time.time() * 1000)) 387 | current_log.device = self.device.adb_name 388 | 389 | if current_log.test_name not in current_log.test_full_package: 390 | current_log.test_full_package += current_log.test_name 391 | 392 | Printer.console(self.TAG + " Test " + current_log.test_name + "\n", end='') 393 | 394 | if reading_stack_in_progress: 395 | if self.TEST_OUTPUT_STACK_STARTED in line_cleaned: 396 | test_error_info = self.TAG + " Test " + current_log.test_name + " - FAILED\n" 397 | Printer.console(test_error_info, end="") 398 | line_cleaned = line_cleaned.replace(self.TEST_OUTPUT_STACK_STARTED, "") 399 | stack += line_cleaned 400 | Printer.console(line_cleaned, end="") 401 | 402 | return current_log, stack, reading_stack_in_progress 403 | 404 | def _pre_api27_logging_flow(self, line_cleaned, current_log, stack, reading_stack_in_progress): 405 | if self.TEST_NAME in line_cleaned and current_log is None: 406 | current_log = TestSummary() 407 | current_log.test_name = line_cleaned.replace(self.TEST_NAME, "").strip() 408 | current_log.test_start_time = int(round(time.time() * 1000)) 409 | current_log.device = self.device.adb_name 410 | 411 | Printer.console(self.TAG + " Test " + current_log.test_name + "\n", end='') 412 | 413 | if self.TEST_PACKAGE in line_cleaned: 414 | line_cleaned = line_cleaned.replace(self.TEST_PACKAGE, "").strip() 415 | package_parts = line_cleaned.split(".") 416 | current_log.test_container = package_parts.pop() 417 | current_log.test_full_package = line_cleaned + "." + current_log.test_name 418 | 419 | if self.TEST_OUTPUT_STACK_STARTED in line_cleaned: 420 | stack = "" 421 | reading_stack_in_progress = True 422 | 423 | if self.TEST_CURRENT_TEST_NUMBER in line_cleaned: 424 | if stack is not None: 425 | current_log.error_messages.append(stack) 426 | reading_stack_in_progress = False 427 | stack = None 428 | 429 | if self.TEST_ENDED_WITH_SUCCESS_0 in line_cleaned: 430 | current_log.test_end_time = int(round(time.time() * 1000)) 431 | current_log.test_status = "success" 432 | self.logs.append(current_log) 433 | current_log = None 434 | 435 | if self.TEST_ENDED_WITH_FAILURE in line_cleaned: 436 | current_log.test_end_time = int(round(time.time() * 1000)) 437 | current_log.test_status = "failure" 438 | self.logs.append(current_log) 439 | current_log = None 440 | 441 | if reading_stack_in_progress: 442 | if self.TEST_OUTPUT_STACK_STARTED in line_cleaned: 443 | test_error_info = self.TAG + " Test " + current_log.test_name + " - FAILED\n" 444 | Printer.console(test_error_info, end="") 445 | line_cleaned = line_cleaned.replace(self.TEST_OUTPUT_STACK_STARTED, "") 446 | stack += line_cleaned 447 | Printer.console(line_cleaned, end="") 448 | 449 | return current_log, stack, reading_stack_in_progress 450 | 451 | def kill_processes(self): 452 | if self.test_process is not None and hasattr(self.test_process, "kill"): 453 | self.test_process.kill() 454 | 455 | 456 | class ApkInstallThread(threading.Thread): 457 | TAG = "ApkInstallThread:" 458 | 459 | def __init__(self, dump_badging_cmd, device, apk_name, apk_path): 460 | super().__init__() 461 | self.dump_badging_cmd = dump_badging_cmd 462 | 463 | self.device = device 464 | self.TAG = self.TAG.replace("device_adb_name", device.adb_name) 465 | 466 | self.apk_name = apk_name 467 | self.apk_path = apk_path 468 | 469 | self.install_time = 0 470 | self.note = None 471 | self.is_finished = False 472 | 473 | def run(self): 474 | start_time = int(round(time.time() * 1000)) 475 | 476 | package = self._get_apk_package() 477 | installed_packages_str = self.device.get_installed_packages() 478 | 479 | if package in installed_packages_str: 480 | Printer.system_message(self.TAG, "Package " + Color.GREEN + package + Color.BLUE + 481 | " is currently installed on device " + Color.GREEN + self.device.adb_name 482 | + Color.BLUE + ". Removing from device...") 483 | self.device.uninstall_package(package) 484 | else: 485 | Printer.system_message(self.TAG, "Package " + Color.GREEN + package + Color.BLUE + 486 | " was not found on device " + Color.GREEN + self.device.adb_name + Color.BLUE + ".") 487 | 488 | Printer.system_message(self.TAG, "Installing .*apk file...") 489 | self.device.install_apk(self.apk_path) 490 | 491 | end_time = int(round(time.time() * 1000)) 492 | self.install_time = (end_time - start_time) / 1000 493 | 494 | Printer.system_message(self.TAG, ".*apk " + Color.GREEN + self.apk_path + Color.BLUE 495 | + " was successfully installed on device " + Color.GREEN + self.device.adb_name 496 | + Color.BLUE + ". It took " + Color.GREEN + str(self.install_time) + Color.BLUE 497 | + " seconds.") 498 | self.is_finished = True 499 | 500 | def _get_apk_package(self): 501 | dump = ShellHelper.execute_shell(self.dump_badging_cmd, False, False) 502 | regex_result = re.findall("package: name='(.+?)'", dump) 503 | if regex_result: 504 | package = str(regex_result[0]) 505 | if ".test" in package: 506 | GlobalConfig.APP_TEST_PACKAGE = package 507 | else: 508 | GlobalConfig.APP_PACKAGE = package 509 | Printer.system_message(self.TAG, "Package that is about to be installed: " + Color.GREEN + package 510 | + Color.BLUE + ".") 511 | else: 512 | message = "Unable to find package of .*apk file: " + self.apk_name 513 | raise LauncherFlowInterruptedException(self.TAG, message) 514 | return package 515 | -------------------------------------------------------------------------------- /settings/GlobalConfig.py: -------------------------------------------------------------------------------- 1 | SHOULD_RESTART_ADB = None 2 | SHOULD_BUILD_NEW_APK = None 3 | SHOULD_RECREATE_EXISTING_AVD = None 4 | SHOULD_LAUNCH_AVD_SEQUENTIALLY = None 5 | SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION = None 6 | SHOULD_RECORD_TESTS = None 7 | SHOULD_RERUN_FAILED_TESTS = None 8 | 9 | AVD_ADB_BOOT_TIMEOUT = None 10 | AVD_SYSTEM_BOOT_TIMEOUT = None 11 | 12 | IGNORED_DEVICE_LIST = [] 13 | 14 | ADB_SCAN_INTERVAL = None 15 | ADB_CALL_BUFFER_SIZE = None 16 | ADB_CALL_BUFFER_DELAY_BETWEEN_CMD = None 17 | 18 | FLAKINESS_RERUN_COUNT = None 19 | 20 | SDK_DIR = "" 21 | AVD_DIR = "" 22 | PROJECT_ROOT_DIR = "" 23 | APK_DIR = "" 24 | OUTPUT_DIR = "" 25 | 26 | OUTPUT_SUMMARY_LOG_DIR = "" 27 | OUTPUT_AVD_LOG_DIR = "" 28 | OUTPUT_TEST_LOG_DIR = "" 29 | OUTPUT_TEST_LOGCAT_DIR = "" 30 | OUTPUT_TEST_RECORDINGS_DIR = "" 31 | OUTPUT_STYLES_FOLDER_DIR = "" 32 | OUTPUT_LOGCAT_HTML_DIR = "" 33 | OUTPUT_INDEX_HTML_DIR = "" 34 | 35 | DEVICE_VIDEO_STORAGE_DIR = "" 36 | LOG_GENERATOR_DIR = "" 37 | 38 | INSTRUMENTATION_RUNNER = "" 39 | APP_PACKAGE = "" 40 | APP_TEST_PACKAGE = "" 41 | -------------------------------------------------------------------------------- /settings/Version.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from error.Exceptions import LauncherFlowInterruptedException 4 | 5 | from system.console import ( 6 | Printer, 7 | Color 8 | ) 9 | 10 | NUMBER = "1.1-beta" 11 | MIN_PYTHON_VER = (3, 6) 12 | 13 | 14 | def info(): 15 | Printer.system_message("", "AutomationTestSupervisor version: " + Color.GREEN + NUMBER + Color.BLUE + ".") 16 | 17 | 18 | def python_check(): 19 | if sys.version_info >= MIN_PYTHON_VER: 20 | Printer.system_message("", "Minimum Python version requirement met! Your version: " + Color.GREEN 21 | + str(sys.version_info) + Color.BLUE + ".") 22 | 23 | else: 24 | message = ("Invalid Python version. Please use at least Python " + str(MIN_PYTHON_VER[0]) + "." 25 | + str(MIN_PYTHON_VER[1]) + ".") 26 | raise LauncherFlowInterruptedException("", message) 27 | -------------------------------------------------------------------------------- /settings/loader/ArgLoader.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | 4 | from system.file.FileUtils import ( 5 | clean_path, 6 | get_project_root, 7 | load_json, 8 | add_ending_slash 9 | ) 10 | 11 | TAG = "ArgLoader:" 12 | 13 | CONFIG_FILES_DIR_DEFAULT_DIR = clean_path(add_ending_slash(get_project_root()) + "config_files_dir.json") 14 | 15 | LAUNCH_MANIFEST_DIR_KEY = "launch_manifest_path" 16 | TEST_MANIFEST_DIR_KEY = "test_manifest_path" 17 | AVD_MANIFEST_DIR_KEY = "avd_manifest_path" 18 | PATH_MANIFEST_DIR_KEY = "path_manifest_path" 19 | 20 | config_files_dir = load_json(CONFIG_FILES_DIR_DEFAULT_DIR) 21 | 22 | 23 | def get_manifest_dir(key): 24 | if config_files_dir is None: 25 | return None 26 | else: 27 | return config_files_dir[key] 28 | 29 | 30 | LAUNCH_PLAN_DEFAULT = "default" 31 | TEST_SET_DEFAULT = "default" 32 | AVD_SET_DEFAULT = "default" 33 | PATH_SET_DEFAULT = "default" 34 | 35 | LAUNCH_PLAN_PREFIX = "-lplan" 36 | TEST_SET_PREFIX = "-tset" 37 | AVD_SET_PREFIX = "-aset" 38 | PATH_SET_PREFIX = "-pset" 39 | 40 | parser = argparse.ArgumentParser() 41 | parser.add_argument(LAUNCH_PLAN_PREFIX, 42 | type=str, 43 | default=LAUNCH_PLAN_DEFAULT, 44 | help="Name of launch plan specified in LaunchManifest.json.") 45 | 46 | parser.add_argument(TEST_SET_PREFIX, 47 | nargs="+", 48 | default=[TEST_SET_DEFAULT], 49 | help="Name of test set specified in TestManifest.json.") 50 | 51 | parser.add_argument(AVD_SET_PREFIX, 52 | type=str, 53 | default=AVD_SET_DEFAULT, 54 | help="Name of AVD set specified in AvdManifest.json.") 55 | 56 | parser.add_argument(PATH_SET_PREFIX, 57 | type=str, 58 | default=PATH_SET_DEFAULT, 59 | help="Name of path set set specified in PathManifest.json.") 60 | 61 | parser_args = parser.parse_args() 62 | 63 | 64 | def get_arg_loaded_by(param): 65 | if param == LAUNCH_PLAN_PREFIX: 66 | return parser_args.lplan 67 | if param == TEST_SET_PREFIX: 68 | return parser_args.tset 69 | if param == AVD_SET_PREFIX: 70 | return parser_args.aset 71 | if param == PATH_SET_PREFIX: 72 | return parser_args.pset 73 | return None 74 | -------------------------------------------------------------------------------- /settings/loader/AvdSetLoader.py: -------------------------------------------------------------------------------- 1 | from error.Exceptions import LauncherFlowInterruptedException 2 | 3 | from settings.loader import ArgLoader 4 | from settings.manifest.models.AvdManifestModels import AvdManifest 5 | 6 | from system.console import ( 7 | Printer, 8 | Color 9 | ) 10 | 11 | from system.file.FileUtils import ( 12 | make_path_absolute 13 | ) 14 | 15 | TAG = "AvdSetLoader:" 16 | 17 | 18 | def init_avd_settings(): 19 | avd_set_name = _load_avd_set_name() 20 | avd_manifest = _load_avd_manifest() 21 | 22 | avd_set = None 23 | avd_schema_dict = None 24 | 25 | if avd_set_name != ArgLoader.AVD_SET_DEFAULT: 26 | avd_set = _load_avd_set(avd_manifest, avd_set_name) 27 | avd_schema_dict = _load_avd_schema(avd_manifest, avd_set, avd_set_name) 28 | 29 | return avd_set, avd_schema_dict 30 | 31 | 32 | def _load_avd_set_name(): 33 | avd_set_name = ArgLoader.get_arg_loaded_by(ArgLoader.AVD_SET_PREFIX) 34 | 35 | if avd_set_name is None: 36 | Printer.system_message(TAG, "No AVD set selected. ""Currently available real devices will be used in test " 37 | "session.") 38 | else: 39 | Printer.system_message(TAG, "Selected avd set: " + Color.GREEN + avd_set_name + Color.BLUE + ".") 40 | return avd_set_name 41 | 42 | 43 | def _load_avd_manifest(): 44 | avd_manifest_dir = make_path_absolute(ArgLoader.get_manifest_dir(ArgLoader.AVD_MANIFEST_DIR_KEY)) 45 | 46 | if avd_manifest_dir is None: 47 | message = ("AvdManifest file directory was not found. Check if config_files_dir.json exists in root of" 48 | + "project. Otherwise check if it's linking to existing file.") 49 | raise LauncherFlowInterruptedException(TAG, message) 50 | else: 51 | avd_manifest = AvdManifest(avd_manifest_dir) 52 | Printer.system_message(TAG, "Created AvdManifest from file: " + Color.GREEN + avd_manifest_dir + Color.BLUE 53 | + ".") 54 | return avd_manifest 55 | 56 | 57 | def _load_avd_set(avd_manifest, avd_set_name): 58 | if avd_manifest.contains_set(avd_set_name): 59 | Printer.system_message(TAG, "AVD set " + Color.GREEN + avd_set_name + Color.BLUE + " was found in AvdManifest.") 60 | 61 | avd_set = avd_manifest.get_set(avd_set_name) 62 | Printer.system_message(TAG, "Requested AVD in set:") 63 | for requested_avd_schema in avd_set.avd_list: 64 | Printer.system_message(TAG, " * " + Color.GREEN + requested_avd_schema.avd_name + Color.BLUE 65 | + " - instances num: " + Color.GREEN + str(requested_avd_schema.instances) 66 | + Color.BLUE + ".") 67 | else: 68 | message = "Invalid AVD set. Set '{}' does not exist in AvdManifest!" 69 | message = message.format(avd_set_name) 70 | raise LauncherFlowInterruptedException(TAG, message) 71 | 72 | return avd_manifest.get_set(avd_set_name) 73 | 74 | 75 | def _load_avd_schema(avd_manifest, avd_set, avd_set_name): 76 | avd_schema_dict = avd_manifest.avd_schema_dict 77 | 78 | for avd in avd_set.avd_list: 79 | if avd_manifest.contains_schema(avd.avd_name): 80 | Printer.system_message(TAG, "AVD schema " + Color.GREEN + avd.avd_name + Color.BLUE 81 | + " was found in AvdManifest.") 82 | else: 83 | message = "Set '{}' requests usage of AVD schema with name '{}' which doesn't exists in AVD schema list." 84 | message = message.format(avd_set_name, avd.avd_name) 85 | raise LauncherFlowInterruptedException(TAG, message) 86 | 87 | return avd_schema_dict 88 | -------------------------------------------------------------------------------- /settings/loader/LaunchPlanLoader.py: -------------------------------------------------------------------------------- 1 | from error.Exceptions import LauncherFlowInterruptedException 2 | from settings import GlobalConfig 3 | from settings.loader import ArgLoader 4 | from settings.manifest.models.LaunchManifestModels import LaunchManifest 5 | from system.console import Color 6 | from system.console import Printer 7 | 8 | from system.file.FileUtils import ( 9 | make_path_absolute 10 | ) 11 | 12 | TAG = "LaunchPlanLoader:" 13 | 14 | 15 | def init_launch_plan(): 16 | launch_plan_name = _load_launch_plan_name() 17 | launch_manifest = _load_launch_plan_manifest() 18 | launch_plan = _load_launch_plan(launch_manifest, launch_plan_name) 19 | 20 | _load_launch_plan_to_global_settings(launch_plan) 21 | 22 | 23 | def _load_launch_plan_to_global_settings(launch_plan): 24 | avd_set = ArgLoader.get_arg_loaded_by(ArgLoader.AVD_SET_PREFIX) 25 | is_avd_session_requested = avd_set is not None and avd_set != ArgLoader.AVD_SET_DEFAULT 26 | 27 | Printer.system_message(TAG, "General:") 28 | general_settings = launch_plan.general 29 | 30 | if is_avd_session_requested: 31 | GlobalConfig.SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION = True 32 | else: 33 | GlobalConfig.SHOULD_USE_ONLY_DEVICES_SPAWNED_IN_SESSION = False 34 | 35 | GlobalConfig.ADB_CALL_BUFFER_SIZE = general_settings.adb_call_buffer_size 36 | if GlobalConfig.ADB_CALL_BUFFER_SIZE > 0: 37 | Printer.system_message(TAG, " * ADB call buffer size set to: " + Color.GREEN + 38 | str(GlobalConfig.ADB_CALL_BUFFER_SIZE) + " slot(s)" + Color.BLUE + ".") 39 | else: 40 | message = "ADB_CALL_BUFFER_SIZE cannot be smaller than 1. Launcher will quit." 41 | raise LauncherFlowInterruptedException(TAG, message) 42 | 43 | GlobalConfig.ADB_CALL_BUFFER_DELAY_BETWEEN_CMD = general_settings.adb_call_buffer_delay_between_cmd 44 | if GlobalConfig.ADB_CALL_BUFFER_DELAY_BETWEEN_CMD >= 0: 45 | if GlobalConfig.ADB_CALL_BUFFER_DELAY_BETWEEN_CMD == 0: 46 | Printer.system_message(TAG, " * ADB call buffer is disabled. ADB_CALL_BUFFER_DELAY_BETWEEN_CMD" 47 | " param set to: " + + Color.GREEN + "0 second(s)" + + Color.BLUE + ".") 48 | else: 49 | Printer.system_message(TAG, " * ADB call buffer will clear slots after " + Color.GREEN + 50 | str(GlobalConfig.ADB_CALL_BUFFER_DELAY_BETWEEN_CMD / 1000) + " second(s)" + 51 | Color.BLUE + " from ADB call.") 52 | else: 53 | message = "ADB_CALL_BUFFER_DELAY_BETWEEN_CMD cannot be negative! Launcher will quit." 54 | raise LauncherFlowInterruptedException(TAG, message) 55 | 56 | if is_avd_session_requested: 57 | Printer.system_message(TAG, "Device preparation phase settings:") 58 | device_prep_settings = launch_plan.device_preparation_phase 59 | 60 | GlobalConfig.SHOULD_RECREATE_EXISTING_AVD = device_prep_settings.avd_should_recreate_existing 61 | if GlobalConfig.SHOULD_RECREATE_EXISTING_AVD: 62 | Printer.system_message(TAG, " * If requested AVD already exists - it will be" + Color.GREEN + 63 | " recreated from scratch" + Color.BLUE + ".") 64 | else: 65 | Printer.system_message(TAG, " * If requested AVD already exists - it will be" + Color.GREEN + 66 | " reused" + Color.BLUE + ".") 67 | 68 | Printer.system_message(TAG, "Device launching phase settings:") 69 | device_launch_settings = launch_plan.device_launching_phase 70 | 71 | GlobalConfig.IGNORED_DEVICE_LIST = device_launch_settings.device_android_id_to_ignore 72 | Printer.system_message(TAG, " * Devices with following Android-IDs will be ignored: " + Color.GREEN + 73 | str(GlobalConfig.IGNORED_DEVICE_LIST) + Color.BLUE + ".") 74 | 75 | if is_avd_session_requested: 76 | GlobalConfig.SHOULD_LAUNCH_AVD_SEQUENTIALLY = device_launch_settings.avd_launch_sequentially 77 | if GlobalConfig.SHOULD_LAUNCH_AVD_SEQUENTIALLY: 78 | Printer.system_message(TAG, 79 | " * AVD will be launched " + Color.GREEN + "one by one" + Color.BLUE + 80 | ". Wait for start will be performed for each AVD separately and it will take more" 81 | " time.") 82 | else: 83 | Printer.system_message(TAG, " * AVD will be launched " + Color.GREEN + "all at once" + Color.BLUE + ".") 84 | Printer.error(TAG, "Warning: when launching AVD simultaneously ADB is unaware of the amount of memory" 85 | " that specific AVD will use. If there is not enough memory in the system and you launch" 86 | " too many AVD at the same time your PC might turn off due to lack of RAM memory.") 87 | 88 | GlobalConfig.ADB_SCAN_INTERVAL = device_launch_settings.avd_status_scan_interval_millis 89 | if GlobalConfig.ADB_SCAN_INTERVAL is "": 90 | message = " * ADB_SCAN_INTERVAL not specified in LaunchManifest. Launcher will quit." 91 | raise LauncherFlowInterruptedException(TAG, message) 92 | else: 93 | Printer.system_message(TAG, " * ADB will be scanned with interval of " + Color.GREEN + 94 | str(GlobalConfig.ADB_SCAN_INTERVAL / 1000) + " second(s)" + Color.BLUE + ".") 95 | 96 | GlobalConfig.AVD_ADB_BOOT_TIMEOUT = device_launch_settings.avd_wait_for_adb_boot_timeout_millis 97 | if GlobalConfig.AVD_ADB_BOOT_TIMEOUT is "": 98 | message = " * AVD_ADB_BOOT_TIMEOUT not specified in LaunchManifest. Launcher will quit." 99 | raise LauncherFlowInterruptedException(TAG, message) 100 | else: 101 | Printer.system_message(TAG, " * AVD - ADB boot timeout set to " + Color.GREEN + 102 | str(GlobalConfig.AVD_ADB_BOOT_TIMEOUT / 1000) + " second(s)" + Color.BLUE + ".") 103 | 104 | GlobalConfig.AVD_SYSTEM_BOOT_TIMEOUT = device_launch_settings.avd_wait_for_system_boot_timeout_millis 105 | if GlobalConfig.AVD_SYSTEM_BOOT_TIMEOUT is "": 106 | message = " * AVD_SYSTEM_BOOT_TIMEOUT not specified in LaunchManifest. Launcher will quit." 107 | raise LauncherFlowInterruptedException(TAG, message) 108 | else: 109 | Printer.system_message(TAG, " * AVD - ADB system boot timeout set to " + Color.GREEN + 110 | str(GlobalConfig.AVD_SYSTEM_BOOT_TIMEOUT / 1000) + " second(s)" + Color.BLUE + ".") 111 | 112 | GlobalConfig.SHOULD_RESTART_ADB = device_launch_settings.device_before_launching_restart_adb 113 | if GlobalConfig.SHOULD_RESTART_ADB: 114 | Printer.system_message(TAG, " * " + Color.GREEN + "ADB will be restarted" + Color.BLUE 115 | + " before launching tests.") 116 | 117 | Printer.system_message(TAG, "Apk preparation phase settings:") 118 | apk_preparation_settings = launch_plan.apk_preparation_phase 119 | 120 | GlobalConfig.SHOULD_BUILD_NEW_APK = apk_preparation_settings.build_new_apk 121 | if GlobalConfig.SHOULD_BUILD_NEW_APK: 122 | Printer.system_message(TAG, " * Launcher will " + Color.GREEN + "build .*apk" + Color.BLUE 123 | + " for tests with commands specified in test set.") 124 | else: 125 | Printer.system_message(TAG, " * Launcher will " + Color.GREEN + "look for existing .*apk" + Color.BLUE 126 | + " look for existing .*apk for tests and try to build only if nothing was found.") 127 | 128 | Printer.system_message(TAG, "Test run phase settings:") 129 | testing_phase = launch_plan.testing_phase 130 | 131 | GlobalConfig.SHOULD_RECORD_TESTS = testing_phase.record_tests 132 | if GlobalConfig.SHOULD_RECORD_TESTS: 133 | Printer.system_message(TAG, " * Launcher will " + Color.GREEN + "record device screens" + Color.BLUE 134 | + " during test session.") 135 | else: 136 | Printer.system_message(TAG, " * Launcher test " + Color.GREEN + "recording is turned off" 137 | + Color.BLUE + ".") 138 | 139 | Printer.system_message(TAG, "Flakiness check phase settings:") 140 | flakiness_check_phase = launch_plan.flakiness_check_phase 141 | 142 | GlobalConfig.SHOULD_RERUN_FAILED_TESTS = flakiness_check_phase.should_rerun_failed_tests 143 | GlobalConfig.FLAKINESS_RERUN_COUNT = flakiness_check_phase.rerun_count 144 | if GlobalConfig.SHOULD_RERUN_FAILED_TESTS: 145 | Printer.system_message(TAG, " * Launcher will " + Color.GREEN + "re-run failed tests {} times".format( 146 | GlobalConfig.FLAKINESS_RERUN_COUNT) + Color.BLUE + " after test session ends.") 147 | else: 148 | Printer.system_message(TAG, " * Launcher test " + Color.GREEN + "re-running option is turned off" 149 | + Color.BLUE + ".") 150 | 151 | 152 | def _load_launch_plan_name(): 153 | launch_plan_name = ArgLoader.get_arg_loaded_by(ArgLoader.LAUNCH_PLAN_PREFIX) 154 | 155 | if launch_plan_name is None: 156 | message = "No launch plan selected. Launcher will quit." 157 | raise LauncherFlowInterruptedException(TAG, message) 158 | else: 159 | Printer.system_message(TAG, "Selected launch plan: " + Color.GREEN + launch_plan_name + Color.BLUE + ".") 160 | return launch_plan_name 161 | 162 | 163 | def _load_launch_plan_manifest(): 164 | launch_manifest_dir = make_path_absolute(ArgLoader.get_manifest_dir(ArgLoader.LAUNCH_MANIFEST_DIR_KEY)) 165 | 166 | if launch_manifest_dir is None: 167 | message = ("LaunchManifest file directory was not found. Check if config_files_dir.json exists in root " 168 | "of project. Otherwise check if it's linking to existing file.") 169 | raise LauncherFlowInterruptedException(TAG, message) 170 | else: 171 | launch_manifest = LaunchManifest(launch_manifest_dir) 172 | Printer.system_message(TAG, "Created LaunchManifest from file: " + Color.GREEN + launch_manifest_dir 173 | + Color.BLUE + ".") 174 | return launch_manifest 175 | 176 | 177 | def _load_launch_plan(launch_manifest, launch_plan_name): 178 | if launch_manifest.contains_plan(launch_plan_name): 179 | Printer.system_message(TAG, "Launch plan " + Color.GREEN + launch_plan_name + Color.BLUE 180 | + " was found in LaunchManifest.") 181 | return launch_manifest.get_plan(launch_plan_name) 182 | else: 183 | message = "Invalid launch plan with name '{}' does not exist in LaunchManifest!" 184 | message = message.format(launch_plan_name) 185 | raise LauncherFlowInterruptedException(TAG, message) 186 | -------------------------------------------------------------------------------- /settings/loader/PathsLoader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from error.Exceptions import LauncherFlowInterruptedException 4 | from settings import GlobalConfig 5 | from settings.loader import ArgLoader 6 | from settings.manifest.models.PathManifestModels import PathManifest 7 | from system.console import ( 8 | Printer, 9 | Color 10 | ) 11 | from system.file.FileUtils import ( 12 | clean_folder_only_dir, 13 | make_path_absolute 14 | ) 15 | 16 | HOME = "~/" 17 | 18 | ANDROID_HOME_ENV = os.getenv('ANDROID_HOME') 19 | ANDROID_SDK_HOME_ENV = os.getenv('ANDROID_SDK_HOME') 20 | 21 | OUTPUT_DIR_DEFAULT = os.path.abspath(os.path.dirname(__name__)) + "/output/" 22 | OUTPUT_SUMMARY_LOG_FOLDER_DEFAULT = "/summary/" 23 | OUTPUT_AVD_LOG_FOLDER_DEFAULT = "/avd_logs/" 24 | OUTPUT_TEST_LOG_FOLDER_DEFAULT = "/test_results/" 25 | OUTPUT_TEST_LOGCAT_FOLDER_DEFAULT = "/test_logcats/" 26 | OUTPUT_TEST_VIDEO_FOLDER_DEFAULT = "/recordings/" 27 | OUTPUT_TEST_LOGCAT_HTML_FOLDER_DEFAULT = "/test_logcats_html/" 28 | OUTPUT_HTML_STYLES_FOLDER_DEFAULT = "/styles" 29 | OUTPUT_HTML_INDEX_FILE_NAME = "index.html" 30 | 31 | LOG_GENERATOR_DIR_DEFAULT = os.path.abspath(os.path.dirname(__name__)) + "/log_generator/" 32 | 33 | DEVICE_VIDEO_STORAGE_FOLDER_DEFAULT = "/sdcard/test_automation_recordings/" 34 | 35 | TAG = "PathsLoader:" 36 | 37 | 38 | def init_paths(): 39 | _display_manifest_source_info() 40 | 41 | path_set_name = _load_path_set_name() 42 | path_manifest = _load_path_manifest() 43 | path_set = _load_path_set(path_manifest, path_set_name) 44 | 45 | _load_paths_to_global_settings(path_set) 46 | 47 | 48 | def _display_manifest_source_info(): 49 | Printer.system_message(TAG, 50 | "File used for locating manifest files: " + Color.GREEN 51 | + ArgLoader.CONFIG_FILES_DIR_DEFAULT_DIR + Color.BLUE + ".") 52 | 53 | 54 | def _load_path_set_name(): 55 | path_set_name = ArgLoader.get_arg_loaded_by(ArgLoader.PATH_SET_PREFIX) 56 | if path_set_name is None: 57 | message = "No path set was selected. Launcher will quit." 58 | raise LauncherFlowInterruptedException(TAG, message) 59 | else: 60 | Printer.system_message(TAG, "Selected path set: " + Color.GREEN + path_set_name + Color.BLUE + ".") 61 | return path_set_name 62 | 63 | 64 | def _load_path_manifest(): 65 | path_manifest_dir = make_path_absolute(ArgLoader.get_manifest_dir(ArgLoader.PATH_MANIFEST_DIR_KEY)) 66 | 67 | if path_manifest_dir is None: 68 | message = ("PathManifest file directory was not found. Check if config_files_dir.json exists in root " 69 | "of project. Otherwise check if it's linking to existing file.") 70 | raise LauncherFlowInterruptedException(TAG, message) 71 | else: 72 | path_manifest = PathManifest(path_manifest_dir) 73 | Printer.system_message(TAG, "Created PathManifest from file: " + Color.GREEN + path_manifest_dir + Color.BLUE 74 | + ".") 75 | return path_manifest 76 | 77 | 78 | def _load_path_set(path_manifest, path_set_name): 79 | if path_manifest.contains_set(path_set_name): 80 | Printer.system_message(TAG, "Path set " + Color.GREEN + path_set_name + Color.BLUE 81 | + " was found in PathManifest.") 82 | return path_manifest.get_set(path_set_name) 83 | else: 84 | message = "Invalid path set with name '{}' does not exist in PathManifest!" 85 | message = message.format(path_set_name) 86 | raise LauncherFlowInterruptedException(TAG, message) 87 | 88 | 89 | def _load_paths_to_global_settings(path_set): 90 | GlobalConfig.SDK_DIR = clean_folder_only_dir((path_set.paths["sdk_dir"]).path_value) 91 | if GlobalConfig.SDK_DIR == "": 92 | Printer.system_message(TAG, "SDK path not set in PathManifest. Will use path set in env variable " 93 | + Color.GREEN + "ANDROID_HOME" + Color.BLUE + ".") 94 | if ANDROID_HOME_ENV is None: 95 | message = "Env variable 'ANDROID_HOME' is not set. Launcher will quit." 96 | raise LauncherFlowInterruptedException(TAG, message) 97 | else: 98 | GlobalConfig.SDK_DIR = clean_folder_only_dir(make_path_absolute(ANDROID_HOME_ENV)) 99 | Printer.system_message(TAG, "Launcher will look for SDK in dir: " + Color.GREEN + GlobalConfig.SDK_DIR 100 | + Color.BLUE + ".") 101 | 102 | GlobalConfig.AVD_DIR = clean_folder_only_dir((path_set.paths["avd_dir"]).path_value) 103 | if GlobalConfig.AVD_DIR == "": 104 | Printer.system_message(TAG, "AVD path not set in PathManifest. " 105 | "Will use path set in env variable 'ANDROID_SDK_HOME'.") 106 | if ANDROID_SDK_HOME_ENV is None: 107 | Printer.system_message(TAG, "Env variable 'ANDROID_SDK_HOME' is not set. " 108 | "Trying to recreate default path from user root.") 109 | GlobalConfig.AVD_DIR = clean_folder_only_dir(make_path_absolute(HOME)) + ".android" 110 | Printer.system_message(TAG, "Launcher will look for AVD images in dir: " + Color.GREEN + GlobalConfig.AVD_DIR 111 | + Color.BLUE + ".") 112 | 113 | GlobalConfig.PROJECT_ROOT_DIR = clean_folder_only_dir( 114 | make_path_absolute((path_set.paths["project_root_dir"]).path_value)) 115 | if GlobalConfig.PROJECT_ROOT_DIR == "": 116 | Printer.system_message(TAG, "Project root was not specified. This field is not obligatory.") 117 | Printer.error(TAG, "Warning: Without project root directory launcher will quit if no " 118 | ".*apk files will be found in directory loaded from 'apk_dir' field of PathManifest.") 119 | else: 120 | Printer.system_message(TAG, "Android project root dir set to: " + Color.GREEN + GlobalConfig.PROJECT_ROOT_DIR 121 | + Color.BLUE + ".") 122 | 123 | GlobalConfig.APK_DIR = clean_folder_only_dir(make_path_absolute((path_set.paths["apk_dir"]).path_value)) 124 | if GlobalConfig.APK_DIR == "": 125 | message = "Directory with .*apk files was not specified. Launcher will quit." 126 | raise LauncherFlowInterruptedException(TAG, message) 127 | Printer.system_message(TAG, "Launcher will look for .*apk files in dir: " + Color.GREEN + GlobalConfig.APK_DIR 128 | + Color.BLUE + ".") 129 | 130 | GlobalConfig.OUTPUT_DIR = clean_folder_only_dir((path_set.paths["output_dir"]).path_value) 131 | if GlobalConfig.OUTPUT_DIR == "": 132 | Printer.system_message(TAG, "Output path not set in PathManifest. Default value will be used.") 133 | GlobalConfig.OUTPUT_DIR = OUTPUT_DIR_DEFAULT 134 | if not os.path.isabs(GlobalConfig.OUTPUT_DIR): 135 | message = "Path " + GlobalConfig.OUTPUT_DIR + " needs to be absolute!" 136 | raise LauncherFlowInterruptedException(TAG, message) 137 | Printer.system_message(TAG, "Launcher will generate log from tests in dir: " + Color.GREEN + 138 | GlobalConfig.OUTPUT_DIR + Color.BLUE + ".") 139 | 140 | GlobalConfig.OUTPUT_SUMMARY_LOG_DIR = clean_folder_only_dir( 141 | GlobalConfig.OUTPUT_DIR + OUTPUT_SUMMARY_LOG_FOLDER_DEFAULT) 142 | if not os.path.isabs(GlobalConfig.OUTPUT_SUMMARY_LOG_DIR): 143 | message = "Path " + GlobalConfig.OUTPUT_SUMMARY_LOG_DIR + " needs to be absolute!" 144 | raise LauncherFlowInterruptedException(TAG, message) 145 | Printer.system_message(TAG, 146 | "Summary log will be stored in dir: " + Color.GREEN + GlobalConfig.OUTPUT_SUMMARY_LOG_DIR 147 | + Color.BLUE + ".") 148 | 149 | GlobalConfig.OUTPUT_AVD_LOG_DIR = clean_folder_only_dir(GlobalConfig.OUTPUT_DIR + OUTPUT_AVD_LOG_FOLDER_DEFAULT) 150 | if not os.path.isabs(GlobalConfig.OUTPUT_AVD_LOG_DIR): 151 | message = "Path " + GlobalConfig.OUTPUT_AVD_LOG_DIR + " needs to be absolute!" 152 | raise LauncherFlowInterruptedException(TAG, message) 153 | Printer.system_message(TAG, "Logs from AVD will be stored in dir: " + Color.GREEN + GlobalConfig.OUTPUT_AVD_LOG_DIR 154 | + Color.BLUE + ".") 155 | 156 | GlobalConfig.OUTPUT_TEST_LOG_DIR = clean_folder_only_dir(GlobalConfig.OUTPUT_DIR + OUTPUT_TEST_LOG_FOLDER_DEFAULT) 157 | if not os.path.isabs(GlobalConfig.OUTPUT_TEST_LOG_DIR): 158 | message = "Path " + GlobalConfig.OUTPUT_TEST_LOG_DIR + " needs to be absolute!" 159 | raise LauncherFlowInterruptedException(TAG, message) 160 | Printer.system_message(TAG, "Logs from tests will be stored in dir: " + Color.GREEN + 161 | GlobalConfig.OUTPUT_TEST_LOG_DIR + Color.BLUE + ".") 162 | 163 | GlobalConfig.OUTPUT_TEST_LOGCAT_DIR = clean_folder_only_dir( 164 | GlobalConfig.OUTPUT_DIR + OUTPUT_TEST_LOGCAT_FOLDER_DEFAULT) 165 | if not os.path.isabs(GlobalConfig.OUTPUT_TEST_LOGCAT_DIR): 166 | message = "Path " + GlobalConfig.OUTPUT_TEST_LOGCAT_DIR + " needs to be absolute!" 167 | raise LauncherFlowInterruptedException(TAG, message) 168 | Printer.system_message(TAG, "Logcat logs from tests will be stored in dir: " + Color.GREEN + 169 | GlobalConfig.OUTPUT_TEST_LOGCAT_DIR + Color.BLUE + ".") 170 | 171 | GlobalConfig.DEVICE_VIDEO_STORAGE_DIR = clean_folder_only_dir(DEVICE_VIDEO_STORAGE_FOLDER_DEFAULT) 172 | Printer.system_message(TAG, "Firstly recordings will be saved in root directory of test device storage in " + 173 | Color.GREEN + GlobalConfig.DEVICE_VIDEO_STORAGE_DIR + Color.BLUE + " folder.") 174 | 175 | GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR = clean_folder_only_dir( 176 | GlobalConfig.OUTPUT_DIR + OUTPUT_TEST_VIDEO_FOLDER_DEFAULT) 177 | if not os.path.isabs(GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR): 178 | message = "Path " + GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR + " needs to be absolute!" 179 | raise LauncherFlowInterruptedException(TAG, message) 180 | Printer.system_message(TAG, "Secondly recordings from tests will be pulled from each device to dir: " + Color.GREEN 181 | + GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR + Color.BLUE + ".") 182 | 183 | GlobalConfig.OUTPUT_STYLES_FOLDER_DIR = clean_folder_only_dir( 184 | GlobalConfig.OUTPUT_DIR + OUTPUT_HTML_STYLES_FOLDER_DEFAULT) 185 | if not os.path.isabs(GlobalConfig.OUTPUT_STYLES_FOLDER_DIR): 186 | message = "Path " + GlobalConfig.OUTPUT_STYLES_FOLDER_DIR + " needs to be absolute!" 187 | raise LauncherFlowInterruptedException(TAG, message) 188 | Printer.system_message(TAG, "Styles for all html logs will be stored in dir: " + Color.GREEN 189 | + GlobalConfig.OUTPUT_STYLES_FOLDER_DIR + Color.BLUE + ".") 190 | 191 | GlobalConfig.OUTPUT_LOGCAT_HTML_DIR = clean_folder_only_dir( 192 | GlobalConfig.OUTPUT_DIR + OUTPUT_TEST_LOGCAT_HTML_FOLDER_DEFAULT) 193 | if not os.path.isabs(GlobalConfig.OUTPUT_LOGCAT_HTML_DIR): 194 | message = "Path " + GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR + " needs to be absolute!" 195 | raise LauncherFlowInterruptedException(TAG, message) 196 | Printer.system_message(TAG, "Html logs presenting logcats from devices will be stored in dir: " + Color.GREEN 197 | + GlobalConfig.OUTPUT_LOGCAT_HTML_DIR + Color.BLUE + ".") 198 | 199 | GlobalConfig.OUTPUT_INDEX_HTML_DIR = clean_folder_only_dir(GlobalConfig.OUTPUT_DIR) + OUTPUT_HTML_INDEX_FILE_NAME 200 | if not os.path.isabs(GlobalConfig.OUTPUT_INDEX_HTML_DIR): 201 | message = "Path " + GlobalConfig.OUTPUT_TEST_RECORDINGS_DIR + " needs to be absolute!" 202 | raise LauncherFlowInterruptedException(TAG, message) 203 | Printer.system_message(TAG, "All html logs containing results from tests, logcats and videos can be accessed from " 204 | + Color.GREEN + GlobalConfig.OUTPUT_INDEX_HTML_DIR + Color.BLUE + " file generated after " 205 | + "session ends.") 206 | 207 | GlobalConfig.LOG_GENERATOR_DIR = clean_folder_only_dir(LOG_GENERATOR_DIR_DEFAULT) 208 | if not os.path.isabs(GlobalConfig.LOG_GENERATOR_DIR): 209 | message = "Path " + GlobalConfig.LOG_GENERATOR_DIR + " needs to be absolute!" 210 | raise LauncherFlowInterruptedException(TAG, message) 211 | Printer.system_message(TAG, "Logs will be generated with usage of " + Color.GREEN + "LogGenerator.py" + Color.BLUE + 212 | " file stored in dir: " + Color.GREEN + GlobalConfig.LOG_GENERATOR_DIR + Color.BLUE + ".") 213 | -------------------------------------------------------------------------------- /settings/loader/TestSetLoader.py: -------------------------------------------------------------------------------- 1 | from error.Exceptions import LauncherFlowInterruptedException 2 | 3 | from settings.loader import ArgLoader 4 | from settings.manifest.models.TestManifestModels import TestManifest, TestSet 5 | 6 | from system.console import ( 7 | Printer, 8 | Color 9 | ) 10 | 11 | from system.file.FileUtils import ( 12 | make_path_absolute 13 | ) 14 | 15 | TAG = "TestSetLoader:" 16 | 17 | 18 | def init_test_settings(): 19 | test_set_names = _load_test_set_name() 20 | test_manifest = _load_manifest() 21 | test_list = _load_test_list(test_manifest) 22 | test_set = _load_test_set(test_manifest, test_set_names) 23 | 24 | return test_set, test_list 25 | 26 | 27 | def _load_test_set_name(): 28 | test_set_names = ArgLoader.get_arg_loaded_by(ArgLoader.TEST_SET_PREFIX) 29 | if test_set_names is None or len(test_set_names) == 0: 30 | message = "No test set inserted. Launcher will quit." 31 | raise LauncherFlowInterruptedException(TAG, message) 32 | else: 33 | Printer.system_message(TAG, "Selected test sets: ") 34 | for t_set_name in test_set_names: 35 | Printer.system_message(TAG, " * " + Color.GREEN + t_set_name + Color.BLUE) 36 | return test_set_names 37 | 38 | 39 | def _load_manifest(): 40 | test_manifest_dir = make_path_absolute(ArgLoader.get_manifest_dir(ArgLoader.TEST_MANIFEST_DIR_KEY)) 41 | 42 | if test_manifest_dir is None: 43 | message = ("TestManifest file directory was not found. Check if config_files_dir.json exists in root " 44 | "of project. Otherwise check if it's linking to existing file.") 45 | raise LauncherFlowInterruptedException(TAG, message) 46 | else: 47 | test_manifest = TestManifest(test_manifest_dir) 48 | Printer.system_message(TAG, "Created TestManifest from file: " + Color.GREEN + test_manifest_dir + Color.BLUE 49 | + ".") 50 | return test_manifest 51 | 52 | 53 | def _load_test_list(test_manifest): 54 | if test_manifest.test_package_list: 55 | return test_manifest.test_package_list 56 | else: 57 | message = "There are no tests specified in TestManifest! Launcher will quit." 58 | raise LauncherFlowInterruptedException(TAG, message) 59 | 60 | 61 | def _check_if_packages_exists(test_manifest, test_set_names): 62 | found_all_packages = True 63 | errors = "" 64 | for test_set_name in test_set_names: 65 | if test_manifest.contains_set(test_set_name): 66 | Printer.system_message(TAG, "Test set " + Color.GREEN + test_set_name + Color.BLUE 67 | + " was found in TestManifest. Contains following package names:") 68 | 69 | test_set = test_manifest.get_set(test_set_name) 70 | for package_name in test_set.set_package_names: 71 | Printer.system_message(TAG, " * " + Color.GREEN + package_name + Color.BLUE) 72 | 73 | for package_name in test_set.set_package_names: 74 | if not test_manifest.contains_package(package_name): 75 | found_all_packages = False 76 | errors += "\n - Test package '" + package_name + "' was not found in TestManifest!" 77 | else: 78 | message = "Test set '{}' not found in TestManifest. Launcher will quit." 79 | message = message.format(test_set_name) 80 | raise LauncherFlowInterruptedException(TAG, message) 81 | if found_all_packages: 82 | Printer.system_message(TAG, "All test packages were found in TestManifest.") 83 | else: 84 | raise LauncherFlowInterruptedException(TAG, errors) 85 | 86 | 87 | def _pick_test_set(test_manifest, test_set_names): 88 | if len(test_set_names) > 1: 89 | Printer.system_message(TAG, "There were " + Color.GREEN + "{}".format(len(test_set_names)) + Color.BLUE 90 | + " test sets passed. Merge will occur now.") 91 | 92 | test_set_dict = dict() 93 | test_set_dict["set_name"] = "merged" 94 | test_set_dict["apk_name_part"] = None 95 | test_set_dict["application_apk_assemble_task"] = None 96 | test_set_dict["test_apk_assemble_task"] = None 97 | test_set_dict["gradle_build_params"] = None 98 | test_set_dict["shard"] = None 99 | test_set_dict["set_package_names"] = set() 100 | 101 | config_compatible = True 102 | errors_tuples = set() 103 | 104 | for test_set_name in test_set_names: 105 | test_set = test_manifest.get_set(test_set_name) 106 | for other_test_set_name in test_set_names: 107 | other_test_set = test_manifest.get_set(other_test_set_name) 108 | 109 | if test_set.apk_name_part != other_test_set.apk_name_part: 110 | config_compatible = False 111 | errors_tuples.add((test_set_name, other_test_set_name, "apk_name_part")) 112 | else: 113 | test_set_dict["apk_name_part"] = test_set.apk_name_part 114 | 115 | if test_set.application_apk_assemble_task != other_test_set.application_apk_assemble_task: 116 | config_compatible = False 117 | errors_tuples.add((test_set_name, other_test_set_name, "application_apk_assemble_task")) 118 | else: 119 | test_set_dict["application_apk_assemble_task"] = test_set.application_apk_assemble_task 120 | 121 | if test_set.test_apk_assemble_task != other_test_set.test_apk_assemble_task: 122 | config_compatible = False 123 | errors_tuples.add((test_set_name, other_test_set_name, "test_apk_assemble_task")) 124 | else: 125 | test_set_dict["test_apk_assemble_task"] = test_set.test_apk_assemble_task 126 | 127 | if test_set.gradle_build_params != other_test_set.gradle_build_params: 128 | config_compatible = False 129 | errors_tuples.add((test_set_name, other_test_set_name, "gradle_build_params")) 130 | else: 131 | test_set_dict["gradle_build_params"] = test_set.gradle_build_params 132 | 133 | if test_set.shard != other_test_set.shard: 134 | config_compatible = False 135 | errors_tuples.add((test_set_name, other_test_set_name, "shard")) 136 | else: 137 | test_set_dict["shard"] = test_set.shard 138 | 139 | for package in test_set.set_package_names: 140 | test_set_dict["set_package_names"].add(package) 141 | 142 | if config_compatible: 143 | Printer.system_message(TAG, 144 | "All tests sets are compatible and were successfully merged. Including packages:") 145 | 146 | merged_test_set = TestSet(test_set_dict) 147 | for package_name in merged_test_set.set_package_names: 148 | Printer.system_message(TAG, " * " + Color.GREEN + package_name + Color.BLUE) 149 | 150 | return merged_test_set 151 | else: 152 | error = "" 153 | for tset1, tset2, parameter_name in errors_tuples: 154 | error += "\n - Test set '{}' and test set '{}' have incompatible ".format(tset1, tset2) \ 155 | + "config (on parameter: {}) and cannot be merged.".format(parameter_name) 156 | raise LauncherFlowInterruptedException(TAG, error) 157 | else: 158 | return test_manifest.get_set(test_set_names[0]) 159 | 160 | def _load_test_set(test_manifest, test_set_names): 161 | _check_if_packages_exists(test_manifest, test_set_names) 162 | return _pick_test_set(test_manifest, test_set_names) 163 | -------------------------------------------------------------------------------- /settings/manifest/models/AvdManifestModels.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from system.file import FileUtils 4 | 5 | 6 | class AvdManifest: 7 | TAG = "AvdManifest:" 8 | 9 | def __init__(self, manifest_dir): 10 | self.avd_manifest_source = FileUtils.load_json(manifest_dir) 11 | self.avd_schema_dict = dict() 12 | self.avd_set_dict = dict() 13 | 14 | for avd_schema in self.avd_manifest_source["avd_schema_list"]: 15 | self.avd_schema_dict.update({avd_schema["avd_name"]: AvdSchema(avd_schema)}) 16 | 17 | for avd_set in self.avd_manifest_source["avd_set_list"]: 18 | self.avd_set_dict.update({avd_set["set_name"]: AvdSet(avd_set)}) 19 | 20 | def contains_set(self, set_name): 21 | set_with_name_found = False 22 | for key in self.avd_set_dict.keys(): 23 | if key == set_name: 24 | set_with_name_found = True 25 | break 26 | return set_with_name_found 27 | 28 | def get_set(self, set_name): 29 | avd_set = None 30 | for key in self.avd_set_dict.keys(): 31 | if key == set_name: 32 | avd_set = self.avd_set_dict[key] 33 | break 34 | return copy.deepcopy(avd_set) 35 | 36 | def contains_schema(self, schema_name): 37 | schema_with_name_found = False 38 | for key in self.avd_schema_dict.keys(): 39 | if key == schema_name: 40 | schema_with_name_found = True 41 | break 42 | return schema_with_name_found 43 | 44 | def get_schema(self, schema_name): 45 | avd_schema = None 46 | for key in self.avd_schema_dict.keys(): 47 | if key == schema_name: 48 | avd_schema = self.avd_schema_dict[key] 49 | break 50 | return copy.deepcopy(avd_schema) 51 | 52 | 53 | class AvdSchema: 54 | def __init__(self, avd_schema_dict): 55 | self.avd_name = avd_schema_dict["avd_name"] 56 | self.create_avd_package = avd_schema_dict["create_avd_package"] 57 | self.create_avd_device = avd_schema_dict["create_device"] 58 | self.create_avd_tag = avd_schema_dict["create_avd_tag"] 59 | self.create_avd_abi = avd_schema_dict["create_avd_abi"] 60 | self.create_avd_hardware_config_filepath = avd_schema_dict["create_avd_hardware_config_filepath"] 61 | self.create_avd_additional_options = avd_schema_dict["create_avd_additional_options"] 62 | self.launch_avd_snapshot_filepath = avd_schema_dict["launch_avd_snapshot_filepath"] 63 | self.launch_avd_launch_binary_name = avd_schema_dict["launch_avd_launch_binary_name"] 64 | self.launch_avd_additional_options = avd_schema_dict["launch_avd_additional_options"] 65 | 66 | 67 | class AvdSet: 68 | def __init__(self, avd_set_dict): 69 | self.set_name = avd_set_dict["set_name"] 70 | self.avd_port_rules = AvdPortRules(avd_set_dict["avd_port_rules"]) 71 | self.avd_list = list() 72 | 73 | for avd in avd_set_dict["avd_list"]: 74 | self.avd_list.append(AvdInSet(avd)) 75 | 76 | 77 | class AvdPortRules: 78 | def __init__(self, avd_port_rules_dict): 79 | self.assign_missing_ports = avd_port_rules_dict["assign_missing_ports"] 80 | self.search_range_min = avd_port_rules_dict["search_range_min"] 81 | self.search_range_max = avd_port_rules_dict["search_range_max"] 82 | self.ports_to_ignore = avd_port_rules_dict["ports_to_ignore"] 83 | self.ports_to_use = avd_port_rules_dict["ports_to_use"] 84 | 85 | 86 | class AvdInSet: 87 | def __init__(self, avd_in_set_dict): 88 | self.avd_name = avd_in_set_dict["avd_name"] 89 | self.instances = avd_in_set_dict["instances"] 90 | -------------------------------------------------------------------------------- /settings/manifest/models/LaunchManifestModels.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from system.file import FileUtils 4 | 5 | 6 | class LaunchManifest: 7 | TAG = "LaunchManifest:" 8 | 9 | def __init__(self, manifest_dir): 10 | self.launch_manifest_source = FileUtils.load_json(manifest_dir) 11 | self.launch_plan_dict = dict() 12 | 13 | for launch_plan in self.launch_manifest_source["launch_plan_list"]: 14 | self.launch_plan_dict.update({launch_plan["plan_name"]: LaunchPlan(launch_plan)}) 15 | 16 | def contains_plan(self, plan_name): 17 | plan_with_name_found = False 18 | for key in self.launch_plan_dict.keys(): 19 | if key == plan_name: 20 | plan_with_name_found = True 21 | break 22 | return plan_with_name_found 23 | 24 | def get_plan(self, plan_name): 25 | launch_plan = None 26 | for key in self.launch_plan_dict.keys(): 27 | if key == plan_name: 28 | launch_plan = self.launch_plan_dict[key] 29 | break 30 | return copy.deepcopy(launch_plan) 31 | 32 | 33 | class LaunchPlan: 34 | def __init__(self, launch_plan_dict): 35 | self.plan_name = launch_plan_dict["plan_name"] 36 | self.general = LaunchGeneralOptions(launch_plan_dict["general"]) 37 | self.device_preparation_phase = DevicePreparationPhaseOptions(launch_plan_dict["device_preparation_phase"]) 38 | self.device_launching_phase = DeviceLaunchingPhaseOptions(launch_plan_dict["device_launching_phase"]) 39 | self.apk_preparation_phase = ApkPreparationPhaseOptions(launch_plan_dict["apk_preparation_phase"]) 40 | self.testing_phase = TestingPhaseOptions(launch_plan_dict["testing_phase"]) 41 | self.flakiness_check_phase = FlakinessCheckPhaseOptions(launch_plan_dict["flakiness_check_phase"]) 42 | 43 | 44 | class LaunchGeneralOptions: 45 | def __init__(self, general_options_dict): 46 | self.adb_call_buffer_size = general_options_dict["adb_call_buffer_size"] 47 | self.adb_call_buffer_delay_between_cmd = general_options_dict["adb_call_buffer_delay_between_cmd"] 48 | 49 | 50 | class DevicePreparationPhaseOptions: 51 | def __init__(self, device_preparation_phase_dict): 52 | self.avd_should_recreate_existing = device_preparation_phase_dict["avd_should_recreate_existing"] 53 | 54 | 55 | class DeviceLaunchingPhaseOptions: 56 | def __init__(self, launching_phase_dict): 57 | self.device_android_id_to_ignore = launching_phase_dict["device_android_id_to_ignore"] 58 | self.avd_launch_sequentially = launching_phase_dict["avd_launch_sequentially"] 59 | self.avd_status_scan_interval_millis = launching_phase_dict["avd_status_scan_interval_millis"] 60 | self.avd_wait_for_adb_boot_timeout_millis = launching_phase_dict["avd_wait_for_adb_boot_timeout_millis"] 61 | self.avd_wait_for_system_boot_timeout_millis = launching_phase_dict["avd_wait_for_system_boot_timeout_millis"] 62 | self.device_before_launching_restart_adb = launching_phase_dict["device_before_launching_restart_adb"] 63 | 64 | 65 | class ApkPreparationPhaseOptions: 66 | def __init__(self, apk_preparation_phase_dict): 67 | self.build_new_apk = apk_preparation_phase_dict["build_new_apk"] 68 | 69 | 70 | class TestingPhaseOptions: 71 | def __init__(self, testing_phase_dict): 72 | self.record_tests = testing_phase_dict["record_tests"] 73 | 74 | 75 | class FlakinessCheckPhaseOptions: 76 | def __init__(self, flakiness_check_phase_dict): 77 | self.should_rerun_failed_tests = flakiness_check_phase_dict["should_rerun_failed_tests"] 78 | self.rerun_count = flakiness_check_phase_dict["rerun_count"] 79 | -------------------------------------------------------------------------------- /settings/manifest/models/PathManifestModels.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from system.file import FileUtils 4 | 5 | 6 | class PathManifest: 7 | TAG = "PathManifest:" 8 | 9 | def __init__(self, manifest_dir): 10 | self.path_manifest_source = FileUtils.load_json(manifest_dir) 11 | self.path_set_list = dict() 12 | for path_set in self.path_manifest_source["path_set_list"]: 13 | self.path_set_list.update({path_set["set_name"]: PathSet(path_set)}) 14 | 15 | def contains_set(self, set_name): 16 | set_with_name_found = False 17 | for key in self.path_set_list.keys(): 18 | if key == set_name: 19 | set_with_name_found = True 20 | break 21 | return set_with_name_found 22 | 23 | def get_set(self, set_name): 24 | path_set = None 25 | for key in self.path_set_list.keys(): 26 | if key == set_name: 27 | path_set = self.path_set_list[key] 28 | break 29 | return copy.deepcopy(path_set) 30 | 31 | 32 | class PathSet: 33 | def __init__(self, path_set_dict): 34 | self.set_name = path_set_dict["set_name"] 35 | self.paths = dict() 36 | 37 | for path in path_set_dict["paths"]: 38 | self.paths.update({path["path_name"]: Path(path)}) 39 | 40 | 41 | class Path: 42 | def __init__(self, path_dict): 43 | self.path_name = path_dict["path_name"] 44 | self.path_value = path_dict["path_value"] 45 | self.path_description = path_dict["path_description"] 46 | -------------------------------------------------------------------------------- /settings/manifest/models/TestManifestModels.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | from system.file import FileUtils 4 | 5 | 6 | class TestManifest: 7 | TAG = "TestManifest:" 8 | 9 | def __init__(self, manifest_dir): 10 | self.path_manifest_source = FileUtils.load_json(manifest_dir) 11 | self.test_package_list = dict() 12 | self.test_set_list = dict() 13 | 14 | for test_package_dict in self.path_manifest_source["test_list"]: 15 | self.test_package_list.update({test_package_dict["test_package_name"]: TestPackage(test_package_dict)}) 16 | 17 | for test_set_dict in self.path_manifest_source["test_set_list"]: 18 | self.test_set_list.update({test_set_dict["set_name"]: TestSet(test_set_dict)}) 19 | 20 | def contains_package(self, package_name): 21 | package_with_name_found = False 22 | for key in self.test_package_list.keys(): 23 | if key == package_name: 24 | package_with_name_found = True 25 | break 26 | return package_with_name_found 27 | 28 | def contains_set(self, set_name): 29 | set_with_name_found = False 30 | for key in self.test_set_list.keys(): 31 | if key == set_name: 32 | set_with_name_found = True 33 | break 34 | return set_with_name_found 35 | 36 | def get_package(self, package_name): 37 | test_package = None 38 | for key in self.test_package_list.keys(): 39 | if key == package_name: 40 | test_package = self.test_package_list[key] 41 | break 42 | return copy.deepcopy(test_package) 43 | 44 | def get_set(self, set_name): 45 | test_set = None 46 | for key in self.test_set_list.keys(): 47 | if key == set_name: 48 | test_set = self.test_set_list[key] 49 | break 50 | return copy.deepcopy(test_set) 51 | 52 | 53 | class TestPackage: 54 | def __init__(self, test_package_dict): 55 | self.test_package_name = test_package_dict["test_package_name"] 56 | self.test_packages = test_package_dict["test_packages"] 57 | self.test_classes = test_package_dict["test_classes"] 58 | self.test_cases = test_package_dict["test_cases"] 59 | 60 | 61 | class TestSet: 62 | def __init__(self, test_set_dict): 63 | self.set_name = test_set_dict["set_name"] 64 | self.apk_name_part = test_set_dict["apk_name_part"] 65 | self.application_apk_assemble_task = test_set_dict["application_apk_assemble_task"] 66 | self.test_apk_assemble_task = test_set_dict["test_apk_assemble_task"] 67 | self.set_package_names = test_set_dict["set_package_names"] 68 | self.gradle_build_params = test_set_dict["gradle_build_params"] 69 | self.shard = test_set_dict["shard"] 70 | -------------------------------------------------------------------------------- /settings/manifest/templates/avdManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "avd_schema_list": [ 3 | { 4 | "avd_name": "", 5 | "create_avd_package": "", 6 | "create_device": "", 7 | "create_avd_tag": "", 8 | "create_avd_abi": "", 9 | "create_avd_additional_options": "", 10 | "create_avd_hardware_config_filepath": "", 11 | "launch_avd_snapshot_filepath": "", 12 | "launch_avd_launch_binary_name": "", 13 | "launch_avd_additional_options": "" 14 | } 15 | ], 16 | "avd_set_list": [ 17 | { 18 | "set_name": "default", 19 | "avd_list": [ 20 | { 21 | "avd_name": "", 22 | "instances": -1 23 | } 24 | ], 25 | "avd_port_rules": { 26 | "assign_missing_ports": true, 27 | "search_range_min": 5556, 28 | "search_range_max": 5580, 29 | "ports_to_ignore": [], 30 | "ports_to_use": [] 31 | } 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /settings/manifest/templates/launchManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "launch_plan_list": [ 3 | { 4 | "plan_name": "default", 5 | "general": { 6 | "adb_call_buffer_size": 3, 7 | "adb_call_buffer_delay_between_cmd": 2000 8 | }, 9 | "device_preparation_phase": { 10 | "avd_should_recreate_existing": true 11 | }, 12 | "apk_preparation_phase": { 13 | "build_new_apk": true 14 | }, 15 | "device_launching_phase": { 16 | "device_android_id_to_ignore": [], 17 | "avd_launch_sequentially": true, 18 | "avd_status_scan_interval_millis": 10000, 19 | "avd_wait_for_adb_boot_timeout_millis": 240000, 20 | "avd_wait_for_system_boot_timeout_millis": 120000, 21 | "device_before_launching_restart_adb": true 22 | }, 23 | "testing_phase": { 24 | "record_tests": true 25 | }, 26 | "flakiness_check_phase": { 27 | "should_rerun_failed_tests": true, 28 | "rerun_count": 3 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /settings/manifest/templates/pathManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "path_set_list": [ 3 | { 4 | "set_name": "default", 5 | "paths": [ 6 | { 7 | "path_name": "sdk_dir", 8 | "path_value": "", 9 | "path_description": "Path to Android Software Development kit. By default stored in ANDROID_HOME env." 10 | }, 11 | { 12 | "path_name": "avd_dir", 13 | "path_value": "", 14 | "path_description": "Location to .android folder where AVD images are stored. By default stored in ANDROID_SDK_HOME env." 15 | }, 16 | { 17 | "path_name": "output_dir", 18 | "path_value": "", 19 | "path_description": "Path to where test process logs will be saved. By default ./output/." 20 | }, 21 | { 22 | "path_name": "project_root_dir", 23 | "path_value": "", 24 | "path_description": "Path to root of application under test. (optional)" 25 | }, 26 | { 27 | "path_name": "apk_dir", 28 | "path_value": "", 29 | "path_description": "Directory where .apk files of application under test are stored." 30 | } 31 | ] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /settings/manifest/templates/testManifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_list": [ 3 | { 4 | "test_package_name": "", 5 | "test_packages": [], 6 | "test_classes": [], 7 | "test_cases": [] 8 | } 9 | ], 10 | "test_set_list": [ 11 | { 12 | "set_name": "default", 13 | "apk_name_part": "", 14 | "application_apk_assemble_task": "", 15 | "test_apk_assemble_task": "", 16 | "gradle_build_params": "", 17 | "shard": false, 18 | "set_package_names": [] 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /system/bin/AndroidBinaryFileControllers.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | 4 | from error.Exceptions import LauncherFlowInterruptedException 5 | 6 | from settings import GlobalConfig 7 | 8 | from system.bin.AndroidShellCommandAssemblers import ( 9 | AaptCommandAssembler, 10 | AdbCommandAssembler, 11 | AdbShellCommandAssembler, 12 | AdbPackageManagerCommandAssembler, 13 | AdbSettingsCommandAssembler, 14 | AdbLogCatCommandAssembler, 15 | AvdManagerCommandAssembler, 16 | EmulatorCommandAssembler, 17 | GradleCommandAssembler, 18 | InstrumentationRunnerCommandAssembler 19 | 20 | ) 21 | from system.console import ( 22 | Printer, 23 | ShellHelper, 24 | Color 25 | ) 26 | from system.file.FileUtils import ( 27 | clean_path, 28 | add_ending_slash, 29 | list_files_in_dir 30 | ) 31 | 32 | 33 | class AaptController: 34 | TAG = "AaptController:" 35 | 36 | aapt_bin = None 37 | 38 | def __init__(self): 39 | build_tools = self._find_latest_build_tools() 40 | self.aapt_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "build-tools/" + build_tools + "/aapt") 41 | self._assert_bin_directory_exists() 42 | self.aapt_command_assembler = AaptCommandAssembler() 43 | 44 | def _find_latest_build_tools(self): 45 | build_tools = list_files_in_dir(clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "build-tools")) 46 | build_tools = [build_tool for build_tool in build_tools if build_tool[0].isdigit()] 47 | build_tools_folder_with_highest_ver = None 48 | Printer.system_message(self.TAG, "Available Android SDK Build-Tools versions: " + str(build_tools)) 49 | for build_tools_folder in build_tools: 50 | if build_tools_folder_with_highest_ver is None: 51 | build_tools_folder_with_highest_ver = build_tools_folder 52 | continue 53 | 54 | ver = int(re.sub("[^0-9]", "", build_tools_folder)) 55 | highest_ver = int(re.sub("[^0-9]", "", build_tools_folder_with_highest_ver)) 56 | 57 | if ver > highest_ver: 58 | build_tools_folder_with_highest_ver = build_tools_folder 59 | if build_tools_folder_with_highest_ver is None: 60 | message = "Android SDK Build-Tools not found. Launcher will quit." 61 | raise LauncherFlowInterruptedException(self.TAG, message) 62 | 63 | else: 64 | Printer.system_message(self.TAG, "Android SDK Build-Tools with latest version were selected: " 65 | + Color.GREEN + str(build_tools_folder_with_highest_ver) + Color.BLUE + ".") 66 | return build_tools_folder_with_highest_ver 67 | 68 | def _assert_bin_directory_exists(self): 69 | if os.path.isfile(self.aapt_bin): 70 | Printer.system_message(self.TAG, "Aapt binary file found at: " + Color.GREEN + self.aapt_bin 71 | + Color.BLUE + ".") 72 | else: 73 | message = "Unable to find Aapt binary at '{}'." 74 | message = message.format(self.aapt_bin) 75 | raise LauncherFlowInterruptedException(self.TAG, message) 76 | 77 | def dump_badging(self, apk_filepath): 78 | cmd = self.aapt_command_assembler.assemble_dump_badging_cmd(self.aapt_bin, apk_filepath) 79 | return ShellHelper.execute_shell(cmd, False, False) 80 | 81 | def list_resources(self, apk_filepath): 82 | cmd = self.aapt_command_assembler.assemble_list_all_cmd(self.aapt_bin, apk_filepath) 83 | return ShellHelper.execute_shell(cmd, False, False) 84 | 85 | 86 | class AdbController: 87 | TAG = "AdbController:" 88 | 89 | adb_bin = None 90 | 91 | def __init__(self): 92 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 93 | self._assert_bin_directory_exists() 94 | self.adb_command_assembler = AdbCommandAssembler() 95 | 96 | def _assert_bin_directory_exists(self): 97 | if os.path.isfile(self.adb_bin): 98 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 99 | + ".") 100 | else: 101 | message = "Unable to find ADB binary at '{}'." 102 | message = message.format(self.adb_bin) 103 | raise LauncherFlowInterruptedException(self.TAG, message) 104 | 105 | def start_server(self): 106 | cmd = self.adb_command_assembler.assemble_start_server_cmd(self.adb_bin) 107 | return ShellHelper.execute_shell(cmd, True, True) 108 | 109 | def kill_server(self): 110 | cmd = self.adb_command_assembler.assemble_kill_server_cmd(self.adb_bin) 111 | return ShellHelper.execute_shell(cmd, True, True) 112 | 113 | def devices(self): 114 | cmd = self.adb_command_assembler.assemble_devices_cmd(self.adb_bin) 115 | return ShellHelper.execute_shell(cmd, False, False) 116 | 117 | def wait_for_device(self): 118 | cmd = self.adb_command_assembler.assemble_wait_for_device_cmd(self.adb_bin) 119 | return ShellHelper.execute_shell(cmd, False, False) 120 | 121 | def kill_device(self, device_adb_name): 122 | cmd = self.adb_command_assembler.assemble_kill_device_cmd(self.adb_bin, device_adb_name) 123 | return ShellHelper.execute_shell(cmd, True, False) 124 | 125 | def install_apk(self, device_adb_name, apk_name): 126 | cmd = self.adb_command_assembler.assemble_install_apk_cmd(self.adb_bin, device_adb_name, apk_name) 127 | return ShellHelper.execute_shell(cmd, True, False) 128 | 129 | def pull(self, device_adb_name, file_dir, file_destination): 130 | cmd = self.adb_command_assembler.assemble_pull_file_cmd(self.adb_bin, 131 | device_adb_name, 132 | file_dir, 133 | file_destination) 134 | return ShellHelper.execute_shell(cmd, False, False) 135 | 136 | 137 | class AdbShellController: 138 | TAG = "AdbShellController:" 139 | 140 | adb_bin = None 141 | 142 | def __init__(self): 143 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 144 | self._assert_bin_directory_exists() 145 | self.adb_shell_command_assembler = AdbShellCommandAssembler() 146 | 147 | def _assert_bin_directory_exists(self): 148 | if os.path.isfile(self.adb_bin): 149 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 150 | + ".") 151 | else: 152 | message = "Unable to find ADB binary at '{}'." 153 | message = message.format(self.adb_bin) 154 | raise LauncherFlowInterruptedException(self.TAG, message) 155 | 156 | def get_property(self, device_adb_name, device_property): 157 | cmd = self.adb_shell_command_assembler.assemble_get_property_cmd(self.adb_bin, device_adb_name, device_property) 158 | return ShellHelper.execute_shell(cmd, False, False) 159 | 160 | def record_screen(self, device_adb_name, file_dir): 161 | cmd = self.adb_shell_command_assembler.assemble_record_screen_cmd(self.adb_bin, device_adb_name, file_dir) 162 | return ShellHelper.execute_shell(cmd, False, False) 163 | 164 | def remove_file(self, device_adb_name, file_dir): 165 | cmd = self.adb_shell_command_assembler.assemble_remove_file_cmd(self.adb_bin, device_adb_name, file_dir) 166 | return ShellHelper.execute_shell(cmd, True, True) 167 | 168 | def remove_files_in_dir(self, device_adb_name, file_dir): 169 | cmd = self.adb_shell_command_assembler.assemble_remove_files_in_dir_cmd(self.adb_bin, device_adb_name, file_dir) 170 | return ShellHelper.execute_shell(cmd, True, True) 171 | 172 | def create_dir(self, device_adb_name, file_dir): 173 | cmd = self.adb_shell_command_assembler.assemble_create_dir_cmd(self.adb_bin, device_adb_name, file_dir) 174 | return ShellHelper.execute_shell(cmd, True, True) 175 | 176 | def check_for_directory(self, device_adb_name, directory): 177 | cmd = self.adb_shell_command_assembler.assemble_check_if_dir_exists_cmd(self.adb_bin, 178 | device_adb_name, 179 | directory) 180 | return ShellHelper.execute_shell(cmd, True, True) 181 | 182 | 183 | class AdbPackageManagerController: 184 | TAG = "AdbPackageManagerController:" 185 | 186 | adb_bin = None 187 | 188 | def __init__(self): 189 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 190 | self._assert_bin_directory_exists() 191 | self.adb_package_manager_command_assembler = AdbPackageManagerCommandAssembler() 192 | 193 | def _assert_bin_directory_exists(self): 194 | if os.path.isfile(self.adb_bin): 195 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 196 | + ".") 197 | else: 198 | message = "Unable to find ADB binary at '{}'." 199 | message = message.format(self.adb_bin) 200 | raise LauncherFlowInterruptedException(self.TAG, message) 201 | 202 | def get_installed_packages(self, device_adb_name): 203 | cmd = self.adb_package_manager_command_assembler.assemble_list_installed_packages_cmd(self.adb_bin, 204 | device_adb_name) 205 | return ShellHelper.execute_shell(cmd, False, False) 206 | 207 | def uninstall_package(self, device_adb_name, package_name): 208 | cmd = self.adb_package_manager_command_assembler.assemble_uninstall_package_cmd(self.adb_bin, 209 | device_adb_name, 210 | package_name) 211 | return ShellHelper.execute_shell(cmd, True, True) 212 | 213 | def clear_package_cache(self, device_adb_name, package_name): 214 | cmd = self.adb_package_manager_command_assembler.assemble_clear_package_cache_cmd(self.adb_bin, 215 | device_adb_name, 216 | package_name) 217 | return ShellHelper.execute_shell(cmd, True, True) 218 | 219 | 220 | class AdbSettingsController: 221 | TAG = "AdbSettingsController:" 222 | 223 | adb_bin = None 224 | 225 | def __init__(self): 226 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 227 | self._assert_bin_directory_exists() 228 | self.adb_settings_command_assembler = AdbSettingsCommandAssembler() 229 | 230 | def _assert_bin_directory_exists(self): 231 | if os.path.isfile(self.adb_bin): 232 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 233 | + ".") 234 | else: 235 | message = "Unable to find ADB binary at '{}'." 236 | message = message.format(self.adb_bin) 237 | raise LauncherFlowInterruptedException(self.TAG, message) 238 | 239 | def get_device_android_id(self, device_adb_name): 240 | cmd = self.adb_settings_command_assembler.assemble_get_device_android_id_cmd(self.adb_bin, 241 | device_adb_name) 242 | return ShellHelper.execute_shell(cmd, False, False) 243 | 244 | 245 | class AdbLogCatController: 246 | TAG = "AdbLogCatController:" 247 | 248 | def __init__(self): 249 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 250 | self._assert_bin_directory_exists() 251 | self.adb_logcat_command_assembler = AdbLogCatCommandAssembler() 252 | 253 | def _assert_bin_directory_exists(self): 254 | if os.path.isfile(self.adb_bin): 255 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 256 | + ".") 257 | else: 258 | message = "Unable to find ADB binary at '{}'." 259 | message = message.format(self.adb_bin) 260 | raise LauncherFlowInterruptedException(self.TAG, message) 261 | 262 | def flush_logcat(self, device_adb_name): 263 | cmd = self.adb_logcat_command_assembler.assemble_flush_logcat_cmd(self.adb_bin, device_adb_name) 264 | 265 | return ShellHelper.execute_shell(cmd, False, False) 266 | 267 | def read_logcat(self, device_adb_name): 268 | cmd = self.adb_logcat_command_assembler.assemble_dump_logcat_cmd(self.adb_bin, device_adb_name) 269 | 270 | return ShellHelper.execute_shell(cmd, False, False) 271 | 272 | def monitor_logcat(self, device_adb_name): 273 | cmd = self.adb_logcat_command_assembler.monitor_logcat_schema(self.adb_bin, device_adb_name) 274 | 275 | return ShellHelper.execute_shell(cmd, False, False) 276 | 277 | 278 | class AvdManagerController: 279 | TAG = "AvdManagerController:" 280 | 281 | avdmanager_bin = None 282 | 283 | def __init__(self): 284 | self.avdmanager_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "tools/bin/avdmanager") 285 | self._assert_bin_directory_exists() 286 | self.avdmanager_command_assembler = AvdManagerCommandAssembler() 287 | 288 | def _assert_bin_directory_exists(self): 289 | if os.path.isfile(self.avdmanager_bin): 290 | Printer.system_message(self.TAG, "AvdManager binary file found at " + Color.GREEN + self.avdmanager_bin 291 | + Color.BLUE + ".") 292 | else: 293 | message = "Unable to find ADB binary at '{}'." 294 | message = message.format(self.avdmanager_bin) 295 | raise LauncherFlowInterruptedException(self.TAG, message) 296 | 297 | def list_avd(self): 298 | cmd = self.avdmanager_command_assembler.assemble_list_avd_cmd(self.avdmanager_bin) 299 | return ShellHelper.execute_shell(cmd, False, False) 300 | 301 | def delete_avd(self, avd_schema): 302 | cmd = self.avdmanager_command_assembler.assemble_delete_avd_cmd(self.avdmanager_bin, avd_schema) 303 | return ShellHelper.execute_shell(cmd, True, True) 304 | 305 | def create_avd(self, avd_schema): 306 | cmd = self.avdmanager_command_assembler.assemble_create_avd_cmd(self.avdmanager_bin, avd_schema) 307 | return ShellHelper.execute_shell(cmd, True, True) 308 | 309 | 310 | class EmulatorController: 311 | TAG = "EmulatorController:" 312 | 313 | emulator_bin_dict = dict() 314 | 315 | def __init__(self): 316 | self.emulator_bin_dict = self._display_emulator_binaries() 317 | self.emulator_command_assembler = EmulatorCommandAssembler() 318 | 319 | def _display_emulator_binaries(self): 320 | emulator_binaries = dict() 321 | 322 | emulator_dir = clean_path(add_ending_slash(str(GlobalConfig.SDK_DIR)) + "emulator/") 323 | try: 324 | for the_file in list_files_in_dir(emulator_dir): 325 | file_path = os.path.join(emulator_dir, the_file) 326 | if os.path.isfile(file_path) and "emulator" in file_path: 327 | binary_name = re.findall("emulator\/(emulator*.+)", file_path) 328 | 329 | if binary_name: 330 | emulator_binaries[str(binary_name[0])] = file_path 331 | finally: 332 | if len(emulator_binaries) == 0: 333 | message = "Unable to find emulator binary files in direction '{}' of Android SDK." 334 | message = message.format(str(emulator_dir)) 335 | raise LauncherFlowInterruptedException(self.TAG, message) 336 | 337 | else: 338 | Printer.system_message(self.TAG, "Emulator related binary files found in Android SDK:") 339 | for path in emulator_binaries.values(): 340 | Printer.system_message(self.TAG, " * " + Color.GREEN + path + Color.BLUE) 341 | 342 | return emulator_binaries 343 | 344 | def launch_avd(self, avd_schema, port, log_file): 345 | emulator_binary = self.emulator_bin_dict[avd_schema.launch_avd_launch_binary_name] 346 | cmd = self.emulator_command_assembler.assemble_launch_avd_cmd(emulator_binary, avd_schema, port, log_file) 347 | return ShellHelper.execute_shell(cmd, True, False) 348 | 349 | def apply_config_to_avd(self, avd_schema): 350 | config_ini_to_apply_filepath = clean_path(avd_schema.create_avd_hardware_config_filepath) 351 | 352 | real_config_ini_file_path = clean_path( 353 | add_ending_slash(GlobalConfig.AVD_DIR) + add_ending_slash("avd") + add_ending_slash( 354 | avd_schema.avd_name + ".avd") + "config.ini") 355 | 356 | real_config_ini_file = None 357 | config_ini_to_apply_file = None 358 | try: 359 | if os.path.isfile(config_ini_to_apply_filepath): 360 | config_ini_to_apply_file = open(config_ini_to_apply_filepath, "r") 361 | real_config_ini_file = open(real_config_ini_file_path, "w") 362 | real_config_ini_file.seek(0) 363 | real_config_ini_file.truncate() 364 | 365 | for config_line in config_ini_to_apply_file.readlines(): 366 | temp_lane = config_line 367 | if "AvdId=" in config_line: 368 | temp_lane = ("AvdId=" + str(avd_schema.avd_name) + "\n") 369 | if "avd.ini.displayname=" in config_line: 370 | temp_lane = ("avd.ini.displayname=" + str(avd_schema.avd_name) + "\n") 371 | real_config_ini_file.write(temp_lane) 372 | else: 373 | message = "Filepath '" + config_ini_to_apply_filepath + "' not found!" 374 | raise LauncherFlowInterruptedException(self.TAG, message) 375 | finally: 376 | if real_config_ini_file is not None: 377 | real_config_ini_file.close() 378 | if config_ini_to_apply_file is not None: 379 | config_ini_to_apply_file.close() 380 | 381 | return "Config.ini successfully applied" 382 | 383 | 384 | class GradleController: 385 | TAG = "GradleController:" 386 | 387 | gradle_bin = None 388 | 389 | project_root_found = False 390 | gradlew_found = False 391 | 392 | def __init__(self): 393 | self.gradle_bin = GlobalConfig.PROJECT_ROOT_DIR + "gradlew" 394 | self._check_if_gradle_binary_was_found() 395 | self.gradle_command_assembler = GradleCommandAssembler() 396 | 397 | def _check_if_gradle_binary_was_found(self): 398 | self.project_root_found = GlobalConfig.PROJECT_ROOT_DIR != "" and os.path.isdir(GlobalConfig.PROJECT_ROOT_DIR) 399 | self.gradlew_found = os.path.isfile(self.gradle_bin) 400 | if self.project_root_found: 401 | Printer.system_message(self.TAG, "Project root dir " + Color.GREEN + GlobalConfig.PROJECT_ROOT_DIR 402 | + Color.BLUE + " was found! Building new .*apk is possible.") 403 | if self.gradlew_found: 404 | Printer.system_message(self.TAG, "gradlew binary found at " + Color.GREEN + str(self.gradle_bin) 405 | + Color.BLUE + ".") 406 | 407 | def build_application_apk(self, test_set): 408 | assemble_task = test_set.application_apk_assemble_task 409 | self._check_if_build_is_possible(assemble_task) 410 | 411 | cmd = self.gradle_command_assembler.assemble_build_application_apk_cmd(self.gradle_bin, 412 | test_set.gradle_build_params, 413 | assemble_task, 414 | GlobalConfig.PROJECT_ROOT_DIR) 415 | ShellHelper.execute_shell(cmd, True, True) 416 | 417 | def build_test_apk(self, test_set): 418 | assemble_task = test_set.test_apk_assemble_task 419 | self._check_if_build_is_possible(assemble_task) 420 | 421 | cmd = self.gradle_command_assembler.assemble_build_test_apk_cmd(self.gradle_bin, 422 | test_set.gradle_build_params, 423 | assemble_task, 424 | GlobalConfig.PROJECT_ROOT_DIR) 425 | ShellHelper.execute_shell(cmd, True, True) 426 | 427 | def _check_if_build_is_possible(self, cmd): 428 | if cmd == "": 429 | message = "Gradle assemble task (for building .*apk) was not specified in TestManifest. Launcher will quit." 430 | raise LauncherFlowInterruptedException(self.TAG, message) 431 | 432 | if not self.project_root_found: 433 | message = "Unable to build new .*apk. Project root not found. Launcher will quit." 434 | raise LauncherFlowInterruptedException(self.TAG, message) 435 | 436 | if not self.gradlew_found: 437 | message = "Unable to build new .*apk. File 'gradlew' not found in dir '{}'. Launcher will quit." 438 | message = message.format(GlobalConfig.PROJECT_ROOT_DIR) 439 | raise LauncherFlowInterruptedException(self.TAG, message) 440 | 441 | 442 | class InstrumentationRunnerController: 443 | TAG = "InstrumentationRunnerController:" 444 | 445 | adb_bin = None 446 | 447 | def __init__(self): 448 | self.adb_bin = clean_path(add_ending_slash(GlobalConfig.SDK_DIR) + "platform-tools/adb") 449 | self._assert_bin_directory_exists() 450 | self.instrumentation_runner_command_assembler = InstrumentationRunnerCommandAssembler() 451 | 452 | def _assert_bin_directory_exists(self): 453 | if os.path.isfile(self.adb_bin): 454 | Printer.system_message(self.TAG, "ADB binary file found at " + Color.GREEN + self.adb_bin + Color.BLUE 455 | + ".") 456 | else: 457 | message = "Unable to find ADB binary at '{}'." 458 | message = message.format(self.adb_bin) 459 | raise LauncherFlowInterruptedException(self.TAG, message) 460 | 461 | 462 | -------------------------------------------------------------------------------- /system/bin/AndroidShellCommandAssemblers.py: -------------------------------------------------------------------------------- 1 | from system.bin.AndroidShellCommands import ( 2 | AaptCommand, 3 | AdbCommand, 4 | AdbShellCommand, 5 | AdbActivityManagerCommand, 6 | AdbPackageManagerCommand, 7 | AdbSettingsCommand, 8 | AdbLogCatCommand, 9 | AvdManagerCommand, 10 | EmulatorCommand, 11 | GradleCommand, 12 | InstrumentationRunnerCommand 13 | ) 14 | from system.bin.SystemShellCommands import GeneralCommand 15 | 16 | 17 | class AaptCommandAssembler: 18 | dump_badging_schema = "{} {}" 19 | list_all_schema = "{} {}" 20 | 21 | def assemble_dump_badging_cmd(self, aapt_bin, apk_filepath): 22 | return self.dump_badging_schema.format(aapt_bin, 23 | AaptCommand.DUMP_BADGING.format(apk_filepath)) 24 | 25 | def assemble_list_all_cmd(self, aapt_bin, apk_filepath): 26 | return self.list_all_schema.format(aapt_bin, 27 | AaptCommand.LIST_ALL.format(apk_filepath)) 28 | 29 | 30 | class AdbCommandAssembler: 31 | start_server_schema = "{} {}" 32 | kill_server_schema = "{} {}" 33 | devices_schema = "{} {}" 34 | waif_for_device_schema = "{} {}" 35 | kill_device_schema = "{} {} {}" 36 | install_apk_schema = "{} {} {} {}" 37 | pull_file_schema = "{} {} {}" 38 | 39 | def assemble_start_server_cmd(self, adb_bin): 40 | return self.start_server_schema.format(adb_bin, 41 | AdbCommand.START_SERVER) 42 | 43 | def assemble_kill_server_cmd(self, adb_bin): 44 | return self.kill_server_schema.format(adb_bin, 45 | AdbCommand.KILL_SERVER) 46 | 47 | def assemble_devices_cmd(self, adb_bin): 48 | return self.devices_schema.format(adb_bin, 49 | AdbCommand.DEVICES) 50 | 51 | def assemble_wait_for_device_cmd(self, adb_bin): 52 | return self.waif_for_device_schema.format(adb_bin, 53 | AdbCommand.WAIT_FOR_DEVICE) 54 | 55 | def assemble_kill_device_cmd(self, adb_bin, device_adb_name): 56 | return self.kill_device_schema.format(adb_bin, 57 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 58 | AdbCommand.KILL_DEVICE) 59 | 60 | def assemble_install_apk_cmd(self, adb_bin, device_adb_name, apk_name): 61 | return self.install_apk_schema.format(adb_bin, 62 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 63 | AdbCommand.INSTALL_APK.format(apk_name), 64 | GeneralCommand.CHANGE_THREAD) 65 | 66 | def assemble_pull_file_cmd(self, adb_bin, device_adb_name, file_dir, file_destination_dir): 67 | return self.pull_file_schema.format(adb_bin, 68 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 69 | AdbCommand.PULL.format(file_dir, file_destination_dir)) 70 | 71 | 72 | class AdbShellCommandAssembler: 73 | get_property_schema = "{} {} {} {}" 74 | record_screen = "{} {} {} {}" 75 | remove_file = "{} {} {} {}" 76 | remove_files_in_dir = "{} {} {} {}" 77 | create_dir = "{} {} {} {}" 78 | check_if_dir_exists = "{} {} {} {}" 79 | 80 | def assemble_get_property_cmd(self, adb_bin, device_adb_name, device_property): 81 | return self.get_property_schema.format(adb_bin, 82 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 83 | AdbShellCommand.SHELL, 84 | AdbShellCommand.GET_PROPERTY.format(device_property)) 85 | 86 | def assemble_record_screen_cmd(self, adb_bin, device_adb_name, file_dir): 87 | return self.record_screen.format(adb_bin, 88 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 89 | AdbShellCommand.SHELL, 90 | AdbShellCommand.RECORD.format(file_dir)) 91 | 92 | def assemble_remove_file_cmd(self, adb_bin, device_adb_name, file_dir): 93 | return self.remove_file.format(adb_bin, 94 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 95 | AdbShellCommand.SHELL, 96 | AdbShellCommand.REMOVE_FILE.format(file_dir)) 97 | 98 | def assemble_remove_files_in_dir_cmd(self, adb_bin, device_adb_name, file_dir): 99 | return self.remove_files_in_dir.format(adb_bin, 100 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 101 | AdbShellCommand.SHELL, 102 | AdbShellCommand.REMOVE_FILES_IN_DIR.format(file_dir)) 103 | 104 | def assemble_create_dir_cmd(self, adb_bin, device_adb_name, file_dir): 105 | return self.create_dir.format(adb_bin, 106 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 107 | AdbShellCommand.SHELL, 108 | AdbShellCommand.CREATE_DIR.format(file_dir)) 109 | 110 | def assemble_check_if_dir_exists_cmd(self, adb_bin, device_adb_name, directory): 111 | return self.check_if_dir_exists.format(adb_bin, 112 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 113 | AdbShellCommand.SHELL, 114 | GeneralCommand.CHECK_IF_DIR_EXISTS.format(directory)) 115 | 116 | 117 | class AdbPackageManagerCommandAssembler: 118 | list_installed_packages_schema = "{} {} {} {} {}" 119 | uninstall_package_schema = "{} {} {} {} {}" 120 | clear_package_cache_schema = "{} {} {} {} {}" 121 | 122 | def assemble_list_installed_packages_cmd(self, adb_bin, device_adb_name): 123 | return self.list_installed_packages_schema.format(adb_bin, 124 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 125 | AdbShellCommand.SHELL, 126 | AdbPackageManagerCommand.PACKAGE_MANAGER, 127 | AdbPackageManagerCommand.LIST_SERVICES) 128 | 129 | def assemble_uninstall_package_cmd(self, adb_bin, device_adb_name, package_name): 130 | return self.uninstall_package_schema.format(adb_bin, 131 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 132 | AdbShellCommand.SHELL, 133 | AdbPackageManagerCommand.PACKAGE_MANAGER, 134 | AdbPackageManagerCommand.UNINSTALL_PACKAGE.format(package_name)) 135 | 136 | def assemble_clear_package_cache_cmd(self, adb_bin, device_adb_name, package_name): 137 | return self.clear_package_cache_schema.format(adb_bin, 138 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 139 | AdbShellCommand.SHELL, 140 | AdbPackageManagerCommand.PACKAGE_MANAGER, 141 | AdbPackageManagerCommand.CLEAR_CACHE.format(package_name)) 142 | 143 | 144 | class AdbSettingsCommandAssembler: 145 | get_device_android_id_schema = "{} {} {} {} {}" 146 | 147 | def assemble_get_device_android_id_cmd(self, adb_bin, device_adb_name): 148 | return self.get_device_android_id_schema.format(adb_bin, 149 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 150 | AdbShellCommand.SHELL, 151 | AdbSettingsCommand.SETTINGS, 152 | AdbSettingsCommand.GET_DEVICE_ANDROID_ID) 153 | 154 | 155 | class AdbLogCatCommandAssembler: 156 | flush_logcat_schema = "{} {} {} {}" 157 | dump_logcat_schema = "{} {} {} {}" 158 | monitor_logcat_schema = "{} {} {}" 159 | 160 | def assemble_monitor_logcat_cmd(self, adb_bin, device_adb_name): 161 | return self.monitor_logcat_schema.format(adb_bin, 162 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 163 | AdbLogCatCommand.LOG_CAT) 164 | 165 | def assemble_flush_logcat_cmd(self, adb_bin, device_adb_name): 166 | return self.flush_logcat_schema.format(adb_bin, 167 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 168 | AdbLogCatCommand.LOG_CAT, 169 | AdbLogCatCommand.FLUSH) 170 | 171 | def assemble_dump_logcat_cmd(self, adb_bin, device_adb_name): 172 | return self.dump_logcat_schema.format(adb_bin, 173 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 174 | AdbLogCatCommand.LOG_CAT, 175 | AdbLogCatCommand.DUMP) 176 | 177 | 178 | class AvdManagerCommandAssembler: 179 | list_avd_schema = "{} {}" 180 | delete_avd_schema = "{} {}" 181 | create_avd_schema = "{} {} {} {} {} {} {} {} {}" 182 | 183 | def assemble_list_avd_cmd(self, avdmanager_bin): 184 | return self.list_avd_schema.format(avdmanager_bin, 185 | AvdManagerCommand.LIST_AVD) 186 | 187 | def assemble_delete_avd_cmd(self, avdmanager_bin, avd_schema): 188 | return self.delete_avd_schema.format(avdmanager_bin, 189 | AvdManagerCommand.DELETE_AVD.format(avd_schema.avd_name)) 190 | 191 | def assemble_create_avd_cmd(self, avdmanager_bin, avd_schema): 192 | part_answer_no = GeneralCommand.ANSWER_NO 193 | part_create_avd = AvdManagerCommand.CreateAvdCommandPart.AVD_CREATE 194 | part_name = AvdManagerCommand.CreateAvdCommandPart.AVD_NAME.format(avd_schema.avd_name) 195 | part_package = AvdManagerCommand.CreateAvdCommandPart.AVD_PACKAGE.format(avd_schema.create_avd_package) 196 | 197 | if avd_schema.create_avd_device == "": 198 | part_device = avd_schema.create_avd_device 199 | else: 200 | part_device = AvdManagerCommand.CreateAvdCommandPart.AVD_DEVICE.format(avd_schema.create_avd_device) 201 | 202 | if avd_schema.create_avd_tag == "": 203 | part_tag = avd_schema.create_avd_tag 204 | else: 205 | part_tag = AvdManagerCommand.CreateAvdCommandPart.AVD_TAG.format(avd_schema.create_avd_tag) 206 | 207 | if avd_schema.create_avd_abi == "": 208 | part_abi = avd_schema.create_avd_abi 209 | else: 210 | part_abi = AvdManagerCommand.CreateAvdCommandPart.AVD_ABI.format(avd_schema.create_avd_abi) 211 | 212 | part_avd_additional_options = AvdManagerCommand.CreateAvdCommandPart.AVD_ADDITIONAL_OPTIONS.format( 213 | avd_schema.create_avd_additional_options) 214 | 215 | return self.create_avd_schema.format(part_answer_no, 216 | avdmanager_bin, 217 | part_create_avd, 218 | part_name, 219 | part_package, 220 | part_device, 221 | part_tag, 222 | part_abi, 223 | part_avd_additional_options) 224 | 225 | 226 | class EmulatorCommandAssembler: 227 | launch_avd_schema = "{} {} {} {} {} {}" 228 | output_file_schema = "{} {}" 229 | 230 | def assemble_launch_avd_cmd(self, emulator_bin, avd_schema, port, log_file): 231 | part_emulator_binary = emulator_bin 232 | part_port = EmulatorCommand.LaunchAvdCommandPart.AVD_PORT.format(port) 233 | part_name = EmulatorCommand.LaunchAvdCommandPart.AVD_NAME.format(avd_schema.avd_name) 234 | 235 | part_snapshot = "" 236 | if avd_schema.launch_avd_snapshot_filepath != "": 237 | part_snapshot = EmulatorCommand.LaunchAvdCommandPart.AVD_SNAPSHOT.format( 238 | avd_schema.launch_avd_snapshot_filepath) 239 | 240 | part_additional_options = EmulatorCommand.LaunchAvdCommandPart.AVD_ADDITIONAL_OPTIONS.format( 241 | avd_schema.launch_avd_additional_options) 242 | 243 | part_output_file = self.output_file_schema.format(GeneralCommand.DELEGATE_OUTPUT_TO_FILE.format(log_file), 244 | GeneralCommand.CHANGE_THREAD) 245 | 246 | return self.launch_avd_schema.format(part_emulator_binary, 247 | part_name, 248 | part_port, 249 | part_snapshot, 250 | part_additional_options, 251 | part_output_file) 252 | 253 | 254 | class GradleCommandAssembler: 255 | gradle_command_schema = "{} {}" 256 | 257 | def assemble_build_application_apk_cmd(self, gradle_bin, gradle_params, assemble_task, project_root_dir): 258 | gradle_cmd = GradleCommand.RUN_TASK_IN_OTHER_DIRECTORY.format(gradle_bin, 259 | project_root_dir, 260 | assemble_task) 261 | return self.gradle_command_schema.format(gradle_cmd, gradle_params) 262 | 263 | def assemble_build_test_apk_cmd(self, gradle_bin, gradle_params, assemble_task, project_root_dir): 264 | gradle_cmd = GradleCommand.RUN_TASK_IN_OTHER_DIRECTORY.format(gradle_bin, 265 | project_root_dir, 266 | assemble_task) 267 | return self.gradle_command_schema.format(gradle_cmd, gradle_params) 268 | 269 | 270 | class InstrumentationRunnerCommandAssembler: 271 | test_command_schema = "{} {} {} {} {} {} {} {}" 272 | single_test_command_schema = "{} {} {} {} {} {} {}{} {}" 273 | 274 | def assemble_run_test_package_cmd(self, adb_binary, device_adb_name, params, instrumentation_runner): 275 | parameters = self._assemble_params(params) 276 | return self.test_command_schema.format(adb_binary, 277 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 278 | AdbShellCommand.SHELL, 279 | AdbActivityManagerCommand.ACTIVITY_MANAGER, 280 | InstrumentationRunnerCommand.INSTRUMENT_PROCESS, 281 | InstrumentationRunnerCommand.DISPLAY_RAW_MESSAGE, 282 | parameters, 283 | InstrumentationRunnerCommand.INSTRUMENTATION_RUNNER.format( 284 | instrumentation_runner) 285 | ) 286 | 287 | def assemble_run_single_test_cmd(self, adb_binary, device_adb_name, test_class, test_name, instrumentation_runner): 288 | return self.single_test_command_schema.format(adb_binary, 289 | AdbCommand.SPECIFIC_DEVICE.format(device_adb_name), 290 | AdbShellCommand.SHELL, 291 | AdbActivityManagerCommand.ACTIVITY_MANAGER, 292 | InstrumentationRunnerCommand.INSTRUMENT_PROCESS, 293 | InstrumentationRunnerCommand.DISPLAY_RAW_MESSAGE, 294 | InstrumentationRunnerCommand.CLASS.format(test_class), 295 | InstrumentationRunnerCommand.TEST_CASE.format(test_name), 296 | InstrumentationRunnerCommand.INSTRUMENTATION_RUNNER.format( 297 | instrumentation_runner) 298 | ) 299 | 300 | @staticmethod 301 | def _assemble_params(params): 302 | parameters = "" 303 | for param, value in params.items(): 304 | if param == "package": 305 | parameters += (" " + InstrumentationRunnerCommand.PACKAGE.format(value)) 306 | if param == "numShards": 307 | parameters += (" " + InstrumentationRunnerCommand.NUM_SHARD.format(value)) 308 | if param == "shardIndex": 309 | parameters += (" " + InstrumentationRunnerCommand.SHARD_INDEX.format(value)) 310 | return parameters 311 | -------------------------------------------------------------------------------- /system/bin/AndroidShellCommands.py: -------------------------------------------------------------------------------- 1 | class AaptCommand: 2 | DUMP_BADGING = "dump badging {}" 3 | LIST_ALL = "l -a {}" 4 | 5 | 6 | class AdbCommand: 7 | KILL_SERVER = "kill-server" 8 | START_SERVER = "start-server" 9 | DEVICES = "devices" 10 | WAIT_FOR_DEVICE = "wait-for-device" 11 | LIST_AVD = "list avd" 12 | KILL_DEVICE = "emu kill" 13 | SPECIFIC_DEVICE = "-s {}" 14 | INSTALL_APK = "install {}" 15 | UNINSTALL_PACKAGE = "uninstall {}" 16 | PULL = "pull {} {}" 17 | 18 | 19 | class AdbShellCommand: 20 | SHELL = "shell" 21 | CREATE_DIR = "mkdir {}" 22 | REMOVE_FILE = "rm -f {}" 23 | REMOVE_FILES_IN_DIR = "rm -r {}" 24 | RECORD = "screenrecord --bit-rate 2000000 {}" 25 | GET_PROPERTY = "getprop {}" 26 | 27 | 28 | class AdbPackageManagerCommand: 29 | PACKAGE_MANAGER = "pm" 30 | CLEAR_CACHE = "clear {}" 31 | LIST_SERVICES = "list packages" 32 | UNINSTALL_PACKAGE = "uninstall {}" 33 | 34 | 35 | class AdbActivityManagerCommand: 36 | ACTIVITY_MANAGER = "am" 37 | 38 | 39 | class AdbSettingsCommand: 40 | SETTINGS = "settings" 41 | GET_DEVICE_ANDROID_ID = "get secure android_id" 42 | 43 | 44 | class AdbLogCatCommand: 45 | LOG_CAT = "logcat" 46 | FLUSH = "-b all -c" 47 | DUMP = "-d" 48 | 49 | 50 | class InstrumentationRunnerCommand: 51 | INSTRUMENT_PROCESS = "instrument -w" 52 | NUM_SHARD = "-e numShards {}" 53 | SHARD_INDEX = "-e shardIndex {}" 54 | PACKAGE = "-e package {}" 55 | DISPLAY_RAW_MESSAGE = "-r" 56 | INSTRUMENTATION_RUNNER = "{}" 57 | CLASS = "-e class {}" 58 | TEST_CASE = "#{}" 59 | 60 | class AvdManagerCommand: 61 | class CreateAvdCommandPart: 62 | AVD_CREATE = "create avd" 63 | AVD_NAME = "--name \"{}\"" 64 | AVD_PACKAGE = "--package \"{}\"" 65 | AVD_TAG = "--tag \"{}\"" 66 | AVD_ABI = "--abi {}" 67 | AVD_DEVICE = "--device \"{}\"" 68 | AVD_ADDITIONAL_OPTIONS = "{}" 69 | 70 | LIST_AVD = "list avd" 71 | DELETE_AVD = "delete avd -n {}" 72 | 73 | 74 | class EmulatorCommand: 75 | class LaunchAvdCommandPart: 76 | AVD_NAME = "-avd \"{}\"" 77 | AVD_PORT = "-port {}" 78 | AVD_SNAPSHOT = "-wipe-data -initdata {}" 79 | AVD_ADDITIONAL_OPTIONS = "{}" 80 | AVD_OUTPUT = "{}" 81 | 82 | 83 | class GradleCommand: 84 | RUN_TASK_IN_OTHER_DIRECTORY = "{} -p {} {}" 85 | -------------------------------------------------------------------------------- /system/bin/SystemShellCommands.py: -------------------------------------------------------------------------------- 1 | class GeneralCommand: 2 | ANSWER_NO = "echo \"no\" |" 3 | CHECK_PORT = "lsof -i:{}" 4 | COPY_FROM_TO = "cat {} > {}" 5 | DELEGATE_OUTPUT_TO_FILE = "&> {}" 6 | DELEGATE_OUTPUT_TO_NULL = "&>/dev/null" 7 | CHANGE_THREAD = "&" 8 | CHECK_IF_DIR_EXISTS = "ls -d {}" 9 | -------------------------------------------------------------------------------- /system/buffer/AdbCallBuffer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | 4 | from settings import GlobalConfig 5 | 6 | _adb_call_buffer = list() 7 | 8 | 9 | def is_adb_cmd(cmd): 10 | adb_bin_found = False 11 | 12 | cmd_parts = cmd.split(" ") 13 | if cmd_parts: 14 | bin_part = cmd_parts[0] 15 | 16 | if bin_part is not None and len(bin_part) >= 3: 17 | last_three_letters = bin_part[-3:] 18 | adb_bin_found = last_three_letters.lower() == "adb" 19 | return adb_bin_found 20 | 21 | 22 | def report_adb_call(): 23 | if _is_full(): 24 | _wait_for_empty_slot() 25 | _report_adb_call() 26 | 27 | 28 | def _is_full(): 29 | return len(_adb_call_buffer) >= GlobalConfig.ADB_CALL_BUFFER_SIZE 30 | 31 | 32 | def _wait_for_empty_slot(): 33 | while True: 34 | if not _is_full(): 35 | break 36 | time.sleep(0.1) 37 | 38 | 39 | def _report_adb_call(): 40 | cmd_call_time = int(round(time.time() * 1000)) 41 | _adb_call_buffer.append(cmd_call_time) 42 | 43 | remove_slot_thread = _ReleaseBufferSlotThread(_adb_call_buffer, cmd_call_time) 44 | remove_slot_thread.start() 45 | 46 | 47 | class _ReleaseBufferSlotThread(threading.Thread): 48 | TAG = "_ReleaseBufferSlotThread" 49 | 50 | def __init__(self, buffer, cmd_call_time): 51 | super().__init__() 52 | 53 | self.buffer = buffer 54 | self.cmd_call_time = cmd_call_time 55 | 56 | def run(self): 57 | while (self._get_current_time_millis() - self.cmd_call_time) < GlobalConfig.ADB_CALL_BUFFER_DELAY_BETWEEN_CMD: 58 | time.sleep(0.1) 59 | self.buffer.remove(self.cmd_call_time) 60 | 61 | @staticmethod 62 | def _get_current_time_millis(): 63 | return int(round(time.time() * 1000)) 64 | -------------------------------------------------------------------------------- /system/console/Color.py: -------------------------------------------------------------------------------- 1 | RED = '\033[91m' 2 | BLUE = '\033[94m' 3 | GREEN = '\033[92m' 4 | YELLOW = '\033[93m' 5 | GOLD = '\033[33m' 6 | DARK_PURPLE = '\033[35m' 7 | PURPLE = '\033[95m' 8 | PURPLE_UNDERLINE = '\033[95;4m' 9 | GRAY = '\033[90m' 10 | END = '\033[0m' 11 | -------------------------------------------------------------------------------- /system/console/Printer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from system.console import Color 4 | 5 | 6 | def phase(message): 7 | print("\n" + Color.PURPLE + "[{:%H:%M:%S}]".format( 8 | datetime.datetime.now()) + " " + Color.PURPLE_UNDERLINE + message + " PHASE" + Color.END) 9 | 10 | 11 | def error(tag, message): 12 | message_assembly = "" 13 | 14 | message_assembly += Color.RED + "[{:%H:%M:%S}] - ".format(datetime.datetime.now()) 15 | if tag != "": 16 | message_assembly += tag + " " 17 | message_assembly += message + Color.END 18 | 19 | print(message_assembly) 20 | 21 | 22 | def step(message): 23 | print(Color.YELLOW + "[{:%H:%M:%S}] ".format(datetime.datetime.now()) + message + Color.END) 24 | 25 | 26 | def system_message(tag, message): 27 | message_assembly = "" 28 | 29 | message_assembly += Color.BLUE + "[{:%H:%M:%S}] - ".format(datetime.datetime.now()) 30 | if tag != "": 31 | message_assembly += tag + " " 32 | message_assembly += message + Color.END 33 | 34 | print(message_assembly) 35 | 36 | 37 | def console_highlighted(tag, message, cmd): 38 | print(Color.BLUE + "[{:%H:%M:%S}] - ".format(datetime.datetime.now()) 39 | + tag + " " + message + Color.GOLD + cmd + Color.END) 40 | 41 | 42 | def console(message, end): 43 | print(Color.DARK_PURPLE + "[{:%H:%M:%S}] > ".format(datetime.datetime.now()) + message + Color.END, end=end) 44 | -------------------------------------------------------------------------------- /system/console/ShellHelper.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from system.console import Printer 4 | from system.buffer import AdbCallBuffer 5 | 6 | TAG = "ShellHelper:" 7 | 8 | 9 | def execute_shell(cmd, show_cmd, monitor_output): 10 | if AdbCallBuffer.is_adb_cmd(cmd): 11 | AdbCallBuffer.report_adb_call() 12 | 13 | if show_cmd: 14 | Printer.console_highlighted(TAG, "Executing shell command: ", cmd) 15 | with subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, bufsize=1, universal_newlines=True) as p: 16 | output = "" 17 | for line in p.stdout: 18 | if monitor_output: 19 | Printer.console(line, end='') 20 | output += line 21 | 22 | if p.returncode != 0: 23 | raise Exception(p.returncode, p.args) 24 | 25 | return output 26 | -------------------------------------------------------------------------------- /system/file/FileUtils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import distutils.dir_util 4 | import codecs 5 | import json 6 | 7 | from error.Exceptions import LauncherFlowInterruptedException 8 | 9 | TAG = "FileUtils:" 10 | 11 | 12 | def copy(from_dir, to_dir): 13 | distutils.dir_util.copy_tree(from_dir, to_dir) 14 | 15 | 16 | def dir_exists(directory): 17 | return os.path.exists(directory) 18 | 19 | 20 | def file_exists(file): 21 | return os.path.isfile(file) 22 | 23 | 24 | def list_files_in_dir(directory): 25 | return os.listdir(directory) 26 | 27 | 28 | def create_dir(directory): 29 | dir_path = clean_path(directory) 30 | 31 | try: 32 | os.makedirs(directory) 33 | except Exception as e: 34 | message = "Unable to create directory '{}'. Error message: {}" 35 | message = message.format(dir_path, str(e)) 36 | raise LauncherFlowInterruptedException(TAG, message) 37 | 38 | return True 39 | 40 | 41 | def clear_dir(directory): 42 | if list_files_in_dir(directory): 43 | for the_file in list_files_in_dir(directory): 44 | file_path = os.path.join(directory, the_file) 45 | try: 46 | if file_exists(file_path): 47 | os.unlink(file_path) 48 | elif file_exists(file_path): 49 | shutil.rmtree(file_path) 50 | except Exception as e: 51 | message = "Unable to delete files in directory '{}'. Error message: {}" 52 | message = message.format(directory, str(e)) 53 | raise LauncherFlowInterruptedException(TAG, message) 54 | 55 | return True 56 | 57 | 58 | def create_file(directory, file_name, extension): 59 | file_path = clean_path(directory + str(file_name) + "." + extension) 60 | 61 | try: 62 | if not dir_exists(directory): 63 | os.makedirs(directory) 64 | open(file_path, "w", encoding="utf-8") 65 | except Exception as e: 66 | message = "Unable to create file '{}.{}'. Error message: {}" 67 | message = message.format(file_path, extension, str(e)) 68 | raise LauncherFlowInterruptedException(TAG, message) 69 | 70 | return True 71 | 72 | 73 | def load_json(json_dir): 74 | json_dir = clean_path(json_dir) 75 | with open(json_dir, "r", encoding="utf-8") as json_file: 76 | json_data = json_file.read() 77 | 78 | try: 79 | json_dict = json.loads(json_data) 80 | except Exception as e: 81 | message = "Unable to parse JSON file '{}/{}'. Error message: {}" 82 | message = message.format(os.getcwd(), json_dir, str(e)) 83 | raise LauncherFlowInterruptedException(TAG, message) 84 | 85 | return json_dict 86 | 87 | 88 | def save_json_dict_to_json(directory, json_dict, file_name): 89 | extension = ".json" 90 | if extension not in file_name: 91 | file_name = file_name + extension 92 | 93 | file_path = clean_path(directory + str(file_name)) 94 | if not dir_exists(directory): 95 | os.makedirs(directory) 96 | 97 | with open(file_path, 'w', encoding="utf-8") as f: 98 | f.write(json.dumps(json_dict, indent=4, ensure_ascii=False)) 99 | 100 | return os.path.abspath(file_path) 101 | 102 | 103 | def clean_folder_only_dir(path): 104 | return remove_slash_pairs(add_ending_slash(clean_path(path))) 105 | 106 | 107 | def clean_path(path): 108 | return codecs.getdecoder("unicode_escape")(os.path.expanduser(path))[0] 109 | 110 | 111 | def add_starting_slash(path): 112 | if path != "" and path[0] != "/": 113 | return "/" + path 114 | else: 115 | return path 116 | 117 | 118 | def add_ending_slash(path): 119 | if path != "" and path[len(path) - 1] != "/": 120 | return path + "/" 121 | else: 122 | return path 123 | 124 | 125 | def remove_slash_pairs(path): 126 | return path.replace("//", "/") 127 | 128 | 129 | def get_project_root(): 130 | return os.path.abspath(os.path.dirname(__name__)) 131 | 132 | 133 | def make_path_absolute(path): 134 | return os.path.abspath(clean_path(path)) 135 | -------------------------------------------------------------------------------- /system/port/PortManager.py: -------------------------------------------------------------------------------- 1 | from error.Exceptions import LauncherFlowInterruptedException 2 | 3 | from system.bin.SystemShellCommands import GeneralCommand 4 | from system.console import ( 5 | ShellHelper, 6 | Printer 7 | ) 8 | 9 | TAG = "PortManager:" 10 | 11 | 12 | def get_open_ports(avd_set): 13 | _check_port_rules(avd_set) 14 | 15 | ports_to_use = avd_set.avd_port_rules.ports_to_use 16 | if len(ports_to_use) > 0: 17 | message = "Requested ports:" 18 | for port in ports_to_use: 19 | message += (" '" + str(port) + "'") 20 | message += "." 21 | message(TAG, message) 22 | 23 | ports_to_ignore = avd_set.avd_port_rules.ports_to_ignore 24 | if len(ports_to_ignore) > 0: 25 | message = "Ignoring ports:" 26 | for port in ports_to_ignore: 27 | message += (" '" + str(port) + "'") 28 | message += "." 29 | Printer.system_message(TAG, message) 30 | 31 | should_assign_missing_ports = avd_set.avd_port_rules.assign_missing_ports 32 | if should_assign_missing_ports: 33 | Printer.system_message(TAG, "Not requested ports will be automatically assigned.") 34 | else: 35 | Printer.system_message(TAG, "Port auto assignment is turned off. Only specified ports will be used.") 36 | 37 | avd_instances = 0 38 | for avd in avd_set.avd_list: 39 | avd_instances += avd.instances 40 | 41 | range_min = avd_set.avd_port_rules.search_range_min 42 | range_max = avd_set.avd_port_rules.search_range_max 43 | Printer.system_message(TAG, "Checking for " + str(avd_instances) + " open ports in range <" + str( 44 | range_min) + ", " + str(range_max) + ">.") 45 | 46 | available_ports = list() 47 | 48 | for port in ports_to_use: 49 | try: 50 | ShellHelper.execute_shell(GeneralCommand.CHECK_PORT.format(port), False, False) 51 | port_open = False 52 | except: 53 | port_open = True 54 | 55 | if port_open: 56 | available_ports.append(port) 57 | else: 58 | message = "Port {} was requested but is currently used." 59 | message = message.format(str(port)) 60 | raise LauncherFlowInterruptedException(TAG, message) 61 | 62 | if should_assign_missing_ports: 63 | temp_port = range_min 64 | for i in range(avd_instances - len(available_ports)): 65 | while temp_port < range_max and len(available_ports) != avd_instances: 66 | try: 67 | ShellHelper.execute_shell(GeneralCommand.CHECK_PORT.format(temp_port), False, False) 68 | port_open = False 69 | except: 70 | port_open = True 71 | 72 | if port_open: 73 | if temp_port not in available_ports and temp_port not in ports_to_ignore: 74 | available_ports.append(temp_port) 75 | else: 76 | Printer.error(TAG, "Port " + str(temp_port) + " is currently used and will be omitted.") 77 | 78 | temp_port += 2 79 | 80 | if len(available_ports) != avd_instances: 81 | message = "There are only {} open ports available in range <{}, {}>. Requested amount: {}." 82 | message = message.format(str(len(available_ports)), str(range_min), str(range_max), str(avd_instances)) 83 | raise LauncherFlowInterruptedException(TAG, message) 84 | 85 | return available_ports 86 | 87 | 88 | def _check_port_rules(avd_set): 89 | avd_instances = 0 90 | avd_rules = avd_set.avd_port_rules 91 | 92 | for avd in avd_set.avd_list: 93 | avd_instances += avd.instances 94 | 95 | if len(avd_rules.ports_to_use) != len(set(avd_rules.ports_to_use)): 96 | message = "'Ports to use' list contains duplicates." 97 | raise LauncherFlowInterruptedException(TAG, message) 98 | 99 | if len(avd_rules.ports_to_ignore) != len(set(avd_rules.ports_to_ignore)): 100 | message = "'Ports to ignore' list contains duplicates." 101 | raise LauncherFlowInterruptedException(TAG, message) 102 | 103 | for port_to_be_used in avd_rules.ports_to_use: 104 | if port_to_be_used % 2 == 1: 105 | message = "Port numbers has to be even." 106 | raise LauncherFlowInterruptedException(TAG, message) 107 | 108 | for port_to_be_used in avd_rules.ports_to_use: 109 | if port_to_be_used < avd_rules.search_range_min or port_to_be_used > avd_rules.search_range_max: 110 | message = "Requested to use port {} is out of range <{}, {}>." 111 | message = message.format(str(port_to_be_used), str(avd_rules.search_range_min), 112 | str(avd_rules.search_range_max)) 113 | raise LauncherFlowInterruptedException(TAG, message) 114 | 115 | for port_to_be_ignored in avd_rules.ports_to_ignore: 116 | for port_to_be_used in avd_rules.ports_to_use: 117 | if port_to_be_used == port_to_be_ignored: 118 | message = "Port {} is set to be used and ignored at the same time." 119 | message = message.format(str(port_to_be_used)) 120 | raise LauncherFlowInterruptedException(TAG, message) 121 | 122 | if not avd_rules.assign_missing_ports: 123 | requested_ports_to_use_num = len(avd_rules.ports_to_use) 124 | if requested_ports_to_use_num != avd_instances: 125 | message = ("There are {} AVD instances about to be created according to set, but there were only {} ports " 126 | "requested to use. Set 'assign_missing_ports' to True or add more ports to list.") 127 | message = message.format(str(avd_instances), str(requested_ports_to_use_num)) 128 | raise LauncherFlowInterruptedException(TAG, message) 129 | --------------------------------------------------------------------------------