# Copyright 2019 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import unittest
import expand_owners
import mock
import os
import shutil
import tempfile
import xml.dom.minidom
def _GetToolsParentDir():
"""Returns an absolute path to the the tools directory's parent directory.
Example: 'C:\a\n\ff\' or '/opt/n/ff/'.
"""
return os.path.abspath(os.path.join(*expand_owners.DIR_ABOVE_TOOLS))
def _GetFileDirective(path):
"""Returns a file directive line.
Args:
path: An absolute path, e.g. '/some/directory/subdirectory/tools/OWNERS'.
Returns:
A file directive that can be used in an OWNERS file, e.g.
file://tools/OWNERS.
"""
return ''.join(['file://', path[len(_GetToolsParentDir()) + 1:]])
def _GetSrcRelativePath(path):
"""Returns a(n) src-relative path for the given file path.
Args:
path: An absolute path, e.g. '/some/directory/subdirectory/tools/OWNERS'.
Returns:
A src-relative path, e.g.'src/tools/OWNERS'.
"""
assert path.startswith(_GetToolsParentDir())
return expand_owners.SRC + path[len(_GetToolsParentDir()) + 1:]
def _MakeOwnersFile(filename, directory):
"""Makes a temporary file in this directory and returns its absolute path.
Args:
filename: A string filename, e.g. 'OWNERS'.
directory: A string directory under which to make the new file.
Returns:
The temporary file's absolute path.
"""
if not directory:
directory = os.path.abspath(os.path.join(os.path.dirname(__file__)))
owners_file = tempfile.NamedTemporaryFile(suffix=filename, dir=directory)
return os.path.abspath(owners_file.name)
class ExpandOwnersTest(unittest.TestCase):
def setUp(self):
super(ExpandOwnersTest, self).setUp()
self.temp_dir = tempfile.mkdtemp(
dir=os.path.abspath(os.path.join(os.path.dirname(__file__))))
# The below construction is used rather than __file__.endswith() because
# the file extension could be .py or .pyc.
assert os.sep.join(
['tools', 'metrics', 'histograms',
'expand_owners_unittest.py']) in __file__
def tearDown(self):
super(ExpandOwnersTest, self).tearDown()
shutil.rmtree(self.temp_dir)
def testExpandOwnersUsesMetadataOverOwners(self):
"""Checks that DIR_METADATA is used if available"""
with open(os.path.join(self.temp_dir, 'DIR_METADATA'), "w+") as md:
md.write("\n".join([
'monorail {', 'component: "Bees"', '}', 'buganizer_public {',
'component_id:123456', '}'
]))
absolute_path = _MakeOwnersFile('simple_OWNERS', self.temp_dir)
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(['[email protected]', '[email protected]']))
self.maxDiff = None
src_relative_path = _GetSrcRelativePath(absolute_path)
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{path}</owner>
<summary>I like coffee.</summary>
</histogram>
<histogram name="Maple.Syrup" units="units">
<owner>[email protected]</owner>
<owner>{path}</owner>
<owner>[email protected]</owner>
<summary>I like maple syrup, too.</summary>
</histogram>
</histograms>
""".format(path=src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
<component>123456</component>
</histogram>
<histogram name="Maple.Syrup" units="units">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like maple syrup, too.</summary>
<component>123456</component>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertMultiLineEqual(histograms.toxml(), expected_histograms.toxml())
@mock.patch('expand_owners.ExtractComponentViaDirmd')
def testExpandOwnersWithSimpleOWNERSFilePath(self, mock_dirmd_extract):
"""Checks that OWNERS files are expanded."""
mock_dirmd_extract.return_value = None
absolute_path = _MakeOwnersFile('simple_OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(
['[email protected]', '[email protected]']))
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{path}</owner>
<summary>I like coffee.</summary>
</histogram>
<histogram name="Maple.Syrup" units="units">
<owner>[email protected]</owner>
<owner>{path}</owner>
<owner>[email protected]</owner>
<summary>I like maple syrup, too.</summary>
</histogram>
</histograms>
""".format(path=src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
<histogram name="Maple.Syrup" units="units">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like maple syrup, too.</summary>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertMultiLineEqual(histograms.toxml(), expected_histograms.toxml())
@mock.patch('expand_owners.ExtractComponentViaDirmd')
def testExpandOwnersWithLongFilePath(self, mock_dirmd_extract):
"""Checks that long OWNERS file paths are supported.
Most OWNERS file paths appear between owners tags on the same line, e.g.
<owner>src/chrome/browser</owner>. However, especially long paths may appear
on their own line between the tags.
"""
mock_dirmd_extract.return_value = None
absolute_path = _MakeOwnersFile('simple_OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(['[email protected]']))
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>
{path}
</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(path=src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertMultiLineEqual(histograms.toxml(), expected_histograms.toxml())
@mock.patch('expand_owners.ExtractComponentViaDirmd')
def testExpandOwnersWithDuplicateOwners(self, mock_dirmd_extract):
"""Checks that owners are unique."""
mock_dirmd_extract.return_value = None
absolute_path = _MakeOwnersFile('simple_OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(
['[email protected]', '[email protected]']))
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{}</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertMultiLineEqual(histograms.toxml(), expected_histograms.toxml())
@mock.patch('expand_owners.ExtractComponentViaDirmd')
def testExpandOwnersWithFileDirectiveOWNERSFilePath(self, mock_dirmd_extract):
"""Checks that OWNERS files with file directives are expanded."""
mock_dirmd_extract.return_value = None
simple_absolute_path = _MakeOwnersFile('simple_OWNERS', self.temp_dir)
with open(simple_absolute_path, 'w') as owners_file:
owners_file.write('[email protected]')
file_directive_absolute_path = (
_MakeOwnersFile('file_directive_OWNERS', self.temp_dir))
file_directive_src_relative_path = (
_GetSrcRelativePath(file_directive_absolute_path))
directive = _GetFileDirective(simple_absolute_path)
with open(file_directive_absolute_path, 'w') as owners_file:
owners_file.write('\n'.join([
'[email protected]', directive, '[email protected]',
]))
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{}</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(file_directive_src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertEqual(histograms.toxml(), expected_histograms.toxml())
@mock.patch('expand_owners.ExtractComponentViaDirmd')
def testExpandOwnersForOWNERSFileWithDuplicateComponents(
self, mock_dirmd_extract):
"""Checks that only one component tag is added if there are duplicates."""
mock_dirmd_extract.return_value = None
absolute_path = _MakeOwnersFile('OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(['[email protected]']))
duplicate_owner_absolute_path = (
_MakeOwnersFile('duplicate_owner_OWNERS', self.temp_dir))
duplicate_owner_src_relative_path = (
_GetSrcRelativePath(duplicate_owner_absolute_path))
with open(duplicate_owner_absolute_path, 'w') as owners_file:
owners_file.write('\n'.join(['[email protected]']))
histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{}</owner>
<owner>{}</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(src_relative_path, duplicate_owner_src_relative_path))
expected_histograms = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
expand_owners.ExpandHistogramsOWNERS(histograms)
self.assertEqual(histograms.toxml(), expected_histograms.toxml())
def testExpandOwnersWithoutOWNERSFilePath(self):
"""Checks that histograms without OWNERS file paths are unchanged."""
histograms_without_file_paths = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
expected_histograms = histograms_without_file_paths
expand_owners.ExpandHistogramsOWNERS(histograms_without_file_paths)
self.assertEqual(histograms_without_file_paths, expected_histograms)
def testExpandOwnersWithoutValidPrimaryOwner_OwnersPath(self):
"""Checks that an error is raised when the primary owner is a file path.
A valid primary owner is an individual's email address, e.g. [email protected],
[email protected], or the owner placeholder.
"""
histograms_without_valid_first_owner = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>src/OWNERS</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error,
'The histogram Caffeination must have a valid primary owner, i.e. a '
'Googler with an @google.com or @chromium.org email address.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_valid_first_owner)
def testExpandOwnersWithoutValidPrimaryOwner_TeamEmail(self):
"""Checks that an error is raised when the primary owner is a team.
A valid primary owner is an individual's email address, e.g. [email protected],
[email protected], or the owner placeholder.
"""
histograms_without_valid_first_owner = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error,
'The histogram Caffeination must have a valid primary owner, i.e. a '
'Googler with an @google.com or @chromium.org email address.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_valid_first_owner)
def testExpandOwnersWithoutValidPrimaryOwner_InvalidEmail(self):
"""Checks that an error is raised when the primary owner's email is invalid.
A valid primary owner is an individual's email address, e.g. [email protected],
[email protected], or the owner placeholder.
"""
histograms_without_valid_first_owner = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error,
'The histogram Caffeination must have a valid primary owner, i.e. a '
'Googler with an @google.com or @chromium.org email address.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_valid_first_owner)
def testExpandOwnersWithFakeFilePath(self):
"""Checks that an error is raised with a fake OWNERS file path."""
histograms_with_fake_file_path = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>src/medium/medium/roast/OWNERS</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error, r'The file at .*medium.*OWNERS does not exist\.'):
expand_owners.ExpandHistogramsOWNERS(histograms_with_fake_file_path)
def testExpandOwnersWithoutOwnersFromFile(self):
"""Checks that an error is raised when no owners can be derived."""
absolute_path = _MakeOwnersFile('empty_OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write('') # Write to the file so that it exists.
histograms_without_owners_from_file = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{}</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(src_relative_path))
with self.assertRaisesRegex(
expand_owners.Error,
r'No emails could be derived from .*empty_OWNERS\.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_owners_from_file)
def testExpandOwnersWithSameOwners(self):
"""
Checks that no error is raised when all owners in a file are already in
<owner> elements.
"""
absolute_path = _MakeOwnersFile('same_OWNERS', self.temp_dir)
src_relative_path = _GetSrcRelativePath(absolute_path)
with open(absolute_path, 'w') as owners_file:
owners_file.write(
'[email protected]') # Write to the file so that it exists.
histograms_string = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>{}</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""".format(src_relative_path))
self.assertIsNone(expand_owners.ExpandHistogramsOWNERS(histograms_string))
def testExpandOwnersWithoutOWNERSPathPrefix(self):
"""Checks that an error is raised when the path is not well-formatted."""
histograms_without_src_prefix = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>latte/OWNERS</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error,
r'The given path latte/OWNERS is not well-formatted.*\.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_src_prefix)
def testExpandOwnersWithoutOWNERSPathSuffix(self):
"""Checks that an error is raised when the path is not well-formatted."""
histograms_without_owners_suffix = xml.dom.minidom.parseString("""
<histograms>
<histogram name="Caffeination" units="mg">
<owner>[email protected]</owner>
<owner>src/latte/file</owner>
<summary>I like coffee.</summary>
</histogram>
</histograms>
""")
with self.assertRaisesRegex(
expand_owners.Error,
r'The given path src/latte/file is not well-formatted.*\.'):
expand_owners.ExpandHistogramsOWNERS(histograms_without_owners_suffix)
def testExtractEmailAddressesUnsupportedSymbolsIgnored(self):
"""Checks that unsupported OWNERS files symbols are ignored.
The unsupported symbols that may appear at the beginning of a line are as
follows:
(i) per-file
(ii) *
(iii) #
(iv) set noparent
(v) white space, e.g. a space or a blank line
"""
absolute_path = _MakeOwnersFile('OWNERS', self.temp_dir)
joe = '[email protected]'
unsupported_symbols = [
'# Words.', ' # Words.', '*', 'per-file *OWNERS=*', 'set noparent'
]
with open(absolute_path, 'w') as owners_file:
owners_file.write('\n'.join([joe + ' # Words.'] + unsupported_symbols))
self.assertEqual(
expand_owners._ExtractEmailAddressesFromOWNERS(absolute_path), [joe])
def testExtractEmailAddressesLoopRaisesError(self):
"""Checks that an error is raised if OWNERS file path results in a loop."""
file_directive_absolute_path = _MakeOwnersFile('loop_OWNERS', self.temp_dir)
directive = _GetFileDirective(file_directive_absolute_path)
with open(file_directive_absolute_path, 'w') as owners_file:
owners_file.write(directive)
with self.assertRaisesRegex(
expand_owners.Error,
r'.*The path.*loop_OWNERS may be part of an OWNERS loop\.'):
expand_owners._ExtractEmailAddressesFromOWNERS(
file_directive_absolute_path)
def testGetHigherLevelPath(self):
"""Checks that higher directories are recursively checked for OWNERS.
Also, checks that there isn't a recursive loop.
"""
path = expand_owners._GetOwnersFilePath('src/banana/chocolate/OWNERS')
result = expand_owners._GetHigherLevelOwnersFilePath(path)
# The condition is true when the tools directory's parent directory is src,
# which is generally the case locally. However, the parent directory is not
# always src, e.g. on various testing bots.
if os.path.basename(_GetToolsParentDir()) == 'src':
self.assertRegex(result, r'.*OWNERS')
else:
self.assertEqual(result, '')
if __name__ == '__main__':
unittest.main()