示例#1
0
class MochiRemote(MochitestDesktop):
    localProfile = None
    logMessages = []

    def __init__(self, options):
        MochitestDesktop.__init__(self, options.flavor, vars(options))

        verbose = False
        if options.log_mach_verbose or options.log_tbpl_level == 'debug' or \
           options.log_mach_level == 'debug' or options.log_raw_level == 'debug':
            verbose = True
        if hasattr(options, 'log'):
            delattr(options, 'log')

        self.certdbNew = True
        self.chromePushed = False

        expected = options.app.split('/')[-1]
        self.device = ADBDeviceFactory(adb=options.adbPath or 'adb',
                                       device=options.deviceSerial,
                                       test_root=options.remoteTestRoot,
                                       verbose=verbose,
                                       run_as_package=expected)

        if options.remoteTestRoot is None:
            options.remoteTestRoot = self.device.test_root
        options.dumpOutputDirectory = options.remoteTestRoot
        self.remoteLogFile = posixpath.join(options.remoteTestRoot, "logs",
                                            "mochitest.log")
        logParent = posixpath.dirname(self.remoteLogFile)
        self.device.rm(logParent, force=True, recursive=True)
        self.device.mkdir(logParent, parents=True)

        self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile")
        self.device.rm(self.remoteProfile, force=True, recursive=True)

        self.counts = dict()
        self.message_logger = MessageLogger(logger=None)
        self.message_logger.logger = self.log
        process_args = {
            'messageLogger': self.message_logger,
            'counts': self.counts
        }
        self.automation = RemoteAutomation(self.device,
                                           options.remoteappname,
                                           self.remoteProfile,
                                           self.remoteLogFile,
                                           processArgs=process_args)
        self.environment = self.automation.environment

        # Check that Firefox is installed
        expected = options.app.split('/')[-1]
        if not self.device.is_app_installed(expected):
            raise Exception("%s is not installed on this device" % expected)

        self.device.clear_logcat()

        self.remoteModulesDir = posixpath.join(options.remoteTestRoot,
                                               "modules/")

        self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/")
        self.device.rm(self.remoteCache, force=True, recursive=True)

        # move necko cache to a location that can be cleaned up
        options.extraPrefs += [
            "browser.cache.disk.parent_directory=%s" % self.remoteCache
        ]

        self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog")
        self.device.rm(self.remoteMozLog, force=True, recursive=True)
        self.device.mkdir(self.remoteMozLog, parents=True)

        self.remoteChromeTestDir = posixpath.join(options.remoteTestRoot,
                                                  "chrome")
        self.device.rm(self.remoteChromeTestDir, force=True, recursive=True)
        self.device.mkdir(self.remoteChromeTestDir, parents=True)

        procName = options.app.split('/')[-1]
        self.device.stop_application(procName)
        if self.device.process_exist(procName):
            self.log.warning("unable to kill %s before running tests!" %
                             procName)

        # Add Android version (SDK level) to mozinfo so that manifest entries
        # can be conditional on android_version.
        self.log.info(
            "Android sdk version '%s'; will use this to filter manifests" %
            str(self.device.version))
        mozinfo.info['android_version'] = str(self.device.version)
        mozinfo.info['is_fennec'] = not ('geckoview' in options.app)
        mozinfo.info['is_emulator'] = self.device._device_serial.startswith(
            'emulator-')

    def cleanup(self, options, final=False):
        if final:
            self.device.rm(self.remoteChromeTestDir,
                           force=True,
                           recursive=True)
            self.chromePushed = False
            uploadDir = os.environ.get('MOZ_UPLOAD_DIR', None)
            if uploadDir and self.device.is_dir(self.remoteMozLog):
                self.device.pull(self.remoteMozLog, uploadDir)
        self.device.rm(self.remoteLogFile, force=True)
        self.device.rm(self.remoteProfile, force=True, recursive=True)
        self.device.rm(self.remoteCache, force=True, recursive=True)
        MochitestDesktop.cleanup(self, options, final)
        self.localProfile = None

    def dumpScreen(self, utilityPath):
        if self.haveDumpedScreen:
            self.log.info(
                "Not taking screenshot here: see the one that was previously logged"
            )
            return
        self.haveDumpedScreen = True
        if self.device._device_serial.startswith('emulator-'):
            dump_screen(utilityPath, self.log)
        else:
            dump_device_screen(self.device, self.log)

    def findPath(self, paths, filename=None):
        for path in paths:
            p = path
            if filename:
                p = os.path.join(p, filename)
            if os.path.exists(self.getFullPath(p)):
                return path
        return None

    # This seems kludgy, but this class uses paths from the remote host in the
    # options, except when calling up to the base class, which doesn't
    # understand the distinction.  This switches out the remote values for local
    # ones that the base class understands.  This is necessary for the web
    # server, SSL tunnel and profile building functions.
    def switchToLocalPaths(self, options):
        """ Set local paths in the options, return a function that will restore remote values """
        remoteXrePath = options.xrePath
        remoteProfilePath = options.profilePath
        remoteUtilityPath = options.utilityPath

        paths = [
            options.xrePath,
        ]
        if build_obj:
            paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
        options.xrePath = self.findPath(paths)
        if options.xrePath is None:
            self.log.error(
                "unable to find xulrunner path for %s, please specify with --xre-path"
                % os.name)
            sys.exit(1)

        xpcshell = "xpcshell"
        if (os.name == "nt"):
            xpcshell += ".exe"

        if options.utilityPath:
            paths = [options.utilityPath, options.xrePath]
        else:
            paths = [options.xrePath]
        options.utilityPath = self.findPath(paths, xpcshell)

        if options.utilityPath is None:
            self.log.error(
                "unable to find utility path for %s, please specify with --utility-path"
                % os.name)
            sys.exit(1)

        xpcshell_path = os.path.join(options.utilityPath, xpcshell)
        if RemoteAutomation.elf_arm(xpcshell_path):
            self.log.error('xpcshell at %s is an ARM binary; please use '
                           'the --utility-path argument to specify the path '
                           'to a desktop version.' % xpcshell_path)
            sys.exit(1)

        if self.localProfile:
            options.profilePath = self.localProfile
        else:
            options.profilePath = None

        def fixup():
            options.xrePath = remoteXrePath
            options.utilityPath = remoteUtilityPath
            options.profilePath = remoteProfilePath

        return fixup

    def startServers(self, options, debuggerInfo, public=None):
        """ Create the servers on the host and start them up """
        restoreRemotePaths = self.switchToLocalPaths(options)
        MochitestDesktop.startServers(self, options, debuggerInfo, public=True)
        restoreRemotePaths()

    def buildProfile(self, options):
        restoreRemotePaths = self.switchToLocalPaths(options)
        if options.testingModulesDir:
            try:
                self.device.push(options.testingModulesDir,
                                 self.remoteModulesDir)
                self.device.chmod(self.remoteModulesDir, recursive=True)
            except Exception:
                self.log.error(
                    "Automation Error: Unable to copy test modules to device.")
                raise
            savedTestingModulesDir = options.testingModulesDir
            options.testingModulesDir = self.remoteModulesDir
        else:
            savedTestingModulesDir = None
        manifest = MochitestDesktop.buildProfile(self, options)
        if savedTestingModulesDir:
            options.testingModulesDir = savedTestingModulesDir
        self.localProfile = options.profilePath

        restoreRemotePaths()
        options.profilePath = self.remoteProfile
        return manifest

    def buildURLOptions(self, options, env):
        saveLogFile = options.logFile
        options.logFile = self.remoteLogFile
        options.profilePath = self.localProfile
        env["MOZ_HIDE_RESULTS_TABLE"] = "1"
        retVal = MochitestDesktop.buildURLOptions(self, options, env)

        # we really need testConfig.js (for browser chrome)
        try:
            self.device.push(options.profilePath, self.remoteProfile)
            self.device.chmod(self.remoteProfile, recursive=True)
        except Exception:
            self.log.error(
                "Automation Error: Unable to copy profile to device.")
            raise

        options.profilePath = self.remoteProfile
        options.logFile = saveLogFile
        return retVal

    def getChromeTestDir(self, options):
        local = super(MochiRemote, self).getChromeTestDir(options)
        remote = self.remoteChromeTestDir
        if options.flavor == 'chrome' and not self.chromePushed:
            self.log.info("pushing %s to %s on device..." % (local, remote))
            local = os.path.join(local, "chrome")
            self.device.push(local, remote)
            self.chromePushed = True
        return remote

    def getLogFilePath(self, logFile):
        return logFile

    def printDeviceInfo(self, printLogcat=False):
        try:
            if printLogcat:
                logcat = self.device.get_logcat(
                    filter_out_regexps=fennecLogcatFilters)
                for l in logcat:
                    ul = l.decode('utf-8', errors='replace')
                    sl = ul.encode('iso8859-1', errors='replace')
                    self.log.info(sl)
            self.log.info("Device info:")
            devinfo = self.device.get_info()
            for category in devinfo:
                if type(devinfo[category]) is list:
                    self.log.info("  %s:" % category)
                    for item in devinfo[category]:
                        self.log.info("     %s" % item)
                else:
                    self.log.info("  %s: %s" % (category, devinfo[category]))
            self.log.info("Test root: %s" % self.device.test_root)
        except ADBTimeoutError:
            raise
        except Exception as e:
            self.log.warning("Error getting device information: %s" % str(e))

    def getGMPPluginPath(self, options):
        # TODO: bug 1149374
        return None

    def buildBrowserEnv(self, options, debugger=False):
        browserEnv = MochitestDesktop.buildBrowserEnv(self,
                                                      options,
                                                      debugger=debugger)
        # remove desktop environment not used on device
        if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
            del browserEnv["XPCOM_MEM_BLOAT_LOG"]
        if self.mozLogs:
            browserEnv["MOZ_LOG_FILE"] = os.path.join(
                self.remoteMozLog,
                "moz-pid=%PID-uid={}.log".format(str(uuid.uuid4())))
        if options.dmd:
            browserEnv['DMD'] = '1'
        # Contents of remoteMozLog will be pulled from device and copied to the
        # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make
        # MOZ_UPLOAD_DIR available to the browser environment so that tests
        # can use it as though they were running on the host.
        browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog
        return browserEnv

    def runApp(self, *args, **kwargs):
        """front-end automation's `runApp` functionality until FennecRunner is written"""

        # remoteautomation `runApp` takes the profile path,
        # whereas runtest.py's `runApp` takes a mozprofile object.
        if 'profileDir' not in kwargs and 'profile' in kwargs:
            kwargs['profileDir'] = kwargs.pop('profile').profile

        # remove args not supported by automation
        kwargs.pop('marionette_args', None)

        ret, _ = self.automation.runApp(*args, **kwargs)
        self.countpass += self.counts['pass']
        self.countfail += self.counts['fail']
        self.counttodo += self.counts['todo']

        return ret, None
示例#2
0
class JUnitTestRunner(MochitestDesktop):
    """
    A test harness to run geckoview junit tests on a remote device.
    """

    def __init__(self, log, options):
        self.log = log
        self.verbose = False
        if (
            options.log_tbpl_level == "debug"
            or options.log_mach_level == "debug"
            or options.verbose
        ):
            self.verbose = True
        self.device = ADBDeviceFactory(
            adb=options.adbPath or "adb",
            device=options.deviceSerial,
            test_root=options.remoteTestRoot,
            verbose=self.verbose,
            run_as_package=options.app,
        )
        self.options = options
        self.log.debug("options=%s" % vars(options))
        update_mozinfo()
        self.remote_profile = posixpath.join(self.device.test_root, "junit-profile")
        self.remote_filter_list = posixpath.join(
            self.device.test_root, "junit-filters.list"
        )

        if self.options.coverage and not self.options.coverage_output_dir:
            raise UserError(
                "--coverage-output-dir is required when using --enable-coverage"
            )
        if self.options.coverage:
            self.remote_coverage_output_file = posixpath.join(
                self.device.test_root, "junit-coverage.ec"
            )
            self.coverage_output_file = os.path.join(
                self.options.coverage_output_dir, "junit-coverage.ec"
            )

        self.server_init()

        self.cleanup()
        self.device.clear_logcat()
        self.build_profile()
        self.startServers(self.options, debuggerInfo=None, public=True)
        self.log.debug("Servers started")

    def collectLogcatForCurrentTest(self):
        # These are unique start and end markers logged by GeckoSessionTestRule.java
        START_MARKER = "1f0befec-3ff2-40ff-89cf-b127eb38b1ec"
        END_MARKER = "c5ee677f-bc83-49bd-9e28-2d35f3d0f059"
        logcat = self.device.get_logcat()
        test_logcat = ""
        started = False
        for l in logcat:
            if START_MARKER in l and self.test_name in l:
                started = True
            if started:
                test_logcat += l + "\n"
            if started and END_MARKER in l:
                return test_logcat

    def needsWebsocketProcessBridge(self, options):
        """
        Overrides MochitestDesktop.needsWebsocketProcessBridge and always
        returns False as the junit tests do not use the websocket process
        bridge. This is needed to satisfy MochitestDesktop.startServers.
        """
        return False

    def server_init(self):
        """
        Additional initialization required to satisfy MochitestDesktop.startServers
        """
        self._locations = None
        self.server = None
        self.wsserver = None
        self.websocketProcessBridge = None
        self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get("debug") else 90
        if self.options.remoteWebServer is None:
            self.options.remoteWebServer = moznetwork.get_ip()
        self.options.webServer = self.options.remoteWebServer
        self.options.webSocketPort = "9988"
        self.options.httpdPath = None
        self.options.keep_open = False
        self.options.pidFile = ""
        self.options.subsuite = None
        self.options.xrePath = None
        if build_obj and "MOZ_HOST_BIN" in os.environ:
            self.options.xrePath = os.environ["MOZ_HOST_BIN"]
            if not self.options.utilityPath:
                self.options.utilityPath = self.options.xrePath
        if not self.options.xrePath:
            self.options.xrePath = self.options.utilityPath
        if build_obj:
            self.options.certPath = os.path.join(
                build_obj.topsrcdir, "build", "pgo", "certs"
            )

    def build_profile(self):
        """
        Create a local profile with test prefs and proxy definitions and
        push it to the remote device.
        """

        self.profile = Profile(locations=self.locations, proxy=self.proxy(self.options))
        self.options.profilePath = self.profile.profile

        # Set preferences
        self.merge_base_profiles(self.options, "geckoview-junit")
        prefs = parse_preferences(self.options.extra_prefs)
        self.profile.set_preferences(prefs)

        if self.fillCertificateDB(self.options):
            self.log.error("Certificate integration failed")

        self.device.push(self.profile.profile, self.remote_profile)
        self.log.debug(
            "profile %s -> %s" % (str(self.profile.profile), str(self.remote_profile))
        )

    def cleanup(self):
        try:
            self.stopServers()
            self.log.debug("Servers stopped")
            self.device.stop_application(self.options.app)
            self.device.rm(self.remote_profile, force=True, recursive=True)
            if hasattr(self, "profile"):
                del self.profile
            self.device.rm(self.remote_filter_list, force=True)
        except Exception:
            traceback.print_exc()
            self.log.info("Caught and ignored an exception during cleanup")

    def build_command_line(self, test_filters_file, test_filters):
        """
        Construct and return the 'am instrument' command line.
        """
        cmd = "am instrument -w -r"
        # profile location
        cmd = cmd + " -e args '-profile %s'" % self.remote_profile
        # chunks (shards)
        shards = self.options.totalChunks
        shard = self.options.thisChunk
        if shards is not None and shard is not None:
            shard -= 1  # shard index is 0 based
            cmd = cmd + " -e numShards %d -e shardIndex %d" % (shards, shard)

        # test filters: limit run to specific test(s)
        # filter can be class-name or 'class-name#method-name' (single test)
        # Multiple filters must be specified as a line-separated text file
        # and then pushed to the device.
        filter_list_name = None

        if test_filters_file:
            # We specified a pre-existing file, so use that
            filter_list_name = test_filters_file
        elif test_filters:
            if len(test_filters) > 1:
                # Generate the list file from test_filters
                with tempfile.NamedTemporaryFile(delete=False, mode="w") as filter_list:
                    for f in test_filters:
                        print(f, file=filter_list)
                    filter_list_name = filter_list.name
            else:
                # A single filter may be directly appended to the command line
                cmd = cmd + " -e class %s" % test_filters[0]

        if filter_list_name:
            self.device.push(filter_list_name, self.remote_filter_list)

            if test_filters:
                # We only remove the filter list if we generated it as a
                # temporary file.
                os.remove(filter_list_name)

            cmd = cmd + " -e testFile %s" % self.remote_filter_list

        # enable code coverage reports
        if self.options.coverage:
            cmd = cmd + " -e coverage true"
            cmd = cmd + " -e coverageFile %s" % self.remote_coverage_output_file
        # environment
        env = {}
        env["MOZ_CRASHREPORTER"] = "1"
        env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
        env["XPCOM_DEBUG_BREAK"] = "stack"
        env["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
        env["MOZ_IN_AUTOMATION"] = "1"
        env["R_LOG_VERBOSE"] = "1"
        env["R_LOG_LEVEL"] = "6"
        env["R_LOG_DESTINATION"] = "stderr"
        if self.options.enable_webrender:
            env["MOZ_WEBRENDER"] = "1"
        else:
            env["MOZ_WEBRENDER"] = "0"
        if self.options.enable_fission:
            env["MOZ_FORCE_ENABLE_FISSION"] = "1"
        # Add additional env variables
        for [key, value] in [p.split("=", 1) for p in self.options.add_env]:
            env[key] = value

        for (env_count, (env_key, env_val)) in enumerate(six.iteritems(env)):
            cmd = cmd + " -e env%d %s=%s" % (env_count, env_key, env_val)
        # runner
        cmd = cmd + " %s/%s" % (self.options.app, self.options.runner)
        return cmd

    @property
    def locations(self):
        if self._locations is not None:
            return self._locations
        locations_file = os.path.join(here, "server-locations.txt")
        self._locations = ServerLocations(locations_file)
        return self._locations

    def need_more_runs(self):
        if self.options.run_until_failure and (self.fail_count == 0):
            return True
        if self.runs <= self.options.repeat:
            return True
        return False

    def run_tests(self, test_filters_file=None, test_filters=None):
        """
        Run the tests.
        """
        if not self.device.is_app_installed(self.options.app):
            raise UserError("%s is not installed" % self.options.app)
        if self.device.process_exist(self.options.app):
            raise UserError(
                "%s already running before starting tests" % self.options.app
            )
        # test_filters_file and test_filters must be mutually-exclusive
        if test_filters_file and test_filters:
            raise UserError(
                "Test filters may not be specified when test-filters-file is provided"
            )

        self.test_started = False
        self.pass_count = 0
        self.fail_count = 0
        self.todo_count = 0
        self.total_count = 0
        self.runs = 0
        self.seen_last_test = False

        def callback(line):
            # Output callback: Parse the raw junit log messages, translating into
            # treeherder-friendly test start/pass/fail messages.

            line = six.ensure_str(line)
            self.log.process_output(self.options.app, str(line))
            # Expect per-test info like: "INSTRUMENTATION_STATUS: class=something"
            match = re.match(r"INSTRUMENTATION_STATUS:\s*class=(.*)", line)
            if match:
                self.class_name = match.group(1)
            # Expect per-test info like: "INSTRUMENTATION_STATUS: test=something"
            match = re.match(r"INSTRUMENTATION_STATUS:\s*test=(.*)", line)
            if match:
                self.test_name = match.group(1)
            match = re.match(r"INSTRUMENTATION_STATUS:\s*numtests=(.*)", line)
            if match:
                self.total_count = int(match.group(1))
            match = re.match(r"INSTRUMENTATION_STATUS:\s*current=(.*)", line)
            if match:
                self.current_test_id = int(match.group(1))
            match = re.match(r"INSTRUMENTATION_STATUS:\s*stack=(.*)", line)
            if match:
                self.exception_message = match.group(1)
            if (
                "org.mozilla.geckoview.test.rule.TestHarnessException"
                in self.exception_message
            ):
                # This is actually a problem in the test harness itself
                raise JavaTestHarnessException(self.exception_message)

            # Expect per-test info like: "INSTRUMENTATION_STATUS_CODE: 0|1|..."
            match = re.match(r"INSTRUMENTATION_STATUS_CODE:\s*([+-]?\d+)", line)
            if match:
                status = match.group(1)
                full_name = "%s#%s" % (self.class_name, self.test_name)
                if full_name == self.current_full_name:
                    # A crash in the test harness might cause us to ignore tests,
                    # so we double check that we've actually ran all the tests
                    if self.total_count == self.current_test_id:
                        self.seen_last_test = True

                    if status == "0":
                        message = ""
                        status = "PASS"
                        expected = "PASS"
                        self.pass_count += 1
                        if self.verbose:
                            self.log.info("Printing logcat for test:")
                            print(self.collectLogcatForCurrentTest())
                    elif status == "-3":  # ignored (skipped)
                        message = ""
                        status = "SKIP"
                        expected = "SKIP"
                        self.todo_count += 1
                    elif status == "-4":  # known fail
                        message = ""
                        status = "FAIL"
                        expected = "FAIL"
                        self.todo_count += 1
                    else:
                        if self.exception_message:
                            message = self.exception_message
                        else:
                            message = "status %s" % status
                        status = "FAIL"
                        expected = "PASS"
                        self.fail_count += 1
                        self.log.info("Printing logcat for test:")
                        print(self.collectLogcatForCurrentTest())
                    self.log.test_end(full_name, status, expected, message)
                    self.test_started = False
                else:
                    if self.test_started:
                        # next test started without reporting previous status
                        self.fail_count += 1
                        status = "FAIL"
                        expected = "PASS"
                        self.log.test_end(
                            self.current_full_name,
                            status,
                            expected,
                            "missing test completion status",
                        )
                    self.log.test_start(full_name)
                    self.test_started = True
                    self.current_full_name = full_name

        # Ideally all test names should be reported to suite_start, but these test
        # names are not known in advance.
        self.log.suite_start(["geckoview-junit"])
        try:
            self.device.grant_runtime_permissions(self.options.app)
            cmd = self.build_command_line(
                test_filters_file=test_filters_file, test_filters=test_filters
            )
            while self.need_more_runs():
                self.class_name = ""
                self.exception_message = ""
                self.test_name = ""
                self.current_full_name = ""
                self.current_test_id = 0
                self.runs += 1
                self.log.info("launching %s" % cmd)
                p = self.device.shell(
                    cmd, timeout=self.options.max_time, stdout_callback=callback
                )
                if p.timedout:
                    self.log.error(
                        "TEST-UNEXPECTED-TIMEOUT | runjunit.py | "
                        "Timed out after %d seconds" % self.options.max_time
                    )
            self.log.info("Passed: %d" % self.pass_count)
            self.log.info("Failed: %d" % self.fail_count)
            self.log.info("Todo: %d" % self.todo_count)
            if not self.seen_last_test:
                self.log.error(
                    "TEST-UNEXPECTED-FAIL | runjunit.py | "
                    "Some tests did not run (probably due to a crash in the harness)"
                )
        finally:
            self.log.suite_end()

        if self.check_for_crashes():
            self.fail_count = 1

        if self.options.coverage:
            try:
                self.device.pull(
                    self.remote_coverage_output_file, self.coverage_output_file
                )
            except ADBError:
                # Avoid a task retry in case the code coverage file is not found.
                self.log.error(
                    "No code coverage file (%s) found on remote device"
                    % self.remote_coverage_output_file
                )
                return -1

        return 1 if self.fail_count else 0

    def check_for_crashes(self):
        symbols_path = self.options.symbolsPath
        try:
            dump_dir = tempfile.mkdtemp()
            remote_dir = posixpath.join(self.remote_profile, "minidumps")
            if not self.device.is_dir(remote_dir):
                return False
            self.device.pull(remote_dir, dump_dir)
            crashed = mozcrash.log_crashes(
                self.log, dump_dir, symbols_path, test=self.current_full_name
            )
        finally:
            try:
                shutil.rmtree(dump_dir)
            except Exception:
                self.log.warning("unable to remove directory: %s" % dump_dir)
        return crashed
示例#3
0
class RemoteReftest(RefTest):
    use_marionette = False
    resolver_cls = RemoteReftestResolver

    def __init__(self, options, scriptDir):
        RefTest.__init__(self, options.suite)
        self.run_by_manifest = False
        self.scriptDir = scriptDir
        self.localLogName = options.localLogName

        verbose = False
        if (options.log_mach_verbose or options.log_tbpl_level == "debug"
                or options.log_mach_level == "debug"
                or options.log_raw_level == "debug"):
            verbose = True
            print("set verbose!")
        expected = options.app.split("/")[-1]
        self.device = ADBDeviceFactory(
            adb=options.adb_path or "adb",
            device=options.deviceSerial,
            test_root=options.remoteTestRoot,
            verbose=verbose,
            run_as_package=expected,
        )
        if options.remoteTestRoot is None:
            options.remoteTestRoot = posixpath.join(self.device.test_root,
                                                    "reftest")
        options.remoteProfile = posixpath.join(options.remoteTestRoot,
                                               "profile")
        options.remoteLogFile = posixpath.join(options.remoteTestRoot,
                                               "reftest.log")
        options.logFile = options.remoteLogFile
        self.remoteProfile = options.remoteProfile
        self.remoteTestRoot = options.remoteTestRoot

        if not options.ignoreWindowSize:
            parts = self.device.get_info("screen")["screen"][0].split()
            width = int(parts[0].split(":")[1])
            height = int(parts[1].split(":")[1])
            if width < 1366 or height < 1050:
                self.error("ERROR: Invalid screen resolution %sx%s, "
                           "please adjust to 1366x1050 or higher" %
                           (width, height))

        self._populate_logger(options)
        self.outputHandler = OutputHandler(self.log, options.utilityPath,
                                           options.symbolsPath)
        # RemoteAutomation.py's 'messageLogger' is also used by mochitest. Mimic a mochitest
        # MessageLogger object to re-use this code path.
        self.outputHandler.write = self.outputHandler.__call__
        args = {"messageLogger": self.outputHandler}
        self.automation = RemoteAutomation(
            self.device,
            appName=options.app,
            remoteProfile=self.remoteProfile,
            remoteLog=options.remoteLogFile,
            processArgs=args,
        )

        self.environment = self.automation.environment
        self.SERVER_STARTUP_TIMEOUT = 90

        self.remoteCache = os.path.join(options.remoteTestRoot, "cache/")

        # Check that Firefox is installed
        expected = options.app.split("/")[-1]
        if not self.device.is_app_installed(expected):
            raise Exception("%s is not installed on this device" % expected)
        self.device.run_as_package = expected
        self.device.clear_logcat()

        self.device.rm(self.remoteCache, force=True, recursive=True)

        procName = options.app.split("/")[-1]
        self.device.stop_application(procName)
        if self.device.process_exist(procName):
            self.log.error("unable to kill %s before starting tests!" %
                           procName)

    def findPath(self, paths, filename=None):
        for path in paths:
            p = path
            if filename:
                p = os.path.join(p, filename)
            if os.path.exists(self.getFullPath(p)):
                return path
        return None

    def startWebServer(self, options):
        """ Create the webserver on the host and start it up """
        remoteXrePath = options.xrePath
        remoteUtilityPath = options.utilityPath

        paths = [options.xrePath]
        if build_obj:
            paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
        options.xrePath = self.findPath(paths)
        if options.xrePath is None:
            print("ERROR: unable to find xulrunner path for %s, "
                  "please specify with --xre-path" % (os.name))
            return 1
        paths.append("bin")
        paths.append(os.path.join("..", "bin"))

        xpcshell = "xpcshell"
        if os.name == "nt":
            xpcshell += ".exe"

        if options.utilityPath:
            paths.insert(0, options.utilityPath)
        options.utilityPath = self.findPath(paths, xpcshell)
        if options.utilityPath is None:
            print("ERROR: unable to find utility path for %s, "
                  "please specify with --utility-path" % (os.name))
            return 1

        options.serverProfilePath = tempfile.mkdtemp()
        self.server = ReftestServer(options, self.scriptDir, self.log)
        retVal = self.server.start()
        if retVal:
            return retVal
        retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
        if retVal:
            return retVal

        options.xrePath = remoteXrePath
        options.utilityPath = remoteUtilityPath
        return 0

    def stopWebServer(self, options):
        self.server.stop()

    def killNamedProc(self, pname, orphans=True):
        """ Kill processes matching the given command name """
        try:
            import psutil
        except ImportError as e:
            self.log.warning("Unable to import psutil: %s" % str(e))
            self.log.warning(
                "Unable to verify that %s is not already running." % pname)
            return

        self.log.info("Checking for %s processes..." % pname)

        for proc in psutil.process_iter():
            try:
                if proc.name() == pname:
                    procd = proc.as_dict(
                        attrs=["pid", "ppid", "name", "username"])
                    if proc.ppid() == 1 or not orphans:
                        self.log.info("killing %s" % procd)
                        try:
                            os.kill(proc.pid,
                                    getattr(signal, "SIGKILL", signal.SIGTERM))
                        except Exception as e:
                            self.log.info("Failed to kill process %d: %s" %
                                          (proc.pid, str(e)))
                    else:
                        self.log.info("NOT killing %s (not an orphan?)" %
                                      procd)
            except Exception:
                # may not be able to access process info for all processes
                continue

    def createReftestProfile(self, options, **kwargs):
        profile = RefTest.createReftestProfile(self,
                                               options,
                                               server=options.remoteWebServer,
                                               port=options.httpPort,
                                               **kwargs)
        profileDir = profile.profile
        prefs = {}
        prefs["app.update.url.android"] = ""
        prefs["reftest.remote"] = True
        prefs[
            "datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
        # move necko cache to a location that can be cleaned up
        prefs["browser.cache.disk.parent_directory"] = self.remoteCache

        prefs["layout.css.devPixelsPerPx"] = "1.0"
        # Because Fennec is a little wacky (see bug 1156817) we need to load the
        # reftest pages at 1.0 zoom, rather than zooming to fit the CSS viewport.
        prefs["apz.allow_zooming"] = False

        # Set the extra prefs.
        profile.set_preferences(prefs)

        try:
            self.device.push(profileDir, options.remoteProfile)
            # make sure the parent directories of the profile which
            # may have been created by the push, also have their
            # permissions set to allow access.
            self.device.chmod(options.remoteTestRoot, recursive=True)
        except Exception:
            print("Automation Error: Failed to copy profiledir to device")
            raise

        return profile

    def printDeviceInfo(self, printLogcat=False):
        try:
            if printLogcat:
                logcat = self.device.get_logcat(
                    filter_out_regexps=fennecLogcatFilters)
                for l in logcat:
                    ul = l.decode("utf-8", errors="replace")
                    sl = ul.encode("iso8859-1", errors="replace")
                    print("%s\n" % sl)
            print("Device info:")
            devinfo = self.device.get_info()
            for category in devinfo:
                if type(devinfo[category]) is list:
                    print("  %s:" % category)
                    for item in devinfo[category]:
                        print("     %s" % item)
                else:
                    print("  %s: %s" % (category, devinfo[category]))
            print("Test root: %s" % self.device.test_root)
        except ADBTimeoutError:
            raise
        except Exception as e:
            print("WARNING: Error getting device information: %s" % str(e))

    def environment(self, **kwargs):
        return self.automation.environment(**kwargs)

    def buildBrowserEnv(self, options, profileDir):
        browserEnv = RefTest.buildBrowserEnv(self, options, profileDir)
        # remove desktop environment not used on device
        if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
            del browserEnv["XPCOM_MEM_BLOAT_LOG"]
        return browserEnv

    def runApp(self,
               options,
               cmdargs=None,
               timeout=None,
               debuggerInfo=None,
               symbolsPath=None,
               valgrindPath=None,
               valgrindArgs=None,
               valgrindSuppFiles=None,
               **profileArgs):
        if cmdargs is None:
            cmdargs = []

        if self.use_marionette:
            cmdargs.append("-marionette")

        binary = options.app
        profile = self.createReftestProfile(options, **profileArgs)

        # browser environment
        env = self.buildBrowserEnv(options, profile.profile)

        self.log.info("Running with e10s: {}".format(options.e10s))
        self.log.info("Running with fission: {}".format(options.fission))
        status, self.lastTestSeen = self.automation.runApp(
            None,
            env,
            binary,
            profile.profile,
            cmdargs,
            utilityPath=options.utilityPath,
            xrePath=options.xrePath,
            debuggerInfo=debuggerInfo,
            symbolsPath=symbolsPath,
            timeout=timeout,
            e10s=options.e10s,
        )

        self.cleanup(profile.profile)
        return status

    def cleanup(self, profileDir):
        self.device.rm(self.remoteTestRoot, force=True, recursive=True)
        self.device.rm(self.remoteProfile, force=True, recursive=True)
        self.device.rm(self.remoteCache, force=True, recursive=True)
        RefTest.cleanup(self, profileDir)
示例#4
0
class MochiRemote(MochitestDesktop):
    localProfile = None
    logMessages = []

    def __init__(self, options):
        MochitestDesktop.__init__(self, options.flavor, vars(options))

        verbose = False
        if (options.log_mach_verbose or options.log_tbpl_level == "debug"
                or options.log_mach_level == "debug"
                or options.log_raw_level == "debug"):
            verbose = True
        if hasattr(options, "log"):
            delattr(options, "log")

        self.certdbNew = True
        self.chromePushed = False

        expected = options.app.split("/")[-1]
        self.device = ADBDeviceFactory(
            adb=options.adbPath or "adb",
            device=options.deviceSerial,
            test_root=options.remoteTestRoot,
            verbose=verbose,
            run_as_package=expected,
        )

        if options.remoteTestRoot is None:
            options.remoteTestRoot = self.device.test_root
        options.dumpOutputDirectory = options.remoteTestRoot
        self.remoteLogFile = posixpath.join(options.remoteTestRoot, "logs",
                                            "mochitest.log")
        logParent = posixpath.dirname(self.remoteLogFile)
        self.device.rm(logParent, force=True, recursive=True)
        self.device.mkdir(logParent, parents=True)

        self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile")
        self.device.rm(self.remoteProfile, force=True, recursive=True)

        self.message_logger = MessageLogger(logger=None)
        self.message_logger.logger = self.log

        # Check that Firefox is installed
        expected = options.app.split("/")[-1]
        if not self.device.is_app_installed(expected):
            raise Exception("%s is not installed on this device" % expected)

        self.device.clear_logcat()

        self.remoteModulesDir = posixpath.join(options.remoteTestRoot,
                                               "modules/")

        self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/")
        self.device.rm(self.remoteCache, force=True, recursive=True)

        # move necko cache to a location that can be cleaned up
        options.extraPrefs += [
            "browser.cache.disk.parent_directory=%s" % self.remoteCache
        ]

        self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog")
        self.device.rm(self.remoteMozLog, force=True, recursive=True)
        self.device.mkdir(self.remoteMozLog, parents=True)

        self.remoteChromeTestDir = posixpath.join(options.remoteTestRoot,
                                                  "chrome")
        self.device.rm(self.remoteChromeTestDir, force=True, recursive=True)
        self.device.mkdir(self.remoteChromeTestDir, parents=True)

        self.appName = options.remoteappname
        self.device.stop_application(self.appName)
        if self.device.process_exist(self.appName):
            self.log.warning("unable to kill %s before running tests!" %
                             self.appName)

        # Add Android version (SDK level) to mozinfo so that manifest entries
        # can be conditional on android_version.
        self.log.info(
            "Android sdk version '%s'; will use this to filter manifests" %
            str(self.device.version))
        mozinfo.info["android_version"] = str(self.device.version)
        mozinfo.info["is_fennec"] = not ("geckoview" in options.app)
        mozinfo.info["is_emulator"] = self.device._device_serial.startswith(
            "emulator-")

    def cleanup(self, options, final=False):
        if final:
            self.device.rm(self.remoteChromeTestDir,
                           force=True,
                           recursive=True)
            self.chromePushed = False
            uploadDir = os.environ.get("MOZ_UPLOAD_DIR", None)
            if uploadDir and self.device.is_dir(self.remoteMozLog):
                self.device.pull(self.remoteMozLog, uploadDir)
        self.device.rm(self.remoteLogFile, force=True)
        self.device.rm(self.remoteProfile, force=True, recursive=True)
        self.device.rm(self.remoteCache, force=True, recursive=True)
        MochitestDesktop.cleanup(self, options, final)
        self.localProfile = None

    def dumpScreen(self, utilityPath):
        if self.haveDumpedScreen:
            self.log.info(
                "Not taking screenshot here: see the one that was previously logged"
            )
            return
        self.haveDumpedScreen = True
        if self.device._device_serial.startswith("emulator-"):
            dump_screen(utilityPath, self.log)
        else:
            dump_device_screen(self.device, self.log)

    def findPath(self, paths, filename=None):
        for path in paths:
            p = path
            if filename:
                p = os.path.join(p, filename)
            if os.path.exists(self.getFullPath(p)):
                return path
        return None

    # This seems kludgy, but this class uses paths from the remote host in the
    # options, except when calling up to the base class, which doesn't
    # understand the distinction.  This switches out the remote values for local
    # ones that the base class understands.  This is necessary for the web
    # server, SSL tunnel and profile building functions.
    def switchToLocalPaths(self, options):
        """ Set local paths in the options, return a function that will restore remote values """
        remoteXrePath = options.xrePath
        remoteProfilePath = options.profilePath
        remoteUtilityPath = options.utilityPath

        paths = [
            options.xrePath,
        ]
        if build_obj:
            paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
        options.xrePath = self.findPath(paths)
        if options.xrePath is None:
            self.log.error(
                "unable to find xulrunner path for %s, please specify with --xre-path"
                % os.name)
            sys.exit(1)

        xpcshell = "xpcshell"
        if os.name == "nt":
            xpcshell += ".exe"

        if options.utilityPath:
            paths = [options.utilityPath, options.xrePath]
        else:
            paths = [options.xrePath]
        options.utilityPath = self.findPath(paths, xpcshell)

        if options.utilityPath is None:
            self.log.error(
                "unable to find utility path for %s, please specify with --utility-path"
                % os.name)
            sys.exit(1)

        xpcshell_path = os.path.join(options.utilityPath, xpcshell)
        if RemoteProcessMonitor.elf_arm(xpcshell_path):
            self.log.error("xpcshell at %s is an ARM binary; please use "
                           "the --utility-path argument to specify the path "
                           "to a desktop version." % xpcshell_path)
            sys.exit(1)

        if self.localProfile:
            options.profilePath = self.localProfile
        else:
            options.profilePath = None

        def fixup():
            options.xrePath = remoteXrePath
            options.utilityPath = remoteUtilityPath
            options.profilePath = remoteProfilePath

        return fixup

    def startServers(self, options, debuggerInfo, public=None):
        """ Create the servers on the host and start them up """
        restoreRemotePaths = self.switchToLocalPaths(options)
        MochitestDesktop.startServers(self, options, debuggerInfo, public=True)
        restoreRemotePaths()

    def buildProfile(self, options):
        restoreRemotePaths = self.switchToLocalPaths(options)
        if options.testingModulesDir:
            try:
                self.device.push(options.testingModulesDir,
                                 self.remoteModulesDir)
                self.device.chmod(self.remoteModulesDir, recursive=True)
            except Exception:
                self.log.error(
                    "Automation Error: Unable to copy test modules to device.")
                raise
            savedTestingModulesDir = options.testingModulesDir
            options.testingModulesDir = self.remoteModulesDir
        else:
            savedTestingModulesDir = None
        manifest = MochitestDesktop.buildProfile(self, options)
        if savedTestingModulesDir:
            options.testingModulesDir = savedTestingModulesDir
        self.localProfile = options.profilePath

        restoreRemotePaths()
        options.profilePath = self.remoteProfile
        return manifest

    def buildURLOptions(self, options, env):
        saveLogFile = options.logFile
        options.logFile = self.remoteLogFile
        options.profilePath = self.localProfile
        env["MOZ_HIDE_RESULTS_TABLE"] = "1"
        retVal = MochitestDesktop.buildURLOptions(self, options, env)

        # we really need testConfig.js (for browser chrome)
        try:
            self.device.push(options.profilePath, self.remoteProfile)
            self.device.chmod(self.remoteProfile, recursive=True)
        except Exception:
            self.log.error(
                "Automation Error: Unable to copy profile to device.")
            raise

        options.profilePath = self.remoteProfile
        options.logFile = saveLogFile
        return retVal

    def getChromeTestDir(self, options):
        local = super(MochiRemote, self).getChromeTestDir(options)
        remote = self.remoteChromeTestDir
        if options.flavor == "chrome" and not self.chromePushed:
            self.log.info("pushing %s to %s on device..." % (local, remote))
            local = os.path.join(local, "chrome")
            self.device.push(local, remote)
            self.chromePushed = True
        return remote

    def getLogFilePath(self, logFile):
        return logFile

    def printDeviceInfo(self, printLogcat=False):
        try:
            if printLogcat:
                logcat = self.device.get_logcat()
                for l in logcat:
                    ul = l.decode("utf-8", errors="replace")
                    sl = ul.encode("iso8859-1", errors="replace")
                    self.log.info(sl)
            self.log.info("Device info:")
            devinfo = self.device.get_info()
            for category in devinfo:
                if type(devinfo[category]) is list:
                    self.log.info("  %s:" % category)
                    for item in devinfo[category]:
                        self.log.info("     %s" % item)
                else:
                    self.log.info("  %s: %s" % (category, devinfo[category]))
            self.log.info("Test root: %s" % self.device.test_root)
        except ADBTimeoutError:
            raise
        except Exception as e:
            self.log.warning("Error getting device information: %s" % str(e))

    def getGMPPluginPath(self, options):
        # TODO: bug 1149374
        return None

    def environment(self, env=None, crashreporter=True, **kwargs):
        # Since running remote, do not mimic the local env: do not copy os.environ
        if env is None:
            env = {}

        if crashreporter:
            env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
            env["MOZ_CRASHREPORTER"] = "1"
            env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
        else:
            env["MOZ_CRASHREPORTER_DISABLE"] = "1"

        # Crash on non-local network connections by default.
        # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
        # enable non-local connections for the purposes of local testing.
        # Don't override the user's choice here.  See bug 1049688.
        env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")

        # Send an env var noting that we are in automation. Passing any
        # value except the empty string will declare the value to exist.
        #
        # This may be used to disabled network connections during testing, e.g.
        # Switchboard & telemetry uploads.
        env.setdefault("MOZ_IN_AUTOMATION", "1")

        # Set WebRTC logging in case it is not set yet.
        env.setdefault("R_LOG_LEVEL", "6")
        env.setdefault("R_LOG_DESTINATION", "stderr")
        env.setdefault("R_LOG_VERBOSE", "1")

        return env

    def buildBrowserEnv(self, options, debugger=False):
        browserEnv = MochitestDesktop.buildBrowserEnv(self,
                                                      options,
                                                      debugger=debugger)
        # remove desktop environment not used on device
        if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
            del browserEnv["XPCOM_MEM_BLOAT_LOG"]
        if self.mozLogs:
            browserEnv["MOZ_LOG_FILE"] = os.path.join(
                self.remoteMozLog,
                "moz-pid=%PID-uid={}.log".format(str(uuid.uuid4())))
        if options.dmd:
            browserEnv["DMD"] = "1"
        # Contents of remoteMozLog will be pulled from device and copied to the
        # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make
        # MOZ_UPLOAD_DIR available to the browser environment so that tests
        # can use it as though they were running on the host.
        browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog
        return browserEnv

    def runApp(
        self,
        testUrl,
        env,
        app,
        profile,
        extraArgs,
        utilityPath,
        debuggerInfo=None,
        valgrindPath=None,
        valgrindArgs=None,
        valgrindSuppFiles=None,
        symbolsPath=None,
        timeout=-1,
        detectShutdownLeaks=False,
        screenshotOnFail=False,
        bisectChunk=None,
        marionette_args=None,
        e10s=True,
        runFailures=False,
        crashAsPass=False,
    ):
        """
        Run the app, log the duration it took to execute, return the status code.
        Kill the app if it outputs nothing for |timeout| seconds.
        """

        if timeout == -1:
            timeout = self.DEFAULT_TIMEOUT

        rpm = RemoteProcessMonitor(
            self.appName,
            self.device,
            self.log,
            self.message_logger,
            self.remoteLogFile,
            self.remoteProfile,
        )
        startTime = datetime.datetime.now()
        status = 0
        profileDirectory = self.remoteProfile + "/"
        args = []
        args.extend(extraArgs)
        args.extend(("-no-remote", "-profile", profileDirectory))

        pid = rpm.launch(
            app,
            debuggerInfo,
            testUrl,
            args,
            env=self.environment(env=env, crashreporter=not debuggerInfo),
            e10s=e10s,
        )

        # TODO: not using runFailures or crashAsPass, if we choose to use them
        # we need to adjust status and check_for_crashes
        self.log.info("runtestsremote.py | Application pid: %d" % pid)
        if not rpm.wait(timeout):
            status = 1
        self.log.info("runtestsremote.py | Application ran for: %s" %
                      str(datetime.datetime.now() - startTime))
        crashed = self.check_for_crashes(symbolsPath, rpm.last_test_seen)
        if crashed:
            status = 1

        self.countpass += rpm.counts["pass"]
        self.countfail += rpm.counts["fail"]
        self.counttodo += rpm.counts["todo"]

        return status, rpm.last_test_seen

    def check_for_crashes(self, symbols_path, last_test_seen):
        """
        Pull any minidumps from remote profile and log any associated crashes.
        """
        try:
            dump_dir = tempfile.mkdtemp()
            remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps")
            if not self.device.is_dir(remote_crash_dir):
                return False
            self.device.pull(remote_crash_dir, dump_dir)
            crashed = mozcrash.log_crashes(self.log,
                                           dump_dir,
                                           symbols_path,
                                           test=last_test_seen)
        finally:
            try:
                shutil.rmtree(dump_dir)
            except Exception as e:
                self.log.warning("unable to remove directory %s: %s" %
                                 (dump_dir, str(e)))
        return crashed
示例#5
0
class RemoteReftest(RefTest):
    use_marionette = False
    resolver_cls = RemoteReftestResolver

    def __init__(self, options, scriptDir):
        RefTest.__init__(self, options.suite)
        self.run_by_manifest = False
        self.scriptDir = scriptDir
        self.localLogName = options.localLogName

        verbose = False
        if (options.log_mach_verbose or options.log_tbpl_level == "debug"
                or options.log_mach_level == "debug"
                or options.log_raw_level == "debug"):
            verbose = True
            print("set verbose!")
        expected = options.app.split("/")[-1]
        self.device = ADBDeviceFactory(
            adb=options.adb_path or "adb",
            device=options.deviceSerial,
            test_root=options.remoteTestRoot,
            verbose=verbose,
            run_as_package=expected,
        )
        if options.remoteTestRoot is None:
            options.remoteTestRoot = posixpath.join(self.device.test_root,
                                                    "reftest")
        options.remoteProfile = posixpath.join(options.remoteTestRoot,
                                               "profile")
        options.remoteLogFile = posixpath.join(options.remoteTestRoot,
                                               "reftest.log")
        options.logFile = options.remoteLogFile
        self.remoteProfile = options.remoteProfile
        self.remoteTestRoot = options.remoteTestRoot

        if not options.ignoreWindowSize:
            parts = self.device.get_info("screen")["screen"][0].split()
            width = int(parts[0].split(":")[1])
            height = int(parts[1].split(":")[1])
            if width < 1366 or height < 1050:
                self.error("ERROR: Invalid screen resolution %sx%s, "
                           "please adjust to 1366x1050 or higher" %
                           (width, height))

        self._populate_logger(options)
        self.outputHandler = OutputHandler(self.log, options.utilityPath,
                                           options.symbolsPath)

        self.SERVER_STARTUP_TIMEOUT = 90

        self.remoteCache = os.path.join(options.remoteTestRoot, "cache/")

        # Check that Firefox is installed
        expected = options.app.split("/")[-1]
        if not self.device.is_app_installed(expected):
            raise Exception("%s is not installed on this device" % expected)
        self.device.run_as_package = expected
        self.device.clear_logcat()

        self.device.rm(self.remoteCache, force=True, recursive=True)

        procName = options.app.split("/")[-1]
        self.device.stop_application(procName)
        if self.device.process_exist(procName):
            self.log.error("unable to kill %s before starting tests!" %
                           procName)

    def findPath(self, paths, filename=None):
        for path in paths:
            p = path
            if filename:
                p = os.path.join(p, filename)
            if os.path.exists(self.getFullPath(p)):
                return path
        return None

    def startWebServer(self, options):
        """ Create the webserver on the host and start it up """
        remoteXrePath = options.xrePath
        remoteUtilityPath = options.utilityPath

        paths = [options.xrePath]
        if build_obj:
            paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
        options.xrePath = self.findPath(paths)
        if options.xrePath is None:
            print("ERROR: unable to find xulrunner path for %s, "
                  "please specify with --xre-path" % (os.name))
            return 1
        paths.append("bin")
        paths.append(os.path.join("..", "bin"))

        xpcshell = "xpcshell"
        if os.name == "nt":
            xpcshell += ".exe"

        if options.utilityPath:
            paths.insert(0, options.utilityPath)
        options.utilityPath = self.findPath(paths, xpcshell)
        if options.utilityPath is None:
            print("ERROR: unable to find utility path for %s, "
                  "please specify with --utility-path" % (os.name))
            return 1

        options.serverProfilePath = tempfile.mkdtemp()
        self.server = ReftestServer(options, self.scriptDir, self.log)
        retVal = self.server.start()
        if retVal:
            return retVal
        retVal = self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
        if retVal:
            return retVal

        options.xrePath = remoteXrePath
        options.utilityPath = remoteUtilityPath
        return 0

    def stopWebServer(self, options):
        self.server.stop()

    def killNamedProc(self, pname, orphans=True):
        """ Kill processes matching the given command name """
        try:
            import psutil
        except ImportError as e:
            self.log.warning("Unable to import psutil: %s" % str(e))
            self.log.warning(
                "Unable to verify that %s is not already running." % pname)
            return

        self.log.info("Checking for %s processes..." % pname)

        for proc in psutil.process_iter():
            try:
                if proc.name() == pname:
                    procd = proc.as_dict(
                        attrs=["pid", "ppid", "name", "username"])
                    if proc.ppid() == 1 or not orphans:
                        self.log.info("killing %s" % procd)
                        try:
                            os.kill(proc.pid,
                                    getattr(signal, "SIGKILL", signal.SIGTERM))
                        except Exception as e:
                            self.log.info("Failed to kill process %d: %s" %
                                          (proc.pid, str(e)))
                    else:
                        self.log.info("NOT killing %s (not an orphan?)" %
                                      procd)
            except Exception:
                # may not be able to access process info for all processes
                continue

    def createReftestProfile(self, options, **kwargs):
        profile = RefTest.createReftestProfile(self,
                                               options,
                                               server=options.remoteWebServer,
                                               port=options.httpPort,
                                               **kwargs)
        profileDir = profile.profile
        prefs = {}
        prefs["app.update.url.android"] = ""
        prefs["reftest.remote"] = True
        prefs[
            "datareporting.policy.dataSubmissionPolicyBypassAcceptance"] = True
        # move necko cache to a location that can be cleaned up
        prefs["browser.cache.disk.parent_directory"] = self.remoteCache

        prefs["layout.css.devPixelsPerPx"] = "1.0"
        # Because Fennec is a little wacky (see bug 1156817) we need to load the
        # reftest pages at 1.0 zoom, rather than zooming to fit the CSS viewport.
        prefs["apz.allow_zooming"] = False

        # Set the extra prefs.
        profile.set_preferences(prefs)

        try:
            self.device.push(profileDir, options.remoteProfile)
            # make sure the parent directories of the profile which
            # may have been created by the push, also have their
            # permissions set to allow access.
            self.device.chmod(options.remoteTestRoot, recursive=True)
        except Exception:
            print("Automation Error: Failed to copy profiledir to device")
            raise

        return profile

    def environment(self, env=None, crashreporter=True, **kwargs):
        # Since running remote, do not mimic the local env: do not copy os.environ
        if env is None:
            env = {}

        if crashreporter:
            env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
            env["MOZ_CRASHREPORTER"] = "1"
            env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
        else:
            env["MOZ_CRASHREPORTER_DISABLE"] = "1"

        # Crash on non-local network connections by default.
        # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
        # enable non-local connections for the purposes of local testing.
        # Don't override the user's choice here.  See bug 1049688.
        env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")

        # Send an env var noting that we are in automation. Passing any
        # value except the empty string will declare the value to exist.
        #
        # This may be used to disabled network connections during testing, e.g.
        # Switchboard & telemetry uploads.
        env.setdefault("MOZ_IN_AUTOMATION", "1")

        # Set WebRTC logging in case it is not set yet.
        env.setdefault("R_LOG_LEVEL", "6")
        env.setdefault("R_LOG_DESTINATION", "stderr")
        env.setdefault("R_LOG_VERBOSE", "1")

        return env

    def buildBrowserEnv(self, options, profileDir):
        browserEnv = RefTest.buildBrowserEnv(self, options, profileDir)
        # remove desktop environment not used on device
        if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
            del browserEnv["XPCOM_MEM_BLOAT_LOG"]
        return browserEnv

    def runApp(self,
               options,
               cmdargs=None,
               timeout=None,
               debuggerInfo=None,
               symbolsPath=None,
               valgrindPath=None,
               valgrindArgs=None,
               valgrindSuppFiles=None,
               **profileArgs):
        if cmdargs is None:
            cmdargs = []

        if self.use_marionette:
            cmdargs.append("-marionette")

        binary = options.app
        profile = self.createReftestProfile(options, **profileArgs)

        # browser environment
        env = self.buildBrowserEnv(options, profile.profile)

        self.log.info("Running with e10s: {}".format(options.e10s))
        self.log.info("Running with fission: {}".format(options.fission))

        rpm = RemoteProcessMonitor(
            binary,
            self.device,
            self.log,
            self.outputHandler,
            options.remoteLogFile,
            self.remoteProfile,
        )
        startTime = datetime.datetime.now()
        status = 0
        profileDirectory = self.remoteProfile + "/"
        cmdargs.extend(("-no-remote", "-profile", profileDirectory))

        pid = rpm.launch(
            binary,
            debuggerInfo,
            None,
            cmdargs,
            env=env,
            e10s=options.e10s,
        )
        self.log.info("remotereftest.py | Application pid: %d" % pid)
        if not rpm.wait(timeout):
            status = 1
        self.log.info("remotereftest.py | Application ran for: %s" %
                      str(datetime.datetime.now() - startTime))
        crashed = self.check_for_crashes(symbolsPath, rpm.last_test_seen)
        if crashed:
            status = 1

        self.cleanup(profile.profile)
        return status

    def check_for_crashes(self, symbols_path, last_test_seen):
        """
        Pull any minidumps from remote profile and log any associated crashes.
        """
        try:
            dump_dir = tempfile.mkdtemp()
            remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps")
            if not self.device.is_dir(remote_crash_dir):
                return False
            self.device.pull(remote_crash_dir, dump_dir)
            crashed = mozcrash.log_crashes(self.log,
                                           dump_dir,
                                           symbols_path,
                                           test=last_test_seen)
        finally:
            try:
                shutil.rmtree(dump_dir)
            except Exception as e:
                self.log.warning("unable to remove directory %s: %s" %
                                 (dump_dir, str(e)))
        return crashed

    def cleanup(self, profileDir):
        self.device.rm(self.remoteTestRoot, force=True, recursive=True)
        self.device.rm(self.remoteProfile, force=True, recursive=True)
        self.device.rm(self.remoteCache, force=True, recursive=True)
        RefTest.cleanup(self, profileDir)