blob: dc0b391c1d8ab7ed123ded70557793bfd3343160 [file] [log] [blame]
#!/usr/bin/env vpython3
# Copyright 2018 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
import os
import shutil
import subprocess
from unittest import mock
import test_env
from recipe_engine.internal import gitattr_checker
class AttrCheckerEquivalenceTests(test_env.RecipeEngineUnitTest):
def setUp(self):
super(AttrCheckerEquivalenceTests, self).setUp()
self.git_repo = self.tempdir()
self.git('init', '-q')
self.attr_checker = gitattr_checker.AttrChecker(self.git_repo, False)
def git(self, *cmd, **kwargs):
stdin = kwargs.pop('stdin', None)
if stdin:
kwargs['stdin'] = subprocess.PIPE
kwargs.setdefault('stdout', subprocess.PIPE)
kwargs.setdefault('stderr', subprocess.PIPE)
kwargs.setdefault('cwd', self.git_repo)
kwargs.setdefault('text', True)
p = subprocess.Popen(['git'] + list(cmd), **kwargs)
stdout, stderr = p.communicate(stdin)
self.assertEqual(p.returncode, 0, stderr)
return stdout.strip()
def write(self, path, contents):
file_path = os.path.join(self.git_repo, path)
dirname = os.path.dirname(file_path)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
with open(os.path.join(self.git_repo, path), 'w') as f:
f.write('\n'.join(contents))
def commit(self):
self.git('add', '-A')
self.git('commit', '-m', 'foo')
return self.git('rev-parse', 'HEAD')
def getRecipeAttrValue(self, revision, files):
self.git('checkout', revision)
output = self.git('check-attr', '-z', '--stdin', 'recipes',
stdin='\0'.join(files))
# The format of the data returned by check-attr -z is:
#
# <path> NUL <attribute> NUL <info> NUL <path2> NUL <attribute> NUL ...
#
# Where info is the value of the attribute ('set', 'unset', 'unspecified').
# In particular, there are no newlines separating records. Every third NUL
# byte indicates a new record, so we take the 3rd field (index 2) jumping
# three fields each line to yield a list with just the <info> fields.
return [
attr_value not in ('unset', 'unspecified')
for attr_value in output.split('\0')[2::3]
]
def assertEqualAttr(self, revision, files):
actual = self.attr_checker.check_files(revision, files)
expected = self.getRecipeAttrValue(revision, files)
self.assertDictEqual(dict(zip(files, actual)), dict(zip(files, expected)))
def testComments(self):
self.write('.gitattributes',
['# Some comment',
r'\# recipes',
'',
'/foo recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['#', '#foo', 'foo', 'foo#', 'bar/foo', 'bar/#', 'bar/foo#',
'bar/#foo'])
def testBang(self):
self.write('.gitattributes',
[r'\!foo recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['!foo', 'foo', 'bar'])
def testSpecialCharacters(self):
"""Test that characters like . + and $ are handled correctly."""
self.write('.gitattributes',
['foo.bar recipes',
'ab+ recipes',
'x^y$z recipes',
'(paren*) recipes',
'this|that recipes',
r's\wt recipes',
r'\[hello] recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo.bar', 'fooxbar', 'foobar',
'ab+', 'abb', 'abc', 'ab',
'y' 'x^y', 'y$z', 'x^y$z',
'(parent)', '(paren)', 'parent', 'paren', '(paren))',
'this', 'that', 'this|that',
's t', r's\wt',
'h', 'e', 'l', 'o', '[', '[hello]'])
def testQuotedPattern(self):
self.write('.gitattributes',
['"space file" recipes',
'"foo bar.*" recipes',
'inside"?"quote recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['space file', 'space file', 'space file.txt', 'foo/space file',
'foo bar.', 'foo bar.txt', 'foo barx.txt', 'foo', 'bar.txt',
'inside quote', 'inside.quote', 'inside quote', 'inside"?"quote',
'inside" "quote', 'inside""quote'])
def testDoubleStars(self):
# Test /**/
self.write('.gitattributes',
['/**/foo recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'foo/bar', 'bar/foo', 'bar/x/foo', 'bar/xfoo', 'bar/foox'])
self.write('.gitattributes',
['foo/**/bar recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'foo/bar', 'foo/baz/bar', 'foo/x/baz/bar', 'foox/bar',
'foo/xbar', 'xfoo/bar', 'foo/xbar'])
# Test **/
self.write('.gitattributes',
['**/foo recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar/foo', 'foo/bar', 'bar/foo/baz', 'bar', 'bar/foox',
'bar/xfoo'])
# Test /**
self.write('.gitattributes',
['/** recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'foo/bar', 'foo/bar/baz'])
self.write('.gitattributes',
['foo/** recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'foo/bar', 'foo/bar/baz', 'foox/bar', 'xfoo/bar'])
def testStar(self):
self.write('.gitattributes',
['* recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'foo/bar', 'foo/bar/baz'])
self.write('.gitattributes',
['foo* recipes',
'*bar recipes',
'b*az recipes',
'*mu* recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'foox', 'xfoo', 'foxo', 'foo/x', 'x/foo', 'x/xfoo', 'x/foox',
'bar', 'barx', 'xbar', 'baxa', 'bar/x', 'x/bar', 'x/xbar', 'x/barx',
'baz', 'bazx', 'xbaz', 'baxz', 'bxaz', 'baz/x', 'x/baz', 'x/xbaz',
'x/bazx', 'mu', 'lemur', 'mur', 'lemur', 'x/lemur', 'lemur/x'])
def testEscapedStar(self):
self.write('.gitattributes',
[r'foo\* recipes',
r'baz/\** recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'foo*', 'foox', 'bar/foo', 'bar/foo*', 'bar/foox', 'baz/x',
'baz/*x', 'baz/*', 'baz/x/y', 'baz/*x/y'])
def testQuestionMark(self):
self.write('.gitattributes',
['f?o recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'fao', 'f o', 'fo', 'bar/foo', 'bar/fo', 'bar/f o'])
def testAbsolutePath(self):
self.write('.gitattributes',
['/*foo',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'xfoo', 'foox', 'bar/foo', 'bar/xfoo', 'bar/foox'])
def testLastAttributeTakesPrecedence(self):
self.write('.gitattributes',
['foo recipes -recipes !recipes',
'bar -recipes recipes !recipes',
'baz !recipes -recipes recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'baz', 'foo/bar', 'bar/foo', 'baz/bar', 'baz/foo',
'bar/foo/baz'])
def testLastLineTakesPrecedence(self):
self.write('.gitattributes',
['/bar/** recipes',
'/bar/foo/** -recipes',
'/bar/baz/** !recipes',
'/bar/foo/x recipes',
'batman recipes',
'/batman -recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['bar', 'bar/x', 'bar/y/z', 'bar/foo/x', 'bar/foo/y', 'bar/baz/x',
'bar/baz/y/z', 'batman', 'nananananananana/batman'])
def testMoreSpecificTakesPrecedence(self):
self.write('.gitattributes',
['foo !recipes',
'bar -recipes',])
self.write('a/.gitattributes',
['foo -recipes',
'bar !recipes',
'baz recipes',])
self.write('a/b/.gitattributes',
['foo recipes',
'bar recipes',])
revision = self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'baz', 'a/foo', 'a/bar', 'a/baz', 'a/b/foo', 'a/b/bar',
'a/b/baz', 'a/b/c/foo', 'a/b/c/bar', 'a/b/c/baz'])
def testProcessesTheRightRevision(self):
self.write('.gitattributes',
['foo recipes',
'bar recipes',
'bar/foo !recipes',])
revision = self.commit()
self.write('.gitattributes',
['foo recipes',
'bar recipes',])
self.commit()
self.assertEqualAttr(
revision,
['foo', 'bar', 'bar/foo', 'foo/bar', 'bar/foo/baz'])
class AttrCheckerMockTests(test_env.RecipeEngineUnitTest):
def setUp(self):
super(AttrCheckerMockTests, self).setUp()
self._attr_checker = gitattr_checker.AttrChecker('repo', False)
self._blobs = {
'blob1': [
'foo recipes',
'bar lemur',
],
'blob2': [
'Neque porro quisquam est qui dolorem ipsum quia dolor',
'sit amet, consectetur, adipisci velit',
],
'blob3': [
'foo -recipes',
'foo lemur',
],
'blob4': [
'foo recipes',
'bar recipes',
],
}
self._tree = {
'rev1': {
'.gitattributes': 'blob1',
'irrelevant/lorem-ipsum.txt': 'blob2',
'irrelevant/.gitattributes': 'blob4',
},
'rev2': {
'.gitattributes': 'blob1',
},
'rev3': {
'.gitattributes': 'blob1',
'baz/.gitattributes': 'blob3',
},
'rev4': {
'.gitattributes': 'blob4',
'baz/.gitattributes': 'blob3',
},
'rev5': {
'irrelevant/lorem-ipsum.txt': 'blob2',
'irrelevant/.gitattributes': 'blob4',
},
}
self._git_mock = mock.Mock()
self._git_mock.side_effect = self._fake_git
mock.patch('recipe_engine.internal.gitattr_checker.AttrChecker._git',
self._git_mock).start()
self.addCleanup(mock.patch.stopall)
def _fake_git(self, cmd, stdin=None):
self.assertEqual(cmd[0], 'cat-file')
if not cmd[1].startswith('--batch-check'):
return self._blobs[cmd[-1]]
self.assertIsNotNone(stdin)
result = []
for line in stdin.splitlines():
rev, path = line.split(':')
if path not in self._tree[rev]:
result.append(line + ' missing')
else:
result.append(self._tree[rev][path])
return result
def assertNewCalls(self, expected_calls):
self.assertEqual(self._git_mock.mock_calls, expected_calls)
self._git_mock.reset_mock()
def testDoesntQueryNonGitattributesFiles(self):
# We should only ask information about the .gitattributes files that affect
# the modified files.
self.assertEqual(
self._attr_checker.check_files('rev1', ['foo', 'bar', 'baz/foo']),
[True, False, True]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev1:.gitattributes\nrev1:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob1']),
])
def testCachesGitattributesFiles(self):
self.assertEqual(
self._attr_checker.check_files('rev1', ['foo', 'bar', 'baz/foo']),
[True, False, True]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev1:.gitattributes\nrev1:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob1']),
])
# The revision changed, but the .gitattributes files did not. We shouldn't
# ask git for information about file blobs.
self.assertEqual(
self._attr_checker.check_files('rev2', ['foo', 'bar', 'baz/foo']),
[True, False, True]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev2:.gitattributes\nrev2:baz/.gitattributes'),
])
def testQueriesNewGitattributesFile(self):
self.assertEqual(
self._attr_checker.check_files('rev2', ['foo', 'bar', 'baz/foo']),
[True, False, True]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev2:.gitattributes\nrev2:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob1']),
])
# A new .gitattribute file was added, but the old one hasn't changed.
self.assertEqual(
self._attr_checker.check_files('rev3', ['foo', 'bar', 'baz/foo']),
[True, False, False]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev3:.gitattributes\nrev3:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob3']),
])
def testQueriesModifiedGitattributesFile(self):
self.assertEqual(
self._attr_checker.check_files('rev3', ['foo', 'bar', 'baz/foo']),
[True, False, False]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev3:.gitattributes\nrev3:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob1']),
mock.call(['cat-file', 'blob', 'blob3']),
])
# The .gitattribute file was modified
self.assertEqual(
self._attr_checker.check_files('rev4', ['foo', 'bar', 'baz/foo']),
[True, True, False]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev4:.gitattributes\nrev4:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob4']),
])
def testDeletedGitattributesFile(self):
self.assertEqual(
self._attr_checker.check_files('rev1', ['foo', 'bar', 'baz/foo']),
[True, False, True]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev1:.gitattributes\nrev1:baz/.gitattributes'),
mock.call(['cat-file', 'blob', 'blob1']),
])
# The .gitattribute file was deleted
self.assertEqual(
self._attr_checker.check_files('rev5', ['foo', 'bar', 'baz/foo']),
[False, False, False]
)
self.assertNewCalls([
mock.call(['cat-file', '--batch-check=%(objectname)'],
'rev5:.gitattributes\nrev5:baz/.gitattributes'),
])
if __name__ == '__main__':
test_env.main()