Ejemplo n.º 1
0
 def get_directory_owners(self):
     """Returns a mapping of email addresses to owners of changed tests."""
     _log.info('Gathering directory owners emails to CC.')
     changed_files = self.host.git().changed_files()
     extractor = DirectoryOwnersExtractor(self.fs)
     extractor.read_owner_map()
     return extractor.list_owners(changed_files)
Ejemplo n.º 2
0
 def setUp(self):
     # We always have an OWNERS file at LayoutTests/external.
     self.filesystem = MockFileSystem(
         files={
             '/mock-checkout/third_party/WebKit/LayoutTests/external/OWNERS':
             '*****@*****.**'
         })
     self.extractor = DirectoryOwnersExtractor(self.filesystem)
Ejemplo n.º 3
0
    def __init__(self, host, chromium_git, local_wpt):
        self.host = host
        self.git = chromium_git
        self.local_wpt = local_wpt

        self.default_port = host.port_factory.get()
        self.finder = PathFinder(host.filesystem)
        self.owners_extractor = DirectoryOwnersExtractor(host.filesystem)
        self.new_failures_by_directory = defaultdict(list)
Ejemplo n.º 4
0
class ImportNotifier(object):

    def __init__(self, host, chromium_git, local_wpt):
        self.host = host
        self.git = chromium_git
        self.local_wpt = local_wpt

        self.default_port = host.port_factory.get()
        self.finder = PathFinder(host.filesystem)
        self.owners_extractor = DirectoryOwnersExtractor(host.filesystem)
        self.new_failures_by_directory = defaultdict(list)

    def main(self, wpt_revision_start, wpt_revision_end, rebaselined_tests, test_expectations, issue, patchset,
             dry_run=True, service_account_key_json=None):
        """Files bug reports for new failures.

        Args:
            wpt_revision_start: The start of the imported WPT revision range
                (exclusive), i.e. the last imported revision.
            wpt_revision_end: The end of the imported WPT revision range
                (inclusive), i.e. the current imported revision.
            rebaselined_tests: A list of test names that have been rebaselined.
            test_expectations: A dictionary mapping names of tests that cannot
                be rebaselined to a list of new test expectation lines.
            issue: The issue number of the import CL (a string).
            patchset: The patchset number of the import CL (a string).
            dry_run: If True, no bugs will be actually filed to crbug.com.
            service_account_key_json: The path to a JSON private key of a
                service account for accessing Monorail. If None, try to load
                from the default location, i.e. the path stored in the
                environment variable GOOGLE_APPLICATION_CREDENTIALS.

        Note: "test names" are paths of the tests relative to LayoutTests.
        """
        gerrit_url = SHORT_GERRIT_PREFIX + issue
        gerrit_url_with_ps = gerrit_url + '/' + patchset + '/'

        changed_test_baselines = self.find_changed_baselines_of_tests(rebaselined_tests)
        self.examine_baseline_changes(changed_test_baselines, gerrit_url_with_ps)
        self.examine_new_test_expectations(test_expectations)

        bugs = self.create_bugs_from_new_failures(wpt_revision_start, wpt_revision_end, gerrit_url)
        self.file_bugs(bugs, dry_run, service_account_key_json)

    def find_changed_baselines_of_tests(self, rebaselined_tests):
        """Finds the corresponding changed baselines of each test.

        Args:
            rebaselined_tests: A list of test names that have been rebaselined.

        Returns:
            A dictionary mapping test names to paths of their baselines changed
            in this import CL (paths relative to the root of Chromium repo).
        """
        test_baselines = {}
        changed_files = self.git.changed_files()
        for test_name in rebaselined_tests:
            test_without_ext, _ = self.host.filesystem.splitext(test_name)
            changed_baselines = []
            # TODO(robertma): Refactor this into layout_tests.port.base.
            baseline_name = test_without_ext + '-expected.txt'
            for changed_file in changed_files:
                if changed_file.endswith(baseline_name):
                    changed_baselines.append(changed_file)
            if changed_baselines:
                test_baselines[test_name] = changed_baselines
        return test_baselines

    def examine_baseline_changes(self, changed_test_baselines, gerrit_url_with_ps):
        """Examines all changed baselines to find new failures.

        Args:
            changed_test_baselines: A dictionary mapping test names to paths of
                changed baselines.
            gerrit_url_with_ps: Gerrit URL of this CL with the patchset number.
        """
        for test_name, changed_baselines in changed_test_baselines.iteritems():
            directory = self.find_owned_directory(test_name)
            if not directory:
                _log.warning('Cannot find OWNERS of %s', test_name)
                continue

            for baseline in changed_baselines:
                if self.more_failures_in_baseline(baseline):
                    self.new_failures_by_directory[directory].append(
                        TestFailure(TestFailure.BASELINE_CHANGE, test_name,
                                    baseline_path=baseline, gerrit_url_with_ps=gerrit_url_with_ps)
                    )

    def more_failures_in_baseline(self, baseline):
        diff = self.git.run(['diff', '-U0', 'origin/master', '--', baseline])
        delta_failures = 0
        for line in diff.splitlines():
            if line.startswith('+FAIL'):
                delta_failures += 1
            if line.startswith('-FAIL'):
                delta_failures -= 1
        return delta_failures > 0

    def examine_new_test_expectations(self, test_expectations):
        """Examines new test expectations to find new failures.

        Args:
            test_expectations: A dictionary mapping names of tests that cannot
                be rebaselined to a list of new test expectation lines.
        """
        for test_name, expectation_lines in test_expectations.iteritems():
            directory = self.find_owned_directory(test_name)
            if not directory:
                _log.warning('Cannot find OWNERS of %s', test_name)
                continue

            for expectation_line in expectation_lines:
                self.new_failures_by_directory[directory].append(
                    TestFailure(TestFailure.NEW_EXPECTATION, test_name,
                                expectation_line=expectation_line)
                )

    def create_bugs_from_new_failures(self, wpt_revision_start, wpt_revision_end, gerrit_url):
        """Files bug reports for new failures.

        Args:
            wpt_revision_start: The start of the imported WPT revision range
                (exclusive), i.e. the last imported revision.
            wpt_revision_end: The end of the imported WPT revision range
                (inclusive), i.e. the current imported revision.
            gerrit_url: Gerrit URL of the CL.

        Return:
            A list of MonorailIssue objects that should be filed.
        """
        imported_commits = self.local_wpt.commits_in_range(wpt_revision_start, wpt_revision_end)
        bugs = []
        for directory, failures in self.new_failures_by_directory.iteritems():
            summary = '[WPT] New failures introduced in {} by import {}'.format(directory, gerrit_url)

            full_directory = self.host.filesystem.join(self.finder.layout_tests_dir(), directory)
            owners_file = self.host.filesystem.join(full_directory, 'OWNERS')
            is_wpt_notify_enabled = self.owners_extractor.is_wpt_notify_enabled(owners_file)

            owners = self.owners_extractor.extract_owners(owners_file)
            # owners may be empty but not None.
            cc = owners + ['*****@*****.**']

            component = self.owners_extractor.extract_component(owners_file)
            # component could be None.
            components = [component] if component else None

            prologue = ('WPT import {} introduced new failures in {}:\n\n'
                        'List of new failures:\n'.format(gerrit_url, directory))
            failure_list = ''
            for failure in failures:
                failure_list += str(failure) + '\n'

            epilogue = '\nThis import contains upstream changes from {} to {}:\n'.format(
                wpt_revision_start, wpt_revision_end
            )
            commit_list = self.format_commit_list(imported_commits, full_directory)

            description = prologue + failure_list + epilogue + commit_list

            bug = MonorailIssue.new_chromium_issue(summary, description, cc, components)
            _log.info(str(bug))

            if is_wpt_notify_enabled:
                _log.info("WPT-NOTIFY enabled in this directory; adding the bug to the pending list.")
                bugs.append(bug)
            else:
                _log.info("WPT-NOTIFY disabled in this directory; discarding the bug.")
        return bugs

    def format_commit_list(self, imported_commits, directory):
        """Formats the list of imported WPT commits.

        Imports affecting the given directory will be highlighted.

        Args:
            imported_commits: A list of (SHA, commit subject) pairs.
            directory: An absolute path of a directory in the Chromium repo, for
                which the list is formatted.

        Returns:
            A multi-line string.
        """
        path_from_wpt = self.host.filesystem.relpath(
            directory, self.finder.path_from_layout_tests('external', 'wpt'))
        commit_list = ''
        for sha, subject in imported_commits:
            line = '{}: {}'.format(subject, GITHUB_COMMIT_PREFIX + sha)
            if self.local_wpt.is_commit_affecting_directory(sha, path_from_wpt):
                line += ' [affecting this directory]'
            commit_list += line + '\n'
        return commit_list

    def find_owned_directory(self, test_name):
        """Finds the lowest directory that contains the test and has OWNERS.

        Args:
            The name of the test (a path relative to LayoutTests).

        Returns:
            The path of the found directory relative to LayoutTests.
        """
        # Always use non-virtual test names when looking up OWNERS.
        if self.default_port.lookup_virtual_test_base(test_name):
            test_name = self.default_port.lookup_virtual_test_base(test_name)
        # find_owners_file takes either a relative path from the *root* of the
        # repository, or an absolute path.
        abs_test_path = self.finder.path_from_layout_tests(test_name)
        owners_file = self.owners_extractor.find_owners_file(self.host.filesystem.dirname(abs_test_path))
        if not owners_file:
            return None
        owned_directory = self.host.filesystem.dirname(owners_file)
        short_directory = self.host.filesystem.relpath(owned_directory, self.finder.layout_tests_dir())
        return short_directory

    def file_bugs(self, bugs, dry_run, service_account_key_json=None):
        """Files a list of bugs to Monorail.

        Args:
            bugs: A list of MonorailIssue objects.
            dry_run: A boolean, whether we are in dry run mode.
            service_account_key_json: Optional, see docs for main().
        """
        # TODO(robertma): Better error handling in this method.
        if dry_run:
            _log.info('[dry_run] Would have filed the %d bugs in the pending list.', len(bugs))
            return

        _log.info('Filing %d bugs in the pending list to Monorail', len(bugs))
        api = self._get_monorail_api(service_account_key_json)
        for index, bug in enumerate(bugs, start=1):
            response = api.insert_issue(bug)
            _log.info('[%d] Filed bug: %s', index, MonorailIssue.crbug_link(response['id']))

    def _get_monorail_api(self, service_account_key_json):
        return MonorailAPI(service_account_key_json=service_account_key_json)
 def setUp(self):
     self.filesystem = MockFileSystem()
     self.extractor = DirectoryOwnersExtractor(self.filesystem)
class DirectoryOwnersExtractorTest(unittest.TestCase):

    def setUp(self):
        self.filesystem = MockFileSystem()
        self.extractor = DirectoryOwnersExtractor(self.filesystem)

    def test_list_owners(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/x.html': '',
            ABS_WPT_BASE + '/foo/OWNERS': '[email protected]\[email protected]\n',
            ABS_WPT_BASE + '/bar/x/y.html': '',
            ABS_WPT_BASE + '/bar/OWNERS': '[email protected]\[email protected]\n',
            ABS_WPT_BASE + '/baz/x/y.html': '',
            ABS_WPT_BASE + '/baz/x/OWNERS': '[email protected]\n',
            ABS_WPT_BASE + '/quux/x/y.html': '',
        }
        changed_files = [
            # Same owners:
            REL_WPT_BASE + '/foo/x.html',
            REL_WPT_BASE + '/bar/x/y.html',
            # Same owned directories:
            REL_WPT_BASE + '/baz/x/y.html',
            REL_WPT_BASE + '/baz/x/z.html',
            # Owners not found:
            REL_WPT_BASE + '/quux/x/y.html',
        ]
        self.assertEqual(
            self.extractor.list_owners(changed_files),
            {('*****@*****.**', '*****@*****.**'): ['external/wpt/bar', 'external/wpt/foo'],
             ('*****@*****.**',): ['external/wpt/baz/x']}
        )

    def test_find_and_extract_owners_current_dir(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS': '*****@*****.**'
        }
        self.assertEqual(self.extractor.find_and_extract_owners(REL_WPT_BASE + '/foo'),
                         (ABS_WPT_BASE + '/foo/OWNERS', ['*****@*****.**']))

    def test_find_and_extract_owners_ancestor(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/x/OWNERS': '*****@*****.**',
            ABS_WPT_BASE + '/x/y/z.html': '',
        }
        self.assertEqual(self.extractor.find_and_extract_owners(REL_WPT_BASE + '/x/y'),
                         (ABS_WPT_BASE + '/x/OWNERS', ['*****@*****.**']))

    def test_find_and_extract_owners_not_found(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS': '*****@*****.**',
            '/mock-checkout/third_party/WebKit/LayoutTests/external/OWNERS': '*****@*****.**',
            ABS_WPT_BASE + '/x/y/z.html': '',
        }
        self.assertEqual(self.extractor.find_and_extract_owners(REL_WPT_BASE + '/x/y'),
                         (None, None))

    def test_find_and_extract_owners_skip_empty(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/x/OWNERS': '*****@*****.**',
            ABS_WPT_BASE + '/x/y/OWNERS': '# [email protected]',
            ABS_WPT_BASE + '/x/y/z.html': '',
        }
        self.assertEqual(self.extractor.find_and_extract_owners(REL_WPT_BASE + '/x/y'),
                         (ABS_WPT_BASE + '/x/OWNERS', ['*****@*****.**']))

    def test_find_and_extract_owners_absolute_path(self):
        with self.assertRaises(AssertionError):
            self.extractor.find_and_extract_owners('/absolute/path')

    def test_find_and_extract_owners_out_of_tree(self):
        with self.assertRaises(AssertionError):
            self.extractor.find_and_extract_owners('third_party/WebKit/LayoutTests/other')
        self.assertEqual(
            self.extractor.find_and_extract_owners('third_party/WebKit/LayoutTests'),
            (None, None))
        self.assertEqual(
            self.extractor.find_and_extract_owners('third_party/WebKit/LayoutTests/FlagExpectations/foo-bar'),
            (None, None))

    def test_extract_owners(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '#This is a comment\n'
            '*\n'
            '[email protected]\n'
            '[email protected]\n'
            'foobar\n'
            '#[email protected]\n'
            '# TEAM: [email protected]\n'
            '# COMPONENT: Blink>Layout\n'
        }
        self.assertEqual(self.extractor.extract_owners(ABS_WPT_BASE + '/foo/OWNERS'),
                         ['*****@*****.**', '*****@*****.**'])
Ejemplo n.º 7
0
class DirectoryOwnersExtractorTest(unittest.TestCase):
    def setUp(self):
        self.filesystem = MockFileSystem()
        self.extractor = DirectoryOwnersExtractor(self.filesystem)

    def test_lines_to_owner_map(self):
        lines = [
            'external/wpt/webgl [ Skip ]',
            '## Owners: [email protected]',
            '# external/wpt/webmessaging [ Pass ]',
            '## Owners: [email protected]',
            '# external/wpt/webrtc [ Pass ]',
            'external/wpt/websockets [ Skip ]',
            '## Owners: [email protected],[email protected]',
            '# external/wpt/webstorage [ Pass ]',
            'external/wpt/webvtt [ Skip ]',
        ]

        self.assertEqual(
            self.extractor.lines_to_owner_map(lines), {
                'external/wpt/webmessaging': ['*****@*****.**'],
                'external/wpt/webrtc': ['*****@*****.**'],
                'external/wpt/webstorage':
                ['*****@*****.**', '*****@*****.**'],
            })

    def test_list_owners(self):
        self.extractor.owner_map = {
            'external/wpt/foo': ['*****@*****.**', '*****@*****.**'],
            'external/wpt/bar': ['*****@*****.**'],
            'external/wpt/baz': ['*****@*****.**', '*****@*****.**'],
        }
        self.filesystem.files = {
            '/mock-checkout/third_party/WebKit/LayoutTests/external/wpt/foo/x/y.html':
            '',
            '/mock-checkout/third_party/WebKit/LayoutTests/external/wpt/bar/x/y.html':
            '',
            '/mock-checkout/third_party/WebKit/LayoutTests/external/wpt/baz/x/y.html':
            '',
            '/mock-checkout/third_party/WebKit/LayoutTests/external/wpt/quux/x/y.html':
            '',
        }
        changed_files = [
            'third_party/WebKit/LayoutTests/external/wpt/foo/x/y.html',
            'third_party/WebKit/LayoutTests/external/wpt/baz/x/y.html',
            'third_party/WebKit/LayoutTests/external/wpt/quux/x/y.html',
        ]
        self.assertEqual(
            self.extractor.list_owners(changed_files), {
                ('*****@*****.**', '*****@*****.**'):
                ['external/wpt/foo', 'external/wpt/baz']
            })

    def test_extract_owner_positive_cases(self):
        self.assertEqual(
            self.extractor.extract_owners('## Owners: [email protected]'),
            ['*****@*****.**'])
        self.assertEqual(
            self.extractor.extract_owners('# Owners: [email protected]'),
            ['*****@*****.**'])
        self.assertEqual(
            self.extractor.extract_owners('## Owners: [email protected],[email protected]'),
            ['*****@*****.**', '*****@*****.**'])
        self.assertEqual(
            self.extractor.extract_owners('## Owners: [email protected], [email protected]'),
            ['*****@*****.**', '*****@*****.**'])
        self.assertEqual(
            self.extractor.extract_owners('## Owner: [email protected]'),
            ['*****@*****.**'])

    def test_extract_owner_negative_cases(self):
        self.assertIsNone(self.extractor.extract_owners(''))
        self.assertIsNone(
            self.extractor.extract_owners('## Something: [email protected]'))
        self.assertIsNone(
            self.extractor.extract_owners('## Owners: not an email address'))

    def test_extract_directory_positive_cases(self):
        self.assertEqual(
            self.extractor.extract_directory('external/a/b [ Pass ]'),
            'external/a/b')
        self.assertEqual(
            self.extractor.extract_directory('# external/c/d [ Pass ]'),
            'external/c/d')
        self.assertEqual(
            self.extractor.extract_directory('# external/e/f [ Skip ]'),
            'external/e/f')
        self.assertEqual(
            self.extractor.extract_directory('# external/g/h/i [ Skip ]'),
            'external/g/h/i')

    def test_extract_directory_negative_cases(self):
        self.assertIsNone(self.extractor.extract_directory(''))
        self.assertIsNone(
            self.extractor.extract_directory('external/a/b [ Skip ]'))
        self.assertIsNone(self.extractor.extract_directory('# some comment'))
Ejemplo n.º 8
0
class DirectoryOwnersExtractorTest(unittest.TestCase):
    def setUp(self):
        # We always have an OWNERS file at LayoutTests/external.
        self.filesystem = MockFileSystem(
            files={
                '/mock-checkout/third_party/WebKit/LayoutTests/external/OWNERS':
                '*****@*****.**'
            })
        self.extractor = DirectoryOwnersExtractor(self.filesystem)

    def _write_files(self, files):
        # Use write_text_file instead of directly assigning to filesystem.files
        # so that intermediary directories are correctly created, too.
        for path, contents in files.iteritems():
            self.filesystem.write_text_file(path, contents)

    def test_list_owners_combines_same_owners(self):
        self._write_files({
            ABS_WPT_BASE + '/foo/x.html':
            '',
            ABS_WPT_BASE + '/foo/OWNERS':
            '[email protected]\[email protected]\n',
            ABS_WPT_BASE + '/bar/x/y.html':
            '',
            ABS_WPT_BASE + '/bar/OWNERS':
            '[email protected]\[email protected]\n',
        })
        changed_files = [
            REL_WPT_BASE + '/foo/x.html',
            REL_WPT_BASE + '/bar/x/y.html',
        ]
        self.assertEqual(
            self.extractor.list_owners(changed_files), {
                ('*****@*****.**', '*****@*****.**'):
                ['external/wpt/bar', 'external/wpt/foo']
            })

    def test_list_owners_combines_same_directory(self):
        self._write_files({
            ABS_WPT_BASE + '/baz/x/y.html': '',
            ABS_WPT_BASE + '/baz/x/y/z.html': '',
            ABS_WPT_BASE + '/baz/x/OWNERS': '[email protected]\n',
        })
        changed_files = [
            REL_WPT_BASE + '/baz/x/y.html',
            REL_WPT_BASE + '/baz/x/y/z.html',
        ]
        self.assertEqual(self.extractor.list_owners(changed_files),
                         {('*****@*****.**', ): ['external/wpt/baz/x']})

    def test_list_owners_skips_empty_owners(self):
        self._write_files({
            ABS_WPT_BASE + '/baz/x/y/z.html': '',
            ABS_WPT_BASE + '/baz/x/y/OWNERS': '# Some comments\n',
            ABS_WPT_BASE + '/baz/x/OWNERS': '[email protected]\n',
        })
        changed_files = [
            REL_WPT_BASE + '/baz/x/y/z.html',
        ]
        self.assertEqual(self.extractor.list_owners(changed_files),
                         {('*****@*****.**', ): ['external/wpt/baz/x']})

    def test_list_owners_not_found(self):
        self._write_files({
            # Although LayoutTests/external/OWNERS exists, it should not be listed.
            ABS_WPT_BASE + '/foo/bar.html':
            '',
            # Files out of external.
            '/mock-checkout/third_party/WebKit/LayoutTests/TestExpectations':
            '',
            '/mock-checkout/third_party/WebKit/LayoutTests/OWNERS':
            '*****@*****.**',
        })
        changed_files = [
            REL_WPT_BASE + '/foo/bar.html',
            'third_party/WebKit/LayoutTests/TestExpectations',
        ]
        self.assertEqual(self.extractor.list_owners(changed_files), {})

    def test_find_owners_file_at_current_dir(self):
        self._write_files({ABS_WPT_BASE + '/foo/OWNERS': '*****@*****.**'})
        self.assertEqual(
            self.extractor.find_owners_file(REL_WPT_BASE + '/foo'),
            ABS_WPT_BASE + '/foo/OWNERS')

    def test_find_owners_file_at_ancestor(self):
        self._write_files({
            ABS_WPT_BASE + '/x/OWNERS': '*****@*****.**',
            ABS_WPT_BASE + '/x/y/z.html': '',
        })
        self.assertEqual(
            self.extractor.find_owners_file(REL_WPT_BASE + '/x/y'),
            ABS_WPT_BASE + '/x/OWNERS')

    def test_find_owners_file_stops_at_external_root(self):
        self._write_files({
            ABS_WPT_BASE + '/x/y/z.html': '',
        })
        self.assertEqual(
            self.extractor.find_owners_file(REL_WPT_BASE + '/x/y'),
            '/mock-checkout/third_party/WebKit/LayoutTests/external/OWNERS')

    def test_find_owners_file_takes_four_kinds_of_paths(self):
        owners_path = ABS_WPT_BASE + '/foo/OWNERS'
        self._write_files({
            owners_path: '*****@*****.**',
            ABS_WPT_BASE + '/foo/bar.html': '',
        })
        # Absolute paths of directories.
        self.assertEqual(
            self.extractor.find_owners_file(ABS_WPT_BASE + '/foo'),
            owners_path)
        # Relative paths of directories.
        self.assertEqual(
            self.extractor.find_owners_file(REL_WPT_BASE + '/foo'),
            owners_path)
        # Absolute paths of files.
        self.assertEqual(
            self.extractor.find_owners_file(ABS_WPT_BASE + '/foo/bar.html'),
            owners_path)
        # Relative paths of files.
        self.assertEqual(
            self.extractor.find_owners_file(REL_WPT_BASE + '/foo/bar.html'),
            owners_path)

    def test_find_owners_file_out_of_external(self):
        self._write_files({
            '/mock-checkout/third_party/WebKit/LayoutTests/OWNERS':
            '*****@*****.**',
            '/mock-checkout/third_party/WebKit/LayoutTests/other/some_file':
            '',
        })
        self.assertIsNone(
            self.extractor.find_owners_file('third_party/WebKit/LayoutTests'))
        self.assertIsNone(
            self.extractor.find_owners_file(
                'third_party/WebKit/LayoutTests/other'))
        self.assertIsNone(self.extractor.find_owners_file('third_party'))

    def test_extract_owners(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '#This is a comment\n'
            '*\n'
            '[email protected]\n'
            '[email protected]\n'
            'foobar\n'
            '#[email protected]\n'
            '# TEAM: [email protected]\n'
            '# COMPONENT: Blink>Layout\n'
        }
        self.assertEqual(
            self.extractor.extract_owners(ABS_WPT_BASE + '/foo/OWNERS'),
            ['*****@*****.**', '*****@*****.**'])

    def test_extract_component(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '# TEAM: [email protected]\n'
            '# COMPONENT: Blink>Layout\n'
        }
        self.assertEqual(
            self.extractor.extract_component(ABS_WPT_BASE + '/foo/OWNERS'),
            'Blink>Layout')

    def test_is_wpt_notify_enabled_true(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '# COMPONENT: Blink>Layout\n'
            '# WPT-NOTIFY: true\n'
        }
        self.assertTrue(
            self.extractor.is_wpt_notify_enabled(ABS_WPT_BASE + '/foo/OWNERS'))

    def test_is_wpt_notify_enabled_false(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '# COMPONENT: Blink>Layout\n'
            '# WPT-NOTIFY: false\n'
        }
        self.assertFalse(
            self.extractor.is_wpt_notify_enabled(ABS_WPT_BASE + '/foo/OWNERS'))

    def test_is_wpt_notify_enabled_absence_is_false(self):
        self.filesystem.files = {
            ABS_WPT_BASE + '/foo/OWNERS':
            '# TEAM: [email protected]\n'
            '# COMPONENT: Blink>Layout\n'
        }
        self.assertFalse(
            self.extractor.is_wpt_notify_enabled(ABS_WPT_BASE + '/foo/OWNERS'))