blob: 5811417f5909522c62c7544a9cbe917f632b2fc1 [file] [log] [blame]
# 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/.
from __future__ import absolute_import, unicode_literals
import json
import os
import mozpack.path as mozpath
from .base import MozbuildObject
from .util import OrderedDefaultDict
from collections import defaultdict
def rewrite_test_base(test, new_base, honor_install_to_subdir=False):
"""Rewrite paths in a test to be under a new base path.
This is useful for running tests from a separate location from where they
were defined.
honor_install_to_subdir and the underlying install-to-subdir field are a
giant hack intended to work around the restriction where the mochitest
runner can't handle single test files with multiple configurations. This
argument should be removed once the mochitest runner talks manifests
(bug 984670).
"""
test['here'] = mozpath.join(new_base, test['dir_relpath'])
if honor_install_to_subdir and test.get('install-to-subdir'):
manifest_relpath = mozpath.relpath(test['path'],
mozpath.dirname(test['manifest']))
test['path'] = mozpath.join(new_base, test['dir_relpath'],
test['install-to-subdir'], manifest_relpath)
else:
test['path'] = mozpath.join(new_base, test['file_relpath'])
return test
class TestMetadata(object):
"""Holds information about tests.
This class provides an API to query tests active in the build
configuration.
"""
def __init__(self, filename=None):
self._tests_by_path = OrderedDefaultDict(list)
self._tests_by_flavor = defaultdict(set)
self._test_dirs = set()
if filename:
with open(filename, 'rt') as fh:
d = json.load(fh)
for path, tests in d.items():
for metadata in tests:
self._tests_by_path[path].append(metadata)
self._test_dirs.add(os.path.dirname(path))
flavor = metadata.get('flavor')
self._tests_by_flavor[flavor].add(path)
def tests_with_flavor(self, flavor):
"""Obtain all tests having the specified flavor.
This is a generator of dicts describing each test.
"""
for path in sorted(self._tests_by_flavor.get(flavor, [])):
yield self._tests_by_path[path]
def resolve_tests(self, paths=None, flavor=None, subsuite=None, under_path=None,
tags=None):
"""Resolve tests from an identifier.
This is a generator of dicts describing each test.
``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, or a prefix of a directory containing tests,
all tests in that directory are returned. If the string appears in a
known test file, that test file is considered. If the path contains
a wildcard pattern, tests matching that pattern are returned.
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
test's installed dir.
If ``flavor`` is a string, it will be used to filter returned tests
to only be the flavor specified. A flavor is something like
``xpcshell``.
If ``subsuite`` is a string, it will be used to filter returned tests
to only be in the subsuite specified.
If ``tags`` are specified, they will be used to filter returned tests
to only those with a matching tag.
"""
if tags:
tags = set(tags)
def fltr(tests):
for test in tests:
if flavor:
if (flavor == 'devtools' and test.get('flavor') != 'browser-chrome') or \
(flavor != 'devtools' and test.get('flavor') != flavor):
continue
if subsuite and test.get('subsuite') != subsuite:
continue
if tags and not (tags & set(test.get('tags', '').split())):
continue
if under_path \
and not test['file_relpath'].startswith(under_path):
continue
# Make a copy so modifications don't change the source.
yield dict(test)
paths = paths or []
paths = [mozpath.normpath(p) for p in paths]
if not paths:
paths = [None]
candidate_paths = set()
for path in sorted(paths):
if path is None:
candidate_paths |= set(self._tests_by_path.keys())
continue
if '*' in path:
candidate_paths |= {p for p in self._tests_by_path
if mozpath.match(p, path)}
continue
# If the path is a directory, or the path is a prefix of a directory
# containing tests, pull in all tests in that directory.
if (path in self._test_dirs or
any(p.startswith(path) for p in self._tests_by_path)):
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
class TestResolver(MozbuildObject):
"""Helper to resolve tests from the current environment to test files."""
def __init__(self, *args, **kwargs):
MozbuildObject.__init__(self, *args, **kwargs)
self._tests = TestMetadata(filename=os.path.join(self.topobjdir,
'all-tests.json'))
self._test_rewrites = {
'a11y': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'a11y'),
'browser-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'browser'),
'jetpack-package': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'jetpack-package'),
'jetpack-addon': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'jetpack-addon'),
'chrome': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'chrome'),
'mochitest': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'tests'),
'webapprt-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'webapprtChrome'),
'webapprt-content': os.path.join(self.topobjdir, '_tests', 'testing',
'mochitest', 'webapprtContent'),
'web-platform-tests': os.path.join(self.topobjdir, '_tests', 'testing',
'web-platform'),
'xpcshell': os.path.join(self.topobjdir, '_tests', 'xpcshell'),
}
def resolve_tests(self, cwd=None, **kwargs):
"""Resolve tests in the context of the current environment.
This is a more intelligent version of TestMetadata.resolve_tests().
This function provides additional massaging and filtering of low-level
results.
Paths in returned tests are automatically translated to the paths in
the _tests directory under the object directory.
If cwd is defined, we will limit our results to tests under the
directory specified. The directory should be defined as an absolute
path under topsrcdir or topobjdir for it to work properly.
"""
rewrite_base = None
if cwd:
norm_cwd = mozpath.normpath(cwd)
norm_srcdir = mozpath.normpath(self.topsrcdir)
norm_objdir = mozpath.normpath(self.topobjdir)
reldir = None
if norm_cwd.startswith(norm_objdir):
reldir = norm_cwd[len(norm_objdir)+1:]
elif norm_cwd.startswith(norm_srcdir):
reldir = norm_cwd[len(norm_srcdir)+1:]
result = self._tests.resolve_tests(under_path=reldir,
**kwargs)
else:
result = self._tests.resolve_tests(**kwargs)
for test in result:
rewrite_base = self._test_rewrites.get(test['flavor'], None)
if rewrite_base:
yield rewrite_test_base(test, rewrite_base,
honor_install_to_subdir=True)
else:
yield test
# These definitions provide a single source of truth for modules attempting
# to get a view of all tests for a build. Used by the emitter to figure out
# how to read/install manifests and by test dependency annotations in Files()
# entries to enumerate test flavors.
# While there are multiple test manifests, the behavior is very similar
# across them. We enforce this by having common handling of all
# manifests and outputting a single class type with the differences
# described inside the instance.
#
# Keys are variable prefixes and values are tuples describing how these
# manifests should be handled:
#
# (flavor, install_prefix, package_tests)
#
# flavor identifies the flavor of this test.
# install_prefix is the path prefix of where to install the files in
# the tests directory.
# package_tests indicates whether to package test files into the test
# package; suites that compile the test files should not install
# them into the test package.
#
TEST_MANIFESTS = dict(
A11Y=('a11y', 'testing/mochitest', 'a11y', True),
BROWSER_CHROME=('browser-chrome', 'testing/mochitest', 'browser', True),
ANDROID_INSTRUMENTATION=('instrumentation', 'instrumentation', '.', False),
JETPACK_PACKAGE=('jetpack-package', 'testing/mochitest', 'jetpack-package', True),
JETPACK_ADDON=('jetpack-addon', 'testing/mochitest', 'jetpack-addon', False),
METRO_CHROME=('metro-chrome', 'testing/mochitest', 'metro', True),
MOCHITEST=('mochitest', 'testing/mochitest', 'tests', True),
MOCHITEST_CHROME=('chrome', 'testing/mochitest', 'chrome', True),
MOCHITEST_WEBAPPRT_CONTENT=('webapprt-content', 'testing/mochitest', 'webapprtContent', True),
MOCHITEST_WEBAPPRT_CHROME=('webapprt-chrome', 'testing/mochitest', 'webapprtChrome', True),
WEBRTC_SIGNALLING_TEST=('steeplechase', 'steeplechase', '.', True),
XPCSHELL_TESTS=('xpcshell', 'xpcshell', '.', True),
)
# Reftests have their own manifest format and are processed separately.
REFTEST_FLAVORS = ('crashtest', 'reftest')
# Web platform tests have their own manifest format and are processed separately.
WEB_PATFORM_TESTS_FLAVORS = ('web-platform-tests',)
def all_test_flavors():
return ([v[0] for v in TEST_MANIFESTS.values()] +
list(REFTEST_FLAVORS) +
list(WEB_PATFORM_TESTS_FLAVORS))