Exemplo n.º 1
0
 def test_commonprefix(self):
     self.assertEqual(
         commonprefix(
             [os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'foo/baz/qux']),
         'foo/')
     self.assertEqual(
         commonprefix(
             [os.path.join('foo', 'bar', 'baz'), 'foo/qux', 'baz/qux']), '')
Exemplo n.º 2
0
 def test_commonprefix(self):
     self.assertEqual(
         commonprefix([
             self.SEP.join(("foo", "bar", "baz")), "foo/qux", "foo/baz/qux"
         ]),
         "foo/",
     )
     self.assertEqual(
         commonprefix(
             [self.SEP.join(("foo", "bar", "baz")), "foo/qux", "baz/qux"]),
         "",
     )
Exemplo n.º 3
0
    def __call__(self, result):
        paths = set(result.issues.keys() + result.suppressed_warnings.keys())

        commonprefix = mozpath.commonprefix(
            [mozpath.abspath(p) for p in paths])
        commonprefix = commonprefix.rsplit('/', 1)[0] + '/'

        summary = defaultdict(lambda: [0, 0])
        for path in paths:
            abspath = mozpath.abspath(path)
            assert abspath.startswith(commonprefix)

            if abspath != commonprefix:
                parts = mozpath.split(mozpath.relpath(
                    abspath, commonprefix))[:self.depth]
                abspath = mozpath.join(commonprefix, *parts)

            summary[abspath][0] += len(
                [r for r in result.issues[path] if r.level == 'error'])
            summary[abspath][1] += len(
                [r for r in result.issues[path] if r.level == 'warning'])
            summary[abspath][1] += result.suppressed_warnings[path]

        msg = []
        for path, (errors, warnings) in sorted(summary.items()):
            warning_str = ", {}".format(pluralize(
                'warning', warnings)) if warnings else ''
            msg.append('{}: {}{}'.format(path, pluralize('error', errors),
                                         warning_str))
        return '\n'.join(msg)
Exemplo n.º 4
0
def find_shell_scripts(config, paths):
    found = dict()

    root = config["root"]
    exclude = [mozpath.join(root, e) for e in config.get("exclude", [])]

    if config.get("extensions"):
        pattern = "**/*.{}".format(config.get("extensions")[0])
    else:
        pattern = "**/*.sh"

    files = []
    for path in paths:
        path = mozpath.normsep(path)
        ignore = [
            e[len(path):].lstrip("/") for e in exclude
            if mozpath.commonprefix((path, e)) == path
        ]
        finder = FileFinder(path, ignore=ignore)
        files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])

    for filename in files:
        shell = determine_shell_from_script(filename)
        if shell:
            found[filename] = shell
    return found
Exemplo n.º 5
0
    def __call__(self, result):
        paths = set(
            list(result.issues.keys()) +
            list(result.suppressed_warnings.keys()))

        commonprefix = mozpath.commonprefix(
            [mozpath.abspath(p) for p in paths])
        commonprefix = commonprefix.rsplit("/", 1)[0] + "/"

        summary = defaultdict(lambda: [0, 0])
        for path in paths:
            abspath = mozpath.abspath(path)
            assert abspath.startswith(commonprefix)

            if abspath != commonprefix:
                parts = mozpath.split(mozpath.relpath(
                    abspath, commonprefix))[:self.depth]
                abspath = mozpath.join(commonprefix, *parts)

            summary[abspath][0] += len(
                [r for r in result.issues[path] if r.level == "error"])
            summary[abspath][1] += len(
                [r for r in result.issues[path] if r.level == "warning"])
            summary[abspath][1] += result.suppressed_warnings[path]

        msg = []
        for path, (errors, warnings) in sorted(summary.items()):
            warning_str = (", {}".format(pluralize("warning", warnings))
                           if warnings else "")
            msg.append("{}: {}{}".format(path, pluralize("error", errors),
                                         warning_str))
        return "\n".join(msg)
Exemplo n.º 6
0
def collapse(paths, base=None, dotfiles=False):
    """Given an iterable of paths, collapse them into the smallest possible set
    of paths that contain the original set (without containing any extra paths).

    For example, if directory 'a' contains two files b.txt and c.txt, calling:

        collapse(['a/b.txt', 'a/c.txt'])

    returns ['a']. But if a third file d.txt also exists, then it will return
    ['a/b.txt', 'a/c.txt'] since ['a'] would also include that extra file.

    :param paths: An iterable of paths (files and directories) to collapse.
    :returns: The smallest set of paths (files and directories) that contain
              the original set of paths and only the original set.
    """
    if not paths:
        if not base:
            return []

        # Need to test whether directory chain is empty. If it is then bubble
        # the base back up so that it counts as 'covered'.
        for _, _, names in os.walk(base):
            if names:
                return []
        return [base]

    if not base:
        paths = list(map(mozpath.abspath, paths))
        base = mozpath.commonprefix(paths)

        if not os.path.isdir(base):
            base = os.path.dirname(base)

    if base in paths:
        return [base]

    covered = set()
    full = set()
    for name in os.listdir(base):
        if not dotfiles and name[0] == '.':
            continue

        path = mozpath.join(base, name)
        full.add(path)

        if path in paths:
            # This path was explicitly specified, so just bubble it back up
            # without recursing down into it (if it was a directory).
            covered.add(path)
        elif os.path.isdir(path):
            new_paths = [p for p in paths if p.startswith(path)]
            covered.update(collapse(new_paths, base=path, dotfiles=dotfiles))

    if full == covered:
        # Every file under this base was covered, so we can collapse them all
        # up into the base path.
        return [base]
    return list(covered)
Exemplo n.º 7
0
 def close(self, auto_root_manifest=True):
     '''
     Add possibly missing bits and push all instructions to the formatter.
     '''
     if auto_root_manifest:
         # Simple package manifests don't contain the root manifests, so
         # find and add them.
         paths = [mozpath.dirname(m) for m in self._manifests]
         path = mozpath.dirname(mozpath.commonprefix(paths))
         for p, f in self._finder.find(mozpath.join(path,
                                                    'chrome.manifest')):
             if p not in self._manifests:
                 self.packager.add(SimpleManifestSink.normalize_path(p), f)
     self.packager.close()
Exemplo n.º 8
0
 def close(self, auto_root_manifest=True):
     '''
     Add possibly missing bits and push all instructions to the formatter.
     '''
     if auto_root_manifest:
         # Simple package manifests don't contain the root manifests, so
         # find and add them.
         paths = [mozpath.dirname(m) for m in self._manifests]
         path = mozpath.dirname(mozpath.commonprefix(paths))
         for p, f in self._finder.find(mozpath.join(path,
                                       'chrome.manifest')):
             if not p in self._manifests:
                 self.packager.add(SimpleManifestSink.normalize_path(p), f)
     self.packager.close()
Exemplo n.º 9
0
def expand_exclusions(paths, config, root):
    """Returns all files that match patterns and aren't excluded.

    This is used by some external linters who receive 'batch' files (e.g dirs)
    but aren't capable of applying their own exclusions. There is an argument
    to be made that this step should just apply to all linters no matter what.

    Args:
        paths (list): List of candidate paths to lint.
        config (dict): Linter's config object.
        root (str): Root of the repository.

    Returns:
        Generator which generates list of paths that weren't excluded.
    """
    extensions = [e.lstrip(".") for e in config.get("extensions", [])]
    find_dotfiles = config.get("find-dotfiles", False)

    def normalize(path):
        path = mozpath.normpath(path)
        if os.path.isabs(path):
            return path
        return mozpath.join(root, path)

    exclude = list(map(normalize, config.get("exclude", [])))
    for path in paths:
        path = mozpath.normsep(path)
        if os.path.isfile(path):
            if any(path.startswith(e) for e in exclude if "*" not in e):
                continue

            if any(mozpath.match(path, e) for e in exclude if "*" in e):
                continue

            yield path
            continue

        ignore = [
            e[len(path):].lstrip("/") for e in exclude
            if mozpath.commonprefix((path, e)) == path
        ]
        finder = FileFinder(path, ignore=ignore, find_dotfiles=find_dotfiles)

        _, ext = os.path.splitext(path)
        ext.lstrip(".")

        for ext in extensions:
            for p, f in finder.find("**/*.{}".format(ext)):
                yield os.path.join(path, p)
Exemplo n.º 10
0
def run_linter(python, paths, config, **lintargs):
    binary = find_executable(python)
    if not binary:
        # TODO bootstrap python3 if not available
        print(
            'error: {} not detected, aborting py-compat check'.format(python))
        if 'MOZ_AUTOMATION' in os.environ:
            return 1
        return []

    root = lintargs['root']
    pattern = "**/*.py"
    exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])]
    files = []
    for path in paths:
        path = mozpath.normsep(path)
        if os.path.isfile(path):
            files.append(path)
            continue

        ignore = [
            e[len(path):].lstrip('/') for e in exclude
            if mozpath.commonprefix((path, e)) == path
        ]
        finder = FileFinder(path, ignore=ignore)
        files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])

    with tempfile.NamedTemporaryFile(mode='w') as fh:
        fh.write('\n'.join(files))
        fh.flush()

        cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name]

        proc = PyCompatProcess(config, cmd)
        proc.run()
        try:
            proc.wait()
        except KeyboardInterrupt:
            proc.kill()

    return results
    def __call__(self, result, **kwargs):
        commonprefix = mozpath.commonprefix(
            [mozpath.abspath(p) for p in result])
        commonprefix = commonprefix.rsplit('/', 1)[0] + '/'

        summary = defaultdict(int)
        for path, errors in result.iteritems():
            path = mozpath.abspath(path)
            assert path.startswith(commonprefix)

            if path == commonprefix:
                summary[path] += len(errors)
                continue

            parts = mozpath.split(mozpath.relpath(path,
                                                  commonprefix))[:self.depth]
            path = mozpath.join(commonprefix, *parts)
            summary[path] += len(errors)

        return '\n'.join(
            ['{}: {}'.format(k, summary[k]) for k in sorted(summary)])
def run_linter(python, paths, config, **lintargs):
    binary = find_executable(python)
    if not binary:
        # If we're in automation, this is fatal. Otherwise, the warning in the
        # setup method was already printed.
        if 'MOZ_AUTOMATION' in os.environ:
            return 1
        return []

    root = lintargs['root']
    pattern = "**/*.py"
    exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])]
    files = []
    for path in paths:
        path = mozpath.normsep(path)
        if os.path.isfile(path):
            files.append(path)
            continue

        ignore = [
            e[len(path):].lstrip('/') for e in exclude
            if mozpath.commonprefix((path, e)) == path
        ]
        finder = FileFinder(path, ignore=ignore)
        files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])

    with mozfile.NamedTemporaryFile(mode='w') as fh:
        fh.write('\n'.join(files))
        fh.flush()

        cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name]

        proc = PyCompatProcess(config, cmd)
        proc.run()
        try:
            proc.wait()
        except KeyboardInterrupt:
            proc.kill()

    return results
Exemplo n.º 13
0
def run_linter(python, paths, config, **lintargs):
    binary = find_executable(python)
    if not binary:
        # TODO bootstrap python3 if not available
        print('error: {} not detected, aborting py-compat check'.format(python))
        if 'MOZ_AUTOMATION' in os.environ:
            return 1
        return []

    root = lintargs['root']
    pattern = "**/*.py"
    exclude = [mozpath.join(root, e) for e in lintargs.get('exclude', [])]
    files = []
    for path in paths:
        path = mozpath.normsep(path)
        if os.path.isfile(path):
            files.append(path)
            continue

        ignore = [e[len(path):].lstrip('/') for e in exclude
                  if mozpath.commonprefix((path, e)) == path]
        finder = FileFinder(path, ignore=ignore)
        files.extend([os.path.join(path, p) for p, f in finder.find(pattern)])

    with tempfile.NamedTemporaryFile(mode='w') as fh:
        fh.write('\n'.join(files))
        fh.flush()

        cmd = [binary, os.path.join(here, 'check_compat.py'), fh.name]

        proc = PyCompatProcess(config, cmd)
        proc.run()
        try:
            proc.wait()
        except KeyboardInterrupt:
            proc.kill()

    return results
Exemplo n.º 14
0
 def test_commonprefix(self):
     self.assertEqual(commonprefix([os.path.join('foo', 'bar', 'baz'),
                                    'foo/qux', 'foo/baz/qux']), 'foo/')
     self.assertEqual(commonprefix([os.path.join('foo', 'bar', 'baz'),
                                    'foo/qux', 'baz/qux']), '')
Exemplo n.º 15
0
 def test_commonprefix(self):
     self.assertEqual(commonprefix([os.path.join("foo", "bar", "baz"), "foo/qux", "foo/baz/qux"]), "foo/")
     self.assertEqual(commonprefix([os.path.join("foo", "bar", "baz"), "foo/qux", "baz/qux"]), "")
Exemplo n.º 16
0
 def test_commonprefix(self):
     self.assertEqual(commonprefix([self.SEP.join(('foo', 'bar', 'baz')),
                                    'foo/qux', 'foo/baz/qux']), 'foo/')
     self.assertEqual(commonprefix([self.SEP.join(('foo', 'bar', 'baz')),
                                    'foo/qux', 'baz/qux']), '')
Exemplo n.º 17
0
    def report(self, components, flavor, subsuite, paths,
               show_manifests, show_tests, show_summary, show_annotations,
               show_activedata,
               filter_values, filter_keys, show_components, output_file,
               branches, days):

        def matches_filters(test):
            '''
               Return True if all of the requested filter_values are found in this test;
               if filter_keys are specified, restrict search to those test keys.
            '''
            for value in filter_values:
                value_found = False
                for key in test:
                    if not filter_keys or key in filter_keys:
                        if re.search(value, test[key]):
                            value_found = True
                            break
                if not value_found:
                    return False
            return True

        start_time = datetime.datetime.now()

        # Ensure useful report by default
        if not show_manifests and not show_tests and not show_summary and not show_annotations:
            show_manifests = True
            show_summary = True

        by_component = {}
        if components:
            components = components.split(',')
        if filter_keys:
            filter_keys = filter_keys.split(',')
        if filter_values:
            filter_values = filter_values.split(',')
        else:
            filter_values = []
        display_keys = (filter_keys or []) + ['skip-if', 'fail-if', 'fails-if']
        display_keys = set(display_keys)

        print("Finding tests...")
        here = os.path.abspath(os.path.dirname(__file__))
        resolver = TestResolver.from_environment(cwd=here, loader_cls=TestManifestLoader)
        tests = list(resolver.resolve_tests(paths=paths, flavor=flavor,
                                            subsuite=subsuite))

        manifest_paths = set()
        for t in tests:
            if 'manifest' in t and t['manifest'] is not None:
                manifest_paths.add(t['manifest'])
        manifest_count = len(manifest_paths)
        print("Resolver found {} tests, {} manifests".format(len(tests), manifest_count))

        if show_manifests:
            topsrcdir = self.build_obj.topsrcdir
            by_component['manifests'] = {}
            manifest_paths = list(manifest_paths)
            manifest_paths.sort()
            relpaths = []
            for manifest_path in manifest_paths:
                relpath = mozpath.relpath(manifest_path, topsrcdir)
                if mozpath.commonprefix((manifest_path, topsrcdir)) != topsrcdir:
                    continue
                relpaths.append(relpath)
            reader = self.build_obj.mozbuild_reader(config_mode='empty')
            files_info = reader.files_info(relpaths)
            for manifest_path in manifest_paths:
                relpath = mozpath.relpath(manifest_path, topsrcdir)
                if mozpath.commonprefix((manifest_path, topsrcdir)) != topsrcdir:
                    continue
                manifest_info = None
                if relpath in files_info:
                    bug_component = files_info[relpath].get('BUG_COMPONENT')
                    if bug_component:
                        key = "{}::{}".format(bug_component.product, bug_component.component)
                    else:
                        key = "<unknown bug component>"
                    if (not components) or (key in components):
                        manifest_info = {
                            'manifest': relpath,
                            'tests': 0,
                            'skipped': 0
                        }
                        rkey = key if show_components else 'all'
                        if rkey in by_component['manifests']:
                            by_component['manifests'][rkey].append(manifest_info)
                        else:
                            by_component['manifests'][rkey] = [manifest_info]
                if manifest_info:
                    for t in tests:
                        if t['manifest'] == manifest_path:
                            manifest_info['tests'] += 1
                            if t.get('skip-if'):
                                manifest_info['skipped'] += 1
            for key in by_component['manifests']:
                by_component['manifests'][key].sort()

        if show_tests:
            by_component['tests'] = {}

        if show_tests or show_summary or show_annotations:
            test_count = 0
            failed_count = 0
            skipped_count = 0
            annotation_count = 0
            condition_count = 0
            component_set = set()
            relpaths = []
            conditions = {}
            known_unconditional_annotations = ['skip', 'fail', 'asserts', 'random']
            known_conditional_annotations = ['skip-if', 'fail-if', 'run-if',
                                             'fails-if', 'fuzzy-if', 'random-if', 'asserts-if']
            for t in tests:
                relpath = t.get('srcdir_relpath')
                relpaths.append(relpath)
            reader = self.build_obj.mozbuild_reader(config_mode='empty')
            files_info = reader.files_info(relpaths)
            for t in tests:
                if not matches_filters(t):
                    continue
                if 'referenced-test' in t:
                    # Avoid double-counting reftests: disregard reference file entries
                    continue
                if show_annotations:
                    for key in t:
                        if key in known_unconditional_annotations:
                            annotation_count += 1
                        if key in known_conditional_annotations:
                            annotation_count += 1
                            # Here 'key' is a manifest annotation type like 'skip-if' and t[key]
                            # is the associated condition. For example, the manifestparser
                            # manifest annotation, "skip-if = os == 'win'", is expected to be
                            # encoded as t['skip-if'] = "os == 'win'".
                            # To allow for reftest manifests, t[key] may have multiple entries
                            # separated by ';', each corresponding to a condition for that test
                            # and annotation type. For example,
                            # "skip-if(Android&&webrender) skip-if(OSX)", would be
                            # encoded as t['skip-if'] = "Android&&webrender;OSX".
                            annotation_conditions = t[key].split(';')
                            for condition in annotation_conditions:
                                condition_count += 1
                                # Trim reftest fuzzy-if ranges: everything after the first comma
                                # eg. "Android,0-2,1-3" -> "Android"
                                condition = condition.split(',')[0]
                                if condition not in conditions:
                                    conditions[condition] = 0
                                conditions[condition] += 1
                test_count += 1
                relpath = t.get('srcdir_relpath')
                if relpath in files_info:
                    bug_component = files_info[relpath].get('BUG_COMPONENT')
                    if bug_component:
                        key = "{}::{}".format(bug_component.product, bug_component.component)
                    else:
                        key = "<unknown bug component>"
                    if (not components) or (key in components):
                        component_set.add(key)
                        test_info = {'test': relpath}
                        for test_key in display_keys:
                            value = t.get(test_key)
                            if value:
                                test_info[test_key] = value
                        if t.get('fail-if'):
                            failed_count += 1
                        if t.get('fails-if'):
                            failed_count += 1
                        if t.get('skip-if'):
                            skipped_count += 1
                        if show_tests:
                            rkey = key if show_components else 'all'
                            if rkey in by_component['tests']:
                                # Avoid duplicates: Some test paths have multiple TestResolver
                                # entries, as when a test is included by multiple manifests.
                                found = False
                                for ctest in by_component['tests'][rkey]:
                                    if ctest['test'] == test_info['test']:
                                        found = True
                                        break
                                if not found:
                                    by_component['tests'][rkey].append(test_info)
                            else:
                                by_component['tests'][rkey] = [test_info]
            if show_tests:
                for key in by_component['tests']:
                    by_component['tests'][key].sort(key=lambda k: k['test'])

        if show_activedata:
            try:
                self.add_activedata(branches, days, by_component)
            except Exception:
                print("Failed to retrieve some ActiveData data.")
                traceback.print_exc()
            self.log_verbose("%d tests updated with matching ActiveData data" %
                             self.total_activedata_matches)
            self.log_verbose("%d seconds waiting for ActiveData" %
                             self.total_activedata_seconds)

        by_component['description'] = self.description(
            components, flavor, subsuite, paths,
            show_manifests, show_tests, show_summary, show_annotations,
            show_activedata,
            filter_values, filter_keys,
            branches, days)

        if show_summary:
            by_component['summary'] = {}
            by_component['summary']['components'] = len(component_set)
            by_component['summary']['manifests'] = manifest_count
            by_component['summary']['tests'] = test_count
            by_component['summary']['failed tests'] = failed_count
            by_component['summary']['skipped tests'] = skipped_count

        if show_annotations:
            by_component['annotations'] = {}
            by_component['annotations']['total annotations'] = annotation_count
            by_component['annotations']['total conditions'] = condition_count
            by_component['annotations']['unique conditions'] = len(conditions)
            by_component['annotations']['conditions'] = conditions

        json_report = json.dumps(by_component, indent=2, sort_keys=True)
        if output_file:
            output_file = os.path.abspath(output_file)
            output_dir = os.path.dirname(output_file)
            if not os.path.isdir(output_dir):
                os.makedirs(output_dir)

            with open(output_file, 'w') as f:
                f.write(json_report)
        else:
            print(json_report)

        end_time = datetime.datetime.now()
        self.log_verbose("%d seconds total to generate report" %
                         (end_time - start_time).total_seconds())
Exemplo n.º 18
0
    def report(
        self,
        components,
        flavor,
        subsuite,
        paths,
        show_manifests,
        show_tests,
        show_summary,
        show_annotations,
        filter_values,
        filter_keys,
        show_components,
        output_file,
    ):
        def matches_filters(test):
            """
            Return True if all of the requested filter_values are found in this test;
            if filter_keys are specified, restrict search to those test keys.
            """
            for value in filter_values:
                value_found = False
                for key in test:
                    if not filter_keys or key in filter_keys:
                        if re.search(value, test[key]):
                            value_found = True
                            break
                if not value_found:
                    return False
            return True

        start_time = datetime.datetime.now()

        # Ensure useful report by default
        if (not show_manifests and not show_tests and not show_summary
                and not show_annotations):
            show_manifests = True
            show_summary = True

        by_component = {}
        if components:
            components = components.split(",")
        if filter_keys:
            filter_keys = filter_keys.split(",")
        if filter_values:
            filter_values = filter_values.split(",")
        else:
            filter_values = []
        display_keys = (filter_keys or []) + ["skip-if", "fail-if", "fails-if"]
        display_keys = set(display_keys)

        print("Finding tests...")
        here = os.path.abspath(os.path.dirname(__file__))
        resolver = TestResolver.from_environment(cwd=here,
                                                 loader_cls=TestManifestLoader)
        tests = list(
            resolver.resolve_tests(paths=paths,
                                   flavor=flavor,
                                   subsuite=subsuite))

        manifest_paths = set()
        for t in tests:
            if "manifest" in t and t["manifest"] is not None:
                manifest_paths.add(t["manifest"])
        manifest_count = len(manifest_paths)
        print("Resolver found {} tests, {} manifests".format(
            len(tests), manifest_count))

        if show_manifests:
            topsrcdir = self.build_obj.topsrcdir
            by_component["manifests"] = {}
            manifest_paths = list(manifest_paths)
            manifest_paths.sort()
            relpaths = []
            for manifest_path in manifest_paths:
                relpath = mozpath.relpath(manifest_path, topsrcdir)
                if mozpath.commonprefix(
                    (manifest_path, topsrcdir)) != topsrcdir:
                    continue
                relpaths.append(relpath)
            reader = self.build_obj.mozbuild_reader(config_mode="empty")
            files_info = reader.files_info(relpaths)
            for manifest_path in manifest_paths:
                relpath = mozpath.relpath(manifest_path, topsrcdir)
                if mozpath.commonprefix(
                    (manifest_path, topsrcdir)) != topsrcdir:
                    continue
                manifest_info = None
                if relpath in files_info:
                    bug_component = files_info[relpath].get("BUG_COMPONENT")
                    if bug_component:
                        key = "{}::{}".format(bug_component.product,
                                              bug_component.component)
                    else:
                        key = "<unknown bug component>"
                    if (not components) or (key in components):
                        manifest_info = {
                            "manifest": relpath,
                            "tests": 0,
                            "skipped": 0
                        }
                        rkey = key if show_components else "all"
                        if rkey in by_component["manifests"]:
                            by_component["manifests"][rkey].append(
                                manifest_info)
                        else:
                            by_component["manifests"][rkey] = [manifest_info]
                if manifest_info:
                    for t in tests:
                        if t["manifest"] == manifest_path:
                            manifest_info["tests"] += 1
                            if t.get("skip-if"):
                                manifest_info["skipped"] += 1
            for key in by_component["manifests"]:
                by_component["manifests"][key].sort(
                    key=lambda k: k["manifest"])

        if show_tests:
            by_component["tests"] = {}

        if show_tests or show_summary or show_annotations:
            test_count = 0
            failed_count = 0
            skipped_count = 0
            annotation_count = 0
            condition_count = 0
            component_set = set()
            relpaths = []
            conditions = {}
            known_unconditional_annotations = [
                "skip", "fail", "asserts", "random"
            ]
            known_conditional_annotations = [
                "skip-if",
                "fail-if",
                "run-if",
                "fails-if",
                "fuzzy-if",
                "random-if",
                "asserts-if",
            ]
            for t in tests:
                relpath = t.get("srcdir_relpath")
                relpaths.append(relpath)
            reader = self.build_obj.mozbuild_reader(config_mode="empty")
            files_info = reader.files_info(relpaths)
            for t in tests:
                if not matches_filters(t):
                    continue
                if "referenced-test" in t:
                    # Avoid double-counting reftests: disregard reference file entries
                    continue
                if show_annotations:
                    for key in t:
                        if key in known_unconditional_annotations:
                            annotation_count += 1
                        if key in known_conditional_annotations:
                            annotation_count += 1
                            # Here 'key' is a manifest annotation type like 'skip-if' and t[key]
                            # is the associated condition. For example, the manifestparser
                            # manifest annotation, "skip-if = os == 'win'", is expected to be
                            # encoded as t['skip-if'] = "os == 'win'".
                            # To allow for reftest manifests, t[key] may have multiple entries
                            # separated by ';', each corresponding to a condition for that test
                            # and annotation type. For example,
                            # "skip-if(Android&&webrender) skip-if(OSX)", would be
                            # encoded as t['skip-if'] = "Android&&webrender;OSX".
                            annotation_conditions = t[key].split(";")
                            for condition in annotation_conditions:
                                condition_count += 1
                                # Trim reftest fuzzy-if ranges: everything after the first comma
                                # eg. "Android,0-2,1-3" -> "Android"
                                condition = condition.split(",")[0]
                                if condition not in conditions:
                                    conditions[condition] = 0
                                conditions[condition] += 1
                test_count += 1
                relpath = t.get("srcdir_relpath")
                if relpath in files_info:
                    bug_component = files_info[relpath].get("BUG_COMPONENT")
                    if bug_component:
                        key = "{}::{}".format(bug_component.product,
                                              bug_component.component)
                    else:
                        key = "<unknown bug component>"
                    if (not components) or (key in components):
                        component_set.add(key)
                        test_info = {"test": relpath}
                        for test_key in display_keys:
                            value = t.get(test_key)
                            if value:
                                test_info[test_key] = value
                        if t.get("fail-if"):
                            failed_count += 1
                        if t.get("fails-if"):
                            failed_count += 1
                        if t.get("skip-if"):
                            skipped_count += 1
                        if show_tests:
                            rkey = key if show_components else "all"
                            if rkey in by_component["tests"]:
                                # Avoid duplicates: Some test paths have multiple TestResolver
                                # entries, as when a test is included by multiple manifests.
                                found = False
                                for ctest in by_component["tests"][rkey]:
                                    if ctest["test"] == test_info["test"]:
                                        found = True
                                        break
                                if not found:
                                    by_component["tests"][rkey].append(
                                        test_info)
                            else:
                                by_component["tests"][rkey] = [test_info]
            if show_tests:
                for key in by_component["tests"]:
                    by_component["tests"][key].sort(key=lambda k: k["test"])

        by_component["description"] = self.description(
            components,
            flavor,
            subsuite,
            paths,
            show_manifests,
            show_tests,
            show_summary,
            show_annotations,
            filter_values,
            filter_keys,
        )

        if show_summary:
            by_component["summary"] = {}
            by_component["summary"]["components"] = len(component_set)
            by_component["summary"]["manifests"] = manifest_count
            by_component["summary"]["tests"] = test_count
            by_component["summary"]["failed tests"] = failed_count
            by_component["summary"]["skipped tests"] = skipped_count

        if show_annotations:
            by_component["annotations"] = {}
            by_component["annotations"]["total annotations"] = annotation_count
            by_component["annotations"]["total conditions"] = condition_count
            by_component["annotations"]["unique conditions"] = len(conditions)
            by_component["annotations"]["conditions"] = conditions

        self.write_report(by_component, output_file)

        end_time = datetime.datetime.now()
        self.log_verbose("%d seconds total to generate report" %
                         (end_time - start_time).total_seconds())