Ejemplo n.º 1
0
class AndroidLauncher(Launcher):
    app_info = None
    adb = None
    package_name = None
    profile_class = FirefoxRegressionProfile
    remote_profile = None

    @abstractmethod
    def _get_package_name(self):
        raise NotImplementedError

    @abstractmethod
    def _launch(self):
        raise NotImplementedError

    @classmethod
    def check_is_runnable(cls):
        try:
            devices = ADBHost().devices()
        except ADBError as adb_error:
            raise LauncherNotRunnable(str(adb_error))
        if not devices:
            raise LauncherNotRunnable(
                "No android device connected." " Connect a device and try again."
            )

    def _install(self, dest):
        # get info now, as dest may be removed
        self.app_info = safe_get_version(binary=dest)
        self.package_name = self.app_info.get("package_name", self._get_package_name())
        self.adb = ADBDeviceFactory()
        try:
            self.adb.uninstall_app(self.package_name)
        except ADBError as msg:
            LOG.warning(
                "Failed to uninstall %s (%s)\nThis is normal if it is the"
                " first time the application is installed." % (self.package_name, msg)
            )
        self.adb.run_as_package = self.adb.install_app(dest)

    def _start(
        self,
        profile=None,
        addons=(),
        cmdargs=(),
        preferences=None,
        adb_profile_dir=None,
    ):
        # for now we don't handle addons on the profile for fennec
        profile = self._create_profile(profile=profile, preferences=preferences)
        # send the profile on the device
        if not adb_profile_dir:
            adb_profile_dir = self.adb.test_root
        self.remote_profile = "/".join([adb_profile_dir, os.path.basename(profile.profile)])
        if self.adb.exists(self.remote_profile):
            self.adb.rm(self.remote_profile, recursive=True)
        LOG.debug("Pushing profile to device (%s -> %s)" % (profile.profile, self.remote_profile))
        self.adb.push(profile.profile, self.remote_profile)
        self._launch()

    def _wait(self):
        while self.adb.process_exist(self.package_name):
            time.sleep(0.1)

    def _stop(self):
        self.adb.stop_application(self.package_name)
        if self.adb.exists(self.remote_profile):
            self.adb.rm(self.remote_profile, recursive=True)

    def get_app_info(self):
        return self.app_info
Ejemplo n.º 2
0
class AndroidDevice:
    def __init__(self, app_name, marionette_port=2828, verbose=False):
        self.app_name = app_name
        self.fennec = "firefox" in app_name

        # XXX make that an option
        if "fenix" in app_name:
            self.activity = "org.mozilla.fenix.IntentReceiverActivity"
        elif self.fennec:
            self.activity = None
        else:
            self.activity = "org.mozilla.geckoview_example.GeckoViewActivity"
        self.verbose = verbose
        self.device = None
        self.marionette_port = marionette_port
        self.profile = None
        self.remote_profile = None
        self.log_file = None
        self._adb_fh = None

    def _set_adb_logger(self, log_file):
        self.log_file = log_file
        if self.log_file is None:
            return
        logger.info("Setting ADB log file to %s" % self.log_file)
        adb_logger = logging.getLogger("adb")
        adb_logger.setLevel(logging.DEBUG)
        self._adb_fh = logging.FileHandler(self.log_file)
        self._adb_fh.setLevel(logging.DEBUG)
        adb_logger.addHandler(self._adb_fh)

    def _unset_adb_logger(self):
        if self._adb_fh is None:
            return
        logging.getLogger("adb").removeHandler(self._adb_fh)
        self._adb_fh = None

    def clear_logcat(self, timeout=None, buffers=[]):
        if not self.device:
            return
        self.device.clear_logcat(timeout, buffers)

    def get_logcat(self):
        if not self.device:
            return None
        # we don't want to have ADBCommand dump the command
        # in the debug stream so we reduce its verbosity here
        # temporarely
        old_verbose = self.device._verbose
        self.device._verbose = False
        try:
            return self.device.get_logcat()
        finally:
            self.device._verbose = old_verbose

    def prepare(self, profile, logfile):
        self._set_adb_logger(logfile)
        try:
            # See android_emulator_pgo.py run_tests for more
            # details on why test_root must be /sdcard/test_root
            # for android pgo due to Android 4.3.
            self.device = ADBDeviceFactory(verbose=self.verbose,
                                           logger_name="adb",
                                           test_root="/sdcard/test_root")
        except Exception:
            logger.error("Cannot initialize device")
            raise
        device = self.device
        self.profile = profile

        # checking that the app is installed
        if not device.is_app_installed(self.app_name):
            raise Exception("%s is not installed" % self.app_name)

        # debug flag
        logger.info("Setting %s as the debug app on the phone" % self.app_name)
        device.shell(
            "am set-debug-app --persistent %s" % self.app_name,
            stdout_callback=logger.info,
        )

        # creating the profile on the device
        logger.info("Creating the profile on the device")
        remote_test_root = posixpath.join(device.test_root, "condprof")
        remote_profile = posixpath.join(remote_test_root, "profile")
        logger.info("The profile on the phone will be at %s" % remote_profile)
        device.rm(remote_test_root, force=True, recursive=True)
        device.mkdir(remote_test_root)

        device.rm(remote_profile, force=True, recursive=True)
        logger.info("Pushing %s on the phone" % self.profile)
        device.push(profile, remote_profile)
        device.chmod(remote_profile, recursive=True)
        self.profile = profile
        self.remote_profile = remote_profile

        # creating the yml file
        yml_data = {
            "args": ["-marionette", "-profile", self.remote_profile],
            "prefs": DEFAULT_PREFS,
            "env": {
                "LOG_VERBOSE": 1,
                "R_LOG_LEVEL": 6,
                "MOZ_LOG": ""
            },
        }

        yml_name = "%s-geckoview-config.yaml" % self.app_name
        yml_on_host = posixpath.join(tempfile.mkdtemp(), yml_name)
        write_yml_file(yml_on_host, yml_data)
        tmp_on_device = posixpath.join("/data", "local", "tmp")
        if not device.exists(tmp_on_device):
            raise IOError("%s does not exists on the device" % tmp_on_device)
        yml_on_device = posixpath.join(tmp_on_device, yml_name)
        try:
            device.rm(yml_on_device, force=True, recursive=True)
            device.push(yml_on_host, yml_on_device)
            device.chmod(yml_on_device, recursive=True)
        except Exception:
            logger.info(
                "could not create the yaml file on device. Permission issue?")
            raise

        # command line 'extra' args not used with geckoview apps; instead we use
        # an on-device config.yml file
        intent = "android.intent.action.VIEW"
        device.stop_application(self.app_name)
        if self.fennec:
            # XXX does the Fennec app picks up the YML file ?
            extra_args = [
                "-profile",
                self.remote_profile,
                "--es",
                "env0",
                "LOG_VERBOSE=1",
                "--es",
                "env1",
                "R_LOG_LEVEL=6",
                "--es",
                "env2",
                "MOZ_WEBRENDER=0",
            ]

            device.launch_fennec(
                self.app_name,
                extra_args=extra_args,
                url="about:blank",
                fail_if_running=False,
            )
        else:
            device.launch_application(self.app_name,
                                      self.activity,
                                      intent,
                                      extras=None,
                                      url="about:blank")
        if not device.process_exist(self.app_name):
            raise Exception("Could not start %s" % self.app_name)

        logger.info("Creating socket forwarding on port %d" %
                    self.marionette_port)
        device.forward(
            local="tcp:%d" % self.marionette_port,
            remote="tcp:%d" % self.marionette_port,
        )

        # we don't have a clean way for now to check that GV or Fenix
        # is ready to handle our tests. So here we just wait 30s
        logger.info("Sleeping for 30s")
        time.sleep(30)

    def stop_browser(self):
        logger.info("Stopping %s" % self.app_name)
        try:
            self.device.stop_application(self.app_name)
        except ADBError:
            logger.info("Could not stop the application using force-stop")

        time.sleep(5)
        if self.device.process_exist(self.app_name):
            logger.info("%s still running, trying SIGKILL" % self.app_name)
            num_tries = 0
            while self.device.process_exist(self.app_name) and num_tries < 5:
                try:
                    self.device.pkill(self.app_name)
                except ADBError:
                    pass
                num_tries += 1
                time.sleep(1)
        logger.info("%s stopped" % self.app_name)

    def collect_profile(self):
        logger.info("Collecting profile from %s" % self.remote_profile)
        self.device.pull(self.remote_profile, self.profile)

    def close(self):
        self._unset_adb_logger()
        if self.device is None:
            return
        try:
            self.device.remove_forwards("tcp:%d" % self.marionette_port)
        except ADBError:
            logger.warning("Could not remove forward port")
Ejemplo n.º 3
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
Ejemplo n.º 4
0
    def run_tests(self):
        """
        Generate the PGO profile data
        """
        from mozhttpd import MozHttpd
        from mozprofile import Preferences
        from mozdevice import ADBDeviceFactory, ADBTimeoutError
        from six import string_types
        from marionette_driver.marionette import Marionette

        app = self.query_package_name()

        IP = "10.0.2.2"
        PORT = 8888

        PATH_MAPPINGS = {
            "/js-input/webkit/PerformanceTests":
            "third_party/webkit/PerformanceTests",
        }

        dirs = self.query_abs_dirs()
        topsrcdir = dirs["abs_src_dir"]
        adb = self.query_exe("adb")

        path_mappings = {
            k: os.path.join(topsrcdir, v)
            for k, v in PATH_MAPPINGS.items()
        }
        httpd = MozHttpd(
            port=PORT,
            docroot=os.path.join(topsrcdir, "build", "pgo"),
            path_mappings=path_mappings,
        )
        httpd.start(block=False)

        profile_data_dir = os.path.join(topsrcdir, "testing", "profiles")
        with open(os.path.join(profile_data_dir, "profiles.json"), "r") as fh:
            base_profiles = json.load(fh)["profileserver"]

        prefpaths = [
            os.path.join(profile_data_dir, profile, "user.js")
            for profile in base_profiles
        ]

        prefs = {}
        for path in prefpaths:
            prefs.update(Preferences.read_prefs(path))

        interpolation = {
            "server": "%s:%d" % httpd.httpd.server_address,
            "OOP": "false"
        }
        for k, v in prefs.items():
            if isinstance(v, string_types):
                v = v.format(**interpolation)
            prefs[k] = Preferences.cast(v)

        outputdir = self.config.get("output_directory", "/sdcard/pgo_profile")
        jarlog = posixpath.join(outputdir, "en-US.log")
        profdata = posixpath.join(outputdir, "default_%p_random_%m.profraw")

        env = {}
        env["XPCOM_DEBUG_BREAK"] = "warn"
        env["MOZ_IN_AUTOMATION"] = "1"
        env["MOZ_JAR_LOG_FILE"] = jarlog
        env["LLVM_PROFILE_FILE"] = profdata

        if self.query_minidump_stackwalk():
            os.environ["MINIDUMP_STACKWALK"] = self.minidump_stackwalk_path
        os.environ["MINIDUMP_SAVE_PATH"] = self.query_abs_dirs(
        )["abs_blob_upload_dir"]
        if not self.symbols_path:
            self.symbols_path = os.environ.get("MOZ_FETCHES_DIR")

        # Force test_root to be on the sdcard for android pgo
        # builds which fail for Android 4.3 when profiles are located
        # in /data/local/tmp/test_root with
        # E AndroidRuntime: FATAL EXCEPTION: Gecko
        # E AndroidRuntime: java.lang.IllegalArgumentException: \
        #    Profile directory must be writable if specified: /data/local/tmp/test_root/profile
        # This occurs when .can-write-sentinel is written to
        # the profile in
        # mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoProfile.java.
        # This is not a problem on later versions of Android. This
        # over-ride of test_root should be removed when Android 4.3 is no
        # longer supported.
        sdcard_test_root = "/sdcard/test_root"
        adbdevice = ADBDeviceFactory(adb=adb,
                                     device="emulator-5554",
                                     test_root=sdcard_test_root)
        if adbdevice.test_root != sdcard_test_root:
            # If the test_root was previously set and shared
            # the initializer will not have updated the shared
            # value. Force it to match the sdcard_test_root.
            adbdevice.test_root = sdcard_test_root
        adbdevice.mkdir(outputdir, parents=True)

        try:
            # Run Fennec a first time to initialize its profile
            driver = Marionette(
                app="fennec",
                package_name=app,
                adb_path=adb,
                bin="geckoview-androidTest.apk",
                prefs=prefs,
                connect_to_running_emulator=True,
                startup_timeout=1000,
                env=env,
                symbols_path=self.symbols_path,
            )
            driver.start_session()

            # Now generate the profile and wait for it to complete
            for page in PAGES:
                driver.navigate("http://%s:%d/%s" % (IP, PORT, page))
                timeout = 2
                if "Speedometer/index.html" in page:
                    # The Speedometer test actually runs many tests internally in
                    # javascript, so it needs extra time to run through them. The
                    # emulator doesn't get very far through the whole suite, but
                    # this extra time at least lets some of them process.
                    timeout = 360
                time.sleep(timeout)

            driver.set_context("chrome")
            driver.execute_script("""
                Components.utils.import("resource://gre/modules/Services.jsm");
                let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
                    .createInstance(Components.interfaces.nsISupportsPRBool);
                Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
                return cancelQuit.data;
            """)
            driver.execute_script("""
                Components.utils.import("resource://gre/modules/Services.jsm");
                Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit)
            """)

            # There is a delay between execute_script() returning and the profile data
            # actually getting written out, so poll the device until we get a profile.
            for i in range(50):
                if not adbdevice.process_exist(app):
                    break
                time.sleep(2)
            else:
                raise Exception("Android App (%s) never quit" % app)

            # Pull all the profraw files and en-US.log
            adbdevice.pull(outputdir, "/builds/worker/workspace/")
        except ADBTimeoutError:
            self.fatal(
                "INFRA-ERROR: Failed with an ADBTimeoutError",
                EXIT_STATUS_DICT[TBPL_RETRY],
            )

        profraw_files = glob.glob("/builds/worker/workspace/*.profraw")
        if not profraw_files:
            self.fatal(
                "Could not find any profraw files in /builds/worker/workspace")
        merge_cmd = [
            os.path.join(os.environ["MOZ_FETCHES_DIR"],
                         "clang/bin/llvm-profdata"),
            "merge",
            "-o",
            "/builds/worker/workspace/merged.profdata",
        ] + profraw_files
        rc = subprocess.call(merge_cmd)
        if rc != 0:
            self.fatal(
                "INFRA-ERROR: Failed to merge profile data. Corrupt profile?",
                EXIT_STATUS_DICT[TBPL_RETRY],
            )

        # tarfile doesn't support xz in this version of Python
        tar_cmd = [
            "tar",
            "-acvf",
            "/builds/worker/artifacts/profdata.tar.xz",
            "-C",
            "/builds/worker/workspace",
            "merged.profdata",
            "en-US.log",
        ]
        subprocess.check_call(tar_cmd)

        httpd.stop()
Ejemplo n.º 5
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
Ejemplo n.º 6
0
class WebExtensionAndroid(PerftestAndroid, WebExtension):
    def __init__(self, app, binary, activity=None, intent=None, **kwargs):
        super(WebExtensionAndroid, self).__init__(app,
                                                  binary,
                                                  profile_class="firefox",
                                                  **kwargs)

        self.config.update({"activity": activity, "intent": intent})

        self.os_baseline_data = None
        self.power_test_time = None
        self.screen_off_timeout = 0
        self.screen_brightness = 127
        self.app_launched = False

    def setup_adb_device(self):
        if self.device is None:
            self.device = ADBDeviceFactory(verbose=True)
            if not self.config.get("disable_perf_tuning", False):
                tune_performance(self.device, log=LOG)

        self.device.run_as_package = self.config["binary"]
        self.remote_test_root = os.path.join(self.device.test_root, "raptor")
        self.remote_profile = os.path.join(self.remote_test_root, "profile")
        if self.config["power_test"]:
            disable_charging(self.device)

        LOG.info("creating remote root folder for raptor: %s" %
                 self.remote_test_root)
        self.device.rm(self.remote_test_root, force=True, recursive=True)
        self.device.mkdir(self.remote_test_root, parents=True)

        self.clear_app_data()
        self.set_debug_app_flag()

    def process_exists(self):
        return self.device is not None and self.device.process_exist(
            self.config["binary"])

    def write_android_app_config(self):
        # geckoview supports having a local on-device config file; use this file
        # to tell the app to use the specified browser profile, as well as other opts
        # on-device: /data/local/tmp/com.yourcompany.yourapp-geckoview-config.yaml
        # https://mozilla.github.io/geckoview/tutorials/automation.html#configuration-file-format

        # only supported for geckoview apps
        if self.config["app"] == "fennec":
            return
        LOG.info("creating android app config.yml")

        yml_config_data = dict(
            args=[
                "--profile",
                self.remote_profile,
                "--allow-downgrade",
            ],
            env=dict(
                LOG_VERBOSE=1,
                R_LOG_LEVEL=6,
                MOZ_WEBRENDER=int(self.config["enable_webrender"]),
            ),
        )

        yml_name = "%s-geckoview-config.yaml" % self.config["binary"]
        yml_on_host = os.path.join(tempfile.mkdtemp(), yml_name)
        write_yml_file(yml_on_host, yml_config_data)
        yml_on_device = os.path.join("/data", "local", "tmp", yml_name)

        try:
            LOG.info("copying %s to device: %s" % (yml_on_host, yml_on_device))
            self.device.rm(yml_on_device, force=True, recursive=True)
            self.device.push(yml_on_host, yml_on_device)

        except Exception:
            LOG.critical("failed to push %s to device!" % yml_on_device)
            raise

    def log_android_device_temperature(self):
        # retrieve and log the android device temperature
        try:
            # use sort since cat gives I/O Error on Pixel 2 - 10.
            thermal_zone0 = self.device.shell_output(
                "sort /sys/class/thermal/thermal_zone0/temp")
            try:
                thermal_zone0 = "%.3f" % (float(thermal_zone0) / 1000)
            except ValueError:
                thermal_zone0 = "Unknown"
        except ADBProcessError:
            thermal_zone0 = "Unknown"
        try:
            zone_type = self.device.shell_output(
                "cat /sys/class/thermal/thermal_zone0/type")
        except ADBProcessError:
            zone_type = "Unknown"
        LOG.info("(thermal_zone0) device temperature: %s zone type: %s" %
                 (thermal_zone0, zone_type))

    def launch_firefox_android_app(self, test_name):
        LOG.info("starting %s" % self.config["app"])

        extra_args = [
            "-profile",
            self.remote_profile,
            "--allow-downgrade",
            "--es",
            "env0",
            "LOG_VERBOSE=1",
            "--es",
            "env1",
            "R_LOG_LEVEL=6",
            "--es",
            "env2",
            "MOZ_WEBRENDER=%d" % self.config["enable_webrender"],
            # Force the app to immediately exit for content crashes
            "--es",
            "env3",
            "MOZ_CRASHREPORTER_SHUTDOWN=1",
        ]

        try:
            # make sure the android app is not already running
            self.device.stop_application(self.config["binary"])

            if self.config["app"] == "fennec":
                self.device.launch_fennec(
                    self.config["binary"],
                    extra_args=extra_args,
                    url="about:blank",
                    fail_if_running=False,
                )
            else:

                # command line 'extra' args not used with geckoview apps; instead we use
                # an on-device config.yml file (see write_android_app_config)

                self.device.launch_application(
                    self.config["binary"],
                    self.config["activity"],
                    self.config["intent"],
                    extras=None,
                    url="about:blank",
                    fail_if_running=False,
                )

            # Check if app has started and it's running
            if not self.process_exists:
                raise Exception(
                    "Error launching %s. App did not start properly!" %
                    self.config["binary"])
            self.app_launched = True
        except Exception as e:
            LOG.error("Exception launching %s" % self.config["binary"])
            LOG.error("Exception: %s %s" % (type(e).__name__, str(e)))
            if self.config["power_test"]:
                finish_android_power_test(self, test_name)
            raise

        # give our control server the device and app info
        self.control_server.device = self.device
        self.control_server.app_name = self.config["binary"]

    def copy_cert_db(self, source_dir, target_dir):
        # copy browser cert db (that was previously created via certutil) from source to target
        cert_db_files = ["pkcs11.txt", "key4.db", "cert9.db"]
        for next_file in cert_db_files:
            _source = os.path.join(source_dir, next_file)
            _dest = os.path.join(target_dir, next_file)
            if os.path.exists(_source):
                LOG.info("copying %s to %s" % (_source, _dest))
                shutil.copyfile(_source, _dest)
            else:
                LOG.critical("unable to find ssl cert db file: %s" % _source)

    def run_tests(self, tests, test_names):
        self.setup_adb_device()

        return super(WebExtensionAndroid, self).run_tests(tests, test_names)

    def run_test_setup(self, test):
        super(WebExtensionAndroid, self).run_test_setup(test)
        self.set_reverse_ports()

    def run_test_teardown(self, test):
        LOG.info("removing reverse socket connections")
        self.device.remove_socket_connections("reverse")

        super(WebExtensionAndroid, self).run_test_teardown(test)

    def run_test(self, test, timeout):
        # tests will be run warm (i.e. NO browser restart between page-cycles)
        # unless otheriwse specified in the test INI by using 'cold = true'
        try:

            if self.config["power_test"]:
                # gather OS baseline data
                init_android_power_test(self)
                LOG.info("Running OS baseline, pausing for 1 minute...")
                time.sleep(60)
                LOG.info("Finishing baseline...")
                finish_android_power_test(self,
                                          "os-baseline",
                                          os_baseline=True)

                # initialize for the test
                init_android_power_test(self)

            if self.config.get("cold") or test.get("cold"):
                self.__run_test_cold(test, timeout)
            else:
                self.__run_test_warm(test, timeout)

        except SignalHandlerException:
            self.device.stop_application(self.config["binary"])
            if self.config["power_test"]:
                enable_charging(self.device)

        finally:
            if self.config["power_test"]:
                finish_android_power_test(self, test["name"])

    def __run_test_cold(self, test, timeout):
        """
        Run the Raptor test but restart the entire browser app between page-cycles.

        Note: For page-load tests, playback will only be started once - at the beginning of all
        browser cycles, and then stopped after all cycles are finished. The proxy is set via prefs
        in the browser profile so those will need to be set again in each new profile/cycle.
        Note that instead of using the certutil tool each time to create a db and import the
        mitmproxy SSL cert (it's done in mozbase/mozproxy) we will simply copy the existing
        cert db from the first cycle's browser profile into the new clean profile; this way
        we don't have to re-create the cert db on each browser cycle.

        Since we're running in cold-mode, before this point (in manifest.py) the
        'expected-browser-cycles' value was already set to the initial 'page-cycles' value;
        and the 'page-cycles' value was set to 1 as we want to perform one page-cycle per
        browser restart.

        The 'browser-cycle' value is the current overall browser start iteration. The control
        server will receive the current 'browser-cycle' and the 'expected-browser-cycles' in
        each results set received; and will pass that on as part of the results so that the
        results processing will know results for multiple browser cycles are being received.

        The default will be to run in warm mode; unless 'cold = true' is set in the test INI.
        """
        LOG.info(
            "test %s is running in cold mode; browser WILL be restarted between "
            "page cycles" % test["name"])

        for test["browser_cycle"] in range(1, test["expected_browser_cycles"] +
                                           1):

            LOG.info("begin browser cycle %d of %d for test %s" %
                     (test["browser_cycle"], test["expected_browser_cycles"],
                      test["name"]))

            self.run_test_setup(test)

            self.clear_app_data()
            self.set_debug_app_flag()

            if test["browser_cycle"] == 1:
                if test.get("playback") is not None:
                    # an ssl cert db has now been created in the profile; copy it out so we
                    # can use the same cert db in future test cycles / browser restarts
                    local_cert_db_dir = tempfile.mkdtemp()
                    LOG.info(
                        "backing up browser ssl cert db that was created via certutil"
                    )
                    self.copy_cert_db(self.config["local_profile_dir"],
                                      local_cert_db_dir)

                if not self.is_localhost:
                    self.delete_proxy_settings_from_profile()

            else:
                # double-check to ensure app has been shutdown
                self.device.stop_application(self.config["binary"])

                # initial browser profile was already created before run_test was called;
                # now additional browser cycles we want to create a new one each time
                self.build_browser_profile()

                if test.get("playback") is not None:
                    # get cert db from previous cycle profile and copy into new clean profile
                    # this saves us from having to start playback again / recreate cert db etc.
                    LOG.info(
                        "copying existing ssl cert db into new browser profile"
                    )
                    self.copy_cert_db(local_cert_db_dir,
                                      self.config["local_profile_dir"])

                self.run_test_setup(test)

            if test.get("playback") is not None:
                self.turn_on_android_app_proxy()

            self.copy_profile_to_device()
            self.log_android_device_temperature()

            # write android app config.yml
            self.write_android_app_config()

            # now start the browser/app under test
            self.launch_firefox_android_app(test["name"])

            # set our control server flag to indicate we are running the browser/app
            self.control_server._finished = False

            if self.config["cpu_test"]:
                # start measuring CPU usage
                self.cpu_profiler = start_android_cpu_profiler(self)

            self.wait_for_test_finish(test, timeout, self.process_exists)

            # in debug mode, and running locally, leave the browser running
            if self.debug_mode and self.config["run_local"]:
                LOG.info(
                    "* debug-mode enabled - please shutdown the browser manually..."
                )
                self.runner.wait(timeout=None)

            # break test execution if a exception is present
            if len(self.results_handler.page_timeout_list) > 0:
                break

    def __run_test_warm(self, test, timeout):
        LOG.info(
            "test %s is running in warm mode; browser will NOT be restarted between "
            "page cycles" % test["name"])

        self.run_test_setup(test)

        if not self.is_localhost:
            self.delete_proxy_settings_from_profile()

        if test.get("playback") is not None:
            self.turn_on_android_app_proxy()

        self.clear_app_data()
        self.set_debug_app_flag()
        self.copy_profile_to_device()
        self.log_android_device_temperature()

        # write android app config.yml
        self.write_android_app_config()

        # now start the browser/app under test
        self.launch_firefox_android_app(test["name"])

        # set our control server flag to indicate we are running the browser/app
        self.control_server._finished = False

        if self.config["cpu_test"]:
            # start measuring CPU usage
            self.cpu_profiler = start_android_cpu_profiler(self)

        self.wait_for_test_finish(test, timeout, self.process_exists)

        # in debug mode, and running locally, leave the browser running
        if self.debug_mode and self.config["run_local"]:
            LOG.info(
                "* debug-mode enabled - please shutdown the browser manually..."
            )

    def check_for_crashes(self):
        super(WebExtensionAndroid, self).check_for_crashes()

        if not self.app_launched:
            LOG.info(
                "skipping check_for_crashes: application has not been launched"
            )
            return
        self.app_launched = False

        try:
            dump_dir = tempfile.mkdtemp()
            remote_dir = posixpath.join(self.remote_profile, "minidumps")
            if not self.device.is_dir(remote_dir):
                return
            self.device.pull(remote_dir, dump_dir)
            self.crashes += mozcrash.log_crashes(LOG, dump_dir,
                                                 self.config["symbols_path"])
        finally:
            try:
                shutil.rmtree(dump_dir)
            except Exception:
                LOG.warning("unable to remove directory: %s" % dump_dir)

    def clean_up(self):
        LOG.info("removing test folder for raptor: %s" % self.remote_test_root)
        self.device.rm(self.remote_test_root, force=True, recursive=True)

        if self.config["power_test"]:
            enable_charging(self.device)

        super(WebExtensionAndroid, self).clean_up()
Ejemplo n.º 7
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
Ejemplo n.º 8
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)
Ejemplo n.º 9
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)