diff options
Diffstat (limited to 'addon-sdk/source/python-lib/cuddlefish/runner.py')
-rw-r--r-- | addon-sdk/source/python-lib/cuddlefish/runner.py | 767 |
1 files changed, 767 insertions, 0 deletions
diff --git a/addon-sdk/source/python-lib/cuddlefish/runner.py b/addon-sdk/source/python-lib/cuddlefish/runner.py new file mode 100644 index 000000000..7f337cc03 --- /dev/null +++ b/addon-sdk/source/python-lib/cuddlefish/runner.py @@ -0,0 +1,767 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import os +import sys +import time +import tempfile +import atexit +import shlex +import subprocess +import re +import shutil + +import mozrunner +from cuddlefish.prefs import DEFAULT_COMMON_PREFS +from cuddlefish.prefs import DEFAULT_FIREFOX_PREFS +from cuddlefish.prefs import DEFAULT_THUNDERBIRD_PREFS +from cuddlefish.prefs import DEFAULT_FENNEC_PREFS +from cuddlefish.prefs import DEFAULT_NO_CONNECTIONS_PREFS +from cuddlefish.prefs import DEFAULT_TEST_PREFS + +# Used to remove noise from ADB output +CLEANUP_ADB = re.compile(r'^(I|E)/(stdout|stderr|GeckoConsole)\s*\(\s*\d+\):\s*(.*)$') +# Used to filter only messages send by `console` module +FILTER_ONLY_CONSOLE_FROM_ADB = re.compile(r'^I/(stdout|stderr)\s*\(\s*\d+\):\s*((info|warning|error|debug): .*)$') + +# Used to detect the currently running test +PARSEABLE_TEST_NAME = re.compile(r'TEST-START \| ([^\n]+)\n') + +# Maximum time we'll wait for tests to finish, in seconds. +# The purpose of this timeout is to recover from infinite loops. It should be +# longer than the amount of time any test run takes, including those on slow +# machines running slow (debug) versions of Firefox. +RUN_TIMEOUT = 5400 #1.5 hours (1.5 * 60 * 60 sec) + +# Maximum time we'll wait for tests to emit output, in seconds. +# The purpose of this timeout is to recover from hangs. It should be longer +# than the amount of time any test takes to report results. +OUTPUT_TIMEOUT = 300 #five minutes (60 * 5 sec) + +def follow_file(filename): + """ + Generator that yields the latest unread content from the given + file, or None if no new content is available. + + For example: + + >>> f = open('temp.txt', 'w') + >>> f.write('hello') + >>> f.flush() + >>> tail = follow_file('temp.txt') + >>> tail.next() + 'hello' + >>> tail.next() is None + True + >>> f.write('there') + >>> f.flush() + >>> tail.next() + 'there' + >>> f.close() + >>> os.remove('temp.txt') + """ + + last_pos = 0 + last_size = 0 + while True: + newstuff = None + if os.path.exists(filename): + size = os.stat(filename).st_size + if size > last_size: + last_size = size + f = open(filename, 'r') + f.seek(last_pos) + newstuff = f.read() + last_pos = f.tell() + f.close() + yield newstuff + +# subprocess.check_output only appeared in python2.7, so this code is taken +# from python source code for compatibility with py2.5/2.6 +class CalledProcessError(Exception): + def __init__(self, returncode, cmd, output=None): + self.returncode = returncode + self.cmd = cmd + self.output = output + def __str__(self): + return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode) + +def check_output(*popenargs, **kwargs): + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise CalledProcessError(retcode, cmd, output=output) + return output + + +class FennecProfile(mozrunner.Profile): + preferences = {} + names = ['fennec'] + +FENNEC_REMOTE_PATH = '/mnt/sdcard/jetpack-profile' + +class RemoteFennecRunner(mozrunner.Runner): + profile_class = FennecProfile + + names = ['fennec'] + + _INTENT_PREFIX = 'org.mozilla.' + + _adb_path = None + + def __init__(self, binary=None, **kwargs): + # Check that we have a binary set + if not binary: + raise ValueError("You have to define `--binary` option set to the " + "path to your ADB executable.") + # Ensure that binary refer to a valid ADB executable + output = subprocess.Popen([binary], stdout=subprocess.PIPE, + stderr=subprocess.PIPE).communicate() + output = "".join(output) + if not ("Android Debug Bridge" in output): + raise ValueError("`--binary` option should be the path to your " + "ADB executable.") + self.binary = binary + + mobile_app_name = kwargs['cmdargs'][0] + self.profile = kwargs['profile'] + self._adb_path = binary + + # This pref has to be set to `false` otherwise, we do not receive + # output of adb commands! + subprocess.call([self._adb_path, "shell", + "setprop log.redirect-stdio false"]) + + # Android apps are launched by their "intent" name, + # Automatically detect already installed firefox by using `pm` program + # or use name given as cfx `--mobile-app` argument. + intents = self.getIntentNames() + if not intents: + raise ValueError("Unable to find any Firefox " + "application on your device.") + elif mobile_app_name: + if not mobile_app_name in intents: + raise ValueError("Unable to find Firefox application " + "with intent name '%s'\n" + "Available ones are: %s" % + (mobile_app_name, ", ".join(intents))) + self._intent_name = self._INTENT_PREFIX + mobile_app_name + else: + if "firefox" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox" + elif "firefox_beta" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox_beta" + elif "firefox_nightly" in intents: + self._intent_name = self._INTENT_PREFIX + "firefox_nightly" + else: + self._intent_name = self._INTENT_PREFIX + intents[0] + + print "Launching mobile application with intent name " + self._intent_name + + # First try to kill firefox if it is already running + pid = self.getProcessPID(self._intent_name) + if pid != None: + print "Killing running Firefox instance ..." + subprocess.call([self._adb_path, "shell", + "am force-stop " + self._intent_name]) + time.sleep(7) + # It appears recently that the PID still exists even after + # Fennec closes, so removing this error still allows the tests + # to pass as the new Fennec instance is able to start. + # Leaving error in but commented out for now. + # + #if self.getProcessPID(self._intent_name) != None: + # raise Exception("Unable to automatically kill running Firefox" + + # " instance. Please close it manually before " + + # "executing cfx.") + + print "Pushing the addon to your device" + + # Create a clean empty profile on the sd card + subprocess.call([self._adb_path, "shell", "rm -r " + FENNEC_REMOTE_PATH]) + subprocess.call([self._adb_path, "shell", "mkdir " + FENNEC_REMOTE_PATH]) + + # Push the profile folder created by mozrunner to the device + # (we can't simply use `adb push` as it doesn't copy empty folders) + localDir = self.profile.profile + remoteDir = FENNEC_REMOTE_PATH + for root, dirs, files in os.walk(localDir, followlinks='true'): + relRoot = os.path.relpath(root, localDir) + # Note about os.path usage below: + # Local files may be using Windows `\` separators but + # remote are always `/`, so we need to convert local ones to `/` + for file in files: + localFile = os.path.join(root, file) + remoteFile = remoteDir.replace("/", os.sep) + if relRoot != ".": + remoteFile = os.path.join(remoteFile, relRoot) + remoteFile = os.path.join(remoteFile, file) + remoteFile = "/".join(remoteFile.split(os.sep)) + subprocess.Popen([self._adb_path, "push", localFile, remoteFile], + stderr=subprocess.PIPE).wait() + for dir in dirs: + targetDir = remoteDir.replace("/", os.sep) + if relRoot != ".": + targetDir = os.path.join(targetDir, relRoot) + targetDir = os.path.join(targetDir, dir) + targetDir = "/".join(targetDir.split(os.sep)) + # `-p` option is not supported on all devices! + subprocess.call([self._adb_path, "shell", "mkdir " + targetDir]) + + @property + def command(self): + """Returns the command list to run.""" + return [self._adb_path, + "shell", + "am start " + + "-a android.activity.MAIN " + + "-n " + self._intent_name + "/" + self._intent_name + ".App " + + "--es args \"-profile " + FENNEC_REMOTE_PATH + "\"" + ] + + def start(self): + subprocess.call(self.command) + + def getProcessPID(self, processName): + p = subprocess.Popen([self._adb_path, "shell", "ps"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + line = p.stdout.readline() + while line: + columns = line.split() + pid = columns[1] + name = columns[-1] + line = p.stdout.readline() + if processName in name: + return pid + return None + + def getIntentNames(self): + p = subprocess.Popen([self._adb_path, "shell", "pm list packages"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + names = [] + for line in p.stdout.readlines(): + line = re.sub("(^package:)|\s", "", line) + if self._INTENT_PREFIX in line: + names.append(line.replace(self._INTENT_PREFIX, "")) + return names + + +class XulrunnerAppProfile(mozrunner.Profile): + preferences = {} + names = [] + +class XulrunnerAppRunner(mozrunner.Runner): + """ + Runner for any XULRunner app. Can use a Firefox binary in XULRunner + mode to execute the app, or can use XULRunner itself. Expects the + app's application.ini to be passed in as one of the items in + 'cmdargs' in the constructor. + + This class relies a lot on the particulars of mozrunner.Runner's + implementation, and does some unfortunate acrobatics to get around + some of the class' limitations/assumptions. + """ + + profile_class = XulrunnerAppProfile + + # This is a default, and will be overridden in the instance if + # Firefox is used in XULRunner mode. + names = ['xulrunner'] + + # Default location of XULRunner on OS X. + __DARWIN_PATH = "/Library/Frameworks/XUL.framework/xulrunner-bin" + __LINUX_PATH = "/usr/bin/xulrunner" + + # What our application.ini's path looks like if it's part of + # an "installed" XULRunner app on OS X. + __DARWIN_APP_INI_SUFFIX = '.app/Contents/Resources/application.ini' + + def __init__(self, binary=None, **kwargs): + if sys.platform == 'darwin' and binary and binary.endswith('.app'): + # Assume it's a Firefox app dir. + binary = os.path.join(binary, 'Contents/MacOS/firefox-bin') + + self.__app_ini = None + self.__real_binary = binary + + mozrunner.Runner.__init__(self, **kwargs) + + # See if we're using a genuine xulrunner-bin from the XULRunner SDK, + # or if we're being asked to use Firefox in XULRunner mode. + self.__is_xulrunner_sdk = 'xulrunner' in self.binary + + if sys.platform == 'linux2' and not self.env.get('LD_LIBRARY_PATH'): + self.env['LD_LIBRARY_PATH'] = os.path.dirname(self.binary) + + newargs = [] + for item in self.cmdargs: + if 'application.ini' in item: + self.__app_ini = item + else: + newargs.append(item) + self.cmdargs = newargs + + if not self.__app_ini: + raise ValueError('application.ini not found in cmdargs') + if not os.path.exists(self.__app_ini): + raise ValueError("file does not exist: '%s'" % self.__app_ini) + + if (sys.platform == 'darwin' and + self.binary == self.__DARWIN_PATH and + self.__app_ini.endswith(self.__DARWIN_APP_INI_SUFFIX)): + # If the application.ini is in an app bundle, then + # it could be inside an "installed" XULRunner app. + # If this is the case, use the app's actual + # binary instead of the XUL framework's, so we get + # a proper app icon, etc. + new_binary = '/'.join(self.__app_ini.split('/')[:-2] + + ['MacOS', 'xulrunner']) + if os.path.exists(new_binary): + self.binary = new_binary + + @property + def command(self): + """Returns the command list to run.""" + + if self.__is_xulrunner_sdk: + return [self.binary, self.__app_ini, '-profile', + self.profile.profile] + else: + return [self.binary, '-app', self.__app_ini, '-profile', + self.profile.profile] + + def __find_xulrunner_binary(self): + if sys.platform == 'darwin': + if os.path.exists(self.__DARWIN_PATH): + return self.__DARWIN_PATH + if sys.platform == 'linux2': + if os.path.exists(self.__LINUX_PATH): + return self.__LINUX_PATH + return None + + def find_binary(self): + # This gets called by the superclass constructor. It will + # always get called, even if a binary was passed into the + # constructor, because we want to have full control over + # what the exact setting of self.binary is. + + if not self.__real_binary: + self.__real_binary = self.__find_xulrunner_binary() + if not self.__real_binary: + dummy_profile = {} + runner = mozrunner.FirefoxRunner(profile=dummy_profile) + self.__real_binary = runner.find_binary() + self.names = runner.names + return self.__real_binary + +def set_overloaded_modules(env_root, app_type, addon_id, preferences, overloads): + # win32 file scheme needs 3 slashes + desktop_file_scheme = "file://" + if not env_root.startswith("/"): + desktop_file_scheme = desktop_file_scheme + "/" + + pref_prefix = "extensions.modules." + addon_id + ".path" + + # Set preferences that will map require prefix to a given path + for name, path in overloads.items(): + if len(name) == 0: + prefName = pref_prefix + else: + prefName = pref_prefix + "." + name + if app_type == "fennec-on-device": + # For testing on device, we have to copy overloaded files from fs + # to the device and use device path instead of local fs path. + # Actual copy of files if done after the call to Profile constructor + preferences[prefName] = "file://" + \ + FENNEC_REMOTE_PATH + "/overloads/" + name + else: + preferences[prefName] = desktop_file_scheme + \ + path.replace("\\", "/") + "/" + +def run_app(harness_root_dir, manifest_rdf, harness_options, + app_type, binary=None, profiledir=None, verbose=False, + parseable=False, enforce_timeouts=False, + logfile=None, addons=None, args=None, extra_environment={}, + norun=None, noquit=None, + used_files=None, enable_mobile=False, + mobile_app_name=None, + env_root=None, + is_running_tests=False, + overload_modules=False, + bundle_sdk=True, + pkgdir="", + enable_e10s=False, + no_connections=False): + if binary: + binary = os.path.expanduser(binary) + + if addons is None: + addons = [] + else: + addons = list(addons) + + cmdargs = [] + preferences = dict(DEFAULT_COMMON_PREFS) + + if is_running_tests: + preferences.update(DEFAULT_TEST_PREFS) + + if no_connections: + preferences.update(DEFAULT_NO_CONNECTIONS_PREFS) + + if enable_e10s: + preferences['browser.tabs.remote.autostart'] = True + else: + preferences['browser.tabs.remote.autostart'] = False + preferences['browser.tabs.remote.autostart.1'] = False + preferences['browser.tabs.remote.autostart.2'] = False + + # For now, only allow running on Mobile with --force-mobile argument + if app_type in ["fennec-on-device"] and not enable_mobile: + print """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return 0 + + if app_type == "fennec-on-device": + profile_class = FennecProfile + preferences.update(DEFAULT_FENNEC_PREFS) + runner_class = RemoteFennecRunner + # We pass the intent name through command arguments + cmdargs.append(mobile_app_name) + elif app_type == "xulrunner": + profile_class = XulrunnerAppProfile + runner_class = XulrunnerAppRunner + cmdargs.append(os.path.join(harness_root_dir, 'application.ini')) + elif app_type == "firefox": + profile_class = mozrunner.FirefoxProfile + preferences.update(DEFAULT_FIREFOX_PREFS) + runner_class = mozrunner.FirefoxRunner + elif app_type == "thunderbird": + profile_class = mozrunner.ThunderbirdProfile + preferences.update(DEFAULT_THUNDERBIRD_PREFS) + runner_class = mozrunner.ThunderbirdRunner + else: + raise ValueError("Unknown app: %s" % app_type) + if sys.platform == 'darwin' and app_type != 'xulrunner': + cmdargs.append('-foreground') + + if args: + cmdargs.extend(shlex.split(args)) + + # TODO: handle logs on remote device + if app_type != "fennec-on-device": + # tempfile.gettempdir() was constant, preventing two simultaneous "cfx + # run"/"cfx test" on the same host. On unix it points at /tmp (which is + # world-writeable), enabling a symlink attack (e.g. imagine some bad guy + # does 'ln -s ~/.ssh/id_rsa /tmp/harness_result'). NamedTemporaryFile + # gives us a unique filename that fixes both problems. We leave the + # (0-byte) file in place until the browser-side code starts writing to + # it, otherwise the symlink attack becomes possible again. + fileno,resultfile = tempfile.mkstemp(prefix="harness-result-") + os.close(fileno) + harness_options['resultFile'] = resultfile + + def maybe_remove_logfile(): + if os.path.exists(logfile): + os.remove(logfile) + + logfile_tail = None + + # We always buffer output through a logfile for two reasons: + # 1. On Windows, it's the only way to print console output to stdout/err. + # 2. It enables us to keep track of the last time output was emitted, + # so we can raise an exception if the test runner hangs. + if not logfile: + fileno,logfile = tempfile.mkstemp(prefix="harness-log-") + os.close(fileno) + logfile_tail = follow_file(logfile) + atexit.register(maybe_remove_logfile) + + logfile = os.path.abspath(os.path.expanduser(logfile)) + maybe_remove_logfile() + + env = {} + env.update(os.environ) + if no_connections: + env['MOZ_DISABLE_NONLOCAL_CONNECTIONS'] = '1' + env['MOZ_NO_REMOTE'] = '1' + env['XPCOM_DEBUG_BREAK'] = 'stack' + env.update(extra_environment) + if norun: + cmdargs.append("-no-remote") + + # Create the addon XPI so mozrunner will copy it to the profile it creates. + # We delete it below after getting mozrunner to create the profile. + from cuddlefish.xpi import build_xpi + xpi_path = tempfile.mktemp(suffix='cfx-tmp.xpi') + build_xpi(template_root_dir=harness_root_dir, + manifest=manifest_rdf, + xpi_path=xpi_path, + harness_options=harness_options, + limit_to=used_files, + bundle_sdk=bundle_sdk, + pkgdir=pkgdir) + addons.append(xpi_path) + + starttime = last_output_time = time.time() + + # Redirect runner output to a file so we can catch output not generated + # by us. + # In theory, we could do this using simple redirection on all platforms + # other than Windows, but this way we only have a single codepath to + # maintain. + fileno,outfile = tempfile.mkstemp(prefix="harness-stdout-") + os.close(fileno) + outfile_tail = follow_file(outfile) + def maybe_remove_outfile(): + if os.path.exists(outfile): + try: + os.remove(outfile) + except Exception, e: + print "Error Cleaning up: " + str(e) + atexit.register(maybe_remove_outfile) + outf = open(outfile, "w") + popen_kwargs = { 'stdout': outf, 'stderr': outf} + + profile = None + + if app_type == "fennec-on-device": + # Install a special addon when we run firefox on mobile device + # in order to be able to kill it + mydir = os.path.dirname(os.path.abspath(__file__)) + addon_dir = os.path.join(mydir, "mobile-utils") + addons.append(addon_dir) + + # Overload addon-specific commonjs modules path with lib/ folder + overloads = dict() + if overload_modules: + overloads[""] = os.path.join(env_root, "lib") + + # Overload tests/ mapping with test/ folder, only when running test + if is_running_tests: + overloads["tests"] = os.path.join(env_root, "test") + + set_overloaded_modules(env_root, app_type, harness_options["jetpackID"], \ + preferences, overloads) + + # the XPI file is copied into the profile here + profile = profile_class(addons=addons, + profile=profiledir, + preferences=preferences) + + # Delete the temporary xpi file + os.remove(xpi_path) + + # Copy overloaded files registered in set_overloaded_modules + # For testing on device, we have to copy overloaded files from fs + # to the device and use device path instead of local fs path. + # (has to be done after the call to profile_class() which eventualy creates + # profile folder) + if app_type == "fennec-on-device": + profile_path = profile.profile + for name, path in overloads.items(): + shutil.copytree(path, \ + os.path.join(profile_path, "overloads", name)) + + runner = runner_class(profile=profile, + binary=binary, + env=env, + cmdargs=cmdargs, + kp_kwargs=popen_kwargs) + + sys.stdout.flush(); sys.stderr.flush() + + if app_type == "fennec-on-device": + if not enable_mobile: + print >>sys.stderr, """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return 0 + + # In case of mobile device, we need to get stdio from `adb logcat` cmd: + + # First flush logs in order to avoid catching previous ones + subprocess.call([binary, "logcat", "-c"]) + + # Launch adb command + runner.start() + + # We can immediatly remove temporary profile folder + # as it has been uploaded to the device + profile.cleanup() + # We are not going to use the output log file + outf.close() + + # Then we simply display stdout of `adb logcat` + p = subprocess.Popen([binary, "logcat", "stderr:V stdout:V GeckoConsole:V *:S"], stdout=subprocess.PIPE) + while True: + line = p.stdout.readline() + if line == '': + break + # mobile-utils addon contains an application quit event observer + # that will print this string: + if "APPLICATION-QUIT" in line: + break + + if verbose: + # if --verbose is given, we display everything: + # All JS Console messages, stdout and stderr. + m = CLEANUP_ADB.match(line) + if not m: + print line.rstrip() + continue + print m.group(3) + else: + # Otherwise, display addons messages dispatched through + # console.[info, log, debug, warning, error](msg) + m = FILTER_ONLY_CONSOLE_FROM_ADB.match(line) + if m: + print m.group(2) + + print >>sys.stderr, "Program terminated successfully." + return 0 + + + print >>sys.stderr, "Using binary at '%s'." % runner.binary + + # Ensure cfx is being used with Firefox 4.0+. + # TODO: instead of dying when Firefox is < 4, warn when Firefox is outside + # the minVersion/maxVersion boundaries. + version_output = check_output(runner.command + ["-v"]) + # Note: this regex doesn't handle all valid versions in the Toolkit Version + # Format <https://developer.mozilla.org/en/Toolkit_version_format>, just the + # common subset that we expect Mozilla apps to use. + mo = re.search(r"Mozilla (Firefox|Iceweasel|Fennec)\b[^ ]* ((\d+)\.\S*)", + version_output) + if not mo: + # cfx may be used with Thunderbird, SeaMonkey or an exotic Firefox + # version. + print """ + WARNING: cannot determine Firefox version; please ensure you are running + a Mozilla application equivalent to Firefox 4.0 or greater. + """ + elif mo.group(1) == "Fennec": + # For now, only allow running on Mobile with --force-mobile argument + if not enable_mobile: + print """ + WARNING: Firefox Mobile support is still experimental. + If you would like to run an addon on this platform, use --force-mobile flag: + + cfx --force-mobile""" + return + else: + version = mo.group(3) + if int(version) < 4: + print """ + cfx requires Firefox 4 or greater and is unable to find a compatible + binary. Please install a newer version of Firefox or provide the path to + your existing compatible version with the --binary flag: + + cfx --binary=PATH_TO_FIREFOX_BINARY""" + return + + # Set the appropriate extensions.checkCompatibility preference to false, + # so the tests run even if the SDK is not marked as compatible with the + # version of Firefox on which they are running, and we don't have to + # ensure we update the maxVersion before the version of Firefox changes + # every six weeks. + # + # The regex we use here is effectively the same as BRANCH_REGEX from + # /toolkit/mozapps/extensions/content/extensions.js, which toolkit apps + # use to determine whether or not to load an incompatible addon. + # + br = re.search(r"^([^\.]+\.[0-9]+[a-z]*).*", mo.group(2), re.I) + if br: + prefname = 'extensions.checkCompatibility.' + br.group(1) + profile.preferences[prefname] = False + # Calling profile.set_preferences here duplicates the list of prefs + # in prefs.js, since the profile calls self.set_preferences in its + # constructor, but that is ok, because it doesn't change the set of + # preferences that are ultimately registered in Firefox. + profile.set_preferences(profile.preferences) + + print >>sys.stderr, "Using profile at '%s'." % profile.profile + sys.stderr.flush() + + if norun: + print "To launch the application, enter the following command:" + print " ".join(runner.command) + " " + (" ".join(runner.cmdargs)) + return 0 + + runner.start() + + done = False + result = None + test_name = "Jetpack startup" + + def Timeout(message, test_name, parseable): + if parseable: + sys.stderr.write("TEST-UNEXPECTED-FAIL | %s | %s\n" % (test_name, message)) + sys.stderr.flush() + return Exception(message) + + try: + while not done: + time.sleep(0.05) + for tail in (logfile_tail, outfile_tail): + if tail: + new_chars = tail.next() + if new_chars: + last_output_time = time.time() + sys.stderr.write(new_chars) + sys.stderr.flush() + if is_running_tests and parseable: + match = PARSEABLE_TEST_NAME.search(new_chars) + if match: + test_name = match.group(1) + if os.path.exists(resultfile): + result = open(resultfile).read() + if result: + if result in ['OK', 'FAIL']: + done = True + else: + sys.stderr.write("Hrm, resultfile (%s) contained something weird (%d bytes)\n" % (resultfile, len(result))) + sys.stderr.write("'"+result+"'\n") + if enforce_timeouts: + if time.time() - last_output_time > OUTPUT_TIMEOUT: + raise Timeout("Test output exceeded timeout (%ds)." % + OUTPUT_TIMEOUT, test_name, parseable) + if time.time() - starttime > RUN_TIMEOUT: + raise Timeout("Test run exceeded timeout (%ds)." % + RUN_TIMEOUT, test_name, parseable) + except: + if not noquit: + runner.stop() + raise + else: + runner.wait(10) + # double kill - hack for bugs 942111, 1006043.. + try: + runner.stop() + except: + pass + finally: + outf.close() + if profile: + profile.cleanup() + + print >>sys.stderr, "Total time: %f seconds" % (time.time() - starttime) + + if result == 'OK': + print >>sys.stderr, "Program terminated successfully." + return 0 + else: + print >>sys.stderr, "Program terminated unsuccessfully." + return -1 |