Bug 987414 - Pass multiple test arguments to mach testing commands; r=ahal

Previously, mach xpcshell-test and mochitest-* were limited to a single
test "path" argument. This patch enables multiple arguments to be passed
in.

TestResolver in the build system has gained the ability to process
multiple paths in a single invocation. The mach commands have been
modified to utilize this new feature.

Only mach code paths that pass manifestdestiny.TestManifest instances
into the test runner can accept multiple arguments. This is because
there is no other way to pass a custom set of tests into the test
runner. If multiple test arguments are used but not supported, a warning
is emitted.

--HG--
extra : rebase_source : 1ce1328a969f654e7b43a7a0bdd15ed86f5ceb21
This commit is contained in:
Gregory Szorc 2014-03-24 16:19:57 -07:00
parent 6cccf7a13e
commit 4ff214391e
4 changed files with 92 additions and 79 deletions

View File

@ -142,7 +142,7 @@ class TestTestMetadata(Base):
def test_resolve_by_dir(self):
t = self._get_test_metadata()
self.assertEqual(len(list(t.resolve_tests('services/common'))), 2)
self.assertEqual(len(list(t.resolve_tests(paths=['services/common']))), 2)
def test_resolve_under_path(self):
t = self._get_test_metadata()
@ -151,6 +151,11 @@ class TestTestMetadata(Base):
self.assertEqual(len(list(t.resolve_tests(flavor='xpcshell',
under_path='services'))), 2)
def test_resolve_multiple_paths(self):
t = self._get_test_metadata()
result = list(t.resolve_tests(paths=['services', 'toolkit']))
self.assertEqual(len(result), 4)
class TestTestResolver(Base):
FAKE_TOPSRCDIR = '/Users/gps/src/firefox'
@ -183,7 +188,7 @@ class TestTestResolver(Base):
# Pretend we're under '/services' and ask for 'common'. This should
# pick up all tests from '/services/common'
tests = list(r.resolve_tests(path='common', cwd=os.path.join(r.topsrcdir,
tests = list(r.resolve_tests(paths=['common'], cwd=os.path.join(r.topsrcdir,
'services')))
self.assertEqual(len(tests), 2)
@ -198,14 +203,14 @@ class TestTestResolver(Base):
r = self._get_resolver()
expected = list(r.resolve_tests(path='services'))
actual = list(r.resolve_tests(path='services', cwd='/'))
expected = list(r.resolve_tests(paths=['services']))
actual = list(r.resolve_tests(paths=['services'], cwd='/'))
self.assertEqual(actual, expected)
actual = list(r.resolve_tests(path='services', cwd=r.topsrcdir))
actual = list(r.resolve_tests(paths=['services'], cwd=r.topsrcdir))
self.assertEqual(actual, expected)
actual = list(r.resolve_tests(path='services', cwd=r.topobjdir))
actual = list(r.resolve_tests(paths=['services'], cwd=r.topobjdir))
self.assertEqual(actual, expected)

View File

@ -69,17 +69,16 @@ class TestMetadata(object):
for path in sorted(self._tests_by_flavor.get(flavor, [])):
yield self._tests_by_path[path]
def resolve_tests(self, path=None, flavor=None, under_path=None):
def resolve_tests(self, paths=None, flavor=None, under_path=None):
"""Resolve tests from an identifier.
This is a generator of dicts describing each test.
If ``path`` is a known test file, the tests associated with that file
are returned. Files can be specified by full path (relative to main
directory), or as a file fragment. The lookup simply tests whether
the string is in the path of a test file.
If ``path`` is a directory, the tests in that directory are returned.
``paths`` can be an iterable of values to use to identify tests to run.
If an entry is a known test file, tests associated with that file are
returned (there may be multiple configurations for a single file). If
an entry is a directory, all tests in that directory are returned. If
the string appears in a known test file, that test file is considered.
If ``under_path`` is a string, it will be used to filter out tests that
aren't in the specified path prefix relative to topsrcdir or the
@ -101,30 +100,33 @@ class TestMetadata(object):
# Make a copy so modifications don't change the source.
yield dict(test)
path = mozpath.normpath(path) if path else None
paths = paths or []
paths = [mozpath.normpath(p) for p in paths]
if not paths:
paths = [None]
if path in self._test_dirs:
candidates = []
for p, tests in sorted(self._tests_by_path.items()):
if not p.startswith(path):
continue
candidate_paths = set()
candidates.extend(tests)
for path in sorted(paths):
if path is None:
candidate_paths |= set(self._tests_by_path.keys())
continue
for test in fltr(candidates):
# If the path is a directory, pull in all tests in that directory.
if path in self._test_dirs:
candidate_paths |= {p for p in self._tests_by_path
if p.startswith(path)}
continue
# If it's a test file, add just that file.
candidate_paths |= {p for p in self._tests_by_path if path in p}
for p in sorted(candidate_paths):
tests = self._tests_by_path[p]
for test in fltr(tests):
yield test
return
# Do file lookup.
candidates = []
for p, tests in sorted(self._tests_by_path.items()):
if path is None or path in p:
candidates.extend(tests)
for test in fltr(candidates):
yield test
class TestResolver(MozbuildObject):
"""Helper to resolve tests from the current environment to test files."""

View File

@ -93,19 +93,21 @@ class MochitestRunner(MozbuildObject):
self.mochitest_dir = os.path.join(self.tests_dir, 'testing', 'mochitest')
self.bin_dir = os.path.join(self.topobjdir, 'dist', 'bin')
def run_b2g_test(self, test_file=None, b2g_home=None, xre_path=None,
def run_b2g_test(self, test_paths=None, b2g_home=None, xre_path=None,
total_chunks=None, this_chunk=None, no_window=None,
**kwargs):
"""Runs a b2g mochitest.
test_file is a path to a test file. It can be a relative path from the
top source directory, an absolute filename, or a directory containing
test files.
test_paths is an enumerable of paths to tests. It can be a relative path
from the top source directory, an absolute filename, or a directory
containing test files.
"""
# Need to call relpath before os.chdir() below.
test_path = ''
if test_file:
test_path = self._wrap_path_argument(test_file).relpath()
if test_paths:
if len(test_paths) > 1:
print('Warning: Only the first test path will be used.')
test_path = self._wrap_path_argument(test_paths[0]).relpath()
# TODO without os.chdir, chained imports fail below
os.chdir(self.mochitest_dir)
@ -181,7 +183,7 @@ class MochitestRunner(MozbuildObject):
options.xrePath = xre_path
return mochitest.run_remote_mochitests(parser, options)
def run_desktop_test(self, context, suite=None, test_file=None, debugger=None,
def run_desktop_test(self, context, suite=None, test_paths=None, debugger=None,
debugger_args=None, slowscript=False, shuffle=False, keep_open=False,
rerun_failures=False, no_autorun=False, repeat=0, run_until_failure=False,
slow=False, chunk_by_dir=0, total_chunks=None, this_chunk=None,
@ -191,7 +193,7 @@ class MochitestRunner(MozbuildObject):
install_extension=None, **kwargs):
"""Runs a mochitest.
test_file is a path to a test file. It can be a relative path from the
test_paths are path to tests. They can be a relative path from the
top source directory, an absolute filename, or a directory containing
test files.
@ -211,14 +213,13 @@ class MochitestRunner(MozbuildObject):
keep_open denotes whether to keep the browser open after tests
complete.
"""
if rerun_failures and test_file:
if rerun_failures and test_paths:
print('Cannot specify both --rerun-failures and a test path.')
return 1
# Need to call relpath before os.chdir() below.
test_path = ''
if test_file:
test_path = self._wrap_path_argument(test_file).relpath()
if test_paths:
test_paths = [self._wrap_path_argument(p).relpath() for p in test_paths]
failure_file_path = os.path.join(self.statedir, 'mochitest_failures.json')
@ -315,10 +316,10 @@ class MochitestRunner(MozbuildObject):
for k, v in kwargs.iteritems():
setattr(options, k, v)
if test_path:
if test_paths:
resolver = self._spawn(TestResolver)
tests = list(resolver.resolve_tests(path=test_path, flavor=flavor,
tests = list(resolver.resolve_tests(paths=test_paths, flavor=flavor,
cwd=context.cwd))
if not tests:
@ -488,7 +489,7 @@ def MochitestCommand(func):
help='Specifies the directory in which to place dumped memory reports.')
func = dumpOutputDirectory(func)
path = CommandArgument('test_file', default=None, nargs='?',
path = CommandArgument('test_paths', default=None, nargs='*',
metavar='TEST',
help='Test to run. Can be specified as a single file, a ' \
'directory, or omitted. If omitted, the entire test suite is ' \
@ -551,7 +552,7 @@ def B2GCommand(func):
help='If specified, will only log subtest results on failure or timeout.')
func = hide_subtests(func)
path = CommandArgument('test_file', default=None, nargs='?',
path = CommandArgument('test_paths', default=None, nargs='*',
metavar='TEST',
help='Test to run. Can be specified as a single file, a ' \
'directory, or omitted. If omitted, the entire test suite is ' \
@ -568,52 +569,52 @@ class MachCommands(MachCommandBase):
conditions=[conditions.is_firefox],
description='Run a plain mochitest.')
@MochitestCommand
def run_mochitest_plain(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'plain', **kwargs)
def run_mochitest_plain(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'plain', **kwargs)
@Command('mochitest-chrome', category='testing',
conditions=[conditions.is_firefox],
description='Run a chrome mochitest.')
@MochitestCommand
def run_mochitest_chrome(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'chrome', **kwargs)
def run_mochitest_chrome(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'chrome', **kwargs)
@Command('mochitest-browser', category='testing',
conditions=[conditions.is_firefox],
description='Run a mochitest with browser chrome.')
@MochitestCommand
def run_mochitest_browser(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'browser', **kwargs)
def run_mochitest_browser(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'browser', **kwargs)
@Command('mochitest-metro', category='testing',
conditions=[conditions.is_firefox],
description='Run a mochitest with metro browser chrome.')
@MochitestCommand
def run_mochitest_metro(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'metro', **kwargs)
def run_mochitest_metro(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'metro', **kwargs)
@Command('mochitest-a11y', category='testing',
conditions=[conditions.is_firefox],
description='Run an a11y mochitest.')
@MochitestCommand
def run_mochitest_a11y(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'a11y', **kwargs)
def run_mochitest_a11y(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'a11y', **kwargs)
@Command('webapprt-test-chrome', category='testing',
conditions=[conditions.is_firefox],
description='Run a webapprt chrome mochitest.')
@MochitestCommand
def run_mochitest_webapprt_chrome(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'webapprt-chrome', **kwargs)
def run_mochitest_webapprt_chrome(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'webapprt-chrome', **kwargs)
@Command('webapprt-test-content', category='testing',
conditions=[conditions.is_firefox],
description='Run a webapprt content mochitest.')
@MochitestCommand
def run_mochitest_webapprt_content(self, test_file, **kwargs):
return self.run_mochitest(test_file, 'webapprt-content', **kwargs)
def run_mochitest_webapprt_content(self, test_paths, **kwargs):
return self.run_mochitest(test_paths, 'webapprt-content', **kwargs)
def run_mochitest(self, test_file, flavor, **kwargs):
def run_mochitest(self, test_paths, flavor, **kwargs):
from mozbuild.controller.building import BuildDriver
self._ensure_state_subdir_exists('.')
@ -624,7 +625,7 @@ class MachCommands(MachCommandBase):
mochitest = self._spawn(MochitestRunner)
return mochitest.run_desktop_test(self._mach_context,
test_file=test_file, suite=flavor, **kwargs)
test_paths=test_paths, suite=flavor, **kwargs)
# TODO For now b2g commands will only work with the emulator,
@ -650,7 +651,7 @@ class B2GCommands(MachCommandBase):
description='Run a remote mochitest.',
conditions=[conditions.is_b2g, is_emulator])
@B2GCommand
def run_mochitest_remote(self, test_file, **kwargs):
def run_mochitest_remote(self, test_paths, **kwargs):
from mozbuild.controller.building import BuildDriver
self._ensure_state_subdir_exists('.')
@ -660,13 +661,13 @@ class B2GCommands(MachCommandBase):
mochitest = self._spawn(MochitestRunner)
return mochitest.run_b2g_test(b2g_home=self.b2g_home,
xre_path=self.xre_path, test_file=test_file, **kwargs)
xre_path=self.xre_path, test_paths=test_paths, **kwargs)
@Command('mochitest-b2g-desktop', category='testing',
conditions=[conditions.is_b2g_desktop],
description='Run a b2g desktop mochitest.')
@B2GCommand
def run_mochitest_b2g_desktop(self, test_file, **kwargs):
def run_mochitest_b2g_desktop(self, test_paths, **kwargs):
from mozbuild.controller.building import BuildDriver
self._ensure_state_subdir_exists('.')
@ -675,4 +676,4 @@ class B2GCommands(MachCommandBase):
driver.install_tests(remove=False)
mochitest = self._spawn(MochitestRunner)
return mochitest.run_b2g_test(test_file=test_file, **kwargs)
return mochitest.run_b2g_test(test_paths=test_paths, **kwargs)

View File

@ -62,7 +62,7 @@ class XPCShellRunner(MozbuildObject):
return self._run_xpcshell_harness(manifest=manifest, **kwargs)
def run_test(self, test_file, interactive=False,
def run_test(self, test_paths, interactive=False,
keep_going=False, sequential=False, shuffle=False,
debugger=None, debuggerArgs=None, debuggerInteractive=None,
rerun_failures=False,
@ -77,7 +77,7 @@ class XPCShellRunner(MozbuildObject):
if build_path not in sys.path:
sys.path.append(build_path)
if test_file == 'all':
if test_paths == ['all']:
self.run_suite(interactive=interactive,
keep_going=keep_going, shuffle=shuffle, sequential=sequential,
debugger=debugger, debuggerArgs=debuggerArgs,
@ -86,7 +86,7 @@ class XPCShellRunner(MozbuildObject):
return
resolver = self._spawn(TestResolver)
tests = list(resolver.resolve_tests(path=test_file, flavor='xpcshell',
tests = list(resolver.resolve_tests(paths=test_paths, flavor='xpcshell',
cwd=self.cwd))
if not tests:
@ -218,7 +218,7 @@ class AndroidXPCShellRunner(MozbuildObject):
"""Run Android xpcshell tests."""
def run_test(self,
test_file, keep_going,
test_paths, keep_going,
devicemanager, ip, port, remote_test_root, no_setup, local_apk,
# ignore parameters from other platforms' options
**kwargs):
@ -258,13 +258,15 @@ class AndroidXPCShellRunner(MozbuildObject):
else:
raise Exception("You must specify an APK")
if test_file == 'all':
if test_paths == ['all']:
testdirs = []
options.testPath = None
options.verbose = False
else:
testdirs = test_file
options.testPath = test_file
if len(test_paths) > 1:
print('Warning: only the first test path argument will be used.')
testdirs = test_paths[0]
options.testPath = test_paths[0]
options.verbose = True
dummy_log = StringIO()
xpcshell = remotexpcshelltests.XPCShellRemote(dm, options, args=testdirs, log=dummy_log)
@ -322,7 +324,7 @@ class B2GXPCShellRunner(MozbuildObject):
f.write(data.read())
return busybox_path
def run_test(self, test_file, b2g_home=None, busybox=None,
def run_test(self, test_paths, b2g_home=None, busybox=None,
# ignore parameters from other platforms' options
**kwargs):
try:
@ -334,8 +336,11 @@ class B2GXPCShellRunner(MozbuildObject):
sys.exit(1)
test_path = None
if test_file:
test_path = self._wrap_path_argument(test_file).relpath()
if test_paths:
if len(test_paths) > 1:
print('Warning: Only the first test path will be used.')
test_path = self._wrap_path_argument(test_paths[0]).relpath()
import runtestsb2g
parser = runtestsb2g.B2GOptions()
@ -378,7 +383,7 @@ class MachCommands(MachCommandBase):
@Command('xpcshell-test', category='testing',
conditions=[is_platform_supported],
description='Run XPCOM Shell tests.')
@CommandArgument('test_file', default='all', nargs='?', metavar='TEST',
@CommandArgument('test_paths', default='all', nargs='*', metavar='TEST',
help='Test to run. Can be specified as a single JS file, a directory, '
'or omitted. If omitted, the entire test suite is executed.')
@CommandArgument("--debugger", default=None, metavar='DEBUGGER',