Thu, 15 Jan 2015 15:59:08 +0100
Implement a real Private Browsing Mode condition by changing the API/ABI;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
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