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

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

     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/.
     5 import os
     6 import sys
     7 import time
     8 import tempfile
     9 import atexit
    10 import shlex
    11 import subprocess
    12 import re
    13 import shutil
    15 import mozrunner
    16 from cuddlefish.prefs import DEFAULT_COMMON_PREFS
    17 from cuddlefish.prefs import DEFAULT_FIREFOX_PREFS
    18 from cuddlefish.prefs import DEFAULT_THUNDERBIRD_PREFS
    19 from cuddlefish.prefs import DEFAULT_FENNEC_PREFS
    21 # Used to remove noise from ADB output
    22 CLEANUP_ADB = re.compile(r'^(I|E)/(stdout|stderr|GeckoConsole)\s*\(\s*\d+\):\s*(.*)$')
    23 # Used to filter only messages send by `console` module
    24 FILTER_ONLY_CONSOLE_FROM_ADB = re.compile(r'^I/(stdout|stderr)\s*\(\s*\d+\):\s*((info|warning|error|debug): .*)$')
    26 # Used to detect the currently running test
    27 PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n')
    29 # Maximum time we'll wait for tests to finish, in seconds.
    30 # The purpose of this timeout is to recover from infinite loops.  It should be
    31 # longer than the amount of time any test run takes, including those on slow
    32 # machines running slow (debug) versions of Firefox.
    33 RUN_TIMEOUT = 1.5 * 60 * 60 # 1.5 Hour
    35 # Maximum time we'll wait for tests to emit output, in seconds.
    36 # The purpose of this timeout is to recover from hangs.  It should be longer
    37 # than the amount of time any test takes to report results.
    38 OUTPUT_TIMEOUT = 60 * 5 # five minutes
    40 def follow_file(filename):
    41     """
    42     Generator that yields the latest unread content from the given
    43     file, or None if no new content is available.
    45     For example:
    47       >>> f = open('temp.txt', 'w')
    48       >>> f.write('hello')
    49       >>> f.flush()
    50       >>> tail = follow_file('temp.txt')
    51       >>> tail.next()
    52       'hello'
    53       >>> tail.next() is None
    54       True
    55       >>> f.write('there')
    56       >>> f.flush()
    57       >>> tail.next()
    58       'there'
    59       >>> f.close()
    60       >>> os.remove('temp.txt')
    61     """
    63     last_pos = 0
    64     last_size = 0
    65     while True:
    66         newstuff = None
    67         if os.path.exists(filename):
    68             size = os.stat(filename).st_size
    69             if size > last_size:
    70                 last_size = size
    71                 f = open(filename, 'r')
    72                 f.seek(last_pos)
    73                 newstuff = f.read()
    74                 last_pos = f.tell()
    75                 f.close()
    76         yield newstuff
    78 # subprocess.check_output only appeared in python2.7, so this code is taken
    79 # from python source code for compatibility with py2.5/2.6
    80 class CalledProcessError(Exception):
    81     def __init__(self, returncode, cmd, output=None):
    82         self.returncode = returncode
    83         self.cmd = cmd
    84         self.output = output
    85     def __str__(self):
    86         return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
    88 def check_output(*popenargs, **kwargs):
    89     if 'stdout' in kwargs:
    90         raise ValueError('stdout argument not allowed, it will be overridden.')
    91     process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs)
    92     output, unused_err = process.communicate()
    93     retcode = process.poll()
    94     if retcode:
    95         cmd = kwargs.get("args")
    96         if cmd is None:
    97             cmd = popenargs[0]
    98         raise CalledProcessError(retcode, cmd, output=output)
    99     return output
   102 class FennecProfile(mozrunner.Profile):
   103     preferences = {}
   104     names = ['fennec']
   106 class FennecRunner(mozrunner.Runner):
   107     profile_class = FennecProfile
   109     names = ['fennec']
   111     __DARWIN_PATH = '/Applications/Fennec.app/Contents/MacOS/fennec'
   113     def __init__(self, binary=None, **kwargs):
   114         if sys.platform == 'darwin' and binary and binary.endswith('.app'):
   115             # Assume it's a Fennec app dir.
   116             binary = os.path.join(binary, 'Contents/MacOS/fennec')
   118         self.__real_binary = binary
   120         mozrunner.Runner.__init__(self, **kwargs)
   122     def find_binary(self):
   123         if not self.__real_binary:
   124             if sys.platform == 'darwin':
   125                 if os.path.exists(self.__DARWIN_PATH):
   126                     return self.__DARWIN_PATH
   127             self.__real_binary = mozrunner.Runner.find_binary(self)
   128         return self.__real_binary
   130 FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile'
   132 class RemoteFennecRunner(mozrunner.Runner):
   133     profile_class = FennecProfile
   135     names = ['fennec']
   137     _INTENT_PREFIX = 'org.mozilla.'
   139     _adb_path = None
   141     def __init__(self, binary=None, **kwargs):
   142         # Check that we have a binary set
   143         if not binary:
   144             raise ValueError("You have to define `--binary` option set to the "
   145                             "path to your ADB executable.")
   146         # Ensure that binary refer to a valid ADB executable
   147         output = subprocess.Popen([binary], stdout=subprocess.PIPE,
   148                                   stderr=subprocess.PIPE).communicate()
   149         output = "".join(output)
   150         if not ("Android Debug Bridge" in output):
   151             raise ValueError("`--binary` option should be the path to your "
   152                             "ADB executable.")
   153         self.binary = binary
   155         mobile_app_name = kwargs['cmdargs'][0]
   156         self.profile = kwargs['profile']
   157         self._adb_path = binary
   159         # This pref has to be set to `false` otherwise, we do not receive
   160         # output of adb commands!
   161         subprocess.call([self._adb_path, "shell",
   162                         "setprop log.redirect-stdio false"])
   164         # Android apps are launched by their "intent" name,
   165         # Automatically detect already installed firefox by using `pm` program
   166         # or use name given as cfx `--mobile-app` argument.
   167         intents = self.getIntentNames()
   168         if not intents:
   169             raise ValueError("Unable to found any Firefox "
   170                              "application on your device.")
   171         elif mobile_app_name:
   172             if not mobile_app_name in intents:
   173                 raise ValueError("Unable to found Firefox application "
   174                                  "with intent name '%s'\n"
   175                                  "Available ones are: %s" %
   176                                  (mobile_app_name, ", ".join(intents)))
   177             self._intent_name = self._INTENT_PREFIX + mobile_app_name
   178         else:
   179             if "firefox" in intents:
   180                 self._intent_name = self._INTENT_PREFIX + "firefox"
   181             elif "firefox_beta" in intents:
   182                 self._intent_name = self._INTENT_PREFIX + "firefox_beta"
   183             elif "firefox_nightly" in intents:
   184                 self._intent_name = self._INTENT_PREFIX + "firefox_nightly"
   185             else:
   186                 self._intent_name = self._INTENT_PREFIX + intents[0]
   188         print "Launching mobile application with intent name " + self._intent_name
   190         # First try to kill firefox if it is already running
   191         pid = self.getProcessPID(self._intent_name)
   192         if pid != None:
   193             print "Killing running Firefox instance ..."
   194             subprocess.call([self._adb_path, "shell",
   195                              "am force-stop " + self._intent_name])
   196             time.sleep(7)
   197             # It appears recently that the PID still exists even after
   198             # Fennec closes, so removing this error still allows the tests
   199             # to pass as the new Fennec instance is able to start.
   200             # Leaving error in but commented out for now.
   201             #
   202             #if self.getProcessPID(self._intent_name) != None:
   203             #    raise Exception("Unable to automatically kill running Firefox" +
   204             #                    " instance. Please close it manually before " +
   205             #                    "executing cfx.")
   207         print "Pushing the addon to your device"
   209         # Create a clean empty profile on the sd card
   210         subprocess.call([self._adb_path, "shell", "rm -r " + FENNEC_REMOTE_PATH])
   211         subprocess.call([self._adb_path, "shell", "mkdir " + FENNEC_REMOTE_PATH])
   213         # Push the profile folder created by mozrunner to the device
   214         # (we can't simply use `adb push` as it doesn't copy empty folders)
   215         localDir = self.profile.profile
   216         remoteDir = FENNEC_REMOTE_PATH
   217         for root, dirs, files in os.walk(localDir, followlinks='true'):
   218             relRoot = os.path.relpath(root, localDir)
   219             # Note about os.path usage below:
   220             # Local files may be using Windows `\` separators but
   221             # remote are always `/`, so we need to convert local ones to `/`
   222             for file in files:
   223                 localFile = os.path.join(root, file)
   224                 remoteFile = remoteDir.replace("/", os.sep)
   225                 if relRoot != ".":
   226                     remoteFile = os.path.join(remoteFile, relRoot)
   227                 remoteFile = os.path.join(remoteFile, file)
   228                 remoteFile = "/".join(remoteFile.split(os.sep))
   229                 subprocess.Popen([self._adb_path, "push", localFile, remoteFile],
   230                                  stderr=subprocess.PIPE).wait()
   231             for dir in dirs:
   232                 targetDir = remoteDir.replace("/", os.sep)
   233                 if relRoot != ".":
   234                     targetDir = os.path.join(targetDir, relRoot)
   235                 targetDir = os.path.join(targetDir, dir)
   236                 targetDir = "/".join(targetDir.split(os.sep))
   237                 # `-p` option is not supported on all devices!
   238                 subprocess.call([self._adb_path, "shell", "mkdir " + targetDir])
   240     @property
   241     def command(self):
   242         """Returns the command list to run."""
   243         return [self._adb_path,
   244             "shell",
   245             "am start " +
   246                 "-a android.activity.MAIN " +
   247                 "-n " + self._intent_name + "/" + self._intent_name + ".App " +
   248                 "--es args \"-profile " + FENNEC_REMOTE_PATH + "\""
   249         ]
   251     def start(self):
   252         subprocess.call(self.command)
   254     def getProcessPID(self, processName):
   255         p = subprocess.Popen([self._adb_path, "shell", "ps"],
   256                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   257         line = p.stdout.readline()
   258         while line:
   259             columns = line.split()
   260             pid = columns[1]
   261             name = columns[-1]
   262             line = p.stdout.readline()
   263             if processName in name:
   264                 return pid
   265         return None
   267     def getIntentNames(self):
   268         p = subprocess.Popen([self._adb_path, "shell", "pm list packages"],
   269                              stdout=subprocess.PIPE, stderr=subprocess.PIPE)
   270         names = []
   271         for line in p.stdout.readlines():
   272             line = re.sub("(^package:)|\s", "", line)
   273             if self._INTENT_PREFIX in line:
   274                 names.append(line.replace(self._INTENT_PREFIX, ""))
   275         return names
   278 class XulrunnerAppProfile(mozrunner.Profile):
   279     preferences = {}
   280     names = []
   282 class XulrunnerAppRunner(mozrunner.Runner):
   283     """
   284     Runner for any XULRunner app. Can use a Firefox binary in XULRunner
   285     mode to execute the app, or can use XULRunner itself. Expects the
   286     app's application.ini to be passed in as one of the items in
   287     'cmdargs' in the constructor.
   289     This class relies a lot on the particulars of mozrunner.Runner's
   290     implementation, and does some unfortunate acrobatics to get around
   291     some of the class' limitations/assumptions.
   292     """
   294     profile_class = XulrunnerAppProfile
   296     # This is a default, and will be overridden in the instance if
   297     # Firefox is used in XULRunner mode.
   298     names = ['xulrunner']
   300     # Default location of XULRunner on OS X.
   301     __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin"
   302     __LINUX_PATH  = "/usr/bin/xulrunner"
   304     # What our application.ini's path looks like if it's part of
   305     # an "installed" XULRunner app on OS X.
   306     __DARWIN_APP_INI_SUFFIX = '.app/Contents/Resources/application.ini'
   308     def __init__(self, binary=None, **kwargs):
   309         if sys.platform == 'darwin' and binary and binary.endswith('.app'):
   310             # Assume it's a Firefox app dir.
   311             binary = os.path.join(binary, 'Contents/MacOS/firefox-bin')
   313         self.__app_ini = None
   314         self.__real_binary = binary
   316         mozrunner.Runner.__init__(self, **kwargs)
   318         # See if we're using a genuine xulrunner-bin from the XULRunner SDK,
   319         # or if we're being asked to use Firefox in XULRunner mode.
   320         self.__is_xulrunner_sdk = 'xulrunner' in self.binary
   322         if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'):
   323             self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary)
   325         newargs = []
   326         for item in self.cmdargs:
   327             if 'application.ini' in item:
   328                 self.__app_ini = item
   329             else:
   330                 newargs.append(item)
   331         self.cmdargs = newargs
   333         if not self.__app_ini:
   334             raise ValueError('application.ini not found in cmdargs')
   335         if not os.path.exists(self.__app_ini):
   336             raise ValueError("file does not exist: '%s'" % self.__app_ini)
   338         if (sys.platform == 'darwin' and
   339             self.binary == self.__DARWIN_PATH and
   340             self.__app_ini.endswith(self.__DARWIN_APP_INI_SUFFIX)):
   341             # If the application.ini is in an app bundle, then
   342             # it could be inside an "installed" XULRunner app.
   343             # If this is the case, use the app's actual
   344             # binary instead of the XUL framework's, so we get
   345             # a proper app icon, etc.
   346             new_binary = '/'.join(self.__app_ini.split('/')[:-2] +
   347                                   ['MacOS', 'xulrunner'])
   348             if os.path.exists(new_binary):
   349                 self.binary = new_binary
   351     @property
   352     def command(self):
   353         """Returns the command list to run."""
   355         if self.__is_xulrunner_sdk:
   356             return [self.binary, self.__app_ini, '-profile',
   357                     self.profile.profile]
   358         else:
   359             return [self.binary, '-app', self.__app_ini, '-profile',
   360                     self.profile.profile]
   362     def __find_xulrunner_binary(self):
   363         if sys.platform == 'darwin':
   364             if os.path.exists(self.__DARWIN_PATH):
   365                 return self.__DARWIN_PATH
   366         if sys.platform == 'linux2':
   367             if os.path.exists(self.__LINUX_PATH):
   368                 return self.__LINUX_PATH
   369         return None
   371     def find_binary(self):
   372         # This gets called by the superclass constructor. It will
   373         # always get called, even if a binary was passed into the
   374         # constructor, because we want to have full control over
   375         # what the exact setting of self.binary is.
   377         if not self.__real_binary:
   378             self.__real_binary = self.__find_xulrunner_binary()
   379             if not self.__real_binary:
   380                 dummy_profile = {}
   381                 runner = mozrunner.FirefoxRunner(profile=dummy_profile)
   382                 self.__real_binary = runner.find_binary()
   383                 self.names = runner.names
   384         return self.__real_binary
   386 def set_overloaded_modules(env_root, app_type, addon_id, preferences, overloads):
   387     # win32 file scheme needs 3 slashes
   388     desktop_file_scheme = "file://"
   389     if not env_root.startswith("/"):
   390       desktop_file_scheme = desktop_file_scheme + "/"
   392     pref_prefix = "extensions.modules." + addon_id + ".path"
   394     # Set preferences that will map require prefix to a given path
   395     for name, path in overloads.items():
   396         if len(name) == 0:
   397             prefName = pref_prefix
   398         else:
   399             prefName = pref_prefix + "." + name
   400         if app_type == "fennec-on-device":
   401             # For testing on device, we have to copy overloaded files from fs
   402             # to the device and use device path instead of local fs path.
   403             # Actual copy of files if done after the call to Profile constructor
   404             preferences[prefName] = "file://" + \
   405                 FENNEC_REMOTE_PATH + "/overloads/" + name
   406         else:
   407             preferences[prefName] = desktop_file_scheme + \
   408                 path.replace("\\", "/") + "/"
   410 def run_app(harness_root_dir, manifest_rdf, harness_options,
   411             app_type, binary=None, profiledir=None, verbose=False,
   412             parseable=False, enforce_timeouts=False,
   413             logfile=None, addons=None, args=None, extra_environment={},
   414             norun=None,
   415             used_files=None, enable_mobile=False,
   416             mobile_app_name=None,
   417             env_root=None,
   418             is_running_tests=False,
   419             overload_modules=False,
   420             bundle_sdk=True,
   421             pkgdir=""):
   422     if binary:
   423         binary = os.path.expanduser(binary)
   425     if addons is None:
   426         addons = []
   427     else:
   428         addons = list(addons)
   430     cmdargs = []
   431     preferences = dict(DEFAULT_COMMON_PREFS)
   433     # For now, only allow running on Mobile with --force-mobile argument
   434     if app_type in ["fennec", "fennec-on-device"] and not enable_mobile:
   435         print """
   436   WARNING: Firefox Mobile support is still experimental.
   437   If you would like to run an addon on this platform, use --force-mobile flag:
   439     cfx --force-mobile"""
   440         return 0
   442     if app_type == "fennec-on-device":
   443         profile_class = FennecProfile
   444         preferences.update(DEFAULT_FENNEC_PREFS)
   445         runner_class = RemoteFennecRunner
   446         # We pass the intent name through command arguments
   447         cmdargs.append(mobile_app_name)
   448     elif enable_mobile or app_type == "fennec":
   449         profile_class = FennecProfile
   450         preferences.update(DEFAULT_FENNEC_PREFS)
   451         runner_class = FennecRunner
   452     elif app_type == "xulrunner":
   453         profile_class = XulrunnerAppProfile
   454         runner_class = XulrunnerAppRunner
   455         cmdargs.append(os.path.join(harness_root_dir, 'application.ini'))
   456     elif app_type == "firefox":
   457         profile_class = mozrunner.FirefoxProfile
   458         preferences.update(DEFAULT_FIREFOX_PREFS)
   459         runner_class = mozrunner.FirefoxRunner
   460     elif app_type == "thunderbird":
   461         profile_class = mozrunner.ThunderbirdProfile
   462         preferences.update(DEFAULT_THUNDERBIRD_PREFS)
   463         runner_class = mozrunner.ThunderbirdRunner
   464     else:
   465         raise ValueError("Unknown app: %s" % app_type)
   466     if sys.platform == 'darwin' and app_type != 'xulrunner':
   467         cmdargs.append('-foreground')
   469     if args:
   470         cmdargs.extend(shlex.split(args))
   472     # TODO: handle logs on remote device
   473     if app_type != "fennec-on-device":
   474         # tempfile.gettempdir() was constant, preventing two simultaneous "cfx
   475         # run"/"cfx test" on the same host. On unix it points at /tmp (which is
   476         # world-writeable), enabling a symlink attack (e.g. imagine some bad guy
   477         # does 'ln -s ~/.ssh/id_rsa /tmp/harness_result'). NamedTemporaryFile
   478         # gives us a unique filename that fixes both problems. We leave the
   479         # (0-byte) file in place until the browser-side code starts writing to
   480         # it, otherwise the symlink attack becomes possible again.
   481         fileno,resultfile = tempfile.mkstemp(prefix="harness-result-")
   482         os.close(fileno)
   483         harness_options['resultFile'] = resultfile
   485     def maybe_remove_logfile():
   486         if os.path.exists(logfile):
   487             os.remove(logfile)
   489     logfile_tail = None
   491     # We always buffer output through a logfile for two reasons:
   492     # 1. On Windows, it's the only way to print console output to stdout/err.
   493     # 2. It enables us to keep track of the last time output was emitted,
   494     #    so we can raise an exception if the test runner hangs.
   495     if not logfile:
   496         fileno,logfile = tempfile.mkstemp(prefix="harness-log-")
   497         os.close(fileno)
   498     logfile_tail = follow_file(logfile)
   499     atexit.register(maybe_remove_logfile)
   501     logfile = os.path.abspath(os.path.expanduser(logfile))
   502     maybe_remove_logfile()
   504     env = {}
   505     env.update(os.environ)
   506     env['MOZ_NO_REMOTE'] = '1'
   507     env['XPCOM_DEBUG_BREAK'] = 'stack'
   508     env['NS_TRACE_MALLOC_DISABLE_STACKS'] = '1'
   509     env.update(extra_environment)
   510     if norun:
   511         cmdargs.append("-no-remote")
   513     # Create the addon XPI so mozrunner will copy it to the profile it creates.
   514     # We delete it below after getting mozrunner to create the profile.
   515     from cuddlefish.xpi import build_xpi
   516     xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi')
   517     build_xpi(template_root_dir=harness_root_dir,
   518               manifest=manifest_rdf,
   519               xpi_path=xpi_path,
   520               harness_options=harness_options,
   521               limit_to=used_files,
   522               bundle_sdk=bundle_sdk,
   523               pkgdir=pkgdir)
   524     addons.append(xpi_path)
   526     starttime = last_output_time = time.time()
   528     # Redirect runner output to a file so we can catch output not generated
   529     # by us.
   530     # In theory, we could do this using simple redirection on all platforms
   531     # other than Windows, but this way we only have a single codepath to
   532     # maintain.
   533     fileno,outfile = tempfile.mkstemp(prefix="harness-stdout-")
   534     os.close(fileno)
   535     outfile_tail = follow_file(outfile)
   536     def maybe_remove_outfile():
   537         if os.path.exists(outfile):
   538             os.remove(outfile)
   539     atexit.register(maybe_remove_outfile)
   540     outf = open(outfile, "w")
   541     popen_kwargs = { 'stdout': outf, 'stderr': outf}
   543     profile = None
   545     if app_type == "fennec-on-device":
   546         # Install a special addon when we run firefox on mobile device
   547         # in order to be able to kill it
   548         mydir = os.path.dirname(os.path.abspath(__file__))
   549         addon_dir = os.path.join(mydir, "mobile-utils")
   550         addons.append(addon_dir)
   552     # Overload addon-specific commonjs modules path with lib/ folder
   553     overloads = dict()
   554     if overload_modules:
   555         overloads[""] = os.path.join(env_root, "lib")
   557     # Overload tests/ mapping with test/ folder, only when running test
   558     if is_running_tests:
   559         overloads["tests"] = os.path.join(env_root, "test")
   561     set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \
   562                            preferences, overloads)
   564     # the XPI file is copied into the profile here
   565     profile = profile_class(addons=addons,
   566                             profile=profiledir,
   567                             preferences=preferences)
   569     # Delete the temporary xpi file
   570     os.remove(xpi_path)
   572     # Copy overloaded files registered in set_overloaded_modules
   573     # For testing on device, we have to copy overloaded files from fs
   574     # to the device and use device path instead of local fs path.
   575     # (has to be done after the call to profile_class() which eventualy creates
   576     #  profile folder)
   577     if app_type == "fennec-on-device":
   578         profile_path = profile.profile
   579         for name, path in overloads.items():
   580             shutil.copytree(path, \
   581                 os.path.join(profile_path, "overloads", name))
   583     runner = runner_class(profile=profile,
   584                           binary=binary,
   585                           env=env,
   586                           cmdargs=cmdargs,
   587                           kp_kwargs=popen_kwargs)
   589     sys.stdout.flush(); sys.stderr.flush()
   591     if app_type == "fennec-on-device":
   592         if not enable_mobile:
   593             print >>sys.stderr, """
   594   WARNING: Firefox Mobile support is still experimental.
   595   If you would like to run an addon on this platform, use --force-mobile flag:
   597     cfx --force-mobile"""
   598             return 0
   600         # In case of mobile device, we need to get stdio from `adb logcat` cmd:
   602         # First flush logs in order to avoid catching previous ones
   603         subprocess.call([binary, "logcat", "-c"])
   605         # Launch adb command
   606         runner.start()
   608         # We can immediatly remove temporary profile folder
   609         # as it has been uploaded to the device
   610         profile.cleanup()
   611         # We are not going to use the output log file
   612         outf.close()
   614         # Then we simply display stdout of `adb logcat`
   615         p = subprocess.Popen([binary, "logcat", "stderr:V stdout:V GeckoConsole:V *:S"], stdout=subprocess.PIPE)
   616         while True:
   617             line = p.stdout.readline()
   618             if line == '':
   619                 break
   620             # mobile-utils addon contains an application quit event observer
   621             # that will print this string:
   622             if "APPLICATION-QUIT" in line:
   623                 break
   625             if verbose:
   626                 # if --verbose is given, we display everything:
   627                 # All JS Console messages, stdout and stderr.
   628                 m = CLEANUP_ADB.match(line)
   629                 if not m:
   630                     print line.rstrip()
   631                     continue
   632                 print m.group(3)
   633             else:
   634                 # Otherwise, display addons messages dispatched through
   635                 # console.[info, log, debug, warning, error](msg)
   636                 m = FILTER_ONLY_CONSOLE_FROM_ADB.match(line)
   637                 if m:
   638                     print m.group(2)
   640         print >>sys.stderr, "Program terminated successfully."
   641         return 0
   644     print >>sys.stderr, "Using binary at '%s'." % runner.binary
   646     # Ensure cfx is being used with Firefox 4.0+.
   647     # TODO: instead of dying when Firefox is < 4, warn when Firefox is outside
   648     # the minVersion/maxVersion boundaries.
   649     version_output = check_output(runner.command + ["-v"])
   650     # Note: this regex doesn't handle all valid versions in the Toolkit Version
   651     # Format <https://developer.mozilla.org/en/Toolkit_version_format>, just the
   652     # common subset that we expect Mozilla apps to use.
   653     mo = re.search(r"Mozilla (Firefox|Iceweasel|Fennec)\b[^ ]* ((\d+)\.\S*)",
   654                    version_output)
   655     if not mo:
   656         # cfx may be used with Thunderbird, SeaMonkey or an exotic Firefox
   657         # version.
   658         print """
   659   WARNING: cannot determine Firefox version; please ensure you are running
   660   a Mozilla application equivalent to Firefox 4.0 or greater.
   661   """
   662     elif mo.group(1) == "Fennec":
   663         # For now, only allow running on Mobile with --force-mobile argument
   664         if not enable_mobile:
   665             print """
   666   WARNING: Firefox Mobile support is still experimental.
   667   If you would like to run an addon on this platform, use --force-mobile flag:
   669     cfx --force-mobile"""
   670             return
   671     else:
   672         version = mo.group(3)
   673         if int(version) < 4:
   674             print """
   675   cfx requires Firefox 4 or greater and is unable to find a compatible
   676   binary. Please install a newer version of Firefox or provide the path to
   677   your existing compatible version with the --binary flag:
   679     cfx --binary=PATH_TO_FIREFOX_BINARY"""
   680             return
   682         # Set the appropriate extensions.checkCompatibility preference to false,
   683         # so the tests run even if the SDK is not marked as compatible with the
   684         # version of Firefox on which they are running, and we don't have to
   685         # ensure we update the maxVersion before the version of Firefox changes
   686         # every six weeks.
   687         #
   688         # The regex we use here is effectively the same as BRANCH_REGEX from
   689         # /toolkit/mozapps/extensions/content/extensions.js, which toolkit apps
   690         # use to determine whether or not to load an incompatible addon.
   691         #
   692         br = re.search(r"^([^\.]+\.[0-9]+[a-z]*).*", mo.group(2), re.I)
   693         if br:
   694             prefname = 'extensions.checkCompatibility.' + br.group(1)
   695             profile.preferences[prefname] = False
   696             # Calling profile.set_preferences here duplicates the list of prefs
   697             # in prefs.js, since the profile calls self.set_preferences in its
   698             # constructor, but that is ok, because it doesn't change the set of
   699             # preferences that are ultimately registered in Firefox.
   700             profile.set_preferences(profile.preferences)
   702     print >>sys.stderr, "Using profile at '%s'." % profile.profile
   703     sys.stderr.flush()
   705     if norun:
   706         print "To launch the application, enter the following command:"
   707         print " ".join(runner.command) + " " + (" ".join(runner.cmdargs))
   708         return 0
   710     runner.start()
   712     done = False
   713     result = None
   714     test_name = "unknown"
   716     def Timeout(message, test_name, parseable):
   717         if parseable:
   718             sys.stderr.write("TEST-UNEXPECTED-FAIL | %s | %s\n" % (test_name, message))
   719             sys.stderr.flush()
   720         return Exception(message)
   722     try:
   723         while not done:
   724             time.sleep(0.05)
   725             for tail in (logfile_tail, outfile_tail):
   726                 if tail:
   727                     new_chars = tail.next()
   728                     if new_chars:
   729                         last_output_time = time.time()
   730                         sys.stderr.write(new_chars)
   731                         sys.stderr.flush()
   732                         if is_running_tests and parseable:
   733                             match = PARSEABLE_TEST_NAME.search(new_chars)
   734                             if match:
   735                                 test_name = match.group(1)
   736             if os.path.exists(resultfile):
   737                 result = open(resultfile).read()
   738                 if result:
   739                     if result in ['OK', 'FAIL']:
   740                         done = True
   741                     else:
   742                         sys.stderr.write("Hrm, resultfile (%s) contained something weird (%d bytes)\n" % (resultfile, len(result)))
   743                         sys.stderr.write("'"+result+"'\n")
   744             if enforce_timeouts:
   745                 if time.time() - last_output_time > OUTPUT_TIMEOUT:
   746                     raise Timeout("Test output exceeded timeout (%ds)." %
   747                                   OUTPUT_TIMEOUT, test_name, parseable)
   748                 if time.time() - starttime > RUN_TIMEOUT:
   749                     raise Timeout("Test run exceeded timeout (%ds)." %
   750                                   RUN_TIMEOUT, test_name, parseable)
   751     except:
   752         runner.stop()
   753         raise
   754     else:
   755         runner.wait(10)
   756     finally:
   757         outf.close()
   758         if profile:
   759             profile.cleanup()
   761     print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime)
   763     if result == 'OK':
   764         print >>sys.stderr, "Program terminated successfully."
   765         return 0
   766     else:
   767         print >>sys.stderr, "Program terminated unsuccessfully."
   768         return -1

mercurial