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

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial