tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

runtestsremoteios.py (15135B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import os
      6 import posixpath
      7 import shutil
      8 import sys
      9 import tempfile
     10 import traceback
     11 import uuid
     12 
     13 sys.path.insert(0, os.path.abspath(os.path.realpath(os.path.dirname(__file__))))
     14 
     15 import mozcrash
     16 import mozinfo
     17 from mochitest_options import MochitestArgumentParser, build_obj
     18 from mozdevice.ios import IosDevice
     19 from runtests import MessageLogger, MochitestDesktop
     20 
     21 SCRIPT_DIR = os.path.abspath(os.path.realpath(os.path.dirname(__file__)))
     22 
     23 
     24 class MochiRemoteIos(MochitestDesktop):
     25    localProfile = None
     26    logMessages = []
     27 
     28    def __init__(self, options):
     29        MochitestDesktop.__init__(self, options.flavor, vars(options))
     30 
     31        # Fixme - support non-simulator devices.
     32        self.isSimulator = True
     33 
     34        if hasattr(options, "log"):
     35            delattr(options, "log")
     36 
     37        self.certdbNew = True
     38        self.chromePushed = False
     39 
     40        self.device = IosDevice.select_device(self.isSimulator)
     41 
     42        if options.remoteTestRoot is None:
     43            options.remoteTestRoot = self.device.test_root(options.remoteappname)
     44        options.dumpOutputDirectory = options.remoteTestRoot
     45 
     46        self.remoteLogFile = posixpath.join(
     47            options.remoteTestRoot, "logs", "mochitest.log"
     48        )
     49        logParent = posixpath.dirname(self.remoteLogFile)
     50        self.device.rm(logParent, recursive=True)
     51        self.device.mkdir(logParent, parents=True)
     52 
     53        self.remoteProfile = posixpath.join(options.remoteTestRoot, "profile")
     54        self.device.rm(self.remoteProfile, force=True, recursive=True)
     55 
     56        self.message_logger = MessageLogger(logger=None)
     57        self.message_logger.logger = self.log
     58 
     59        # FIXME: There doesn't appear to be a way to check if an app is
     60        # installed on an iOS device?
     61 
     62        self.remoteModulesDir = posixpath.join(options.remoteTestRoot, "modules/")
     63 
     64        self.remoteCache = posixpath.join(options.remoteTestRoot, "cache/")
     65        self.device.rm(self.remoteCache, force=True, recursive=True)
     66 
     67        # move necko cache to a location that can be cleaned up
     68        options.extraPrefs += [
     69            f"browser.cache.disk.parent_directory={self.remoteCache}"
     70        ]
     71 
     72        self.remoteMozLog = posixpath.join(options.remoteTestRoot, "mozlog")
     73        self.device.rm(self.remoteMozLog, force=True, recursive=True)
     74        self.device.mkdir(self.remoteMozLog, parents=True)
     75 
     76        self.remoteChromeTestDir = posixpath.join(options.remoteTestRoot, "chrome")
     77        self.device.rm(self.remoteChromeTestDir, force=True, recursive=True)
     78        self.device.mkdir(self.remoteChromeTestDir, parents=True)
     79 
     80        self.appName = options.remoteappname
     81        self.device.stop_application(self.appName)
     82 
     83        mozinfo.info["is_emulator"] = self.isSimulator
     84 
     85    def cleanup(self, options, final=False):
     86        if final:
     87            self.device.rm(self.remoteChromeTestDir, force=True, recursive=True)
     88            self.chromePushed = False
     89            uploadDir = os.environ.get("MOZ_UPLOAD_DIR", None)
     90            if uploadDir and self.device.is_dir(self.remoteMozLog):
     91                self.device.pull(self.remoteMozLog, uploadDir)
     92        self.device.rm(self.remoteLogFile, force=True)
     93        self.device.rm(self.remoteProfile, force=True, recursive=True)
     94        self.device.rm(self.remoteCache, force=True, recursive=True)
     95        MochitestDesktop.cleanup(self, options, final)
     96        self.localProfile = None
     97 
     98    def dumpScreen(self, utilityPath):
     99        self.log.info("Would take a screenshot, but not implemented for iOS")
    100 
    101    def findPath(self, paths, filename=None):
    102        for path in paths:
    103            p = path
    104            if filename:
    105                p = os.path.join(p, filename)
    106            if os.path.exists(self.getFullPath(p)):
    107                return path
    108        return None
    109 
    110    # This seems kludgy, but this class uses paths from the remote host in the
    111    # options, except when calling up to the base class, which doesn't
    112    # understand the distinction.  This switches out the remote values for local
    113    # ones that the base class understands.  This is necessary for the web
    114    # server, SSL tunnel and profile building functions.
    115    def switchToLocalPaths(self, options):
    116        """Set local paths in the options, return a function that will restore remote values"""
    117        remoteXrePath = options.xrePath
    118        remoteProfilePath = options.profilePath
    119        remoteUtilityPath = options.utilityPath
    120 
    121        paths = [
    122            options.xrePath,
    123        ]
    124        if build_obj:
    125            paths.append(os.path.join(build_obj.topobjdir, "dist", "bin"))
    126        options.xrePath = self.findPath(paths)
    127        if options.xrePath is None:
    128            self.log.error(
    129                f"unable to find xulrunner path for {os.name}, please specify with --xre-path"
    130            )
    131            sys.exit(1)
    132 
    133        xpcshell = "xpcshell"
    134        if os.name == "nt":
    135            xpcshell += ".exe"
    136 
    137        if options.utilityPath:
    138            paths = [options.utilityPath, options.xrePath]
    139        else:
    140            paths = [options.xrePath]
    141        options.utilityPath = self.findPath(paths, xpcshell)
    142 
    143        if options.utilityPath is None:
    144            self.log.error(
    145                f"unable to find utility path for {os.name}, please specify with --utility-path"
    146            )
    147            sys.exit(1)
    148 
    149        if self.localProfile:
    150            options.profilePath = self.localProfile
    151        else:
    152            options.profilePath = None
    153 
    154        def fixup():
    155            options.xrePath = remoteXrePath
    156            options.utilityPath = remoteUtilityPath
    157            options.profilePath = remoteProfilePath
    158 
    159        return fixup
    160 
    161    def startServers(self, options, debuggerInfo, public=None):
    162        """Create the servers on the host and start them up"""
    163        restoreRemotePaths = self.switchToLocalPaths(options)
    164        MochitestDesktop.startServers(self, options, debuggerInfo, public=True)
    165        restoreRemotePaths()
    166 
    167    def buildProfile(self, options):
    168        restoreRemotePaths = self.switchToLocalPaths(options)
    169        if options.testingModulesDir:
    170            try:
    171                self.device.push(options.testingModulesDir, self.remoteModulesDir)
    172                self.device.chmod(self.remoteModulesDir, recursive=True)
    173            except Exception:
    174                self.log.error(
    175                    "Automation Error: Unable to copy test modules to device."
    176                )
    177                raise
    178            savedTestingModulesDir = options.testingModulesDir
    179            options.testingModulesDir = self.remoteModulesDir
    180        else:
    181            savedTestingModulesDir = None
    182        manifest = MochitestDesktop.buildProfile(self, options)
    183        if savedTestingModulesDir:
    184            options.testingModulesDir = savedTestingModulesDir
    185        self.localProfile = options.profilePath
    186 
    187        restoreRemotePaths()
    188        options.profilePath = self.remoteProfile
    189        return manifest
    190 
    191    def buildURLOptions(self, options, env):
    192        saveLogFile = options.logFile
    193        options.logFile = self.remoteLogFile
    194        options.profilePath = self.localProfile
    195        env["MOZ_HIDE_RESULTS_TABLE"] = "1"
    196        retVal = MochitestDesktop.buildURLOptions(self, options, env)
    197 
    198        # we really need testConfig.js (for browser chrome)
    199        try:
    200            self.device.push(options.profilePath, self.remoteProfile)
    201            self.device.chmod(self.remoteProfile, recursive=True)
    202        except Exception:
    203            self.log.error("Automation Error: Unable to copy profile to device.")
    204            raise
    205 
    206        options.profilePath = self.remoteProfile
    207        options.logFile = saveLogFile
    208        return retVal
    209 
    210    def getChromeTestDir(self, options):
    211        local = super().getChromeTestDir(options)
    212        remote = self.remoteChromeTestDir
    213        if options.flavor == "chrome" and not self.chromePushed:
    214            self.log.info(f"pushing {local} to {remote} on device...")
    215            local = os.path.join(local, "chrome")
    216            self.device.push(local, remote)
    217            self.chromePushed = True
    218        return remote
    219 
    220    def getLogFilePath(self, logFile):
    221        return logFile
    222 
    223    def getGMPPluginPath(self, options):
    224        # TODO: bug 1149374
    225        return None
    226 
    227    def environment(self, env=None, crashreporter=True, **kwargs):
    228        # Since running remote, do not mimic the local env: do not copy os.environ
    229        if env is None:
    230            env = {}
    231 
    232        if crashreporter:
    233            env["MOZ_CRASHREPORTER_NO_REPORT"] = "1"
    234            env["MOZ_CRASHREPORTER"] = "1"
    235            env["MOZ_CRASHREPORTER_SHUTDOWN"] = "1"
    236        else:
    237            env["MOZ_CRASHREPORTER_DISABLE"] = "1"
    238 
    239        # Crash on non-local network connections by default.
    240        # MOZ_DISABLE_NONLOCAL_CONNECTIONS can be set to "0" to temporarily
    241        # enable non-local connections for the purposes of local testing.
    242        # Don't override the user's choice here.  See bug 1049688.
    243        env.setdefault("MOZ_DISABLE_NONLOCAL_CONNECTIONS", "1")
    244 
    245        # Send an env var noting that we are in automation. Passing any
    246        # value except the empty string will declare the value to exist.
    247        #
    248        # This may be used to disabled network connections during testing, e.g.
    249        # Switchboard & telemetry uploads.
    250        env.setdefault("MOZ_IN_AUTOMATION", "1")
    251 
    252        # Set WebRTC logging in case it is not set yet.
    253        env.setdefault("R_LOG_LEVEL", "6")
    254        env.setdefault("R_LOG_DESTINATION", "stderr")
    255        env.setdefault("R_LOG_VERBOSE", "1")
    256 
    257        return env
    258 
    259    def buildBrowserEnv(self, options, debugger=False):
    260        browserEnv = MochitestDesktop.buildBrowserEnv(self, options, debugger=debugger)
    261        # remove desktop environment not used on device
    262        if "XPCOM_MEM_BLOAT_LOG" in browserEnv:
    263            del browserEnv["XPCOM_MEM_BLOAT_LOG"]
    264        if self.mozLogs:
    265            browserEnv["MOZ_LOG_FILE"] = os.path.join(
    266                self.remoteMozLog, f"moz-pid=%PID-uid={str(uuid.uuid4())}.log"
    267            )
    268        if options.dmd:
    269            browserEnv["DMD"] = "1"
    270        # Contents of remoteMozLog will be pulled from device and copied to the
    271        # host MOZ_UPLOAD_DIR, to be made available as test artifacts. Make
    272        # MOZ_UPLOAD_DIR available to the browser environment so that tests
    273        # can use it as though they were running on the host.
    274        browserEnv["MOZ_UPLOAD_DIR"] = self.remoteMozLog
    275        return browserEnv
    276 
    277    def runApp(
    278        self,
    279        testUrl,
    280        env,
    281        app,
    282        profile,
    283        extraArgs,
    284        utilityPath,
    285        debuggerInfo=None,
    286        valgrindPath=None,
    287        valgrindArgs=None,
    288        valgrindSuppFiles=None,
    289        symbolsPath=None,
    290        timeout=-1,
    291        detectShutdownLeaks=False,
    292        screenshotOnFail=False,
    293        bisectChunk=None,
    294        restartAfterFailure=False,
    295        marionette_args=None,
    296        e10s=True,
    297        runFailures=False,
    298        crashAsPass=False,
    299        currentManifest=None,
    300    ):
    301        """
    302        Run the app, log the duration it took to execute, return the status code.
    303        Kill the app if it outputs nothing for |timeout| seconds.
    304        """
    305 
    306        if timeout == -1:
    307            timeout = self.DEFAULT_TIMEOUT
    308 
    309        status = 0
    310        profileDirectory = self.remoteProfile + "/"
    311        args = []
    312        args.extend(extraArgs)
    313        args.extend(("-no-remote", "-profile", profileDirectory))
    314 
    315        environ = self.environment(env=env, crashreporter=not debuggerInfo)
    316        environ["MOZ_TEST_URL"] = testUrl
    317 
    318        # create an instance to process the output
    319        outputHandler = self.OutputHandler(
    320            harness=self,
    321            utilityPath=utilityPath,
    322            symbolsPath=symbolsPath,
    323            dump_screen_on_timeout=not debuggerInfo,
    324            dump_screen_on_fail=screenshotOnFail,
    325            shutdownLeaks=None,
    326            lsanLeaks=None,
    327            bisectChunk=bisectChunk,
    328            restartAfterFailure=restartAfterFailure,
    329        )
    330 
    331        proc = self.device.launch_process(
    332            self.appName, args, env=environ, processOutputLine=[outputHandler]
    333        )
    334        proc.run(None, timeout)
    335 
    336        status = proc.wait()
    337        if status is None:
    338            self.log.warning(
    339                "runtestsremoteios.py | Failed to get app exit code - running/crashed?"
    340            )
    341            # must report an integer to process_exit()
    342            status = 0
    343        self.log.process_exit("Main app process", status)
    344 
    345        # finalize output handler
    346        outputHandler.finish()
    347 
    348        lastTestSeen = currentManifest or "Main app process exited normally"
    349 
    350        crashed = self.check_for_crashes(symbolsPath, lastTestSeen)
    351        if crashed:
    352            status = 1
    353 
    354        return status, lastTestSeen
    355 
    356    def check_for_crashes(self, symbols_path, last_test_seen):
    357        """
    358        Pull any minidumps from remote profile and log any associated crashes.
    359        """
    360        try:
    361            dump_dir = tempfile.mkdtemp()
    362            remote_crash_dir = posixpath.join(self.remoteProfile, "minidumps")
    363            if not self.device.is_dir(remote_crash_dir):
    364                return False
    365            self.device.pull(remote_crash_dir, dump_dir)
    366            crashed = mozcrash.log_crashes(
    367                self.log, dump_dir, symbols_path, test=last_test_seen
    368            )
    369        finally:
    370            try:
    371                shutil.rmtree(dump_dir)
    372            except Exception as e:
    373                self.log.warning(f"unable to remove directory {dump_dir}: {str(e)}")
    374        return crashed
    375 
    376    # Override for the desktop output handler - used for simctl tests.
    377    class OutputHandler(MochitestDesktop.OutputHandler):
    378        # Disable the stack fixer, as there currently isn't one for iOS.
    379        def stackFixer(self):
    380            return None
    381 
    382 
    383 def run_test_harness(parser, options):
    384    parser.validate(options)
    385 
    386    if options is None:
    387        raise ValueError(
    388            "Invalid options specified, use --help for a list of valid options"
    389        )
    390 
    391    options.runByManifest = True
    392 
    393    mochitest = MochiRemoteIos(options)
    394 
    395    try:
    396        if options.verify:
    397            retVal = mochitest.verifyTests(options)
    398        else:
    399            retVal = mochitest.runTests(options)
    400    except Exception:
    401        mochitest.log.error("Automation Error: Exception caught while running tests")
    402        traceback.print_exc()
    403        try:
    404            mochitest.cleanup(options)
    405        except Exception:
    406            # device error cleaning up... oh well!
    407            traceback.print_exc()
    408        retVal = 1
    409 
    410    mochitest.archiveMozLogs()
    411    mochitest.message_logger.finish()
    412 
    413    return retVal
    414 
    415 
    416 def main(args=sys.argv[1:]):
    417    parser = MochitestArgumentParser(app="ios")
    418    options = parser.parse_args(args)
    419 
    420    return run_test_harness(parser, options)
    421 
    422 
    423 if __name__ == "__main__":
    424    sys.exit(main())