diff --git a/git_cl.py b/git_cl.py index 43c9d365b..289640a77 100755 --- a/git_cl.py +++ b/git_cl.py @@ -8,6 +8,7 @@ """A git-command for integrating reviews on Rietveld.""" from distutils.version import LooseVersion +import glob import json import logging import optparse @@ -38,6 +39,7 @@ import scm import subcommand import subprocess2 import watchlists +import owners_finder __version__ = '1.0' @@ -2126,6 +2128,35 @@ def CMDset_close(parser, args): return 0 +def CMDowners(parser, args): + """interactively find the owners for reviewing""" + parser.add_option( + '--no-color', + action='store_true', + help='Use this option to disable color output') + options, args = parser.parse_args(args) + + author = RunGit(['config', 'user.email']).strip() or None + + cl = Changelist() + + if args: + if len(args) > 1: + parser.error('Unknown args') + base_branch = args[0] + else: + # Default to diffing against the common ancestor of the upstream branch. + base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip() + + change = cl.GetChange(base_branch, None) + return owners_finder.OwnersFinder( + [f.LocalPath() for f in + cl.GetChange(base_branch, None).AffectedFiles()], + change.RepositoryRoot(), author, + fopen=file, os_path=os.path, glob=glob.glob, + disable_color=options.no_color).run() + + def CMDformat(parser, args): """Runs clang-format on the diff.""" CLANG_EXTS = ['.cc', '.cpp', '.h'] diff --git a/owners.py b/owners.py index cc667beea..30646ffa2 100644 --- a/owners.py +++ b/owners.py @@ -78,7 +78,7 @@ class SyntaxErrorInOwnersFile(Exception): self.msg = msg def __str__(self): - return "%s:%d syntax error: %s" % (self.path, self.lineno, self.msg) + return '%s:%d syntax error: %s' % (self.path, self.lineno, self.msg) class Database(object): @@ -111,6 +111,9 @@ class Database(object): # Mapping of paths to authorized owners. self.owners_for = {} + # Mapping reviewers to the preceding comment per file in the OWNERS files. + self.comments = {} + # Set of paths that stop us from looking above them for owners. # (This is implicitly true for the root directory). self.stop_looking = set(['']) @@ -122,7 +125,7 @@ class Database(object): If author is nonempty, we ensure it is not included in the set returned in order avoid suggesting the author as a reviewer for their own changes.""" self._check_paths(files) - self._load_data_needed_for(files) + self.load_data_needed_for(files) suggested_owners = self._covering_set_of_owners_for(files, author) if EVERYONE in suggested_owners: if len(suggested_owners) > 1: @@ -140,7 +143,7 @@ class Database(object): """ self._check_paths(files) self._check_reviewers(reviewers) - self._load_data_needed_for(files) + self.load_data_needed_for(files) covered_objs = self._objs_covered_by(reviewers) uncovered_files = [f for f in files @@ -182,7 +185,7 @@ class Database(object): 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: dirpath = self.os_path.dirname(f) while not dirpath in self.owners_for: @@ -195,18 +198,27 @@ class Database(object): owners_path = self.os_path.join(self.root, dirpath, 'OWNERS') if not self.os_path.exists(owners_path): return - + comment = [] + in_comment = False lineno = 0 for line in self.fopen(owners_path): lineno += 1 line = line.strip() - if line.startswith('#') or line == '': + if line.startswith('#'): + if not in_comment: + comment = [] + comment.append(line[1:].strip()) + in_comment = True continue + if line == '': + continue + in_comment = False + if line == 'set noparent': self.stop_looking.add(dirpath) continue - m = re.match("per-file (.+)=(.+)", line) + m = re.match('per-file (.+)=(.+)', line) if m: glob_string = m.group(1).strip() directive = m.group(2).strip() @@ -217,20 +229,24 @@ class Database(object): line) baselines = self.glob(full_glob_string) for baseline in (self.os_path.relpath(b, self.root) for b in baselines): - self._add_entry(baseline, directive, "per-file line", - owners_path, lineno) + self._add_entry(baseline, directive, 'per-file line', + owners_path, lineno, '\n'.join(comment)) continue if line.startswith('set '): raise SyntaxErrorInOwnersFile(owners_path, lineno, 'unknown option: "%s"' % line[4:].strip()) - self._add_entry(dirpath, line, "line", owners_path, lineno) + self._add_entry(dirpath, line, 'line', owners_path, lineno, + ' '.join(comment)) - def _add_entry(self, path, directive, line_type, owners_path, lineno): - if directive == "set noparent": + def _add_entry(self, path, directive, + line_type, owners_path, lineno, comment): + if directive == 'set noparent': self.stop_looking.add(path) elif self.email_regexp.match(directive) or directive == EVERYONE: + self.comments.setdefault(directive, {}) + self.comments[directive][path] = comment self.owned_by.setdefault(directive, set()).add(path) self.owners_for.setdefault(path, set()).add(directive) else: @@ -240,7 +256,7 @@ class Database(object): def _covering_set_of_owners_for(self, files, author): dirs_remaining = set(self._enclosing_dir_with_owners(f) for f in files) - all_possible_owners = self._all_possible_owners(dirs_remaining, author) + all_possible_owners = self.all_possible_owners(dirs_remaining, author) suggested_owners = set() while dirs_remaining: owner = self.lowest_cost_owner(all_possible_owners, dirs_remaining) @@ -249,7 +265,7 @@ class Database(object): dirs_remaining -= dirs_to_remove return suggested_owners - def _all_possible_owners(self, dirs, author): + def all_possible_owners(self, dirs, author): """Returns a list of (potential owner, distance-from-dir) tuples; a distance of 1 is the lowest/closest possible distance (which makes the subsequent math easier).""" @@ -273,13 +289,13 @@ class Database(object): return all_possible_owners @staticmethod - def lowest_cost_owner(all_possible_owners, dirs): + def total_costs_by_owner(all_possible_owners, dirs): # We want to minimize both the number of reviewers and the distance # from the files/dirs needing reviews. The "pow(X, 1.75)" below is # an arbitrarily-selected scaling factor that seems to work well - it # will select one reviewer in the parent directory over three reviewers # in subdirs, but not one reviewer over just two. - total_costs_by_owner = {} + result = {} for owner in all_possible_owners: total_distance = 0 num_directories_owned = 0 @@ -287,13 +303,18 @@ class Database(object): if dirname in dirs: total_distance += distance num_directories_owned += 1 - if num_directories_owned: - total_costs_by_owner[owner] = (total_distance / - pow(num_directories_owned, 1.75)) + if num_directories_owned: + result[owner] = (total_distance / + pow(num_directories_owned, 1.75)) + return result + @staticmethod + def lowest_cost_owner(all_possible_owners, dirs): + total_costs_by_owner = Database.total_costs_by_owner(all_possible_owners, + dirs) # Return the lowest cost owner. In the case of a tie, pick one randomly. lowest_cost = min(total_costs_by_owner.itervalues()) lowest_cost_owners = filter( - lambda owner: total_costs_by_owner[owner] == lowest_cost, - total_costs_by_owner) + lambda owner: total_costs_by_owner[owner] == lowest_cost, + total_costs_by_owner) return random.Random().choice(lowest_cost_owners) diff --git a/owners_finder.py b/owners_finder.py new file mode 100644 index 000000000..a0d50304d --- /dev/null +++ b/owners_finder.py @@ -0,0 +1,365 @@ +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Interactive tool for finding reviewers/owners for a change.""" + +import os +import copy +import owners as owners_module + + +def first(iterable): + for element in iterable: + return element + + +class OwnersFinder(object): + COLOR_LINK = '\033[4m' + COLOR_BOLD = '\033[1;32m' + COLOR_GREY = '\033[0;37m' + COLOR_RESET = '\033[0m' + + indentation = 0 + + def __init__(self, files, local_root, author, + fopen, os_path, glob, + email_postfix='@chromium.org', + disable_color=False): + self.email_postfix = email_postfix + + if os.name == 'nt' or disable_color: + self.COLOR_LINK = '' + self.COLOR_BOLD = '' + self.COLOR_GREY = '' + self.COLOR_RESET = '' + + self.db = owners_module.Database(local_root, fopen, os_path, glob) + self.db.load_data_needed_for(files) + + self.os_path = os_path + + self.author = author + + filtered_files = files + + # Eliminate files that author himself can review. + if author: + if author in self.db.owned_by: + for dir_name in self.db.owned_by[author]: + filtered_files = [ + file_name for file_name in filtered_files + if not file_name.startswith(dir_name)] + + filtered_files = list(filtered_files) + + # Eliminate files that everyone can review. + if owners_module.EVERYONE in self.db.owned_by: + for dir_name in self.db.owned_by[owners_module.EVERYONE]: + filtered_files = filter( + lambda file_name: not file_name.startswith(dir_name), + filtered_files) + + # If some files are eliminated. + if len(filtered_files) != len(files): + files = filtered_files + # Reload the database. + self.db = owners_module.Database(local_root, fopen, os_path, glob) + self.db.load_data_needed_for(files) + + self.all_possible_owners = self.db.all_possible_owners(files, None) + + self.owners_to_files = {} + self._map_owners_to_files(files) + + self.files_to_owners = {} + self._map_files_to_owners() + + self.owners_score = self.db.total_costs_by_owner( + self.all_possible_owners, files) + + self.original_files_to_owners = copy.deepcopy(self.files_to_owners) + self.comments = self.db.comments + + # This is the queue that will be shown in the interactive questions. + # It is initially sorted by the score in descending order. In the + # interactive questions a user can choose to "defer" its decision, then the + # owner will be put to the end of the queue and shown later. + self.owners_queue = [] + + self.unreviewed_files = set() + self.reviewed_by = {} + self.selected_owners = set() + self.deselected_owners = set() + self.reset() + + def run(self): + self.reset() + while self.owners_queue and self.unreviewed_files: + owner = self.owners_queue[0] + + if (owner in self.selected_owners) or (owner in self.deselected_owners): + continue + + if not any((file_name in self.unreviewed_files) + for file_name in self.owners_to_files[owner]): + self.deselect_owner(owner) + continue + + self.print_info(owner) + + while True: + inp = self.input_command(owner) + if inp == 'y' or inp == 'yes': + self.select_owner(owner) + break + elif inp == 'n' or inp == 'no': + self.deselect_owner(owner) + break + elif inp == '' or inp == 'd' or inp == 'defer': + self.owners_queue.append(self.owners_queue.pop(0)) + break + elif inp == 'f' or inp == 'files': + self.list_files() + break + elif inp == 'o' or inp == 'owners': + self.list_owners(self.owners_queue) + break + elif inp == 'p' or inp == 'pick': + self.pick_owner(raw_input('Pick an owner: ')) + break + elif inp.startswith('p ') or inp.startswith('pick '): + self.pick_owner(inp.split(' ', 2)[1].strip()) + break + elif inp == 'r' or inp == 'restart': + self.reset() + break + elif inp == 'q' or inp == 'quit': + # Exit with error + return 1 + + self.print_result() + return 0 + + def _map_owners_to_files(self, files): + for owner in self.all_possible_owners: + for dir_name, _ in self.all_possible_owners[owner]: + for file_name in files: + if file_name.startswith(dir_name): + self.owners_to_files.setdefault(owner, set()) + self.owners_to_files[owner].add(file_name) + + def _map_files_to_owners(self): + for owner in self.owners_to_files: + for file_name in self.owners_to_files[owner]: + self.files_to_owners.setdefault(file_name, set()) + self.files_to_owners[file_name].add(owner) + + def reset(self): + self.files_to_owners = copy.deepcopy(self.original_files_to_owners) + self.unreviewed_files = set(self.files_to_owners.keys()) + self.reviewed_by = {} + self.selected_owners = set() + self.deselected_owners = set() + + # Initialize owners queue, sort it by the score + self.owners_queue = list(sorted(self.owners_to_files.keys(), + key=lambda owner: self.owners_score[owner])) + self.find_mandatory_owners() + + def select_owner(self, owner, findMandatoryOwners=True): + if owner in self.selected_owners or owner in self.deselected_owners\ + or not (owner in self.owners_queue): + return + self.writeln('Selected: ' + owner) + self.owners_queue.remove(owner) + self.selected_owners.add(owner) + for file_name in filter( + lambda file_name: file_name in self.unreviewed_files, + self.owners_to_files[owner]): + self.unreviewed_files.remove(file_name) + self.reviewed_by[file_name] = owner + if findMandatoryOwners: + self.find_mandatory_owners() + + def deselect_owner(self, owner, findMandatoryOwners=True): + if owner in self.selected_owners or owner in self.deselected_owners\ + or not (owner in self.owners_queue): + return + self.writeln('Deselected: ' + owner) + self.owners_queue.remove(owner) + self.deselected_owners.add(owner) + for file_name in self.owners_to_files[owner] & self.unreviewed_files: + self.files_to_owners[file_name].remove(owner) + if findMandatoryOwners: + self.find_mandatory_owners() + + def find_mandatory_owners(self): + continues = True + for owner in self.owners_queue: + if owner in self.selected_owners: + continue + if owner in self.deselected_owners: + continue + if len(self.owners_to_files[owner] & self.unreviewed_files) == 0: + self.deselect_owner(owner, False) + + while continues: + continues = False + for file_name in filter( + lambda file_name: len(self.files_to_owners[file_name]) == 1, + self.unreviewed_files): + owner = first(self.files_to_owners[file_name]) + self.select_owner(owner, False) + continues = True + break + + def print_comments(self, owner): + if owner not in self.comments: + self.writeln(self.bold_name(owner)) + else: + self.writeln(self.bold_name(owner) + ' is commented as:') + self.indent() + for path in self.comments[owner]: + if len(self.comments[owner][path]) > 0: + self.writeln(self.greyed(self.comments[owner][path]) + + ' (at ' + self.bold(path or '') + ')') + else: + self.writeln(self.greyed('[No comment] ') + ' (at ' + + self.bold(path or '') + ')') + self.unindent() + + def print_file_info(self, file_name, except_owner=''): + if file_name not in self.unreviewed_files: + self.writeln(self.greyed(file_name + + ' (by ' + + self.bold_name(self.reviewed_by[file_name]) + + ')')) + else: + if len(self.files_to_owners[file_name]) <= 3: + other_owners = [] + for ow in self.files_to_owners[file_name]: + if ow != except_owner: + other_owners.append(self.bold_name(ow)) + self.writeln(file_name + + ' [' + (', '.join(other_owners)) + ']') + else: + self.writeln(file_name + ' [' + + self.bold(str(len(self.files_to_owners[file_name]))) + + ']') + + def print_file_info_detailed(self, file_name): + self.writeln(file_name) + self.indent() + for ow in sorted(self.files_to_owners[file_name]): + if ow in self.deselected_owners: + self.writeln(self.bold_name(self.greyed(ow))) + elif ow in self.selected_owners: + self.writeln(self.bold_name(self.greyed(ow))) + else: + self.writeln(self.bold_name(ow)) + self.unindent() + + def print_owned_files_for(self, owner): + # Print owned files + self.print_comments(owner) + self.writeln(self.bold_name(owner) + ' owns ' + + str(len(self.owners_to_files[owner])) + ' file(s):') + self.indent() + for file_name in sorted(self.owners_to_files[owner]): + self.print_file_info(file_name, owner) + self.unindent() + self.writeln() + + def list_owners(self, owners_queue): + if (len(self.owners_to_files) - len(self.deselected_owners) - + len(self.selected_owners)) > 3: + for ow in owners_queue: + if ow not in self.deselected_owners and ow not in self.selected_owners: + self.print_comments(ow) + else: + for ow in owners_queue: + if ow not in self.deselected_owners and ow not in self.selected_owners: + self.writeln() + self.print_owned_files_for(ow) + + def list_files(self): + self.indent() + if len(self.unreviewed_files) > 5: + for file_name in sorted(self.unreviewed_files): + self.print_file_info(file_name) + else: + for file_name in self.unreviewed_files: + self.print_file_info_detailed(file_name) + self.unindent() + + def pick_owner(self, ow): + # Allowing to omit domain suffixes + if ow not in self.owners_to_files: + if ow + self.email_postfix in self.owners_to_files: + ow += self.email_postfix + + if ow not in self.owners_to_files: + self.writeln('You cannot pick ' + self.bold_name(ow) + ' manually. ' + + 'It\'s an invalid name or not related to the change list.') + return False + elif ow in self.selected_owners: + self.writeln('You cannot pick ' + self.bold_name(ow) + ' manually. ' + + 'It\'s already selected.') + return False + elif ow in self.deselected_owners: + self.writeln('You cannot pick ' + self.bold_name(ow) + ' manually.' + + 'It\'s already unselected.') + return False + + self.select_owner(ow) + return True + + def print_result(self): + # Print results + self.writeln() + self.writeln() + self.writeln('** You selected these owners **') + self.writeln() + for owner in self.selected_owners: + self.writeln(self.bold_name(owner) + ':') + self.indent() + for file_name in sorted(self.owners_to_files[owner]): + self.writeln(file_name) + self.unindent() + + def bold(self, text): + return self.COLOR_BOLD + text + self.COLOR_RESET + + def bold_name(self, name): + return (self.COLOR_BOLD + + name.replace(self.email_postfix, '') + self.COLOR_RESET) + + def greyed(self, text): + return self.COLOR_GREY + text + self.COLOR_RESET + + def indent(self): + self.indentation += 1 + + def unindent(self): + self.indentation -= 1 + + def print_indent(self): + return ' ' * self.indentation + + def writeln(self, text=''): + print self.print_indent() + text + + def hr(self): + self.writeln('=====================') + + def print_info(self, owner): + self.hr() + self.writeln( + self.bold(str(len(self.unreviewed_files))) + ' file(s) left.') + self.print_owned_files_for(owner) + + def input_command(self, owner): + self.writeln('Add ' + self.bold_name(owner) + ' as your reviewer? ') + return raw_input( + '[yes/no/Defer/pick/files/owners/quit/restart]: ').lower() diff --git a/tests/owners_finder_test.py b/tests/owners_finder_test.py new file mode 100755 index 000000000..779361294 --- /dev/null +++ b/tests/owners_finder_test.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# Copyright 2013 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Unit tests for owners_finder.py.""" + +import os +import sys +import unittest + + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from testing_support import filesystem_mock + +import owners_finder +import owners + + +ben = 'ben@example.com' +brett = 'brett@example.com' +darin = 'darin@example.com' +john = 'john@example.com' +ken = 'ken@example.com' +peter = 'peter@example.com' +tom = 'tom@example.com' + + +def owners_file(*email_addresses, **kwargs): + s = '' + if kwargs.get('comment'): + s += '# %s\n' % kwargs.get('comment') + if kwargs.get('noparent'): + s += 'set noparent\n' + s += '\n'.join(kwargs.get('lines', [])) + '\n' + return s + '\n'.join(email_addresses) + '\n' + + +def test_repo(): + return filesystem_mock.MockFileSystem(files={ + '/DEPS': '', + '/OWNERS': owners_file(ken, peter, tom), + '/base/vlog.h': '', + '/chrome/OWNERS': owners_file(ben, brett), + '/chrome/browser/OWNERS': owners_file(brett), + '/chrome/browser/defaults.h': '', + '/chrome/gpu/OWNERS': owners_file(ken), + '/chrome/gpu/gpu_channel.h': '', + '/chrome/renderer/OWNERS': owners_file(peter), + '/chrome/renderer/gpu/gpu_channel_host.h': '', + '/chrome/renderer/safe_browsing/scorer.h': '', + '/content/OWNERS': owners_file(john, darin, comment='foo', noparent=True), + '/content/content.gyp': '', + '/content/bar/foo.cc': '', + '/content/baz/OWNERS': owners_file(brett), + '/content/baz/froboz.h': '', + '/content/baz/ugly.cc': '', + '/content/baz/ugly.h': '', + '/content/views/OWNERS': owners_file(ben, john, owners.EVERYONE, + noparent=True), + '/content/views/pie.h': '', + }) + + +class OutputInterceptedOwnersFinder(owners_finder.OwnersFinder): + def __init__(self, files, local_root, + fopen, os_path, glob, + disable_color=False): + super(OutputInterceptedOwnersFinder, self).__init__( + files, local_root, None, + fopen, os_path, glob, disable_color=disable_color) + self.output = [] + self.indentation_stack = [] + + def resetText(self): + self.output = [] + self.indentation_stack = [] + + def indent(self): + self.indentation_stack.append(self.output) + self.output = [] + + def unindent(self): + block = self.output + self.output = self.indentation_stack.pop() + self.output.append(block) + + def writeln(self, text=''): + self.output.append(text) + + +class _BaseTestCase(unittest.TestCase): + default_files = [ + 'base/vlog.h', + 'chrome/browser/defaults.h', + 'chrome/gpu/gpu_channel.h', + 'chrome/renderer/gpu/gpu_channel_host.h', + 'chrome/renderer/safe_browsing/scorer.h', + 'content/content.gyp', + 'content/bar/foo.cc', + 'content/baz/ugly.cc', + 'content/baz/ugly.h', + 'content/views/pie.h' + ] + + def setUp(self): + self.repo = test_repo() + self.root = '/' + self.fopen = self.repo.open_for_reading + self.glob = self.repo.glob + + def ownersFinder(self, files): + finder = OutputInterceptedOwnersFinder(files, self.root, + fopen=self.fopen, + os_path=self.repo, + glob=self.glob, + disable_color=True) + return finder + + def defaultFinder(self): + return self.ownersFinder(self.default_files) + + +class OwnersFinderTests(_BaseTestCase): + def test_constructor(self): + self.assertNotEquals(self.defaultFinder(), None) + + def test_reset(self): + finder = self.defaultFinder() + i = 0 + while i < 2: + i += 1 + self.assertEqual(finder.owners_queue, + [brett, john, darin, peter, ken, ben, tom]) + self.assertEqual(finder.unreviewed_files, { + 'base/vlog.h', + 'chrome/browser/defaults.h', + 'chrome/gpu/gpu_channel.h', + 'chrome/renderer/gpu/gpu_channel_host.h', + 'chrome/renderer/safe_browsing/scorer.h', + 'content/content.gyp', + 'content/bar/foo.cc', + 'content/baz/ugly.cc', + 'content/baz/ugly.h' + }) + self.assertEqual(finder.selected_owners, set()) + self.assertEqual(finder.deselected_owners, set()) + self.assertEqual(finder.reviewed_by, {}) + self.assertEqual(finder.output, []) + + finder.select_owner(john) + finder.reset() + finder.resetText() + + def test_select(self): + finder = self.defaultFinder() + finder.select_owner(john) + self.assertEqual(finder.owners_queue, [brett, peter, ken, ben, tom]) + self.assertEqual(finder.selected_owners, {john}) + self.assertEqual(finder.deselected_owners, {darin}) + self.assertEqual(finder.reviewed_by, {'content/bar/foo.cc': john, + 'content/baz/ugly.cc': john, + 'content/baz/ugly.h': john, + 'content/content.gyp': john}) + self.assertEqual(finder.output, + ['Selected: ' + john, 'Deselected: ' + darin]) + + finder = self.defaultFinder() + finder.select_owner(darin) + self.assertEqual(finder.owners_queue, [brett, peter, ken, ben, tom]) + self.assertEqual(finder.selected_owners, {darin}) + self.assertEqual(finder.deselected_owners, {john}) + self.assertEqual(finder.reviewed_by, {'content/bar/foo.cc': darin, + 'content/baz/ugly.cc': darin, + 'content/baz/ugly.h': darin, + 'content/content.gyp': darin}) + self.assertEqual(finder.output, + ['Selected: ' + darin, 'Deselected: ' + john]) + + finder = self.defaultFinder() + finder.select_owner(brett) + self.assertEqual(finder.owners_queue, [john, darin, peter, ken, tom]) + self.assertEqual(finder.selected_owners, {brett}) + self.assertEqual(finder.deselected_owners, {ben}) + self.assertEqual(finder.reviewed_by, + {'chrome/browser/defaults.h': brett, + 'chrome/gpu/gpu_channel.h': brett, + 'chrome/renderer/gpu/gpu_channel_host.h': brett, + 'chrome/renderer/safe_browsing/scorer.h': brett, + 'content/baz/ugly.cc': brett, + 'content/baz/ugly.h': brett}) + self.assertEqual(finder.output, + ['Selected: ' + brett, 'Deselected: ' + ben]) + + def test_deselect(self): + finder = self.defaultFinder() + finder.deselect_owner(john) + self.assertEqual(finder.owners_queue, [brett, peter, ken, ben, tom]) + self.assertEqual(finder.selected_owners, {darin}) + self.assertEqual(finder.deselected_owners, {john}) + self.assertEqual(finder.reviewed_by, {'content/bar/foo.cc': darin, + 'content/baz/ugly.cc': darin, + 'content/baz/ugly.h': darin, + 'content/content.gyp': darin}) + self.assertEqual(finder.output, + ['Deselected: ' + john, 'Selected: ' + darin]) + + def test_print_file_info(self): + finder = self.defaultFinder() + finder.print_file_info('chrome/browser/defaults.h') + self.assertEqual(finder.output, ['chrome/browser/defaults.h [5]']) + finder.resetText() + + finder.print_file_info('chrome/renderer/gpu/gpu_channel_host.h') + self.assertEqual(finder.output, + ['chrome/renderer/gpu/gpu_channel_host.h [5]']) + + def test_print_file_info_detailed(self): + finder = self.defaultFinder() + finder.print_file_info_detailed('chrome/browser/defaults.h') + self.assertEqual(finder.output, + ['chrome/browser/defaults.h', + [ben, brett, ken, peter, tom]]) + finder.resetText() + + finder.print_file_info_detailed('chrome/renderer/gpu/gpu_channel_host.h') + self.assertEqual(finder.output, + ['chrome/renderer/gpu/gpu_channel_host.h', + [ben, brett, ken, peter, tom]]) + + def test_print_comments(self): + finder = self.defaultFinder() + finder.print_comments(darin) + self.assertEqual(finder.output, + [darin + ' is commented as:', ['foo (at content)']]) + + +if __name__ == '__main__': + unittest.main()