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 ######################################################################
24 from svntest
.main
import server_has_mergeinfo
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"
37 keys
= ['revision', 'author', 'text']
43 # Tokenize and parse each line
44 for line_str
in output
:
48 this_line
['merged'] = (line_str
[0] == 'G')
49 line_str
= line_str
[2:]
51 tokens
= line_str
.split(None, max_split
)
54 this_line
['revision'] = None
56 this_line
['revision'] = int(tokens
[0])
59 this_line
['author'] = None
61 this_line
['author'] = tokens
[1]
63 this_line
['text'] = tokens
[2]
65 results
.append(this_line
)
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
]
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 ######################################################################
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"
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',
98 svntest
.main
.run_svn(None, 'blame', file_path
)
101 def blame_binary(sbox
):
102 "annotate a binary file"
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',
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',
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',
126 exit_code
, 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 exit_code
, 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
150 sbox
.build(read_only
= True)
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 exit_code
, 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
):
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"
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
,
187 # Retrieve last changed date from svn info
188 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
190 'log', file_path
, '--xml', '-r1:2')
195 if line
.find("<date>") >= 0:
203 raise svntest
.Failure
205 template
= ['<?xml version="1.0"?>\n',
208 ' path="' + file_path
+ '">\n',
210 ' line-number="1">\n',
213 '<author>jrandom</author>\n',
218 ' line-number="2">\n',
221 '<author>jrandom</author>\n',
228 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
230 'blame', file_path
, '--xml')
232 for i
in range(0, len(output
)):
233 if output
[i
] != template
[i
]:
234 raise svntest
.Failure
237 # For a line changed before the requested start revision, blame should not
238 # print a revision number (as fixed in r8035) or crash (as it did with
239 # "--verbose" before being fixed in r9890).
241 def blame_on_unknown_revision(sbox
):
242 "blame lines from unknown revisions"
248 file_path
= os
.path
.join(wc_dir
, file_name
)
251 svntest
.main
.file_append(file_path
, "\nExtra line %d" % (i
))
252 expected_output
= svntest
.wc
.State(wc_dir
, {
253 'iota' : Item(verb
='Sending'),
255 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
258 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
260 'blame', file_path
, '-rHEAD:HEAD')
262 if output
[0].find(" - This is the file 'iota'.") == -1:
263 raise svntest
.Failure
265 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
267 'blame', file_path
, '--verbose', '-rHEAD:HEAD')
269 if output
[0].find(" - This is the file 'iota'.") == -1:
270 raise svntest
.Failure
274 # The default blame revision range should be 1:N, where N is the
275 # peg-revision of the target, or BASE or HEAD if no peg-revision is
278 def blame_peg_rev(sbox
):
279 "blame targets with peg-revisions"
283 expected_output_r1
= [
284 " 1 jrandom This is the file 'iota'.\n" ]
286 os
.chdir(sbox
.wc_dir
)
288 # Modify iota and commit it (r2).
289 svntest
.main
.file_write('iota', "This is no longer the file 'iota'.\n")
290 expected_output
= svntest
.wc
.State('.', {
291 'iota' : Item(verb
='Sending'),
293 svntest
.actions
.run_and_verify_commit('.', expected_output
, None)
295 # Check that we get a blame of r1 when we specify a peg revision of r1
296 # and no explicit revision.
297 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
300 # Check that an explicit revision overrides the default provided by
302 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
303 'blame', 'iota@2', '-r1')
305 def blame_eol_styles(sbox
):
306 "blame with different eol styles"
313 file_path
= os
.path
.join(wc_dir
, file_name
)
315 expected_output
= svntest
.wc
.State(wc_dir
, {
316 'iota' : Item(verb
='Sending'),
319 # do the test for each eol-style
320 for eol
in ['CR', 'LF', 'CRLF', 'native']:
321 svntest
.main
.file_write(file_path
, "This is no longer the file 'iota'.\n")
324 svntest
.main
.file_append(file_path
, "Extra line %d" % (i
) + "\n")
325 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
328 svntest
.main
.run_svn(None, 'propset', 'svn:eol-style', eol
,
331 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
334 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
336 'blame', file_path
, '-r1:HEAD')
338 # output is a list of lines, there should be 3 lines
340 raise svntest
.Failure('Expected 3 lines in blame output but got %d: \n' %
341 len(output
) + str(output
))
343 def blame_ignore_whitespace(sbox
):
344 "ignore whitespace when blaming"
350 file_path
= os
.path
.join(wc_dir
, file_name
)
352 svntest
.main
.file_write(file_path
,
356 expected_output
= svntest
.wc
.State(wc_dir
, {
357 'iota' : Item(verb
='Sending'),
359 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
362 # commit only whitespace changes
363 svntest
.main
.file_write(file_path
,
367 expected_output
= svntest
.wc
.State(wc_dir
, {
368 'iota' : Item(verb
='Sending'),
370 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
373 # match the blame output, as defined in the blame code:
374 # "%6ld %10s %s %s%s", rev, author ? author : " -",
375 # time_stdout , line, APR_EOL_STR
382 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
383 None, expected_output
, [],
384 'blame', '-x', '-w', file_path
)
386 # commit some changes
387 svntest
.main
.file_write(file_path
,
392 expected_output
= svntest
.wc
.State(wc_dir
, {
393 'iota' : Item(verb
='Sending'),
395 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
400 " 4 jrandom Xxxx X\n",
401 " 4 jrandom Bb b \n",
405 svntest
.actions
.run_and_verify_svn(None, expected_output
, [],
406 'blame', '-x', '-w', file_path
)
408 def blame_ignore_eolstyle(sbox
):
409 "ignore eol styles when blaming"
415 file_path
= os
.path
.join(wc_dir
, file_name
)
417 svntest
.main
.file_write(file_path
,
421 expected_output
= svntest
.wc
.State(wc_dir
, {
422 'iota' : Item(verb
='Sending'),
424 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
427 # commit only eol changes
428 svntest
.main
.file_write(file_path
,
432 expected_output
= svntest
.wc
.State(wc_dir
, {
433 'iota' : Item(verb
='Sending'),
435 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
444 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
445 None, expected_output
, [],
446 'blame', '-x', '--ignore-eol-style', file_path
)
449 def blame_merge_info(sbox
):
450 "test 'svn blame -g'"
452 from log_tests
import merge_history_repos
453 merge_history_repos(sbox
)
456 iota_path
= os
.path
.join(wc_dir
, 'trunk', 'iota')
458 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
460 'blame', '-g', iota_path
)
464 'author' : 'jrandom',
465 'text' : "This is the file 'iota'.\n",
469 'author' : 'jrandom',
470 'text' : "'A' has changed a bit, with 'upsilon', and 'xi'.\n",
474 parse_and_verify_blame(output
, expected_blame
, 1)
477 def blame_merge_out_of_range(sbox
):
478 "don't look for merged files out of range"
480 from log_tests
import merge_history_repos
481 merge_history_repos(sbox
)
484 upsilon_path
= os
.path
.join(wc_dir
, 'trunk', 'A', 'upsilon')
486 exit_code
, output
, error
= svntest
.actions
.run_and_verify_svn(
488 'blame', '-g', upsilon_path
)
492 'author' : 'jrandom',
493 'text' : "This is the file 'upsilon'.\n",
498 'text' : "There is also the file 'xi'.\n",
502 parse_and_verify_blame(output
, expected_blame
, 1)
504 # test for issue #2888: 'svn blame' aborts over ra_serf
505 def blame_peg_rev_file_not_in_head(sbox
):
506 "blame target not in HEAD with peg-revisions"
510 expected_output_r1
= [
511 " 1 jrandom This is the file 'iota'.\n" ]
513 os
.chdir(sbox
.wc_dir
)
515 # Modify iota and commit it (r2).
516 svntest
.main
.file_write('iota', "This is no longer the file 'iota'.\n")
517 expected_output
= svntest
.wc
.State('.', {
518 'iota' : Item(verb
='Sending'),
520 svntest
.actions
.run_and_verify_commit('.', expected_output
, None)
522 # Delete iota so that it doesn't exist in HEAD
523 svntest
.main
.run_svn(None, 'rm', sbox
.repo_url
+ '/iota',
526 # Check that we get a blame of r1 when we specify a peg revision of r1
527 # and no explicit revision.
528 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
531 # Check that an explicit revision overrides the default provided by
533 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
534 'blame', 'iota@2', '-r1')
536 def blame_file_not_in_head(sbox
):
537 "blame target not in HEAD"
539 sbox
.build(create_wc
= False, read_only
= True)
540 notexisting_url
= sbox
.repo_url
+ '/notexisting'
542 # Check that a correct error message is printed when blaming a target that
543 # doesn't exist (in HEAD).
544 expected_err
= ".*notexisting' (is not a file.*|path not found)"
545 svntest
.actions
.run_and_verify_svn(None, [], expected_err
,
546 'blame', notexisting_url
)
549 ########################################################################
553 # list all tests here, starting with None:
559 blame_on_unknown_revision
,
562 blame_ignore_whitespace
,
563 blame_ignore_eolstyle
,
564 SkipUnless(blame_merge_info
, server_has_mergeinfo
),
565 SkipUnless(blame_merge_out_of_range
, server_has_mergeinfo
),
566 blame_peg_rev_file_not_in_head
,
567 blame_file_not_in_head
,
570 if __name__
== '__main__':
571 svntest
.main
.run_tests(test_list
)