Add a little more to the svn_rangelist_intersect test to test the
[svn.git] / subversion / tests / cmdline / blame_tests.py
blobf49ef6759d5aa8e517bab257378342cc5eec4bb9
1 #!/usr/bin/env python
3 # blame_tests.py: testing line-by-line annotation.
5 # Subversion is a tool for revision control.
6 # See http://subversion.tigris.org for more information.
8 # ====================================================================
9 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
11 # This software is licensed as described in the file COPYING, which
12 # you should have received as part of this distribution. The terms
13 # are also available at http://subversion.tigris.org/license-1.html.
14 # If newer versions of this license are posted there, you may use a
15 # newer version instead, at your option.
17 ######################################################################
19 # General modules
20 import os, sys
22 # Our testing module
23 import svntest
24 from svntest.main import server_has_mergeinfo
26 # (abbreviation)
27 Skip = svntest.testcase.Skip
28 SkipUnless = svntest.testcase.SkipUnless
29 XFail = svntest.testcase.XFail
30 Item = svntest.wc.StateItem
32 # Helper function to validate the output of a particular run of blame.
33 def parse_and_verify_blame(output, expected_blame, with_merged=0):
34 "tokenize and validate the output of blame"
36 max_split = 2
37 keys = ['revision', 'author', 'text']
38 if with_merged:
39 keys.append('merged')
41 results = []
43 # Tokenize and parse each line
44 for line_str in output:
45 this_line = {}
47 if with_merged:
48 this_line['merged'] = (line_str[0] == 'G')
49 line_str = line_str[2:]
51 tokens = line_str.split(None, max_split)
53 if tokens[0] == '-':
54 this_line['revision'] = None
55 else:
56 this_line['revision'] = int(tokens[0])
58 if tokens[1] == '-':
59 this_line['author'] = None
60 else:
61 this_line['author'] = tokens[1]
63 this_line['text'] = tokens[2]
65 results.append(this_line)
67 # Verify the results
68 if len(results) != len(expected_blame):
69 raise svntest.Failure, "expected and actual results not the same length"
71 pairs = zip(results, expected_blame)
72 for num in xrange(len(pairs)):
73 (item, expected_item) = pairs[num]
74 for key in keys:
75 if item[key] != expected_item[key]:
76 raise svntest.Failure, 'on line %d, expecting %s "%s", found "%s"' % \
77 (num+1, key, str(expected_item[key]), str(item[key]))
80 ######################################################################
81 # Tests
83 # Each test must return on success or raise on failure.
86 #----------------------------------------------------------------------
88 def blame_space_in_name(sbox):
89 "annotate a file whose name contains a space"
90 sbox.build()
92 file_path = os.path.join(sbox.wc_dir, 'space in name')
93 svntest.main.file_append(file_path, "Hello\n")
94 svntest.main.run_svn(None, 'add', file_path)
95 svntest.main.run_svn(None, 'ci',
96 '-m', '', file_path)
98 svntest.main.run_svn(None, 'blame', file_path)
101 def blame_binary(sbox):
102 "annotate a binary file"
103 sbox.build()
104 wc_dir = sbox.wc_dir
106 # First, make a new revision of iota.
107 iota = os.path.join(wc_dir, 'iota')
108 svntest.main.file_append(iota, "New contents for iota\n")
109 svntest.main.run_svn(None, 'ci',
110 '-m', '', iota)
112 # Then do it again, but this time we set the mimetype to binary.
113 iota = os.path.join(wc_dir, 'iota')
114 svntest.main.file_append(iota, "More new contents for iota\n")
115 svntest.main.run_svn(None, 'propset', 'svn:mime-type', 'image/jpeg', iota)
116 svntest.main.run_svn(None, 'ci',
117 '-m', '', iota)
119 # Once more, but now let's remove that mimetype.
120 iota = os.path.join(wc_dir, 'iota')
121 svntest.main.file_append(iota, "Still more new contents for iota\n")
122 svntest.main.run_svn(None, 'propdel', 'svn:mime-type', iota)
123 svntest.main.run_svn(None, 'ci',
124 '-m', '', iota)
126 output, errput = svntest.main.run_svn(2, 'blame', iota)
127 if (len(errput) != 1) or (errput[0].find('Skipping') == -1):
128 raise svntest.Failure
130 # But with --force, it should work.
131 output, errput = svntest.main.run_svn(2, 'blame', '--force', iota)
132 if (len(errput) != 0 or len(output) != 4):
133 raise svntest.Failure
138 # Issue #2154 - annotating a directory should fail
139 # (change needed if the desired behavior is to
140 # run blame recursively on all the files in it)
142 def blame_directory(sbox):
143 "annotating a directory not allowed"
145 # Issue 2154 - blame on directory fails without error message
147 import re
149 # Setup
150 sbox.build(read_only = True)
151 wc_dir = sbox.wc_dir
152 dir = os.path.join(wc_dir, 'A')
154 # Run blame against directory 'A'. The repository error will
155 # probably include a leading slash on the path, but we'll tolerate
156 # it either way, since either way it would still be a clean error.
157 expected_error = ".*'[/]{0,1}A' is not a file"
158 outlines, errlines = svntest.main.run_svn(1, 'blame', dir)
160 # Verify expected error message is output
161 for line in errlines:
162 if re.match(expected_error, line):
163 break
164 else:
165 raise svntest.Failure('Failed to find %s in %s' %
166 (expected_error, str(errlines)))
170 # Basic test for svn blame --xml.
172 def blame_in_xml(sbox):
173 "blame output in XML format"
175 sbox.build()
176 wc_dir = sbox.wc_dir
178 file_name = "iota"
179 file_path = os.path.join(wc_dir, file_name)
180 svntest.main.file_append(file_path, "Testing svn blame --xml\n")
181 expected_output = svntest.wc.State(wc_dir, {
182 'iota' : Item(verb='Sending'),
184 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
185 None, None, wc_dir)
187 # Retrieve last changed date from svn info
188 output, error = svntest.actions.run_and_verify_svn(None, None, [],
189 'log', file_path,
190 '--xml', '-r1:2')
191 date1 = None
192 date2 = None
193 for line in output:
194 if line.find("<date>") >= 0:
195 if date1 is None:
196 date1 = line
197 continue
198 elif date2 is None:
199 date2 = line
200 break
201 else:
202 raise svntest.Failure
204 template = ['<?xml version="1.0"?>\n',
205 '<blame>\n',
206 '<target\n',
207 ' path="' + file_path + '">\n',
208 '<entry\n',
209 ' line-number="1">\n',
210 '<commit\n',
211 ' revision="1">\n',
212 '<author>jrandom</author>\n',
213 '%s' % date1,
214 '</commit>\n',
215 '</entry>\n',
216 '<entry\n',
217 ' line-number="2">\n',
218 '<commit\n',
219 ' revision="2">\n',
220 '<author>jrandom</author>\n',
221 '%s' % date2,
222 '</commit>\n',
223 '</entry>\n',
224 '</target>\n',
225 '</blame>\n']
227 output, error = svntest.actions.run_and_verify_svn(None, None, [],
228 'blame', file_path,
229 '--xml')
230 for i in range(0, len(output)):
231 if output[i] != template[i]:
232 raise svntest.Failure
235 # For a line changed before the requested start revision, blame should not
236 # print a revision number (as fixed in r8035) or crash (as it did with
237 # "--verbose" before being fixed in r9890).
239 def blame_on_unknown_revision(sbox):
240 "blame lines from unknown revisions"
242 sbox.build()
243 wc_dir = sbox.wc_dir
245 file_name = "iota"
246 file_path = os.path.join(wc_dir, file_name)
248 for i in range(1,3):
249 svntest.main.file_append(file_path, "\nExtra line %d" % (i))
250 expected_output = svntest.wc.State(wc_dir, {
251 'iota' : Item(verb='Sending'),
253 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
254 None, None, wc_dir)
256 output, error = svntest.actions.run_and_verify_svn(None, None, [],
257 'blame', file_path,
258 '-rHEAD:HEAD')
260 if output[0].find(" - This is the file 'iota'.") == -1:
261 raise svntest.Failure
263 output, error = svntest.actions.run_and_verify_svn(None, None, [],
264 'blame', file_path,
265 '--verbose',
266 '-rHEAD:HEAD')
268 if output[0].find(" - This is the file 'iota'.") == -1:
269 raise svntest.Failure
273 # The default blame revision range should be 1:N, where N is the
274 # peg-revision of the target, or BASE or HEAD if no peg-revision is
275 # specified.
277 def blame_peg_rev(sbox):
278 "blame targets with peg-revisions"
280 sbox.build()
282 expected_output_r1 = [
283 " 1 jrandom This is the file 'iota'.\n" ]
285 os.chdir(sbox.wc_dir)
287 # Modify iota and commit it (r2).
288 svntest.main.file_write('iota', "This is no longer the file 'iota'.\n")
289 expected_output = svntest.wc.State('.', {
290 'iota' : Item(verb='Sending'),
292 svntest.actions.run_and_verify_commit('.', expected_output, None)
294 # Check that we get a blame of r1 when we specify a peg revision of r1
295 # and no explicit revision.
296 svntest.actions.run_and_verify_svn(None, expected_output_r1, [],
297 'blame', 'iota@1')
299 # Check that an explicit revision overrides the default provided by
300 # the peg revision.
301 svntest.actions.run_and_verify_svn(None, expected_output_r1, [],
302 'blame', 'iota@2', '-r1')
304 def blame_eol_styles(sbox):
305 "blame with different eol styles"
307 sbox.build()
308 wc_dir = sbox.wc_dir
310 # CR
311 file_name = "iota"
312 file_path = os.path.join(wc_dir, file_name)
314 expected_output = svntest.wc.State(wc_dir, {
315 'iota' : Item(verb='Sending'),
318 # do the test for each eol-style
319 for eol in ['CR', 'LF', 'CRLF', 'native']:
320 svntest.main.file_write(file_path, "This is no longer the file 'iota'.\n")
322 for i in range(1,3):
323 svntest.main.file_append(file_path, "Extra line %d" % (i) + "\n")
324 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
325 None, None, wc_dir)
327 svntest.main.run_svn(None, 'propset', 'svn:eol-style', eol,
328 file_path)
330 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
331 None, None, wc_dir)
333 output, error = svntest.actions.run_and_verify_svn(None, None, [],
334 'blame', file_path,
335 '-r1:HEAD')
337 # output is a list of lines, there should be 3 lines
338 if len(output) != 3:
339 raise svntest.Failure('Expected 3 lines in blame output but got %d: \n' %
340 len(output) + str(output))
342 def blame_ignore_whitespace(sbox):
343 "ignore whitespace when blaming"
345 sbox.build()
346 wc_dir = sbox.wc_dir
348 file_name = "iota"
349 file_path = os.path.join(wc_dir, file_name)
351 svntest.main.file_write(file_path,
352 "Aa\n"
353 "Bb\n"
354 "Cc\n")
355 expected_output = svntest.wc.State(wc_dir, {
356 'iota' : Item(verb='Sending'),
358 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
359 None, None, wc_dir)
361 # commit only whitespace changes
362 svntest.main.file_write(file_path,
363 " A a \n"
364 " B b \n"
365 " C c \n")
366 expected_output = svntest.wc.State(wc_dir, {
367 'iota' : Item(verb='Sending'),
369 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
370 None, None, wc_dir)
372 # match the blame output, as defined in the blame code:
373 # "%6ld %10s %s %s%s", rev, author ? author : " -",
374 # time_stdout , line, APR_EOL_STR
375 expected_output = [
376 " 2 jrandom A a \n",
377 " 2 jrandom B b \n",
378 " 2 jrandom C c \n",
381 output, error = svntest.actions.run_and_verify_svn(None, expected_output, [],
382 'blame', '-x', '-w', file_path)
384 # commit some changes
385 svntest.main.file_write(file_path,
386 " A a \n"
387 "Xxxx X\n"
388 " Bb b \n"
389 " C c \n")
390 expected_output = svntest.wc.State(wc_dir, {
391 'iota' : Item(verb='Sending'),
393 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
394 None, None, wc_dir)
396 expected_output = [
397 " 2 jrandom A a \n",
398 " 4 jrandom Xxxx X\n",
399 " 4 jrandom Bb b \n",
400 " 2 jrandom C c \n",
403 svntest.actions.run_and_verify_svn(None, expected_output, [],
404 'blame', '-x', '-w', file_path)
406 def blame_ignore_eolstyle(sbox):
407 "ignore eol styles when blaming"
409 sbox.build()
410 wc_dir = sbox.wc_dir
412 file_name = "iota"
413 file_path = os.path.join(wc_dir, file_name)
415 svntest.main.file_write(file_path,
416 "Aa\n"
417 "Bb\n"
418 "Cc\n")
419 expected_output = svntest.wc.State(wc_dir, {
420 'iota' : Item(verb='Sending'),
422 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
423 None, None, wc_dir)
425 # commit only eol changes
426 svntest.main.file_write(file_path,
427 "Aa\r"
428 "Bb\r"
429 "Cc")
430 expected_output = svntest.wc.State(wc_dir, {
431 'iota' : Item(verb='Sending'),
433 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
434 None, None, wc_dir)
436 expected_output = [
437 " 2 jrandom Aa\n",
438 " 2 jrandom Bb\n",
439 " 3 jrandom Cc\n",
442 output, error = svntest.actions.run_and_verify_svn(None, expected_output, [],
443 'blame', '-x', '--ignore-eol-style', file_path)
446 def blame_merge_info(sbox):
447 "test 'svn blame -g'"
449 from log_tests import merge_history_repos
450 merge_history_repos(sbox)
452 wc_dir = sbox.wc_dir
453 iota_path = os.path.join(wc_dir, 'trunk', 'iota')
455 output, error = svntest.actions.run_and_verify_svn(None, None, [],
456 'blame', '-g', iota_path)
457 expected_blame = [
458 { 'revision' : 2,
459 'author' : 'jrandom',
460 'text' : "This is the file 'iota'.\n",
461 'merged' : 0,
463 { 'revision' : 11,
464 'author' : 'jrandom',
465 'text' : "'A' has changed a bit, with 'upsilon', and 'xi'.\n",
466 'merged' : 1,
469 parse_and_verify_blame(output, expected_blame, 1)
472 def blame_merge_out_of_range(sbox):
473 "don't look for merged files out of range"
475 from log_tests import merge_history_repos
476 merge_history_repos(sbox)
478 wc_dir = sbox.wc_dir
479 upsilon_path = os.path.join(wc_dir, 'trunk', 'A', 'upsilon')
481 output, error = svntest.actions.run_and_verify_svn(None, None, [],
482 'blame', '-g',
483 upsilon_path)
484 expected_blame = [
485 { 'revision' : 4,
486 'author' : 'jrandom',
487 'text' : "This is the file 'upsilon'.\n",
488 'merged' : 0,
490 { 'revision' : 11,
491 'author': 'jrandom',
492 'text' : "There is also the file 'xi'.\n",
493 'merged' : 1,
496 parse_and_verify_blame(output, expected_blame, 1)
498 # test for issue #2888: 'svn blame' aborts over ra_serf
499 def blame_peg_rev_file_not_in_head(sbox):
500 "blame target not in HEAD with peg-revisions"
502 sbox.build()
504 expected_output_r1 = [
505 " 1 jrandom This is the file 'iota'.\n" ]
507 os.chdir(sbox.wc_dir)
509 # Modify iota and commit it (r2).
510 svntest.main.file_write('iota', "This is no longer the file 'iota'.\n")
511 expected_output = svntest.wc.State('.', {
512 'iota' : Item(verb='Sending'),
514 svntest.actions.run_and_verify_commit('.', expected_output, None)
516 # Delete iota so that it doesn't exist in HEAD
517 svntest.main.run_svn(None, 'rm', sbox.repo_url + '/iota',
518 '-m', 'log message')
520 # Check that we get a blame of r1 when we specify a peg revision of r1
521 # and no explicit revision.
522 svntest.actions.run_and_verify_svn(None, expected_output_r1, [],
523 'blame', 'iota@1')
525 # Check that an explicit revision overrides the default provided by
526 # the peg revision.
527 svntest.actions.run_and_verify_svn(None, expected_output_r1, [],
528 'blame', 'iota@2', '-r1')
530 def blame_file_not_in_head(sbox):
531 "blame target not in HEAD"
533 sbox.build(create_wc = False, read_only = True)
534 notexisting_url = sbox.repo_url + '/notexisting'
536 # Check that a correct error message is printed when blaming a target that
537 # doesn't exist (in HEAD).
538 expected_err = ".*notexisting' (is not a file.*|path not found)"
539 svntest.actions.run_and_verify_svn(None, [], expected_err,
540 'blame', notexisting_url)
543 ########################################################################
544 # Run the tests
547 # list all tests here, starting with None:
548 test_list = [ None,
549 blame_space_in_name,
550 blame_binary,
551 blame_directory,
552 blame_in_xml,
553 blame_on_unknown_revision,
554 blame_peg_rev,
555 blame_eol_styles,
556 blame_ignore_whitespace,
557 blame_ignore_eolstyle,
558 SkipUnless(blame_merge_info, server_has_mergeinfo),
559 SkipUnless(blame_merge_out_of_range, server_has_mergeinfo),
560 blame_peg_rev_file_not_in_head,
561 blame_file_not_in_head,
564 if __name__ == '__main__':
565 svntest.main.run_tests(test_list)
566 # NOTREACHED
569 ### End of file.