Index: third_party/mozrunner/mozrunner/runner.py |
=================================================================== |
--- third_party/mozrunner/mozrunner/runner.py (revision 0) |
+++ third_party/mozrunner/mozrunner/runner.py (revision 0) |
@@ -0,0 +1,388 @@ |
+#!/usr/bin/env python |
+ |
+# 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/. |
+ |
+__all__ = ['Runner', 'ThunderbirdRunner', 'FirefoxRunner', 'runners', 'CLI', 'cli', 'package_metadata'] |
+ |
+import mozinfo |
+import optparse |
+import os |
+import platform |
+import subprocess |
+import sys |
+import ConfigParser |
+ |
+from utils import get_metadata_from_egg |
+from utils import findInPath |
+from mozprofile import * |
+from mozprocess.processhandler import ProcessHandler |
+ |
+if mozinfo.isMac: |
+ from plistlib import readPlist |
+ |
+package_metadata = get_metadata_from_egg('mozrunner') |
+ |
+# Map of debugging programs to information about them |
+# from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59 |
+debuggers = {'gdb': {'interactive': True, |
+ 'args': ['-q', '--args'],}, |
+ 'valgrind': {'interactive': False, |
+ 'args': ['--leak-check=full']} |
+ } |
+ |
+def debugger_arguments(debugger, arguments=None, interactive=None): |
+ """ |
+ finds debugger arguments from debugger given and defaults |
+ * debugger : debugger name or path to debugger |
+ * arguments : arguments to the debugger, or None to use defaults |
+ * interactive : whether the debugger should be run in interactive mode, or None to use default |
+ """ |
+ |
+ # find debugger executable if not a file |
+ executable = debugger |
+ if not os.path.exists(executable): |
+ executable = findInPath(debugger) |
+ if executable is None: |
+ raise Exception("Path to '%s' not found" % debugger) |
+ |
+ # if debugger not in dictionary of knowns return defaults |
+ dirname, debugger = os.path.split(debugger) |
+ if debugger not in debuggers: |
+ return ([executable] + (arguments or []), bool(interactive)) |
+ |
+ # otherwise use the dictionary values for arguments unless specified |
+ if arguments is None: |
+ arguments = debuggers[debugger].get('args', []) |
+ if interactive is None: |
+ interactive = debuggers[debugger].get('interactive', False) |
+ return ([executable] + arguments, interactive) |
+ |
+class Runner(object): |
+ """Handles all running operations. Finds bins, runs and kills the process.""" |
+ |
+ profile_class = Profile # profile class to use by default |
+ |
+ @classmethod |
+ def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None, |
+ clean_profile=True, process_class=ProcessHandler): |
+ profile = cls.profile_class(**(profile_args or {})) |
+ return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs, |
+ clean_profile=clean_profile, process_class=process_class) |
+ |
+ def __init__(self, profile, binary, cmdargs=None, env=None, |
+ kp_kwargs=None, clean_profile=True, process_class=ProcessHandler): |
+ self.process_handler = None |
+ self.process_class = process_class |
+ self.profile = profile |
+ self.clean_profile = clean_profile |
+ |
+ # find the binary |
+ self.binary = binary |
+ if not self.binary: |
+ raise Exception("Binary not specified") |
+ if not os.path.exists(self.binary): |
+ raise OSError("Binary path does not exist: %s" % self.binary) |
+ |
+ # allow Mac binaries to be specified as an app bundle |
+ plist = '%s/Contents/Info.plist' % self.binary |
+ if mozinfo.isMac and os.path.exists(plist): |
+ info = readPlist(plist) |
+ self.binary = os.path.join(self.binary, "Contents/MacOS/", |
+ info['CFBundleExecutable']) |
+ |
+ self.cmdargs = cmdargs or [] |
+ _cmdargs = [i for i in self.cmdargs |
+ if i != '-foreground'] |
+ if len(_cmdargs) != len(self.cmdargs): |
+ # foreground should be last; see |
+ # - https://bugzilla.mozilla.org/show_bug.cgi?id=625614 |
+ # - https://bugzilla.mozilla.org/show_bug.cgi?id=626826 |
+ self.cmdargs = _cmdargs |
+ self.cmdargs.append('-foreground') |
+ |
+ # process environment |
+ if env is None: |
+ self.env = os.environ.copy() |
+ else: |
+ self.env = env.copy() |
+ # allows you to run an instance of Firefox separately from any other instances |
+ self.env['MOZ_NO_REMOTE'] = '1' |
+ # keeps Firefox attached to the terminal window after it starts |
+ self.env['NO_EM_RESTART'] = '1' |
+ |
+ # set the library path if needed on linux |
+ if sys.platform == 'linux2' and self.binary.endswith('-bin'): |
+ dirname = os.path.dirname(self.binary) |
+ if os.environ.get('LD_LIBRARY_PATH', None): |
+ self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname) |
+ else: |
+ self.env['LD_LIBRARY_PATH'] = dirname |
+ |
+ # arguments for ProfessHandler.Process |
+ self.kp_kwargs = kp_kwargs or {} |
+ |
+ @property |
+ def command(self): |
+ """Returns the command list to run.""" |
+ return [self.binary, '-profile', self.profile.profile] |
+ |
+ def get_repositoryInfo(self): |
+ """Read repository information from application.ini and platform.ini.""" |
+ |
+ config = ConfigParser.RawConfigParser() |
+ dirname = os.path.dirname(self.binary) |
+ repository = { } |
+ |
+ for file, section in [('application', 'App'), ('platform', 'Build')]: |
+ config.read(os.path.join(dirname, '%s.ini' % file)) |
+ |
+ for key, id in [('SourceRepository', 'repository'), |
+ ('SourceStamp', 'changeset')]: |
+ try: |
+ repository['%s_%s' % (file, id)] = config.get(section, key); |
+ except: |
+ repository['%s_%s' % (file, id)] = None |
+ |
+ return repository |
+ |
+ def is_running(self): |
+ return self.process_handler is not None |
+ |
+ def start(self, debug_args=None, interactive=False, timeout=None, outputTimeout=None): |
+ """ |
+ Run self.command in the proper environment. |
+ - debug_args: arguments for the debugger |
+ - interactive: uses subprocess.Popen directly |
+ - read_output: sends program output to stdout [default=False] |
+ - timeout: see process_handler.waitForFinish |
+ - outputTimeout: see process_handler.waitForFinish |
+ """ |
+ |
+ # ensure you are stopped |
+ self.stop() |
+ |
+ # ensure the profile exists |
+ if not self.profile.exists(): |
+ self.profile.reset() |
+ assert self.profile.exists(), "%s : failure to reset profile" % self.__class__.__name__ |
+ |
+ cmd = self._wrap_command(self.command+self.cmdargs) |
+ |
+ # attach a debugger, if specified |
+ if debug_args: |
+ cmd = list(debug_args) + cmd |
+ |
+ if interactive: |
+ self.process_handler = subprocess.Popen(cmd, env=self.env) |
+ # TODO: other arguments |
+ else: |
+ # this run uses the managed processhandler |
+ self.process_handler = self.process_class(cmd, env=self.env, **self.kp_kwargs) |
+ self.process_handler.run(timeout, outputTimeout) |
+ |
+ def wait(self, timeout=None): |
+ """ |
+ Wait for the app to exit. |
+ |
+ If timeout is not None, will return after timeout seconds. |
+ Use is_running() to determine whether or not a timeout occured. |
+ Timeout is ignored if interactive was set to True. |
+ """ |
+ if self.process_handler is None: |
+ return |
+ |
+ if isinstance(self.process_handler, subprocess.Popen): |
+ self.process_handler.wait() |
+ else: |
+ self.process_handler.wait(timeout) |
+ if self.process_handler.proc.poll() is None: |
+ # waitForFinish timed out |
+ return |
+ |
+ self.process_handler = None |
+ |
+ def stop(self): |
+ """Kill the app""" |
+ if self.process_handler is None: |
+ return |
+ self.process_handler.kill() |
+ self.process_handler = None |
+ |
+ def reset(self): |
+ """ |
+ reset the runner between runs |
+ currently, only resets the profile, but probably should do more |
+ """ |
+ self.profile.reset() |
+ |
+ def cleanup(self): |
+ self.stop() |
+ if self.clean_profile: |
+ self.profile.cleanup() |
+ |
+ def _wrap_command(self, cmd): |
+ """ |
+ If running on OS X 10.5 or older, wrap |cmd| so that it will |
+ be executed as an i386 binary, in case it's a 32-bit/64-bit universal |
+ binary. |
+ """ |
+ if mozinfo.isMac and hasattr(platform, 'mac_ver') and \ |
+ platform.mac_ver()[0][:4] < '10.6': |
+ return ["arch", "-arch", "i386"] + cmd |
+ return cmd |
+ |
+ __del__ = cleanup |
+ |
+ |
+class FirefoxRunner(Runner): |
+ """Specialized Runner subclass for running Firefox.""" |
+ |
+ profile_class = FirefoxProfile |
+ |
+ def __init__(self, profile, binary=None, **kwargs): |
+ |
+ # take the binary from BROWSER_PATH environment variable |
+ if (not binary) and 'BROWSER_PATH' in os.environ: |
+ binary = os.environ['BROWSER_PATH'] |
+ |
+ Runner.__init__(self, profile, binary, **kwargs) |
+ |
+class ThunderbirdRunner(Runner): |
+ """Specialized Runner subclass for running Thunderbird""" |
+ profile_class = ThunderbirdProfile |
+ |
+runners = {'firefox': FirefoxRunner, |
+ 'thunderbird': ThunderbirdRunner} |
+ |
+class CLI(MozProfileCLI): |
+ """Command line interface.""" |
+ |
+ module = "mozrunner" |
+ |
+ def __init__(self, args=sys.argv[1:]): |
+ """ |
+ Setup command line parser and parse arguments |
+ - args : command line arguments |
+ """ |
+ |
+ self.metadata = getattr(sys.modules[self.module], |
+ 'package_metadata', |
+ {}) |
+ version = self.metadata.get('Version') |
+ parser_args = {'description': self.metadata.get('Summary')} |
+ if version: |
+ parser_args['version'] = "%prog " + version |
+ self.parser = optparse.OptionParser(**parser_args) |
+ self.add_options(self.parser) |
+ (self.options, self.args) = self.parser.parse_args(args) |
+ |
+ if getattr(self.options, 'info', None): |
+ self.print_metadata() |
+ sys.exit(0) |
+ |
+ # choose appropriate runner and profile classes |
+ try: |
+ self.runner_class = runners[self.options.app] |
+ except KeyError: |
+ self.parser.error('Application "%s" unknown (should be one of "firefox" or "thunderbird")' % self.options.app) |
+ |
+ def add_options(self, parser): |
+ """add options to the parser""" |
+ |
+ # add profile options |
+ MozProfileCLI.add_options(self, parser) |
+ |
+ # add runner options |
+ parser.add_option('-b', "--binary", |
+ dest="binary", help="Binary path.", |
+ metavar=None, default=None) |
+ parser.add_option('--app', dest='app', default='firefox', |
+ help="Application to use [DEFAULT: %default]") |
+ parser.add_option('--app-arg', dest='appArgs', |
+ default=[], action='append', |
+ help="provides an argument to the test application") |
+ parser.add_option('--debugger', dest='debugger', |
+ help="run under a debugger, e.g. gdb or valgrind") |
+ parser.add_option('--debugger-args', dest='debugger_args', |
+ action='append', default=None, |
+ help="arguments to the debugger") |
+ parser.add_option('--interactive', dest='interactive', |
+ action='store_true', |
+ help="run the program interactively") |
+ if self.metadata: |
+ parser.add_option("--info", dest="info", default=False, |
+ action="store_true", |
+ help="Print module information") |
+ |
+ ### methods for introspecting data |
+ |
+ def get_metadata_from_egg(self): |
+ import pkg_resources |
+ ret = {} |
+ dist = pkg_resources.get_distribution(self.module) |
+ if dist.has_metadata("PKG-INFO"): |
+ for line in dist.get_metadata_lines("PKG-INFO"): |
+ key, value = line.split(':', 1) |
+ ret[key] = value |
+ if dist.has_metadata("requires.txt"): |
+ ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt") |
+ return ret |
+ |
+ def print_metadata(self, data=("Name", "Version", "Summary", "Home-page", |
+ "Author", "Author-email", "License", "Platform", "Dependencies")): |
+ for key in data: |
+ if key in self.metadata: |
+ print key + ": " + self.metadata[key] |
+ |
+ ### methods for running |
+ |
+ def command_args(self): |
+ """additional arguments for the mozilla application""" |
+ return self.options.appArgs |
+ |
+ def runner_args(self): |
+ """arguments to instantiate the runner class""" |
+ return dict(cmdargs=self.command_args(), |
+ binary=self.options.binary, |
+ profile_args=self.profile_args()) |
+ |
+ def create_runner(self): |
+ return self.runner_class.create(**self.runner_args()) |
+ |
+ def run(self): |
+ runner = self.create_runner() |
+ self.start(runner) |
+ runner.cleanup() |
+ |
+ def debugger_arguments(self): |
+ """ |
+ returns a 2-tuple of debugger arguments: |
+ (debugger_arguments, interactive) |
+ """ |
+ debug_args = self.options.debugger_args |
+ interactive = self.options.interactive |
+ if self.options.debugger: |
+ debug_args, interactive = debugger_arguments(self.options.debugger) |
+ return debug_args, interactive |
+ |
+ def start(self, runner): |
+ """Starts the runner and waits for Firefox to exit or Keyboard Interrupt. |
+ Shoule be overwritten to provide custom running of the runner instance.""" |
+ |
+ # attach a debugger if specified |
+ debug_args, interactive = self.debugger_arguments() |
+ runner.start(debug_args=debug_args, interactive=interactive) |
+ print 'Starting:', ' '.join(runner.command) |
+ try: |
+ runner.wait() |
+ except KeyboardInterrupt: |
+ runner.stop() |
+ |
+ |
+def cli(args=sys.argv[1:]): |
+ CLI(args).run() |
+ |
+if __name__ == '__main__': |
+ cli() |
Property changes on: third_party/mozrunner/mozrunner/runner.py |
___________________________________________________________________ |
Added: svn:eol-style |
+ LF |
Added: svn:executable |
+ * |