Fix compiler warning due to missing function prototype.
[svn.git] / subversion / tests / cmdline / blame_tests.py
blob019d209a611f68554ede6224af039287e1a08a0b
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 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
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 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):
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 exit_code, output, error = svntest.actions.run_and_verify_svn(
189 None, None, [],
190 'log', file_path, '--xml', '-r1:2')
192 date1 = None
193 date2 = None
194 for line in output:
195 if line.find("<date>") >= 0:
196 if date1 is None:
197 date1 = line
198 continue
199 elif date2 is None:
200 date2 = line
201 break
202 else:
203 raise svntest.Failure
205 template = ['<?xml version="1.0"?>\n',
206 '<blame>\n',
207 '<target\n',
208 ' path="' + file_path + '">\n',
209 '<entry\n',
210 ' line-number="1">\n',
211 '<commit\n',
212 ' revision="1">\n',
213 '<author>jrandom</author>\n',
214 '%s' % date1,
215 '</commit>\n',
216 '</entry>\n',
217 '<entry\n',
218 ' line-number="2">\n',
219 '<commit\n',
220 ' revision="2">\n',
221 '<author>jrandom</author>\n',
222 '%s' % date2,
223 '</commit>\n',
224 '</entry>\n',
225 '</target>\n',
226 '</blame>\n']
228 exit_code, output, error = svntest.actions.run_and_verify_svn(
229 None, None, [],
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"
244 sbox.build()
245 wc_dir = sbox.wc_dir
247 file_name = "iota"
248 file_path = os.path.join(wc_dir, file_name)
250 for i in range(1,3):
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,
256 None, None, wc_dir)
258 exit_code, output, error = svntest.actions.run_and_verify_svn(
259 None, None, [],
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(
266 None, None, [],
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
276 # specified.
278 def blame_peg_rev(sbox):
279 "blame targets with peg-revisions"
281 sbox.build()
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, [],
298 'blame', 'iota@1')
300 # Check that an explicit revision overrides the default provided by
301 # the peg revision.
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"
308 sbox.build()
309 wc_dir = sbox.wc_dir
311 # CR
312 file_name = "iota"
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")
323 for i in range(1,3):
324 svntest.main.file_append(file_path, "Extra line %d" % (i) + "\n")
325 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
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, wc_dir)
334 exit_code, output, error = svntest.actions.run_and_verify_svn(
335 None, None, [],
336 'blame', file_path, '-r1:HEAD')
338 # output is a list of lines, there should be 3 lines
339 if len(output) != 3:
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"
346 sbox.build()
347 wc_dir = sbox.wc_dir
349 file_name = "iota"
350 file_path = os.path.join(wc_dir, file_name)
352 svntest.main.file_write(file_path,
353 "Aa\n"
354 "Bb\n"
355 "Cc\n")
356 expected_output = svntest.wc.State(wc_dir, {
357 'iota' : Item(verb='Sending'),
359 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
360 None, None, wc_dir)
362 # commit only whitespace changes
363 svntest.main.file_write(file_path,
364 " A a \n"
365 " B b \n"
366 " C c \n")
367 expected_output = svntest.wc.State(wc_dir, {
368 'iota' : Item(verb='Sending'),
370 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
371 None, None, wc_dir)
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
376 expected_output = [
377 " 2 jrandom A a \n",
378 " 2 jrandom B b \n",
379 " 2 jrandom C c \n",
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,
388 " A a \n"
389 "Xxxx X\n"
390 " Bb b \n"
391 " C c \n")
392 expected_output = svntest.wc.State(wc_dir, {
393 'iota' : Item(verb='Sending'),
395 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
396 None, None, wc_dir)
398 expected_output = [
399 " 2 jrandom A a \n",
400 " 4 jrandom Xxxx X\n",
401 " 4 jrandom Bb b \n",
402 " 2 jrandom C c \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"
411 sbox.build()
412 wc_dir = sbox.wc_dir
414 file_name = "iota"
415 file_path = os.path.join(wc_dir, file_name)
417 svntest.main.file_write(file_path,
418 "Aa\n"
419 "Bb\n"
420 "Cc\n")
421 expected_output = svntest.wc.State(wc_dir, {
422 'iota' : Item(verb='Sending'),
424 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
425 None, None, wc_dir)
427 # commit only eol changes
428 svntest.main.file_write(file_path,
429 "Aa\r"
430 "Bb\r"
431 "Cc")
432 expected_output = svntest.wc.State(wc_dir, {
433 'iota' : Item(verb='Sending'),
435 svntest.actions.run_and_verify_commit(wc_dir, expected_output,
436 None, None, wc_dir)
438 expected_output = [
439 " 2 jrandom Aa\n",
440 " 2 jrandom Bb\n",
441 " 3 jrandom Cc\n",
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)
455 wc_dir = sbox.wc_dir
456 iota_path = os.path.join(wc_dir, 'trunk', 'iota')
458 exit_code, output, error = svntest.actions.run_and_verify_svn(
459 None, None, [],
460 'blame', '-g', iota_path)
462 expected_blame = [
463 { 'revision' : 2,
464 'author' : 'jrandom',
465 'text' : "This is the file 'iota'.\n",
466 'merged' : 0,
468 { 'revision' : 11,
469 'author' : 'jrandom',
470 'text' : "'A' has changed a bit, with 'upsilon', and 'xi'.\n",
471 'merged' : 1,
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)
483 wc_dir = sbox.wc_dir
484 upsilon_path = os.path.join(wc_dir, 'trunk', 'A', 'upsilon')
486 exit_code, output, error = svntest.actions.run_and_verify_svn(
487 None, None, [],
488 'blame', '-g', upsilon_path)
490 expected_blame = [
491 { 'revision' : 4,
492 'author' : 'jrandom',
493 'text' : "This is the file 'upsilon'.\n",
494 'merged' : 0,
496 { 'revision' : 11,
497 'author': 'jrandom',
498 'text' : "There is also the file 'xi'.\n",
499 'merged' : 1,
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"
508 sbox.build()
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',
524 '-m', 'log message')
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, [],
529 'blame', 'iota@1')
531 # Check that an explicit revision overrides the default provided by
532 # the peg revision.
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 ########################################################################
550 # Run the tests
553 # list all tests here, starting with None:
554 test_list = [ None,
555 blame_space_in_name,
556 blame_binary,
557 blame_directory,
558 blame_in_xml,
559 blame_on_unknown_revision,
560 blame_peg_rev,
561 blame_eol_styles,
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)
572 # NOTREACHED
575 ### End of file.