Bug 1318091 - Support |mach gtest| for android; r=bc
authorGeoff Brown <gbrown@mozilla.com>
Mon, 22 Apr 2019 15:55:23 +0000
changeset 470362 98df47a219e2088d431e3961fe6ea7f99017a1be
parent 470361 f1cc15c2bd27c9f04c15a70a0fd71239156ec104
child 470363 927791bc9befd7ec45fed4a8ac5eca36538e9e60
push id35903
push useropoprus@mozilla.com
push dateMon, 22 Apr 2019 21:46:44 +0000
treeherdermozilla-central@a11bd690638f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbc
bugs1318091, 1519369
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1318091 - Support |mach gtest| for android; r=bc Add basic support for 'mach gtest' on Android. Handling of Android-only and desktop-only options is awkward; I hope to re-visit this after bug 1519369. Differential Revision: https://phabricator.services.mozilla.com/D28129
python/mozbuild/mozbuild/mach_commands.py
testing/gtest/remotegtests.py
--- a/python/mozbuild/mozbuild/mach_commands.py
+++ b/python/mozbuild/mozbuild/mach_commands.py
@@ -592,27 +592,47 @@ class GTestCommands(MachCommandBase):
              "optionally followed by a '-' and another ':'-separated pattern list (called the negative patterns).")
     @CommandArgument('--jobs', '-j', default='1', nargs='?', metavar='jobs', type=int,
         help='Run the tests in parallel using multiple processes.')
     @CommandArgument('--tbpl-parser', '-t', action='store_true',
         help='Output test results in a format that can be parsed by TBPL.')
     @CommandArgument('--shuffle', '-s', action='store_true',
         help='Randomize the execution order of tests.')
 
+    @CommandArgument('--package',
+        default='org.mozilla.geckoview.test',
+        help='(Android only) Package name of test app.')
+    @CommandArgument('--adbpath',
+        dest='adb_path',
+        help='(Android only) Path to adb binary.')
+    @CommandArgument('--deviceSerial',
+        dest='device_serial',
+        help="(Android only) adb serial number of remote device. "
+             "Required when more than one device is connected to the host. "
+             "Use 'adb devices' to see connected devices.")
+    @CommandArgument('--remoteTestRoot',
+        dest='remote_test_root',
+        help='(Android only) Remote directory to use as test root '
+             '(eg. /mnt/sdcard/tests or /data/local/tests).')
+    @CommandArgument('--libxul',
+        dest='libxul_path',
+        help='(Android only) Path to gtest libxul.so.')
+
     @CommandArgumentGroup('debugging')
     @CommandArgument('--debug', action='store_true', group='debugging',
         help='Enable the debugger. Not specifying a --debugger option will result in the default debugger being used.')
     @CommandArgument('--debugger', default=None, type=str, group='debugging',
         help='Name of debugger to use.')
     @CommandArgument('--debugger-args', default=None, metavar='params', type=str,
         group='debugging',
         help='Command-line arguments to pass to the debugger itself; split as the Bourne shell would.')
 
-    def gtest(self, shuffle, jobs, gtest_filter, tbpl_parser, debug, debugger,
-              debugger_args):
+    def gtest(self, shuffle, jobs, gtest_filter, tbpl_parser,
+              package, adb_path, device_serial, remote_test_root, libxul_path,
+              debug, debugger, debugger_args):
 
         # We lazy build gtest because it's slow to link
         try:
             config = self.config_environment
         except Exception:
             print("Please run |./mach build| before |./mach gtest|.")
             return 1
 
@@ -627,16 +647,27 @@ class GTestCommands(MachCommandBase):
         if res:
             print("Could not build xul-gtest")
             return res
 
         if self.substs.get('MOZ_WIDGET_TOOLKIT') == 'cocoa':
             self._run_make(directory='browser/app', target='repackage',
                            ensure_exit_code=True)
 
+        if conditions.is_android(self):
+            if jobs != 1:
+                print("--jobs is not supported on Android and will be ignored")
+            if debug or debugger or debugger_args:
+                print("--debug options are not supported on Android and will be ignored")
+            return self.android_gtest(shuffle, gtest_filter,
+                                      package, adb_path, device_serial, remote_test_root, libxul_path)
+
+        if package or adb_path or device_serial or remote_test_root or libxul_path:
+            print("One or more Android-only options will be ignored")
+
         app_path = self.get_binary_path('app')
         args = [app_path, '-unittest', '--gtest_death_test_style=threadsafe'];
 
         if sys.platform.startswith('win') and \
             'MOZ_LAUNCHER_PROCESS' in self.defines:
             args.append('--wait-for-browser')
 
         if debug or debugger or debugger_args:
@@ -703,16 +734,46 @@ class GTestCommands(MachCommandBase):
 
         # Clamp error code to 255 to prevent overflowing multiple of
         # 256 into 0
         if exit_code > 255:
             exit_code = 255
 
         return exit_code
 
+    def android_gtest(self, shuffle, gtest_filter,
+                      package, adb_path, device_serial, remote_test_root, libxul_path):
+        # setup logging for mozrunner
+        from mozlog.commandline import setup_logging
+        format_args = {'level': self._mach_context.settings['test']['level']}
+        default_format = self._mach_context.settings['test']['format']
+        log = setup_logging('mach-gtest', {}, {default_format: sys.stdout}, format_args)
+
+        # ensure that a device is available and test app is installed
+        from mozrunner.devices.android_device import (verify_android_device, get_adb_path)
+        verify_android_device(self, install=True, app=package, device_serial=device_serial)
+
+        if not adb_path:
+            adb_path = get_adb_path(self)
+        if not libxul_path:
+            libxul_path = os.path.join(self.topobjdir, "dist", "bin", "gtest", "libxul.so")
+
+        # run gtest via remotegtests.py
+        import imp
+        path = os.path.join('testing', 'gtest', 'remotegtests.py')
+        with open(path, 'r') as fh:
+            imp.load_module('remotegtests', fh, path,
+                            ('.py', 'r', imp.PY_SOURCE))
+        import remotegtests
+        tester = remotegtests.RemoteGTests()
+        tester.run_gtest(shuffle, gtest_filter, package, adb_path, device_serial,
+                         remote_test_root, libxul_path, None)
+
+        return 0
+
     def prepend_debugger_args(self, args, debugger, debugger_args):
         '''
         Given an array with program arguments, prepend arguments to run it under a
         debugger.
 
         :param args: The executable and arguments used to run the process normally.
         :param debugger: The debugger to use, or empty to use the default debugger.
         :param debugger_args: Any additional parameters to pass to the debugger.
--- a/testing/gtest/remotegtests.py
+++ b/testing/gtest/remotegtests.py
@@ -25,80 +25,83 @@ LOGGER_NAME = 'gtest'
 log = mozlog.unstructured.getLogger(LOGGER_NAME)
 
 
 class RemoteGTests(object):
     """
        A test harness to run gtest on Android.
     """
 
-    def build_environment(self, options, test_filter):
+    def build_environment(self, shuffle, test_filter):
         """
            Create and return a dictionary of all the appropriate env variables
            and values.
         """
         env = {}
         env["XPCOM_DEBUG_BREAK"] = "stack-and-abort"
         env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
         env["MOZ_CRASHREPORTER"] = "1"
         env["MOZ_RUN_GTEST"] = "1"
+        # custom output parser is mandatory on Android
         env["MOZ_TBPL_PARSER"] = "1"
         env["MOZ_GTEST_LOG_PATH"] = self.remote_log
         env["MOZ_GTEST_MINIDUMPS_PATH"] = self.remote_minidumps
         env["MOZ_IN_AUTOMATION"] = "1"
-        if options.shuffle:
+        if shuffle:
             env["GTEST_SHUFFLE"] = "True"
         if test_filter:
             env["GTEST_FILTER"] = test_filter
 
         return env
 
-    def run_gtest(self, options, test_filter):
+    def run_gtest(self, shuffle, test_filter, package, adb_path, device_serial,
+                  remote_test_root, libxul_path, symbols_path):
         """
            Launch the test app, run gtest, collect test results and wait for completion.
            Return False if a crash or other failure is detected, else True.
         """
-        self.device = mozdevice.ADBDevice(adb=options.adb_path,
-                                          device=options.device_serial,
-                                          test_root=options.test_root,
+        update_mozinfo()
+        self.device = mozdevice.ADBDevice(adb=adb_path,
+                                          device=device_serial,
+                                          test_root=remote_test_root,
                                           logger_name=LOGGER_NAME,
                                           verbose=True)
         root = self.device.test_root
         self.remote_profile = posixpath.join(root, 'gtest-profile')
         self.remote_minidumps = posixpath.join(root, 'gtest-minidumps')
         self.remote_log = posixpath.join(root, 'gtest.log')
-        self.package = options.package
+        self.package = package
         self.cleanup()
         self.device.mkdir(self.remote_profile, parents=True)
         self.device.mkdir(self.remote_minidumps, parents=True)
 
         log.info("Running Android gtest")
         if not self.device.is_app_installed(self.package):
             raise Exception("%s is not installed on this device" % self.package)
         if not self.device._have_root_shell:
             raise Exception("a device with a root shell is required to run Android gtest")
 
         # TODO -- consider packaging the gtest libxul.so in an apk
         remote = "/data/app/%s-1/lib/x86_64/" % self.package
-        self.device.push(options.libxul_path, remote)
+        self.device.push(libxul_path, remote)
 
-        env = self.build_environment(options, test_filter)
+        env = self.build_environment(shuffle, test_filter)
         args = ["-unittest", "--gtest_death_test_style=threadsafe",
                 "-profile %s" % self.remote_profile]
         if 'geckoview' in self.package:
             activity = "TestRunnerActivity"
             self.device.launch_activity(self.package, activity_name=activity,
                                         e10s=False,  # gtest is non-e10s on desktop
                                         moz_env=env, extra_args=args)
         else:
             self.device.launch_fennec(self.package, moz_env=env, extra_args=args)
         waiter = AppWaiter(self.device, self.remote_log)
         timed_out = waiter.wait(self.package)
         self.shutdown(use_kill=True if timed_out else False)
-        if self.check_for_crashes(options.symbols_path):
+        if self.check_for_crashes(symbols_path):
             return False
         return True
 
     def shutdown(self, use_kill):
         """
            Stop the remote application.
            If use_kill is specified, a multi-stage kill procedure is used,
            attempting to trigger ANR and minidump reports before ending
@@ -308,17 +311,17 @@ class remoteGtestOptions(OptionParser):
                         type=str,
                         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. ")
         self.add_option("--remoteTestRoot",
                         action="store",
                         type=str,
-                        dest="test_root",
+                        dest="remote_test_root",
                         help="Remote directory to use as test root "
                              "(eg. /mnt/sdcard/tests or /data/local/tests).")
         self.add_option("--libxul",
                         action="store",
                         type=str,
                         dest="libxul_path",
                         default=None,
                         help="Path to gtest libxul.so.")
@@ -352,22 +355,24 @@ def main():
     options, args = parser.parse_args()
     if not options.libxul_path:
         parser.error("--libxul is required")
         sys.exit(1)
     if len(args) > 1:
         parser.error("only one test_filter is allowed")
         sys.exit(1)
     test_filter = args[0] if args else None
-    update_mozinfo()
     tester = RemoteGTests()
     result = False
     try:
         device_exception = False
-        result = tester.run_gtest(options, test_filter)
+        result = tester.run_gtest(options.shuffle, test_filter, options.package,
+                                  options.adb_path, options.device_serial,
+                                  options.remote_test_root, options.libxul_path,
+                                  options.symbols_path)
     except KeyboardInterrupt:
         log.info("gtest | Received keyboard interrupt")
     except Exception as e:
         log.error(str(e))
         if isinstance(e, mozdevice.ADBTimeoutError):
             device_exception = True
     finally:
         if not device_exception: