Bug 1414399 - [mozbuild/moztest] Move mozbuild.testing.TestResolver to moztest.resolve r=gps
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 06 Nov 2017 08:41:42 -0500
changeset 390811 1ede5a47bb9877a7bd01eb4303d65b8462f1d32c
parent 390810 2a52e1d4c300aa71652652c4349d4e06c74c9dc7
child 390812 3170c6d82e89453a2301a8a8f594df2254db014e
push id97124
push userarchaeopteryx@coole-files.de
push dateWed, 08 Nov 2017 22:33:09 +0000
treeherdermozilla-inbound@7d799a93ed72 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgps
bugs1414399
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1414399 - [mozbuild/moztest] Move mozbuild.testing.TestResolver to moztest.resolve r=gps The TestMetadata and TestResolver classes aren't technically part of the build system. The only connection is that they consume some build system output. The next patch in this series is going to be merging in a bunch of other test resolving logic from other parts of the tree. Moving this out first allows us to keep that extra logic out of mozbuild. MozReview-Commit-ID: 1eq4SjFVCyW
python/mach_commands.py
python/mozbuild/mozbuild/test/python.ini
python/mozbuild/mozbuild/test/test_testing.py
python/mozbuild/mozbuild/testing.py
testing/mach_commands.py
testing/mochitest/mach_commands.py
testing/mozbase/moztest/moztest/resolve.py
testing/mozbase/moztest/tests/manifest.ini
testing/mozbase/moztest/tests/test_resolve.py
tools/tryselect/mach_commands.py
--- a/python/mach_commands.py
+++ b/python/mach_commands.py
@@ -107,17 +107,17 @@ class MachCommands(MachCommandBase):
 
         # Python's unittest, and in particular discover, has problems with
         # clashing namespaces when importing multiple test modules. What follows
         # is a simple way to keep environments separate, at the price of
         # launching Python multiple times. Most tests are run via mozunit,
         # which produces output in the format Mozilla infrastructure expects.
         # Some tests are run via pytest.
         if test_objects is None:
-            from mozbuild.testing import TestResolver
+            from moztest.resolve import TestResolver
             resolver = self._spawn(TestResolver)
             if tests:
                 # If we were given test paths, try to find tests matching them.
                 test_objects = resolver.resolve_tests(paths=tests,
                                                       flavor='python')
             else:
                 # Otherwise just run everything in PYTHON_UNITTEST_MANIFESTS
                 test_objects = resolver.resolve_tests(flavor='python')
--- a/python/mozbuild/mozbuild/test/python.ini
+++ b/python/mozbuild/mozbuild/test/python.ini
@@ -35,10 +35,9 @@
 [test_expression.py]
 [test_jarmaker.py]
 [test_line_endings.py]
 [test_makeutil.py]
 [test_mozconfig.py]
 [test_mozinfo.py]
 [test_preprocessor.py]
 [test_pythonutil.py]
-[test_testing.py]
 [test_util.py]
--- a/python/mozbuild/mozbuild/testing.py
+++ b/python/mozbuild/mozbuild/testing.py
@@ -8,261 +8,18 @@ import cPickle as pickle
 import os
 import sys
 
 import mozpack.path as mozpath
 
 from mozpack.copier import FileCopier
 from mozpack.manifests import InstallManifest
 
-from .base import MozbuildObject
-from .util import OrderedDefaultDict
-from collections import defaultdict
-
 import manifestparser
 
-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, all_tests, test_defaults=None):
-        self._tests_by_path = OrderedDefaultDict(list)
-        self._tests_by_flavor = defaultdict(set)
-        self._test_dirs = set()
-
-        with open(all_tests, 'rb') as fh:
-            test_data = pickle.load(fh)
-        defaults = None
-        if test_defaults:
-            with open(test_defaults, 'rb') as fh:
-                defaults = pickle.load(fh)
-        for path, tests in test_data.items():
-            for metadata in tests:
-                if defaults:
-                    defaults_manifests = [metadata['manifest']]
-
-                    ancestor_manifest = metadata.get('ancestor-manifest')
-                    if ancestor_manifest:
-                        defaults_manifests.append(ancestor_manifest)
-
-                    for manifest in defaults_manifests:
-                        manifest_defaults = defaults.get(manifest)
-                        if manifest_defaults:
-                            metadata = manifestparser.combine_fields(manifest_defaults,
-                                                                     metadata)
-                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)
-
-        # If installing tests is going to result in re-generating the build
-        # backend, we need to do this here, so that the updated contents of
-        # all-tests.pkl make it to the set of tests to run.
-        self._run_make(
-            target='backend.TestManifestBackend', pass_thru=True, print_directory=False,
-            filename=mozpath.join(self.topsrcdir, 'build', 'rebuild-backend.mk'),
-            append_env={
-                b'PYTHON': self.virtualenv_manager.python_path,
-                b'BUILD_BACKEND_FILES': b'backend.TestManifestBackend',
-                b'BACKEND_GENERATION_SCRIPT': mozpath.join(
-                    self.topsrcdir, 'build', 'gen_test_backend.py'),
-            },
-        )
-
-        self._tests = TestMetadata(os.path.join(self.topobjdir,
-                                                'all-tests.pkl'),
-                                   test_defaults=os.path.join(self.topobjdir,
-                                                              'test-defaults.pkl'))
-
-        self._test_rewrites = {
-            'a11y': os.path.join(self.topobjdir, '_tests', 'testing',
-                'mochitest', 'a11y'),
-            'browser-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
-                'mochitest', 'browser'),
-            'chrome': os.path.join(self.topobjdir, '_tests', 'testing',
-                'mochitest', 'chrome'),
-            'mochitest': os.path.join(self.topobjdir, '_tests', 'testing',
-                'mochitest', 'tests'),
-            '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
--- a/testing/mach_commands.py
+++ b/testing/mach_commands.py
@@ -219,17 +219,17 @@ class Test(MachCommandBase):
         When paths or directories are given, they are first resolved to test
         files known to the build system.
 
         If resolved tests belong to more than one test type/flavor/harness,
         the harness for each relevant type/flavor will be invoked. e.g. if
         you specify a directory with xpcshell and browser chrome mochitests,
         both harnesses will be invoked.
         """
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
 
         # Parse arguments and assemble a test "plan."
         run_suites = set()
         run_tests = []
         resolver = self._spawn(TestResolver)
 
         for entry in what:
             # If the path matches the name or alias of an entire suite, run
@@ -512,17 +512,17 @@ class CramTest(MachCommandBase):
                      help="Extra arguments to pass down to the cram binary. See "
                           "'./mach python -m cram -- -h' for a list of available options.")
     def cramtest(self, cram_args=None, test_paths=None, test_objects=None):
         self._activate_virtualenv()
         import mozinfo
         from manifestparser import TestManifest
 
         if test_objects is None:
-            from mozbuild.testing import TestResolver
+            from moztest.resolve import TestResolver
             resolver = self._spawn(TestResolver)
             if test_paths:
                 # If we were given test paths, try to find tests matching them.
                 test_objects = resolver.resolve_tests(paths=test_paths, flavor='cram')
             else:
                 # Otherwise just run everything in CRAMTEST_MANIFESTS
                 test_objects = resolver.resolve_tests(flavor='cram')
 
@@ -622,17 +622,17 @@ class ChunkFinder(MachCommandBase):
              description='Find which chunk a test belongs to (works for mochitest).',
              parser=get_parser)
     def chunk_finder(self, **kwargs):
         total_chunks = kwargs['total_chunks']
         test_path = kwargs['test_path'][0]
         suite_name = kwargs['suite_name'][0]
         _, dump_tests = tempfile.mkstemp()
 
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
         resolver = self._spawn(TestResolver)
         relpath = self._wrap_path_argument(test_path).relpath()
         tests = list(resolver.resolve_tests(paths=[relpath]))
         if len(tests) != 1:
             print('No test found for test_path: %s' % test_path)
             sys.exit(1)
 
         flavor = tests[0]['flavor']
@@ -824,17 +824,17 @@ class TestInfoCommand(MachCommandBase):
         if self.full_test_name:
             self.full_test_name.replace(os.sep, posixpath.sep)
             print("Found %s in source control." % self.full_test_name)
         else:
             print("Unable to validate test name '%s'!" % self.test_name)
             self.full_test_name = self.test_name
 
         # search for full_test_name in test manifests
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
         resolver = self._spawn(TestResolver)
         relpath = self._wrap_path_argument(self.full_test_name).relpath()
         tests = list(resolver.resolve_tests(paths=[relpath]))
         if len(tests) == 1:
             relpath = self._wrap_path_argument(tests[0]['manifest']).relpath()
             print("%s found in manifest %s" % (self.full_test_name, relpath))
             if tests[0].get('flavor'):
                 print("  flavor: %s" % tests[0]['flavor'])
--- a/testing/mochitest/mach_commands.py
+++ b/testing/mochitest/mach_commands.py
@@ -102,17 +102,17 @@ class MochitestRunner(MozbuildObject):
             'testing',
             'mochitest')
         self.bin_dir = os.path.join(self.topobjdir, 'dist', 'bin')
 
     def resolve_tests(self, test_paths, test_objects=None, cwd=None):
         if test_objects:
             return test_objects
 
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
         resolver = self._spawn(TestResolver)
         tests = list(resolver.resolve_tests(paths=test_paths, cwd=cwd))
         return tests
 
     def run_desktop_test(self, context, tests=None, suite=None, **kwargs):
         """Runs a mochitest.
 
         suite is the type of mochitest to run. It can be one of ('plain',
@@ -442,17 +442,17 @@ class RobocopCommands(MachCommandBase):
                                                 'mochitest', 'robocop.ini')
 
         from mozbuild.controller.building import BuildDriver
         self._ensure_state_subdir_exists('.')
 
         test_paths = kwargs['test_paths']
         kwargs['test_paths'] = []
 
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
         resolver = self._spawn(TestResolver)
         tests = list(resolver.resolve_tests(paths=test_paths, cwd=self._mach_context.cwd,
                                             flavor='instrumentation', subsuite='robocop'))
         driver = self._spawn(BuildDriver)
         driver.install_tests(tests)
 
         if len(tests) < 1:
             print(ROBOCOP_TESTS_NOT_FOUND.format('\n'.join(
new file mode 100644
--- /dev/null
+++ b/testing/mozbase/moztest/moztest/resolve.py
@@ -0,0 +1,254 @@
+# 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 cPickle as pickle
+import os
+from collections import defaultdict
+
+import manifestparser
+import mozpack.path as mozpath
+from mozbuild.base import MozbuildObject
+from mozbuild.util import OrderedDefaultDict
+
+
+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, all_tests, test_defaults=None):
+        self._tests_by_path = OrderedDefaultDict(list)
+        self._tests_by_flavor = defaultdict(set)
+        self._test_dirs = set()
+
+        with open(all_tests, 'rb') as fh:
+            test_data = pickle.load(fh)
+        defaults = None
+        if test_defaults:
+            with open(test_defaults, 'rb') as fh:
+                defaults = pickle.load(fh)
+        for path, tests in test_data.items():
+            for metadata in tests:
+                if defaults:
+                    defaults_manifests = [metadata['manifest']]
+
+                    ancestor_manifest = metadata.get('ancestor-manifest')
+                    if ancestor_manifest:
+                        defaults_manifests.append(ancestor_manifest)
+
+                    for manifest in defaults_manifests:
+                        manifest_defaults = defaults.get(manifest)
+                        if manifest_defaults:
+                            metadata = manifestparser.combine_fields(manifest_defaults,
+                                                                     metadata)
+                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':
+                        continue
+                    if 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)
+
+        # If installing tests is going to result in re-generating the build
+        # backend, we need to do this here, so that the updated contents of
+        # all-tests.pkl make it to the set of tests to run.
+        self._run_make(
+            target='backend.TestManifestBackend', pass_thru=True, print_directory=False,
+            filename=mozpath.join(self.topsrcdir, 'build', 'rebuild-backend.mk'),
+            append_env={
+                b'PYTHON': self.virtualenv_manager.python_path,
+                b'BUILD_BACKEND_FILES': b'backend.TestManifestBackend',
+                b'BACKEND_GENERATION_SCRIPT': mozpath.join(
+                    self.topsrcdir, 'build', 'gen_test_backend.py'),
+            },
+        )
+
+        self._tests = TestMetadata(os.path.join(self.topobjdir,
+                                                'all-tests.pkl'),
+                                   test_defaults=os.path.join(self.topobjdir,
+                                                              'test-defaults.pkl'))
+
+        self._test_rewrites = {
+            'a11y': os.path.join(self.topobjdir, '_tests', 'testing',
+                                 'mochitest', 'a11y'),
+            'browser-chrome': os.path.join(self.topobjdir, '_tests', 'testing',
+                                           'mochitest', 'browser'),
+            'chrome': os.path.join(self.topobjdir, '_tests', 'testing',
+                                   'mochitest', 'chrome'),
+            'mochitest': os.path.join(self.topobjdir, '_tests', 'testing',
+                                      'mochitest', 'tests'),
+            '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
--- a/testing/mozbase/moztest/tests/manifest.ini
+++ b/testing/mozbase/moztest/tests/manifest.ini
@@ -1,3 +1,4 @@
 [DEFAULT]
 subsuite = mozbase, os == "linux"
 [test.py]
+[test_resolve.py]
rename from python/mozbuild/mozbuild/test/test_testing.py
rename to testing/mozbase/moztest/tests/test_resolve.py
--- a/python/mozbuild/mozbuild/test/test_testing.py
+++ b/testing/mozbase/moztest/tests/test_resolve.py
@@ -1,164 +1,165 @@
 # 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/.
+# flake8: noqa: E501
 
-from __future__ import unicode_literals
+from __future__ import absolute_import, unicode_literals
 
 import cPickle as pickle
 import os
 import shutil
 import tempfile
 import unittest
 
 import mozpack.path as mozpath
-
+import mozunit
+import pytest
+from mozbuild.base import MozbuildObject
 from mozfile import NamedTemporaryFile
-from mozunit import main
 
-from mozbuild.base import MozbuildObject
-from mozbuild.testing import (
+from moztest.resolve import (
     TestMetadata,
     TestResolver,
 )
 
 
 ALL_TESTS = {
     "accessible/tests/mochitest/actions/test_anchors.html": [
         {
             "dir_relpath": "accessible/tests/mochitest/actions",
             "expected": "pass",
             "file_relpath": "accessible/tests/mochitest/actions/test_anchors.html",
             "flavor": "a11y",
-            "here": "/Users/gps/src/firefox/accessible/tests/mochitest/actions",
-            "manifest": "/Users/gps/src/firefox/accessible/tests/mochitest/actions/a11y.ini",
+            "here": "/firefox/accessible/tests/mochitest/actions",
+            "manifest": "/firefox/accessible/tests/mochitest/actions/a11y.ini",
             "name": "test_anchors.html",
-            "path": "/Users/gps/src/firefox/accessible/tests/mochitest/actions/test_anchors.html",
+            "path": "/firefox/accessible/tests/mochitest/actions/test_anchors.html",
             "relpath": "test_anchors.html"
         }
     ],
     "services/common/tests/unit/test_async_chain.js": [
         {
             "dir_relpath": "services/common/tests/unit",
             "file_relpath": "services/common/tests/unit/test_async_chain.js",
             "firefox-appdir": "browser",
             "flavor": "xpcshell",
             "head": "head_global.js head_helpers.js head_http.js",
-            "here": "/Users/gps/src/firefox/services/common/tests/unit",
-            "manifest": "/Users/gps/src/firefox/services/common/tests/unit/xpcshell.ini",
+            "here": "/firefox/services/common/tests/unit",
+            "manifest": "/firefox/services/common/tests/unit/xpcshell.ini",
             "name": "test_async_chain.js",
-            "path": "/Users/gps/src/firefox/services/common/tests/unit/test_async_chain.js",
+            "path": "/firefox/services/common/tests/unit/test_async_chain.js",
             "relpath": "test_async_chain.js",
         }
     ],
     "services/common/tests/unit/test_async_querySpinningly.js": [
         {
             "dir_relpath": "services/common/tests/unit",
             "file_relpath": "services/common/tests/unit/test_async_querySpinningly.js",
             "firefox-appdir": "browser",
             "flavor": "xpcshell",
             "head": "head_global.js head_helpers.js head_http.js",
-            "here": "/Users/gps/src/firefox/services/common/tests/unit",
-            "manifest": "/Users/gps/src/firefox/services/common/tests/unit/xpcshell.ini",
+            "here": "/firefox/services/common/tests/unit",
+            "manifest": "/firefox/services/common/tests/unit/xpcshell.ini",
             "name": "test_async_querySpinningly.js",
-            "path": "/Users/gps/src/firefox/services/common/tests/unit/test_async_querySpinningly.js",
+            "path": "/firefox/services/common/tests/unit/test_async_querySpinningly.js",
             "relpath": "test_async_querySpinningly.js",
         }
     ],
-   "toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js": [
+    "toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js": [
         {
             "dir_relpath": "toolkit/mozapps/update/test/unit",
             "file_relpath": "toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
             "flavor": "xpcshell",
             "generated-files": "head_update.js",
             "head": "head_update.js",
-            "here": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit",
-            "manifest": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini",
+            "here": "/firefox/toolkit/mozapps/update/test/unit",
+            "manifest": "/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini",
             "name": "test_0201_app_launch_apply_update.js",
-            "path": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
+            "path": "/firefox/toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
             "reason": "bug 820380",
             "relpath": "test_0201_app_launch_apply_update.js",
             "run-sequentially": "Launches application.",
             "skip-if": "os == 'android'",
         },
         {
             "dir_relpath": "toolkit/mozapps/update/test/unit",
             "file_relpath": "toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
             "flavor": "xpcshell",
             "generated-files": "head_update.js",
             "head": "head_update.js head2.js",
-            "here": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit",
-            "manifest": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini",
+            "here": "/firefox/toolkit/mozapps/update/test/unit",
+            "manifest": "/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini",
             "name": "test_0201_app_launch_apply_update.js",
-            "path": "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
+            "path": "/firefox/toolkit/mozapps/update/test/unit/test_0201_app_launch_apply_update.js",
             "reason": "bug 820380",
             "relpath": "test_0201_app_launch_apply_update.js",
             "run-sequentially": "Launches application.",
             "skip-if": "os == 'android'",
         }
     ],
     "mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java": [
         {
             "dir_relpath": "mobile/android/tests/background/junit3/src/common",
             "file_relpath": "mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java",
             "flavor": "instrumentation",
-            "here": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/background/junit3",
-            "manifest": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/background/junit3/instrumentation.ini",
+            "here": "/firefox/mobile/android/tests/background/junit3",
+            "manifest": "/firefox/mobile/android/tests/background/junit3/instrumentation.ini",
             "name": "src/common/TestAndroidLogWriters.java",
-            "path": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java",
+            "path": "/firefox/mobile/android/tests/background/junit3/src/common/TestAndroidLogWriters.java",
             "relpath": "src/common/TestAndroidLogWriters.java",
             "subsuite": "background"
         }
     ],
     "mobile/android/tests/browser/junit3/src/TestDistribution.java": [
         {
             "dir_relpath": "mobile/android/tests/browser/junit3/src",
             "file_relpath": "mobile/android/tests/browser/junit3/src/TestDistribution.java",
             "flavor": "instrumentation",
-            "here": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/browser/junit3",
-            "manifest": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/browser/junit3/instrumentation.ini",
+            "here": "/firefox/mobile/android/tests/browser/junit3",
+            "manifest": "/firefox/mobile/android/tests/browser/junit3/instrumentation.ini",
             "name": "src/TestDistribution.java",
-            "path": "/Users/nalexander/Mozilla/gecko-dev/mobile/android/tests/browser/junit3/src/TestDistribution.java",
+            "path": "/firefox/mobile/android/tests/browser/junit3/src/TestDistribution.java",
             "relpath": "src/TestDistribution.java",
             "subsuite": "browser"
         }
     ],
     "image/test/browser/browser_bug666317.js": [
         {
             "dir_relpath": "image/test/browser",
             "file_relpath": "image/test/browser/browser_bug666317.js",
             "flavor": "browser-chrome",
-            "here": "/home/chris/m-c/obj-dbg/_tests/testing/mochitest/browser/image/test/browser",
-            "manifest": "/home/chris/m-c/image/test/browser/browser.ini",
+            "here": "/firefox/testing/mochitest/browser/image/test/browser",
+            "manifest": "/firefox/image/test/browser/browser.ini",
             "name": "browser_bug666317.js",
-            "path": "/home/chris/m-c/obj-dbg/_tests/testing/mochitest/browser/image/test/browser/browser_bug666317.js",
+            "path": "/firefox/testing/mochitest/browser/image/test/browser/browser_bug666317.js",
             "relpath": "image/test/browser/browser_bug666317.js",
             "skip-if": "e10s # Bug 948194 - Decoded Images seem to not be discarded on memory-pressure notification with e10s enabled",
             "subsuite": ""
         }
    ],
    "devtools/client/markupview/test/browser_markupview_copy_image_data.js": [
         {
             "dir_relpath": "devtools/client/markupview/test",
             "file_relpath": "devtools/client/markupview/test/browser_markupview_copy_image_data.js",
             "flavor": "browser-chrome",
-            "here": "/home/chris/m-c/obj-dbg/_tests/testing/mochitest/browser/devtools/client/markupview/test",
-            "manifest": "/home/chris/m-c/devtools/client/markupview/test/browser.ini",
+            "here": "/firefox/testing/mochitest/browser/devtools/client/markupview/test",
+            "manifest": "/firefox/devtools/client/markupview/test/browser.ini",
             "name": "browser_markupview_copy_image_data.js",
-            "path": "/home/chris/m-c/obj-dbg/_tests/testing/mochitest/browser/devtools/client/markupview/test/browser_markupview_copy_image_data.js",
+            "path": "/firefox/testing/mochitest/browser/devtools/client/markupview/test/browser_markupview_copy_image_data.js",
             "relpath": "devtools/client/markupview/test/browser_markupview_copy_image_data.js",
             "subsuite": "devtools",
             "tags": "devtools"
         }
    ]
 }
 
 TEST_DEFAULTS = {
-    "/Users/gps/src/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini": {"support-files": "\ndata/**\nxpcshell_updater.ini"}
+    "/firefox/toolkit/mozapps/update/test/unit/xpcshell_updater.ini": {"support-files": "\ndata/**\nxpcshell_updater.ini"}
 }
 
 
 class Base(unittest.TestCase):
     def setUp(self):
         self._temp_files = []
 
     def tearDown(self):
@@ -225,17 +226,17 @@ class TestTestMetadata(Base):
 
     def test_resolve_path_prefix(self):
         t = self._get_test_metadata()
         result = list(t.resolve_tests(paths=['image']))
         self.assertEqual(len(result), 1)
 
 
 class TestTestResolver(Base):
-    FAKE_TOPSRCDIR = '/Users/gps/src/firefox'
+    FAKE_TOPSRCDIR = '/firefox'
 
     def setUp(self):
         Base.setUp(self)
 
         self._temp_dirs = []
 
     def tearDown(self):
         Base.tearDown(self)
@@ -320,9 +321,9 @@ class TestTestResolver(Base):
         tests = list(r.resolve_tests(paths=['**/**.js', 'accessible/**']))
         self.assertEqual(len(tests), 7)
         for t in tests:
             path = t['file_relpath']
             self.assertTrue(path.startswith('accessible') or path.endswith('.js'))
 
 
 if __name__ == '__main__':
-    main()
+    mozunit.main()
--- a/tools/tryselect/mach_commands.py
+++ b/tools/tryselect/mach_commands.py
@@ -200,17 +200,17 @@ class TrySelect(MachCommandBase):
         scheduled. If no platform is selected a default is taken from
         the AUTOTRY_PLATFORM_HINT environment variable, if set.
 
         The command requires either its own mercurial extension ("push-to-try",
         installable from mach mercurial-setup) or a git repo using git-cinnabar
         (available at https://github.com/glandium/git-cinnabar).
 
         """
-        from mozbuild.testing import TestResolver
+        from moztest.resolve import TestResolver
         from tryselect.selectors.syntax import AutoTry
 
         try:
             if self.substs.get("MOZ_ARTIFACT_BUILDS"):
                 kwargs['local_artifact_build'] = True
         except BuildEnvironmentNotFoundException:
             # If we don't have a build locally, we can't tell whether
             # an artifact build is desired, but we still want the