Wed, 31 Dec 2014 06:55:50 +0100
Added tag UPSTREAM_283F7C6 for changeset ca08bd8f51b2
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/.
5 from __future__ import unicode_literals
7 import logging
8 import mozpack.path
9 import os
10 import platform
11 import sys
12 import warnings
13 import which
15 from mozbuild.base import (
16 MachCommandBase,
17 MachCommandConditions as conditions,
18 MozbuildObject,
19 )
21 from mach.decorators import (
22 CommandArgument,
23 CommandProvider,
24 Command,
25 )
27 from mach.logging import StructuredHumanFormatter
29 ADB_NOT_FOUND = '''
30 The %s command requires the adb binary to be on your path.
32 If you have a B2G build, this can be found in
33 '%s/out/host/<platform>/bin'.
34 '''.lstrip()
36 GAIA_PROFILE_NOT_FOUND = '''
37 The %s command requires a non-debug gaia profile. Either pass in --profile,
38 or set the GAIA_PROFILE environment variable.
40 If you do not have a non-debug gaia profile, you can build one:
41 $ git clone https://github.com/mozilla-b2g/gaia
42 $ cd gaia
43 $ make
45 The profile should be generated in a directory called 'profile'.
46 '''.lstrip()
48 GAIA_PROFILE_IS_DEBUG = '''
49 The %s command requires a non-debug gaia profile. The specified profile,
50 %s, is a debug profile.
52 If you do not have a non-debug gaia profile, you can build one:
53 $ git clone https://github.com/mozilla-b2g/gaia
54 $ cd gaia
55 $ make
57 The profile should be generated in a directory called 'profile'.
58 '''.lstrip()
61 class UnexpectedFilter(logging.Filter):
62 def filter(self, record):
63 msg = getattr(record, 'params', {}).get('msg', '')
64 return 'TEST-UNEXPECTED-' in msg
67 class MochitestRunner(MozbuildObject):
68 """Easily run mochitests.
70 This currently contains just the basics for running mochitests. We may want
71 to hook up result parsing, etc.
72 """
74 def get_webapp_runtime_path(self):
75 import mozinfo
76 appname = 'webapprt-stub' + mozinfo.info.get('bin_suffix', '')
77 if sys.platform.startswith('darwin'):
78 appname = os.path.join(self.distdir, self.substs['MOZ_MACBUNDLE_NAME'],
79 'Contents', 'MacOS', appname)
80 else:
81 appname = os.path.join(self.distdir, 'bin', appname)
82 return appname
84 def __init__(self, *args, **kwargs):
85 MozbuildObject.__init__(self, *args, **kwargs)
87 # TODO Bug 794506 remove once mach integrates with virtualenv.
88 build_path = os.path.join(self.topobjdir, 'build')
89 if build_path not in sys.path:
90 sys.path.append(build_path)
92 self.tests_dir = os.path.join(self.topobjdir, '_tests')
93 self.mochitest_dir = os.path.join(self.tests_dir, 'testing', 'mochitest')
94 self.bin_dir = os.path.join(self.topobjdir, 'dist', 'bin')
96 def run_b2g_test(self, test_paths=None, b2g_home=None, xre_path=None,
97 total_chunks=None, this_chunk=None, no_window=None,
98 **kwargs):
99 """Runs a b2g mochitest.
101 test_paths is an enumerable of paths to tests. It can be a relative path
102 from the top source directory, an absolute filename, or a directory
103 containing test files.
104 """
105 # Need to call relpath before os.chdir() below.
106 test_path = ''
107 if test_paths:
108 if len(test_paths) > 1:
109 print('Warning: Only the first test path will be used.')
110 test_path = self._wrap_path_argument(test_paths[0]).relpath()
112 # TODO without os.chdir, chained imports fail below
113 os.chdir(self.mochitest_dir)
115 # The imp module can spew warnings if the modules below have
116 # already been imported, ignore them.
117 with warnings.catch_warnings():
118 warnings.simplefilter('ignore')
120 import imp
121 path = os.path.join(self.mochitest_dir, 'runtestsb2g.py')
122 with open(path, 'r') as fh:
123 imp.load_module('mochitest', fh, path,
124 ('.py', 'r', imp.PY_SOURCE))
126 import mochitest
127 from mochitest_options import B2GOptions
129 parser = B2GOptions()
130 options = parser.parse_args([])[0]
132 test_path_dir = False;
133 if test_path:
134 test_root_file = mozpack.path.join(self.mochitest_dir, 'tests', test_path)
135 if not os.path.exists(test_root_file):
136 print('Specified test path does not exist: %s' % test_root_file)
137 return 1
138 if os.path.isdir(test_root_file):
139 test_path_dir = True;
140 options.testPath = test_path
142 for k, v in kwargs.iteritems():
143 setattr(options, k, v)
144 options.noWindow = no_window
145 options.totalChunks = total_chunks
146 options.thisChunk = this_chunk
148 options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
150 options.consoleLevel = 'INFO'
151 if conditions.is_b2g_desktop(self):
153 options.profile = options.profile or os.environ.get('GAIA_PROFILE')
154 if not options.profile:
155 print(GAIA_PROFILE_NOT_FOUND % 'mochitest-b2g-desktop')
156 return 1
158 if os.path.isfile(os.path.join(options.profile, 'extensions', \
159 'httpd@gaiamobile.org')):
160 print(GAIA_PROFILE_IS_DEBUG % ('mochitest-b2g-desktop',
161 options.profile))
162 return 1
164 options.desktop = True
165 options.app = self.get_binary_path()
166 if not options.app.endswith('-bin'):
167 options.app = '%s-bin' % options.app
168 if not os.path.isfile(options.app):
169 options.app = options.app[:-len('-bin')]
171 return mochitest.run_desktop_mochitests(parser, options)
173 try:
174 which.which('adb')
175 except which.WhichError:
176 # TODO Find adb automatically if it isn't on the path
177 print(ADB_NOT_FOUND % ('mochitest-remote', b2g_home))
178 return 1
180 options.b2gPath = b2g_home
181 options.logcat_dir = self.mochitest_dir
182 options.httpdPath = self.mochitest_dir
183 options.xrePath = xre_path
184 return mochitest.run_remote_mochitests(parser, options)
186 def run_desktop_test(self, context, suite=None, test_paths=None, debugger=None,
187 debugger_args=None, slowscript=False, screenshot_on_fail = False, shuffle=False, keep_open=False,
188 rerun_failures=False, no_autorun=False, repeat=0, run_until_failure=False,
189 slow=False, chunk_by_dir=0, total_chunks=None, this_chunk=None,
190 jsdebugger=False, debug_on_failure=False, start_at=None, end_at=None,
191 e10s=False, dmd=False, dump_output_directory=None,
192 dump_about_memory_after_test=False, dump_dmd_after_test=False,
193 install_extension=None, quiet=False, environment=[], **kwargs):
194 """Runs a mochitest.
196 test_paths are path to tests. They can be a relative path from the
197 top source directory, an absolute filename, or a directory containing
198 test files.
200 suite is the type of mochitest to run. It can be one of ('plain',
201 'chrome', 'browser', 'metro', 'a11y').
203 debugger is a program name or path to a binary (presumably a debugger)
204 to run the test in. e.g. 'gdb'
206 debugger_args are the arguments passed to the debugger.
208 slowscript is true if the user has requested the SIGSEGV mechanism of
209 invoking the slow script dialog.
211 shuffle is whether test order should be shuffled (defaults to false).
213 keep_open denotes whether to keep the browser open after tests
214 complete.
215 """
216 if rerun_failures and test_paths:
217 print('Cannot specify both --rerun-failures and a test path.')
218 return 1
220 # Need to call relpath before os.chdir() below.
221 if test_paths:
222 test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
224 failure_file_path = os.path.join(self.statedir, 'mochitest_failures.json')
226 if rerun_failures and not os.path.exists(failure_file_path):
227 print('No failure file present. Did you run mochitests before?')
228 return 1
230 from StringIO import StringIO
232 # runtests.py is ambiguous, so we load the file/module manually.
233 if 'mochitest' not in sys.modules:
234 import imp
235 path = os.path.join(self.mochitest_dir, 'runtests.py')
236 with open(path, 'r') as fh:
237 imp.load_module('mochitest', fh, path,
238 ('.py', 'r', imp.PY_SOURCE))
240 import mozinfo
241 import mochitest
242 from manifestparser import TestManifest
243 from mozbuild.testing import TestResolver
245 # This is required to make other components happy. Sad, isn't it?
246 os.chdir(self.topobjdir)
248 # Automation installs its own stream handler to stdout. Since we want
249 # all logging to go through us, we just remove their handler.
250 remove_handlers = [l for l in logging.getLogger().handlers
251 if isinstance(l, logging.StreamHandler)]
252 for handler in remove_handlers:
253 logging.getLogger().removeHandler(handler)
255 runner = mochitest.Mochitest()
257 opts = mochitest.MochitestOptions()
258 options, args = opts.parse_args([])
260 options.subsuite = ''
261 flavor = suite
263 # Need to set the suite options before verifyOptions below.
264 if suite == 'plain':
265 # Don't need additional options for plain.
266 flavor = 'mochitest'
267 elif suite == 'chrome':
268 options.chrome = True
269 elif suite == 'browser':
270 options.browserChrome = True
271 flavor = 'browser-chrome'
272 elif suite == 'devtools':
273 options.browserChrome = True
274 options.subsuite = 'devtools'
275 elif suite == 'metro':
276 options.immersiveMode = True
277 options.browserChrome = True
278 elif suite == 'a11y':
279 options.a11y = True
280 elif suite == 'webapprt-content':
281 options.webapprtContent = True
282 options.app = self.get_webapp_runtime_path()
283 elif suite == 'webapprt-chrome':
284 options.webapprtChrome = True
285 options.app = self.get_webapp_runtime_path()
286 options.browserArgs.append("-test-mode")
287 else:
288 raise Exception('None or unrecognized mochitest suite type.')
290 if dmd:
291 options.dmdPath = self.bin_dir
293 options.autorun = not no_autorun
294 options.closeWhenDone = not keep_open
295 options.slowscript = slowscript
296 options.screenshotOnFail = screenshot_on_fail
297 options.shuffle = shuffle
298 options.consoleLevel = 'INFO'
299 options.repeat = repeat
300 options.runUntilFailure = run_until_failure
301 options.runSlower = slow
302 options.testingModulesDir = os.path.join(self.tests_dir, 'modules')
303 options.extraProfileFiles.append(os.path.join(self.distdir, 'plugins'))
304 options.symbolsPath = os.path.join(self.distdir, 'crashreporter-symbols')
305 options.chunkByDir = chunk_by_dir
306 options.totalChunks = total_chunks
307 options.thisChunk = this_chunk
308 options.jsdebugger = jsdebugger
309 options.debugOnFailure = debug_on_failure
310 options.startAt = start_at
311 options.endAt = end_at
312 options.e10s = e10s
313 options.dumpAboutMemoryAfterTest = dump_about_memory_after_test
314 options.dumpDMDAfterTest = dump_dmd_after_test
315 options.dumpOutputDirectory = dump_output_directory
316 options.quiet = quiet
317 options.environment = environment
319 options.failureFile = failure_file_path
320 if install_extension != None:
321 options.extensionsToInstall = [os.path.join(self.topsrcdir,install_extension)]
323 for k, v in kwargs.iteritems():
324 setattr(options, k, v)
326 if test_paths:
327 resolver = self._spawn(TestResolver)
329 tests = list(resolver.resolve_tests(paths=test_paths, flavor=flavor,
330 cwd=context.cwd))
332 if not tests:
333 print('No tests could be found in the path specified. Please '
334 'specify a path that is a test file or is a directory '
335 'containing tests.')
336 return 1
338 manifest = TestManifest()
339 manifest.tests.extend(tests)
341 options.manifestFile = manifest
343 if rerun_failures:
344 options.testManifest = failure_file_path
346 if debugger:
347 options.debugger = debugger
349 if debugger_args:
350 if options.debugger == None:
351 print("--debugger-args passed, but no debugger specified.")
352 return 1
353 options.debuggerArgs = debugger_args
355 options = opts.verifyOptions(options, runner)
357 if options is None:
358 raise Exception('mochitest option validator failed.')
360 # We need this to enable colorization of output.
361 self.log_manager.enable_unstructured()
363 # Output processing is a little funky here. The old make targets
364 # grepped the log output from TEST-UNEXPECTED-* and printed these lines
365 # after test execution. Ideally the test runner would expose a Python
366 # API for obtaining test results and we could just format failures
367 # appropriately. Unfortunately, it doesn't yet do that. So, we capture
368 # all output to a buffer then "grep" the buffer after test execution.
369 # Bug 858197 tracks a Python API that would facilitate this.
370 test_output = StringIO()
371 handler = logging.StreamHandler(test_output)
372 handler.addFilter(UnexpectedFilter())
373 handler.setFormatter(StructuredHumanFormatter(0, write_times=False))
374 logging.getLogger().addHandler(handler)
376 result = runner.runTests(options)
378 # Need to remove our buffering handler before we echo failures or else
379 # it will catch them again!
380 logging.getLogger().removeHandler(handler)
381 self.log_manager.disable_unstructured()
383 if test_output.getvalue():
384 result = 1
385 for line in test_output.getvalue().splitlines():
386 self.log(logging.INFO, 'unexpected', {'msg': line}, '{msg}')
388 return result
391 def MochitestCommand(func):
392 """Decorator that adds shared command arguments to mochitest commands."""
394 # This employs light Python magic. Keep in mind a decorator is just a
395 # function that takes a function, does something with it, then returns a
396 # (modified) function. Here, we chain decorators onto the passed in
397 # function.
399 debugger = CommandArgument('--debugger', '-d', metavar='DEBUGGER',
400 help='Debugger binary to run test in. Program name or path.')
401 func = debugger(func)
403 debugger_args = CommandArgument('--debugger-args',
404 metavar='DEBUGGER_ARGS', help='Arguments to pass to the debugger.')
405 func = debugger_args(func)
407 # Bug 933807 introduced JS_DISABLE_SLOW_SCRIPT_SIGNALS to avoid clever
408 # segfaults induced by the slow-script-detecting logic for Ion/Odin JITted
409 # code. If we don't pass this, the user will need to periodically type
410 # "continue" to (safely) resume execution. There are ways to implement
411 # automatic resuming; see the bug.
412 slowscript = CommandArgument('--slowscript', action='store_true',
413 help='Do not set the JS_DISABLE_SLOW_SCRIPT_SIGNALS env variable; when not set, recoverable but misleading SIGSEGV instances may occur in Ion/Odin JIT code')
414 func = slowscript(func)
416 screenshot_on_fail = CommandArgument('--screenshot-on-fail', action='store_true',
417 help='Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory for storing the screenshots.')
418 func = screenshot_on_fail(func)
420 shuffle = CommandArgument('--shuffle', action='store_true',
421 help='Shuffle execution order.')
422 func = shuffle(func)
424 keep_open = CommandArgument('--keep-open', action='store_true',
425 help='Keep the browser open after tests complete.')
426 func = keep_open(func)
428 rerun = CommandArgument('--rerun-failures', action='store_true',
429 help='Run only the tests that failed during the last test run.')
430 func = rerun(func)
432 autorun = CommandArgument('--no-autorun', action='store_true',
433 help='Do not starting running tests automatically.')
434 func = autorun(func)
436 repeat = CommandArgument('--repeat', type=int, default=0,
437 help='Repeat the test the given number of times.')
438 func = repeat(func)
440 runUntilFailure = CommandArgument("--run-until-failure", action='store_true',
441 help='Run tests repeatedly and stops on the first time a test fails. ' \
442 'Default cap is 30 runs, which can be overwritten ' \
443 'with the --repeat parameter.')
444 func = runUntilFailure(func)
446 slow = CommandArgument('--slow', action='store_true',
447 help='Delay execution between tests.')
448 func = slow(func)
450 end_at = CommandArgument('--end-at', type=str,
451 help='Stop running the test sequence at this test.')
452 func = end_at(func)
454 start_at = CommandArgument('--start-at', type=str,
455 help='Start running the test sequence at this test.')
456 func = start_at(func)
458 chunk_dir = CommandArgument('--chunk-by-dir', type=int,
459 help='Group tests together in chunks by this many top directories.')
460 func = chunk_dir(func)
462 chunk_total = CommandArgument('--total-chunks', type=int,
463 help='Total number of chunks to split tests into.')
464 func = chunk_total(func)
466 this_chunk = CommandArgument('--this-chunk', type=int,
467 help='If running tests by chunks, the number of the chunk to run.')
468 func = this_chunk(func)
470 hide_subtests = CommandArgument('--hide-subtests', action='store_true',
471 help='If specified, will only log subtest results on failure or timeout.')
472 func = hide_subtests(func)
474 debug_on_failure = CommandArgument('--debug-on-failure', action='store_true',
475 help='Breaks execution and enters the JS debugger on a test failure. ' \
476 'Should be used together with --jsdebugger.')
477 func = debug_on_failure(func)
479 jsdebugger = CommandArgument('--jsdebugger', action='store_true',
480 help='Start the browser JS debugger before running the test. Implies --no-autorun.')
481 func = jsdebugger(func)
483 this_chunk = CommandArgument('--e10s', action='store_true',
484 help='Run tests with electrolysis preferences and test filtering enabled.')
485 func = this_chunk(func)
487 dmd = CommandArgument('--dmd', action='store_true',
488 help='Run tests with DMD active.')
489 func = dmd(func)
491 dumpAboutMemory = CommandArgument('--dump-about-memory-after-test', action='store_true',
492 help='Dump an about:memory log after every test.')
493 func = dumpAboutMemory(func)
495 dumpDMD = CommandArgument('--dump-dmd-after-test', action='store_true',
496 help='Dump a DMD log after every test.')
497 func = dumpDMD(func)
499 dumpOutputDirectory = CommandArgument('--dump-output-directory', action='store',
500 help='Specifies the directory in which to place dumped memory reports.')
501 func = dumpOutputDirectory(func)
503 path = CommandArgument('test_paths', default=None, nargs='*',
504 metavar='TEST',
505 help='Test to run. Can be specified as a single file, a ' \
506 'directory, or omitted. If omitted, the entire test suite is ' \
507 'executed.')
508 func = path(func)
510 install_extension = CommandArgument('--install-extension',
511 help='Install given extension before running selected tests. ' \
512 'Parameter is a path to xpi file.')
513 func = install_extension(func)
515 quiet = CommandArgument('--quiet', default=False, action='store_true',
516 help='Do not print test log lines unless a failure occurs.')
517 func = quiet(func)
519 setenv = CommandArgument('--setenv', default=[], action='append',
520 metavar='NAME=VALUE', dest='environment',
521 help="Sets the given variable in the application's environment")
522 func = setenv(func)
524 return func
526 def B2GCommand(func):
527 """Decorator that adds shared command arguments to b2g mochitest commands."""
529 busybox = CommandArgument('--busybox', default=None,
530 help='Path to busybox binary to install on device')
531 func = busybox(func)
533 logcatdir = CommandArgument('--logcat-dir', default=None,
534 help='directory to store logcat dump files')
535 func = logcatdir(func)
537 profile = CommandArgument('--profile', default=None,
538 help='for desktop testing, the path to the \
539 gaia profile to use')
540 func = profile(func)
542 geckopath = CommandArgument('--gecko-path', default=None,
543 help='the path to a gecko distribution that should \
544 be installed on the emulator prior to test')
545 func = geckopath(func)
547 nowindow = CommandArgument('--no-window', action='store_true', default=False,
548 help='Pass --no-window to the emulator')
549 func = nowindow(func)
551 sdcard = CommandArgument('--sdcard', default="10MB",
552 help='Define size of sdcard: 1MB, 50MB...etc')
553 func = sdcard(func)
555 emulator = CommandArgument('--emulator', default='arm',
556 help='Architecture of emulator to use: x86 or arm')
557 func = emulator(func)
559 marionette = CommandArgument('--marionette', default=None,
560 help='host:port to use when connecting to Marionette')
561 func = marionette(func)
563 chunk_total = CommandArgument('--total-chunks', type=int,
564 help='Total number of chunks to split tests into.')
565 func = chunk_total(func)
567 this_chunk = CommandArgument('--this-chunk', type=int,
568 help='If running tests by chunks, the number of the chunk to run.')
569 func = this_chunk(func)
571 hide_subtests = CommandArgument('--hide-subtests', action='store_true',
572 help='If specified, will only log subtest results on failure or timeout.')
573 func = hide_subtests(func)
575 path = CommandArgument('test_paths', default=None, nargs='*',
576 metavar='TEST',
577 help='Test to run. Can be specified as a single file, a ' \
578 'directory, or omitted. If omitted, the entire test suite is ' \
579 'executed.')
580 func = path(func)
582 return func
586 @CommandProvider
587 class MachCommands(MachCommandBase):
588 @Command('mochitest-plain', category='testing',
589 conditions=[conditions.is_firefox],
590 description='Run a plain mochitest.')
591 @MochitestCommand
592 def run_mochitest_plain(self, test_paths, **kwargs):
593 return self.run_mochitest(test_paths, 'plain', **kwargs)
595 @Command('mochitest-chrome', category='testing',
596 conditions=[conditions.is_firefox],
597 description='Run a chrome mochitest.')
598 @MochitestCommand
599 def run_mochitest_chrome(self, test_paths, **kwargs):
600 return self.run_mochitest(test_paths, 'chrome', **kwargs)
602 @Command('mochitest-browser', category='testing',
603 conditions=[conditions.is_firefox],
604 description='Run a mochitest with browser chrome.')
605 @MochitestCommand
606 def run_mochitest_browser(self, test_paths, **kwargs):
607 return self.run_mochitest(test_paths, 'browser', **kwargs)
609 @Command('mochitest-devtools', category='testing',
610 conditions=[conditions.is_firefox],
611 description='Run a devtools mochitest with browser chrome.')
612 @MochitestCommand
613 def run_mochitest_devtools(self, test_paths, **kwargs):
614 return self.run_mochitest(test_paths, 'devtools', **kwargs)
616 @Command('mochitest-metro', category='testing',
617 conditions=[conditions.is_firefox],
618 description='Run a mochitest with metro browser chrome.')
619 @MochitestCommand
620 def run_mochitest_metro(self, test_paths, **kwargs):
621 return self.run_mochitest(test_paths, 'metro', **kwargs)
623 @Command('mochitest-a11y', category='testing',
624 conditions=[conditions.is_firefox],
625 description='Run an a11y mochitest.')
626 @MochitestCommand
627 def run_mochitest_a11y(self, test_paths, **kwargs):
628 return self.run_mochitest(test_paths, 'a11y', **kwargs)
630 @Command('webapprt-test-chrome', category='testing',
631 conditions=[conditions.is_firefox],
632 description='Run a webapprt chrome mochitest.')
633 @MochitestCommand
634 def run_mochitest_webapprt_chrome(self, test_paths, **kwargs):
635 return self.run_mochitest(test_paths, 'webapprt-chrome', **kwargs)
637 @Command('webapprt-test-content', category='testing',
638 conditions=[conditions.is_firefox],
639 description='Run a webapprt content mochitest.')
640 @MochitestCommand
641 def run_mochitest_webapprt_content(self, test_paths, **kwargs):
642 return self.run_mochitest(test_paths, 'webapprt-content', **kwargs)
644 def run_mochitest(self, test_paths, flavor, **kwargs):
645 from mozbuild.controller.building import BuildDriver
647 self._ensure_state_subdir_exists('.')
649 driver = self._spawn(BuildDriver)
650 driver.install_tests(remove=False)
652 mochitest = self._spawn(MochitestRunner)
654 return mochitest.run_desktop_test(self._mach_context,
655 test_paths=test_paths, suite=flavor, **kwargs)
658 # TODO For now b2g commands will only work with the emulator,
659 # they should be modified to work with all devices.
660 def is_emulator(cls):
661 """Emulator needs to be configured."""
662 return cls.device_name.find('emulator') == 0
665 @CommandProvider
666 class B2GCommands(MachCommandBase):
667 """So far these are only mochitest plain. They are
668 implemented separately because their command lines
669 are completely different.
670 """
671 def __init__(self, context):
672 MachCommandBase.__init__(self, context)
674 for attr in ('b2g_home', 'xre_path', 'device_name'):
675 setattr(self, attr, getattr(context, attr, None))
677 @Command('mochitest-remote', category='testing',
678 description='Run a remote mochitest.',
679 conditions=[conditions.is_b2g, is_emulator])
680 @B2GCommand
681 def run_mochitest_remote(self, test_paths, **kwargs):
682 from mozbuild.controller.building import BuildDriver
684 self._ensure_state_subdir_exists('.')
686 driver = self._spawn(BuildDriver)
687 driver.install_tests(remove=False)
689 mochitest = self._spawn(MochitestRunner)
690 return mochitest.run_b2g_test(b2g_home=self.b2g_home,
691 xre_path=self.xre_path, test_paths=test_paths, **kwargs)
693 @Command('mochitest-b2g-desktop', category='testing',
694 conditions=[conditions.is_b2g_desktop],
695 description='Run a b2g desktop mochitest.')
696 @B2GCommand
697 def run_mochitest_b2g_desktop(self, test_paths, **kwargs):
698 from mozbuild.controller.building import BuildDriver
700 self._ensure_state_subdir_exists('.')
702 driver = self._spawn(BuildDriver)
703 driver.install_tests(remove=False)
705 mochitest = self._spawn(MochitestRunner)
706 return mochitest.run_b2g_test(test_paths=test_paths, **kwargs)