# HG changeset patch # User Steve Losh # Date 1276301240 14400 # Node ID a909c2ba47f0cbf2c3ea13ee6bed47ff340f8f17 # Parent a55ffc78928ccf433f7021b7d86ca52b8d61078d GTFO whitespace. diff -r a55ffc78928c -r a909c2ba47f0 review/api.py --- a/review/api.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/api.py Fri Jun 11 20:07:20 2010 -0400 @@ -8,28 +8,27 @@ from mercurial.node import hex from mercurial import ui as _ui - try: from os.path import relpath except ImportError: # python < 2.6 from os.path import curdir, abspath, sep, commonprefix, pardir, join def relpath(path, start=curdir): """Return a relative version of a path""" - + if not path: raise ValueError("no path specified") - + start_list = abspath(start).split(sep) path_list = abspath(path).split(sep) - + # Work out how much of the filepath is shared by start and path. i = len(commonprefix([start_list, path_list])) - + rel_list = [pardir] * (len(start_list)-i) + path_list[i:] if not rel_list: return curdir return join(*rel_list) - + DEFAULT_DATASTORE_DIRNAME = os.path.join('.hg', 'review') @@ -38,20 +37,19 @@ def __init__(self, committed): super(PreexistingDatastore, self).__init__() self.committed = committed - class UninitializedDatastore(Exception): """Raised when trying to access a datastore that does not exist. - + The committed attribute will be True if someone else has already initialized code review for the target (i.e. the .hgreview file is present in the target repository), or False otherwise. - + """ def __init__(self, committed): super(UninitializedDatastore, self).__init__() self.committed = committed - + class DatastoreRequiresRemotePath(Exception): """Raised when initializing a fresh datastore without a remote path.""" @@ -75,15 +73,14 @@ def __init__(self, filename): super(FileNotInChangeset, self).__init__() self.filename = filename - def _split_path_dammit(p): """Take a file path (from the current platform) and split it. Really. - + os.path doesn't seem to have an easy way to say "Split this path into a list of pieces." - + >>> _split_path_dammit('') [] >>> _split_path_dammit('one') @@ -94,22 +91,22 @@ ['one', 'two', 'three'] >>> _split_path_dammit('one/two/three.py') ['one', 'two', 'three.py'] - + """ def _spd(p): p, i = os.path.split(p) while i or p: yield i p, i = os.path.split(p) - + return filter(None, list(_spd(p)))[::-1] def _parse_hgrf(repo): """Parse the .hgreview file and return the data inside. - + The .hgreview file will be pulled from the tip revision of the given repository. If it is not committed it will not be found! - + """ data = {} hgrd = repo['tip']['.hgreview'].data().split('\n') @@ -118,22 +115,22 @@ label, _, path = [i.strip() for i in line.partition('=')] if label == 'remote': data['rpath'] = path - + return data def _commitfunc(ui, repo, message, match, opts): """A function used by the guts of Mercurial. - + Mercurial needs a "commit function" parameter when using cmdutil.commit. This is a simple function for *only* that purpose. - + """ return repo.commit(message, opts.get('user'), opts.get('date'), match) def _parse_data(data): """Parse the data (string) of a stored _ReviewObject and return a dict.""" meta, _, message = data.partition('\n\n') - + data = {} for m in meta.split('\n'): label, _, val = m.partition(':') @@ -143,16 +140,16 @@ def _datetime_from_hgdate(hgdate): """Return a datetime.datetime for the given Mecurial-style date tuple. - + It will have NO timezone information -- the date and time are what a clock next to the current computer would have read at the instant represented by the Mercurial-style date! - + """ offset = abs(hgdate[1] - util.makedate()[1]) later = offset < 0 offset = datetime.timedelta(seconds=offset) - + conversion_format = '%Y-%m-%d %H:%M:%S' bare = util.datestr(hgdate, format=conversion_format) bare_datetime = datetime.datetime.strptime(bare, conversion_format) @@ -164,17 +161,17 @@ def sanitize_path(p, repo=None): """Sanitize a (platform-specific) path. - + If no repository is given, the path's separators will be replaced with forward slashes (the form Mercurial uses internally). - + If a repository is given, the result will be relative to the root of the repository. This is useful for turning relative paths into normalized paths that can be used to look up files from a changectx. - + This function is idempotent. If you sanitize a path multiple times against the same repository the result will not change. - + """ if repo: p = relpath(os.path.realpath(p), start=repo.root) @@ -185,36 +182,36 @@ """The code review data for a particular repository.""" def __init__(self, ui, repo, lpath=None, rpath=None, create=False): """Initialize a ReviewDatastore for a Mercurial repository. - + To get a ReviewDatastore for a repository that has already been initialized for code reviewing: - + review_data = ReviewDatastore(ui, repo) - + To set up a repository to support code review: - + review_data = ReviewDatastore(ui, repo, create=True) - + If you want to specify your own path to the code review repository for this repo, pass the FULL path to the repository as the lpath parameter. - + Error handling is a bit tricky at the moment. I need to refactor and/or document this. - + """ self.ui = ui self.target = repo self.lpath = lpath or os.path.join( self.target.root, DEFAULT_DATASTORE_DIRNAME ) - + if not create: if not '.hgreview' in repo['tip']: raise UninitializedDatastore(False) - + data = _parse_hgrf(repo) self.rpath = data['rpath'] - + try: self.repo = hg.repository(_ui.ui(), self.lpath) except error.RepoError: @@ -222,10 +219,10 @@ elif '.hgreview' in repo['tip']: data = _parse_hgrf(self.target) self.rpath = data['rpath'] - + if self.rpath.startswith('.'): raise RelativeRemotePath - + try: hg.repository(ui, self.lpath) except error.RepoError: @@ -241,64 +238,63 @@ raise RelativeRemotePath else: self.rpath = rpath - + with open(os.path.join(self.target.root, '.hgreview'), 'w') as hgrf: hgrf.write('remote = %s\n' % self.rpath) self.target.add(['.hgreview']) - + self.repo = hg.repository(ui, self.lpath, create) - + def __getitem__(self, rev): """Return a ReviewChangeset for the given revision.""" node = hex(self.target[str(rev)].node()) return ReviewChangeset(self.ui, self.repo, self.target, node) - class ReviewChangeset(object): """The review data about one changeset in the target repository. - + Individual changesets can be retrieved from a ReviewDatastore. - + Each ReviewChangeset stores a list of ReviewComment objects and a list of ReviewSignoff objects: - + rcset = rd['tip'] rcset.comments rcset.signoffs - + Comments and signoffs should be added to a changeset by using the add_comment and add_signoff methods: - + rcset = rd['tip'] rcset.add_comment(...) rcset.add_signoff(...) - + Diffs for files modified in a changeset can be retrived with the diffs and full_diffs methods. See the docs of those methods for more info. - + """ def __init__(self, ui, repo, target, node): """Initialize a ReviewChangeset. - + You shouldn't need to create these directly -- use a ReviewDatastore object to get them: - + review_data = ReviewDatastore(ui, repo) tip_review_data = review_data['tip'] - + """ self.repo = repo self.target = target self.ui = ui self.node = node - + if '%s/.exists' % self.node in self.repo['tip']: _match = lambda p: lambda fn: fn.startswith(p) - + relevant = filter(_match(node), self.repo['tip']) commentfns = filter(_match('%s/comments' % node), relevant) signofffns = filter(_match('%s/signoffs' % node), relevant) - + self.comments = [] for fn in commentfns: data = _parse_data(self.repo['tip'][fn].data()) @@ -308,7 +304,7 @@ data['identifier'] = _split_path_dammit(fn)[-1] self.comments.append(ReviewComment(**data)) self.comments.sort(key=operator.attrgetter('local_datetime')) - + self.signoffs = [] for fn in signofffns: data = _parse_data(self.repo['tip'][fn].data()) @@ -319,77 +315,77 @@ else: self.comments = [] self.signoffs = [] - + path = os.path.join(self.repo.root, self.node) os.mkdir(path) with open(os.path.join(path, '.exists'), 'w') as e: pass - + cmdutil.commit(ui, self.repo, _commitfunc, [os.path.join(path, '.exists')], { 'message': 'Initialize review data for changeset %s' % self.node, 'addremove': True, }) - + def signoffs_for_user(self, username): return filter(lambda s: s.author == username, self.signoffs) - + def signoffs_for_current_user(self): return self.signoffs_for_user(self.ui.username()) - + def add_signoff(self, message, opinion='', force=False): """Add (and commit) a signoff for the given revision. - + The opinion argument should be 'yes', 'no', or ''. - + If a signoff from the user already exists, a SignoffExists exception will be raised unless the force argument is used. - + """ existing = self.signoffs_for_current_user() - + if existing: if not force: raise SignoffExists existing[0]._delete(self.ui, self.repo) - + signoff = ReviewSignoff(self.ui.username(), util.makedate(), self.node, opinion, message) signoff._commit(self.ui, self.repo) - + def add_comment(self, message, filename='', lines=[]): """Add (and commit) a comment for the given file and lines. - + The filename should be normalized to the format Mercurial expects, that is: relative to the root of the repository and using forward slashes as the separator. Paths can be converted with the sanitize_path function in this module. - + If the comment is on one or more lines, a filename *must* be given. - + Line numbers should be passed as a list, even if there is only one. See the full_diffs function for how to refer to line numbers. - + """ if filename and filename not in self.target[self.node].files(): raise FileNotInChangeset(filename) - + comment = ReviewComment(self.ui.username(), util.makedate(), self.node, filename, lines, message) comment._commit(self.ui, self.repo) - - + + def full_diffs(self, filenames=None, opts={}): """Return full diffs of the given files (or all files). - + If the filenames argument is not used, diffs for every file in the changeset will be returned. - + The diffs are returned as a dictionary in the form: - + { 'filename': 'string of the diff' } - + All headers are stripped, so the an entire diff looks like this: - + unchanged line unchanged line -removed line @@ -400,52 +396,52 @@ -removed line unchanged line unchanged line - + When adding a comment, the line number given should be the line number from this diff (starting at 0). To comment on the first two removed lines in the above example you would pass [2, 3]. - + """ - + target_files = self.target[self.node].files() if not filenames: filenames = target_files else: filenames = filter(lambda f: self.has_diff(f), filenames) - + opts['unified'] = '100000' node2 = self.node node1 = self.target[node2].parents()[0].node() - + diffs = {} for filename in filenames: m = cmdutil.matchfiles(self.target, [filename]) d = patch.diff(self.target, node1, node2, match=m, opts=patch.diffopts(self.ui, opts)) - + # patch.diff will give us back a generator with two items # the first is the diff --git header, which we don't care about d.next() - + # the second is the diff's contents, which is what we want, # minus the header diffs[filename] = '\n'.join(d.next().splitlines()[3:]) - + return diffs - + def diffs(self, filenames=None, context=5): """Return a mapping of diff lines for the given files (or all). - + If the filenames argument is not used, diffs for every file in the changeset will be returned. - + The diffs are returned in a dictionary of the form: - - { + + { 'filename': { # the line number of the last line of the FULL diff 'max': 90, - + # A sorted list of tuples of (line_number, line_content) 'content': [ (10, ' context line'), @@ -463,70 +459,70 @@ ], }, } - + There's a lot of structure there, but it will provide everything you need to display contextualized diffs. """ - + ds = self.full_diffs(filenames, {}) - + def _filter_diff(d): for n, line in enumerate(d): start = n - context if n > context else 0 end = n + context + 1 if any(filter(lambda l: l[0] in '+-', d[start:end])): yield (n, line) - + for filename, content in ds.iteritems(): content = content.splitlines() ds[filename] = { 'max': len(content) - 1, 'content': list(_filter_diff(content)), } - + return ds - + def annotated_diff(self, filename, context=5): """Return a generator that yields annotated lines of a diff. - + The first item yielded will be a simple integer of the last line number of the diff. This is ugly but useful when creating monospaced line-number-prefixed output. - + Each line yielded will be of the form: - + { # If 'skipped' is not None, this line is a "skip" line, which # represents a group of lines that were skipped due to context. 'skipped': 23, - + # The line number of this line, or None for skip lines. 'number': 585, - + # The actual content of this line, or None for skip lines. 'content': '+added line', - + # Any comments that apply to this line. # If the line is a skip line, this will be any comments that apply # to any line in the skipped group. 'comments': [ReviewComment(), ReviewComment()], } """ - + diffs = self.diffs([filename], context).values() if not diffs: return - + diff = diffs[0] max_line, content = diff['max'], diff['content'] line_level_comments = self.line_level_comments(filename) previous_n = -1 - + if content: yield content[-1][0] else: yield 0 - + for n, line in content: if n - 1 > previous_n: yield { @@ -537,7 +533,7 @@ line_level_comments ), } - + yield { 'skipped': None, 'number': n, 'content': line, @@ -545,9 +541,9 @@ lambda c: max(c.lines) == n, line_level_comments ) } - + previous_n = n - + if previous_n < max_line: yield { 'skipped': max_line - previous_n, @@ -557,26 +553,26 @@ line_level_comments ), } - + def has_diff(self, filename): """Return whether the given filename has a diff in this revision.""" return filename in self.files() - + def files(self): """Return the list of files in the revision for this ReviewChangeset.""" return self.target[self.node].files() - - + + def review_level_comments(self): """Comments on this changeset which aren't on a particular file.""" return filter(lambda c: not c.filename, self.comments) - + def file_level_comments(self, filename=None): """Comments on this changeset that are on a file, but not a line. - + If a file is given only comments for that file will be returned. """ - + if filename: return filter( lambda c: filename == c.filename and not c.lines, self.comments @@ -585,13 +581,13 @@ return filter( lambda c: filename and not c.lines, self.comments ) - + def line_level_comments(self, filename=None): """Comments on this changeset that are on a line of file. - + If a file is given only comments for that file will be returned. """ - + if filename: return filter( lambda c: filename == c.filename and c.lines, self.comments @@ -600,7 +596,6 @@ return filter( lambda c: filename and c.lines, self.comments ) - class _ReviewObject(object): """A base object for some kind of review data (a signoff or comment).""" @@ -608,52 +603,51 @@ self.container = container self.commit_message = commit_message self.delete_message = delete_message - + def _commit(self, ui, repo): """Write and commit this object to the given repo.""" - + path = os.path.join(repo.root, self.node, self.container) if not os.path.exists(path): os.mkdir(path) - + data = self._render_data() filename = util.sha1(data).hexdigest() objectpath = os.path.join(path, filename) - + with open(objectpath, 'w') as objectfile: objectfile.write(data) - + cmdutil.commit(ui, repo, _commitfunc, [objectpath], { 'message': self.commit_message % self.node, 'addremove': True, }) - + def _delete(self, ui, repo): """Delete and commit this object in the given repo.""" - + if not self.delete_message: raise CannotDeleteObject - + data = self._render_data() filename = util.sha1(data).hexdigest() objectpath = os.path.join(repo.root, self.node, self.container, filename) - + os.remove(objectpath) - + cmdutil.commit(ui, repo, _commitfunc, [objectpath], { 'message': self.delete_message % self.node, 'addremove': True, }) - - + + @property def local_datetime(self): return _datetime_from_hgdate(self.hgdate) - class ReviewComment(_ReviewObject): """A single review comment. - + A list of comments can be retrieved from a ReviewChangeset. - + The following pieces of information are stored for comments: - + comment = rcset.comments[0] comment.author comment.hgdate @@ -663,30 +657,30 @@ comment.message comment.local_datetime comment.identifier - + Each item is a string, except for lines, hgdate, and local_datetime. - + lines is a list of ints. - + hgdate is a tuple of (seconds from the epoch, seconds offset from UTC), which is the format Mercurial itself uses internally. - + local_datetime is a datetime object representing what a clock on the wall next to the current computer would have read at the instant the comment was added. - + """ def __init__(self, author, hgdate, node, filename, lines, message, identifier=None, **extra): """Initialize a ReviewComment. - + You shouldn't need to create these directly -- use a ReviewChangeset to add comments and retrieve existing ones: - + review_data = ReviewDatastore(ui, repo) tip_review = review_data['tip'] tip_review.add_comment(...) tip_comments = tip_review.comments - + """ super(ReviewComment, self).__init__( container='comments', commit_message=messages.COMMIT_COMMENT, @@ -698,22 +692,22 @@ self.lines = lines self.message = message self.identifier = identifier - + def _render_data(self): """Render the data of this comment into a string for writing to disk. - + You probably don't need to call this directly, the add_comment method of a ReviewChangeset will handle it for you. - + """ rendered_date = util.datestr(self.hgdate) lines = ','.join(self.lines) return file_templates.COMMENT_FILE_TEMPLATE % ( self.author, rendered_date, self.node, self.filename, lines, self.message ) - + def __str__(self): """Stringify this comment for easy printing (for debugging).""" - + return '\n'.join(map(str, [ self.author, self.hgdate, @@ -723,15 +717,14 @@ self.message, '\n', ])) - class ReviewSignoff(_ReviewObject): """A single review signoff. - + A list of signoffs can be retrieved from a ReviewChangeset. - + The following pieces of information are stored for signoffs: - + signoff = rcset.comments[0] signoff.author signoff.hgdate @@ -739,28 +732,28 @@ signoff.opinion signoff.message signoff.identifier - + Each item is a string, except for hgdate and local_datetime. - + hgdate is a tuple of (seconds from the epoch, seconds offset from UTC), which is the format Mercurial itself uses internally. - + local_datetime is a datetime object representing what a clock on the wall next to the current computer would have read at the instant the signoff was added. - + """ def __init__(self, author, hgdate, node, opinion, message, identifier=None, **extra): """Initialize a ReviewSignoff. - + You shouldn't need to create these directly -- use a ReviewChangeset to add signoffs and retrieve existing ones: - + review_data = ReviewDatastore(ui, repo) tip_review = review_data['tip'] tip_review.add_signoff(...) tip_signoffs = tip_review.signoffs - + """ super(ReviewSignoff, self).__init__( container='signoffs', commit_message=messages.COMMIT_SIGNOFF, @@ -772,16 +765,16 @@ self.opinion = opinion self.message = message self.identifier = identifier - + def _render_data(self): """Render the data of this signoff into a string for writing to disk. - + You probably don't need to call this directly, the add_signoff method of a ReviewChangeset will handle it for you. - + """ rendered_date = util.datestr(self.hgdate) return file_templates.SIGNOFF_FILE_TEMPLATE % ( self.author, rendered_date, self.node, self.opinion, self.message ) - + diff -r a55ffc78928c -r a909c2ba47f0 review/tests/test_comment.py --- a/review/tests/test_comment.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/tests/test_comment.py Fri Jun 11 20:07:20 2010 -0400 @@ -6,7 +6,6 @@ from mercurial import util as hgutil from mercurial.node import hex - a1, a2 = (messages.REVIEW_LOG_COMMENT_AUTHOR % '|').split('|') @with_setup(setup_reviewed_sandbox, teardown_sandbox) @@ -14,7 +13,6 @@ output = review() assert messages.REVIEW_LOG_COMMENTS % (0, 0) in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_blank_comment(): try: @@ -25,77 +23,73 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_parent_rev(): review(comment=True, message='Test comment one.') - + output = review() assert messages.REVIEW_LOG_COMMENTS % (1, 1) in output - + assert a1 in output assert a2 in output - + assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + review(comment=True, message='Test comment two.') - + output = review() assert messages.REVIEW_LOG_COMMENTS % (2, 1) in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment two.' in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_specific_rev(): review(comment=True, message='Test comment one.', rev='0') - + output = review(rev='0') assert messages.REVIEW_LOG_COMMENTS % (1, 1) in output - + assert a1 in output assert a2 in output - + assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + output = review() assert messages.REVIEW_LOG_COMMENTS % (0, 0) in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - + review(comment=True, message='Test comment two.', rev='0') - + output = review(rev='0') assert messages.REVIEW_LOG_COMMENTS % (2, 1) in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment two.' in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file(): review(comment=True, message='Test comment one.', rev='1', files=['file_one']) - + output = review(rev='1', files=['file_one']) assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + output = review(rev='1', files=['file_two']) assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - + output = review(rev='0', files=['file_one']) assert a1 not in output assert a2 not in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_multiple_files(): review(comment=True, message='Test comment.', rev='1', files=['file_one', 'always_changing']) - + output = review(rev='1') assert output.count(messages.REVIEW_LOG_COMMENT_LINE % 'Test comment.') == 2 - + try: review(comment=True, rev='1', message='Test bad comment.', lines='1', files=['file_one', 'always_changing']) @@ -105,7 +99,6 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_bad_file(): try: @@ -116,7 +109,6 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file_line(): try: @@ -126,17 +118,17 @@ assert messages.COMMENT_LINES_REQUIRE_FILE in error else: assert False, 'The correct error message was not printed.' - + review(comment=True, rev='1', message='Test comment one.', files=['file_one'], lines='1') - + output = review(rev='1', files=['file_one']) - + # Make sure the comment is present at all. assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + # Make sure it's in the correct place output = output.splitlines() for n, line in enumerate(output): @@ -144,19 +136,18 @@ assert output[n-1].strip().startswith('1') break - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file_lines(): review(comment=True, rev='1', message='Test comment one.', files=['file_one'], lines='1,2') - + output = review(rev='1', files=['file_one']) - + # Make sure the comment is present at all. assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + # Make sure it's in the correct place output = output.splitlines() for n, line in enumerate(output): @@ -164,96 +155,92 @@ assert output[n-1].strip().startswith('2') break - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file_in_subdir(): filename = os.path.join('test_dir', 'test_file') - + review(comment=True, message='Test comment one.', rev='1', files=[filename]) - + output = review(rev='1', files=[filename]) assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + output = review(rev='1', files=['file_two']) assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - + output = review(rev='0', files=[filename]) assert a1 not in output assert a2 not in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file_in_cwd(): os.chdir('test_dir') review(comment=True, message='Test comment one.', rev='1', files=['test_file']) - + output = review(rev='1', files=['test_file']) assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + output = review(rev='1', files=['file_two']) assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - + output = review(rev='0', files=['test_file']) assert a1 not in output assert a2 not in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_add_comments_to_file_in_reldir(): filename = os.path.join('..', 'file_three') - + os.chdir('test_dir') review(comment=True, message='Test comment one.', rev='1', files=[filename]) - + output = review(rev='1', files=[filename]) assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + output = review(rev='1', files=['file_two']) assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - + output = review(rev='0', files=[filename]) assert a1 not in output assert a2 not in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_comment_identifiers(): review(comment=True, message='Test comment one.', rev='1', files=['file_one']) - + rd = api.ReviewDatastore(get_ui(), get_sandbox_repo()) dsr = get_datastore_repo() - + comment = rd['1'].comments[0] - + identifier = comment.identifier short_identifier = identifier[:12] - + comment_filename = api._split_path_dammit(dsr['tip'].files()[0])[-1] comment_cset = hex(dsr['tip'].node()) - + assert identifier == comment_filename assert identifier != comment_cset - + verbose_identifier = messages.REVIEW_LOG_IDENTIFIER % identifier[:12] debug_identifier = messages.REVIEW_LOG_IDENTIFIER % identifier - + normal_output = review(rev='1') assert verbose_identifier not in normal_output assert debug_identifier not in normal_output - + verbose_output = review(rev='1', verbose=True) assert verbose_identifier in verbose_output assert debug_identifier not in verbose_output - + debug_output = review(rev='1', debug=True) assert verbose_identifier not in debug_output assert debug_identifier in debug_output diff -r a55ffc78928c -r a909c2ba47f0 review/tests/test_diffs.py --- a/review/tests/test_diffs.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/tests/test_diffs.py Fri Jun 11 20:07:20 2010 -0400 @@ -2,74 +2,68 @@ from util import * from .. import messages -import os - - a1, a2 = (messages.REVIEW_LOG_COMMENT_AUTHOR % '|').split('|') s1, s2 = (messages.REVIEW_LOG_SKIPPED % 1111).split('1111') @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_review_diff_default_context(): output = review(rev='1', files=['long_file'], unified='5') - + assert ' 0:' not in output assert messages.REVIEW_LOG_SKIPPED % 1 in output - + for n in range(1, 20): assert '%2d:' % n in output - + assert '-g' in output assert '+X' in output assert '-m' in output assert '+Y' in output - + assert '20:' not in output assert messages.REVIEW_LOG_SKIPPED % 2 in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_review_diff_full_context(): output = review(rev='1', files=['long_file'], unified='10000') - + assert s1 not in output assert s2 not in output - + for n in range(0, 21): assert '%2d:' % n in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_review_diff_small_context(): output = review(rev='1', files=['long_file'], unified='2') - + assert ' 3:' not in output assert messages.REVIEW_LOG_SKIPPED % 4 in output - + assert '-g' in output assert '+X' in output - + assert '10:' not in output assert messages.REVIEW_LOG_SKIPPED % 2 in output - + assert '-m' in output assert '+Y' in output - + assert '17:' not in output assert messages.REVIEW_LOG_SKIPPED % 5 in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_review_diff_with_comment(): review(comment=True, rev='1', message='Test comment one.', files=['long_file'], lines='6,7') - + output = review(rev='1', files=['long_file'], unified=0) - + # Make sure the comment is present at all. assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + # Make sure it's in the correct place output = output.splitlines() for n, line in enumerate(output): @@ -77,19 +71,18 @@ assert output[n-1].strip().startswith('7') break - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_review_diff_with_skipped_comment(): review(comment=True, rev='1', message='Test comment one.', files=['long_file'], lines='3') - + output = review(rev='1', files=['long_file'], unified=0) - + # Make sure the comment is present at all. assert a1 in output assert a2 in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output - + # Make sure it's in the correct place output = output.splitlines() for n, line in enumerate(output): diff -r a55ffc78928c -r a909c2ba47f0 review/tests/test_init.py --- a/review/tests/test_init.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/tests/test_init.py Fri Jun 11 20:07:20 2010 -0400 @@ -7,24 +7,22 @@ import os from mercurial import util as hgutil - @with_setup(setup_sandbox, teardown_sandbox) def test_init(): sandbox = get_sandbox_repo() - + output = review(init=True, remote_path='/sandbox-review') assert messages.INIT_SUCCESS_UNCOMMITTED in output - + assert '.hgreview' not in sandbox['tip'] assert os.path.exists('.hgreview') assert os.path.isdir(api.DEFAULT_DATASTORE_DIRNAME) assert get_datastore_repo(api.DEFAULT_DATASTORE_DIRNAME) - + with open('.hgreview', 'r') as hgrf: hgr = hgrf.read() assert 'remote = /sandbox-review' in hgr - @with_setup(setup_sandbox, teardown_sandbox) def test_init_without_remote_path(): try: @@ -35,11 +33,10 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_sandbox, teardown_sandbox) def test_init_twice(): review(init=True, remote_path='/sandbox-review') - + try: review(init=True, remote_path='/sandbox-review') except hgutil.Abort, e: @@ -48,21 +45,20 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_init_clone(): review(comment=True, message='Test comment one.') review(comment=True, rev='0', message='Test comment two.') - + clone_sandbox_repo() os.chdir(sandbox_clone_path) - + review(init=True) - + output = review() assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment two.' not in output - + output = review(rev='0') assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment one.' not in output assert messages.REVIEW_LOG_COMMENT_LINE % 'Test comment two.' in output diff -r a55ffc78928c -r a909c2ba47f0 review/tests/test_signoff.py --- a/review/tests/test_signoff.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/tests/test_signoff.py Fri Jun 11 20:07:20 2010 -0400 @@ -1,11 +1,8 @@ from nose import * from util import * -from .. import messages - -import os from mercurial import util as hgutil from mercurial.node import hex - +from .. import messages s1, s2 = (messages.REVIEW_LOG_SIGNOFF_AUTHOR % ('|', 'neutral')).split('|') sy1, sy2 = (messages.REVIEW_LOG_SIGNOFF_AUTHOR % ('|', 'yes')).split('|') @@ -16,7 +13,6 @@ output = review() assert messages.REVIEW_LOG_SIGNOFFS % (0, 0, 0, 0) in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_blank_signoff(): try: @@ -27,34 +23,31 @@ else: assert False, 'The correct error message was not printed.' - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_signoff_on_parent_rev(): review(signoff=True, message='Test signoff one.') - + output = review() assert messages.REVIEW_LOG_SIGNOFFS % (1, 0, 0, 1) in output - + assert s1 in output assert s1 in output assert messages.REVIEW_LOG_SIGNOFF_LINE % 'Test signoff one.' in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_signoff_on_specific_rev(): review(signoff=True, message='Test signoff one.', rev='0') - + output = review(rev='0') assert messages.REVIEW_LOG_SIGNOFFS % (1, 0, 0, 1) in output - + output = review() assert messages.REVIEW_LOG_SIGNOFFS % (0, 0, 0, 0) in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_multiple_signoffs(): review(signoff=True, message='Test signoff one.') - + try: review(signoff=True, message='Test signoff two.') except hgutil.Abort, e: @@ -62,66 +55,63 @@ assert messages.SIGNOFF_EXISTS in error else: assert False, 'The correct error message was not printed.' - + review(signoff=True, message='Test signoff two.', force=True) - + output = review() assert messages.REVIEW_LOG_SIGNOFFS % (1, 0, 0, 1) in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_signoff_yes(): review(signoff=True, yes=True, message='Test signoff one.') - + output = review() assert messages.REVIEW_LOG_SIGNOFFS % (1, 1, 0, 0) in output - + assert sy1 in output assert sy1 in output assert messages.REVIEW_LOG_SIGNOFF_LINE % 'Test signoff one.' in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_signoff_no(): review(signoff=True, no=True, message='Test signoff one.') - + output = review() assert messages.REVIEW_LOG_SIGNOFFS % (1, 0, 1, 0) in output - + assert sn1 in output assert sn1 in output assert messages.REVIEW_LOG_SIGNOFF_LINE % 'Test signoff one.' in output - @with_setup(setup_reviewed_sandbox, teardown_sandbox) def test_signoff_identifiers(): review(signoff=True, message='Test signoff one.', rev='0') - + rd = api.ReviewDatastore(get_ui(), get_sandbox_repo()) dsr = get_datastore_repo() - + signoff = rd['0'].signoffs[0] - + identifier = signoff.identifier short_identifier = identifier[:12] - + signoff_filename = api._split_path_dammit(dsr['tip'].files()[0])[-1] signoff_cset = hex(dsr['tip'].node()) - + assert identifier == signoff_filename assert identifier != signoff_cset - + verbose_identifier = messages.REVIEW_LOG_IDENTIFIER % identifier[:12] debug_identifier = messages.REVIEW_LOG_IDENTIFIER % identifier - + normal_output = review(rev='0') assert verbose_identifier not in normal_output assert debug_identifier not in normal_output - + verbose_output = review(rev='0', verbose=True) assert verbose_identifier in verbose_output assert debug_identifier not in verbose_output - + debug_output = review(rev='0', debug=True) assert verbose_identifier not in debug_output assert debug_identifier in debug_output diff -r a55ffc78928c -r a909c2ba47f0 review/tests/util.py --- a/review/tests/util.py Fri Jun 11 19:59:34 2010 -0400 +++ b/review/tests/util.py Fri Jun 11 20:07:20 2010 -0400 @@ -1,19 +1,20 @@ +from __future__ import with_statement + """Utilities for writing unit tests for hg-review.""" -from __future__ import with_statement + import os, shutil import sample_data from mercurial import cmdutil, commands, hg, ui from .. import api, extension_ui - _ui = ui.ui() def review(init=False, comment=False, signoff=False, yes=False, no=False, force=False, message='', rev='.', remote_path='', lines='', files=None, unified='5', web=False, verbose=False, debug=False): - + if not files: files = [] - + _ui.pushbuffer() if debug: _ui.debugflag = True @@ -29,7 +30,7 @@ ) _ui.verbose, _ui.debugflag = False, False output = _ui.popbuffer() - + print output return output @@ -41,29 +42,29 @@ def setup_sandbox(): os.mkdir(sandbox_path) os.chdir(sandbox_path) - + os.mkdir(sandbox_repo_path) os.chdir(sandbox_repo_path) - + commands.init(_ui) sandbox = get_sandbox_repo() - + opts = { 'addremove': True, 'date': None, 'user': 'Review Tester', 'logfile': None, 'message': "Sandbox commit.", } for state in sample_data.log: for filename in state: dirname, key = None, filename - + # Support one-level-deep directories in the sample data. if '/' in filename: dirname, _, filename = filename.partition('/') if not os.path.exists(dirname): os.mkdir(dirname) os.chdir(dirname) - + with open(filename, 'w') as f: f.write(state[key]) - + if dirname: os.chdir('..') commands.commit(_ui, sandbox, **opts) @@ -71,10 +72,10 @@ def setup_reviewed_sandbox(): setup_sandbox() sandbox = get_sandbox_repo() - + rpath = os.path.join(sandbox.root, api.DEFAULT_DATASTORE_DIRNAME) review(init=True, remote_path=rpath) - + opts = { 'addremove': True, 'date': None, 'user': 'Review Tester', 'logfile': None, 'message': "Add the code review.", } commands.commit(_ui, sandbox, **opts)