testing/mozbase/mozrunner/mozrunner/local.py

Wed, 31 Dec 2014 06:55:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:50 +0100
changeset 2
7e26c7da4463
permissions
-rw-r--r--

Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2

michael@0 1 #!/usr/bin/env python
michael@0 2
michael@0 3 # This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 # You can obtain one at http://mozilla.org/MPL/2.0/.
michael@0 6
michael@0 7 import ConfigParser
michael@0 8 import mozinfo
michael@0 9 import optparse
michael@0 10 import os
michael@0 11 import platform
michael@0 12 import subprocess
michael@0 13 import sys
michael@0 14
michael@0 15 if mozinfo.isMac:
michael@0 16 from plistlib import readPlist
michael@0 17
michael@0 18 from mozprofile import Profile, FirefoxProfile, MetroFirefoxProfile, ThunderbirdProfile, MozProfileCLI
michael@0 19
michael@0 20 from .base import Runner
michael@0 21 from .utils import findInPath, get_metadata_from_egg
michael@0 22
michael@0 23
michael@0 24 __all__ = ['CLI',
michael@0 25 'cli',
michael@0 26 'LocalRunner',
michael@0 27 'local_runners',
michael@0 28 'package_metadata',
michael@0 29 'FirefoxRunner',
michael@0 30 'MetroFirefoxRunner',
michael@0 31 'ThunderbirdRunner']
michael@0 32
michael@0 33
michael@0 34 package_metadata = get_metadata_from_egg('mozrunner')
michael@0 35
michael@0 36
michael@0 37 # Map of debugging programs to information about them
michael@0 38 # from http://mxr.mozilla.org/mozilla-central/source/build/automationutils.py#59
michael@0 39 debuggers = {'gdb': {'interactive': True,
michael@0 40 'args': ['-q', '--args'],},
michael@0 41 'valgrind': {'interactive': False,
michael@0 42 'args': ['--leak-check=full']}
michael@0 43 }
michael@0 44
michael@0 45
michael@0 46 def debugger_arguments(debugger, arguments=None, interactive=None):
michael@0 47 """Finds debugger arguments from debugger given and defaults
michael@0 48
michael@0 49 :param debugger: name or path to debugger
michael@0 50 :param arguments: arguments for the debugger, or None to use defaults
michael@0 51 :param interactive: whether the debugger should run in interactive mode
michael@0 52
michael@0 53 """
michael@0 54 # find debugger executable if not a file
michael@0 55 executable = debugger
michael@0 56 if not os.path.exists(executable):
michael@0 57 executable = findInPath(debugger)
michael@0 58 if executable is None:
michael@0 59 raise Exception("Path to '%s' not found" % debugger)
michael@0 60
michael@0 61 # if debugger not in dictionary of knowns return defaults
michael@0 62 dirname, debugger = os.path.split(debugger)
michael@0 63 if debugger not in debuggers:
michael@0 64 return ([executable] + (arguments or []), bool(interactive))
michael@0 65
michael@0 66 # otherwise use the dictionary values for arguments unless specified
michael@0 67 if arguments is None:
michael@0 68 arguments = debuggers[debugger].get('args', [])
michael@0 69 if interactive is None:
michael@0 70 interactive = debuggers[debugger].get('interactive', False)
michael@0 71 return ([executable] + arguments, interactive)
michael@0 72
michael@0 73
michael@0 74 class LocalRunner(Runner):
michael@0 75 """Handles all running operations. Finds bins, runs and kills the process"""
michael@0 76
michael@0 77 profile_class = Profile # profile class to use by default
michael@0 78
michael@0 79 @classmethod
michael@0 80 def create(cls, binary=None, cmdargs=None, env=None, kp_kwargs=None, profile_args=None,
michael@0 81 clean_profile=True, process_class=None, **kwargs):
michael@0 82 profile = cls.profile_class(**(profile_args or {}))
michael@0 83 return cls(profile, binary=binary, cmdargs=cmdargs, env=env, kp_kwargs=kp_kwargs,
michael@0 84 clean_profile=clean_profile, process_class=process_class, **kwargs)
michael@0 85
michael@0 86 def __init__(self, profile, binary, cmdargs=None, env=None,
michael@0 87 kp_kwargs=None, clean_profile=None, process_class=None, **kwargs):
michael@0 88
michael@0 89 Runner.__init__(self, profile, clean_profile=clean_profile, kp_kwargs=kp_kwargs,
michael@0 90 process_class=process_class, env=env, **kwargs)
michael@0 91
michael@0 92 # find the binary
michael@0 93 self.binary = binary
michael@0 94 if not self.binary:
michael@0 95 raise Exception("Binary not specified")
michael@0 96 if not os.path.exists(self.binary):
michael@0 97 raise OSError("Binary path does not exist: %s" % self.binary)
michael@0 98
michael@0 99 # To be safe the absolute path of the binary should be used
michael@0 100 self.binary = os.path.abspath(self.binary)
michael@0 101
michael@0 102 # allow Mac binaries to be specified as an app bundle
michael@0 103 plist = '%s/Contents/Info.plist' % self.binary
michael@0 104 if mozinfo.isMac and os.path.exists(plist):
michael@0 105 info = readPlist(plist)
michael@0 106 self.binary = os.path.join(self.binary, "Contents/MacOS/",
michael@0 107 info['CFBundleExecutable'])
michael@0 108
michael@0 109 self.cmdargs = cmdargs or []
michael@0 110 _cmdargs = [i for i in self.cmdargs
michael@0 111 if i != '-foreground']
michael@0 112 if len(_cmdargs) != len(self.cmdargs):
michael@0 113 # foreground should be last; see
michael@0 114 # https://bugzilla.mozilla.org/show_bug.cgi?id=625614
michael@0 115 self.cmdargs = _cmdargs
michael@0 116 self.cmdargs.append('-foreground')
michael@0 117 if mozinfo.isMac and '-foreground' not in self.cmdargs:
michael@0 118 # runner should specify '-foreground' on Mac; see
michael@0 119 # https://bugzilla.mozilla.org/show_bug.cgi?id=916512
michael@0 120 self.cmdargs.append('-foreground')
michael@0 121
michael@0 122 # process environment
michael@0 123 if env is None:
michael@0 124 self.env = os.environ.copy()
michael@0 125 else:
michael@0 126 self.env = env.copy()
michael@0 127 # allows you to run an instance of Firefox separately from any other instances
michael@0 128 self.env['MOZ_NO_REMOTE'] = '1'
michael@0 129 # keeps Firefox attached to the terminal window after it starts
michael@0 130 self.env['NO_EM_RESTART'] = '1'
michael@0 131
michael@0 132 # set the library path if needed on linux
michael@0 133 if sys.platform == 'linux2' and self.binary.endswith('-bin'):
michael@0 134 dirname = os.path.dirname(self.binary)
michael@0 135 if os.environ.get('LD_LIBRARY_PATH', None):
michael@0 136 self.env['LD_LIBRARY_PATH'] = '%s:%s' % (os.environ['LD_LIBRARY_PATH'], dirname)
michael@0 137 else:
michael@0 138 self.env['LD_LIBRARY_PATH'] = dirname
michael@0 139
michael@0 140 @property
michael@0 141 def command(self):
michael@0 142 """Returns the command list to run"""
michael@0 143 commands = [self.binary, '-profile', self.profile.profile]
michael@0 144
michael@0 145 # Bug 775416 - Ensure that binary options are passed in first
michael@0 146 commands[1:1] = self.cmdargs
michael@0 147
michael@0 148 return commands
michael@0 149
michael@0 150 def get_repositoryInfo(self):
michael@0 151 """Read repository information from application.ini and platform.ini"""
michael@0 152 config = ConfigParser.RawConfigParser()
michael@0 153 dirname = os.path.dirname(self.binary)
michael@0 154 repository = { }
michael@0 155
michael@0 156 for file, section in [('application', 'App'), ('platform', 'Build')]:
michael@0 157 config.read(os.path.join(dirname, '%s.ini' % file))
michael@0 158
michael@0 159 for key, id in [('SourceRepository', 'repository'),
michael@0 160 ('SourceStamp', 'changeset')]:
michael@0 161 try:
michael@0 162 repository['%s_%s' % (file, id)] = config.get(section, key);
michael@0 163 except:
michael@0 164 repository['%s_%s' % (file, id)] = None
michael@0 165
michael@0 166 return repository
michael@0 167
michael@0 168
michael@0 169 class FirefoxRunner(LocalRunner):
michael@0 170 """Specialized LocalRunner subclass for running Firefox."""
michael@0 171
michael@0 172 profile_class = FirefoxProfile
michael@0 173
michael@0 174 def __init__(self, profile, binary=None, **kwargs):
michael@0 175
michael@0 176 # if no binary given take it from the BROWSER_PATH environment variable
michael@0 177 binary = binary or os.environ.get('BROWSER_PATH')
michael@0 178 LocalRunner.__init__(self, profile, binary, **kwargs)
michael@0 179
michael@0 180
michael@0 181 class MetroFirefoxRunner(LocalRunner):
michael@0 182 """Specialized LocalRunner subclass for running Firefox Metro"""
michael@0 183
michael@0 184 profile_class = MetroFirefoxProfile
michael@0 185
michael@0 186 # helper application to launch Firefox in Metro mode
michael@0 187 here = os.path.dirname(os.path.abspath(__file__))
michael@0 188 immersiveHelperPath = os.path.sep.join([here,
michael@0 189 'resources',
michael@0 190 'metrotestharness.exe'])
michael@0 191
michael@0 192 def __init__(self, profile, binary=None, **kwargs):
michael@0 193
michael@0 194 # if no binary given take it from the BROWSER_PATH environment variable
michael@0 195 binary = binary or os.environ.get('BROWSER_PATH')
michael@0 196 LocalRunner.__init__(self, profile, binary, **kwargs)
michael@0 197
michael@0 198 if not os.path.exists(self.immersiveHelperPath):
michael@0 199 raise OSError('Can not find Metro launcher: %s' % self.immersiveHelperPath)
michael@0 200
michael@0 201 if not mozinfo.isWin:
michael@0 202 raise Exception('Firefox Metro mode is only supported on Windows 8 and onwards')
michael@0 203
michael@0 204 @property
michael@0 205 def command(self):
michael@0 206 command = LocalRunner.command.fget(self)
michael@0 207 command[:0] = [self.immersiveHelperPath, '-firefoxpath']
michael@0 208
michael@0 209 return command
michael@0 210
michael@0 211
michael@0 212 class ThunderbirdRunner(LocalRunner):
michael@0 213 """Specialized LocalRunner subclass for running Thunderbird"""
michael@0 214 profile_class = ThunderbirdProfile
michael@0 215
michael@0 216
michael@0 217 local_runners = {'firefox': FirefoxRunner,
michael@0 218 'metrofirefox' : MetroFirefoxRunner,
michael@0 219 'thunderbird': ThunderbirdRunner}
michael@0 220
michael@0 221
michael@0 222 class CLI(MozProfileCLI):
michael@0 223 """Command line interface"""
michael@0 224
michael@0 225 module = "mozrunner"
michael@0 226
michael@0 227 def __init__(self, args=sys.argv[1:]):
michael@0 228 self.metadata = getattr(sys.modules[self.module],
michael@0 229 'package_metadata',
michael@0 230 {})
michael@0 231 version = self.metadata.get('Version')
michael@0 232 parser_args = {'description': self.metadata.get('Summary')}
michael@0 233 if version:
michael@0 234 parser_args['version'] = "%prog " + version
michael@0 235 self.parser = optparse.OptionParser(**parser_args)
michael@0 236 self.add_options(self.parser)
michael@0 237 (self.options, self.args) = self.parser.parse_args(args)
michael@0 238
michael@0 239 if getattr(self.options, 'info', None):
michael@0 240 self.print_metadata()
michael@0 241 sys.exit(0)
michael@0 242
michael@0 243 # choose appropriate runner and profile classes
michael@0 244 try:
michael@0 245 self.runner_class = local_runners[self.options.app]
michael@0 246 except KeyError:
michael@0 247 self.parser.error('Application "%s" unknown (should be one of "%s")' %
michael@0 248 (self.options.app, ', '.join(local_runners.keys())))
michael@0 249
michael@0 250 def add_options(self, parser):
michael@0 251 """add options to the parser"""
michael@0 252
michael@0 253 # add profile options
michael@0 254 MozProfileCLI.add_options(self, parser)
michael@0 255
michael@0 256 # add runner options
michael@0 257 parser.add_option('-b', "--binary",
michael@0 258 dest="binary", help="Binary path.",
michael@0 259 metavar=None, default=None)
michael@0 260 parser.add_option('--app', dest='app', default='firefox',
michael@0 261 help="Application to use [DEFAULT: %default]")
michael@0 262 parser.add_option('--app-arg', dest='appArgs',
michael@0 263 default=[], action='append',
michael@0 264 help="provides an argument to the test application")
michael@0 265 parser.add_option('--debugger', dest='debugger',
michael@0 266 help="run under a debugger, e.g. gdb or valgrind")
michael@0 267 parser.add_option('--debugger-args', dest='debugger_args',
michael@0 268 action='store',
michael@0 269 help="arguments to the debugger")
michael@0 270 parser.add_option('--interactive', dest='interactive',
michael@0 271 action='store_true',
michael@0 272 help="run the program interactively")
michael@0 273 if self.metadata:
michael@0 274 parser.add_option("--info", dest="info", default=False,
michael@0 275 action="store_true",
michael@0 276 help="Print module information")
michael@0 277
michael@0 278 ### methods for introspecting data
michael@0 279
michael@0 280 def get_metadata_from_egg(self):
michael@0 281 import pkg_resources
michael@0 282 ret = {}
michael@0 283 dist = pkg_resources.get_distribution(self.module)
michael@0 284 if dist.has_metadata("PKG-INFO"):
michael@0 285 for line in dist.get_metadata_lines("PKG-INFO"):
michael@0 286 key, value = line.split(':', 1)
michael@0 287 ret[key] = value
michael@0 288 if dist.has_metadata("requires.txt"):
michael@0 289 ret["Dependencies"] = "\n" + dist.get_metadata("requires.txt")
michael@0 290 return ret
michael@0 291
michael@0 292 def print_metadata(self, data=("Name", "Version", "Summary", "Home-page",
michael@0 293 "Author", "Author-email", "License", "Platform", "Dependencies")):
michael@0 294 for key in data:
michael@0 295 if key in self.metadata:
michael@0 296 print key + ": " + self.metadata[key]
michael@0 297
michael@0 298 ### methods for running
michael@0 299
michael@0 300 def command_args(self):
michael@0 301 """additional arguments for the mozilla application"""
michael@0 302 return map(os.path.expanduser, self.options.appArgs)
michael@0 303
michael@0 304 def runner_args(self):
michael@0 305 """arguments to instantiate the runner class"""
michael@0 306 return dict(cmdargs=self.command_args(),
michael@0 307 binary=self.options.binary,
michael@0 308 profile_args=self.profile_args())
michael@0 309
michael@0 310 def create_runner(self):
michael@0 311 return self.runner_class.create(**self.runner_args())
michael@0 312
michael@0 313 def run(self):
michael@0 314 runner = self.create_runner()
michael@0 315 self.start(runner)
michael@0 316 runner.cleanup()
michael@0 317
michael@0 318 def debugger_arguments(self):
michael@0 319 """Get the debugger arguments
michael@0 320
michael@0 321 returns a 2-tuple of debugger arguments:
michael@0 322 (debugger_arguments, interactive)
michael@0 323
michael@0 324 """
michael@0 325 debug_args = self.options.debugger_args
michael@0 326 if debug_args is not None:
michael@0 327 debug_args = debug_args.split()
michael@0 328 interactive = self.options.interactive
michael@0 329 if self.options.debugger:
michael@0 330 debug_args, interactive = debugger_arguments(self.options.debugger, debug_args, interactive)
michael@0 331 return debug_args, interactive
michael@0 332
michael@0 333 def start(self, runner):
michael@0 334 """Starts the runner and waits for the application to exit
michael@0 335
michael@0 336 It can also happen via a keyboard interrupt. It should be
michael@0 337 overwritten to provide custom running of the runner instance.
michael@0 338
michael@0 339 """
michael@0 340 # attach a debugger if specified
michael@0 341 debug_args, interactive = self.debugger_arguments()
michael@0 342 runner.start(debug_args=debug_args, interactive=interactive)
michael@0 343 print 'Starting: ' + ' '.join(runner.command)
michael@0 344 try:
michael@0 345 runner.wait()
michael@0 346 except KeyboardInterrupt:
michael@0 347 runner.stop()
michael@0 348
michael@0 349
michael@0 350 def cli(args=sys.argv[1:]):
michael@0 351 CLI(args).run()
michael@0 352
michael@0 353
michael@0 354 if __name__ == '__main__':
michael@0 355 cli()

mercurial