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