Commit f46aed99 authored by pam@chromium.org's avatar pam@chromium.org

Show a list of directories missing OWNER reviewers on upload, and show...

Show a list of directories missing OWNER reviewers on upload, and show directories rather than files missing OWNER approvals on commit.

BUG=117166
TEST=covered by presubmit unittests

Review URL: http://codereview.chromium.org/9621012

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@125588 0039d316-1c4b-4281-b951-d872f2087c98
parent dd447942
# Copyright (c) 2010 The Chromium Authors. All rights reserved. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
...@@ -75,29 +75,27 @@ class Database(object): ...@@ -75,29 +75,27 @@ class Database(object):
self._load_data_needed_for(files) self._load_data_needed_for(files)
return self._covering_set_of_owners_for(files) return self._covering_set_of_owners_for(files)
def files_are_covered_by(self, files, reviewers): def directories_not_covered_by(self, files, reviewers):
"""Returns whether every file is owned by at least one reviewer.""" """Returns the set of directories that are not owned by a reviewer.
return not self.files_not_covered_by(files, reviewers)
def files_not_covered_by(self, files, reviewers): Determines which of the given files are not owned by at least one of the
"""Returns the set of files that are not owned by at least one reviewer. reviewers, then returns a set containing the applicable enclosing
directories, i.e. the ones upward from the files that have OWNERS files.
Args: Args:
files is a sequence of paths relative to (and under) self.root. files is a sequence of paths relative to (and under) self.root.
reviewers is a sequence of strings matching self.email_regexp.""" reviewers is a sequence of strings matching self.email_regexp.
"""
self._check_paths(files) self._check_paths(files)
self._check_reviewers(reviewers) self._check_reviewers(reviewers)
if not reviewers:
return files
self._load_data_needed_for(files) self._load_data_needed_for(files)
files_by_dir = self._files_by_dir(files)
dirs = set([self.os_path.dirname(f) for f in files])
covered_dirs = self._dirs_covered_by(reviewers) covered_dirs = self._dirs_covered_by(reviewers)
uncovered_files = [] uncovered_dirs = [self._enclosing_dir_with_owners(d) for d in dirs
for d, files_in_d in files_by_dir.iteritems(): if not self._is_dir_covered_by(d, covered_dirs)]
if not self._is_dir_covered_by(d, covered_dirs):
uncovered_files.extend(files_in_d) return set(uncovered_dirs)
return set(uncovered_files)
def _check_paths(self, files): def _check_paths(self, files):
def _is_under(f, pfx): def _is_under(f, pfx):
...@@ -109,12 +107,6 @@ class Database(object): ...@@ -109,12 +107,6 @@ class Database(object):
_assert_is_collection(reviewers) _assert_is_collection(reviewers)
assert all(self.email_regexp.match(r) for r in reviewers) assert all(self.email_regexp.match(r) for r in reviewers)
def _files_by_dir(self, files):
dirs = {}
for f in files:
dirs.setdefault(self.os_path.dirname(f), []).append(f)
return dirs
def _dirs_covered_by(self, reviewers): def _dirs_covered_by(self, reviewers):
dirs = self.owned_by[EVERYONE] dirs = self.owned_by[EVERYONE]
for r in reviewers: for r in reviewers:
...@@ -129,6 +121,15 @@ class Database(object): ...@@ -129,6 +121,15 @@ class Database(object):
dirname = self.os_path.dirname(dirname) dirname = self.os_path.dirname(dirname)
return dirname in covered_dirs return dirname in covered_dirs
def _enclosing_dir_with_owners(self, directory):
"""Returns the innermost enclosing directory that has an OWNERS file."""
dirpath = directory
while not dirpath in self.owners_for:
if self._stop_looking(dirpath):
break
dirpath = self.os_path.dirname(dirpath)
return dirpath
def _load_data_needed_for(self, files): def _load_data_needed_for(self, files):
for f in files: for f in files:
dirpath = self.os_path.dirname(f) dirpath = self.os_path.dirname(f)
......
...@@ -729,47 +729,62 @@ def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings, ...@@ -729,47 +729,62 @@ def CheckBuildbotPendingBuilds(input_api, output_api, url, max_pendings,
def CheckOwners(input_api, output_api, source_file_filter=None): def CheckOwners(input_api, output_api, source_file_filter=None):
if not input_api.is_committing: if input_api.is_committing:
return [] if input_api.tbr:
if input_api.tbr: return [output_api.PresubmitNotifyResult(
return [output_api.PresubmitNotifyResult( '--tbr was specified, skipping OWNERS check')]
'--tbr was specified, skipping OWNERS check')] if not input_api.change.issue:
if not input_api.change.issue: return [output_api.PresubmitError("OWNERS check failed: this change has "
return [output_api.PresubmitError( "no Rietveld issue number, so we can't check it for approvals.")]
"OWNERS check failed: this change has no Rietveld issue number, so " needed = 'LGTM from an OWNER'
"we can't check it for approvals.")] output = output_api.PresubmitError
else:
needed = 'OWNER reviewers'
output = output_api.PresubmitNotifyResult
affected_files = set([f.LocalPath() for f in affected_files = set([f.LocalPath() for f in
input_api.change.AffectedFiles(file_filter=source_file_filter)]) input_api.change.AffectedFiles(file_filter=source_file_filter)])
owners_db = input_api.owners_db owners_db = input_api.owners_db
owner_email, approvers = _RietveldOwnerAndApprovers(input_api, owner_email, reviewers = _RietveldOwnerAndReviewers(
owners_db.email_regexp) input_api,
if not owner_email: owners_db.email_regexp,
approval_needed=input_api.is_committing)
if owner_email:
reviewers_plus_owner = reviewers.union(set([owner_email]))
elif input_api.is_committing:
return [output_api.PresubmitWarning( return [output_api.PresubmitWarning(
'The issue was not uploaded so you have no OWNER approval.')] 'The issue was not uploaded so you have no OWNER approval.')]
else:
owner_email = ''
reviewers_plus_owner = set()
approvers_plus_owner = approvers.union(set([owner_email])) missing_directories = owners_db.directories_not_covered_by(affected_files,
reviewers_plus_owner)
missing_files = owners_db.files_not_covered_by(affected_files, if missing_directories:
approvers_plus_owner) return [output('Missing %s for files in these directories:\n %s' %
if missing_files: (needed, '\n '.join(missing_directories)))]
return [output_api.PresubmitError('Missing LGTM from an OWNER for: %s' %
','.join(missing_files))]
if not approvers: if input_api.is_committing and not reviewers:
return [output_api.PresubmitError('Missing LGTM from someone other than %s' return [output('Missing LGTM from someone other than %s' % owner_email)]
% owner_email)]
return [] return []
def _RietveldOwnerAndApprovers(input_api, email_regexp): def _RietveldOwnerAndReviewers(input_api, email_regexp, approval_needed=False):
"""Return the owner and approvers of a change, if any.""" """Return the owner and reviewers of a change, if any.
If approval_needed is True, only reviewers who have approved the change
will be returned.
"""
if not input_api.change.issue: if not input_api.change.issue:
return None, None return None, None
issue_props = input_api.rietveld.get_issue_properties( issue_props = input_api.rietveld.get_issue_properties(
int(input_api.change.issue), True) int(input_api.change.issue), True)
if not approval_needed:
return issue_props['owner_email'], set(issue_props['reviewers'])
owner_email = issue_props['owner_email'] owner_email = issue_props['owner_email']
def match_reviewer(r): def match_reviewer(r):
......
#!/usr/bin/env python #!/usr/bin/env python
# Copyright (c) 2011 The Chromium Authors. All rights reserved. # Copyright (c) 2012 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be # Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file. # found in the LICENSE file.
...@@ -44,6 +44,9 @@ def test_repo(): ...@@ -44,6 +44,9 @@ def test_repo():
'/chrome/renderer/safe_browsing/scorer.h': '', '/chrome/renderer/safe_browsing/scorer.h': '',
'/content/OWNERS': owners_file(john, darin, comment='foo', noparent=True), '/content/OWNERS': owners_file(john, darin, comment='foo', noparent=True),
'/content/content.gyp': '', '/content/content.gyp': '',
'/content/bar/foo.cc': '',
'/content/baz/OWNERS': owners_file(brett),
'/content/baz/froboz.h': '',
'/content/views/OWNERS': owners_file(ben, john, owners.EVERYONE, '/content/views/OWNERS': owners_file(ben, john, owners.EVERYONE,
noparent=True), noparent=True),
}) })
...@@ -65,69 +68,67 @@ class OwnersDatabaseTest(unittest.TestCase): ...@@ -65,69 +68,67 @@ class OwnersDatabaseTest(unittest.TestCase):
def test_constructor(self): def test_constructor(self):
self.assertNotEquals(self.db(), None) self.assertNotEquals(self.db(), None)
def assert_covered_by(self, files, reviewers): def test_dirs_not_covered_by__valid_inputs(self):
db = self.db() db = self.db()
self.assertTrue(db.files_are_covered_by(set(files), set(reviewers)))
def test_covered_by__everyone(self): # Check that we're passed in a sequence that isn't a string.
self.assert_covered_by(['DEPS'], [john]) self.assertRaises(AssertionError, db.directories_not_covered_by, 'foo', [])
self.assert_covered_by(['DEPS'], [darin]) if hasattr(owners.collections, 'Iterable'):
self.assertRaises(AssertionError, db.directories_not_covered_by,
def test_covered_by__explicit(self): (f for f in ['x', 'y']), [])
self.assert_covered_by(['content/content.gyp'], [john])
self.assert_covered_by(['chrome/gpu/OWNERS'], [ken])
def test_covered_by__owners_plus_everyone(self):
self.assert_covered_by(['/content/views/OWNERS'], [ben])
self.assert_covered_by(['/content/views/OWNERS'], [ken])
def test_covered_by__owners_propagates_down(self): # Check that the files are under the root.
self.assert_covered_by(['chrome/gpu/OWNERS'], [ben]) db.root = '/checkout'
self.assertRaises(AssertionError, db.directories_not_covered_by,
['/OWNERS'], [])
db.root = '/'
def test_covered_by__no_file_in_dir(self): # Check invalid email address.
self.assert_covered_by(['/chrome/renderer/gpu/gpu_channel_host.h'], [peter]) self.assertRaises(AssertionError, db.directories_not_covered_by,
['OWNERS'], ['foo'])
def assert_not_covered_by(self, files, reviewers, unreviewed_files): def assert_dirs_not_covered_by(self, files, reviewers, unreviewed_dirs):
db = self.db() db = self.db()
self.assertEquals(db.files_not_covered_by(set(files), set(reviewers)), self.assertEquals(
set(unreviewed_files)) db.directories_not_covered_by(set(files), set(reviewers)),
set(unreviewed_dirs))
def test_not_covered_by__need_at_least_one_reviewer(self): def test_dirs_not_covered_by__owners_propagates_down(self):
self.assert_not_covered_by(['DEPS'], [], ['DEPS']) self.assert_dirs_not_covered_by(
def test_not_covered_by__owners_propagates_down(self):
self.assert_not_covered_by(
['chrome/gpu/gpu_channel.h', 'chrome/renderer/gpu/gpu_channel_host.h'], ['chrome/gpu/gpu_channel.h', 'chrome/renderer/gpu/gpu_channel_host.h'],
[ben], []) [ben], [])
def test_not_covered_by__partial_covering(self): def test_dirs_not_covered_by__partial_covering(self):
self.assert_not_covered_by( self.assert_dirs_not_covered_by(
['content/content.gyp', 'chrome/renderer/gpu/gpu_channel_host.h'], ['content/content.gyp', 'chrome/renderer/gpu/gpu_channel_host.h'],
[peter], ['content/content.gyp']) [peter], ['content'])
def test_not_covered_by__set_noparent_works(self):
self.assert_not_covered_by(['content/content.gyp'], [ben],
['content/content.gyp'])
def test_not_covered_by__valid_inputs(self): def test_dirs_not_covered_by__set_noparent_works(self):
db = self.db() self.assert_dirs_not_covered_by(['content/content.gyp'], [ben],
['content'])
# Check that we're passed in a sequence that isn't a string.
self.assertRaises(AssertionError, db.files_not_covered_by, 'foo', [])
if hasattr(owners.collections, 'Iterable'):
self.assertRaises(AssertionError, db.files_not_covered_by,
(f for f in ['x', 'y']), [])
# Check that the files are under the root.
db.root = '/checkout'
self.assertRaises(AssertionError, db.files_not_covered_by, ['/OWNERS'],
[])
db.root = '/'
# Check invalid email address.
self.assertRaises(AssertionError, db.files_not_covered_by, ['OWNERS'],
['foo'])
def test_dirs_not_covered_by__no_reviewer(self):
self.assert_dirs_not_covered_by(
['content/content.gyp', 'chrome/renderer/gpu/gpu_channel_host.h'],
[], ['content'])
def test_dirs_not_covered_by__combines_directories(self):
self.assert_dirs_not_covered_by(['content/content.gyp',
'content/bar/foo.cc',
'chrome/renderer/gpu/gpu_channel_host.h'],
[peter],
['content'])
def test_dirs_not_covered_by__multiple_directories(self):
self.assert_dirs_not_covered_by(
['content/content.gyp', # Not covered
'content/bar/foo.cc', # Not covered (combines in)
'content/baz/froboz.h', # Not covered
'chrome/gpu/gpu_channel.h', # Owned by ken
'chrome/renderer/gpu/gpu_channel_host.h' # Owned by * via parent
],
[ken],
['content', 'content/baz'])
def assert_reviewers_for(self, files, expected_reviewers): def assert_reviewers_for(self, files, expected_reviewers):
db = self.db() db = self.db()
......
...@@ -2109,11 +2109,15 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2109,11 +2109,15 @@ class CannedChecksUnittest(PresubmitTestsBase):
presubmit.OutputApi.PresubmitNotifyResult) presubmit.OutputApi.PresubmitNotifyResult)
def AssertOwnersWorks(self, tbr=False, issue='1', approvers=None, def AssertOwnersWorks(self, tbr=False, issue='1', approvers=None,
rietveld_response=None, uncovered_files=None, expected_output=''): reviewers=None, is_committing=True, rietveld_response=None,
uncovered_dirs=None, expected_output=''):
if approvers is None: if approvers is None:
approvers = set() approvers = set()
if uncovered_files is None: if reviewers is None:
uncovered_files = set() reviewers = set()
reviewers = reviewers.union(approvers)
if uncovered_dirs is None:
uncovered_dirs = set()
change = self.mox.CreateMock(presubmit.Change) change = self.mox.CreateMock(presubmit.Change)
change.issue = issue change.issue = issue
...@@ -2122,11 +2126,11 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2122,11 +2126,11 @@ class CannedChecksUnittest(PresubmitTestsBase):
fake_db = self.mox.CreateMock(owners.Database) fake_db = self.mox.CreateMock(owners.Database)
fake_db.email_regexp = input_api.re.compile(owners.BASIC_EMAIL_REGEXP) fake_db.email_regexp = input_api.re.compile(owners.BASIC_EMAIL_REGEXP)
input_api.owners_db = fake_db input_api.owners_db = fake_db
input_api.is_committing = True input_api.is_committing = is_committing
input_api.tbr = tbr input_api.tbr = tbr
if not tbr and issue: if not is_committing or (not tbr and issue):
affected_file.LocalPath().AndReturn('foo.cc') affected_file.LocalPath().AndReturn('foo/xyz.cc')
change.AffectedFiles(file_filter=None).AndReturn([affected_file]) change.AffectedFiles(file_filter=None).AndReturn([affected_file])
owner_email = 'john@example.com' owner_email = 'john@example.com'
if not rietveld_response: if not rietveld_response:
...@@ -2136,11 +2140,21 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2136,11 +2140,21 @@ class CannedChecksUnittest(PresubmitTestsBase):
{"sender": a, "text": "I approve", "approval": True} {"sender": a, "text": "I approve", "approval": True}
for a in approvers for a in approvers
], ],
"reviewers": reviewers
} }
input_api.rietveld.get_issue_properties(
int(input_api.change.issue), True).AndReturn(rietveld_response) if is_committing:
fake_db.files_not_covered_by(set(['foo.cc']), people = approvers
approvers.union(set([owner_email]))).AndReturn(uncovered_files) else:
people = reviewers
if issue:
input_api.rietveld.get_issue_properties(
int(input_api.change.issue), True).AndReturn(rietveld_response)
people.add(owner_email)
fake_db.directories_not_covered_by(set(['foo/xyz.cc']),
people).AndReturn(uncovered_dirs)
self.mox.ReplayAll() self.mox.ReplayAll()
output = presubmit.PresubmitOutput() output = presubmit.PresubmitOutput()
...@@ -2158,11 +2172,17 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2158,11 +2172,17 @@ class CannedChecksUnittest(PresubmitTestsBase):
"sender": "ben@example.com", "text": "foo", "approval": True, "sender": "ben@example.com", "text": "foo", "approval": True,
}, },
], ],
"reviewers": ["ben@example.com"],
} }
self.AssertOwnersWorks(approvers=set(['ben@example.com']), self.AssertOwnersWorks(approvers=set(['ben@example.com']),
rietveld_response=response, rietveld_response=response,
expected_output='') expected_output='')
self.AssertOwnersWorks(approvers=set(['ben@example.com']),
is_committing=False,
rietveld_response=response,
expected_output='')
def testCannedCheckOwners_NotApproved(self): def testCannedCheckOwners_NotApproved(self):
response = { response = {
"owner_email": "john@example.com", "owner_email": "john@example.com",
...@@ -2171,46 +2191,89 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2171,46 +2191,89 @@ class CannedChecksUnittest(PresubmitTestsBase):
"sender": "ben@example.com", "text": "foo", "approval": False, "sender": "ben@example.com", "text": "foo", "approval": False,
}, },
], ],
"reviewers": ["ben@example.com"],
} }
self.AssertOwnersWorks( self.AssertOwnersWorks(
approvers=set(), approvers=set(),
reviewers=set(["ben@example.com"]),
rietveld_response=response, rietveld_response=response,
expected_output= expected_output=
'Missing LGTM from someone other than john@example.com\n') 'Missing LGTM from someone other than john@example.com\n')
self.AssertOwnersWorks(
approvers=set(),
reviewers=set(["ben@example.com"]),
is_committing=False,
rietveld_response=response,
expected_output='')
def testCannedCheckOwners_NoReviewers(self):
response = {
"owner_email": "john@example.com",
"messages": [
{
"sender": "ben@example.com", "text": "foo", "approval": False,
},
],
"reviewers":[],
}
self.AssertOwnersWorks(
approvers=set(),
reviewers=set(),
rietveld_response=response,
expected_output=
'Missing LGTM from someone other than john@example.com\n')
self.AssertOwnersWorks(
approvers=set(),
reviewers=set(),
is_committing=False,
rietveld_response=response,
expected_output='')
def testCannedCheckOwners_NoIssue(self): def testCannedCheckOwners_NoIssue(self):
self.AssertOwnersWorks(issue=None, self.AssertOwnersWorks(issue=None,
expected_output="OWNERS check failed: this change has no Rietveld " expected_output="OWNERS check failed: this change has no Rietveld "
"issue number, so we can't check it for approvals.\n") "issue number, so we can't check it for approvals.\n")
self.AssertOwnersWorks(issue=None, is_committing=False,
expected_output="")
def testCannedCheckOwners_NoLGTM(self): def testCannedCheckOwners_NoLGTM(self):
self.AssertOwnersWorks(expected_output='Missing LGTM from someone ' self.AssertOwnersWorks(expected_output='Missing LGTM from someone '
'other than john@example.com\n') 'other than john@example.com\n')
self.AssertOwnersWorks(is_committing=False, expected_output='')
def testCannedCheckOwners_OnlyOwnerLGTM(self): def testCannedCheckOwners_OnlyOwnerLGTM(self):
self.AssertOwnersWorks(approvers=set(['john@example.com']), self.AssertOwnersWorks(approvers=set(['john@example.com']),
expected_output='Missing LGTM from someone ' expected_output='Missing LGTM from someone '
'other than john@example.com\n') 'other than john@example.com\n')
self.AssertOwnersWorks(approvers=set(['john@example.com']),
is_committing=False,
expected_output='')
def testCannedCheckOwners_TBR(self): def testCannedCheckOwners_TBR(self):
self.AssertOwnersWorks(tbr=True, self.AssertOwnersWorks(tbr=True,
expected_output='--tbr was specified, skipping OWNERS check\n') expected_output='--tbr was specified, skipping OWNERS check\n')
self.AssertOwnersWorks(tbr=True, is_committing=False, expected_output='')
def testCannedCheckOwners_Upload(self):
class FakeInputAPI(object):
is_committing = False
results = presubmit_canned_checks.CheckOwners(FakeInputAPI(),
presubmit.OutputApi)
self.assertEqual(results, [])
def testCannedCheckOwners_WithoutOwnerLGTM(self): def testCannedCheckOwners_WithoutOwnerLGTM(self):
self.AssertOwnersWorks(uncovered_files=set(['foo.cc']), self.AssertOwnersWorks(uncovered_dirs=set(['foo']),
expected_output='Missing LGTM from an OWNER for: foo.cc\n') expected_output='Missing LGTM from an OWNER for files in these '
'directories:\n'
' foo\n')
self.AssertOwnersWorks(uncovered_dirs=set(['foo']),
is_committing=False,
expected_output='Missing OWNER reviewers for files in these '
'directories:\n'
' foo\n')
def testCannedCheckOwners_WithLGTMs(self): def testCannedCheckOwners_WithLGTMs(self):
self.AssertOwnersWorks(approvers=set(['ben@example.com']), self.AssertOwnersWorks(approvers=set(['ben@example.com']),
uncovered_files=set()) uncovered_dirs=set())
self.AssertOwnersWorks(approvers=set(['ben@example.com']),
is_committing=False,
uncovered_dirs=set())
def testCannedRunUnitTests(self): def testCannedRunUnitTests(self):
change = presubmit.Change( change = presubmit.Change(
...@@ -2299,7 +2362,7 @@ class CannedChecksUnittest(PresubmitTestsBase): ...@@ -2299,7 +2362,7 @@ class CannedChecksUnittest(PresubmitTestsBase):
text_files=None, text_files=None,
license_header=None, license_header=None,
project_name=None, project_name=None,
owners_check=True) owners_check=False)
self.assertEqual(1, len(results)) self.assertEqual(1, len(results))
self.assertEqual( self.assertEqual(
'Found line ending with white spaces in:', results[0]._message) 'Found line ending with white spaces in:', results[0]._message)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment