~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to testing/mozbase/mozrunner/mozrunner/runner.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

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