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 for (num
, (item
, expected_item
)) in enumerate(zip(results
, expected_blame
)):
73 if item
[key
] != expected_item
[key
]:
74 raise svntest
.Failure
, 'on line %d, expecting %s "%s", found "%s"' % \
75 (num
+1, key
, str(expected_item
[key
]), str(item
[key
]))
78 ######################################################################
81 # Each test must return on success or raise on failure.
84 #----------------------------------------------------------------------
86 def blame_space_in_name(sbox
):
87 "annotate a file whose name contains a space"
90 file_path
= os
.path
.join(sbox
.wc_dir
, 'space in name')
91 svntest
.main
.file_append(file_path
, "Hello\n")
92 svntest
.main
.run_svn(None, 'add', file_path
)
93 svntest
.main
.run_svn(None, 'ci',
96 svntest
.main
.run_svn(None, 'blame', file_path
)
99 def blame_binary(sbox
):
100 "annotate a binary file"
104 # First, make a new revision of iota.
105 iota
= os
.path
.join(wc_dir
, 'iota')
106 svntest
.main
.file_append(iota
, "New contents for iota\n")
107 svntest
.main
.run_svn(None, 'ci',
110 # Then do it again, but this time we set the mimetype to binary.
111 iota
= os
.path
.join(wc_dir
, 'iota')
112 svntest
.main
.file_append(iota
, "More new contents for iota\n")
113 svntest
.main
.run_svn(None, 'propset', 'svn:mime-type', 'image/jpeg', iota
)
114 svntest
.main
.run_svn(None, 'ci',
117 # Once more, but now let's remove that mimetype.
118 iota
= os
.path
.join(wc_dir
, 'iota')
119 svntest
.main
.file_append(iota
, "Still more new contents for iota\n")
120 svntest
.main
.run_svn(None, 'propdel', 'svn:mime-type', iota
)
121 svntest
.main
.run_svn(None, 'ci',
124 output
, errput
= svntest
.main
.run_svn(2, 'blame', iota
)
125 if (len(errput
) != 1) or (errput
[0].find('Skipping') == -1):
126 raise svntest
.Failure
128 # But with --force, it should work.
129 output
, errput
= svntest
.main
.run_svn(2, 'blame', '--force', iota
)
130 if (len(errput
) != 0 or len(output
) != 4):
131 raise svntest
.Failure
136 # Issue #2154 - annotating a directory should fail
137 # (change needed if the desired behavior is to
138 # run blame recursively on all the files in it)
140 def blame_directory(sbox
):
141 "annotating a directory not allowed"
143 # Issue 2154 - blame on directory fails without error message
150 dir = os
.path
.join(wc_dir
, 'A')
152 # Run blame against directory 'A'. The repository error will
153 # probably include a leading slash on the path, but we'll tolerate
154 # it either way, since either way it would still be a clean error.
155 expected_error
= ".*'[/]{0,1}A' is not a file"
156 outlines
, errlines
= svntest
.main
.run_svn(1, 'blame', dir)
158 # Verify expected error message is output
159 for line
in errlines
:
160 if re
.match(expected_error
, line
):
163 raise svntest
.Failure('Failed to find %s in %s' %
164 (expected_error
, str(errlines
)))
168 # Basic test for svn blame --xml.
170 def blame_in_xml(sbox
):
171 "blame output in XML format"
177 file_path
= os
.path
.join(wc_dir
, file_name
)
178 svntest
.main
.file_append(file_path
, "Testing svn blame --xml\n")
179 expected_output
= svntest
.wc
.State(wc_dir
, {
180 'iota' : Item(verb
='Sending'),
182 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
183 None, None, None, None,
186 # Retrieve last changed date from svn info
187 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
193 if line
.find("<date>") >= 0:
201 raise svntest
.Failure
203 template
= ['<?xml version="1.0"?>\n',
206 ' path="' + file_path
+ '">\n',
208 ' line-number="1">\n',
211 '<author>jrandom</author>\n',
216 ' line-number="2">\n',
219 '<author>jrandom</author>\n',
226 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
229 for i
in range(0, len(output
)):
230 if output
[i
] != template
[i
]:
231 raise svntest
.Failure
234 # For a line changed before the requested start revision, blame should not
235 # print a revision number (as fixed in r8035) or crash (as it did with
236 # "--verbose" before being fixed in r9890).
238 def blame_on_unknown_revision(sbox
):
239 "blame lines from unknown revisions"
245 file_path
= os
.path
.join(wc_dir
, file_name
)
248 svntest
.main
.file_append(file_path
, "\nExtra line %d" % (i
))
249 expected_output
= svntest
.wc
.State(wc_dir
, {
250 'iota' : Item(verb
='Sending'),
252 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
253 None, None, None, None,
256 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
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, [],
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
277 def blame_peg_rev(sbox
):
278 "blame targets with peg-revisions"
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
, [],
299 # Check that an explicit revision overrides the default provided by
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"
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")
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, None, None,
328 svntest
.main
.run_svn(None, 'propset', 'svn:eol-style', eol
,
331 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
332 None, None, None, None,
335 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
339 # output is a list of lines, there should be 3 lines
341 raise svntest
.Failure('Expected 3 lines in blame output but got %d: \n' %
342 len(output
) + str(output
))
344 def blame_ignore_whitespace(sbox
):
345 "ignore whitespace when blaming"
351 file_path
= os
.path
.join(wc_dir
, file_name
)
353 svntest
.main
.file_write(file_path
,
357 expected_output
= svntest
.wc
.State(wc_dir
, {
358 'iota' : Item(verb
='Sending'),
360 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
361 None, None, None, None,
364 # commit only whitespace changes
365 svntest
.main
.file_write(file_path
,
369 expected_output
= svntest
.wc
.State(wc_dir
, {
370 'iota' : Item(verb
='Sending'),
372 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
373 None, None, None, None,
376 # match the blame output, as defined in the blame code:
377 # "%6ld %10s %s %s%s", rev, author ? author : " -",
378 # time_stdout , line, APR_EOL_STR
385 output
, error
= svntest
.actions
.run_and_verify_svn(None, expected_output
, [],
386 'blame', '-x', '-w', file_path
)
388 # commit some changes
389 svntest
.main
.file_write(file_path
,
394 expected_output
= svntest
.wc
.State(wc_dir
, {
395 'iota' : Item(verb
='Sending'),
397 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
398 None, None, None, None,
403 " 4 jrandom Xxxx X\n",
404 " 4 jrandom Bb b \n",
408 svntest
.actions
.run_and_verify_svn(None, expected_output
, [],
409 'blame', '-x', '-w', file_path
)
411 def blame_ignore_eolstyle(sbox
):
412 "ignore eol styles when blaming"
418 file_path
= os
.path
.join(wc_dir
, file_name
)
420 svntest
.main
.file_write(file_path
,
424 expected_output
= svntest
.wc
.State(wc_dir
, {
425 'iota' : Item(verb
='Sending'),
427 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
428 None, None, None, None,
431 # commit only eol changes
432 svntest
.main
.file_write(file_path
,
436 expected_output
= svntest
.wc
.State(wc_dir
, {
437 'iota' : Item(verb
='Sending'),
439 svntest
.actions
.run_and_verify_commit(wc_dir
, expected_output
,
440 None, None, None, None,
449 output
, error
= svntest
.actions
.run_and_verify_svn(None, expected_output
, [],
450 'blame', '-x', '--ignore-eol-style', file_path
)
453 def blame_merge_info(sbox
):
454 "test 'svn blame -g'"
456 svntest
.actions
.load_repo(sbox
, os
.path
.join(os
.path
.dirname(sys
.argv
[0]),
457 'mergetracking_data',
461 iota_path
= os
.path
.join(wc_dir
, 'trunk', 'iota')
463 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
464 'blame', '-g', iota_path
)
467 'author' : 'jrandom',
468 'text' : "This is the file 'iota'.\n",
472 'author' : 'jrandom',
473 'text' : "'A' has changed a bit, with 'upsilon', and 'xi'.\n",
477 parse_and_verify_blame(output
, expected_blame
, 1)
480 def blame_merge_out_of_range(sbox
):
481 "don't look for merged files out of range"
483 svntest
.actions
.load_repo(sbox
, os
.path
.join(os
.path
.dirname(sys
.argv
[0]),
484 'mergetracking_data',
488 upsilon_path
= os
.path
.join(wc_dir
, 'trunk', 'A', 'upsilon')
490 output
, error
= svntest
.actions
.run_and_verify_svn(None, None, [],
495 'author' : 'jrandom',
496 'text' : "This is the file 'upsilon'.\n",
501 'text' : "There is also the file 'xi'.\n",
505 parse_and_verify_blame(output
, expected_blame
, 1)
507 # test for issue #2888: 'svn blame' aborts over ra_serf
508 def blame_peg_rev_file_not_in_head(sbox
):
509 "blame target not in HEAD with peg-revisions"
513 expected_output_r1
= [
514 " 1 jrandom This is the file 'iota'.\n" ]
516 os
.chdir(sbox
.wc_dir
)
518 # Modify iota and commit it (r2).
519 svntest
.main
.file_write('iota', "This is no longer the file 'iota'.\n")
520 expected_output
= svntest
.wc
.State('.', {
521 'iota' : Item(verb
='Sending'),
523 svntest
.actions
.run_and_verify_commit('.', expected_output
, None)
525 # Delete iota so that it doesn't exist in HEAD
526 svntest
.main
.run_svn(None, 'rm', sbox
.repo_url
+ '/iota',
529 # Check that we get a blame of r1 when we specify a peg revision of r1
530 # and no explicit revision.
531 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
534 # Check that an explicit revision overrides the default provided by
536 svntest
.actions
.run_and_verify_svn(None, expected_output_r1
, [],
537 'blame', 'iota@2', '-r1')
539 def blame_file_not_in_head(sbox
):
540 "blame target not in HEAD"
542 sbox
.build(create_wc
= False)
543 notexisting_url
= sbox
.repo_url
+ '/notexisting'
545 # Check that a correct error message is printed when blaming a target that
546 # doesn't exist (in HEAD).
547 expected_err
= ".*notexisting' (is not a file.*|path not found)"
548 svntest
.actions
.run_and_verify_svn(None, [], expected_err
,
549 'blame', notexisting_url
)
552 ########################################################################
556 # list all tests here, starting with None:
562 blame_on_unknown_revision
,
565 blame_ignore_whitespace
,
566 blame_ignore_eolstyle
,
567 SkipUnless(blame_merge_info
,
568 server_has_mergeinfo
),
569 SkipUnless(blame_merge_out_of_range
,
570 server_has_mergeinfo
),
571 blame_peg_rev_file_not_in_head
,
572 blame_file_not_in_head
,
575 if __name__
== '__main__':
576 svntest
.main
.run_tests(test_list
)