summaryrefslogtreecommitdiffstats
path: root/testing/mozharness/scripts/android_wrench.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozharness/scripts/android_wrench.py')
-rw-r--r--testing/mozharness/scripts/android_wrench.py256
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()