summaryrefslogtreecommitdiffstats
path: root/testing/remotecppunittests.py
diff options
context:
space:
mode:
Diffstat (limited to 'testing/remotecppunittests.py')
-rw-r--r--testing/remotecppunittests.py317
1 files changed, 317 insertions, 0 deletions
diff --git a/testing/remotecppunittests.py b/testing/remotecppunittests.py
new file mode 100644
index 0000000000..91063b5701
--- /dev/null
+++ b/testing/remotecppunittests.py
@@ -0,0 +1,317 @@
+#!/usr/bin/env python
+#
+# 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/.
+
+import os
+import posixpath
+import subprocess
+import sys
+import traceback
+from zipfile import ZipFile
+
+import mozcrash
+import mozfile
+import mozinfo
+import mozlog
+import runcppunittests as cppunittests
+from mozdevice import ADBDeviceFactory, ADBProcessError, ADBTimeoutError
+
+try:
+ from mozbuild.base import MozbuildObject
+
+ build_obj = MozbuildObject.from_environment()
+except ImportError:
+ build_obj = None
+
+
+class RemoteCPPUnitTests(cppunittests.CPPUnitTests):
+ def __init__(self, options, progs):
+ cppunittests.CPPUnitTests.__init__(self)
+ self.options = options
+ self.device = ADBDeviceFactory(
+ adb=options.adb_path or "adb",
+ device=options.device_serial,
+ test_root=options.remote_test_root,
+ )
+ self.remote_test_root = posixpath.join(self.device.test_root, "cppunittests")
+ self.remote_bin_dir = posixpath.join(self.remote_test_root, "b")
+ self.remote_tmp_dir = posixpath.join(self.remote_test_root, "tmp")
+ self.remote_home_dir = posixpath.join(self.remote_test_root, "h")
+ if options.setup:
+ self.setup_bin(progs)
+
+ def setup_bin(self, progs):
+ self.device.rm(self.remote_test_root, force=True, recursive=True)
+ self.device.mkdir(self.remote_home_dir, parents=True)
+ self.device.mkdir(self.remote_tmp_dir)
+ self.device.mkdir(self.remote_bin_dir)
+ self.push_libs()
+ self.push_progs(progs)
+ self.device.chmod(self.remote_bin_dir, recursive=True)
+
+ def push_libs(self):
+ if self.options.local_apk:
+ with mozfile.TemporaryDirectory() as tmpdir:
+ apk_contents = ZipFile(self.options.local_apk)
+
+ for info in apk_contents.infolist():
+ if info.filename.endswith(".so"):
+ print("Pushing %s.." % info.filename, file=sys.stderr)
+ remote_file = posixpath.join(
+ self.remote_bin_dir, os.path.basename(info.filename)
+ )
+ apk_contents.extract(info, tmpdir)
+ local_file = os.path.join(tmpdir, info.filename)
+ with open(local_file, "rb") as f:
+ # Decompress xz-compressed file.
+ if f.read(5)[1:] == "7zXZ":
+ cmd = ["xz", "-df", "--suffix", ".so", local_file]
+ subprocess.check_output(cmd)
+ # xz strips the ".so" file suffix.
+ os.rename(local_file[:-3], local_file)
+ self.device.push(local_file, remote_file)
+
+ elif self.options.local_lib:
+ for path in os.listdir(self.options.local_lib):
+ if path.endswith(".so"):
+ print("Pushing {}..".format(path), file=sys.stderr)
+ remote_file = posixpath.join(self.remote_bin_dir, path)
+ local_file = os.path.join(self.options.local_lib, path)
+ self.device.push(local_file, remote_file)
+ # Additional libraries may be found in a sub-directory such as
+ # "lib/armeabi-v7a"
+ for subdir in ["assets", "lib"]:
+ local_arm_lib = os.path.join(self.options.local_lib, subdir)
+ if os.path.isdir(local_arm_lib):
+ for root, dirs, paths in os.walk(local_arm_lib):
+ for path in paths:
+ if path.endswith(".so"):
+ print("Pushing {}..".format(path), file=sys.stderr)
+ remote_file = posixpath.join(self.remote_bin_dir, path)
+ local_file = os.path.join(root, path)
+ self.device.push(local_file, remote_file)
+
+ def push_progs(self, progs):
+ for local_file in progs:
+ remote_file = posixpath.join(
+ self.remote_bin_dir, os.path.basename(local_file)
+ )
+ self.device.push(local_file, remote_file)
+
+ def build_environment(self):
+ env = self.build_core_environment({})
+ env["LD_LIBRARY_PATH"] = self.remote_bin_dir
+ env["TMPDIR"] = self.remote_tmp_dir
+ env["HOME"] = self.remote_home_dir
+ env["MOZ_XRE_DIR"] = self.remote_bin_dir
+ if self.options.add_env:
+ for envdef in self.options.add_env:
+ envdef_parts = envdef.split("=", 1)
+ if len(envdef_parts) == 2:
+ env[envdef_parts[0]] = envdef_parts[1]
+ elif len(envdef_parts) == 1:
+ env[envdef_parts[0]] = ""
+ else:
+ self.log.warning("invalid --addEnv option skipped: %s" % envdef)
+
+ return env
+
+ def run_one_test(
+ self,
+ prog,
+ env,
+ symbols_path=None,
+ utility_path=None,
+ interactive=False,
+ timeout_factor=1,
+ ):
+ """
+ Run a single C++ unit test program remotely.
+
+ Arguments:
+ * prog: The path to the test program to run.
+ * env: The environment to use for running the program.
+ * symbols_path: A path to a directory containing Breakpad-formatted
+ symbol files for producing stack traces on crash.
+ * timeout_factor: An optional test-specific timeout multiplier.
+
+ Return True if the program exits with a zero status, False otherwise.
+ """
+ basename = os.path.basename(prog)
+ remote_bin = posixpath.join(self.remote_bin_dir, basename)
+ self.log.test_start(basename)
+ test_timeout = cppunittests.CPPUnitTests.TEST_PROC_TIMEOUT * timeout_factor
+
+ try:
+ output = self.device.shell_output(
+ remote_bin, env=env, cwd=self.remote_home_dir, timeout=test_timeout
+ )
+ returncode = 0
+ except ADBTimeoutError:
+ raise
+ except ADBProcessError as e:
+ output = e.adb_process.stdout
+ returncode = e.adb_process.exitcode
+
+ self.log.process_output(basename, "\n%s" % output, command=[remote_bin])
+ with mozfile.TemporaryDirectory() as tempdir:
+ self.device.pull(self.remote_home_dir, tempdir)
+ if mozcrash.check_for_crashes(tempdir, symbols_path, test_name=basename):
+ self.log.test_end(basename, status="CRASH", expected="PASS")
+ return False
+ result = returncode == 0
+ if not result:
+ self.log.test_end(
+ basename,
+ status="FAIL",
+ expected="PASS",
+ message=("test failed with return code %s" % returncode),
+ )
+ else:
+ self.log.test_end(basename, status="PASS", expected="PASS")
+ return result
+
+
+class RemoteCPPUnittestOptions(cppunittests.CPPUnittestOptions):
+ def __init__(self):
+ cppunittests.CPPUnittestOptions.__init__(self)
+ defaults = {}
+
+ self.add_option(
+ "--deviceSerial",
+ action="store",
+ type="string",
+ dest="device_serial",
+ help="adb serial number of remote device. This is required "
+ "when more than one device is connected to the host. "
+ "Use 'adb devices' to see connected devices.",
+ )
+ defaults["device_serial"] = None
+
+ self.add_option(
+ "--adbPath",
+ action="store",
+ type="string",
+ dest="adb_path",
+ help="Path to adb binary.",
+ )
+ defaults["adb_path"] = None
+
+ self.add_option(
+ "--noSetup",
+ action="store_false",
+ dest="setup",
+ help="Do not copy any files to device (to be used only if "
+ "device is already setup).",
+ )
+ defaults["setup"] = True
+
+ self.add_option(
+ "--localLib",
+ action="store",
+ type="string",
+ dest="local_lib",
+ help="Location of libraries to push -- preferably stripped.",
+ )
+ defaults["local_lib"] = None
+
+ self.add_option(
+ "--apk",
+ action="store",
+ type="string",
+ dest="local_apk",
+ help="Local path to Firefox for Android APK.",
+ )
+ defaults["local_apk"] = None
+
+ self.add_option(
+ "--localBinDir",
+ action="store",
+ type="string",
+ dest="local_bin",
+ help="Local path to bin directory.",
+ )
+ defaults["local_bin"] = build_obj.bindir if build_obj is not None else None
+
+ self.add_option(
+ "--remoteTestRoot",
+ action="store",
+ type="string",
+ dest="remote_test_root",
+ help="Remote directory to use as test root "
+ "(eg. /data/local/tmp/test_root).",
+ )
+
+ # /data/local/tmp/test_root is used because it is usually not
+ # possible to set +x permissions on binaries on /mnt/sdcard
+ # and since scope storage on Android 10 causes permission
+ # errors on the sdcard.
+ defaults["remote_test_root"] = "/data/local/tmp/test_root"
+
+ self.add_option(
+ "--addEnv",
+ action="append",
+ type="string",
+ dest="add_env",
+ help="additional remote environment variable definitions "
+ '(eg. --addEnv "somevar=something")',
+ )
+ defaults["add_env"] = None
+
+ self.set_defaults(**defaults)
+
+
+def run_test_harness(options, args):
+ options.xre_path = os.path.abspath(options.xre_path)
+ cppunittests.update_mozinfo()
+ progs = cppunittests.extract_unittests_from_args(
+ args, mozinfo.info, options.manifest_path
+ )
+ tester = RemoteCPPUnitTests(options, [item[0] for item in progs])
+ result = tester.run_tests(
+ progs,
+ options.xre_path,
+ options.symbols_path,
+ )
+ return result
+
+
+def main():
+ parser = RemoteCPPUnittestOptions()
+ mozlog.commandline.add_logging_group(parser)
+ options, args = parser.parse_args()
+ if not args:
+ print(
+ """Usage: %s <test binary> [<test binary>...]""" % sys.argv[0],
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ if options.local_lib is not None and not os.path.isdir(options.local_lib):
+ print(
+ """Error: --localLib directory %s not found""" % options.local_lib,
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ if options.local_apk is not None and not os.path.isfile(options.local_apk):
+ print("""Error: --apk file %s not found""" % options.local_apk, file=sys.stderr)
+ sys.exit(1)
+ if not options.xre_path:
+ print("""Error: --xre-path is required""", file=sys.stderr)
+ sys.exit(1)
+
+ log = mozlog.commandline.setup_logging(
+ "remotecppunittests", options, {"tbpl": sys.stdout}
+ )
+ try:
+ result = run_test_harness(options, args)
+ except Exception as e:
+ log.error(str(e))
+ traceback.print_exc()
+ result = False
+ sys.exit(0 if result else 1)
+
+
+if __name__ == "__main__":
+ main()