Bug 1320194 - Refactor test manifest related frontend code into a TestManifestEmitter, r?gps draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Mon, 28 Nov 2016 10:36:25 -0500
changeset 447153 0a7349433d7ae70618942a125bb482ac230a8f01
parent 447152 900cbbf1d0e9b55b1dd9b29cd96541d8f47b25af
child 447154 694d7ea56dff3398c19805513f314fe0cd0f74bd
push id38007
push userahalberstadt@mozilla.com
push dateFri, 02 Dec 2016 22:10:48 +0000
reviewersgps
bugs1320194
milestone53.0a1
Bug 1320194 - Refactor test manifest related frontend code into a TestManifestEmitter, r?gps TestManifestEmitter will only emit TestManifest objects. This will allow test resolvers to only process the metadata that is relevant to them without wasting computation. TreeMetadataEmitter will still emit everything that TestManifestEmitter does because the RecursiveMakeBackend still needs to consume TestManifest objects to create root test manifests and install manifests. MozReview-Commit-ID: 5HtvpViYiue
python/mozbuild/mozbuild/frontend/emitter.py
python/mozbuild/mozbuild/test/frontend/test_emitter.py
--- a/python/mozbuild/mozbuild/frontend/emitter.py
+++ b/python/mozbuild/mozbuild/frontend/emitter.py
@@ -2,32 +2,34 @@
 # 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 itertools
 import logging
 import os
-import traceback
 import sys
 import time
+import traceback
+from abc import ABCMeta, abstractmethod
+from collections import defaultdict, OrderedDict
 
-from collections import defaultdict, OrderedDict
+import mozinfo
+import pytoml
 from mach.mixin.logging import LoggingMixin
-from mozbuild.util import (
-    memoize,
-    OrderedDefaultDict,
+
+import mozpack.path as mozpath
+from mozpack.chrome.manifest import (
+    ManifestBinaryComponent,
+    Manifest,
 )
 
-import mozpack.path as mozpath
-import mozinfo
-import pytoml
-
-from .data import (
+from mozbuild.base import ExecutionSummary
+from mozbuild.frontend.data import (
     AndroidAssetsDirs,
     AndroidExtraPackages,
     AndroidExtraResDirs,
     AndroidResDirs,
     BaseSources,
     BrandingFiles,
     ChromeManifestEntry,
     ConfigFileSubstitution,
@@ -65,74 +67,321 @@ from .data import (
     RustLibrary,
     RustProgram,
     SdkFiles,
     SharedLibrary,
     SimpleProgram,
     Sources,
     StaticLibrary,
     TestHarnessFiles,
+    TestManifest,
     TestWebIDLFile,
-    TestManifest,
     UnifiedSources,
     VariablePassthru,
     WebIDLFile,
     XPIDLFile,
 )
-from mozpack.chrome.manifest import (
-    ManifestBinaryComponent,
-    Manifest,
-)
 
-from .reader import SandboxValidationError
-
-from ..testing import (
-    TEST_MANIFESTS,
-    REFTEST_FLAVORS,
-    WEB_PLATFORM_TESTS_FLAVORS,
-    SupportFilesConverter,
-)
-
-from .context import (
+from mozbuild.frontend.reader import SandboxValidationError
+from mozbuild.frontend.context import (
     Context,
     SourcePath,
     ObjDirPath,
     Path,
     SubContext,
     TemplateContext,
 )
 
-from mozbuild.base import ExecutionSummary
+from mozbuild.testing import (
+    TEST_MANIFESTS,
+    REFTEST_FLAVORS,
+    WEB_PLATFORM_TESTS_FLAVORS,
+    SupportFilesConverter
+)
+
+from mozbuild.util import (
+    memoize,
+    OrderedDefaultDict,
+)
 
 
 ALLOWED_XPCOM_GLUE = {
     ('xpcshell', 'js/xpconnect/shell'),
     ('testcrasher', 'toolkit/crashreporter/test'),
     ('mediaconduit_unittests', 'media/webrtc/signaling/test'),
     ('mediapipeline_unittest', 'media/webrtc/signaling/test'),
     ('signaling_unittests', 'media/webrtc/signaling/test'),
     ('TestMailCookie', 'mailnews/base/test'),
     ('calbasecomps', 'calendar/base/backend/libical/build'),
     ('purplexpcom', 'extensions/purple/purplexpcom/src'),
     ('ipdlunittest', 'ipc/ipdl/test/cxx/app'),
 }
 
 
-class TreeMetadataEmitter(LoggingMixin):
+class BaseEmitter(LoggingMixin):
+    """Base emitter class for consuming moz.build metadata.
+
+    Subclasses should implement emit_from_context."""
+    __metaclass__ = ABCMeta
+
+    def __init__(self, config):
+        self.populate_logger()
+        self.config = config
+
+        self._emitter_time = 0.0
+        self._object_count = 0
+        self._contexts = {}
+
+    @abstractmethod
+    def emit_from_context(self, context):
+        pass
+
+    def summary(self):
+        return ExecutionSummary(
+            'Processed into {object_count:d} build config descriptors in '
+            '{execution_time:.2f}s',
+            execution_time=self._emitter_time,
+            object_count=self._object_count)
+
+    def _emit_objs(self, objs):
+        for o in objs:
+            self._object_count += 1
+            yield o
+
+    def emit(self, output):
+        """Convert the BuildReader output into data structures.
+
+        The return value from BuildReader.read_topsrcdir() (a generator) is
+        typically fed into this function.
+        """
+        for out in output:
+            # Nothing in sub-contexts is currently of interest to us. Filter
+            # them all out.
+            if isinstance(out, SubContext):
+                continue
+
+            if isinstance(out, Context):
+                # Keep all contexts around, we will need them later.
+                self._contexts[out.objdir] = out
+
+                start = time.time()
+                # We need to expand the generator for the timings to work.
+                objs = list(self.emit_from_context(out))
+                self._emitter_time += time.time() - start
+
+                for o in self._emit_objs(objs): yield o
+
+            else:
+                raise Exception('Unhandled output type: %s' % type(out))
+
+
+class TestManifestEmitter(BaseEmitter):
+    """Emitter that creates test manifest data structures.
+
+    All other in-tree metadata derived from moz.build is ignored.
+    """
+
+    def __init__(self, config):
+        BaseEmitter.__init__(self, config)
+        self._test_files_converter = SupportFilesConverter()
+
+    def emit_from_context(self, context):
+        for prefix, info in TEST_MANIFESTS.items():
+            for path, manifest in context.get('%s_MANIFESTS' % prefix, []):
+                for obj in self._process_test_manifest(context, info, path, manifest):
+                    yield obj
+
+        for flavor in REFTEST_FLAVORS:
+            for path, manifest in context.get('%s_MANIFESTS' % flavor.upper(), []):
+                for obj in self._process_reftest_manifest(context, flavor, path, manifest):
+                    yield obj
+
+        for flavor in WEB_PLATFORM_TESTS_FLAVORS:
+            for path, manifest in context.get("%s_MANIFESTS" % flavor.upper().replace('-', '_'), []):
+                for obj in self._process_web_platform_tests_manifest(context, path, manifest):
+                    yield obj
+
+    def _process_test_manifest(self, context, info, manifest_path, mpmanifest):
+        flavor, install_root, install_subdir, package_tests = info
+
+        path = mozpath.normpath(mozpath.join(context.srcdir, manifest_path))
+        manifest_dir = mozpath.dirname(path)
+        manifest_reldir = mozpath.dirname(mozpath.relpath(path,
+            context.config.topsrcdir))
+        manifest_sources = [mozpath.relpath(pth, context.config.topsrcdir)
+                            for pth in mpmanifest.source_files]
+        install_prefix = mozpath.join(install_root, install_subdir)
+
+        try:
+            if not mpmanifest.tests:
+                raise SandboxValidationError('Empty test manifest: %s'
+                    % path, context)
+
+            defaults = mpmanifest.manifest_defaults[os.path.normpath(path)]
+            obj = TestManifest(context, path, mpmanifest, flavor=flavor,
+                install_prefix=install_prefix,
+                relpath=mozpath.join(manifest_reldir, mozpath.basename(path)),
+                sources=manifest_sources,
+                dupe_manifest='dupe-manifest' in defaults)
+
+            filtered = mpmanifest.tests
+
+            # Jetpack add-on tests are expected to be generated during the
+            # build process so they won't exist here.
+            if flavor != 'jetpack-addon':
+                missing = [t['name'] for t in filtered if not os.path.exists(t['path'])]
+                if missing:
+                    raise SandboxValidationError('Test manifest (%s) lists '
+                        'test that does not exist: %s' % (
+                        path, ', '.join(missing)), context)
+
+            out_dir = mozpath.join(install_prefix, manifest_reldir)
+            if 'install-to-subdir' in defaults:
+                # This is terrible, but what are you going to do?
+                out_dir = mozpath.join(out_dir, defaults['install-to-subdir'])
+                obj.manifest_obj_relpath = mozpath.join(manifest_reldir,
+                                                        defaults['install-to-subdir'],
+                                                        mozpath.basename(path))
+
+            def process_support_files(test):
+                install_info = self._test_files_converter.convert_support_files(
+                    test, install_root, manifest_dir, out_dir)
+
+                obj.pattern_installs.extend(install_info.pattern_installs)
+                for source, dest in install_info.installs:
+                    obj.installs[source] = (dest, False)
+                obj.external_installs |= install_info.external_installs
+                for install_path in install_info.deferred_installs:
+                    if all(['*' not in install_path,
+                            not os.path.isfile(mozpath.join(context.config.topsrcdir,
+                                                            install_path[2:])),
+                            install_path not in install_info.external_installs]):
+                        raise SandboxValidationError('Error processing test '
+                           'manifest %s: entry in support-files not present '
+                           'in the srcdir: %s' % (path, install_path), context)
+
+                obj.deferred_installs |= install_info.deferred_installs
+
+            for test in filtered:
+                obj.tests.append(test)
+
+                # Some test files are compiled and should not be copied into the
+                # test package. They function as identifiers rather than files.
+                if package_tests:
+                    manifest_relpath = mozpath.relpath(test['path'],
+                        mozpath.dirname(test['manifest']))
+                    obj.installs[mozpath.normpath(test['path'])] = \
+                        ((mozpath.join(out_dir, manifest_relpath)), True)
+
+                process_support_files(test)
+
+            for path, m_defaults in mpmanifest.manifest_defaults.items():
+                process_support_files(m_defaults)
+
+            # We also copy manifests into the output directory,
+            # including manifests from [include:foo] directives.
+            for mpath in mpmanifest.manifests():
+                mpath = mozpath.normpath(mpath)
+                out_path = mozpath.join(out_dir, mozpath.basename(mpath))
+                obj.installs[mpath] = (out_path, False)
+
+            # Some manifests reference files that are auto generated as
+            # part of the build or shouldn't be installed for some
+            # reason. Here, we prune those files from the install set.
+            # FUTURE we should be able to detect autogenerated files from
+            # other build metadata. Once we do that, we can get rid of this.
+            for f in defaults.get('generated-files', '').split():
+                # We re-raise otherwise the stack trace isn't informative.
+                try:
+                    del obj.installs[mozpath.join(manifest_dir, f)]
+                except KeyError:
+                    raise SandboxValidationError('Error processing test '
+                        'manifest %s: entry in generated-files not present '
+                        'elsewhere in manifest: %s' % (path, f), context)
+
+            yield obj
+        except (AssertionError, Exception):
+            raise SandboxValidationError('Error processing test '
+                'manifest file %s: %s' % (path,
+                    '\n'.join(traceback.format_exception(*sys.exc_info()))),
+                context)
+
+    def _process_reftest_manifest(self, context, flavor, manifest_path, manifest):
+        manifest_full_path = mozpath.normpath(mozpath.join(
+            context.srcdir, manifest_path))
+        manifest_reldir = mozpath.dirname(mozpath.relpath(manifest_full_path,
+            context.config.topsrcdir))
+
+        # reftest manifests don't come from manifest parser. But they are
+        # similar enough that we can use the same emitted objects. Note
+        # that we don't perform any installs for reftests.
+        obj = TestManifest(context, manifest_full_path, manifest,
+                flavor=flavor, install_prefix='%s/' % flavor,
+                relpath=mozpath.join(manifest_reldir,
+                    mozpath.basename(manifest_path)))
+
+        for test, source_manifest in sorted(manifest.tests):
+            obj.tests.append({
+                'path': test,
+                'here': mozpath.dirname(test),
+                'manifest': source_manifest,
+                'name': mozpath.basename(test),
+                'head': '',
+                'tail': '',
+                'support-files': '',
+                'subsuite': '',
+            })
+
+        yield obj
+
+    def _process_web_platform_tests_manifest(self, context, paths, manifest):
+        manifest_path, tests_root = paths
+        manifest_full_path = mozpath.normpath(mozpath.join(
+            context.srcdir, manifest_path))
+        manifest_reldir = mozpath.dirname(mozpath.relpath(manifest_full_path,
+            context.config.topsrcdir))
+        tests_root = mozpath.normpath(mozpath.join(context.srcdir, tests_root))
+
+        # Create a equivalent TestManifest object
+        obj = TestManifest(context, manifest_full_path, manifest,
+                           flavor="web-platform-tests",
+                           relpath=mozpath.join(manifest_reldir,
+                                                mozpath.basename(manifest_path)),
+                           install_prefix="web-platform/")
+
+        for path, tests in manifest:
+            path = mozpath.join(tests_root, path)
+            for test in tests:
+                if test.item_type not in ["testharness", "reftest"]:
+                    continue
+
+                obj.tests.append({
+                    'path': path,
+                    'here': mozpath.dirname(path),
+                    'manifest': manifest_path,
+                    'name': test.id,
+                    'head': '',
+                    'tail': '',
+                    'support-files': '',
+                    'subsuite': '',
+                })
+
+        yield obj
+
+
+class TreeMetadataEmitter(BaseEmitter):
     """Converts the executed mozbuild files into data structures.
 
     This is a bridge between reader.py and data.py. It takes what was read by
     reader.BuildReader and converts it into the classes defined in the data
-    module.
+    module. It will emit all metadata, including TestManifest objects.
     """
 
     def __init__(self, config):
-        self.populate_logger()
-
-        self.config = config
+        BaseEmitter.__init__(self, config)
 
         mozinfo.find_and_update_from_json(config.topobjdir)
 
         # Python 2.6 doesn't allow unicode keys to be used for keyword
         # arguments. This gross hack works around the problem until we
         # rid ourselves of 2.6.
         self.info = {}
         for k, v in mozinfo.info.items():
@@ -151,67 +400,32 @@ class TreeMetadataEmitter(LoggingMixin):
         # from what we run a subconfigure in. We'll eliminate some directories
         # as we traverse them with moz.build (e.g. js/src).
         subconfigures = os.path.join(self.config.topobjdir, 'subconfigures')
         paths = []
         if os.path.exists(subconfigures):
             paths = open(subconfigures).read().splitlines()
         self._external_paths = set(mozpath.normsep(d) for d in paths)
 
-        self._emitter_time = 0.0
-        self._object_count = 0
-        self._test_files_converter = SupportFilesConverter()
-
-    def summary(self):
-        return ExecutionSummary(
-            'Processed into {object_count:d} build config descriptors in '
-            '{execution_time:.2f}s',
-            execution_time=self._emitter_time,
-            object_count=self._object_count)
+        # The RecursiveMakeBackend still consumes TestManifest objects in
+        # order to generate root test manifests and install manifests, so we
+        # still need to emit them here.
+        self._test_emitter = TestManifestEmitter(self.config)
 
     def emit(self, output):
-        """Convert the BuildReader output into data structures.
-
-        The return value from BuildReader.read_topsrcdir() (a generator) is
-        typically fed into this function.
-        """
-        contexts = {}
-
-        def emit_objs(objs):
-            for o in objs:
-                self._object_count += 1
-                yield o
-
-        for out in output:
-            # Nothing in sub-contexts is currently of interest to us. Filter
-            # them all out.
-            if isinstance(out, SubContext):
-                continue
-
-            if isinstance(out, Context):
-                # Keep all contexts around, we will need them later.
-                contexts[out.objdir] = out
-
-                start = time.time()
-                # We need to expand the generator for the timings to work.
-                objs = list(self.emit_from_context(out))
-                self._emitter_time += time.time() - start
-
-                for o in emit_objs(objs): yield o
-
-            else:
-                raise Exception('Unhandled output type: %s' % type(out))
+        for o in BaseEmitter.emit(self, output):
+            yield o
 
         # Don't emit Linkable objects when COMPILE_ENVIRONMENT is not set
         if self.config.substs.get('COMPILE_ENVIRONMENT'):
             start = time.time()
-            objs = list(self._emit_libs_derived(contexts))
+            objs = list(self._emit_libs_derived(self._contexts))
             self._emitter_time += time.time() - start
 
-            for o in emit_objs(objs): yield o
+            for o in self._emit_objs(objs): yield o
 
     def _emit_libs_derived(self, contexts):
         # First do FINAL_LIBRARY linkage.
         for lib in (l for libs in self._libs.values() for l in libs):
             if not isinstance(lib, (StaticLibrary, RustLibrary)) or not lib.link_into:
                 continue
             if lib.link_into not in self._libs:
                 raise SandboxValidationError(
@@ -1122,18 +1336,17 @@ class TreeMetadataEmitter(LoggingMixin):
                                          'https://developer.mozilla.org/en/XPCOM/XPCOM_changes_in_Gecko_2.0 .',
                                          context);
 
         for c in components:
             if c.endswith('.manifest'):
                 yield ChromeManifestEntry(context, 'chrome.manifest',
                                           Manifest('components',
                                                    mozpath.basename(c)))
-
-        for obj in self._process_test_manifests(context):
+        for obj in self._test_emitter.emit_from_context(context):
             yield obj
 
         for obj in self._process_jar_manifests(context):
             yield obj
 
         for name, jar in context.get('JAVA_JAR_TARGETS', {}).items():
             yield ContextWrapped(context, jar)
 
@@ -1244,199 +1457,16 @@ class TreeMetadataEmitter(LoggingMixin):
                             % (f, p.full_path), context)
                     inputs.append(p)
             else:
                 script = None
                 method = None
             yield GeneratedFile(context, script, method, outputs, inputs,
                                 flags.flags)
 
-    def _process_test_manifests(self, context):
-        for prefix, info in TEST_MANIFESTS.items():
-            for path, manifest in context.get('%s_MANIFESTS' % prefix, []):
-                for obj in self._process_test_manifest(context, info, path, manifest):
-                    yield obj
-
-        for flavor in REFTEST_FLAVORS:
-            for path, manifest in context.get('%s_MANIFESTS' % flavor.upper(), []):
-                for obj in self._process_reftest_manifest(context, flavor, path, manifest):
-                    yield obj
-
-        for flavor in WEB_PLATFORM_TESTS_FLAVORS:
-            for path, manifest in context.get("%s_MANIFESTS" % flavor.upper().replace('-', '_'), []):
-                for obj in self._process_web_platform_tests_manifest(context, path, manifest):
-                    yield obj
-
-    def _process_test_manifest(self, context, info, manifest_path, mpmanifest):
-        flavor, install_root, install_subdir, package_tests = info
-
-        path = mozpath.normpath(mozpath.join(context.srcdir, manifest_path))
-        manifest_dir = mozpath.dirname(path)
-        manifest_reldir = mozpath.dirname(mozpath.relpath(path,
-            context.config.topsrcdir))
-        manifest_sources = [mozpath.relpath(pth, context.config.topsrcdir)
-                            for pth in mpmanifest.source_files]
-        install_prefix = mozpath.join(install_root, install_subdir)
-
-        try:
-            if not mpmanifest.tests:
-                raise SandboxValidationError('Empty test manifest: %s'
-                    % path, context)
-
-            defaults = mpmanifest.manifest_defaults[os.path.normpath(path)]
-            obj = TestManifest(context, path, mpmanifest, flavor=flavor,
-                install_prefix=install_prefix,
-                relpath=mozpath.join(manifest_reldir, mozpath.basename(path)),
-                sources=manifest_sources,
-                dupe_manifest='dupe-manifest' in defaults)
-
-            filtered = mpmanifest.tests
-
-            # Jetpack add-on tests are expected to be generated during the
-            # build process so they won't exist here.
-            if flavor != 'jetpack-addon':
-                missing = [t['name'] for t in filtered if not os.path.exists(t['path'])]
-                if missing:
-                    raise SandboxValidationError('Test manifest (%s) lists '
-                        'test that does not exist: %s' % (
-                        path, ', '.join(missing)), context)
-
-            out_dir = mozpath.join(install_prefix, manifest_reldir)
-            if 'install-to-subdir' in defaults:
-                # This is terrible, but what are you going to do?
-                out_dir = mozpath.join(out_dir, defaults['install-to-subdir'])
-                obj.manifest_obj_relpath = mozpath.join(manifest_reldir,
-                                                        defaults['install-to-subdir'],
-                                                        mozpath.basename(path))
-
-            def process_support_files(test):
-                install_info = self._test_files_converter.convert_support_files(
-                    test, install_root, manifest_dir, out_dir)
-
-                obj.pattern_installs.extend(install_info.pattern_installs)
-                for source, dest in install_info.installs:
-                    obj.installs[source] = (dest, False)
-                obj.external_installs |= install_info.external_installs
-                for install_path in install_info.deferred_installs:
-                    if all(['*' not in install_path,
-                            not os.path.isfile(mozpath.join(context.config.topsrcdir,
-                                                            install_path[2:])),
-                            install_path not in install_info.external_installs]):
-                        raise SandboxValidationError('Error processing test '
-                           'manifest %s: entry in support-files not present '
-                           'in the srcdir: %s' % (path, install_path), context)
-
-                obj.deferred_installs |= install_info.deferred_installs
-
-            for test in filtered:
-                obj.tests.append(test)
-
-                # Some test files are compiled and should not be copied into the
-                # test package. They function as identifiers rather than files.
-                if package_tests:
-                    manifest_relpath = mozpath.relpath(test['path'],
-                        mozpath.dirname(test['manifest']))
-                    obj.installs[mozpath.normpath(test['path'])] = \
-                        ((mozpath.join(out_dir, manifest_relpath)), True)
-
-                process_support_files(test)
-
-            for path, m_defaults in mpmanifest.manifest_defaults.items():
-                process_support_files(m_defaults)
-
-            # We also copy manifests into the output directory,
-            # including manifests from [include:foo] directives.
-            for mpath in mpmanifest.manifests():
-                mpath = mozpath.normpath(mpath)
-                out_path = mozpath.join(out_dir, mozpath.basename(mpath))
-                obj.installs[mpath] = (out_path, False)
-
-            # Some manifests reference files that are auto generated as
-            # part of the build or shouldn't be installed for some
-            # reason. Here, we prune those files from the install set.
-            # FUTURE we should be able to detect autogenerated files from
-            # other build metadata. Once we do that, we can get rid of this.
-            for f in defaults.get('generated-files', '').split():
-                # We re-raise otherwise the stack trace isn't informative.
-                try:
-                    del obj.installs[mozpath.join(manifest_dir, f)]
-                except KeyError:
-                    raise SandboxValidationError('Error processing test '
-                        'manifest %s: entry in generated-files not present '
-                        'elsewhere in manifest: %s' % (path, f), context)
-
-            yield obj
-        except (AssertionError, Exception):
-            raise SandboxValidationError('Error processing test '
-                'manifest file %s: %s' % (path,
-                    '\n'.join(traceback.format_exception(*sys.exc_info()))),
-                context)
-
-    def _process_reftest_manifest(self, context, flavor, manifest_path, manifest):
-        manifest_full_path = mozpath.normpath(mozpath.join(
-            context.srcdir, manifest_path))
-        manifest_reldir = mozpath.dirname(mozpath.relpath(manifest_full_path,
-            context.config.topsrcdir))
-
-        # reftest manifests don't come from manifest parser. But they are
-        # similar enough that we can use the same emitted objects. Note
-        # that we don't perform any installs for reftests.
-        obj = TestManifest(context, manifest_full_path, manifest,
-                flavor=flavor, install_prefix='%s/' % flavor,
-                relpath=mozpath.join(manifest_reldir,
-                    mozpath.basename(manifest_path)))
-
-        for test, source_manifest in sorted(manifest.tests):
-            obj.tests.append({
-                'path': test,
-                'here': mozpath.dirname(test),
-                'manifest': source_manifest,
-                'name': mozpath.basename(test),
-                'head': '',
-                'tail': '',
-                'support-files': '',
-                'subsuite': '',
-            })
-
-        yield obj
-
-    def _process_web_platform_tests_manifest(self, context, paths, manifest):
-        manifest_path, tests_root = paths
-        manifest_full_path = mozpath.normpath(mozpath.join(
-            context.srcdir, manifest_path))
-        manifest_reldir = mozpath.dirname(mozpath.relpath(manifest_full_path,
-            context.config.topsrcdir))
-        tests_root = mozpath.normpath(mozpath.join(context.srcdir, tests_root))
-
-        # Create a equivalent TestManifest object
-        obj = TestManifest(context, manifest_full_path, manifest,
-                           flavor="web-platform-tests",
-                           relpath=mozpath.join(manifest_reldir,
-                                                mozpath.basename(manifest_path)),
-                           install_prefix="web-platform/")
-
-
-        for path, tests in manifest:
-            path = mozpath.join(tests_root, path)
-            for test in tests:
-                if test.item_type not in ["testharness", "reftest"]:
-                    continue
-
-                obj.tests.append({
-                    'path': path,
-                    'here': mozpath.dirname(path),
-                    'manifest': manifest_path,
-                    'name': test.id,
-                    'head': '',
-                    'tail': '',
-                    'support-files': '',
-                    'subsuite': '',
-                })
-
-        yield obj
 
     def _process_jar_manifests(self, context):
         jar_manifests = context.get('JAR_MANIFESTS', [])
         if len(jar_manifests) > 1:
             raise SandboxValidationError('While JAR_MANIFESTS is a list, '
                 'it is currently limited to one value.', context)
 
         for path in jar_manifests:
--- a/python/mozbuild/mozbuild/test/frontend/test_emitter.py
+++ b/python/mozbuild/mozbuild/test/frontend/test_emitter.py
@@ -41,17 +41,17 @@ from mozbuild.frontend.data import (
     SimpleProgram,
     Sources,
     StaticLibrary,
     TestHarnessFiles,
     TestManifest,
     UnifiedSources,
     VariablePassthru,
 )
-from mozbuild.frontend.emitter import TreeMetadataEmitter
+from mozbuild.frontend.emitter import TreeMetadataEmitter, TestManifestEmitter
 from mozbuild.frontend.reader import (
     BuildReader,
     BuildReaderError,
     SandboxValidationError,
 )
 from mozpack.chrome import manifest
 
 from mozbuild.test.common import MockConfig
@@ -80,18 +80,19 @@ class TestEmitterBasic(unittest.TestCase
             COMPILE_ENVIRONMENT='1',
         )
         if extra_substs:
             substs.update(extra_substs)
         config = MockConfig(mozpath.join(data_path, name), extra_substs=substs)
 
         return BuildReader(config)
 
-    def read_topsrcdir(self, reader, filter_common=True):
-        emitter = TreeMetadataEmitter(reader.config)
+    def read_topsrcdir(self, reader, filter_common=True, cls=None):
+        cls = cls or TreeMetadataEmitter
+        emitter = cls(reader.config)
         objs = list(emitter.emit(reader.read_topsrcdir()))
         self.assertGreater(len(objs), 0)
 
         filtered = []
         for obj in objs:
             if filter_common and isinstance(obj, DirectoryTraversal):
                 continue
 
@@ -413,48 +414,48 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEqual(objs[1].program, 'test_program1.prog')
         self.assertEqual(objs[2].program, 'test_program2.prog')
 
     def test_test_manifest_missing_manifest(self):
         """A missing manifest file should result in an error."""
         reader = self.reader('test-manifest-missing-manifest')
 
         with self.assertRaisesRegexp(BuildReaderError, 'IOError: Missing files'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_empty_test_manifest_rejected(self):
         """A test manifest without any entries is rejected."""
         reader = self.reader('test-manifest-empty')
 
         with self.assertRaisesRegexp(SandboxValidationError, 'Empty test manifest'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
 
     def test_test_manifest_just_support_files(self):
         """A test manifest with no tests but support-files is not supported."""
         reader = self.reader('test-manifest-just-support')
 
         with self.assertRaisesRegexp(SandboxValidationError, 'Empty test manifest'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_test_manifest_dupe_support_files(self):
         """A test manifest with dupe support-files in a single test is not
         supported.
         """
         reader = self.reader('test-manifest-dupes')
 
         with self.assertRaisesRegexp(SandboxValidationError, 'bar.js appears multiple times '
             'in a test manifest under a support-files field, please omit the duplicate entry.'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_test_manifest_absolute_support_files(self):
         """Support files starting with '/' are placed relative to the install root"""
         reader = self.reader('test-manifest-absolute-support')
 
-        objs = self.read_topsrcdir(reader)
+        objs = self.read_topsrcdir(reader, cls=TestManifestEmitter)
         self.assertEqual(len(objs), 1)
         o = objs[0]
         self.assertEqual(len(o.installs), 3)
         expected = [
             mozpath.normpath(mozpath.join(o.install_prefix, "../.well-known/foo.txt")),
             mozpath.join(o.install_prefix, "absolute-support.ini"),
             mozpath.join(o.install_prefix, "test_file.js"),
         ]
@@ -462,17 +463,17 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEqual(paths, expected)
 
     @unittest.skip('Bug 1304316 - Items in the second set but not the first')
     def test_test_manifest_shared_support_files(self):
         """Support files starting with '!' are given separate treatment, so their
         installation can be resolved when running tests.
         """
         reader = self.reader('test-manifest-shared-support')
-        supported, child = self.read_topsrcdir(reader)
+        supported, child = self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
         expected_deferred_installs = {
             '!/child/test_sub.js',
             '!/child/another-file.sjs',
             '!/child/data/**',
         }
 
         self.assertEqual(len(supported.installs), 3)
@@ -482,23 +483,23 @@ class TestEmitterBasic(unittest.TestCase
         self.assertEqual(len(child.pattern_installs), 1)
 
     def test_test_manifest_deffered_install_missing(self):
         """A non-existent shared support file reference produces an error."""
         reader = self.reader('test-manifest-shared-missing')
 
         with self.assertRaisesRegexp(SandboxValidationError,
                                      'entry in support-files not present in the srcdir'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_test_manifest_install_to_subdir(self):
         """ """
         reader = self.reader('test-manifest-install-subdir')
 
-        objs = self.read_topsrcdir(reader)
+        objs = self.read_topsrcdir(reader, cls=TestManifestEmitter)
         self.assertEqual(len(objs), 1)
         o = objs[0]
         self.assertEqual(len(o.installs), 3)
         self.assertEqual(o.manifest_relpath, "subdir.ini")
         self.assertEqual(o.manifest_obj_relpath, "subdir/subdir.ini")
         expected = [
             mozpath.normpath(mozpath.join(o.install_prefix, "subdir/subdir.ini")),
             mozpath.normpath(mozpath.join(o.install_prefix, "subdir/support.txt")),
@@ -506,17 +507,17 @@ class TestEmitterBasic(unittest.TestCase
         ]
         paths = sorted([v[0] for v in o.installs.values()])
         self.assertEqual(paths, expected)
 
     def test_test_manifest_install_includes(self):
         """Ensure that any [include:foo.ini] are copied to the objdir."""
         reader = self.reader('test-manifest-install-includes')
 
-        objs = self.read_topsrcdir(reader)
+        objs = self.read_topsrcdir(reader, cls=TestManifestEmitter)
         self.assertEqual(len(objs), 1)
         o = objs[0]
         self.assertEqual(len(o.installs), 3)
         self.assertEqual(o.manifest_relpath, "mochitest.ini")
         self.assertEqual(o.manifest_obj_relpath, "subdir/mochitest.ini")
         expected = [
             mozpath.normpath(mozpath.join(o.install_prefix, "subdir/common.ini")),
             mozpath.normpath(mozpath.join(o.install_prefix, "subdir/mochitest.ini")),
@@ -524,34 +525,34 @@ class TestEmitterBasic(unittest.TestCase
         ]
         paths = sorted([v[0] for v in o.installs.values()])
         self.assertEqual(paths, expected)
 
     def test_test_manifest_includes(self):
         """Ensure that manifest objects from the emitter list a correct manifest.
         """
         reader = self.reader('test-manifest-emitted-includes')
-        [obj] = self.read_topsrcdir(reader)
+        [obj] = self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
         # Expected manifest leafs for our tests.
         expected_manifests = {
             'reftest1.html': 'reftest.list',
             'reftest1-ref.html': 'reftest.list',
             'reftest2.html': 'included-reftest.list',
             'reftest2-ref.html': 'included-reftest.list',
         }
 
         for t in obj.tests:
             self.assertTrue(t['manifest'].endswith(expected_manifests[t['name']]))
 
     def test_test_manifest_keys_extracted(self):
         """Ensure all metadata from test manifests is extracted."""
         reader = self.reader('test-manifest-keys-extracted')
 
-        objs = [o for o in self.read_topsrcdir(reader)
+        objs = [o for o in self.read_topsrcdir(reader, cls=TestManifestEmitter)
                 if isinstance(o, TestManifest)]
 
         self.assertEqual(len(objs), 9)
 
         metadata = {
             'a11y.ini': {
                 'flavor': 'a11y',
                 'installs': {
@@ -644,23 +645,23 @@ class TestEmitterBasic(unittest.TestCase
             if 'pattern-installs' in m:
                 self.assertEqual(len(o.pattern_installs), m['pattern-installs'])
 
     def test_test_manifest_unmatched_generated(self):
         reader = self.reader('test-manifest-unmatched-generated')
 
         with self.assertRaisesRegexp(SandboxValidationError,
             'entry in generated-files not present elsewhere'):
-            self.read_topsrcdir(reader),
+            self.read_topsrcdir(reader, cls=TestManifestEmitter),
 
     def test_test_manifest_parent_support_files_dir(self):
         """support-files referencing a file in a parent directory works."""
         reader = self.reader('test-manifest-parent-support-files-dir')
 
-        objs = [o for o in self.read_topsrcdir(reader)
+        objs = [o for o in self.read_topsrcdir(reader, cls=TestManifestEmitter)
                 if isinstance(o, TestManifest)]
 
         self.assertEqual(len(objs), 1)
 
         o = objs[0]
 
         expected = mozpath.join(o.srcdir, 'support-file.txt')
         self.assertIn(expected, o.installs)
@@ -668,25 +669,25 @@ class TestEmitterBasic(unittest.TestCase
             ('testing/mochitest/tests/child/support-file.txt', False))
 
     def test_test_manifest_missing_test_error(self):
         """Missing test files should result in error."""
         reader = self.reader('test-manifest-missing-test-file')
 
         with self.assertRaisesRegexp(SandboxValidationError,
             'lists test that does not exist: test_missing.html'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_test_manifest_missing_test_error_unfiltered(self):
         """Missing test files should result in error, even when the test list is not filtered."""
         reader = self.reader('test-manifest-missing-test-file-unfiltered')
 
         with self.assertRaisesRegexp(SandboxValidationError,
             'lists test that does not exist: missing.js'):
-            self.read_topsrcdir(reader)
+            self.read_topsrcdir(reader, cls=TestManifestEmitter)
 
     def test_ipdl_sources(self):
         reader = self.reader('ipdl_sources')
         objs = self.read_topsrcdir(reader)
 
         ipdls = []
         for o in objs:
             if isinstance(o, IPDLFile):