In the command-line client, forbid
[svn.git] / subversion / tests / cmdline / blame_tests.py
blob6bebe80813b2a1a114dc3e810d6a5d68a61a7a5f
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 for (num, (item, expected_item)) in enumerate(zip(results, expected_blame)):
72 for key in keys:
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 ######################################################################
79 # Tests
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"
88 sbox.build()
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',
94 '-m', '', file_path)
96 svntest.main.run_svn(None, 'blame', file_path)
99 def blame_binary(sbox):
100 "annotate a binary file"
101 sbox.build()
102 wc_dir = sbox.wc_dir
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',
108 '-m', '', iota)
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',
115 '-m', '', iota)
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',
122 '-m', '', iota)
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
145 import re
147 # Setup
148 sbox.build()
149 wc_dir = sbox.wc_dir
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):
161 break
162 else:
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"
173 sbox.build()
174 wc_dir = sbox.wc_dir
176 file_name = "iota"
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,
184 None, None, wc_dir)
186 # Retrieve last changed date from svn info
187 output, error = svntest.actions.run_and_verify_svn(None, None, [],
188 'log', file_path,
189 '--xml', '-r1:2')
190 date1 = None
191 date2 = None
192 for line in output:
193 if line.find("<date>") >= 0:
194 if date1 is None:
195 date1 = line
196 continue
197 elif date2 is None:
198 date2 = line
199 break
200 else:
201 raise svntest.Failure
203 template = ['<?xml version="1.0"?>\n',
204 '<blame>\n',
205 '<target\n',
206 ' path="' + file_path + '">\n',
207 '<entry\n',
208 ' line-number="1">\n',
209 '<commit\n',
210 ' revision="1">\n',
211 '<author>jrandom</author>\n',
212 '%s' % date1,
213 '</commit>\n',
214 '</entry>\n',
215 '<entry\n',
216 ' line-number="2">\n',
217 '<commit\n',
218 ' revision="2">\n',
219 '<author>jrandom</author>\n',
220 '%s' % date2,
221 '</commit>\n',
222 '</entry>\n',
223 '</target>\n',
224 '</blame>\n']
226 output, error = svntest.actions.run_and_verify_svn(None, None, [],
227 'blame', file_path,
228 '--xml')
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"
241 sbox.build()
242 wc_dir = sbox.wc_dir
244 file_name = "iota"
245 file_path = os.path.join(wc_dir, file_name)
247 for i in range(1,3):
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,
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, None, None,
326 None, None, wc_dir)
328 svntest.main.run_svn(None, 'propset', 'svn:eol-style', eol,
329 file_path)
331 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
332 None, None, None, None,
333 None, None, wc_dir)
335 output, error = svntest.actions.run_and_verify_svn(None, None, [],
336 'blame', file_path,
337 '-r1:HEAD')
339 # output is a list of lines, there should be 3 lines
340 if len(output) != 3:
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"
347 sbox.build()
348 wc_dir = sbox.wc_dir
350 file_name = "iota"
351 file_path = os.path.join(wc_dir, file_name)
353 svntest.main.file_write(file_path,
354 "Aa\n"
355 "Bb\n"
356 "Cc\n")
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,
362 None, None, wc_dir)
364 # commit only whitespace changes
365 svntest.main.file_write(file_path,
366 " A a \n"
367 " B b \n"
368 " C c \n")
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,
374 None, None, wc_dir)
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
379 expected_output = [
380 " 2 jrandom A a \n",
381 " 2 jrandom B b \n",
382 " 2 jrandom C c \n",
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,
390 " A a \n"
391 "Xxxx X\n"
392 " Bb b \n"
393 " C c \n")
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,
399 None, None, wc_dir)
401 expected_output = [
402 " 2 jrandom A a \n",
403 " 4 jrandom Xxxx X\n",
404 " 4 jrandom Bb b \n",
405 " 2 jrandom C c \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"
414 sbox.build()
415 wc_dir = sbox.wc_dir
417 file_name = "iota"
418 file_path = os.path.join(wc_dir, file_name)
420 svntest.main.file_write(file_path,
421 "Aa\n"
422 "Bb\n"
423 "Cc\n")
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,
429 None, None, wc_dir)
431 # commit only eol changes
432 svntest.main.file_write(file_path,
433 "Aa\r"
434 "Bb\r"
435 "Cc")
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,
441 None, None, wc_dir)
443 expected_output = [
444 " 2 jrandom Aa\n",
445 " 2 jrandom Bb\n",
446 " 3 jrandom Cc\n",
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',
458 'basic-merge.dump'))
460 wc_dir = sbox.wc_dir
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)
465 expected_blame = [
466 { 'revision' : 2,
467 'author' : 'jrandom',
468 'text' : "This is the file 'iota'.\n",
469 'merged' : 0,
471 { 'revision' : 11,
472 'author' : 'jrandom',
473 'text' : "'A' has changed a bit, with 'upsilon', and 'xi'.\n",
474 'merged' : 1,
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',
485 'basic-merge.dump'))
487 wc_dir = sbox.wc_dir
488 upsilon_path = os.path.join(wc_dir, 'trunk', 'A', 'upsilon')
490 output, error = svntest.actions.run_and_verify_svn(None, None, [],
491 'blame', '-g',
492 upsilon_path)
493 expected_blame = [
494 { 'revision' : 4,
495 'author' : 'jrandom',
496 'text' : "This is the file 'upsilon'.\n",
497 'merged' : 1,
499 { 'revision' : 11,
500 'author': 'jrandom',
501 'text' : "There is also the file 'xi'.\n",
502 'merged' : 1,
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"
511 sbox.build()
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',
527 '-m', 'log message')
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, [],
532 'blame', 'iota@1')
534 # Check that an explicit revision overrides the default provided by
535 # the peg revision.
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 ########################################################################
553 # Run the tests
556 # list all tests here, starting with None:
557 test_list = [ None,
558 blame_space_in_name,
559 blame_binary,
560 blame_directory,
561 blame_in_xml,
562 blame_on_unknown_revision,
563 blame_peg_rev,
564 blame_eol_styles,
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)
577 # NOTREACHED
580 ### End of file.