3 # run_tests.py: test suite for cvs2svn
5 # Subversion is a tool for revision control.
6 # See http://subversion.tigris.org for more information.
8 # ====================================================================
9 # Copyright (c) 2000-2004 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 ######################################################################
30 # This script needs to run in tools/cvs2svn/. Make sure we're there.
31 if not (os
.path
.exists('cvs2svn.py') and os
.path
.exists('test-data')):
32 sys
.stderr
.write("error: I need to be run in 'tools/cvs2svn/' "
33 "in the Subversion tree.\n")
36 # Find the Subversion test framework.
37 sys
.path
+= [os
.path
.abspath('../../subversion/tests/clients/cmdline')]
41 Skip
= svntest
.testcase
.Skip
42 XFail
= svntest
.testcase
.XFail
43 Item
= svntest
.wc
.StateItem
45 cvs2svn
= os
.path
.abspath('cvs2svn.py')
47 # We use the installed svn and svnlook binaries, instead of using
48 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
49 # behavior -- or even existence -- of local builds shouldn't affect
50 # the cvs2svn test suite.
54 test_data_dir
= 'test-data'
58 #----------------------------------------------------------------------
60 #----------------------------------------------------------------------
63 class RunProgramException
:
66 class MissingErrorException
:
69 def run_program(program
, error_re
, *varargs
):
70 """Run PROGRAM with VARARGS, return stdout as a list of lines.
71 If there is any stderr and ERROR_RE is None, raise
72 RunProgramException, and print the stderr lines if
73 svntest.main.verbose_mode is true.
75 If ERROR_RE is not None, it is a string regular expression that must
76 match some line of stderr. If it fails to match, raise
77 MissingErrorExpection."""
78 out
, err
= svntest
.main
.run_command(program
, 1, 0, *varargs
)
82 if re
.match(error_re
, line
):
84 raise MissingErrorException
86 if svntest
.main
.verbose_mode
:
87 print '\n%s said:\n' % program
91 raise RunProgramException
95 def run_cvs2svn(error_re
, *varargs
):
96 """Run cvs2svn with VARARGS, return stdout as a list of lines.
97 If there is any stderr and ERROR_RE is None, raise
98 RunProgramException, and print the stderr lines if
99 svntest.main.verbose_mode is true.
101 If ERROR_RE is not None, it is a string regular expression that must
102 match some line of stderr. If it fails to match, raise
103 MissingErrorException."""
104 if sys
.platform
== "win32":
105 # For an unknown reason, without this special case, the cmd.exe process
106 # invoked by os.system('sort ...') in cvs2svn.py receives invalid stdio
107 # handles. Therefore, the redirection of the output to the .s-revs file
109 return run_program("python", error_re
, cvs2svn
, *varargs
)
111 return run_program(cvs2svn
, error_re
, *varargs
)
114 def run_svn(*varargs
):
115 """Run svn with VARARGS; return stdout as a list of lines.
116 If there is any stderr, raise RunProgramException, and print the
117 stderr lines if svntest.main.verbose_mode is true."""
118 return run_program(svn
, None, *varargs
)
121 def repos_to_url(path_to_svn_repos
):
122 """This does what you think it does."""
123 rpath
= os
.path
.abspath(path_to_svn_repos
)
126 return 'file://%s' % string
.replace(rpath
, os
.sep
, '/')
128 if hasattr(time
, 'strptime'):
129 def svn_strptime(timestr
):
130 return time
.strptime(timestr
, '%Y-%m-%d %H:%M:%S')
132 # This is for Python earlier than 2.3 on Windows
133 _re_rev_date
= re
.compile(r
'(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)')
134 def svn_strptime(timestr
):
135 matches
= _re_rev_date
.match(timestr
).groups()
136 return tuple(map(int, matches
)) + (0, 1, -1)
139 def __init__(self
, revision
, author
, date
):
140 self
.revision
= revision
143 # Internally, we represent the date as seconds since epoch (UTC).
144 # Since standard subversion log output shows dates in localtime
146 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
148 # and time.mktime() converts from localtime, it all works out very
150 self
.date
= time
.mktime(svn_strptime(date
[0:19]))
152 # The changed paths will be accumulated later, as log data is read.
153 # Keys here are paths such as '/trunk/foo/bar', values are letter
154 # codes such as 'M', 'A', and 'D'.
155 self
.changed_paths
= { }
157 # The msg will be accumulated later, as log data is read.
161 def parse_log(svn_repos
):
162 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS."""
165 'Make a list of lines behave like an open file handle.'
166 def __init__(self
, lines
):
169 if len(self
.lines
) > 0:
170 return self
.lines
.pop(0)
174 def absorb_changed_paths(out
, log
):
175 'Read changed paths from OUT into Log item LOG, until no more.'
177 line
= out
.readline()
178 if len(line
) == 1: return
180 op_portion
= line
[3:4]
181 path_portion
= line
[5:]
182 # # We could parse out history information, but currently we
183 # # just leave it in the path portion because that's how some
186 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
188 # path_portion = m.group(1)
189 log
.changed_paths
[path_portion
] = op_portion
191 def absorb_message_body(out
, num_lines
, log
):
192 'Read NUM_LINES of log message body from OUT into Log item LOG.'
195 line
= out
.readline()
199 log_start_re
= re
.compile('^r(?P<rev>[0-9]+) \| '
200 '(?P<author>[^\|]+) \| '
202 '\| (?P<lines>[0-9]+) (line|lines)$')
204 log_separator
= '-' * 72
208 out
= LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos
)))
212 line
= out
.readline()
216 if line
.find(log_separator
) == 0:
217 line
= out
.readline()
220 m
= log_start_re
.match(line
)
222 this_log
= Log(int(m
.group('rev')), m
.group('author'), m
.group('date'))
223 line
= out
.readline()
224 if not line
.find('Changed paths:') == 0:
225 print 'unexpected log output (missing changed paths)'
226 print "Line: '%s'" % line
228 absorb_changed_paths(out
, this_log
)
229 absorb_message_body(out
, int(m
.group('lines')), this_log
)
230 logs
[this_log
.revision
] = this_log
232 break # We've reached the end of the log output.
234 print 'unexpected log output (missing revision line)'
235 print "Line: '%s'" % line
238 print 'unexpected log output (missing log separator)'
239 print "Line: '%s'" % line
246 """Unconditionally remove PATH and its subtree, if any. PATH may be
247 non-existent, a file or symlink, or a directory."""
248 if os
.path
.isdir(path
):
250 elif os
.path
.exists(path
):
254 # List of already converted names; see the NAME argument to ensure_conversion.
256 # Keys are names, values are tuples: (svn_repos, svn_wc, log_dictionary).
257 # The log_dictionary comes from parse_log(svn_repos).
258 already_converted
= { }
260 def ensure_conversion(name
, error_re
=None, trunk_only
=None,
261 no_prune
=None, encoding
=None):
262 """Convert CVS repository NAME to Subversion, but only if it has not
263 been converted before by this invocation of this script. If it has
264 been converted before, do nothing.
266 If no error, return a tuple:
268 svn_repository_path, wc_path, log_dict
270 ...log_dict being the type of dictionary returned by parse_log().
272 If ERROR_RE is a string, it is a regular expression expected to
273 match some line of stderr printed by the conversion. If there is an
274 error and ERROR_RE is not set, then raise svntest.Failure.
276 If TRUNK_ONLY is set, then pass the --trunk-only option to cvs2svn.py
277 if converting NAME for the first time.
279 If NO_PRUNE is set, then pass the --no-prune option to cvs2svn.py
280 if converting NAME for the first time.
282 NAME is just one word. For example, 'main' would mean to convert
283 './test-data/main-cvsrepos', and after the conversion, the resulting
284 Subversion repository would be in './tmp/main-svnrepos', and a
285 checked out head working copy in './tmp/main-wc'."""
287 cvsrepos
= os
.path
.abspath(os
.path
.join(test_data_dir
, '%s-cvsrepos' % name
))
289 if not already_converted
.has_key(name
):
291 if not os
.path
.isdir(tmp_dir
):
294 saved_wd
= os
.getcwd()
298 svnrepos
= '%s-svnrepos' % name
301 # Clean up from any previous invocations of this script.
306 arg_list
= [ '--bdb-txn-nosync', '--create', '-s', svnrepos
, cvsrepos
]
309 arg_list
[:0] = [ '--no-prune' ]
312 arg_list
[:0] = [ '--trunk-only' ]
315 arg_list
[:0] = [ '--encoding=' + encoding
]
317 arg_list
[:0] = [ error_re
]
319 ret
= apply(run_cvs2svn
, arg_list
)
320 except RunProgramException
:
321 raise svntest
.Failure
322 except MissingErrorException
:
323 print "Test failed because no error matched '%s'" % error_re
324 raise svntest
.Failure
326 if not os
.path
.isdir(svnrepos
):
327 print "Repository not created: '%s'" \
328 % os
.path
.join(os
.getcwd(), svnrepos
)
329 raise svntest
.Failure
331 run_svn('co', repos_to_url(svnrepos
), wc
)
332 log_dict
= parse_log(svnrepos
)
336 # This name is done for the rest of this session.
337 already_converted
[name
] = (os
.path
.join('tmp', svnrepos
),
338 os
.path
.join('tmp', wc
),
341 return already_converted
[name
]
344 #----------------------------------------------------------------------
346 #----------------------------------------------------------------------
350 "cvs2svn with no arguments shows usage"
351 out
= run_cvs2svn(None)
352 if out
[0].find('USAGE') < 0:
353 print 'Basic cvs2svn invocation failed.'
354 raise svntest
.Failure
358 "detection of the executable flag"
359 repos
, wc
, logs
= ensure_conversion('main')
360 st
= os
.stat(os
.path
.join(wc
, 'trunk', 'single-files', 'attr-exec'))
361 if not st
[0] & stat
.S_IXUSR
:
362 raise svntest
.Failure
366 "conversion of filename with a space"
367 repos
, wc
, logs
= ensure_conversion('main')
368 if not os
.path
.exists(os
.path
.join(wc
, 'trunk', 'single-files',
370 raise svntest
.Failure
374 "two commits in quick succession"
375 repos
, wc
, logs
= ensure_conversion('main')
376 logs2
= parse_log(os
.path
.join(repos
, 'trunk', 'single-files', 'twoquick'))
378 raise svntest
.Failure
381 def prune_with_care():
382 "prune, but never too much"
383 # Robert Pluim encountered this lovely one while converting the
384 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
385 # repository (see issue #1302). Step 4 is the doozy:
387 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
388 # revision 2: adds trunk/blah/NEWS
389 # revision 3: deletes trunk/blah/cookie
390 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
391 # revision 5: does nothing
393 # After fixing cvs2svn, the sequence (correctly) looks like this:
395 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
396 # revision 2: adds trunk/blah/NEWS
397 # revision 3: deletes trunk/blah/cookie
398 # revision 4: does nothing [because trunk/blah/cookie already deleted]
399 # revision 5: deletes blah
401 # The difference is in 4 and 5. In revision 4, it's not correct to
402 # prune blah/, because NEWS is still in there, so revision 4 does
403 # nothing now. But when we delete NEWS in 5, that should bubble up
404 # and prune blah/ instead.
406 # ### Note that empty revisions like 4 are probably going to become
407 # ### at least optional, if not banished entirely from cvs2svn's
408 # ### output. Hmmm, or they may stick around, with an extra
409 # ### revision property explaining what happened. Need to think
410 # ### about that. In some sense, it's a bug in Subversion itself,
411 # ### that such revisions don't show up in 'svn log' output.
413 # In the test below, 'trunk/full-prune/first' represents
414 # cookie, and 'trunk/full-prune/second' represents NEWS.
416 repos
, wc
, logs
= ensure_conversion('main')
418 # Confirm that revision 4 removes '/trunk/full-prune/first',
419 # and that revision 6 removes '/trunk/full-prune'.
421 # Also confirm similar things about '/full-prune-reappear/...',
422 # which is similar, except that later on it reappears, restored
423 # from pruneland, because a file gets added to it.
425 # And finally, a similar thing for '/partial-prune/...', except that
426 # in its case, a permanent file on the top level prevents the
427 # pruning from going farther than the subdirectory containing first
431 for path
in ('/trunk/full-prune/first',
432 '/trunk/full-prune-reappear/sub/first',
433 '/trunk/partial-prune/sub/first'):
434 if not (logs
[rev
].changed_paths
.get(path
) == 'D'):
435 print "Revision %d failed to remove '%s'." % (rev
, path
)
436 raise svntest
.Failure
439 for path
in ('/trunk/full-prune',
440 '/trunk/full-prune-reappear',
441 '/trunk/partial-prune/sub'):
442 if not (logs
[rev
].changed_paths
.get(path
) == 'D'):
443 print "Revision %d failed to remove '%s'." % (rev
, path
)
444 raise svntest
.Failure
447 for path
in ('/trunk/full-prune-reappear',
448 '/trunk/full-prune-reappear',
449 '/trunk/full-prune-reappear/appears-later'):
450 if not (logs
[rev
].changed_paths
.get(path
) == 'A'):
451 print "Revision %d failed to create path '%s'." % (rev
, path
)
452 raise svntest
.Failure
455 def interleaved_commits():
456 "two interleaved trunk commits, different log msgs"
457 # See test-data/main-cvsrepos/proj/README.
458 repos
, wc
, logs
= ensure_conversion('main')
460 # The initial import.
462 for path
in ('/trunk/interleaved',
463 '/trunk/interleaved/1',
464 '/trunk/interleaved/2',
465 '/trunk/interleaved/3',
466 '/trunk/interleaved/4',
467 '/trunk/interleaved/5',
468 '/trunk/interleaved/a',
469 '/trunk/interleaved/b',
470 '/trunk/interleaved/c',
471 '/trunk/interleaved/d',
472 '/trunk/interleaved/e',):
473 if not (logs
[rev
].changed_paths
.get(path
) == 'A'):
474 raise svntest
.Failure
476 if logs
[rev
].msg
.find('Initial revision') != 0:
477 raise svntest
.Failure
479 # This PEP explains why we pass the 'logs' parameter to these two
480 # nested functions, instead of just inheriting it from the enclosing
481 # scope: http://www.python.org/peps/pep-0227.html
483 def check_letters(rev
, logs
):
484 'Return 1 if REV is the rev where only letters were committed, else None.'
485 for path
in ('/trunk/interleaved/a',
486 '/trunk/interleaved/b',
487 '/trunk/interleaved/c',
488 '/trunk/interleaved/d',
489 '/trunk/interleaved/e',):
490 if not (logs
[rev
].changed_paths
.get(path
) == 'M'):
492 if logs
[rev
].msg
.find('Committing letters only.') != 0:
496 def check_numbers(rev
, logs
):
497 'Return 1 if REV is the rev where only numbers were committed, else None.'
498 for path
in ('/trunk/interleaved/1',
499 '/trunk/interleaved/2',
500 '/trunk/interleaved/3',
501 '/trunk/interleaved/4',
502 '/trunk/interleaved/5',):
503 if not (logs
[rev
].changed_paths
.get(path
) == 'M'):
505 if logs
[rev
].msg
.find('Committing numbers only.') != 0:
509 # One of the commits was letters only, the other was numbers only.
510 # But they happened "simultaneously", so we don't assume anything
511 # about which commit appeared first, we just try both ways.
513 if not ((check_letters(rev
, logs
) and check_numbers(rev
+ 1, logs
))
514 or (check_numbers(rev
, logs
) and check_letters(rev
+ 1, logs
))):
515 raise svntest
.Failure
518 def simple_commits():
519 "simple trunk commits"
520 # See test-data/main-cvsrepos/proj/README.
521 repos
, wc
, logs
= ensure_conversion('main')
523 # The initial import.
525 if not logs
[rev
].changed_paths
== {
527 '/trunk/proj/default': 'A',
528 '/trunk/proj/sub1': 'A',
529 '/trunk/proj/sub1/default': 'A',
530 '/trunk/proj/sub1/subsubA': 'A',
531 '/trunk/proj/sub1/subsubA/default': 'A',
532 '/trunk/proj/sub1/subsubB': 'A',
533 '/trunk/proj/sub1/subsubB/default': 'A',
534 '/trunk/proj/sub2': 'A',
535 '/trunk/proj/sub2/default': 'A',
536 '/trunk/proj/sub2/subsubA': 'A',
537 '/trunk/proj/sub2/subsubA/default': 'A',
538 '/trunk/proj/sub3': 'A',
539 '/trunk/proj/sub3/default': 'A',
541 raise svntest
.Failure
543 if logs
[rev
].msg
.find('Initial revision') != 0:
544 raise svntest
.Failure
548 if not logs
[rev
].changed_paths
== {
549 '/trunk/proj/sub1/subsubA/default': 'M',
550 '/trunk/proj/sub3/default': 'M',
552 raise svntest
.Failure
554 if logs
[rev
].msg
.find('First commit to proj, affecting two files.') != 0:
555 raise svntest
.Failure
559 if not logs
[rev
].changed_paths
== {
560 '/trunk/proj/default': 'M',
561 '/trunk/proj/sub1/default': 'M',
562 '/trunk/proj/sub1/subsubA/default': 'M',
563 '/trunk/proj/sub1/subsubB/default': 'M',
564 '/trunk/proj/sub2/default': 'M',
565 '/trunk/proj/sub2/subsubA/default': 'M',
566 '/trunk/proj/sub3/default': 'M'
568 raise svntest
.Failure
570 if logs
[rev
].msg
.find('Second commit to proj, affecting all 7 files.') != 0:
571 raise svntest
.Failure
575 "simple tags and branches with no commits"
576 # See test-data/main-cvsrepos/proj/README.
577 repos
, wc
, logs
= ensure_conversion('main')
579 # Verify the copy source for the tags we are about to check
580 # No need to verify r16, as simple_commits did that
582 if not logs
[rev
].changed_paths
== {
583 '/branches/vendorbranch/proj (from /trunk/proj:16)': 'A',
585 raise svntest
.Failure
587 if logs
[rev
].msg
.find('Initial import.') != 0:
588 raise svntest
.Failure
590 # Tag on rev 1.1.1.1 of all files in proj
592 if not logs
[rev
].changed_paths
== {
593 '/tags/T_ALL_INITIAL_FILES (from /branches/vendorbranch:17)': 'A',
594 '/tags/T_ALL_INITIAL_FILES/single-files': 'D',
595 '/tags/T_ALL_INITIAL_FILES/partial-prune': 'D',
597 raise svntest
.Failure
599 # The same, as a branch
601 if not logs
[rev
].changed_paths
== {
602 '/branches/B_FROM_INITIALS (from /branches/vendorbranch:17)': 'A',
603 '/branches/B_FROM_INITIALS/single-files': 'D',
604 '/branches/B_FROM_INITIALS/partial-prune': 'D',
606 raise svntest
.Failure
608 # Tag on rev 1.1.1.1 of all files in proj, except one
610 if not logs
[rev
].changed_paths
== {
611 '/tags/T_ALL_INITIAL_FILES_BUT_ONE (from /branches/vendorbranch:17)': 'A',
612 '/tags/T_ALL_INITIAL_FILES_BUT_ONE/single-files': 'D',
613 '/tags/T_ALL_INITIAL_FILES_BUT_ONE/partial-prune': 'D',
614 '/tags/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB': 'D',
616 raise svntest
.Failure
618 # The same, as a branch
620 if not logs
[rev
].changed_paths
== {
621 '/branches/B_FROM_INITIALS_BUT_ONE (from /branches/vendorbranch:17)': 'A',
622 '/branches/B_FROM_INITIALS_BUT_ONE/single-files': 'D',
623 '/branches/B_FROM_INITIALS_BUT_ONE/partial-prune': 'D',
624 '/branches/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB': 'D',
626 raise svntest
.Failure
628 def simple_branch_commits():
629 "simple branch commits"
630 # See test-data/main-cvsrepos/proj/README.
631 repos
, wc
, logs
= ensure_conversion('main')
634 if not logs
[rev
].changed_paths
== {
635 '/branches/B_MIXED/proj/default': 'M',
636 '/branches/B_MIXED/proj/sub1/default': 'M',
637 '/branches/B_MIXED/proj/sub2/subsubA/default': 'M',
639 raise svntest
.Failure
641 if logs
[rev
].msg
.find('Modify three files, on branch B_MIXED.') != 0:
642 raise svntest
.Failure
645 def mixed_time_tag():
647 # See test-data/main-cvsrepos/proj/README.
648 repos
, wc
, logs
= ensure_conversion('main')
651 if not logs
[rev
].changed_paths
== {
653 '/tags/T_MIXED (from /trunk:19)': 'A',
654 '/tags/T_MIXED/partial-prune': 'D',
655 '/tags/T_MIXED/single-files': 'D',
656 '/tags/T_MIXED/proj/sub2/subsubA (from /trunk/proj/sub2/subsubA:16)': 'R',
657 '/tags/T_MIXED/proj/sub3 (from /trunk/proj/sub3:18)': 'R',
659 raise svntest
.Failure
662 def mixed_time_branch_with_added_file():
663 "mixed-time branch, and a file added to the branch"
664 # See test-data/main-cvsrepos/proj/README.
665 repos
, wc
, logs
= ensure_conversion('main')
667 # Empty revision, purely to store the log message of the dead 1.1 revision
668 # required by the RCS file format
670 if not logs
[rev
].changed_paths
== { }:
671 raise svntest
.Failure
673 if logs
[rev
].msg
.find('file branch_B_MIXED_only was initially added on '
674 'branch B_MIXED.') != 0:
675 raise svntest
.Failure
677 # A branch from the same place as T_MIXED in the previous test,
678 # plus a file added directly to the branch
680 if not logs
[rev
].changed_paths
== {
681 '/branches/B_MIXED (from /trunk:20)': 'A',
682 '/branches/B_MIXED/partial-prune': 'D',
683 '/branches/B_MIXED/single-files': 'D',
684 '/branches/B_MIXED/proj/sub2/subsubA (from /trunk/proj/sub2/subsubA:16)':
686 '/branches/B_MIXED/proj/sub3 (from /trunk/proj/sub3:18)': 'R',
687 '/branches/B_MIXED/proj/sub2/branch_B_MIXED_only': 'A',
689 raise svntest
.Failure
691 if logs
[rev
].msg
.find('Add a file on branch B_MIXED.') != 0:
692 raise svntest
.Failure
696 "a commit affecting both trunk and a branch"
697 # See test-data/main-cvsrepos/proj/README.
698 repos
, wc
, logs
= ensure_conversion('main')
701 if not logs
[rev
].changed_paths
== {
702 '/trunk/proj/sub2/default': 'M',
703 '/branches/B_MIXED/proj/sub2/branch_B_MIXED_only': 'M',
705 raise svntest
.Failure
707 if logs
[rev
].msg
.find('A single commit affecting one file on branch B_MIXED '
708 'and one on trunk.') != 0:
709 raise svntest
.Failure
712 def split_time_branch():
713 "branch some trunk files, and later branch the rest"
714 # See test-data/main-cvsrepos/proj/README.
715 repos
, wc
, logs
= ensure_conversion('main')
717 # First change on the branch, creating it
719 if not logs
[rev
].changed_paths
== {
720 '/branches/B_SPLIT (from /trunk:23)': 'A',
721 '/branches/B_SPLIT/partial-prune': 'D',
722 '/branches/B_SPLIT/single-files': 'D',
723 '/branches/B_SPLIT/proj/default': 'M',
724 '/branches/B_SPLIT/proj/sub1/default': 'M',
725 '/branches/B_SPLIT/proj/sub1/subsubA/default': 'M',
726 '/branches/B_SPLIT/proj/sub1/subsubB': 'D',
727 '/branches/B_SPLIT/proj/sub2/default': 'M',
728 '/branches/B_SPLIT/proj/sub2/subsubA/default': 'M',
730 raise svntest
.Failure
732 if logs
[rev
].msg
.find('First change on branch B_SPLIT.') != 0:
733 raise svntest
.Failure
735 # A trunk commit for the file which was not branched
737 if not logs
[rev
].changed_paths
== {
738 '/trunk/proj/sub1/subsubB/default': 'M',
740 raise svntest
.Failure
742 if logs
[rev
].msg
.find('A trunk change to sub1/subsubB/default. '
743 'This was committed about an') != 0:
744 raise svntest
.Failure
746 # Add the file not already branched to the branch, with modification:w
748 if not logs
[rev
].changed_paths
== {
749 '/branches/B_SPLIT/proj/sub1/subsubB (from /trunk/proj/sub1/subsubB:29)':
751 '/branches/B_SPLIT/proj/sub1/subsubB/default': 'M',
752 '/branches/B_SPLIT/proj/sub3/default': 'M',
754 raise svntest
.Failure
756 if logs
[rev
].msg
.find('This change affects sub3/default and '
757 'sub1/subsubB/default, on branch') != 0:
758 raise svntest
.Failure
762 "conversion of invalid symbolic names"
763 ret
, ign
, ign
= ensure_conversion('bogus-tag')
766 def overlapping_branch():
767 "ignore a file with a branch with two names"
768 repos
, wc
, logs
= ensure_conversion('overlapping-branch',
769 '.*cannot also have name \'vendorB\'')
770 nonlap_path
= '/trunk/nonoverlapping-branch'
771 lap_path
= '/trunk/overlapping-branch'
772 if not (logs
[3].changed_paths
.get('/branches/vendorA (from /trunk:2)')
774 raise svntest
.Failure
775 # We don't know what order the first two commits would be in, since
776 # they have different log messages but the same timestamps. As only
777 # one of the files would be on the vendorB branch in the regression
778 # case being tested here, we allow for either order.
779 if ((logs
[3].changed_paths
.get('/branches/vendorB (from /trunk:1)')
781 or (logs
[3].changed_paths
.get('/branches/vendorB (from /trunk:2)')
783 raise svntest
.Failure
785 raise svntest
.Failure
788 def tolerate_corruption():
789 "convert as much as can, despite a corrupt ,v file"
790 repos
, wc
, logs
= ensure_conversion('corrupt', None, 1)
791 if not ((logs
[1].changed_paths
.get('/trunk') == 'A')
792 and (logs
[1].changed_paths
.get('/trunk/good') == 'A')
793 and (len(logs
[1].changed_paths
) == 2)):
794 print "Even the valid good,v was not converted."
795 raise svntest
.Failure
798 def phoenix_branch():
799 "convert a branch file rooted in a 'dead' revision"
800 repos
, wc
, logs
= ensure_conversion('phoenix')
801 chpaths
= logs
[4].changed_paths
802 if not ((chpaths
.get('/branches/volsung_20010721 (from /trunk:3)') == 'A')
803 and (chpaths
.get('/branches/volsung_20010721/phoenix') == 'A')
804 and (len(chpaths
) == 2)):
805 print "Revision 4 not as expected."
806 raise svntest
.Failure
809 def ctrl_char_in_log():
810 "handle a control char in a log message"
811 # This was issue #1106.
812 repos
, wc
, logs
= ensure_conversion('ctrl-char-in-log')
813 if not ((logs
[1].changed_paths
.get('/trunk') == 'A')
814 and (logs
[1].changed_paths
.get('/trunk/ctrl-char-in-log') == 'A')
815 and (len(logs
[1].changed_paths
) == 2)):
816 print "Revision 1 of 'ctrl-char-in-log,v' was not converted successfully."
817 raise svntest
.Failure
818 if logs
[1].msg
.find('\x04') < 0:
819 print "Log message of 'ctrl-char-in-log,v' (rev 1) is wrong."
820 raise svntest
.Failure
824 "handle tags rooted in a redeleted revision"
825 repos
, wc
, logs
= ensure_conversion('overdead')
828 def no_trunk_prune():
829 "ensure that trunk doesn't get pruned"
830 repos
, wc
, logs
= ensure_conversion('overdead')
831 for rev
in logs
.keys():
833 for changed_path
in rev_logs
.changed_paths
.keys():
834 if changed_path
== '/trunk' \
835 and rev_logs
.changed_paths
[changed_path
] == 'D':
836 raise svntest
.Failure
840 "file deleted twice, in the root of the repository"
841 # This really tests several things: how we handle a file that's
842 # removed (state 'dead') in two successive revisions; how we
843 # handle a file in the root of the repository (there were some
844 # bugs in cvs2svn's svn path construction for top-level files); and
845 # the --no-prune option.
846 repos
, wc
, logs
= ensure_conversion('double-delete', None, 1, 1)
848 path
= '/trunk/twice-removed'
850 if not (logs
[1].changed_paths
.get(path
) == 'A'):
851 raise svntest
.Failure
853 if logs
[1].msg
.find('Initial revision') != 0:
854 raise svntest
.Failure
856 if not (logs
[2].changed_paths
.get(path
) == 'D'):
857 raise svntest
.Failure
859 if logs
[2].msg
.find('Remove this file for the first time.') != 0:
860 raise svntest
.Failure
862 if logs
[2].changed_paths
.has_key('/trunk'):
863 raise svntest
.Failure
867 "branch created from both trunk and another branch"
868 # See test-data/split-branch-cvsrepos/README.
870 # The conversion will fail if the bug is present, and
871 # ensure_conversion would raise svntest.Failure.
872 repos
, wc
, logs
= ensure_conversion('split-branch')
875 def resync_misgroups():
876 "resyncing should not misorder commit groups"
877 # See test-data/resync-misgroups-cvsrepos/README.
879 # The conversion will fail if the bug is present, and
880 # ensure_conversion would raise svntest.Failure.
881 repos
, wc
, logs
= ensure_conversion('resync-misgroups')
884 def tagged_branch_and_trunk():
885 "allow tags with mixed trunk and branch sources"
886 repos
, wc
, logs
= ensure_conversion('tagged-branch-n-trunk')
887 a_path
= os
.path
.join(wc
, 'tags', 'some-tag', 'a.txt')
888 b_path
= os
.path
.join(wc
, 'tags', 'some-tag', 'b.txt')
889 if not (os
.path
.exists(a_path
) and os
.path
.exists(b_path
)):
890 raise svntest
.Failure
891 if (open(a_path
, 'r').read().find('1.24') == -1) \
892 or (open(b_path
, 'r').read().find('1.5') == -1):
893 raise svntest
.Failure
897 "never use the rev-in-progress as a copy source"
898 # See issue #1427 and r8544.
899 repos
, wc
, logs
= ensure_conversion('enroot-race')
900 if not ((logs
[6].changed_paths
.get('/branches/mybranch (from /trunk:5)')
902 and (logs
[6].changed_paths
.get('/branches/mybranch/proj/c.txt')
904 and (logs
[6].changed_paths
.get('/trunk/proj/a.txt') == 'M')
905 and (logs
[6].changed_paths
.get('/trunk/proj/b.txt') == 'M')):
906 raise svntest
.Failure
909 def enroot_race_obo():
910 "do use the last completed rev as a copy source"
911 repos
, wc
, logs
= ensure_conversion('enroot-race-obo')
912 if not ((len(logs
) == 2) and
913 (logs
[2].changed_paths
.get('/branches/BRANCH (from /trunk:1)') == 'A')):
914 raise svntest
.Failure
917 def branch_delete_first():
918 "correctly handle deletion as initial branch action"
919 # See test-data/branch-delete-first-cvsrepos/README.
921 # The conversion will fail if the bug is present, and
922 # ensure_conversion would raise svntest.Failure.
923 repos
, wc
, logs
= ensure_conversion('branch-delete-first')
925 # 'file' was deleted from branch-1 and branch-2, but not branch-3
926 if os
.path
.exists(os
.path
.join(wc
, 'branches', 'branch-1', 'file')):
927 raise svntest
.Failure
928 if os
.path
.exists(os
.path
.join(wc
, 'branches', 'branch-2', 'file')):
929 raise svntest
.Failure
930 if not os
.path
.exists(os
.path
.join(wc
, 'branches', 'branch-3', 'file')):
931 raise svntest
.Failure
934 def nonascii_filenames():
935 "non ascii files converted incorrectly"
938 # on a en_US.iso-8859-1 machine this test fails with
939 # svn: Can't recode ...
941 # as described in the issue
943 # on a en_US.UTF-8 machine this test fails with
944 # svn: Malformed XML ...
946 # which means at least it fails. Unfortunately it won't fail
947 # with the same error...
949 # mangle current locale settings so we know we're not running
950 # a UTF-8 locale (which does not exhibit this problem)
951 current_locale
= locale
.getlocale()
952 new_locale
= 'en_US.ISO8859-1'
953 locale_changed
= None
956 # change locale to non-UTF-8 locale to generate latin1 names
957 locale
.setlocale(locale
.LC_ALL
, # this might be too broad?
964 testdata_path
= os
.path
.abspath('test-data')
965 srcrepos_path
= os
.path
.join(testdata_path
,'main-cvsrepos')
966 dstrepos_path
= os
.path
.join(testdata_path
,'non-ascii-cvsrepos')
967 if not os
.path
.exists(dstrepos_path
):
968 # create repos from existing main repos
969 shutil
.copytree(srcrepos_path
, dstrepos_path
)
970 base_path
= os
.path
.join(dstrepos_path
, 'single-files')
971 shutil
.copyfile(os
.path
.join(base_path
, 'twoquick,v'),
972 os
.path
.join(base_path
, 'two\366uick,v'))
973 new_path
= os
.path
.join(dstrepos_path
, 'single\366files')
974 os
.rename(base_path
, new_path
)
976 # if ensure_conversion can generate a
977 repos
, wc
, logs
= ensure_conversion('non-ascii', encoding
='latin1')
980 locale
.setlocale(locale
.LC_ALL
, current_locale
)
981 shutil
.rmtree(dstrepos_path
)
984 def vendor_branch_sameness():
985 "avoid spurious changes for initial revs "
986 repos
, wc
, logs
= ensure_conversion('vendor-branch-sameness')
988 # There are four files in the repository:
990 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
991 # the same contents, the file's default branch is 1.1.1,
992 # and both revisions are in state 'Exp'.
994 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
995 # 1.1 (the addition of a line of text).
997 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
999 # d.txt: This file was created by 'cvs add' instead of import, so
1000 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1001 # The timestamp on the add is exactly the same as for the
1002 # imports of the other files.
1004 # (Log messages for the same revisions are the same in all files.)
1006 # What we expect to see is everyone added in r1, then trunk/proj
1007 # copied in r2. In the copy, only a.txt should be left untouched;
1008 # b.txt should be 'M'odified, and (for different reasons) c.txt and
1009 # d.txt should be 'D'eleted.
1011 if logs
[1].msg
.find('Initial revision') != 0:
1012 raise svntest
.Failure
1014 if not logs
[1].changed_paths
== {
1016 '/trunk/proj' : 'A',
1017 '/trunk/proj/a.txt' : 'A',
1018 '/trunk/proj/b.txt' : 'A',
1019 '/trunk/proj/c.txt' : 'A',
1020 '/trunk/proj/d.txt' : 'A',
1022 raise svntest
.Failure
1024 if logs
[2].msg
.find('First vendor branch revision.') != 0:
1025 raise svntest
.Failure
1027 if not logs
[2].changed_paths
== {
1029 '/branches/vbranchA (from /trunk:1)' : 'A',
1030 '/branches/vbranchA/proj/b.txt' : 'M',
1031 '/branches/vbranchA/proj/c.txt' : 'D',
1032 '/branches/vbranchA/proj/d.txt' : 'D',
1034 raise svntest
.Failure
1037 def default_branches():
1038 "handle default branches correctly "
1039 repos
, wc
, logs
= ensure_conversion('default-branches')
1041 # There are seven files in the repository:
1044 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1045 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1046 # committed (thus losing the default branch "1.1.1"), then
1047 # 1.1.1.4 was imported. All vendor import release tags are
1051 # Like a.txt, but without rev 1.2.
1054 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1057 # Same as the previous two, but 1.1.1 branch is unlabeled.
1060 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1062 # deleted-on-vendor-branch.txt,v:
1063 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1065 # added-then-imported.txt,v:
1066 # Added with 'cvs add' to create 1.1, then imported with
1067 # completely different contents to create 1.1.1.1, therefore
1068 # never had a default branch.
1071 if logs
[14].msg
.find("This commit was manufactured by cvs2svn "
1072 "to create tag 'vtag-4'.") != 0:
1073 raise svntest
.Failure
1075 if not logs
[14].changed_paths
== {
1076 '/tags/vtag-4 (from /branches/vbranchA:9)' : 'A',
1077 '/tags/vtag-4/proj/d.txt '
1078 '(from /branches/unlabeled-1.1.1/proj/d.txt:9)' : 'A',
1080 raise svntest
.Failure
1082 if logs
[13].msg
.find("This commit was manufactured by cvs2svn "
1083 "to create tag 'vtag-1'.") != 0:
1084 raise svntest
.Failure
1086 if not logs
[13].changed_paths
== {
1087 '/tags/vtag-1 (from /branches/vbranchA:2)' : 'A',
1088 '/tags/vtag-1/proj/d.txt '
1089 '(from /branches/unlabeled-1.1.1/proj/d.txt:2)' : 'A',
1091 raise svntest
.Failure
1093 if logs
[12].msg
.find("This commit was manufactured by cvs2svn "
1094 "to create tag 'vtag-2'.") != 0:
1095 raise svntest
.Failure
1096 if not logs
[12].changed_paths
== {
1097 '/tags/vtag-2 (from /branches/vbranchA:3)' : 'A',
1098 '/tags/vtag-2/proj/d.txt '
1099 '(from /branches/unlabeled-1.1.1/proj/d.txt:3)' : 'A',
1101 raise svntest
.Failure
1103 if logs
[11].msg
.find("This commit was manufactured by cvs2svn "
1104 "to create tag 'vtag-3'.") != 0:
1105 raise svntest
.Failure
1106 if not logs
[11].changed_paths
== {
1108 '/tags/vtag-3 (from /branches/vbranchA:5)' : 'A',
1109 '/tags/vtag-3/proj/d.txt '
1110 '(from /branches/unlabeled-1.1.1/proj/d.txt:5)' : 'A',
1111 '/tags/vtag-3/proj/e.txt '
1112 '(from /branches/unlabeled-1.1.1/proj/e.txt:5)' : 'A',
1114 raise svntest
.Failure
1116 if logs
[10].msg
.find("This commit was generated by cvs2svn "
1117 "to compensate for changes in r9,") != 0:
1118 raise svntest
.Failure
1119 if not logs
[10].changed_paths
== {
1120 '/trunk/proj/b.txt (from /branches/vbranchA/proj/b.txt:9)' : 'R',
1121 '/trunk/proj/c.txt (from /branches/vbranchA/proj/c.txt:9)' : 'R',
1122 '/trunk/proj/d.txt (from /branches/unlabeled-1.1.1/proj/d.txt:9)' : 'R',
1123 '/trunk/proj/deleted-on-vendor-branch.txt '
1124 '(from /branches/vbranchA/proj/deleted-on-vendor-branch.txt:9)' : 'A',
1125 '/trunk/proj/e.txt (from /branches/unlabeled-1.1.1/proj/e.txt:9)' : 'R',
1127 raise svntest
.Failure
1129 if logs
[9].msg
.find("Import (vbranchA, vtag-4).") != 0:
1130 raise svntest
.Failure
1132 if not logs
[9].changed_paths
== {
1133 '/branches/unlabeled-1.1.1/proj/d.txt' : 'M',
1134 '/branches/unlabeled-1.1.1/proj/e.txt' : 'M',
1135 '/branches/vbranchA/proj/a.txt' : 'M',
1136 '/branches/vbranchA/proj/added-then-imported.txt '
1137 '(from /trunk/proj/added-then-imported.txt:7)' : 'A',
1138 '/branches/vbranchA/proj/b.txt' : 'M',
1139 '/branches/vbranchA/proj/c.txt' : 'M',
1140 '/branches/vbranchA/proj/deleted-on-vendor-branch.txt' : 'A',
1142 raise svntest
.Failure
1144 if logs
[8].msg
.find("First regular commit, to a.txt, on vtag-3.") != 0:
1145 raise svntest
.Failure
1147 if not logs
[8].changed_paths
== {
1148 '/trunk/proj/a.txt' : 'M',
1150 raise svntest
.Failure
1152 if logs
[7].msg
.find("Add a file to the working copy.") != 0:
1153 raise svntest
.Failure
1155 if not logs
[7].changed_paths
== {
1156 '/trunk/proj/added-then-imported.txt' : 'A',
1158 raise svntest
.Failure
1160 if logs
[6].msg
.find("This commit was generated by cvs2svn "
1161 "to compensate for changes in r5,") != 0:
1162 raise svntest
.Failure
1163 if not logs
[6].changed_paths
== {
1164 '/trunk/proj/a.txt (from /branches/vbranchA/proj/a.txt:5)' : 'R',
1165 '/trunk/proj/b.txt (from /branches/vbranchA/proj/b.txt:5)' : 'R',
1166 '/trunk/proj/c.txt (from /branches/vbranchA/proj/c.txt:5)' : 'R',
1167 '/trunk/proj/d.txt (from /branches/unlabeled-1.1.1/proj/d.txt:5)' : 'R',
1168 '/trunk/proj/deleted-on-vendor-branch.txt' : 'D',
1169 '/trunk/proj/e.txt (from /branches/unlabeled-1.1.1/proj/e.txt:5)' : 'R',
1171 raise svntest
.Failure
1173 if logs
[5].msg
.find("Import (vbranchA, vtag-3).") != 0:
1174 raise svntest
.Failure
1176 if not logs
[5].changed_paths
== {
1177 '/branches/unlabeled-1.1.1/proj/d.txt' : 'M',
1178 '/branches/unlabeled-1.1.1/proj/e.txt' : 'M',
1179 '/branches/vbranchA/proj/a.txt' : 'M',
1180 '/branches/vbranchA/proj/b.txt' : 'M',
1181 '/branches/vbranchA/proj/c.txt' : 'M',
1182 '/branches/vbranchA/proj/deleted-on-vendor-branch.txt' : 'D',
1184 raise svntest
.Failure
1186 if logs
[4].msg
.find("This commit was generated by cvs2svn "
1187 "to compensate for changes in r3,") != 0:
1188 raise svntest
.Failure
1189 if not logs
[4].changed_paths
== {
1190 '/trunk/proj/a.txt (from /branches/vbranchA/proj/a.txt:3)' : 'R',
1191 '/trunk/proj/b.txt (from /branches/vbranchA/proj/b.txt:3)' : 'R',
1192 '/trunk/proj/c.txt (from /branches/vbranchA/proj/c.txt:3)' : 'R',
1193 '/trunk/proj/d.txt (from /branches/unlabeled-1.1.1/proj/d.txt:3)' : 'R',
1194 '/trunk/proj/deleted-on-vendor-branch.txt '
1195 '(from /branches/vbranchA/proj/deleted-on-vendor-branch.txt:3)' : 'R',
1196 '/trunk/proj/e.txt (from /branches/unlabeled-1.1.1/proj/e.txt:3)' : 'R',
1198 raise svntest
.Failure
1200 if logs
[3].msg
.find("Import (vbranchA, vtag-2).") != 0:
1201 raise svntest
.Failure
1203 if not logs
[3].changed_paths
== {
1204 '/branches/unlabeled-1.1.1/proj/d.txt' : 'M',
1205 '/branches/unlabeled-1.1.1/proj/e.txt' : 'M',
1206 '/branches/vbranchA/proj/a.txt' : 'M',
1207 '/branches/vbranchA/proj/b.txt' : 'M',
1208 '/branches/vbranchA/proj/c.txt' : 'M',
1209 '/branches/vbranchA/proj/deleted-on-vendor-branch.txt' : 'M',
1211 raise svntest
.Failure
1213 if logs
[2].msg
.find("Import (vbranchA, vtag-1).") != 0:
1214 raise svntest
.Failure
1216 if not logs
[2].changed_paths
== {
1218 '/branches/unlabeled-1.1.1 (from /trunk:1)' : 'A',
1219 '/branches/unlabeled-1.1.1/proj/a.txt' : 'D',
1220 '/branches/unlabeled-1.1.1/proj/b.txt' : 'D',
1221 '/branches/unlabeled-1.1.1/proj/c.txt' : 'D',
1222 '/branches/unlabeled-1.1.1/proj/deleted-on-vendor-branch.txt' : 'D',
1223 '/branches/vbranchA (from /trunk:1)' : 'A',
1224 '/branches/vbranchA/proj/d.txt' : 'D',
1225 '/branches/vbranchA/proj/e.txt' : 'D',
1227 raise svntest
.Failure
1229 if logs
[1].msg
.find("Initial revision") != 0:
1230 raise svntest
.Failure
1232 if not logs
[1].changed_paths
== {
1234 '/trunk/proj' : 'A',
1235 '/trunk/proj/a.txt' : 'A',
1236 '/trunk/proj/b.txt' : 'A',
1237 '/trunk/proj/c.txt' : 'A',
1238 '/trunk/proj/d.txt' : 'A',
1239 '/trunk/proj/deleted-on-vendor-branch.txt' : 'A',
1240 '/trunk/proj/e.txt' : 'A',
1242 raise svntest
.Failure
1245 def compose_tag_three_sources():
1246 "compose a tag from three sources"
1247 repos
, wc
, logs
= ensure_conversion('compose-tag-three-sources')
1249 if not logs
[1].changed_paths
== { '/trunk': 'A', '/trunk/a': 'A',
1250 '/trunk/b': 'A', '/trunk/c': 'A' }: raise svntest
.Failure
1252 if not logs
[2].changed_paths
== { '/branches': 'A',
1253 '/branches/b1 (from /trunk:1)': 'A', '/branches/b1/a': 'M',
1254 '/branches/b1/b': 'M', '/branches/b1/c': 'M',
1255 }: raise svntest
.Failure
1257 if not logs
[3].changed_paths
== {
1258 '/branches/b2 (from /trunk:1)': 'A', '/branches/b2/a': 'M',
1259 '/branches/b2/b': 'M', '/branches/b2/c': 'M',
1260 }: raise svntest
.Failure
1262 if not logs
[4].changed_paths
== { '/tags': 'A',
1263 '/tags/T (from /trunk:1)': 'A',
1264 '/tags/T/b (from /branches/b1/b:2)': 'R',
1265 '/tags/T/c (from /branches/b2/c:3)': 'R',
1266 }: raise svntest
.Failure
1269 #----------------------------------------------------------------------
1271 ########################################################################
1274 # list all tests here, starting with None:
1281 interleaved_commits
,
1284 simple_branch_commits
,
1286 mixed_time_branch_with_added_file
,
1291 tolerate_corruption
,
1299 tagged_branch_and_trunk
,
1302 branch_delete_first
,
1304 vendor_branch_sameness
,
1306 XFail(compose_tag_three_sources
),
1309 if __name__
== '__main__':
1310 svntest
.main
.run_tests(test_list
)