addon-sdk/source/python-lib/cuddlefish/runner.py

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/addon-sdk/source/python-lib/cuddlefish/runner.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,768 @@
     1.4 +# This Source Code Form is subject to the terms of the Mozilla Public
     1.5 +# License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 +# file, You can obtain one at http://mozilla.org/MPL/2.0/.
     1.7 +
     1.8 +import os
     1.9 +import sys
    1.10 +import time
    1.11 +import tempfile
    1.12 +import atexit
    1.13 +import shlex
    1.14 +import subprocess
    1.15 +import re
    1.16 +import shutil
    1.17 +
    1.18 +import mozrunner
    1.19 +from cuddlefish.prefs import DEFAULT_COMMON_PREFS
    1.20 +from cuddlefish.prefs import DEFAULT_FIREFOX_PREFS
    1.21 +from cuddlefish.prefs import DEFAULT_THUNDERBIRD_PREFS
    1.22 +from cuddlefish.prefs import DEFAULT_FENNEC_PREFS
    1.23 +
    1.24 +# Used to remove noise from ADB output
    1.25 +CLEANUP_ADB = re.compile(r'^(I|E)/(stdout|stderr|GeckoConsole)\s*\(\s*\d+\):\s*(.*)$')
    1.26 +# Used to filter only messages send by `console` module
    1.27 +FILTER_ONLY_CONSOLE_FROM_ADB = re.compile(r'^I/(stdout|stderr)\s*\(\s*\d+\):\s*((info|warning|error|debug): .*)$')
    1.28 +
    1.29 +# Used to detect the currently running test
    1.30 +PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n')
    1.31 +
    1.32 +# Maximum time we'll wait for tests to finish, in seconds.
    1.33 +# The purpose of this timeout is to recover from infinite loops.  It should be
    1.34 +# longer than the amount of time any test run takes, including those on slow
    1.35 +# machines running slow (debug) versions of Firefox.
    1.36 +RUN_TIMEOUT = 1.5 * 60 * 60 # 1.5 Hour
    1.37 +
    1.38 +# Maximum time we'll wait for tests to emit output, in seconds.
    1.39 +# The purpose of this timeout is to recover from hangs.  It should be longer
    1.40 +# than the amount of time any test takes to report results.
    1.41 +OUTPUT_TIMEOUT = 60 * 5 # five minutes
    1.42 +
    1.43 +def follow_file(filename):
    1.44 +    """
    1.45 +    Generator that yields the latest unread content from the given
    1.46 +    file, or None if no new content is available.
    1.47 +
    1.48 +    For example:
    1.49 +
    1.50 +      >>> f = open('temp.txt', 'w')
    1.51 +      >>> f.write('hello')
    1.52 +      >>> f.flush()
    1.53 +      >>> tail = follow_file('temp.txt')
    1.54 +      >>> tail.next()
    1.55 +      'hello'
    1.56 +      >>> tail.next() is None
    1.57 +      True
    1.58 +      >>> f.write('there')
    1.59 +      >>> f.flush()
    1.60 +      >>> tail.next()
    1.61 +      'there'
    1.62 +      >>> f.close()
    1.63 +      >>> os.remove('temp.txt')
    1.64 +    """
    1.65 +
    1.66 +    last_pos = 0
    1.67 +    last_size = 0
    1.68 +    while True:
    1.69 +        newstuff = None
    1.70 +        if os.path.exists(filename):
    1.71 +            size = os.stat(filename).st_size
    1.72 +            if size > last_size:
    1.73 +                last_size = size
    1.74 +                f = open(filename, 'r')
    1.75 +                f.seek(last_pos)
    1.76 +                newstuff = f.read()
    1.77 +                last_pos = f.tell()
    1.78 +                f.close()
    1.79 +        yield newstuff
    1.80 +
    1.81 +# subprocess.check_output only appeared in python2.7, so this code is taken
    1.82 +# from python source code for compatibility with py2.5/2.6
    1.83 +class CalledProcessError(Exception):
    1.84 +    def __init__(self, returncode, cmd, output=None):
    1.85 +        self.returncode = returncode
    1.86 +        self.cmd = cmd
    1.87 +        self.output = output
    1.88 +    def __str__(self):
    1.89 +        return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
    1.90 +
    1.91 +def check_output(*popenargs, **kwargs):
    1.92 +    if 'stdout' in kwargs:
    1.93 +        raise ValueError('stdout argument not allowed, it will be overridden.')
    1.94 +    process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
    1.95 +    output, unused_err = process.communicate()
    1.96 +    retcode = process.poll()
    1.97 +    if retcode:
    1.98 +        cmd = kwargs.get("args")
    1.99 +        if cmd is None:
   1.100 +            cmd = popenargs[0]
   1.101 +        raise CalledProcessError(retcode, cmd, output=output)
   1.102 +    return output
   1.103 +
   1.104 +
   1.105 +class FennecProfile(mozrunner.Profile):
   1.106 +    preferences = {}
   1.107 +    names = ['fennec']
   1.108 +
   1.109 +class FennecRunner(mozrunner.Runner):
   1.110 +    profile_class = FennecProfile
   1.111 +
   1.112 +    names = ['fennec']
   1.113 +
   1.114 +    __DARWIN_PATH = '/Applications/Fennec.app/Contents/MacOS/fennec'
   1.115 +
   1.116 +    def __init__(self, binary=None, **kwargs):
   1.117 +        if sys.platform == 'darwin' and binary and binary.endswith('.app'):
   1.118 +            # Assume it's a Fennec app dir.
   1.119 +            binary = os.path.join(binary, 'Contents/MacOS/fennec')
   1.120 +
   1.121 +        self.__real_binary = binary
   1.122 +
   1.123 +        mozrunner.Runner.__init__(self, **kwargs)
   1.124 +
   1.125 +    def find_binary(self):
   1.126 +        if not self.__real_binary:
   1.127 +            if sys.platform == 'darwin':
   1.128 +                if os.path.exists(self.__DARWIN_PATH):
   1.129 +                    return self.__DARWIN_PATH
   1.130 +            self.__real_binary = mozrunner.Runner.find_binary(self)
   1.131 +        return self.__real_binary
   1.132 +
   1.133 +FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile'
   1.134 +
   1.135 +class RemoteFennecRunner(mozrunner.Runner):
   1.136 +    profile_class = FennecProfile
   1.137 +
   1.138 +    names = ['fennec']
   1.139 +
   1.140 +    _INTENT_PREFIX = 'org.mozilla.'
   1.141 +
   1.142 +    _adb_path = None
   1.143 +
   1.144 +    def __init__(self, binary=None, **kwargs):
   1.145 +        # Check that we have a binary set
   1.146 +        if not binary:
   1.147 +            raise ValueError("You have to define `--binary` option set to the "
   1.148 +                            "path to your ADB executable.")
   1.149 +        # Ensure that binary refer to a valid ADB executable
   1.150 +        output = subprocess.Popen([binary], stdout=subprocess.PIPE,
   1.151 +                                  stderr=subprocess.PIPE).communicate()
   1.152 +        output = "".join(output)
   1.153 +        if not ("Android Debug Bridge" in output):
   1.154 +            raise ValueError("`--binary` option should be the path to your "
   1.155 +                            "ADB executable.")
   1.156 +        self.binary = binary
   1.157 +
   1.158 +        mobile_app_name = kwargs['cmdargs'][0]
   1.159 +        self.profile = kwargs['profile']
   1.160 +        self._adb_path = binary
   1.161 +
   1.162 +        # This pref has to be set to `false` otherwise, we do not receive
   1.163 +        # output of adb commands!
   1.164 +        subprocess.call([self._adb_path, "shell",
   1.165 +                        "setprop log.redirect-stdio false"])
   1.166 +
   1.167 +        # Android apps are launched by their "intent" name,
   1.168 +        # Automatically detect already installed firefox by using `pm` program
   1.169 +        # or use name given as cfx `--mobile-app` argument.
   1.170 +        intents = self.getIntentNames()
   1.171 +        if not intents:
   1.172 +            raise ValueError("Unable to found any Firefox "
   1.173 +                             "application on your device.")
   1.174 +        elif mobile_app_name:
   1.175 +            if not mobile_app_name in intents:
   1.176 +                raise ValueError("Unable to found Firefox application "
   1.177 +                                 "with intent name '%s'\n"
   1.178 +                                 "Available ones are: %s" %
   1.179 +                                 (mobile_app_name, ", ".join(intents)))
   1.180 +            self._intent_name = self._INTENT_PREFIX + mobile_app_name
   1.181 +        else:
   1.182 +            if "firefox" in intents:
   1.183 +                self._intent_name = self._INTENT_PREFIX + "firefox"
   1.184 +            elif "firefox_beta" in intents:
   1.185 +                self._intent_name = self._INTENT_PREFIX + "firefox_beta"
   1.186 +            elif "firefox_nightly" in intents:
   1.187 +                self._intent_name = self._INTENT_PREFIX + "firefox_nightly"
   1.188 +            else:
   1.189 +                self._intent_name = self._INTENT_PREFIX + intents[0]
   1.190 +
   1.191 +        print "Launching mobile application with intent name " + self._intent_name
   1.192 +
   1.193 +        # First try to kill firefox if it is already running
   1.194 +        pid = self.getProcessPID(self._intent_name)
   1.195 +        if pid != None:
   1.196 +            print "Killing running Firefox instance ..."
   1.197 +            subprocess.call([self._adb_path, "shell",
   1.198 +                             "am force-stop " + self._intent_name])
   1.199 +            time.sleep(7)
   1.200 +            # It appears recently that the PID still exists even after
   1.201 +            # Fennec closes, so removing this error still allows the tests
   1.202 +            # to pass as the new Fennec instance is able to start.
   1.203 +            # Leaving error in but commented out for now.
   1.204 +            #
   1.205 +            #if self.getProcessPID(self._intent_name) != None:
   1.206 +            #    raise Exception("Unable to automatically kill running Firefox" +
   1.207 +            #                    " instance. Please close it manually before " +
   1.208 +            #                    "executing cfx.")
   1.209 +
   1.210 +        print "Pushing the addon to your device"
   1.211 +
   1.212 +        # Create a clean empty profile on the sd card
   1.213 +        subprocess.call([self._adb_path, "shell", "rm -r " + FENNEC_REMOTE_PATH])
   1.214 +        subprocess.call([self._adb_path, "shell", "mkdir " + FENNEC_REMOTE_PATH])
   1.215 +
   1.216 +        # Push the profile folder created by mozrunner to the device
   1.217 +        # (we can't simply use `adb push` as it doesn't copy empty folders)
   1.218 +        localDir = self.profile.profile
   1.219 +        remoteDir = FENNEC_REMOTE_PATH
   1.220 +        for root, dirs, files in os.walk(localDir, followlinks='true'):
   1.221 +            relRoot = os.path.relpath(root, localDir)
   1.222 +            # Note about os.path usage below:
   1.223 +            # Local files may be using Windows `\` separators but
   1.224 +            # remote are always `/`, so we need to convert local ones to `/`
   1.225 +            for file in files:
   1.226 +                localFile = os.path.join(root, file)
   1.227 +                remoteFile = remoteDir.replace("/", os.sep)
   1.228 +                if relRoot != ".":
   1.229 +                    remoteFile = os.path.join(remoteFile, relRoot)
   1.230 +                remoteFile = os.path.join(remoteFile, file)
   1.231 +                remoteFile = "/".join(remoteFile.split(os.sep))
   1.232 +                subprocess.Popen([self._adb_path, "push", localFile, remoteFile],
   1.233 +                                 stderr=subprocess.PIPE).wait()
   1.234 +            for dir in dirs:
   1.235 +                targetDir = remoteDir.replace("/", os.sep)
   1.236 +                if relRoot != ".":
   1.237 +                    targetDir = os.path.join(targetDir, relRoot)
   1.238 +                targetDir = os.path.join(targetDir, dir)
   1.239 +                targetDir = "/".join(targetDir.split(os.sep))
   1.240 +                # `-p` option is not supported on all devices!
   1.241 +                subprocess.call([self._adb_path, "shell", "mkdir " + targetDir])
   1.242 +
   1.243 +    @property
   1.244 +    def command(self):
   1.245 +        """Returns the command list to run."""
   1.246 +        return [self._adb_path,
   1.247 +            "shell",
   1.248 +            "am start " +
   1.249 +                "-a android.activity.MAIN " +
   1.250 +                "-n " + self._intent_name + "/" + self._intent_name + ".App " +
   1.251 +                "--es args \"-profile " + FENNEC_REMOTE_PATH + "\""
   1.252 +        ]
   1.253 +
   1.254 +    def start(self):
   1.255 +        subprocess.call(self.command)
   1.256 +
   1.257 +    def getProcessPID(self, processName):
   1.258 +        p = subprocess.Popen([self._adb_path, "shell", "ps"],
   1.259 +                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   1.260 +        line = p.stdout.readline()
   1.261 +        while line:
   1.262 +            columns = line.split()
   1.263 +            pid = columns[1]
   1.264 +            name = columns[-1]
   1.265 +            line = p.stdout.readline()
   1.266 +            if processName in name:
   1.267 +                return pid
   1.268 +        return None
   1.269 +
   1.270 +    def getIntentNames(self):
   1.271 +        p = subprocess.Popen([self._adb_path, "shell", "pm list packages"],
   1.272 +                             stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   1.273 +        names = []
   1.274 +        for line in p.stdout.readlines():
   1.275 +            line = re.sub("(^package:)|\s", "", line)
   1.276 +            if self._INTENT_PREFIX in line:
   1.277 +                names.append(line.replace(self._INTENT_PREFIX, ""))
   1.278 +        return names
   1.279 +
   1.280 +
   1.281 +class XulrunnerAppProfile(mozrunner.Profile):
   1.282 +    preferences = {}
   1.283 +    names = []
   1.284 +
   1.285 +class XulrunnerAppRunner(mozrunner.Runner):
   1.286 +    """
   1.287 +    Runner for any XULRunner app. Can use a Firefox binary in XULRunner
   1.288 +    mode to execute the app, or can use XULRunner itself. Expects the
   1.289 +    app's application.ini to be passed in as one of the items in
   1.290 +    'cmdargs' in the constructor.
   1.291 +
   1.292 +    This class relies a lot on the particulars of mozrunner.Runner's
   1.293 +    implementation, and does some unfortunate acrobatics to get around
   1.294 +    some of the class' limitations/assumptions.
   1.295 +    """
   1.296 +
   1.297 +    profile_class = XulrunnerAppProfile
   1.298 +
   1.299 +    # This is a default, and will be overridden in the instance if
   1.300 +    # Firefox is used in XULRunner mode.
   1.301 +    names = ['xulrunner']
   1.302 +
   1.303 +    # Default location of XULRunner on OS X.
   1.304 +    __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin"
   1.305 +    __LINUX_PATH  = "/usr/bin/xulrunner"
   1.306 +
   1.307 +    # What our application.ini's path looks like if it's part of
   1.308 +    # an "installed" XULRunner app on OS X.
   1.309 +    __DARWIN_APP_INI_SUFFIX = '.app/Contents/Resources/application.ini'
   1.310 +
   1.311 +    def __init__(self, binary=None, **kwargs):
   1.312 +        if sys.platform == 'darwin' and binary and binary.endswith('.app'):
   1.313 +            # Assume it's a Firefox app dir.
   1.314 +            binary = os.path.join(binary, 'Contents/MacOS/firefox-bin')
   1.315 +
   1.316 +        self.__app_ini = None
   1.317 +        self.__real_binary = binary
   1.318 +
   1.319 +        mozrunner.Runner.__init__(self, **kwargs)
   1.320 +
   1.321 +        # See if we're using a genuine xulrunner-bin from the XULRunner SDK,
   1.322 +        # or if we're being asked to use Firefox in XULRunner mode.
   1.323 +        self.__is_xulrunner_sdk = 'xulrunner' in self.binary
   1.324 +
   1.325 +        if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'):
   1.326 +            self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary)
   1.327 +
   1.328 +        newargs = []
   1.329 +        for item in self.cmdargs:
   1.330 +            if 'application.ini' in item:
   1.331 +                self.__app_ini = item
   1.332 +            else:
   1.333 +                newargs.append(item)
   1.334 +        self.cmdargs = newargs
   1.335 +
   1.336 +        if not self.__app_ini:
   1.337 +            raise ValueError('application.ini not found in cmdargs')
   1.338 +        if not os.path.exists(self.__app_ini):
   1.339 +            raise ValueError("file does not exist: '%s'" % self.__app_ini)
   1.340 +
   1.341 +        if (sys.platform == 'darwin' and
   1.342 +            self.binary == self.__DARWIN_PATH and
   1.343 +            self.__app_ini.endswith(self.__DARWIN_APP_INI_SUFFIX)):
   1.344 +            # If the application.ini is in an app bundle, then
   1.345 +            # it could be inside an "installed" XULRunner app.
   1.346 +            # If this is the case, use the app's actual
   1.347 +            # binary instead of the XUL framework's, so we get
   1.348 +            # a proper app icon, etc.
   1.349 +            new_binary = '/'.join(self.__app_ini.split('/')[:-2] +
   1.350 +                                  ['MacOS', 'xulrunner'])
   1.351 +            if os.path.exists(new_binary):
   1.352 +                self.binary = new_binary
   1.353 +
   1.354 +    @property
   1.355 +    def command(self):
   1.356 +        """Returns the command list to run."""
   1.357 +
   1.358 +        if self.__is_xulrunner_sdk:
   1.359 +            return [self.binary, self.__app_ini, '-profile',
   1.360 +                    self.profile.profile]
   1.361 +        else:
   1.362 +            return [self.binary, '-app', self.__app_ini, '-profile',
   1.363 +                    self.profile.profile]
   1.364 +
   1.365 +    def __find_xulrunner_binary(self):
   1.366 +        if sys.platform == 'darwin':
   1.367 +            if os.path.exists(self.__DARWIN_PATH):
   1.368 +                return self.__DARWIN_PATH
   1.369 +        if sys.platform == 'linux2':
   1.370 +            if os.path.exists(self.__LINUX_PATH):
   1.371 +                return self.__LINUX_PATH
   1.372 +        return None
   1.373 +
   1.374 +    def find_binary(self):
   1.375 +        # This gets called by the superclass constructor. It will
   1.376 +        # always get called, even if a binary was passed into the
   1.377 +        # constructor, because we want to have full control over
   1.378 +        # what the exact setting of self.binary is.
   1.379 +
   1.380 +        if not self.__real_binary:
   1.381 +            self.__real_binary = self.__find_xulrunner_binary()
   1.382 +            if not self.__real_binary:
   1.383 +                dummy_profile = {}
   1.384 +                runner = mozrunner.FirefoxRunner(profile=dummy_profile)
   1.385 +                self.__real_binary = runner.find_binary()
   1.386 +                self.names = runner.names
   1.387 +        return self.__real_binary
   1.388 +
   1.389 +def set_overloaded_modules(env_root, app_type, addon_id, preferences, overloads):
   1.390 +    # win32 file scheme needs 3 slashes
   1.391 +    desktop_file_scheme = "file://"
   1.392 +    if not env_root.startswith("/"):
   1.393 +      desktop_file_scheme = desktop_file_scheme + "/"
   1.394 +
   1.395 +    pref_prefix = "extensions.modules." + addon_id + ".path"
   1.396 +
   1.397 +    # Set preferences that will map require prefix to a given path
   1.398 +    for name, path in overloads.items():
   1.399 +        if len(name) == 0:
   1.400 +            prefName = pref_prefix
   1.401 +        else:
   1.402 +            prefName = pref_prefix + "." + name
   1.403 +        if app_type == "fennec-on-device":
   1.404 +            # For testing on device, we have to copy overloaded files from fs
   1.405 +            # to the device and use device path instead of local fs path.
   1.406 +            # Actual copy of files if done after the call to Profile constructor
   1.407 +            preferences[prefName] = "file://" + \
   1.408 +                FENNEC_REMOTE_PATH + "/overloads/" + name
   1.409 +        else:
   1.410 +            preferences[prefName] = desktop_file_scheme + \
   1.411 +                path.replace("\\", "/") + "/"
   1.412 +
   1.413 +def run_app(harness_root_dir, manifest_rdf, harness_options,
   1.414 +            app_type, binary=None, profiledir=None, verbose=False,
   1.415 +            parseable=False, enforce_timeouts=False,
   1.416 +            logfile=None, addons=None, args=None, extra_environment={},
   1.417 +            norun=None,
   1.418 +            used_files=None, enable_mobile=False,
   1.419 +            mobile_app_name=None,
   1.420 +            env_root=None,
   1.421 +            is_running_tests=False,
   1.422 +            overload_modules=False,
   1.423 +            bundle_sdk=True,
   1.424 +            pkgdir=""):
   1.425 +    if binary:
   1.426 +        binary = os.path.expanduser(binary)
   1.427 +
   1.428 +    if addons is None:
   1.429 +        addons = []
   1.430 +    else:
   1.431 +        addons = list(addons)
   1.432 +
   1.433 +    cmdargs = []
   1.434 +    preferences = dict(DEFAULT_COMMON_PREFS)
   1.435 +
   1.436 +    # For now, only allow running on Mobile with --force-mobile argument
   1.437 +    if app_type in ["fennec", "fennec-on-device"] and not enable_mobile:
   1.438 +        print """
   1.439 +  WARNING: Firefox Mobile support is still experimental.
   1.440 +  If you would like to run an addon on this platform, use --force-mobile flag:
   1.441 +
   1.442 +    cfx --force-mobile"""
   1.443 +        return 0
   1.444 +
   1.445 +    if app_type == "fennec-on-device":
   1.446 +        profile_class = FennecProfile
   1.447 +        preferences.update(DEFAULT_FENNEC_PREFS)
   1.448 +        runner_class = RemoteFennecRunner
   1.449 +        # We pass the intent name through command arguments
   1.450 +        cmdargs.append(mobile_app_name)
   1.451 +    elif enable_mobile or app_type == "fennec":
   1.452 +        profile_class = FennecProfile
   1.453 +        preferences.update(DEFAULT_FENNEC_PREFS)
   1.454 +        runner_class = FennecRunner
   1.455 +    elif app_type == "xulrunner":
   1.456 +        profile_class = XulrunnerAppProfile
   1.457 +        runner_class = XulrunnerAppRunner
   1.458 +        cmdargs.append(os.path.join(harness_root_dir, 'application.ini'))
   1.459 +    elif app_type == "firefox":
   1.460 +        profile_class = mozrunner.FirefoxProfile
   1.461 +        preferences.update(DEFAULT_FIREFOX_PREFS)
   1.462 +        runner_class = mozrunner.FirefoxRunner
   1.463 +    elif app_type == "thunderbird":
   1.464 +        profile_class = mozrunner.ThunderbirdProfile
   1.465 +        preferences.update(DEFAULT_THUNDERBIRD_PREFS)
   1.466 +        runner_class = mozrunner.ThunderbirdRunner
   1.467 +    else:
   1.468 +        raise ValueError("Unknown app: %s" % app_type)
   1.469 +    if sys.platform == 'darwin' and app_type != 'xulrunner':
   1.470 +        cmdargs.append('-foreground')
   1.471 +
   1.472 +    if args:
   1.473 +        cmdargs.extend(shlex.split(args))
   1.474 +
   1.475 +    # TODO: handle logs on remote device
   1.476 +    if app_type != "fennec-on-device":
   1.477 +        # tempfile.gettempdir() was constant, preventing two simultaneous "cfx
   1.478 +        # run"/"cfx test" on the same host. On unix it points at /tmp (which is
   1.479 +        # world-writeable), enabling a symlink attack (e.g. imagine some bad guy
   1.480 +        # does 'ln -s ~/.ssh/id_rsa /tmp/harness_result'). NamedTemporaryFile
   1.481 +        # gives us a unique filename that fixes both problems. We leave the
   1.482 +        # (0-byte) file in place until the browser-side code starts writing to
   1.483 +        # it, otherwise the symlink attack becomes possible again.
   1.484 +        fileno,resultfile = tempfile.mkstemp(prefix="harness-result-")
   1.485 +        os.close(fileno)
   1.486 +        harness_options['resultFile'] = resultfile
   1.487 +
   1.488 +    def maybe_remove_logfile():
   1.489 +        if os.path.exists(logfile):
   1.490 +            os.remove(logfile)
   1.491 +
   1.492 +    logfile_tail = None
   1.493 +
   1.494 +    # We always buffer output through a logfile for two reasons:
   1.495 +    # 1. On Windows, it's the only way to print console output to stdout/err.
   1.496 +    # 2. It enables us to keep track of the last time output was emitted,
   1.497 +    #    so we can raise an exception if the test runner hangs.
   1.498 +    if not logfile:
   1.499 +        fileno,logfile = tempfile.mkstemp(prefix="harness-log-")
   1.500 +        os.close(fileno)
   1.501 +    logfile_tail = follow_file(logfile)
   1.502 +    atexit.register(maybe_remove_logfile)
   1.503 +
   1.504 +    logfile = os.path.abspath(os.path.expanduser(logfile))
   1.505 +    maybe_remove_logfile()
   1.506 +
   1.507 +    env = {}
   1.508 +    env.update(os.environ)
   1.509 +    env['MOZ_NO_REMOTE'] = '1'
   1.510 +    env['XPCOM_DEBUG_BREAK'] = 'stack'
   1.511 +    env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
   1.512 +    env.update(extra_environment)
   1.513 +    if norun:
   1.514 +        cmdargs.append("-no-remote")
   1.515 +
   1.516 +    # Create the addon XPI so mozrunner will copy it to the profile it creates.
   1.517 +    # We delete it below after getting mozrunner to create the profile.
   1.518 +    from cuddlefish.xpi import build_xpi
   1.519 +    xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi')
   1.520 +    build_xpi(template_root_dir=harness_root_dir,
   1.521 +              manifest=manifest_rdf,
   1.522 +              xpi_path=xpi_path,
   1.523 +              harness_options=harness_options,
   1.524 +              limit_to=used_files,
   1.525 +              bundle_sdk=bundle_sdk,
   1.526 +              pkgdir=pkgdir)
   1.527 +    addons.append(xpi_path)
   1.528 +
   1.529 +    starttime = last_output_time = time.time()
   1.530 +
   1.531 +    # Redirect runner output to a file so we can catch output not generated
   1.532 +    # by us.
   1.533 +    # In theory, we could do this using simple redirection on all platforms
   1.534 +    # other than Windows, but this way we only have a single codepath to
   1.535 +    # maintain.
   1.536 +    fileno,outfile = tempfile.mkstemp(prefix="harness-stdout-")
   1.537 +    os.close(fileno)
   1.538 +    outfile_tail = follow_file(outfile)
   1.539 +    def maybe_remove_outfile():
   1.540 +        if os.path.exists(outfile):
   1.541 +            os.remove(outfile)
   1.542 +    atexit.register(maybe_remove_outfile)
   1.543 +    outf = open(outfile, "w")
   1.544 +    popen_kwargs = { 'stdout': outf, 'stderr': outf}
   1.545 +
   1.546 +    profile = None
   1.547 +
   1.548 +    if app_type == "fennec-on-device":
   1.549 +        # Install a special addon when we run firefox on mobile device
   1.550 +        # in order to be able to kill it
   1.551 +        mydir = os.path.dirname(os.path.abspath(__file__))
   1.552 +        addon_dir = os.path.join(mydir, "mobile-utils")
   1.553 +        addons.append(addon_dir)
   1.554 +
   1.555 +    # Overload addon-specific commonjs modules path with lib/ folder
   1.556 +    overloads = dict()
   1.557 +    if overload_modules:
   1.558 +        overloads[""] = os.path.join(env_root, "lib")
   1.559 +
   1.560 +    # Overload tests/ mapping with test/ folder, only when running test
   1.561 +    if is_running_tests:
   1.562 +        overloads["tests"] = os.path.join(env_root, "test")
   1.563 +
   1.564 +    set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \
   1.565 +                           preferences, overloads)
   1.566 +
   1.567 +    # the XPI file is copied into the profile here
   1.568 +    profile = profile_class(addons=addons,
   1.569 +                            profile=profiledir,
   1.570 +                            preferences=preferences)
   1.571 +
   1.572 +    # Delete the temporary xpi file
   1.573 +    os.remove(xpi_path)
   1.574 +
   1.575 +    # Copy overloaded files registered in set_overloaded_modules
   1.576 +    # For testing on device, we have to copy overloaded files from fs
   1.577 +    # to the device and use device path instead of local fs path.
   1.578 +    # (has to be done after the call to profile_class() which eventualy creates
   1.579 +    #  profile folder)
   1.580 +    if app_type == "fennec-on-device":
   1.581 +        profile_path = profile.profile
   1.582 +        for name, path in overloads.items():
   1.583 +            shutil.copytree(path, \
   1.584 +                os.path.join(profile_path, "overloads", name))
   1.585 +
   1.586 +    runner = runner_class(profile=profile,
   1.587 +                          binary=binary,
   1.588 +                          env=env,
   1.589 +                          cmdargs=cmdargs,
   1.590 +                          kp_kwargs=popen_kwargs)
   1.591 +
   1.592 +    sys.stdout.flush(); sys.stderr.flush()
   1.593 +
   1.594 +    if app_type == "fennec-on-device":
   1.595 +        if not enable_mobile:
   1.596 +            print >>sys.stderr, """
   1.597 +  WARNING: Firefox Mobile support is still experimental.
   1.598 +  If you would like to run an addon on this platform, use --force-mobile flag:
   1.599 +
   1.600 +    cfx --force-mobile"""
   1.601 +            return 0
   1.602 +
   1.603 +        # In case of mobile device, we need to get stdio from `adb logcat` cmd:
   1.604 +
   1.605 +        # First flush logs in order to avoid catching previous ones
   1.606 +        subprocess.call([binary, "logcat", "-c"])
   1.607 +
   1.608 +        # Launch adb command
   1.609 +        runner.start()
   1.610 +
   1.611 +        # We can immediatly remove temporary profile folder
   1.612 +        # as it has been uploaded to the device
   1.613 +        profile.cleanup()
   1.614 +        # We are not going to use the output log file
   1.615 +        outf.close()
   1.616 +
   1.617 +        # Then we simply display stdout of `adb logcat`
   1.618 +        p = subprocess.Popen([binary, "logcat", "stderr:V stdout:V GeckoConsole:V *:S"], stdout=subprocess.PIPE)
   1.619 +        while True:
   1.620 +            line = p.stdout.readline()
   1.621 +            if line == '':
   1.622 +                break
   1.623 +            # mobile-utils addon contains an application quit event observer
   1.624 +            # that will print this string:
   1.625 +            if "APPLICATION-QUIT" in line:
   1.626 +                break
   1.627 +
   1.628 +            if verbose:
   1.629 +                # if --verbose is given, we display everything:
   1.630 +                # All JS Console messages, stdout and stderr.
   1.631 +                m = CLEANUP_ADB.match(line)
   1.632 +                if not m:
   1.633 +                    print line.rstrip()
   1.634 +                    continue
   1.635 +                print m.group(3)
   1.636 +            else:
   1.637 +                # Otherwise, display addons messages dispatched through
   1.638 +                # console.[info, log, debug, warning, error](msg)
   1.639 +                m = FILTER_ONLY_CONSOLE_FROM_ADB.match(line)
   1.640 +                if m:
   1.641 +                    print m.group(2)
   1.642 +
   1.643 +        print >>sys.stderr, "Program terminated successfully."
   1.644 +        return 0
   1.645 +
   1.646 +
   1.647 +    print >>sys.stderr, "Using binary at '%s'." % runner.binary
   1.648 +
   1.649 +    # Ensure cfx is being used with Firefox 4.0+.
   1.650 +    # TODO: instead of dying when Firefox is < 4, warn when Firefox is outside
   1.651 +    # the minVersion/maxVersion boundaries.
   1.652 +    version_output = check_output(runner.command + ["-v"])
   1.653 +    # Note: this regex doesn't handle all valid versions in the Toolkit Version
   1.654 +    # Format <https://developer.mozilla.org/en/Toolkit_version_format>, just the
   1.655 +    # common subset that we expect Mozilla apps to use.
   1.656 +    mo = re.search(r"Mozilla (Firefox|Iceweasel|Fennec)\b[^ ]* ((\d+)\.\S*)",
   1.657 +                   version_output)
   1.658 +    if not mo:
   1.659 +        # cfx may be used with Thunderbird, SeaMonkey or an exotic Firefox
   1.660 +        # version.
   1.661 +        print """
   1.662 +  WARNING: cannot determine Firefox version; please ensure you are running
   1.663 +  a Mozilla application equivalent to Firefox 4.0 or greater.
   1.664 +  """
   1.665 +    elif mo.group(1) == "Fennec":
   1.666 +        # For now, only allow running on Mobile with --force-mobile argument
   1.667 +        if not enable_mobile:
   1.668 +            print """
   1.669 +  WARNING: Firefox Mobile support is still experimental.
   1.670 +  If you would like to run an addon on this platform, use --force-mobile flag:
   1.671 +
   1.672 +    cfx --force-mobile"""
   1.673 +            return
   1.674 +    else:
   1.675 +        version = mo.group(3)
   1.676 +        if int(version) < 4:
   1.677 +            print """
   1.678 +  cfx requires Firefox 4 or greater and is unable to find a compatible
   1.679 +  binary. Please install a newer version of Firefox or provide the path to
   1.680 +  your existing compatible version with the --binary flag:
   1.681 +
   1.682 +    cfx --binary=PATH_TO_FIREFOX_BINARY"""
   1.683 +            return
   1.684 +
   1.685 +        # Set the appropriate extensions.checkCompatibility preference to false,
   1.686 +        # so the tests run even if the SDK is not marked as compatible with the
   1.687 +        # version of Firefox on which they are running, and we don't have to
   1.688 +        # ensure we update the maxVersion before the version of Firefox changes
   1.689 +        # every six weeks.
   1.690 +        #
   1.691 +        # The regex we use here is effectively the same as BRANCH_REGEX from
   1.692 +        # /toolkit/mozapps/extensions/content/extensions.js, which toolkit apps
   1.693 +        # use to determine whether or not to load an incompatible addon.
   1.694 +        #
   1.695 +        br = re.search(r"^([^\.]+\.[0-9]+[a-z]*).*", mo.group(2), re.I)
   1.696 +        if br:
   1.697 +            prefname = 'extensions.checkCompatibility.' + br.group(1)
   1.698 +            profile.preferences[prefname] = False
   1.699 +            # Calling profile.set_preferences here duplicates the list of prefs
   1.700 +            # in prefs.js, since the profile calls self.set_preferences in its
   1.701 +            # constructor, but that is ok, because it doesn't change the set of
   1.702 +            # preferences that are ultimately registered in Firefox.
   1.703 +            profile.set_preferences(profile.preferences)
   1.704 +
   1.705 +    print >>sys.stderr, "Using profile at '%s'." % profile.profile
   1.706 +    sys.stderr.flush()
   1.707 +
   1.708 +    if norun:
   1.709 +        print "To launch the application, enter the following command:"
   1.710 +        print " ".join(runner.command) + " " + (" ".join(runner.cmdargs))
   1.711 +        return 0
   1.712 +
   1.713 +    runner.start()
   1.714 +
   1.715 +    done = False
   1.716 +    result = None
   1.717 +    test_name = "unknown"
   1.718 +
   1.719 +    def Timeout(message, test_name, parseable):
   1.720 +        if parseable:
   1.721 +            sys.stderr.write("TEST-UNEXPECTED-FAIL | %s | %s\n" % (test_name, message))
   1.722 +            sys.stderr.flush()
   1.723 +        return Exception(message)
   1.724 +
   1.725 +    try:
   1.726 +        while not done:
   1.727 +            time.sleep(0.05)
   1.728 +            for tail in (logfile_tail, outfile_tail):
   1.729 +                if tail:
   1.730 +                    new_chars = tail.next()
   1.731 +                    if new_chars:
   1.732 +                        last_output_time = time.time()
   1.733 +                        sys.stderr.write(new_chars)
   1.734 +                        sys.stderr.flush()
   1.735 +                        if is_running_tests and parseable:
   1.736 +                            match = PARSEABLE_TEST_NAME.search(new_chars)
   1.737 +                            if match:
   1.738 +                                test_name = match.group(1)
   1.739 +            if os.path.exists(resultfile):
   1.740 +                result = open(resultfile).read()
   1.741 +                if result:
   1.742 +                    if result in ['OK', 'FAIL']:
   1.743 +                        done = True
   1.744 +                    else:
   1.745 +                        sys.stderr.write("Hrm, resultfile (%s) contained something weird (%d bytes)\n" % (resultfile, len(result)))
   1.746 +                        sys.stderr.write("'"+result+"'\n")
   1.747 +            if enforce_timeouts:
   1.748 +                if time.time() - last_output_time > OUTPUT_TIMEOUT:
   1.749 +                    raise Timeout("Test output exceeded timeout (%ds)." %
   1.750 +                                  OUTPUT_TIMEOUT, test_name, parseable)
   1.751 +                if time.time() - starttime > RUN_TIMEOUT:
   1.752 +                    raise Timeout("Test run exceeded timeout (%ds)." %
   1.753 +                                  RUN_TIMEOUT, test_name, parseable)
   1.754 +    except:
   1.755 +        runner.stop()
   1.756 +        raise
   1.757 +    else:
   1.758 +        runner.wait(10)
   1.759 +    finally:
   1.760 +        outf.close()
   1.761 +        if profile:
   1.762 +            profile.cleanup()
   1.763 +
   1.764 +    print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime)
   1.765 +
   1.766 +    if result == 'OK':
   1.767 +        print >>sys.stderr, "Program terminated successfully."
   1.768 +        return 0
   1.769 +    else:
   1.770 +        print >>sys.stderr, "Program terminated unsuccessfully."
   1.771 +        return -1

mercurial