Change the OWNERS check to print files, not directories

Currently, when we run the OWNERS check, we print the list of directories
that contain the relevant OWNERS files for any modified files in a change
still needing approval. 

This has two problems:

1) if we bubble all the way up to the top level OWNERS, we print "" instead of
"src/" or something more useful (bug 157191)

2) for OWNERS files that contain per-file set-noparent entries (like changes to IPC messages), this can be really confusing because an owner of other stuff in the directory might've approved things already.

This change will now print the list of files in the CL that are still unapproved.
This might be a lot more verbose (since you get N lines rather than 1 for N files in a given directory), but hopefully it'll be clearer in the two cases above.

Also, this change takes care of some lingering clean-up in the code to rename some methods to be clearer.

R=maruel@chromium.org
BUG=157191




Review URL: https://chromiumcodereview.appspot.com/12314044

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@184219 0039d316-1c4b-4281-b951-d872f2087c98
experimental/szager/collated-output
dpranke@chromium.org 13 years ago
parent 0c16fe6f4a
commit 6b1e3ee9e3

@ -129,13 +129,8 @@ class Database(object):
suggested_owners = set(['<anyone>']) suggested_owners = set(['<anyone>'])
return suggested_owners return suggested_owners
# TODO(dpranke): rename to objects_not_covered_by def files_not_covered_by(self, files, reviewers):
def directories_not_covered_by(self, files, reviewers): """Returns the files not owned by one of the reviewers.
"""Returns the set of directories that are not owned by a reviewer.
Determines which of the given files are not owned by at least one of the
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.
@ -145,20 +140,11 @@ class Database(object):
self._check_reviewers(reviewers) self._check_reviewers(reviewers)
self._load_data_needed_for(files) self._load_data_needed_for(files)
objs = set()
for f in files:
if f in self.owners_for:
objs.add(f)
else:
objs.add(self.os_path.dirname(f))
covered_objs = self._objs_covered_by(reviewers) covered_objs = self._objs_covered_by(reviewers)
uncovered_objs = [self._enclosing_obj_with_owners(o) for o in objs uncovered_files = [f for f in files
if not self._is_obj_covered_by(o, covered_objs)] if not self._is_obj_covered_by(f, covered_objs)]
return set(uncovered_objs) return set(uncovered_files)
objects_not_covered_by = directories_not_covered_by
def _check_paths(self, files): def _check_paths(self, files):
def _is_under(f, pfx): def _is_under(f, pfx):
@ -171,38 +157,29 @@ 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)
# TODO(dpranke): Rename to _objs_covered_by and update_callers def _objs_covered_by(self, reviewers):
def _dirs_covered_by(self, reviewers): objs = self.owned_by[EVERYONE]
dirs = self.owned_by[EVERYONE]
for r in reviewers: for r in reviewers:
dirs = dirs | self.owned_by.get(r, set()) objs = objs | self.owned_by.get(r, set())
return dirs return objs
_objs_covered_by = _dirs_covered_by
def _stop_looking(self, dirname): def _stop_looking(self, objname):
return dirname in self.stop_looking return objname in self.stop_looking
# TODO(dpranke): Rename to _is_dir_covered_by and update callers. def _is_obj_covered_by(self, objname, covered_objs):
def _is_dir_covered_by(self, dirname, covered_dirs): while not objname in covered_objs and not self._stop_looking(objname):
while not dirname in covered_dirs and not self._stop_looking(dirname): objname = self.os_path.dirname(objname)
dirname = self.os_path.dirname(dirname) return objname in covered_objs
return dirname in covered_dirs
_is_obj_covered_by = _is_dir_covered_by def _enclosing_dir_with_owners(self, objname):
# TODO(dpranke): Rename to _enclosing_obj_with_owners and update callers.
def _enclosing_dir_with_owners(self, directory):
"""Returns the innermost enclosing directory that has an OWNERS file.""" """Returns the innermost enclosing directory that has an OWNERS file."""
dirpath = directory dirpath = objname
while not dirpath in self.owners_for: while not dirpath in self.owners_for:
if self._stop_looking(dirpath): if self._stop_looking(dirpath):
break break
dirpath = self.os_path.dirname(dirpath) dirpath = self.os_path.dirname(dirpath)
return dirpath return dirpath
_enclosing_obj_with_owners = _enclosing_dir_with_owners
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)

@ -797,16 +797,16 @@ def CheckOwners(input_api, output_api, source_file_filter=None):
reviewers_plus_owner = reviewers.union(set([owner_email])) reviewers_plus_owner = reviewers.union(set([owner_email]))
else: else:
message = ('\nUntil the issue is uploaded, this list will include ' message = ('\nUntil the issue is uploaded, this list will include '
'directories for which you \nare an OWNER.') 'files for which you are an OWNER.')
owner_email = '' owner_email = ''
reviewers_plus_owner = set() reviewers_plus_owner = set()
missing_directories = owners_db.directories_not_covered_by(affected_files, missing_files = owners_db.files_not_covered_by(affected_files,
reviewers_plus_owner) reviewers_plus_owner)
if missing_directories: if missing_files:
output_list = [ output_list = [
output('Missing %s for files in these directories:\n %s%s' % output('Missing %s for these files:\n %s%s' %
(needed, '\n '.join(missing_directories), message))] (needed, '\n '.join(missing_files), message))]
if not input_api.is_committing: if not input_api.is_committing:
suggested_owners = owners_db.reviewers_for(affected_files) suggested_owners = owners_db.reviewers_for(affected_files)
output_list.append(output('Suggested OWNERS:\n %s' % output_list.append(output('Suggested OWNERS:\n %s' %

@ -80,59 +80,59 @@ class OwnersDatabaseTest(_BaseTestCase):
def test_constructor(self): def test_constructor(self):
self.assertNotEquals(self.db(), None) self.assertNotEquals(self.db(), None)
def test_dirs_not_covered_by__valid_inputs(self): def test_files_not_covered_by__valid_inputs(self):
db = self.db() db = self.db()
# Check that we're passed in a sequence that isn't a string. # Check that we're passed in a sequence that isn't a string.
self.assertRaises(AssertionError, db.directories_not_covered_by, 'foo', []) self.assertRaises(AssertionError, db.files_not_covered_by, 'foo', [])
if hasattr(owners.collections, 'Iterable'): if hasattr(owners.collections, 'Iterable'):
self.assertRaises(AssertionError, db.directories_not_covered_by, self.assertRaises(AssertionError, db.files_not_covered_by,
(f for f in ['x', 'y']), []) (f for f in ['x', 'y']), [])
# Check that the files are under the root. # Check that the files are under the root.
db.root = '/checkout' db.root = '/checkout'
self.assertRaises(AssertionError, db.directories_not_covered_by, self.assertRaises(AssertionError, db.files_not_covered_by,
['/OWNERS'], []) ['/OWNERS'], [])
db.root = '/' db.root = '/'
# Check invalid email address. # Check invalid email address.
self.assertRaises(AssertionError, db.directories_not_covered_by, self.assertRaises(AssertionError, db.files_not_covered_by,
['OWNERS'], ['foo']) ['OWNERS'], ['foo'])
def assert_dirs_not_covered_by(self, files, reviewers, unreviewed_dirs): def assert_files_not_covered_by(self, files, reviewers, unreviewed_files):
db = self.db() db = self.db()
self.assertEquals( self.assertEquals(db.files_not_covered_by(set(files), set(reviewers)),
db.directories_not_covered_by(set(files), set(reviewers)), set(unreviewed_files))
set(unreviewed_dirs))
def test_dirs_not_covered_by__owners_propagates_down(self): def test_files_not_covered_by__owners_propagates_down(self):
self.assert_dirs_not_covered_by( self.assert_files_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_dirs_not_covered_by__partial_covering(self): def test_files_not_covered_by__partial_covering(self):
self.assert_dirs_not_covered_by( self.assert_files_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']) [peter], ['content/content.gyp'])
def test_dirs_not_covered_by__set_noparent_works(self): def test_files_not_covered_by__set_noparent_works(self):
self.assert_dirs_not_covered_by(['content/content.gyp'], [ben], self.assert_files_not_covered_by(['content/content.gyp'], [ben],
['content']) ['content/content.gyp'])
def test_dirs_not_covered_by__no_reviewer(self): def test_files_not_covered_by__no_reviewer(self):
self.assert_dirs_not_covered_by( self.assert_files_not_covered_by(
['content/content.gyp', 'chrome/renderer/gpu/gpu_channel_host.h'], ['content/content.gyp', 'chrome/renderer/gpu/gpu_channel_host.h'],
[], ['content']) [], ['content/content.gyp'])
def test_dirs_not_covered_by__combines_directories(self): def test_files_not_covered_by__combines_directories(self):
self.assert_dirs_not_covered_by(['content/content.gyp', self.assert_files_not_covered_by(['content/content.gyp',
'content/bar/foo.cc', 'content/bar/foo.cc',
'chrome/renderer/gpu/gpu_channel_host.h'], 'chrome/renderer/gpu/gpu_channel_host.h'],
[peter], [peter],
['content']) ['content/content.gyp',
'content/bar/foo.cc'])
def test_dirs_not_covered_by__multiple_directories(self): def test_files_not_covered_by__multiple_directories(self):
self.assert_dirs_not_covered_by( self.assert_files_not_covered_by(
['content/content.gyp', # Not covered ['content/content.gyp', # Not covered
'content/bar/foo.cc', # Not covered (combines in) 'content/bar/foo.cc', # Not covered (combines in)
'content/baz/froboz.h', # Not covered 'content/baz/froboz.h', # Not covered
@ -140,23 +140,23 @@ class OwnersDatabaseTest(_BaseTestCase):
'chrome/renderer/gpu/gpu_channel_host.h' # Owned by * via parent 'chrome/renderer/gpu/gpu_channel_host.h' # Owned by * via parent
], ],
[ken], [ken],
['content', 'content/baz']) ['content/content.gyp', 'content/bar/foo.cc', 'content/baz/froboz.h'])
def test_per_file(self): def test_per_file(self):
# brett isn't allowed to approve ugly.cc # brett isn't allowed to approve ugly.cc
self.files['/content/baz/OWNERS'] = owners_file(brett, self.files['/content/baz/OWNERS'] = owners_file(brett,
lines=['per-file ugly.*=tom@example.com']) lines=['per-file ugly.*=tom@example.com'])
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[brett], [brett],
[]) [])
# tom is allowed to approve ugly.cc, but not froboz.h # tom is allowed to approve ugly.cc, but not froboz.h
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[tom], [tom],
[]) [])
self.assert_dirs_not_covered_by(['content/baz/froboz.h'], self.assert_files_not_covered_by(['content/baz/froboz.h'],
[tom], [tom],
['content/baz']) ['content/baz/froboz.h'])
def test_per_file_with_spaces(self): def test_per_file_with_spaces(self):
# This is the same as test_per_file(), except that we include spaces # This is the same as test_per_file(), except that we include spaces
@ -164,16 +164,16 @@ class OwnersDatabaseTest(_BaseTestCase):
# tom is allowed to approve ugly.cc, but not froboz.h # tom is allowed to approve ugly.cc, but not froboz.h
self.files['/content/baz/OWNERS'] = owners_file(brett, self.files['/content/baz/OWNERS'] = owners_file(brett,
lines=['per-file ugly.* = tom@example.com']) lines=['per-file ugly.* = tom@example.com'])
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[brett], [brett],
[]) [])
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[tom], [tom],
[]) [])
self.assert_dirs_not_covered_by(['content/baz/froboz.h'], self.assert_files_not_covered_by(['content/baz/froboz.h'],
[tom], [tom],
['content/baz']) ['content/baz/froboz.h'])
def test_per_file__set_noparent(self): def test_per_file__set_noparent(self):
self.files['/content/baz/OWNERS'] = owners_file(brett, self.files['/content/baz/OWNERS'] = owners_file(brett,
@ -181,22 +181,22 @@ class OwnersDatabaseTest(_BaseTestCase):
'per-file ugly.*=set noparent']) 'per-file ugly.*=set noparent'])
# brett isn't allowed to approve ugly.cc # brett isn't allowed to approve ugly.cc
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[brett], [brett],
['content/baz/ugly.cc']) ['content/baz/ugly.cc'])
# tom is allowed to approve ugly.cc, but not froboz.h # tom is allowed to approve ugly.cc, but not froboz.h
self.assert_dirs_not_covered_by(['content/baz/ugly.cc'], self.assert_files_not_covered_by(['content/baz/ugly.cc'],
[tom], [tom],
[]) [])
self.assert_dirs_not_covered_by(['content/baz/froboz.h'], self.assert_files_not_covered_by(['content/baz/froboz.h'],
[tom], [tom],
['content/baz']) ['content/baz/froboz.h'])
def test_per_file_wildcard(self): def test_per_file_wildcard(self):
self.files['/OWNERS'] = 'per-file DEPS=*\n' self.files['/OWNERS'] = 'per-file DEPS=*\n'
self.assert_dirs_not_covered_by(['DEPS'], [brett], []) self.assert_files_not_covered_by(['DEPS'], [brett], [])
def test_mock_relpath(self): def test_mock_relpath(self):
# This test ensures the mock relpath has the arguments in the right # This test ensures the mock relpath has the arguments in the right
@ -207,7 +207,7 @@ class OwnersDatabaseTest(_BaseTestCase):
def test_per_file_glob_across_dirs_not_allowed(self): def test_per_file_glob_across_dirs_not_allowed(self):
self.files['/OWNERS'] = 'per-file content/*=john@example.org\n' self.files['/OWNERS'] = 'per-file content/*=john@example.org\n'
self.assertRaises(owners.SyntaxErrorInOwnersFile, self.assertRaises(owners.SyntaxErrorInOwnersFile,
self.db().directories_not_covered_by, ['DEPS'], [brett]) self.db().files_not_covered_by, ['DEPS'], [brett])
def assert_syntax_error(self, owners_file_contents): def assert_syntax_error(self, owners_file_contents):
db = self.db() db = self.db()

@ -2219,14 +2219,14 @@ class CannedChecksUnittest(PresubmitTestsBase):
def AssertOwnersWorks(self, tbr=False, issue='1', approvers=None, def AssertOwnersWorks(self, tbr=False, issue='1', approvers=None,
reviewers=None, is_committing=True, rietveld_response=None, reviewers=None, is_committing=True, rietveld_response=None,
uncovered_dirs=None, expected_output=''): uncovered_files=None, expected_output=''):
if approvers is None: if approvers is None:
approvers = set() approvers = set()
if reviewers is None: if reviewers is None:
reviewers = set() reviewers = set()
reviewers = reviewers.union(approvers) reviewers = reviewers.union(approvers)
if uncovered_dirs is None: if uncovered_files is None:
uncovered_dirs = set() uncovered_files = set()
change = self.mox.CreateMock(presubmit.Change) change = self.mox.CreateMock(presubmit.Change)
change.issue = issue change.issue = issue
@ -2263,9 +2263,9 @@ class CannedChecksUnittest(PresubmitTestsBase):
rietveld_response) rietveld_response)
people.add(owner_email) people.add(owner_email)
fake_db.directories_not_covered_by(set(['foo/xyz.cc']), fake_db.files_not_covered_by(set(['foo/xyz.cc']),
people).AndReturn(uncovered_dirs) people).AndReturn(uncovered_files)
if not is_committing and uncovered_dirs: if not is_committing and uncovered_files:
fake_db.reviewers_for(set(['foo/xyz.cc'])).AndReturn(owner_email) fake_db.reviewers_for(set(['foo/xyz.cc'])).AndReturn(owner_email)
self.mox.ReplayAll() self.mox.ReplayAll()
@ -2352,18 +2352,16 @@ class CannedChecksUnittest(PresubmitTestsBase):
def testCannedCheckOwners_NoIssue(self): def testCannedCheckOwners_NoIssue(self):
self.AssertOwnersWorks(issue=None, self.AssertOwnersWorks(issue=None,
uncovered_dirs=set(['foo']), uncovered_files=set(['foo']),
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, self.AssertOwnersWorks(issue=None,
is_committing=False, is_committing=False,
uncovered_dirs=set(['foo']), uncovered_files=set(['foo']),
expected_output='Missing OWNER reviewers for files in these ' expected_output='Missing OWNER reviewers for these files:\n'
'directories:\n'
' foo\n' ' foo\n'
'Until the issue is uploaded, this list will include ' 'Until the issue is uploaded, this list will include '
'directories for which you \n' 'files for which you are an OWNER.\n')
'are an OWNER.\n')
def testCannedCheckOwners_NoLGTM(self): def testCannedCheckOwners_NoLGTM(self):
self.AssertOwnersWorks(expected_output='Missing LGTM from someone ' self.AssertOwnersWorks(expected_output='Missing LGTM from someone '
@ -2384,22 +2382,20 @@ class CannedChecksUnittest(PresubmitTestsBase):
self.AssertOwnersWorks(tbr=True, is_committing=False, expected_output='') self.AssertOwnersWorks(tbr=True, is_committing=False, expected_output='')
def testCannedCheckOwners_WithoutOwnerLGTM(self): def testCannedCheckOwners_WithoutOwnerLGTM(self):
self.AssertOwnersWorks(uncovered_dirs=set(['foo']), self.AssertOwnersWorks(uncovered_files=set(['foo']),
expected_output='Missing LGTM from an OWNER for files in these ' expected_output='Missing LGTM from an OWNER for these files:\n'
'directories:\n'
' foo\n') ' foo\n')
self.AssertOwnersWorks(uncovered_dirs=set(['foo']), self.AssertOwnersWorks(uncovered_files=set(['foo']),
is_committing=False, is_committing=False,
expected_output='Missing OWNER reviewers for files in these ' expected_output='Missing OWNER reviewers for these files:\n'
'directories:\n'
' foo\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_dirs=set()) uncovered_files=set())
self.AssertOwnersWorks(approvers=set(['ben@example.com']), self.AssertOwnersWorks(approvers=set(['ben@example.com']),
is_committing=False, is_committing=False,
uncovered_dirs=set()) uncovered_files=set())
def testCannedRunUnitTests(self): def testCannedRunUnitTests(self):
change = presubmit.Change( change = presubmit.Change(

Loading…
Cancel
Save