def get_coverage( self, device: Device, coverage_folder_local_path: str, accumulated_output: str, accumulated_errors: str, ) -> int: # get the unique ids of the methods listed in all the coverage.dat files output, errors, result_code = run_cmd( f"cat {coverage_folder_local_path}/coverage.dat* | grep -v \"#\" | sort -u | wc -l" ) accumulated_output += output accumulated_errors += errors number_of_methods_covered = float(output.strip().rstrip('\n')) output, errors, result_code = run_cmd( f"cat {coverage_folder_local_path}/covids | wc -l") accumulated_output += output accumulated_errors += errors total_number_of_methods = float(output.strip().rstrip('\n')) method_coverage = int(number_of_methods_covered / total_number_of_methods * 100) return method_coverage
def boot_emulators(self, wait_to_be_ready: bool = False) -> None: while True: self.next_available_emulator_port = 5554 self.next_available_adb_server_port = 5038 self.devices = [] logger.log_progress( f"\nBooting devices: {str(0)}/{str(self.emulators_number)}") for i in range(0, self.emulators_number): logger.log_progress( f"\nBooting devices: {str(i + 1)}/{str(self.emulators_number)}" ) emulator = Emulator(self) emulator.boot() self.devices.append(emulator) if wait_to_be_ready: try: self.wait_devices_to_be_ready() except WaitDevicesTimeout as e: logger.log_progress(f"\n{str(e)}") logger.log_progress( "\nForce kill on all current emulator processes") run_cmd("pgrep emu | xargs kill -9", discard_output=True) time.sleep(5) continue break
def parse_adb_devices(self, adb_port: int, emulators_found: int, real_devices_found: int) -> Tuple[int, int]: devices_cmd = adb.adb_cmd_prefix + ' devices' try: output, errors, result_code = run_cmd( devices_cmd, env={"ANDROID_ADB_SERVER_PORT": str(adb_port)}) except TimeoutExpired as e: return emulators_found, real_devices_found error_lines = errors.split("\n") for line in error_lines: if "daemon not running" in line: continue if "daemon started successfully" in line: continue if line.strip() != "": raise Exception( f"There was an error running \'adb devices\' command: {errors}" ) lines = output.split("\n") for line in lines: if "List of devices attached" in line: continue if line.strip() == "": continue if "offline" not in line: device_name = line.split("\t")[0].strip() matching_devices = [ device for device in self.devices if device.name == device_name ] if len(matching_devices) > 0: device = matching_devices.pop(0) if device.state < State.reachable: device.state = State.reachable if type(device) is Emulator: emulators_found += 1 else: real_devices_found += 1 elif "emulator" in line and emulators_found < self.emulators_number: self.devices.append( Emulator(self, device_name, state=State.reachable)) emulators_found += 1 elif "device" in line and real_devices_found < self.real_devices_number: self.devices.append( Device(self, device_name, state=State.reachable)) real_devices_found += 1 return emulators_found, real_devices_found
def adb_command(device: 'Device', command: str, timeout: Optional[int] = None, retry: int = 1, discard_output: bool = False) -> RunCmdResult: cmd = f"{adb_cmd_prefix} -s {device.name} {command}" tries = 0 while True: tries += 1 log_adb_command(device, cmd) try: output, errors, result_code = run_cmd( cmd, timeout=timeout, discard_output=discard_output, env={"ANDROID_ADB_SERVER_PORT": str(device.adb_port)}) if tries >= retry or result_code == 0: return output, errors, result_code time.sleep(2) except TimeoutExpired as e: if tries >= retry: return e.stdout, e.stderr, 124 restart_server(device.adb_port) time.sleep(5)
def instrument(self) -> None: self.app_path: str = RequiredFeature('app_path').request() self.result_dir: str = RequiredFeature('result_dir').request() self.instrumented_subjects_path: str = RequiredFeature('instrumented_subjects_path').request() self.emma_instrument_path: str = RequiredFeature('emma_instrument_path').request() # first, check if we should assume apps are already instrumented assume_subjects_instrumented = RequiredFeature('assume_subjects_instrumented').request() if assume_subjects_instrumented: features.provide('instrumented_app_path', self.app_path) output, errors, result_code = run_cmd(f"aapt dump badging {self.app_path} | grep package:\\ name") package_name = output.split("package: name=\'")[1].split("\'")[0] features.provide('package_name', package_name) self.prepare_files_for_coverage() return logger.log_progress(f"\nInstrumenting app: {os.path.basename(self.app_path)}") # copy sources and instrument application instrumented_app_path, package_name = self.prepare_app_for_instrumentation() features.provide('package_name', package_name) features.provide('instrumented_app_path', instrumented_app_path) self.instrument_gradle_file(instrumented_app_path, package_name) result_code = os.system(f"./gradlew assembleDebug 2>&1 >{self.result_dir}/build.log") if result_code != 0: raise Exception("Unable run assembleDebug") os.chdir(settings.WORKING_DIR) self.prepare_files_for_coverage()
def dump_script_coverage(self, device: Device, coverage_folder_local_path: str, accumulated_output: str, accumulated_errors: str, script_path: str, generation: int, individual_index: int, test_case_index: int, unique_crashes: Set[str], scripts_crash_status: Dict[str, bool]) -> bool: result_dir: str = RequiredFeature('result_dir').request() if crash_handler.handle(device, script_path, generation, individual_index, test_case_index, unique_crashes): scripts_crash_status[script_path] = True return False else: # no crash, collect coverage scripts_crash_status[script_path] = False adb.log_evaluation_result(device, result_dir, script_path, True) # save the coverage.dat file of this test script output, errors, result_code = run_cmd( f"mv {self.get_coverage_dat_path(device)} {coverage_folder_local_path}/" ) if result_code != 0: raise Exception( f"Unable to move the coverage dat file for test script, path is: {self.get_coverage_dat_path(device)}" ) return True
def get_current_activity(device: 'Device') -> str: cmd = f"{get_adb_cmd_prefix_for_device(device)} shell dumpsys activity activities | grep Activities | head -n 1" log_adb_command(device, cmd) res = run_cmd(cmd)[0].split("ActivityRecord")[1].split(" ")[2].split( "/")[1] return res
def alter_MainActivity(self, manifest_path, main_activity) -> None: """ Take care of changing MainActivity if it is written in Kotlin and is not marked as open. :param source_root: :param main_activity: :return: """ manifest_folder = os.path.dirname(manifest_path) main_acivity_name = main_activity.split(".")[-1] output, errors, result_code = run_cmd( f"find -L {manifest_folder} -name {main_acivity_name}.*") main_activity_path = output.strip("\n") if ".kt" not in main_activity_path: # nothing to do here return content = "" in_stream = open(main_activity_path) for index, line in enumerate(in_stream): if line.find(f"class {main_acivity_name}") != -1 and line.find( f"open ") == -1: # append "open" modifier at begging of line content += f"open {line}" else: content += line in_stream.close() os.remove(main_activity_path) new_file = open(main_activity_path, "w") new_file.write(content) new_file.close()
def stop_ella(self, device: Device) -> None: # stop ELLA server output, errors, result_code = run_cmd(f"./ella.sh k", cwd=self.get_device_ella_folder_path(device), timeout=300) if result_code != 0: raise Exception("Unable to stop ELLA server") # wait a bit for ELLA server to stop (it is not quite immediate) time.sleep(3)
def extract_coverage(self, html_path: str) -> int: xpath_missed_lines = "html/body/table/tfoot/tr/td[8]/text()" xpath_missed_lines_cmd = f"xmllint --html -xpath \"{xpath_missed_lines}\" {html_path}" output, errors, result_code = run_cmd(xpath_missed_lines_cmd) missed_lines_str = output.strip("\n") xpath_total_lines = "html/body/table/tfoot/tr/td[9]/text()" xpath_total_lines_cmd = f"xmllint --html -xpath \"{xpath_total_lines}\" {html_path}" output, errors, result_code = run_cmd(xpath_total_lines_cmd) total_lines_str = output.strip("\n") missed_lines = float(missed_lines_str.replace(",", "")) total_lines = float(total_lines_str.replace(",", "")) covered_lines = total_lines - missed_lines coverage = int(covered_lines / total_lines * 100) return coverage
def pkill(device: 'Device', string: str) -> int: adb_cmd = f"{get_adb_cmd_prefix_for_device(device)} shell " pkill_cmd = adb_cmd + "ps | grep " + string + " | awk '{print $2}' | xargs -I pid " + adb_cmd + "kill pid " log_adb_command(device, pkill_cmd) try: output, errors, result_code = run_cmd(pkill_cmd) return result_code except TimeoutExpired as e: return 124
def instrument_device(self, device: Device) -> bool: # build the TCP relay Android app output, errors, result_code = run_cmd(f"./gradlew assembleDebug", cwd=self.tcp_relay_android) if result_code != 0: raise Exception("Unable build the TCP relay Android app") output, errors, result_code = run_cmd( f"find {self.tcp_relay_android} -type f -name \"*apk\"") if result_code != 0: raise Exception( "Unable find the APK for the TCP relay Android app") tcp_relay_apk_path = output.rstrip('\n') adb.uninstall(device, self.tcp_relay_android_package_name) adb.install(device, self.tcp_relay_android_package_name, tcp_relay_apk_path) return True
def instrument(self) -> None: self.app_path: str = RequiredFeature('app_path').request() self.result_dir: str = RequiredFeature('result_dir').request() self.instrumented_subjects_path: str = RequiredFeature( 'instrumented_subjects_path').request() self.emma_instrument_path: str = RequiredFeature( 'emma_instrument_path').request() # first, check if we should assume apps are already instrumented assume_subjects_instrumented = RequiredFeature( 'assume_subjects_instrumented').request() if assume_subjects_instrumented: features.provide('instrumented_app_path', self.app_path) # copy emma generated file result_code = os.system( f"cp bin/coverage.em {self.result_dir}/{logger.redirect_string()}" ) if result_code != 0: raise Exception("Unable to copy coverage.em file") output, errors, result_code = run_cmd( f"aapt dump badging {self.app_path} | grep package:\\ name") package_name = output.split("package: name=\'")[1].split("\'")[0] features.provide('package_name', package_name) features.provide('compiled_package_name', package_name) return logger.log_progress( f"\nInstrumenting app: {os.path.basename(self.app_path)}") # copy sources and instrument application instrumented_app_path, package_name = self.prepare_app_for_instrumentation( ) # compile with emma data result_code = os.system( f"ant clean emma debug 2>&1 >{self.result_dir}/build.log") if result_code != 0: raise Exception("Unable run ant clean emma debug") # copy emma generated file result_code = os.system( f"cp bin/coverage.em {self.result_dir}/{logger.redirect_string()}") if result_code != 0: raise Exception("Unable to copy coverage.em file") os.chdir(settings.WORKING_DIR) features.provide('package_name', package_name) features.provide('instrumented_app_path', instrumented_app_path) # assume same compiled package name as the one declard in AndroidManifest.xml file features.provide('compiled_package_name', package_name)
def instrument_gradle_file(self, instrumented_app_path, package_name): build_gradle_path = self.find_build_gradle_path(instrumented_app_path) # check which changes need to be made to the build.gradle file add_jacoco_plugin = False enable_test_coverage = False output, errors, result_code = run_cmd(f"cat {build_gradle_path} | grep \"apply plugin: 'jacoco'\"") if output.strip() == "": add_jacoco_plugin = True output, errors, result_code = run_cmd(f"cat {build_gradle_path} | grep \"testCoverageEnabled = true\"") if output.strip() == "": enable_test_coverage = True if add_jacoco_plugin: self.add_jacoco_plugin_to_gradle_file(build_gradle_path, package_name) if enable_test_coverage: self.enable_test_coverage_in_gradle_file(build_gradle_path, package_name) self.provide_compiled_package_name(build_gradle_path, package_name)
def find_build_gradle_path(self, instrumented_app_path): find_gradle_path_cmd = f"grep -l -R \"'com.android.application'\" {settings.WORKING_DIR}{instrumented_app_path} " find_gradle_path_cmd += "| xargs -I {} grep -L \"com.google.android.support:wearable\" {}" find_gradle_path_cmd += "| xargs -I {} grep -L \"com.google.android.wearable:wearable\" {}" find_gradle_path_cmd += "| grep \"build.gradle$\"" output, errors, result_code = run_cmd(find_gradle_path_cmd) grep_result = list(filter(lambda p: p != "", output.split("\n"))) if len(grep_result) != 1: raise Exception("Unable to find build.gradle file in instrumented app path") return grep_result[0]
def get_application_folder_path(self): grep_cmd = f"grep -l -R \"'com.android.application'\" {self.root_project_path()} " grep_cmd += "| xargs -I {} grep -L \"com.google.android.support:wearable\" {}" grep_cmd += "| xargs -I {} grep -L \"com.google.android.wearable:wearable\" {}" grep_cmd += "| grep \"build.gradle$\"" output, errors, result_code = run_cmd(grep_cmd) grep_result = output.strip("\n") if grep_result == "": raise Exception("Unable to find application path inside project.") return str(Path(grep_result).parent) + "/"
def alter_AndroidManifest(self, path: str, package_name: str) -> None: output, errors, result_code = run_cmd( f"cat {path} | grep \"emma updated\"") if output.strip() != "": # we will assume Android Manifest was already modified return is_mod = False content = "" in_stream = open(path) for index, line in enumerate(in_stream): if line.find("</application>") != -1: content += \ f''' <!-- emma updated --> <activity android:label="EmmaInstrumentationActivity" android:name="{package_name}.EmmaInstrument.InstrumentedActivity"/> <receiver android:name="{package_name}.EmmaInstrument.CollectCoverageReceiver"> <intent-filter> <action android:name="evolutiz.emma.COLLECT_COVERAGE" /> </intent-filter> </receiver> <!-- emma updated --> {line} <!-- emma updated --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <instrumentation android:handleProfiling="true" android:label="EmmaInstrumentation" android:name="{package_name}.EmmaInstrument.EmmaInstrumentation" android:targetPackage="{package_name}"/> <!-- emma updated --> ''' is_mod = True else: content += line in_stream.close() os.remove(path) new_file = open(path, "w") new_file.write(content) new_file.close() if not is_mod: print("[Error] Failed when update AndroidManifest.xml")
def start_ella(self, device: Device) -> None: self.prepare_ella_folder_for_device(device) run_cmd(f"find {self.get_device_ella_output_folder_path(device)} -type f -name \"coverage.dat.*\" | " f"xargs -I {{}} rm {{}}") # start ELLA server output, errors, result_code = run_cmd(f"./ella.sh s", cwd=self.get_device_ella_folder_path(device)) if result_code != 0: raise Exception("Unable to start ELLA server") # start the TCP relay service on the device output, errors, result_code = adb.shell_command( device, f"am startservice -n {self.tcp_relay_android_package_name}/.RelayService -a start " f"--ei pA {self.ella_original_port} --ei pB {self.get_device_tcp_relay_port(device)}") if result_code != 0: raise Exception("Unable to start the TCP relay service") # forward the TCP relay port from the emulator to the local PC device.forward_port(self.get_device_tcp_relay_port(device), self.get_device_tcp_relay_port(device)) # forward, locally, the TCP relay port to the ELLA server port run_cmd(f"socat TCP:localhost:{self.get_device_ella_port(device)} TCP:localhost:{self.get_device_tcp_relay_port(device)} &", discard_output=True)
def get_current_apk_output_folder(self, device: Optional[Device] = None) -> str: self.app_path: str = RequiredFeature('app_path').request() # find folder in ELLa's output folder that has the APK's path (or its hash if it is too long) in its name aux = self.app_path.replace("/", "_") if len(aux) > 100: aux = sha256(aux.encode()).hexdigest() output, errors, result_code = run_cmd( f"find -L {self.ella_original_output_folder_path if device is None else self.get_device_ella_output_folder_path(device)} -type d -name \"*{aux}*\"") folder = output.rstrip('\n') if folder == '': raise Exception(f"Unable to find ELLA output folder for app {self.app_path}") return folder
def compress_results(strategy_with_runner_name: str) -> None: app_path = RequiredFeature('app_path').request() app_name = os.path.basename(app_path) repetitions_dir = f"{settings.WORKING_DIR}results/{strategy_with_runner_name}/{app_name}/" output, errors, return_code = run_cmd( f"find -L {repetitions_dir} -maxdepth 1 -mindepth 1 -type d") repetition_dir_paths = output.split('\n') pool = mp.Pool(processes=len(repetition_dir_paths)) for path in repetition_dir_paths: if path.strip() != '': pool.apply_async(compress_repetition, args=(path, )) pool.close() pool.join()
def get_imei(device: 'Device') -> Optional[str]: if device.name not in devices_imei: adb_cmd = f"{get_adb_cmd_prefix_for_device(device)} shell " imei_cmd = f"{adb_cmd}dumpsys iphonesubinfo | grep 'Device ID' | cut -d ' ' -f 6 " # leave commented to avoid infinite recursion # log_adb_command(device, cmd) try: output, errors, result_code = run_cmd(imei_cmd) devices_imei[device.name] = output.strip() except TimeoutExpired as e: return None return devices_imei[device.name]
def generate_test_coverage(self, device: Device, coverage_folder_local_path: str, accumulated_output: str, accumulated_errors: str, script_path: str, generation: int, individual_index: int, test_case_index: int, unique_crashes: Set[str], scripts_crash_status: Dict[str, bool]) -> bool: compiled_package_name: str = RequiredFeature( 'compiled_package_name').request() result_dir: str = RequiredFeature('result_dir').request() # clear app's data and state output, errors, result_code = adb.shell_command( device, f"pm clear {compiled_package_name}") accumulated_output += str(output) accumulated_errors += str(errors) if result_code != 0: adb.log_evaluation_result(device, result_dir, script_path, False) if self.verbose_level > 0: logger.log_progress( f"\n{accumulated_output}\n{accumulated_errors}") raise Exception( f"Unable to clear package for script_path {script_path} in device: {device.name}" ) # copy the covids file to the coverage_folder_local_path ella_coverage_ids_file_path = f"{self.get_current_apk_output_folder(device)}/covids" output, errors, result_code = run_cmd( f"cp {ella_coverage_ids_file_path} {coverage_folder_local_path}/") if result_code != 0: raise Exception( f"Unable to copy the coverage ids file for test script, path is: {ella_coverage_ids_file_path}" ) # Run test case using the requested test runner. # Internally, the test runner will take care of calling the methods AppInstrumentator#setup_for_test_run and # AppInstrumentator#teardown_after_test_run, which will start and stop the ELLA server correspondingly. script_name = script_path.split("/")[-1] test_runner = RequiredFeature('test_runner').request() test_runner.run(device, compiled_package_name, script_name) return self.dump_script_coverage(device, coverage_folder_local_path, accumulated_output, accumulated_errors, script_path, generation, individual_index, test_case_index, unique_crashes, scripts_crash_status)
def get_manifest_path(self, root_path: str) -> str: find_manifest_cmd = f"find -L {root_path} -type f -name AndroidManifest.xml | " # find all manifests find_manifest_cmd += "xargs -I {} grep -l \"android.intent.action.MAIN\" {} | " # which contain a Main Activity find_manifest_cmd += "xargs -I {} grep -L wearable {} | " # and are not a wearable app find_manifest_cmd += "grep -v build | grep -v androidTest | grep -v bin" # also, discard build and test related manifests output, errors, result_code = run_cmd(find_manifest_cmd) files = list( filter( lambda p: p != "" and not ("classes" in os.listdir( os.path.dirname(p)) and "/bin/" in p), output.split("\n"))) if len(files) != 1: raise Exception( f"Unable to find AndroidManifest.xml file for instrumentation. " + f"There seems to be several matches: {', '.join(files)}") return files[0]
def get_coverage( self, device: Device, coverage_folder_local_path: str, accumulated_output: str, accumulated_errors: str, ) -> int: result_dir: str = RequiredFeature('result_dir').request() coverage_ec_local_path = f"{coverage_folder_local_path}/coverage.ec" # pull coverage.ec file from device jacoco_coverage_class_files_path = RequiredFeature('jacoco_coverage_class_files_path').request() output, errors, result_code = adb.pull(device, self.coverage_ec_device_backup_path, coverage_ec_local_path) accumulated_output += output accumulated_errors += errors if result_code != 0: adb.log_evaluation_result(device, result_dir, "pull-coverage", False) if self.verbose_level > 0: logger.log_progress(f"\n{accumulated_output}\n{accumulated_errors}") raise Exception(f"Unable to pull coverage for device: {device.name}") # process coverage.ec file app_path = RequiredFeature('app_path').request() jacoco_cmd = f"java -jar {settings.WORKING_DIR}lib/jacococli.jar " \ f"report coverage.ec " \ f"--classfiles {jacoco_coverage_class_files_path} " \ f"--sourcefiles {settings.WORKING_DIR}{app_path}/src " \ f"--xml jacoco_report.xml " \ f"--html jacoco_html_report" output, errors, result_code = run_cmd(jacoco_cmd, cwd=coverage_folder_local_path) accumulated_output += output accumulated_errors += errors if result_code != 0: adb.log_evaluation_result(device, result_dir, "process-coverage", False) if self.verbose_level > 0: logger.log_progress(f"\n{accumulated_output}\n{accumulated_errors}") raise Exception(f"Unable to process coverage.ec file fetched from device: {device.name}") # parse generated html to extract global line coverage html_path = f"{coverage_folder_local_path}/jacoco_html_report/index.html" return self.extract_coverage(html_path)
def prepare_ella_folder_for_device(self, device: Device): if device.name in self.devices_with_ella_folder: return device_ella_folder_path = self.get_device_ella_folder_path(device) device_ella_settings_path = f"{device_ella_folder_path}ella.settings" device_ella_port = self.get_device_ella_port(device) # copy folder and change port in ELLA settings run_cmd(f"rm -rf {device_ella_folder_path}") run_cmd(f"cp -r {self.ella_original_folder_path} {device_ella_folder_path}", timeout=300) run_cmd(f"sed -i \'s/ella.server.port=23745/ella.server.port={device_ella_port}/g\' {device_ella_settings_path}") self.devices_with_ella_folder.add(device.name)
def install(device: 'Device', package_name: str, apk_path: str) -> None: verbose_level = RequiredFeature('verbose_level').request() output, errors, result_code = adb_command(device, f"install {apk_path}") if result_code != 0 or 'INSTALL_FAILED' in output: error_msg = f"Unable to install apk: {apk_path} on device: {device.name}" if verbose_level > 0: logger.log_progress("\n" + error_msg) logger.log_progress("\n" + output) logger.log_progress("\n" + errors) raise Exception(error_msg) cmd = f"{get_adb_cmd_prefix_for_device(device)} shell pm list packages | grep {package_name}" log_adb_command(device, cmd) res = run_cmd(cmd)[0].strip() if package_name not in res: raise Exception( f"Unable to install apk: {apk_path} on device: {device.name}")
def get_coverage( self, device: Device, coverage_folder_local_path: str, accumulated_output: str, accumulated_errors: str, ) -> int: result_dir: str = RequiredFeature('result_dir').request() coverage_ec_local_path = f"{coverage_folder_local_path}/coverage.ec" # pull coverage.ec file from device output, errors, result_code = adb.pull(device, self.coverage_ec_device_backup_path, coverage_ec_local_path) accumulated_output += output accumulated_errors += errors if result_code != 0: adb.log_evaluation_result(device, result_dir, "pull-coverage", False) if self.verbose_level > 0: logger.log_progress(f"\n{accumulated_output}\n{accumulated_errors}") raise Exception(f"Unable to pull coverage for device: {device.name}") # process coverage.ec file app_path = RequiredFeature('app_path').request() emma_cmd = f"java -cp {settings.WORKING_DIR}lib/emma.jar emma report -r html" \ f" -in coverage.em,coverage.ec -sp {settings.WORKING_DIR}{app_path}/src" output, errors, result_code = run_cmd(emma_cmd, cwd=coverage_folder_local_path) accumulated_output += output accumulated_errors += errors if result_code != 0: adb.log_evaluation_result(device, result_dir, "process-coverage", False) if self.verbose_level > 0: logger.log_progress(f"\n{accumulated_output}\n{accumulated_errors}") raise Exception(f"Unable to process coverage.ec file fetched from device: {device.name}") # parse generated html to extract global line coverage html_path = f"{coverage_folder_local_path}/coverage/index.html" coverage_str = self.extract_coverage(html_path) aux = coverage_str.split("%") coverage = int(aux[0]) return coverage
def get_apk_path(self) -> None: self.apk_path = None if self.instrumented_app_path.endswith(".apk"): self.apk_path = self.instrumented_app_path else: # now find its name output, errors, result_code = run_cmd( f"find -L {self.instrumented_app_path} -name \"*.apk\" | grep -v androidTest | grep -v unaligned" ) apk_paths = [] for file_path in output.split("\n"): if file_path != "": apk_paths.append(file_path) if len(apk_paths) == 0: raise Exception( f"No APKs found inside folder {self.instrumented_app_path} after build." ) if len(apk_paths) > 1: # try to filter APKs based on ETG.config file (might not be present) etg_config_path = f"{self.instrumented_app_path}/etg.config" if os.path.isfile(etg_config_path): etg_config = ETGConfig(etg_config_path) # try to filter by build type build_type = etg_config.build_type() apk_paths = list( filter(lambda path: f"/{build_type}/" in path, apk_paths)) # try to filter by product flavors product_flavors = etg_config.product_flavors() if len(product_flavors) > 0: product_flavors_combined = '' for index, flavor in enumerate(product_flavors): if index == 0: product_flavors_combined += flavor.lower() else: product_flavors_combined += flavor.capitalize() apk_paths = list( filter( lambda path: f"/{product_flavors_combined}/" in path, apk_paths)) if len(apk_paths) == 0: raise Exception( f"Evolutiz was unable to determine which APK inside folder " f"{self.instrumented_app_path} should it use, since neither of them satisfy the " f"combined product flavor provided: {product_flavors_combined} in the ETG config " f"file") else: # TODO: provide more info about ETG config files raise Exception( f"There are several APKs found inside folder {self.instrumented_app_path} after " f"build. Evolutiz was unable to determine which one should it use. " f"You can help it by providing an ETG config file at the root of the app's folder." ) self.apk_path = apk_paths[0] features.provide('apk_path', self.apk_path) assert self.apk_path is not None
def instrument(self) -> None: self.app_path: str = RequiredFeature('app_path').request() self.result_dir: str = RequiredFeature('result_dir').request() self.instrumented_subjects_path: str = RequiredFeature( 'instrumented_subjects_path').request() if not self.app_path.endswith(".apk"): cause = "ELLA instrumentation only works with APKs.\n" \ f"Provided app path was: {self.app_path}" logger.log_progress(cause) raise Exception(cause) apk_filename = os.path.basename(self.app_path) logger.log_progress(f"\nInstrumenting app: {apk_filename}") # delete any previous content in ELLA's output folder run_cmd(f"rm -rf {self.ella_original_output_folder_path}/*") # delete any previous content generated by ELLA in /tmp folder run_cmd(f"rm /tmp/ella* /tmp/inputclasses*") # prepare the ella-customized jars output, errors, result_code = run_cmd( f"python gen-ella-wrappers.py", cwd=self.ella_original_folder_path, timeout=120) if result_code != 0: raise Exception("Unable generate ELLA wrappers") output, errors, result_code = run_cmd( f"ant clean build", cwd=self.ella_original_folder_path) if result_code != 0: raise Exception("Unable compile ELLA JARs") # perform the instrumentation of the APK output, errors, result_code = run_cmd( f"./ella.sh i {self.app_path}", cwd=self.ella_original_folder_path, timeout=120) if result_code != 0: raise Exception( f"Unable instrument {self.app_path} APK using ELLA") os.chdir(settings.WORKING_DIR) # find instrumented APK in ELLA's output folder output, errors, result_code = run_cmd( f"find {self.get_current_apk_output_folder()} -type f -name \"instrumented.apk\"" ) instrumented_app_path = output.rstrip('\n') features.provide('instrumented_app_path', instrumented_app_path) # get package and Main activity name directly from the original APK output, errors, result_code = run_cmd( f"aapt dump badging {self.app_path} | grep package:\\ name") package_name = output.split("package: name=\'")[1].split("\'")[0] features.provide('package_name', package_name) features.provide('compiled_package_name', package_name) output, errors, result_code = run_cmd( f"aapt dump badging {self.app_path} | grep launchable-activity | cut -d' ' -f2 | cut -d\"'\" -f 2" ) main_activity_name = output.rstrip('\n') features.provide('main_activity', main_activity_name)
def get_current_package_name(device: 'Device') -> str: cmd = f"{get_adb_cmd_prefix_for_device(device)} shell dumpsys activity recents | grep 'Recent #0' | cut -d= -f2 | sed 's| .*||' | cut -d '/' -f1" log_adb_command(device, cmd) res = run_cmd(cmd)[0].strip("\n") return res