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

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:73133bbc254b
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/.
4
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
14
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
20
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): .*)$')
25
26 # Used to detect the currently running test
27 PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n')
28
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
34
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
39
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.
44
45 For example:
46
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 """
62
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
77
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)
87
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
100
101
102 class FennecProfile(mozrunner.Profile):
103 preferences = {}
104 names = ['fennec']
105
106 class FennecRunner(mozrunner.Runner):
107 profile_class = FennecProfile
108
109 names = ['fennec']
110
111 __DARWIN_PATH = '/Applications/Fennec.app/Contents/MacOS/fennec'
112
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')
117
118 self.__real_binary = binary
119
120 mozrunner.Runner.__init__(self, **kwargs)
121
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
129
130 FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile'
131
132 class RemoteFennecRunner(mozrunner.Runner):
133 profile_class = FennecProfile
134
135 names = ['fennec']
136
137 _INTENT_PREFIX = 'org.mozilla.'
138
139 _adb_path = None
140
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
154
155 mobile_app_name = kwargs['cmdargs'][0]
156 self.profile = kwargs['profile']
157 self._adb_path = binary
158
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"])
163
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]
187
188 print "Launching mobile application with intent name " + self._intent_name
189
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.")
206
207 print "Pushing the addon to your device"
208
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])
212
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])
239
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 ]
250
251 def start(self):
252 subprocess.call(self.command)
253
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
266
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
276
277
278 class XulrunnerAppProfile(mozrunner.Profile):
279 preferences = {}
280 names = []
281
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.
288
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 """
293
294 profile_class = XulrunnerAppProfile
295
296 # This is a default, and will be overridden in the instance if
297 # Firefox is used in XULRunner mode.
298 names = ['xulrunner']
299
300 # Default location of XULRunner on OS X.
301 __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin"
302 __LINUX_PATH = "/usr/bin/xulrunner"
303
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'
307
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')
312
313 self.__app_ini = None
314 self.__real_binary = binary
315
316 mozrunner.Runner.__init__(self, **kwargs)
317
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
321
322 if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'):
323 self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary)
324
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
332
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)
337
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
350
351 @property
352 def command(self):
353 """Returns the command list to run."""
354
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]
361
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
370
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.
376
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
385
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 + "/"
391
392 pref_prefix = "extensions.modules." + addon_id + ".path"
393
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("\\", "/") + "/"
409
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)
424
425 if addons is None:
426 addons = []
427 else:
428 addons = list(addons)
429
430 cmdargs = []
431 preferences = dict(DEFAULT_COMMON_PREFS)
432
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:
438
439 cfx --force-mobile"""
440 return 0
441
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')
468
469 if args:
470 cmdargs.extend(shlex.split(args))
471
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
484
485 def maybe_remove_logfile():
486 if os.path.exists(logfile):
487 os.remove(logfile)
488
489 logfile_tail = None
490
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)
500
501 logfile = os.path.abspath(os.path.expanduser(logfile))
502 maybe_remove_logfile()
503
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")
512
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)
525
526 starttime = last_output_time = time.time()
527
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}
542
543 profile = None
544
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)
551
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")
556
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")
560
561 set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \
562 preferences, overloads)
563
564 # the XPI file is copied into the profile here
565 profile = profile_class(addons=addons,
566 profile=profiledir,
567 preferences=preferences)
568
569 # Delete the temporary xpi file
570 os.remove(xpi_path)
571
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))
582
583 runner = runner_class(profile=profile,
584 binary=binary,
585 env=env,
586 cmdargs=cmdargs,
587 kp_kwargs=popen_kwargs)
588
589 sys.stdout.flush(); sys.stderr.flush()
590
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:
596
597 cfx --force-mobile"""
598 return 0
599
600 # In case of mobile device, we need to get stdio from `adb logcat` cmd:
601
602 # First flush logs in order to avoid catching previous ones
603 subprocess.call([binary, "logcat", "-c"])
604
605 # Launch adb command
606 runner.start()
607
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()
613
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
624
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)
639
640 print >>sys.stderr, "Program terminated successfully."
641 return 0
642
643
644 print >>sys.stderr, "Using binary at '%s'." % runner.binary
645
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:
668
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:
678
679 cfx --binary=PATH_TO_FIREFOX_BINARY"""
680 return
681
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)
701
702 print >>sys.stderr, "Using profile at '%s'." % profile.profile
703 sys.stderr.flush()
704
705 if norun:
706 print "To launch the application, enter the following command:"
707 print " ".join(runner.command) + " " + (" ".join(runner.cmdargs))
708 return 0
709
710 runner.start()
711
712 done = False
713 result = None
714 test_name = "unknown"
715
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)
721
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()
760
761 print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime)
762
763 if result == 'OK':
764 print >>sys.stderr, "Program terminated successfully."
765 return 0
766 else:
767 print >>sys.stderr, "Program terminated unsuccessfully."
768 return -1

mercurial