diff options
Diffstat (limited to 'testing/mozharness/scripts/android_wrench.py')
-rw-r--r-- | testing/mozharness/scripts/android_wrench.py | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/testing/mozharness/scripts/android_wrench.py b/testing/mozharness/scripts/android_wrench.py new file mode 100644 index 0000000000..be22d403f1 --- /dev/null +++ b/testing/mozharness/scripts/android_wrench.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python +# ***** BEGIN LICENSE BLOCK ***** +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. +# ***** END LICENSE BLOCK ***** + +from __future__ import absolute_import +import datetime +import os +import subprocess +import sys +import time + +# load modules from parent dir +sys.path.insert(1, os.path.dirname(sys.path[0])) + +from mozharness.base.script import BaseScript +from mozharness.mozilla.automation import ( + EXIT_STATUS_DICT, + TBPL_FAILURE, +) +from mozharness.mozilla.mozbase import MozbaseMixin +from mozharness.mozilla.testing.android import AndroidMixin +from mozharness.mozilla.testing.testbase import TestingMixin + + +class AndroidWrench(TestingMixin, BaseScript, MozbaseMixin, AndroidMixin): + def __init__(self, require_config_file=False): + # code in BaseScript.__init__ iterates all the properties to attach + # pre- and post-flight listeners, so we need _is_emulator be defined + # before that happens. Doesn't need to be a real value though. + self._is_emulator = None + + super(AndroidWrench, self).__init__() + if self.device_serial is None: + # Running on an emulator. + self._is_emulator = True + self.device_serial = "emulator-5554" + self.use_gles3 = True + else: + # Running on a device, ensure self.is_emulator returns False. + # The adb binary is preinstalled on the bitbar image and is + # already on the $PATH. + self._is_emulator = False + self._adb_path = "adb" + self._errored = False + + @property + def is_emulator(self): + """Overrides the is_emulator property on AndroidMixin.""" + if self._is_emulator is None: + self._is_emulator = self.device_serial is None + return self._is_emulator + + def activate_virtualenv(self): + """Overrides the method on AndroidMixin to be a no-op, because the + setup for wrench doesn't require a special virtualenv.""" + pass + + def query_abs_dirs(self): + if self.abs_dirs: + return self.abs_dirs + + abs_dirs = {} + + abs_dirs["abs_work_dir"] = os.path.expanduser("~/.wrench") + if os.environ.get("MOZ_AUTOMATION", "0") == "1": + # In automation use the standard work dir if there is one + parent_abs_dirs = super(AndroidWrench, self).query_abs_dirs() + if "abs_work_dir" in parent_abs_dirs: + abs_dirs["abs_work_dir"] = parent_abs_dirs["abs_work_dir"] + + abs_dirs["abs_avds_dir"] = os.path.join(abs_dirs["abs_work_dir"], "avds") + abs_dirs["abs_blob_upload_dir"] = os.path.join(abs_dirs["abs_work_dir"], "logs") + abs_dirs["abs_apk_path"] = os.environ.get( + "WRENCH_APK", "gfx/wr/target/android-artifacts/debug/apk/wrench.apk" + ) + abs_dirs["abs_reftests_path"] = os.environ.get( + "WRENCH_REFTESTS", "gfx/wr/wrench/reftests" + ) + if os.environ.get("MOZ_AUTOMATION", "0") == "1": + fetches_dir = os.environ.get("MOZ_FETCHES_DIR") + if self.is_emulator and fetches_dir: + abs_dirs["abs_sdk_dir"] = os.path.join(fetches_dir, "android-sdk-linux") + else: + abs_dirs["abs_sdk_dir"] = os.path.join( + abs_dirs["abs_work_dir"], "android-sdk-linux" + ) + else: + mozbuild_path = os.environ.get( + "MOZBUILD_STATE_PATH", os.path.expanduser("~/.mozbuild") + ) + mozbuild_sdk = os.environ.get( + "ANDROID_SDK_HOME", os.path.join(mozbuild_path, "android-sdk-linux") + ) + abs_dirs["abs_sdk_dir"] = mozbuild_sdk + + self.abs_dirs = abs_dirs + return self.abs_dirs + + def logcat_start(self): + """Overrides logcat_start in android.py - ensures any pre-existing logcat + is cleared before starting to record the new logcat. This is helpful + when running multiple times in a local emulator.""" + logcat_cmd = [self.adb_path, "-s", self.device_serial, "logcat", "-c"] + self.info(" ".join(logcat_cmd)) + subprocess.check_call(logcat_cmd) + super(AndroidWrench, self).logcat_start() + + def wait_until_process_done(self, process_name, timeout): + """Waits until the specified process has exited. Polls the process list + every 5 seconds until the process disappears. + + :param process_name: string containing the package name of the + application. + :param timeout: integer specifying the maximum time in seconds + to wait for the application to finish. + :returns: boolean - True if the process exited within the indicated + timeout, False if the process had not exited by the timeout. + """ + end_time = datetime.datetime.now() + datetime.timedelta(seconds=timeout) + while self.device.process_exist(process_name, timeout=timeout): + if datetime.datetime.now() > end_time: + return False + time.sleep(5) + + return True + + def setup_sdcard(self): + # Note that we hard-code /sdcard/wrench as the path here, rather than + # using something like self.device.test_root, because it needs to be + # kept in sync with the path hard-coded inside the wrench source code. + self.device.rm("/sdcard/wrench", recursive=True, force=True) + self.device.mkdir("/sdcard/wrench", parents=True) + self.device.push( + self.query_abs_dirs()["abs_reftests_path"], "/sdcard/wrench/reftests" + ) + args_file = os.path.join(self.query_abs_dirs()["abs_work_dir"], "wrench_args") + with open(args_file, "w") as argfile: + if self.is_emulator: + argfile.write("env: WRENCH_REFTEST_CONDITION_EMULATOR=1\n") + else: + argfile.write("env: WRENCH_REFTEST_CONDITION_DEVICE=1\n") + argfile.write("reftest") + self.device.push(args_file, "/sdcard/wrench/args") + + def run_tests(self): + self.timed_screenshots(None) + self.device.launch_application( + app_name="org.mozilla.wrench", + activity_name="android.app.NativeActivity", + intent=None, + ) + self.info("App launched") + done = self.wait_until_process_done("org.mozilla.wrench", timeout=60 * 30) + if not done: + self._errored = True + self.error("Wrench still running after timeout") + + def scrape_logcat(self): + """Wrench will dump the test output to logcat, but for convenience we + want it to show up in the main log. So we scrape it out of the logcat + and dump it to our own log. Note that all output from wrench goes + through the cargo-apk glue stuff, which uses the RustAndroidGlueStdouterr + tag on the output. Also it limits the line length to 512 bytes + (including the null terminator). For reftest unexpected-fail output + this means that the base64 image dump gets wrapped over multiple + lines, so part of what this function does is unwrap that so that the + resulting log is readable by the reftest analyzer.""" + + with open(self.logcat_path(), "r") as f: + self.info("=== scraped logcat output ===") + tag = "RustAndroidGlueStdouterr: " + long_line = None + for line in f: + tag_index = line.find(tag) + if tag_index == -1: + # not a line we care about + continue + line = line[tag_index + len(tag) :].rstrip() + if ( + long_line is None + and "REFTEST " not in line + and "panicked" not in line + ): + # non-interesting line + continue + if long_line is not None: + # continuation of a wrapped line + long_line += line + if len(line) >= 511: + if long_line is None: + # start of a new long line + long_line = line + # else "middle" of a long line that keeps going to the next line + continue + # this line doesn't wrap over to the next, so we can + # print it + if long_line is not None: + line = long_line + long_line = None + if "UNEXPECTED-FAIL" in line or "panicked" in line: + self._errored = True + self.error(line) + else: + self.info(line) + self.info("=== end scraped logcat output ===") + self.info("(see logcat artifact for full logcat") + + def setup_emulator(self): + # Running setup_avds will clobber the existing AVD and redownload it. + # For local testing that's kinda expensive, so we omit that if we + # already have that dir. + if not os.path.exists(self.query_abs_dirs()["abs_avds_dir"]): + self.setup_avds() + + sdk_path = self.query_abs_dirs()["abs_sdk_dir"] + if not os.path.exists(sdk_path): + self.error("Unable to find android SDK at %s" % sdk_path) + return + if os.environ.get("MOZ_AUTOMATION", "0") == "1": + self.start_emulator() + else: + # Can't use start_emulator because it tries to download a non-public + # artifact. Instead we just manually run the launch. + self._launch_emulator() + + def do_test(self): + if self.is_emulator: + self.setup_emulator() + + self.verify_device() + self.info("Logging device properties...") + self.info(self.shell_output("getprop")) + self.info("Installing APK...") + self.install_apk(self.query_abs_dirs()["abs_apk_path"], replace=True) + self.info("Setting up SD card...") + self.setup_sdcard() + self.info("Running tests...") + self.run_tests() + self.info("Tests done; parsing logcat...") + self.logcat_stop() + self.scrape_logcat() + self.info("All done!") + + def check_errors(self): + if self._errored: + self.info("Errors encountered, terminating with error code...") + exit(EXIT_STATUS_DICT[TBPL_FAILURE]) + + +if __name__ == "__main__": + test = AndroidWrench() + test.do_test() + test.check_errors() |