* cvs2svn.1: Use .IP instead of .TP, for brevity.
[cvs2svn.git] / run-tests.py
blobcce1654759fedb60fd4c37397fa58ba6604c5893
1 #!/usr/bin/env python
3 # run_tests.py: test suite for cvs2svn
5 # Usage: run_tests.py [-v | --verbose] [list | <num>]
7 # Options:
8 # -v, --verbose
9 # enable verbose output
11 # Arguments (at most one argument is allowed):
12 # list
13 # If the word "list" is passed as an argument, the list of
14 # available tests is printed (but no tests are run).
16 # <num>
17 # If a number is passed as an argument, then only the test
18 # with that number is run.
20 # If no argument is specified, then all tests are run.
22 # Subversion is a tool for revision control.
23 # See http://subversion.tigris.org for more information.
25 # ====================================================================
26 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
28 # This software is licensed as described in the file COPYING, which
29 # you should have received as part of this distribution. The terms
30 # are also available at http://subversion.tigris.org/license-1.html.
31 # If newer versions of this license are posted there, you may use a
32 # newer version instead, at your option.
34 ######################################################################
36 # General modules
37 import sys
38 import shutil
39 import stat
40 import re
41 import os
42 import time
43 import os.path
44 import locale
45 import textwrap
46 from difflib import Differ
48 # Make sure that a supported version of Python is being used:
49 if not (0x02040000 <= sys.hexversion < 0x03000000):
50 sys.stderr.write(
51 'error: Python 2, version 2.4 or higher required.\n'
53 sys.exit(1)
55 # This script needs to run in the correct directory. Make sure we're there.
56 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
57 sys.stderr.write("error: I need to be run in the directory containing "
58 "'cvs2svn' and 'test-data'.\n")
59 sys.exit(1)
61 # Load the Subversion test framework.
62 import svntest
63 from svntest import Failure
64 from svntest.main import run_command
65 from svntest.main import run_tests
66 from svntest.main import safe_rmtree
67 from svntest.testcase import TestCase
68 from svntest.testcase import Skip
69 from svntest.testcase import XFail
70 from svntest.tree import build_tree_from_wc
71 from svntest.tree import get_child
73 cvs2svn = os.path.abspath('cvs2svn')
74 cvs2git = os.path.abspath('cvs2git')
76 # We use the installed svn and svnlook binaries, instead of using
77 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
78 # behavior -- or even existence -- of local builds shouldn't affect
79 # the cvs2svn test suite.
80 svn = 'svn'
81 svnlook = 'svnlook'
83 test_data_dir = 'test-data'
84 tmp_dir = 'cvs2svn-tmp'
87 #----------------------------------------------------------------------
88 # Helpers.
89 #----------------------------------------------------------------------
92 # The value to expect for svn:keywords if it is set:
93 KEYWORDS = 'Author Date Id Revision'
96 class RunProgramException(Failure):
97 pass
100 class MissingErrorException(Failure):
101 def __init__(self, error_re):
102 Failure.__init__(
103 self, "Test failed because no error matched '%s'" % (error_re,)
107 def run_program(program, error_re, *varargs):
108 """Run PROGRAM with VARARGS, return stdout as a list of lines.
110 If there is any stderr and ERROR_RE is None, raise
111 RunProgramException, and print the stderr lines if
112 svntest.main.verbose_mode is true.
114 If ERROR_RE is not None, it is a string regular expression that must
115 match some line of stderr. If it fails to match, raise
116 MissingErrorExpection."""
118 # FIXME: exit_code is currently ignored.
119 exit_code, out, err = run_command(program, 1, 0, *varargs)
121 if error_re:
122 # Specified error expected on stderr.
123 if not err:
124 raise MissingErrorException(error_re)
125 else:
126 for line in err:
127 if re.match(error_re, line):
128 return out
129 raise MissingErrorException(error_re)
130 else:
131 # No stderr allowed.
132 if err:
133 if svntest.main.verbose_mode:
134 print '\n%s said:\n' % program
135 for line in err:
136 print ' ' + line,
137 print
138 raise RunProgramException()
140 return out
143 def run_cvs2svn(error_re, *varargs):
144 """Run cvs2svn with VARARGS, return stdout as a list of lines.
146 If there is any stderr and ERROR_RE is None, raise
147 RunProgramException, and print the stderr lines if
148 svntest.main.verbose_mode is true.
150 If ERROR_RE is not None, it is a string regular expression that must
151 match some line of stderr. If it fails to match, raise
152 MissingErrorException."""
154 # Use the same python that is running this script
155 return run_program(sys.executable, error_re, cvs2svn, *varargs)
156 # On Windows, for an unknown reason, the cmd.exe process invoked by
157 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
158 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
159 # this. Therefore, the redirection of the output to the .s-revs file fails.
160 # We no longer use the problematic invocation on any system, but this
161 # comment remains to warn about this problem.
164 def run_cvs2git(error_re, *varargs):
165 """Run cvs2git with VARARGS, returning stdout as a list of lines.
167 If there is any stderr and ERROR_RE is None, raise
168 RunProgramException, and print the stderr lines if
169 svntest.main.verbose_mode is true.
171 If ERROR_RE is not None, it is a string regular expression that must
172 match some line of stderr. If it fails to match, raise
173 MissingErrorException."""
175 # Use the same python that is running this script
176 return run_program(sys.executable, error_re, cvs2git, *varargs)
177 # On Windows, for an unknown reason, the cmd.exe process invoked by
178 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
179 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
180 # this. Therefore, the redirection of the output to the .s-revs file fails.
181 # We no longer use the problematic invocation on any system, but this
182 # comment remains to warn about this problem.
185 def run_svn(*varargs):
186 """Run svn with VARARGS; return stdout as a list of lines.
187 If there is any stderr, raise RunProgramException, and print the
188 stderr lines if svntest.main.verbose_mode is true."""
189 return run_program(svn, None, *varargs)
192 def repos_to_url(path_to_svn_repos):
193 """This does what you think it does."""
194 rpath = os.path.abspath(path_to_svn_repos)
195 if rpath[0] != '/':
196 rpath = '/' + rpath
197 return 'file://%s' % rpath.replace(os.sep, '/')
200 def svn_strptime(timestr):
201 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
204 class Log:
205 def __init__(self, revision, author, date, symbols):
206 self.revision = revision
207 self.author = author
209 # Internally, we represent the date as seconds since epoch (UTC).
210 # Since standard subversion log output shows dates in localtime
212 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
214 # and time.mktime() converts from localtime, it all works out very
215 # happily.
216 self.date = time.mktime(svn_strptime(date[0:19]))
218 # The following symbols are used for string interpolation when
219 # checking paths:
220 self.symbols = symbols
222 # The changed paths will be accumulated later, as log data is read.
223 # Keys here are paths such as '/trunk/foo/bar', values are letter
224 # codes such as 'M', 'A', and 'D'.
225 self.changed_paths = { }
227 # The msg will be accumulated later, as log data is read.
228 self.msg = ''
230 def absorb_changed_paths(self, out):
231 'Read changed paths from OUT into self, until no more.'
232 while 1:
233 line = out.readline()
234 if len(line) == 1: return
235 line = line[:-1]
236 op_portion = line[3:4]
237 path_portion = line[5:]
238 # If we're running on Windows we get backslashes instead of
239 # forward slashes.
240 path_portion = path_portion.replace('\\', '/')
241 # # We could parse out history information, but currently we
242 # # just leave it in the path portion because that's how some
243 # # tests expect it.
245 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
246 # if m:
247 # path_portion = m.group(1)
248 self.changed_paths[path_portion] = op_portion
250 def __cmp__(self, other):
251 return cmp(self.revision, other.revision) or \
252 cmp(self.author, other.author) or cmp(self.date, other.date) or \
253 cmp(self.changed_paths, other.changed_paths) or \
254 cmp(self.msg, other.msg)
256 def get_path_op(self, path):
257 """Return the operator for the change involving PATH.
259 PATH is allowed to include string interpolation directives (e.g.,
260 '%(trunk)s'), which are interpolated against self.symbols. Return
261 None if there is no record for PATH."""
262 return self.changed_paths.get(path % self.symbols)
264 def check_msg(self, msg):
265 """Verify that this Log's message starts with the specified MSG."""
266 if self.msg.find(msg) != 0:
267 raise Failure(
268 "Revision %d log message was:\n%s\n\n"
269 "It should have begun with:\n%s\n\n"
270 % (self.revision, self.msg, msg,)
273 def check_change(self, path, op):
274 """Verify that this Log includes a change for PATH with operator OP.
276 PATH is allowed to include string interpolation directives (e.g.,
277 '%(trunk)s'), which are interpolated against self.symbols."""
279 path = path % self.symbols
280 found_op = self.changed_paths.get(path, None)
281 if found_op is None:
282 raise Failure(
283 "Revision %d does not include change for path %s "
284 "(it should have been %s).\n"
285 % (self.revision, path, op,)
287 if found_op != op:
288 raise Failure(
289 "Revision %d path %s had op %s (it should have been %s)\n"
290 % (self.revision, path, found_op, op,)
293 def check_changes(self, changed_paths):
294 """Verify that this Log has precisely the CHANGED_PATHS specified.
296 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
297 strings are allowed to include string interpolation directives
298 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
300 cp = {}
301 for (path, op) in changed_paths:
302 cp[path % self.symbols] = op
304 if self.changed_paths != cp:
305 raise Failure(
306 "Revision %d changed paths list was:\n%s\n\n"
307 "It should have been:\n%s\n\n"
308 % (self.revision, self.changed_paths, cp,)
311 def check(self, msg, changed_paths):
312 """Verify that this Log has the MSG and CHANGED_PATHS specified.
314 Convenience function to check two things at once. MSG is passed
315 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
317 self.check_msg(msg)
318 self.check_changes(changed_paths)
321 def parse_log(svn_repos, symbols):
322 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
324 Initialize the Logs' symbols with SYMBOLS."""
326 class LineFeeder:
327 'Make a list of lines behave like an open file handle.'
328 def __init__(self, lines):
329 self.lines = lines
330 def readline(self):
331 if len(self.lines) > 0:
332 return self.lines.pop(0)
333 else:
334 return None
336 def absorb_message_body(out, num_lines, log):
337 """Read NUM_LINES of log message body from OUT into Log item LOG."""
339 for i in range(num_lines):
340 log.msg += out.readline()
342 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
343 '(?P<author>[^\|]+) \| '
344 '(?P<date>[^\|]+) '
345 '\| (?P<lines>[0-9]+) (line|lines)$')
347 log_separator = '-' * 72
349 logs = { }
351 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
353 while 1:
354 this_log = None
355 line = out.readline()
356 if not line: break
357 line = line[:-1]
359 if line.find(log_separator) == 0:
360 line = out.readline()
361 if not line: break
362 line = line[:-1]
363 m = log_start_re.match(line)
364 if m:
365 this_log = Log(
366 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
367 line = out.readline()
368 if not line.find('Changed paths:') == 0:
369 print 'unexpected log output (missing changed paths)'
370 print "Line: '%s'" % line
371 sys.exit(1)
372 this_log.absorb_changed_paths(out)
373 absorb_message_body(out, int(m.group('lines')), this_log)
374 logs[this_log.revision] = this_log
375 elif len(line) == 0:
376 break # We've reached the end of the log output.
377 else:
378 print 'unexpected log output (missing revision line)'
379 print "Line: '%s'" % line
380 sys.exit(1)
381 else:
382 print 'unexpected log output (missing log separator)'
383 print "Line: '%s'" % line
384 sys.exit(1)
386 return logs
389 def erase(path):
390 """Unconditionally remove PATH and its subtree, if any. PATH may be
391 non-existent, a file or symlink, or a directory."""
392 if os.path.isdir(path):
393 safe_rmtree(path)
394 elif os.path.exists(path):
395 os.remove(path)
398 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
400 def sym_log_msg(symbolic_name, is_tag=None):
401 """Return the expected log message for a cvs2svn-synthesized revision
402 creating branch or tag SYMBOLIC_NAME."""
404 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
405 if is_tag:
406 type = 'tag'
407 else:
408 type = 'branch'
410 return log_msg_text_wrapper.fill(
411 "This commit was manufactured by cvs2svn to create %s '%s'."
412 % (type, symbolic_name)
416 def make_conversion_id(
417 name, args, passbypass, options_file=None, symbol_hints_file=None
419 """Create an identifying tag for a conversion.
421 The return value can also be used as part of a filesystem path.
423 NAME is the name of the CVS repository.
425 ARGS are the extra arguments to be passed to cvs2svn.
427 PASSBYPASS is a boolean indicating whether the conversion is to be
428 run one pass at a time.
430 If OPTIONS_FILE is specified, it is an options file that will be
431 used for the conversion.
433 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
434 will be used for the conversion.
436 The 1-to-1 mapping between cvs2svn command parameters and
437 conversion_ids allows us to avoid running the same conversion more
438 than once, when multiple tests use exactly the same conversion."""
440 conv_id = name
442 _win32_fname_mapping = { '/': '_sl_', '\\': '_bs_', ':': '_co_',
443 '*': '_st_', '?': '_qm_', '"': '_qq_',
444 '<': '_lt_', '>': '_gt_', '|': '_pi_', }
445 for arg in args:
446 # Replace some characters that Win32 isn't happy about having in a
447 # filename (which was causing the eol_mime test to fail).
448 sanitized_arg = arg
449 for a, b in _win32_fname_mapping.items():
450 sanitized_arg = sanitized_arg.replace(a, b)
451 conv_id += sanitized_arg
453 if passbypass:
454 conv_id += '-passbypass'
456 if options_file is not None:
457 conv_id += '--options=%s' % options_file
459 if symbol_hints_file is not None:
460 conv_id += '--symbol-hints=%s' % symbol_hints_file
462 return conv_id
465 class Conversion:
466 """A record of a cvs2svn conversion.
468 Fields:
470 conv_id -- the conversion id for this Conversion.
472 name -- a one-word name indicating the involved repositories.
474 dumpfile -- the name of the SVN dumpfile created by the conversion
475 (if the DUMPFILE constructor argument was used); otherwise,
476 None.
478 repos -- the path to the svn repository. Unset if DUMPFILE was
479 specified.
481 logs -- a dictionary of Log instances, as returned by parse_log().
482 Unset if DUMPFILE was specified.
484 symbols -- a dictionary of symbols used for string interpolation
485 in path names.
487 stdout -- a list of lines written by cvs2svn to stdout
489 _wc -- the basename of the svn working copy (within tmp_dir).
490 Unset if DUMPFILE was specified.
492 _wc_path -- the path to the svn working copy, if it has already
493 been created; otherwise, None. (The working copy is created
494 lazily when get_wc() is called.) Unset if DUMPFILE was
495 specified.
497 _wc_tree -- the tree built from the svn working copy, if it has
498 already been created; otherwise, None. The tree is created
499 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
500 specified.
502 _svnrepos -- the basename of the svn repository (within tmp_dir).
503 Unset if DUMPFILE was specified."""
505 # The number of the last cvs2svn pass (determined lazily by
506 # get_last_pass()).
507 last_pass = None
509 @classmethod
510 def get_last_pass(cls):
511 """Return the number of cvs2svn's last pass."""
513 if cls.last_pass is None:
514 out = run_cvs2svn(None, '--help-passes')
515 cls.last_pass = int(out[-1].split()[0])
516 return cls.last_pass
518 def __init__(
519 self, conv_id, name, error_re, passbypass, symbols, args,
520 options_file=None, symbol_hints_file=None, dumpfile=None,
522 self.conv_id = conv_id
523 self.name = name
524 self.symbols = symbols
525 if not os.path.isdir(tmp_dir):
526 os.mkdir(tmp_dir)
528 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
530 if dumpfile:
531 self.dumpfile = os.path.join(tmp_dir, dumpfile)
532 # Clean up from any previous invocations of this script.
533 erase(self.dumpfile)
534 else:
535 self.dumpfile = None
536 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
537 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
538 self._wc_path = None
539 self._wc_tree = None
541 # Clean up from any previous invocations of this script.
542 erase(self.repos)
543 erase(self._wc)
545 args = list(args)
546 if options_file:
547 self.options_file = os.path.join(cvsrepos, options_file)
548 args.extend([
549 '--options=%s' % self.options_file,
551 assert not symbol_hints_file
552 else:
553 self.options_file = None
554 if tmp_dir != 'cvs2svn-tmp':
555 # Only include this argument if it differs from cvs2svn's default:
556 args.extend([
557 '--tmpdir=%s' % tmp_dir,
560 if symbol_hints_file:
561 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
562 args.extend([
563 '--symbol-hints=%s' % self.symbol_hints_file,
566 if self.dumpfile:
567 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
568 else:
569 args.extend(['-s', self.repos])
570 args.extend([cvsrepos])
572 if passbypass:
573 self.stdout = []
574 for p in range(1, self.get_last_pass() + 1):
575 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
576 else:
577 self.stdout = run_cvs2svn(error_re, *args)
579 if self.dumpfile:
580 if not os.path.isfile(self.dumpfile):
581 raise Failure(
582 "Dumpfile not created: '%s'"
583 % os.path.join(os.getcwd(), self.dumpfile)
585 else:
586 if os.path.isdir(self.repos):
587 self.logs = parse_log(self.repos, self.symbols)
588 elif error_re is None:
589 raise Failure(
590 "Repository not created: '%s'"
591 % os.path.join(os.getcwd(), self.repos)
594 def output_found(self, pattern):
595 """Return True if PATTERN matches any line in self.stdout.
597 PATTERN is a regular expression pattern as a string.
600 pattern_re = re.compile(pattern)
602 for line in self.stdout:
603 if pattern_re.match(line):
604 # We found the pattern that we were looking for.
605 return 1
606 else:
607 return 0
609 def find_tag_log(self, tagname):
610 """Search LOGS for a log message containing 'TAGNAME' and return the
611 log in which it was found."""
612 for i in xrange(len(self.logs), 0, -1):
613 if self.logs[i].msg.find("'"+tagname+"'") != -1:
614 return self.logs[i]
615 raise ValueError("Tag %s not found in logs" % tagname)
617 def get_wc(self, *args):
618 """Return the path to the svn working copy, or a path within the WC.
620 If a working copy has not been created yet, create it now.
622 If ARGS are specified, then they should be strings that form
623 fragments of a path within the WC. They are joined using
624 os.path.join() and appended to the WC path."""
626 if self._wc_path is None:
627 run_svn('co', repos_to_url(self.repos), self._wc)
628 self._wc_path = self._wc
629 return os.path.join(self._wc_path, *args)
631 def get_wc_tree(self):
632 if self._wc_tree is None:
633 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
634 return self._wc_tree
636 def path_exists(self, *args):
637 """Return True if the specified path exists within the repository.
639 (The strings in ARGS are first joined into a path using
640 os.path.join().)"""
642 return os.path.exists(self.get_wc(*args))
644 def check_props(self, keys, checks):
645 """Helper function for checking lots of properties. For a list of
646 files in the conversion, check that the values of the properties
647 listed in KEYS agree with those listed in CHECKS. CHECKS is a
648 list of tuples: [ (filename, [value, value, ...]), ...], where the
649 values are listed in the same order as the key names are listed in
650 KEYS."""
652 for (file, values) in checks:
653 assert len(values) == len(keys)
654 props = props_for_path(self.get_wc_tree(), file)
655 for i in range(len(keys)):
656 if props.get(keys[i]) != values[i]:
657 raise Failure(
658 "File %s has property %s set to \"%s\" "
659 "(it should have been \"%s\").\n"
660 % (file, keys[i], props.get(keys[i]), values[i],)
664 class GitConversion:
665 """A record of a cvs2svn conversion.
667 Fields:
669 name -- a one-word name indicating the CVS repository to be converted.
671 stdout -- a list of lines written by cvs2svn to stdout."""
673 def __init__(self, name, error_re, args, options_file=None):
674 self.name = name
675 if not os.path.isdir(tmp_dir):
676 os.mkdir(tmp_dir)
678 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
680 args = list(args)
681 if options_file:
682 self.options_file = os.path.join(cvsrepos, options_file)
683 args.extend([
684 '--options=%s' % self.options_file,
686 else:
687 self.options_file = None
689 self.stdout = run_cvs2git(error_re, *args)
692 # Cache of conversions that have already been done. Keys are conv_id;
693 # values are Conversion instances.
694 already_converted = { }
696 def ensure_conversion(
697 name, error_re=None, passbypass=None,
698 trunk=None, branches=None, tags=None,
699 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
701 """Convert CVS repository NAME to Subversion, but only if it has not
702 been converted before by this invocation of this script. If it has
703 been converted before, return the Conversion object from the
704 previous invocation.
706 If no error, return a Conversion instance.
708 If ERROR_RE is a string, it is a regular expression expected to
709 match some line of stderr printed by the conversion. If there is an
710 error and ERROR_RE is not set, then raise Failure.
712 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
713 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
715 NAME is just one word. For example, 'main' would mean to convert
716 './test-data/main-cvsrepos', and after the conversion, the resulting
717 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
718 a checked out head working copy in './cvs2svn-tmp/main-wc'.
720 Any other options to pass to cvs2svn should be in ARGS, each element
721 being one option, e.g., '--trunk-only'. If the option takes an
722 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
723 are passed to cvs2svn in the order that they appear in ARGS.
725 If OPTIONS_FILE is specified, then it should be the name of a file
726 within the main directory of the cvs repository associated with this
727 test. It is passed to cvs2svn using the --options option (which
728 suppresses some other options that are incompatible with --options).
730 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
731 file within the main directory of the cvs repository associated with
732 this test. It is passed to cvs2svn using the --symbol-hints option.
734 If DUMPFILE is specified, then it is the name of a dumpfile within
735 the temporary directory to which the conversion output should be
736 written."""
738 if args is None:
739 args = []
740 else:
741 args = list(args)
743 if trunk is None:
744 trunk = 'trunk'
745 else:
746 args.append('--trunk=%s' % (trunk,))
748 if branches is None:
749 branches = 'branches'
750 else:
751 args.append('--branches=%s' % (branches,))
753 if tags is None:
754 tags = 'tags'
755 else:
756 args.append('--tags=%s' % (tags,))
758 conv_id = make_conversion_id(
759 name, args, passbypass, options_file, symbol_hints_file
762 if conv_id not in already_converted:
763 try:
764 # Run the conversion and store the result for the rest of this
765 # session:
766 already_converted[conv_id] = Conversion(
767 conv_id, name, error_re, passbypass,
768 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
769 args, options_file, symbol_hints_file, dumpfile,
771 except Failure:
772 # Remember the failure so that a future attempt to run this conversion
773 # does not bother to retry, but fails immediately.
774 already_converted[conv_id] = None
775 raise
777 conv = already_converted[conv_id]
778 if conv is None:
779 raise Failure()
780 return conv
783 class Cvs2SvnTestCase(TestCase):
784 def __init__(
785 self, name, description=None, variant=None,
786 error_re=None, passbypass=None,
787 trunk=None, branches=None, tags=None,
788 args=None,
789 options_file=None, symbol_hints_file=None, dumpfile=None,
791 TestCase.__init__(self)
792 self.name = name
794 if description is not None:
795 self._description = description
796 else:
797 # By default, use the first line of the class docstring as the
798 # description:
799 self._description = self.__doc__.splitlines()[0]
801 # Check that the original description is OK before we tinker with
802 # it:
803 self.check_description()
805 if variant is not None:
806 # Modify description to show the variant. Trim description
807 # first if necessary to stay within the 50-character limit.
808 suffix = '...variant %s' % (variant,)
809 self._description = self._description[:50 - len(suffix)] + suffix
810 # Check that the description is still OK:
811 self.check_description()
813 self.error_re = error_re
814 self.passbypass = passbypass
815 self.trunk = trunk
816 self.branches = branches
817 self.tags = tags
818 self.args = args
819 self.options_file = options_file
820 self.symbol_hints_file = symbol_hints_file
821 self.dumpfile = dumpfile
823 def get_description(self):
824 return self._description
826 def ensure_conversion(self):
827 return ensure_conversion(
828 self.name,
829 error_re=self.error_re, passbypass=self.passbypass,
830 trunk=self.trunk, branches=self.branches, tags=self.tags,
831 args=self.args,
832 options_file=self.options_file,
833 symbol_hints_file=self.symbol_hints_file,
834 dumpfile=self.dumpfile,
838 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
839 """Test properties resulting from a conversion."""
841 def __init__(self, name, props_to_test, expected_props, **kw):
842 """Initialize an instance of Cvs2SvnPropertiesTestCase.
844 NAME is the name of the test, passed to Cvs2SvnTestCase.
845 PROPS_TO_TEST is a list of the names of svn properties that should
846 be tested. EXPECTED_PROPS is a list of tuples [(filename,
847 [value,...])], where the second item in each tuple is a list of
848 values expected for the properties listed in PROPS_TO_TEST for the
849 specified filename. If a property must *not* be set, then its
850 value should be listed as None."""
852 Cvs2SvnTestCase.__init__(self, name, **kw)
853 self.props_to_test = props_to_test
854 self.expected_props = expected_props
856 def run(self):
857 conv = self.ensure_conversion()
858 conv.check_props(self.props_to_test, self.expected_props)
861 #----------------------------------------------------------------------
862 # Tests.
863 #----------------------------------------------------------------------
866 def show_usage():
867 "cvs2svn with no arguments shows usage"
868 out = run_cvs2svn(None)
869 if (len(out) > 2 and out[0].find('ERROR:') == 0
870 and out[1].find('DBM module')):
871 print 'cvs2svn cannot execute due to lack of proper DBM module.'
872 print 'Exiting without running any further tests.'
873 sys.exit(1)
874 if out[0].find('Usage:') < 0:
875 raise Failure('Basic cvs2svn invocation failed.')
878 def show_help_passes():
879 "cvs2svn --help-passes shows pass information"
880 out = run_cvs2svn(None, '--help-passes')
881 if out[0].find('PASSES') < 0:
882 raise Failure('cvs2svn --help-passes failed.')
885 def attr_exec():
886 "detection of the executable flag"
887 if sys.platform == 'win32':
888 raise svntest.Skip()
889 conv = ensure_conversion('main')
890 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
891 if not st[0] & stat.S_IXUSR:
892 raise Failure()
895 def space_fname():
896 "conversion of filename with a space"
897 conv = ensure_conversion('main')
898 if not conv.path_exists('trunk', 'single-files', 'space fname'):
899 raise Failure()
902 def two_quick():
903 "two commits in quick succession"
904 conv = ensure_conversion('main')
905 logs = parse_log(
906 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
907 if len(logs) != 2:
908 raise Failure()
911 class PruneWithCare(Cvs2SvnTestCase):
912 "prune, but never too much"
914 def __init__(self, **kw):
915 Cvs2SvnTestCase.__init__(self, 'main', **kw)
917 def run(self):
918 # Robert Pluim encountered this lovely one while converting the
919 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
920 # repository (see issue #1302). Step 4 is the doozy:
922 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
923 # revision 2: adds trunk/blah/NEWS
924 # revision 3: deletes trunk/blah/cookie
925 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
926 # revision 5: does nothing
928 # After fixing cvs2svn, the sequence (correctly) looks like this:
930 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
931 # revision 2: adds trunk/blah/NEWS
932 # revision 3: deletes trunk/blah/cookie
933 # revision 4: does nothing [because trunk/blah/cookie already deleted]
934 # revision 5: deletes blah
936 # The difference is in 4 and 5. In revision 4, it's not correct to
937 # prune blah/, because NEWS is still in there, so revision 4 does
938 # nothing now. But when we delete NEWS in 5, that should bubble up
939 # and prune blah/ instead.
941 # ### Note that empty revisions like 4 are probably going to become
942 # ### at least optional, if not banished entirely from cvs2svn's
943 # ### output. Hmmm, or they may stick around, with an extra
944 # ### revision property explaining what happened. Need to think
945 # ### about that. In some sense, it's a bug in Subversion itself,
946 # ### that such revisions don't show up in 'svn log' output.
948 # In the test below, 'trunk/full-prune/first' represents
949 # cookie, and 'trunk/full-prune/second' represents NEWS.
951 conv = self.ensure_conversion()
953 # Confirm that revision 4 removes '/trunk/full-prune/first',
954 # and that revision 6 removes '/trunk/full-prune'.
956 # Also confirm similar things about '/full-prune-reappear/...',
957 # which is similar, except that later on it reappears, restored
958 # from pruneland, because a file gets added to it.
960 # And finally, a similar thing for '/partial-prune/...', except that
961 # in its case, a permanent file on the top level prevents the
962 # pruning from going farther than the subdirectory containing first
963 # and second.
965 for path in ('full-prune/first',
966 'full-prune-reappear/sub/first',
967 'partial-prune/sub/first'):
968 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
970 for path in ('full-prune',
971 'full-prune-reappear',
972 'partial-prune/sub'):
973 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
975 for path in ('full-prune-reappear',
976 'full-prune-reappear/appears-later'):
977 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
980 def interleaved_commits():
981 "two interleaved trunk commits, different log msgs"
982 # See test-data/main-cvsrepos/proj/README.
983 conv = ensure_conversion('main')
985 # The initial import.
986 rev = 26
987 conv.logs[rev].check('Initial import.', (
988 ('/%(trunk)s/interleaved', 'A'),
989 ('/%(trunk)s/interleaved/1', 'A'),
990 ('/%(trunk)s/interleaved/2', 'A'),
991 ('/%(trunk)s/interleaved/3', 'A'),
992 ('/%(trunk)s/interleaved/4', 'A'),
993 ('/%(trunk)s/interleaved/5', 'A'),
994 ('/%(trunk)s/interleaved/a', 'A'),
995 ('/%(trunk)s/interleaved/b', 'A'),
996 ('/%(trunk)s/interleaved/c', 'A'),
997 ('/%(trunk)s/interleaved/d', 'A'),
998 ('/%(trunk)s/interleaved/e', 'A'),
1001 def check_letters(rev):
1002 """Check if REV is the rev where only letters were committed."""
1004 conv.logs[rev].check('Committing letters only.', (
1005 ('/%(trunk)s/interleaved/a', 'M'),
1006 ('/%(trunk)s/interleaved/b', 'M'),
1007 ('/%(trunk)s/interleaved/c', 'M'),
1008 ('/%(trunk)s/interleaved/d', 'M'),
1009 ('/%(trunk)s/interleaved/e', 'M'),
1012 def check_numbers(rev):
1013 """Check if REV is the rev where only numbers were committed."""
1015 conv.logs[rev].check('Committing numbers only.', (
1016 ('/%(trunk)s/interleaved/1', 'M'),
1017 ('/%(trunk)s/interleaved/2', 'M'),
1018 ('/%(trunk)s/interleaved/3', 'M'),
1019 ('/%(trunk)s/interleaved/4', 'M'),
1020 ('/%(trunk)s/interleaved/5', 'M'),
1023 # One of the commits was letters only, the other was numbers only.
1024 # But they happened "simultaneously", so we don't assume anything
1025 # about which commit appeared first, so we just try both ways.
1026 rev += 1
1027 try:
1028 check_letters(rev)
1029 check_numbers(rev + 1)
1030 except Failure:
1031 check_numbers(rev)
1032 check_letters(rev + 1)
1035 def simple_commits():
1036 "simple trunk commits"
1037 # See test-data/main-cvsrepos/proj/README.
1038 conv = ensure_conversion('main')
1040 # The initial import.
1041 conv.logs[13].check('Initial import.', (
1042 ('/%(trunk)s/proj', 'A'),
1043 ('/%(trunk)s/proj/default', 'A'),
1044 ('/%(trunk)s/proj/sub1', 'A'),
1045 ('/%(trunk)s/proj/sub1/default', 'A'),
1046 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1047 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1048 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1049 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1050 ('/%(trunk)s/proj/sub2', 'A'),
1051 ('/%(trunk)s/proj/sub2/default', 'A'),
1052 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1053 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1054 ('/%(trunk)s/proj/sub3', 'A'),
1055 ('/%(trunk)s/proj/sub3/default', 'A'),
1058 # The first commit.
1059 conv.logs[18].check('First commit to proj, affecting two files.', (
1060 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1061 ('/%(trunk)s/proj/sub3/default', 'M'),
1064 # The second commit.
1065 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1066 ('/%(trunk)s/proj/default', 'M'),
1067 ('/%(trunk)s/proj/sub1/default', 'M'),
1068 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1069 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1070 ('/%(trunk)s/proj/sub2/default', 'M'),
1071 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1072 ('/%(trunk)s/proj/sub3/default', 'M')
1076 class SimpleTags(Cvs2SvnTestCase):
1077 "simple tags and branches, no commits"
1079 def __init__(self, **kw):
1080 # See test-data/main-cvsrepos/proj/README.
1081 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1083 def run(self):
1084 conv = self.ensure_conversion()
1086 # Verify the copy source for the tags we are about to check
1087 # No need to verify the copyfrom revision, as simple_commits did that
1088 conv.logs[13].check('Initial import.', (
1089 ('/%(trunk)s/proj', 'A'),
1090 ('/%(trunk)s/proj/default', 'A'),
1091 ('/%(trunk)s/proj/sub1', 'A'),
1092 ('/%(trunk)s/proj/sub1/default', 'A'),
1093 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1094 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1095 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1096 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1097 ('/%(trunk)s/proj/sub2', 'A'),
1098 ('/%(trunk)s/proj/sub2/default', 'A'),
1099 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1100 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1101 ('/%(trunk)s/proj/sub3', 'A'),
1102 ('/%(trunk)s/proj/sub3/default', 'A'),
1105 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1107 # Tag on rev 1.1.1.1 of all files in proj
1108 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1109 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1110 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1111 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1114 # The same, as a tag
1115 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1116 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1117 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1120 # Tag on rev 1.1.1.1 of all files in proj, except one
1121 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1122 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1123 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1124 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1127 # The same, as a branch
1128 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1129 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1130 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1134 def simple_branch_commits():
1135 "simple branch commits"
1136 # See test-data/main-cvsrepos/proj/README.
1137 conv = ensure_conversion('main')
1139 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1140 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1141 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1142 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1146 def mixed_time_tag():
1147 "mixed-time tag"
1148 # See test-data/main-cvsrepos/proj/README.
1149 conv = ensure_conversion('main')
1151 log = conv.find_tag_log('T_MIXED')
1152 log.check_changes((
1153 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1157 def mixed_time_branch_with_added_file():
1158 "mixed-time branch, and a file added to the branch"
1159 # See test-data/main-cvsrepos/proj/README.
1160 conv = ensure_conversion('main')
1162 # A branch from the same place as T_MIXED in the previous test,
1163 # plus a file added directly to the branch
1164 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1165 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1166 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1167 ('/%(branches)s/B_MIXED/single-files', 'D'),
1168 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1169 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1170 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1173 conv.logs[22].check('Add a file on branch B_MIXED.', (
1174 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1178 def mixed_commit():
1179 "a commit affecting both trunk and a branch"
1180 # See test-data/main-cvsrepos/proj/README.
1181 conv = ensure_conversion('main')
1183 conv.logs[24].check(
1184 'A single commit affecting one file on branch B_MIXED '
1185 'and one on trunk.', (
1186 ('/%(trunk)s/proj/sub2/default', 'M'),
1187 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1191 def split_time_branch():
1192 "branch some trunk files, and later branch the rest"
1193 # See test-data/main-cvsrepos/proj/README.
1194 conv = ensure_conversion('main')
1196 # First change on the branch, creating it
1197 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1198 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1199 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1200 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1201 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1204 conv.logs[29].check('First change on branch B_SPLIT.', (
1205 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1206 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1207 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1208 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1209 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1212 # A trunk commit for the file which was not branched
1213 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1214 'This was committed about an', (
1215 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1218 # Add the file not already branched to the branch, with modification:w
1219 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1220 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1221 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1224 conv.logs[32].check('This change affects sub3/default and '
1225 'sub1/subsubB/default, on branch', (
1226 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1227 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1231 def multiple_tags():
1232 "multiple tags referring to same revision"
1233 conv = ensure_conversion('main')
1234 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1235 raise Failure()
1236 if not conv.path_exists(
1237 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1238 raise Failure()
1241 def multiply_defined_symbols():
1242 "multiple definitions of symbol names"
1244 # We can only check one line of the error output at a time, so test
1245 # twice. (The conversion only have to be done once because the
1246 # results are cached.)
1247 conv = ensure_conversion(
1248 'multiply-defined-symbols',
1249 error_re=(
1250 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1251 r"1\.2\.4 1\.2\.2"
1254 conv = ensure_conversion(
1255 'multiply-defined-symbols',
1256 error_re=(
1257 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1258 r"1\.2 1\.1"
1263 def multiply_defined_symbols_renamed():
1264 "rename multiply defined symbols"
1266 conv = ensure_conversion(
1267 'multiply-defined-symbols',
1268 options_file='cvs2svn-rename.options',
1272 def multiply_defined_symbols_ignored():
1273 "ignore multiply defined symbols"
1275 conv = ensure_conversion(
1276 'multiply-defined-symbols',
1277 options_file='cvs2svn-ignore.options',
1281 def repeatedly_defined_symbols():
1282 "multiple identical definitions of symbol names"
1284 # If a symbol is defined multiple times but has the same value each
1285 # time, that should not be an error.
1287 conv = ensure_conversion('repeatedly-defined-symbols')
1290 def bogus_tag():
1291 "conversion of invalid symbolic names"
1292 conv = ensure_conversion('bogus-tag')
1295 def overlapping_branch():
1296 "ignore a file with a branch with two names"
1297 conv = ensure_conversion('overlapping-branch')
1299 if not conv.output_found('.*cannot also have name \'vendorB\''):
1300 raise Failure()
1302 conv.logs[2].check('imported', (
1303 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1304 ('/%(trunk)s/overlapping-branch', 'A'),
1307 if len(conv.logs) != 2:
1308 raise Failure()
1311 class PhoenixBranch(Cvs2SvnTestCase):
1312 "convert a branch file rooted in a 'dead' revision"
1314 def __init__(self, **kw):
1315 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1317 def run(self):
1318 conv = self.ensure_conversion()
1319 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1320 ('/%(branches)s/volsung_20010721', 'A'),
1321 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1323 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1324 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1328 ###TODO: We check for 4 changed paths here to accomodate creating tags
1329 ###and branches in rev 1, but that will change, so this will
1330 ###eventually change back.
1331 def ctrl_char_in_log():
1332 "handle a control char in a log message"
1333 # This was issue #1106.
1334 rev = 2
1335 conv = ensure_conversion('ctrl-char-in-log')
1336 conv.logs[rev].check_changes((
1337 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1339 if conv.logs[rev].msg.find('\x04') < 0:
1340 raise Failure(
1341 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1344 def overdead():
1345 "handle tags rooted in a redeleted revision"
1346 conv = ensure_conversion('overdead')
1349 class NoTrunkPrune(Cvs2SvnTestCase):
1350 "ensure that trunk doesn't get pruned"
1352 def __init__(self, **kw):
1353 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1355 def run(self):
1356 conv = self.ensure_conversion()
1357 for rev in conv.logs.keys():
1358 rev_logs = conv.logs[rev]
1359 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1360 raise Failure()
1363 def double_delete():
1364 "file deleted twice, in the root of the repository"
1365 # This really tests several things: how we handle a file that's
1366 # removed (state 'dead') in two successive revisions; how we
1367 # handle a file in the root of the repository (there were some
1368 # bugs in cvs2svn's svn path construction for top-level files); and
1369 # the --no-prune option.
1370 conv = ensure_conversion(
1371 'double-delete', args=['--trunk-only', '--no-prune'])
1373 path = '/%(trunk)s/twice-removed'
1374 rev = 2
1375 conv.logs[rev].check('Updated CVS', (
1376 (path, 'A'),
1378 conv.logs[rev + 1].check('Remove this file for the first time.', (
1379 (path, 'D'),
1381 conv.logs[rev + 2].check('Remove this file for the second time,', (
1385 def split_branch():
1386 "branch created from both trunk and another branch"
1387 # See test-data/split-branch-cvsrepos/README.
1389 # The conversion will fail if the bug is present, and
1390 # ensure_conversion will raise Failure.
1391 conv = ensure_conversion('split-branch')
1394 def resync_misgroups():
1395 "resyncing should not misorder commit groups"
1396 # See test-data/resync-misgroups-cvsrepos/README.
1398 # The conversion will fail if the bug is present, and
1399 # ensure_conversion will raise Failure.
1400 conv = ensure_conversion('resync-misgroups')
1403 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1404 "allow tags with mixed trunk and branch sources"
1406 def __init__(self, **kw):
1407 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1409 def run(self):
1410 conv = self.ensure_conversion()
1412 tags = conv.symbols.get('tags', 'tags')
1414 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1415 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1416 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1417 raise Failure()
1418 if (open(a_path, 'r').read().find('1.24') == -1) \
1419 or (open(b_path, 'r').read().find('1.5') == -1):
1420 raise Failure()
1423 def enroot_race():
1424 "never use the rev-in-progress as a copy source"
1426 # See issue #1427 and r8544.
1427 conv = ensure_conversion('enroot-race')
1428 rev = 6
1429 conv.logs[rev].check_changes((
1430 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1431 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1432 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1434 conv.logs[rev + 1].check_changes((
1435 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1436 ('/%(trunk)s/proj/a.txt', 'M'),
1437 ('/%(trunk)s/proj/b.txt', 'M'),
1441 def enroot_race_obo():
1442 "do use the last completed rev as a copy source"
1443 conv = ensure_conversion('enroot-race-obo')
1444 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1445 if not len(conv.logs) == 3:
1446 raise Failure()
1449 class BranchDeleteFirst(Cvs2SvnTestCase):
1450 "correctly handle deletion as initial branch action"
1452 def __init__(self, **kw):
1453 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1455 def run(self):
1456 # See test-data/branch-delete-first-cvsrepos/README.
1458 # The conversion will fail if the bug is present, and
1459 # ensure_conversion would raise Failure.
1460 conv = self.ensure_conversion()
1462 branches = conv.symbols.get('branches', 'branches')
1464 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1465 if conv.path_exists(branches, 'branch-1', 'file'):
1466 raise Failure()
1467 if conv.path_exists(branches, 'branch-2', 'file'):
1468 raise Failure()
1469 if not conv.path_exists(branches, 'branch-3', 'file'):
1470 raise Failure()
1473 def nonascii_filenames():
1474 "non ascii files converted incorrectly"
1475 # see issue #1255
1477 # on a en_US.iso-8859-1 machine this test fails with
1478 # svn: Can't recode ...
1480 # as described in the issue
1482 # on a en_US.UTF-8 machine this test fails with
1483 # svn: Malformed XML ...
1485 # which means at least it fails. Unfortunately it won't fail
1486 # with the same error...
1488 # mangle current locale settings so we know we're not running
1489 # a UTF-8 locale (which does not exhibit this problem)
1490 current_locale = locale.getlocale()
1491 new_locale = 'en_US.ISO8859-1'
1492 locale_changed = None
1494 # From http://docs.python.org/lib/module-sys.html
1496 # getfilesystemencoding():
1498 # Return the name of the encoding used to convert Unicode filenames
1499 # into system file names, or None if the system default encoding is
1500 # used. The result value depends on the operating system:
1502 # - On Windows 9x, the encoding is ``mbcs''.
1503 # - On Mac OS X, the encoding is ``utf-8''.
1504 # - On Unix, the encoding is the user's preference according to the
1505 # result of nl_langinfo(CODESET), or None if the
1506 # nl_langinfo(CODESET) failed.
1507 # - On Windows NT+, file names are Unicode natively, so no conversion is
1508 # performed.
1510 # So we're going to skip this test on Mac OS X for now.
1511 if sys.platform == "darwin":
1512 raise svntest.Skip()
1514 try:
1515 # change locale to non-UTF-8 locale to generate latin1 names
1516 locale.setlocale(locale.LC_ALL, # this might be too broad?
1517 new_locale)
1518 locale_changed = 1
1519 except locale.Error:
1520 raise svntest.Skip()
1522 try:
1523 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1524 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1525 if not os.path.exists(dstrepos_path):
1526 # create repos from existing main repos
1527 shutil.copytree(srcrepos_path, dstrepos_path)
1528 base_path = os.path.join(dstrepos_path, 'single-files')
1529 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1530 os.path.join(base_path, 'two\366uick,v'))
1531 new_path = os.path.join(dstrepos_path, 'single\366files')
1532 os.rename(base_path, new_path)
1534 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1535 finally:
1536 if locale_changed:
1537 locale.setlocale(locale.LC_ALL, current_locale)
1538 safe_rmtree(dstrepos_path)
1541 class UnicodeTest(Cvs2SvnTestCase):
1542 "metadata contains unicode"
1544 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1546 def __init__(self, name, warning_expected, **kw):
1547 if warning_expected:
1548 error_re = self.warning_pattern
1549 else:
1550 error_re = None
1552 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1553 self.warning_expected = warning_expected
1555 def run(self):
1556 try:
1557 # ensure the availability of the "utf_8" encoding:
1558 u'a'.encode('utf_8').decode('utf_8')
1559 except LookupError:
1560 raise svntest.Skip()
1562 self.ensure_conversion()
1565 class UnicodeAuthor(UnicodeTest):
1566 "author name contains unicode"
1568 def __init__(self, warning_expected, **kw):
1569 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1572 class UnicodeLog(UnicodeTest):
1573 "log message contains unicode"
1575 def __init__(self, warning_expected, **kw):
1576 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1579 def vendor_branch_sameness():
1580 "avoid spurious changes for initial revs"
1581 conv = ensure_conversion(
1582 'vendor-branch-sameness', args=['--keep-trivial-imports']
1585 # The following files are in this repository:
1587 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1588 # the same contents, the file's default branch is 1.1.1,
1589 # and both revisions are in state 'Exp'.
1591 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1592 # 1.1 (the addition of a line of text).
1594 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1596 # d.txt: This file was created by 'cvs add' instead of import, so
1597 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1598 # The timestamp on the add is exactly the same as for the
1599 # imports of the other files.
1601 # e.txt: Like a.txt, except that the log message for revision 1.1
1602 # is not the standard import log message.
1604 # (Aside from e.txt, the log messages for the same revisions are the
1605 # same in all files.)
1607 # We expect that only a.txt is recognized as an import whose 1.1
1608 # revision can be omitted. The other files should be added on trunk
1609 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1610 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1611 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1612 # and c.txt should be 'D'eleted.
1614 rev = 2
1615 conv.logs[rev].check('Initial revision', (
1616 ('/%(trunk)s/proj', 'A'),
1617 ('/%(trunk)s/proj/b.txt', 'A'),
1618 ('/%(trunk)s/proj/c.txt', 'A'),
1619 ('/%(trunk)s/proj/d.txt', 'A'),
1622 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1623 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1624 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1627 conv.logs[rev + 2].check('First vendor branch revision.', (
1628 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1629 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1630 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1633 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1634 'to compensate for changes in r4,', (
1635 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1636 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1637 ('/%(trunk)s/proj/c.txt', 'D'),
1640 rev = 7
1641 conv.logs[rev].check('This log message is not the standard', (
1642 ('/%(trunk)s/proj/e.txt', 'A'),
1645 conv.logs[rev + 2].check('First vendor branch revision', (
1646 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1649 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1650 'to compensate for changes in r9,', (
1651 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1655 def vendor_branch_trunk_only():
1656 "handle vendor branches with --trunk-only"
1657 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1659 rev = 2
1660 conv.logs[rev].check('Initial revision', (
1661 ('/%(trunk)s/proj', 'A'),
1662 ('/%(trunk)s/proj/b.txt', 'A'),
1663 ('/%(trunk)s/proj/c.txt', 'A'),
1664 ('/%(trunk)s/proj/d.txt', 'A'),
1667 conv.logs[rev + 1].check('First vendor branch revision', (
1668 ('/%(trunk)s/proj/a.txt', 'A'),
1669 ('/%(trunk)s/proj/b.txt', 'M'),
1670 ('/%(trunk)s/proj/c.txt', 'D'),
1673 conv.logs[rev + 2].check('This log message is not the standard', (
1674 ('/%(trunk)s/proj/e.txt', 'A'),
1677 conv.logs[rev + 3].check('First vendor branch revision', (
1678 ('/%(trunk)s/proj/e.txt', 'M'),
1682 def default_branches():
1683 "handle default branches correctly"
1684 conv = ensure_conversion('default-branches')
1686 # There are seven files in the repository:
1688 # a.txt:
1689 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1690 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1691 # committed (thus losing the default branch "1.1.1"), then
1692 # 1.1.1.4 was imported. All vendor import release tags are
1693 # still present.
1695 # b.txt:
1696 # Like a.txt, but without rev 1.2.
1698 # c.txt:
1699 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1701 # d.txt:
1702 # Same as the previous two, but 1.1.1 branch is unlabeled.
1704 # e.txt:
1705 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1707 # deleted-on-vendor-branch.txt,v:
1708 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1710 # added-then-imported.txt,v:
1711 # Added with 'cvs add' to create 1.1, then imported with
1712 # completely different contents to create 1.1.1.1, therefore
1713 # never had a default branch.
1716 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1717 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1718 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1719 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1720 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1721 ('/%(branches)s/vbranchA', 'A'),
1722 ('/%(branches)s/vbranchA/proj', 'A'),
1723 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1724 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1725 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1726 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1729 conv.logs[3].check("This commit was generated by cvs2svn "
1730 "to compensate for changes in r2,", (
1731 ('/%(trunk)s/proj', 'A'),
1732 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1733 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1734 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1735 ('/%(trunk)s/proj/d.txt '
1736 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1737 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1738 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1739 ('/%(trunk)s/proj/e.txt '
1740 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1743 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1744 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1745 ('/%(tags)s/vtag-1/proj/d.txt '
1746 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1749 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1750 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1751 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1752 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1753 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1754 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1755 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1758 conv.logs[6].check("This commit was generated by cvs2svn "
1759 "to compensate for changes in r5,", (
1760 ('/%(trunk)s/proj/a.txt '
1761 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1762 ('/%(trunk)s/proj/b.txt '
1763 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1764 ('/%(trunk)s/proj/c.txt '
1765 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1766 ('/%(trunk)s/proj/d.txt '
1767 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1768 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1769 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1770 'R'),
1771 ('/%(trunk)s/proj/e.txt '
1772 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1775 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1776 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1777 ('/%(tags)s/vtag-2/proj/d.txt '
1778 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1781 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1782 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1783 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1784 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1785 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1786 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1787 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1790 conv.logs[9].check("This commit was generated by cvs2svn "
1791 "to compensate for changes in r8,", (
1792 ('/%(trunk)s/proj/a.txt '
1793 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1794 ('/%(trunk)s/proj/b.txt '
1795 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1796 ('/%(trunk)s/proj/c.txt '
1797 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1798 ('/%(trunk)s/proj/d.txt '
1799 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1800 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1801 ('/%(trunk)s/proj/e.txt '
1802 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1805 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1806 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1807 ('/%(tags)s/vtag-3/proj/d.txt '
1808 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1809 ('/%(tags)s/vtag-3/proj/e.txt '
1810 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1813 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1814 ('/%(trunk)s/proj/a.txt', 'M'),
1817 conv.logs[12].check("Add a file to the working copy.", (
1818 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1821 conv.logs[13].check(sym_log_msg('vbranchA'), (
1822 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1823 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1826 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1827 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1828 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1829 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1830 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1831 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1832 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1833 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1836 conv.logs[15].check("This commit was generated by cvs2svn "
1837 "to compensate for changes in r14,", (
1838 ('/%(trunk)s/proj/b.txt '
1839 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1840 ('/%(trunk)s/proj/c.txt '
1841 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1842 ('/%(trunk)s/proj/d.txt '
1843 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1844 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1845 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1846 'A'),
1847 ('/%(trunk)s/proj/e.txt '
1848 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1851 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1852 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1853 ('/%(tags)s/vtag-4/proj/d.txt '
1854 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1858 def default_branches_trunk_only():
1859 "handle default branches with --trunk-only"
1861 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1863 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1864 ('/%(trunk)s/proj', 'A'),
1865 ('/%(trunk)s/proj/a.txt', 'A'),
1866 ('/%(trunk)s/proj/b.txt', 'A'),
1867 ('/%(trunk)s/proj/c.txt', 'A'),
1868 ('/%(trunk)s/proj/d.txt', 'A'),
1869 ('/%(trunk)s/proj/e.txt', 'A'),
1870 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1873 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1874 ('/%(trunk)s/proj/a.txt', 'M'),
1875 ('/%(trunk)s/proj/b.txt', 'M'),
1876 ('/%(trunk)s/proj/c.txt', 'M'),
1877 ('/%(trunk)s/proj/d.txt', 'M'),
1878 ('/%(trunk)s/proj/e.txt', 'M'),
1879 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1882 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1883 ('/%(trunk)s/proj/a.txt', 'M'),
1884 ('/%(trunk)s/proj/b.txt', 'M'),
1885 ('/%(trunk)s/proj/c.txt', 'M'),
1886 ('/%(trunk)s/proj/d.txt', 'M'),
1887 ('/%(trunk)s/proj/e.txt', 'M'),
1888 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1891 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1892 ('/%(trunk)s/proj/a.txt', 'M'),
1895 conv.logs[6].check("Add a file to the working copy.", (
1896 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1899 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1900 ('/%(trunk)s/proj/b.txt', 'M'),
1901 ('/%(trunk)s/proj/c.txt', 'M'),
1902 ('/%(trunk)s/proj/d.txt', 'M'),
1903 ('/%(trunk)s/proj/e.txt', 'M'),
1904 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1908 def default_branch_and_1_2():
1909 "do not allow 1.2 revision with default branch"
1911 conv = ensure_conversion(
1912 'default-branch-and-1-2',
1913 error_re=(
1914 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
1919 def compose_tag_three_sources():
1920 "compose a tag from three sources"
1921 conv = ensure_conversion('compose-tag-three-sources')
1923 conv.logs[2].check("Add on trunk", (
1924 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
1925 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
1926 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
1927 ('/%(trunk)s/tagged-on-b1', 'A'),
1928 ('/%(trunk)s/tagged-on-b2', 'A'),
1931 conv.logs[3].check(sym_log_msg('b1'), (
1932 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
1935 conv.logs[4].check(sym_log_msg('b2'), (
1936 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
1939 conv.logs[5].check("Commit on branch b1", (
1940 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
1941 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
1942 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
1943 ('/%(branches)s/b1/tagged-on-b1', 'M'),
1944 ('/%(branches)s/b1/tagged-on-b2', 'M'),
1947 conv.logs[6].check("Commit on branch b2", (
1948 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
1949 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
1950 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
1951 ('/%(branches)s/b2/tagged-on-b1', 'M'),
1952 ('/%(branches)s/b2/tagged-on-b2', 'M'),
1955 conv.logs[7].check("Commit again on trunk", (
1956 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
1957 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
1958 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
1959 ('/%(trunk)s/tagged-on-b1', 'M'),
1960 ('/%(trunk)s/tagged-on-b2', 'M'),
1963 conv.logs[8].check(sym_log_msg('T',1), (
1964 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
1965 ('/%(tags)s/T/tagged-on-trunk-1.1 '
1966 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
1967 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
1968 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
1972 def pass5_when_to_fill():
1973 "reserve a svn revnum for a fill only when required"
1974 # The conversion will fail if the bug is present, and
1975 # ensure_conversion would raise Failure.
1976 conv = ensure_conversion('pass5-when-to-fill')
1979 class EmptyTrunk(Cvs2SvnTestCase):
1980 "don't break when the trunk is empty"
1982 def __init__(self, **kw):
1983 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
1985 def run(self):
1986 # The conversion will fail if the bug is present, and
1987 # ensure_conversion would raise Failure.
1988 conv = self.ensure_conversion()
1991 def no_spurious_svn_commits():
1992 "ensure that we don't create any spurious commits"
1993 conv = ensure_conversion('phoenix')
1995 # Check spurious commit that could be created in
1996 # SVNCommitCreator._pre_commit()
1998 # (When you add a file on a branch, CVS creates a trunk revision
1999 # in state 'dead'. If the log message of that commit is equal to
2000 # the one that CVS generates, we do not ever create a 'fill'
2001 # SVNCommit for it.)
2003 # and spurious commit that could be created in
2004 # SVNCommitCreator._commit()
2006 # (When you add a file on a branch, CVS creates a trunk revision
2007 # in state 'dead'. If the log message of that commit is equal to
2008 # the one that CVS generates, we do not create a primary SVNCommit
2009 # for it.)
2010 conv.logs[17].check('File added on branch xiphophorus', (
2011 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2014 # Check to make sure that a commit *is* generated:
2015 # (When you add a file on a branch, CVS creates a trunk revision
2016 # in state 'dead'. If the log message of that commit is NOT equal
2017 # to the one that CVS generates, we create a primary SVNCommit to
2018 # serve as a home for the log message in question.
2019 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2020 + 'branch xiphophorus,\nand this log message was tweaked', ())
2022 # Check spurious commit that could be created in
2023 # SVNCommitCreator._commit_symbols().
2024 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2025 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2029 class PeerPathPruning(Cvs2SvnTestCase):
2030 "make sure that filling prunes paths correctly"
2032 def __init__(self, **kw):
2033 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2035 def run(self):
2036 conv = self.ensure_conversion()
2037 conv.logs[6].check(sym_log_msg('BRANCH'), (
2038 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2039 ('/%(branches)s/BRANCH/bar', 'D'),
2040 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2044 def invalid_closings_on_trunk():
2045 "verify correct revs are copied to default branches"
2046 # The conversion will fail if the bug is present, and
2047 # ensure_conversion would raise Failure.
2048 conv = ensure_conversion('invalid-closings-on-trunk')
2051 def individual_passes():
2052 "run each pass individually"
2053 conv = ensure_conversion('main')
2054 conv2 = ensure_conversion('main', passbypass=1)
2056 if conv.logs != conv2.logs:
2057 raise Failure()
2060 def resync_bug():
2061 "reveal a big bug in our resync algorithm"
2062 # This will fail if the bug is present
2063 conv = ensure_conversion('resync-bug')
2066 def branch_from_default_branch():
2067 "reveal a bug in our default branch detection code"
2068 conv = ensure_conversion('branch-from-default-branch')
2070 # This revision will be a default branch synchronization only
2071 # if cvs2svn is correctly determining default branch revisions.
2073 # The bug was that cvs2svn was treating revisions on branches off of
2074 # default branches as default branch revisions, resulting in
2075 # incorrectly regarding the branch off of the default branch as a
2076 # non-trunk default branch. Crystal clear? I thought so. See
2077 # issue #42 for more incoherent blathering.
2078 conv.logs[5].check("This commit was generated by cvs2svn", (
2079 ('/%(trunk)s/proj/file.txt '
2080 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2084 def file_in_attic_too():
2085 "die if a file exists in and out of the attic"
2086 ensure_conversion(
2087 'file-in-attic-too',
2088 error_re=(
2089 r'.*A CVS repository cannot contain both '
2090 r'(.*)' + re.escape(os.sep) + r'(.*) '
2091 + r'and '
2092 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2097 def retain_file_in_attic_too():
2098 "test --retain-conflicting-attic-files option"
2099 conv = ensure_conversion(
2100 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2101 if not conv.path_exists('trunk', 'file.txt'):
2102 raise Failure()
2103 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2104 raise Failure()
2107 def symbolic_name_filling_guide():
2108 "reveal a big bug in our SymbolFillingGuide"
2109 # This will fail if the bug is present
2110 conv = ensure_conversion('symbolic-name-overfill')
2113 # Helpers for tests involving file contents and properties.
2115 class NodeTreeWalkException:
2116 "Exception class for node tree traversals."
2117 pass
2119 def node_for_path(node, path):
2120 "In the tree rooted under SVNTree NODE, return the node at PATH."
2121 if node.name != '__SVN_ROOT_NODE':
2122 raise NodeTreeWalkException()
2123 path = path.strip('/')
2124 components = path.split('/')
2125 for component in components:
2126 node = get_child(node, component)
2127 return node
2129 # Helper for tests involving properties.
2130 def props_for_path(node, path):
2131 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2132 return node_for_path(node, path).props
2135 class EOLMime(Cvs2SvnPropertiesTestCase):
2136 """eol settings and mime types together
2138 The files are as follows:
2140 trunk/foo.txt: no -kb, mime file says nothing.
2141 trunk/foo.xml: no -kb, mime file says text.
2142 trunk/foo.zip: no -kb, mime file says non-text.
2143 trunk/foo.bin: has -kb, mime file says nothing.
2144 trunk/foo.csv: has -kb, mime file says text.
2145 trunk/foo.dbf: has -kb, mime file says non-text.
2148 def __init__(self, args, **kw):
2149 # TODO: It's a bit klugey to construct this path here. But so far
2150 # there's only one test with a mime.types file. If we have more,
2151 # we should abstract this into some helper, which would be located
2152 # near ensure_conversion(). Note that it is a convention of this
2153 # test suite for a mime.types file to be located in the top level
2154 # of the CVS repository to which it applies.
2155 self.mime_path = os.path.join(
2156 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2158 Cvs2SvnPropertiesTestCase.__init__(
2159 self, 'eol-mime',
2160 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2161 args=['--mime-types=%s' % self.mime_path] + args,
2162 **kw)
2165 # We do four conversions. Each time, we pass --mime-types=FILE with
2166 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2167 # Thus there's one conversion with neither flag, one with just the
2168 # former, one with just the latter, and one with both.
2171 # Neither --no-default-eol nor --eol-from-mime-type:
2172 eol_mime1 = EOLMime(
2173 variant=1,
2174 args=[],
2175 expected_props=[
2176 ('trunk/foo.txt', [None, None, None]),
2177 ('trunk/foo.xml', [None, 'text/xml', None]),
2178 ('trunk/foo.zip', [None, 'application/zip', None]),
2179 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2180 ('trunk/foo.csv', [None, 'text/csv', None]),
2181 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2185 # Just --no-default-eol, not --eol-from-mime-type:
2186 eol_mime2 = EOLMime(
2187 variant=2,
2188 args=['--default-eol=native'],
2189 expected_props=[
2190 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2191 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2192 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2193 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2194 ('trunk/foo.csv', [None, 'text/csv', None]),
2195 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2199 # Just --eol-from-mime-type, not --no-default-eol:
2200 eol_mime3 = EOLMime(
2201 variant=3,
2202 args=['--eol-from-mime-type'],
2203 expected_props=[
2204 ('trunk/foo.txt', [None, None, None]),
2205 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2206 ('trunk/foo.zip', [None, 'application/zip', None]),
2207 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2208 ('trunk/foo.csv', [None, 'text/csv', None]),
2209 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2213 # Both --no-default-eol and --eol-from-mime-type:
2214 eol_mime4 = EOLMime(
2215 variant=4,
2216 args=['--eol-from-mime-type', '--default-eol=native'],
2217 expected_props=[
2218 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2219 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2220 ('trunk/foo.zip', [None, 'application/zip', None]),
2221 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2222 ('trunk/foo.csv', [None, 'text/csv', None]),
2223 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2227 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2228 'eol-mime',
2229 description='test non-setting of cvs2svn:cvs-rev property',
2230 args=[],
2231 props_to_test=['cvs2svn:cvs-rev'],
2232 expected_props=[
2233 ('trunk/foo.txt', [None]),
2234 ('trunk/foo.xml', [None]),
2235 ('trunk/foo.zip', [None]),
2236 ('trunk/foo.bin', [None]),
2237 ('trunk/foo.csv', [None]),
2238 ('trunk/foo.dbf', [None]),
2242 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2243 'eol-mime',
2244 description='test setting of cvs2svn:cvs-rev property',
2245 args=['--cvs-revnums'],
2246 props_to_test=['cvs2svn:cvs-rev'],
2247 expected_props=[
2248 ('trunk/foo.txt', ['1.2']),
2249 ('trunk/foo.xml', ['1.2']),
2250 ('trunk/foo.zip', ['1.2']),
2251 ('trunk/foo.bin', ['1.2']),
2252 ('trunk/foo.csv', ['1.2']),
2253 ('trunk/foo.dbf', ['1.2']),
2257 keywords = Cvs2SvnPropertiesTestCase(
2258 'keywords',
2259 description='test setting of svn:keywords property among others',
2260 args=['--default-eol=native'],
2261 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2262 expected_props=[
2263 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2264 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2265 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2266 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2267 ('trunk/foo.kk', [None, 'native', None]),
2268 ('trunk/foo.ko', [None, 'native', None]),
2269 ('trunk/foo.kv', [None, 'native', None]),
2273 def ignore():
2274 "test setting of svn:ignore property"
2275 conv = ensure_conversion('cvsignore')
2276 wc_tree = conv.get_wc_tree()
2277 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2278 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2280 if topdir_props['svn:ignore'] != \
2281 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2282 raise Failure()
2284 if subdir_props['svn:ignore'] != \
2285 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2286 raise Failure()
2289 def requires_cvs():
2290 "test that CVS can still do what RCS can't"
2291 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2292 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2294 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2295 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2297 if atsign_contents[-1:] == "@":
2298 raise Failure()
2299 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2300 raise Failure()
2302 if not (conv.logs[21].author == "William Lyon Phelps III" and
2303 conv.logs[20].author == "j random"):
2304 raise Failure()
2307 def questionable_branch_names():
2308 "test that we can handle weird branch names"
2309 conv = ensure_conversion('questionable-symbols')
2310 # If the conversion succeeds, then we're okay. We could check the
2311 # actual branch paths, too, but the main thing is to know that the
2312 # conversion doesn't fail.
2315 def questionable_tag_names():
2316 "test that we can handle weird tag names"
2317 conv = ensure_conversion('questionable-symbols')
2318 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2319 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2321 conv.find_tag_log('TagWith/Backslash_E').check(
2322 sym_log_msg('TagWith/Backslash_E',1),
2324 ('/%(tags)s/TagWith', 'A'),
2325 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2328 conv.find_tag_log('TagWith/Slash_Z').check(
2329 sym_log_msg('TagWith/Slash_Z',1),
2331 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2336 def revision_reorder_bug():
2337 "reveal a bug that reorders file revisions"
2338 conv = ensure_conversion('revision-reorder-bug')
2339 # If the conversion succeeds, then we're okay. We could check the
2340 # actual revisions, too, but the main thing is to know that the
2341 # conversion doesn't fail.
2344 def exclude():
2345 "test that exclude really excludes everything"
2346 conv = ensure_conversion('main', args=['--exclude=.*'])
2347 for log in conv.logs.values():
2348 for item in log.changed_paths.keys():
2349 if item.startswith('/branches/') or item.startswith('/tags/'):
2350 raise Failure()
2353 def vendor_branch_delete_add():
2354 "add trunk file that was deleted on vendor branch"
2355 # This will error if the bug is present
2356 conv = ensure_conversion('vendor-branch-delete-add')
2359 def resync_pass2_pull_forward():
2360 "ensure pass2 doesn't pull rev too far forward"
2361 conv = ensure_conversion('resync-pass2-pull-forward')
2362 # If the conversion succeeds, then we're okay. We could check the
2363 # actual revisions, too, but the main thing is to know that the
2364 # conversion doesn't fail.
2367 def native_eol():
2368 "only LFs for svn:eol-style=native files"
2369 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2370 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2371 conv.repos)
2372 # Verify that all files in the dump have LF EOLs. We're actually
2373 # testing the whole dump file, but the dump file itself only uses
2374 # LF EOLs, so we're safe.
2375 for line in lines:
2376 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2377 raise Failure()
2380 def double_fill():
2381 "reveal a bug that created a branch twice"
2382 conv = ensure_conversion('double-fill')
2383 # If the conversion succeeds, then we're okay. We could check the
2384 # actual revisions, too, but the main thing is to know that the
2385 # conversion doesn't fail.
2388 def double_fill2():
2389 "reveal a second bug that created a branch twice"
2390 conv = ensure_conversion('double-fill2')
2391 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2392 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2393 try:
2394 # This check should fail:
2395 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2396 except Failure:
2397 pass
2398 else:
2399 raise Failure('Symbol filled twice in a row')
2402 def resync_pass2_push_backward():
2403 "ensure pass2 doesn't push rev too far backward"
2404 conv = ensure_conversion('resync-pass2-push-backward')
2405 # If the conversion succeeds, then we're okay. We could check the
2406 # actual revisions, too, but the main thing is to know that the
2407 # conversion doesn't fail.
2410 def double_add():
2411 "reveal a bug that added a branch file twice"
2412 conv = ensure_conversion('double-add')
2413 # If the conversion succeeds, then we're okay. We could check the
2414 # actual revisions, too, but the main thing is to know that the
2415 # conversion doesn't fail.
2418 def bogus_branch_copy():
2419 "reveal a bug that copies a branch file wrongly"
2420 conv = ensure_conversion('bogus-branch-copy')
2421 # If the conversion succeeds, then we're okay. We could check the
2422 # actual revisions, too, but the main thing is to know that the
2423 # conversion doesn't fail.
2426 def nested_ttb_directories():
2427 "require error if ttb directories are not disjoint"
2428 opts_list = [
2429 {'trunk' : 'a', 'branches' : 'a',},
2430 {'trunk' : 'a', 'tags' : 'a',},
2431 {'branches' : 'a', 'tags' : 'a',},
2432 # This option conflicts with the default trunk path:
2433 {'branches' : 'trunk',},
2434 # Try some nested directories:
2435 {'trunk' : 'a', 'branches' : 'a/b',},
2436 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2437 {'branches' : 'a', 'tags' : 'a/b',},
2440 for opts in opts_list:
2441 ensure_conversion(
2442 'main', error_re=r'The following paths are not disjoint\:', **opts
2446 class AutoProps(Cvs2SvnPropertiesTestCase):
2447 """Test auto-props.
2449 The files are as follows:
2451 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2452 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2453 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2454 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2455 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2456 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2457 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2458 trunk/foo.UPCASE1: no -kb, no mime type.
2459 trunk/foo.UPCASE2: no -kb, no mime type.
2462 def __init__(self, args, **kw):
2463 ### TODO: It's a bit klugey to construct this path here. See also
2464 ### the comment in eol_mime().
2465 auto_props_path = os.path.join(
2466 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2468 Cvs2SvnPropertiesTestCase.__init__(
2469 self, 'eol-mime',
2470 props_to_test=[
2471 'myprop',
2472 'svn:eol-style',
2473 'svn:mime-type',
2474 'svn:keywords',
2475 'svn:executable',
2477 args=[
2478 '--auto-props=%s' % auto_props_path,
2479 '--eol-from-mime-type'
2480 ] + args,
2481 **kw)
2484 auto_props_ignore_case = AutoProps(
2485 description="test auto-props",
2486 args=['--default-eol=native'],
2487 expected_props=[
2488 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2489 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2490 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2491 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2492 ('trunk/foo.bin',
2493 ['bin', None, 'application/octet-stream', None, '']),
2494 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2495 ('trunk/foo.dbf',
2496 ['dbf', None, 'application/what-is-dbf', None, None]),
2497 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2498 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2502 def ctrl_char_in_filename():
2503 "do not allow control characters in filenames"
2505 try:
2506 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2507 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2508 if os.path.exists(dstrepos_path):
2509 safe_rmtree(dstrepos_path)
2511 # create repos from existing main repos
2512 shutil.copytree(srcrepos_path, dstrepos_path)
2513 base_path = os.path.join(dstrepos_path, 'single-files')
2514 try:
2515 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2516 os.path.join(base_path, 'two\rquick,v'))
2517 except:
2518 # Operating systems that don't allow control characters in
2519 # filenames will hopefully have thrown an exception; in that
2520 # case, just skip this test.
2521 raise svntest.Skip()
2523 conv = ensure_conversion(
2524 'ctrl-char-filename',
2525 error_re=(r'.*Character .* in filename .* '
2526 r'is not supported by Subversion\.'),
2528 finally:
2529 safe_rmtree(dstrepos_path)
2532 def commit_dependencies():
2533 "interleaved and multi-branch commits to same files"
2534 conv = ensure_conversion("commit-dependencies")
2535 conv.logs[2].check('adding', (
2536 ('/%(trunk)s/interleaved', 'A'),
2537 ('/%(trunk)s/interleaved/file1', 'A'),
2538 ('/%(trunk)s/interleaved/file2', 'A'),
2540 conv.logs[3].check('big commit', (
2541 ('/%(trunk)s/interleaved/file1', 'M'),
2542 ('/%(trunk)s/interleaved/file2', 'M'),
2544 conv.logs[4].check('dependant small commit', (
2545 ('/%(trunk)s/interleaved/file1', 'M'),
2547 conv.logs[5].check('adding', (
2548 ('/%(trunk)s/multi-branch', 'A'),
2549 ('/%(trunk)s/multi-branch/file1', 'A'),
2550 ('/%(trunk)s/multi-branch/file2', 'A'),
2552 conv.logs[6].check(sym_log_msg("branch"), (
2553 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2554 ('/%(branches)s/branch/interleaved', 'D'),
2556 conv.logs[7].check('multi-branch-commit', (
2557 ('/%(trunk)s/multi-branch/file1', 'M'),
2558 ('/%(trunk)s/multi-branch/file2', 'M'),
2559 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2560 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2564 def double_branch_delete():
2565 "fill branches before modifying files on them"
2566 conv = ensure_conversion('double-branch-delete')
2568 # Test for issue #102. The file IMarshalledValue.java is branched,
2569 # deleted, readded on the branch, and then deleted again. If the
2570 # fill for the file on the branch is postponed until after the
2571 # modification, the file will end up live on the branch instead of
2572 # dead! Make sure it happens at the right time.
2574 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2575 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2578 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2579 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2583 def symbol_mismatches():
2584 "error for conflicting tag/branch"
2586 ensure_conversion(
2587 'symbol-mess',
2588 args=['--symbol-default=strict'],
2589 error_re=r'.*Problems determining how symbols should be converted',
2593 def overlook_symbol_mismatches():
2594 "overlook conflicting tag/branch when --trunk-only"
2596 # This is a test for issue #85.
2598 ensure_conversion('symbol-mess', args=['--trunk-only'])
2601 def force_symbols():
2602 "force symbols to be tags/branches"
2604 conv = ensure_conversion(
2605 'symbol-mess',
2606 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2607 if conv.path_exists('tags', 'BRANCH') \
2608 or not conv.path_exists('branches', 'BRANCH'):
2609 raise Failure()
2610 if not conv.path_exists('tags', 'TAG') \
2611 or conv.path_exists('branches', 'TAG'):
2612 raise Failure()
2613 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2614 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2615 raise Failure()
2616 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2617 or conv.path_exists('branches', 'MOSTLY_TAG'):
2618 raise Failure()
2621 def commit_blocks_tags():
2622 "commit prevents forced tag"
2624 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2625 ensure_conversion(
2626 'symbol-mess',
2627 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2628 error_re=(
2629 r'.*The following branches cannot be forced to be tags '
2630 r'because they have commits'
2635 def blocked_excludes():
2636 "error for blocked excludes"
2638 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2639 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2640 try:
2641 ensure_conversion(
2642 'symbol-mess',
2643 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2644 raise MissingErrorException()
2645 except Failure:
2646 pass
2649 def unblock_blocked_excludes():
2650 "excluding blocker removes blockage"
2652 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2653 for blocker in ['BRANCH', 'COMMIT']:
2654 ensure_conversion(
2655 'symbol-mess',
2656 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2657 '--exclude=BLOCKING_%s' % blocker]))
2660 def regexp_force_symbols():
2661 "force symbols via regular expressions"
2663 conv = ensure_conversion(
2664 'symbol-mess',
2665 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2666 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2667 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2668 raise Failure()
2669 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2670 or conv.path_exists('branches', 'MOSTLY_TAG'):
2671 raise Failure()
2674 def heuristic_symbol_default():
2675 "test 'heuristic' symbol default"
2677 conv = ensure_conversion(
2678 'symbol-mess', args=['--symbol-default=heuristic'])
2679 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2680 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2681 raise Failure()
2682 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2683 or conv.path_exists('branches', 'MOSTLY_TAG'):
2684 raise Failure()
2687 def branch_symbol_default():
2688 "test 'branch' symbol default"
2690 conv = ensure_conversion(
2691 'symbol-mess', args=['--symbol-default=branch'])
2692 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2693 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2694 raise Failure()
2695 if conv.path_exists('tags', 'MOSTLY_TAG') \
2696 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2697 raise Failure()
2700 def tag_symbol_default():
2701 "test 'tag' symbol default"
2703 conv = ensure_conversion(
2704 'symbol-mess', args=['--symbol-default=tag'])
2705 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2706 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2707 raise Failure()
2708 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2709 or conv.path_exists('branches', 'MOSTLY_TAG'):
2710 raise Failure()
2713 def symbol_transform():
2714 "test --symbol-transform"
2716 conv = ensure_conversion(
2717 'symbol-mess',
2718 args=[
2719 '--symbol-default=heuristic',
2720 '--symbol-transform=BRANCH:branch',
2721 '--symbol-transform=TAG:tag',
2722 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2724 if not conv.path_exists('branches', 'branch'):
2725 raise Failure()
2726 if not conv.path_exists('tags', 'tag'):
2727 raise Failure()
2728 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2729 raise Failure()
2730 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2731 raise Failure()
2734 def write_symbol_info():
2735 "test --write-symbol-info"
2737 expected_lines = [
2738 ['0', '.trunk.',
2739 'trunk', 'trunk', '.'],
2740 ['0', 'BLOCKED_BY_UNNAMED',
2741 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2742 ['0', 'BLOCKING_COMMIT',
2743 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2744 ['0', 'BLOCKED_BY_COMMIT',
2745 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2746 ['0', 'BLOCKING_BRANCH',
2747 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2748 ['0', 'BLOCKED_BY_BRANCH',
2749 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2750 ['0', 'MOSTLY_BRANCH',
2751 '.', '.', '.'],
2752 ['0', 'MOSTLY_TAG',
2753 '.', '.', '.'],
2754 ['0', 'BRANCH_WITH_COMMIT',
2755 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2756 ['0', 'BRANCH',
2757 'branch', 'branches/BRANCH', '.trunk.'],
2758 ['0', 'TAG',
2759 'tag', 'tags/TAG', '.trunk.'],
2760 ['0', 'unlabeled-1.1.12.1.2',
2761 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2763 expected_lines.sort()
2765 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2766 try:
2767 ensure_conversion(
2768 'symbol-mess',
2769 args=[
2770 '--symbol-default=strict',
2771 '--write-symbol-info=%s' % (symbol_info_file,),
2772 '--passes=:CollateSymbolsPass',
2775 raise MissingErrorException()
2776 except Failure:
2777 pass
2778 lines = []
2779 comment_re = re.compile(r'^\s*\#')
2780 for l in open(symbol_info_file, 'r'):
2781 if comment_re.match(l):
2782 continue
2783 lines.append(l.strip().split())
2784 lines.sort()
2785 if lines != expected_lines:
2786 s = ['Symbol info incorrect\n']
2787 differ = Differ()
2788 for diffline in differ.compare(
2789 [' '.join(line) + '\n' for line in expected_lines],
2790 [' '.join(line) + '\n' for line in lines],
2792 s.append(diffline)
2793 raise Failure(''.join(s))
2796 def symbol_hints():
2797 "test --symbol-hints for setting branch/tag"
2799 conv = ensure_conversion(
2800 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2802 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2803 raise Failure()
2804 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2805 raise Failure()
2806 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2807 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2809 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2810 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2812 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2813 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2817 def parent_hints():
2818 "test --symbol-hints for setting parent"
2820 conv = ensure_conversion(
2821 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2823 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2824 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2828 def parent_hints_invalid():
2829 "test --symbol-hints with an invalid parent"
2831 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2832 # this symbol hints file sets the preferred parent to BRANCH
2833 # instead:
2834 conv = ensure_conversion(
2835 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2836 error_re=(
2837 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2842 def parent_hints_wildcards():
2843 "test --symbol-hints wildcards"
2845 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2846 # this symbol hints file sets the preferred parent to BRANCH
2847 # instead:
2848 conv = ensure_conversion(
2849 'symbol-mess',
2850 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2852 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2853 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2857 def path_hints():
2858 "test --symbol-hints for setting svn paths"
2860 conv = ensure_conversion(
2861 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
2863 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2864 ('/trunk', 'A'),
2865 ('/a', 'A'),
2866 ('/a/strange', 'A'),
2867 ('/a/strange/trunk', 'A'),
2868 ('/a/strange/trunk/path', 'A'),
2869 ('/branches', 'A'),
2870 ('/tags', 'A'),
2872 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2873 ('/special', 'A'),
2874 ('/special/tag', 'A'),
2875 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
2877 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2878 ('/special/other', 'A'),
2879 ('/special/other/branch', 'A'),
2880 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
2882 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2883 ('/special/branch', 'A'),
2884 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
2888 def issue_99():
2889 "test problem from issue 99"
2891 conv = ensure_conversion('issue-99')
2894 def issue_100():
2895 "test problem from issue 100"
2897 conv = ensure_conversion('issue-100')
2898 file1 = conv.get_wc('trunk', 'file1.txt')
2899 if file(file1).read() != 'file1.txt<1.2>\n':
2900 raise Failure()
2903 def issue_106():
2904 "test problem from issue 106"
2906 conv = ensure_conversion('issue-106')
2909 def options_option():
2910 "use of the --options option"
2912 conv = ensure_conversion('main', options_file='cvs2svn.options')
2915 def multiproject():
2916 "multiproject conversion"
2918 conv = ensure_conversion(
2919 'main', options_file='cvs2svn-multiproject.options'
2921 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2922 ('/partial-prune', 'A'),
2923 ('/partial-prune/trunk', 'A'),
2924 ('/partial-prune/branches', 'A'),
2925 ('/partial-prune/tags', 'A'),
2926 ('/partial-prune/releases', 'A'),
2930 def crossproject():
2931 "multiproject conversion with cross-project commits"
2933 conv = ensure_conversion(
2934 'main', options_file='cvs2svn-crossproject.options'
2938 def tag_with_no_revision():
2939 "tag defined but revision is deleted"
2941 conv = ensure_conversion('tag-with-no-revision')
2944 def delete_cvsignore():
2945 "svn:ignore should vanish when .cvsignore does"
2947 # This is issue #81.
2949 conv = ensure_conversion('delete-cvsignore')
2951 wc_tree = conv.get_wc_tree()
2952 props = props_for_path(wc_tree, 'trunk/proj')
2954 if props.has_key('svn:ignore'):
2955 raise Failure()
2958 def repeated_deltatext():
2959 "ignore repeated deltatext blocks with warning"
2961 conv = ensure_conversion('repeated-deltatext')
2962 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
2963 if not conv.output_found(warning_re):
2964 raise Failure()
2967 def nasty_graphs():
2968 "process some nasty dependency graphs"
2970 # It's not how well the bear can dance, but that the bear can dance
2971 # at all:
2972 conv = ensure_conversion('nasty-graphs')
2975 def tagging_after_delete():
2976 "optimal tag after deleting files"
2978 conv = ensure_conversion('tagging-after-delete')
2980 # tag should be 'clean', no deletes
2981 log = conv.find_tag_log('tag1')
2982 expected = (
2983 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
2985 log.check_changes(expected)
2988 def crossed_branches():
2989 "branches created in inconsistent orders"
2991 conv = ensure_conversion('crossed-branches')
2994 def file_directory_conflict():
2995 "error when filename conflicts with directory name"
2997 conv = ensure_conversion(
2998 'file-directory-conflict',
2999 error_re=r'.*Directory name conflicts with filename',
3003 def attic_directory_conflict():
3004 "error when attic filename conflicts with dirname"
3006 # This tests the problem reported in issue #105.
3008 conv = ensure_conversion(
3009 'attic-directory-conflict',
3010 error_re=r'.*Directory name conflicts with filename',
3014 def internal_co():
3015 "verify that --use-internal-co works"
3017 rcs_conv = ensure_conversion(
3018 'main', args=['--use-rcs', '--default-eol=native'],
3020 conv = ensure_conversion(
3021 'main', args=['--default-eol=native'],
3023 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3024 raise Failure()
3025 rcs_lines = run_program(
3026 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3027 rcs_conv.repos)
3028 lines = run_program(
3029 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3030 conv.repos)
3031 # Compare all lines following the repository UUID:
3032 if lines[3:] != rcs_lines[3:]:
3033 raise Failure()
3036 def internal_co_exclude():
3037 "verify that --use-internal-co --exclude=... works"
3039 rcs_conv = ensure_conversion(
3040 'internal-co',
3041 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3043 conv = ensure_conversion(
3044 'internal-co',
3045 args=['--exclude=BRANCH', '--default-eol=native'],
3047 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3048 raise Failure()
3049 rcs_lines = run_program(
3050 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3051 rcs_conv.repos)
3052 lines = run_program(
3053 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3054 conv.repos)
3055 # Compare all lines following the repository UUID:
3056 if lines[3:] != rcs_lines[3:]:
3057 raise Failure()
3060 def internal_co_trunk_only():
3061 "verify that --use-internal-co --trunk-only works"
3063 conv = ensure_conversion(
3064 'internal-co',
3065 args=['--trunk-only', '--default-eol=native'],
3067 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3068 raise Failure()
3071 def leftover_revs():
3072 "check for leftover checked-out revisions"
3074 conv = ensure_conversion(
3075 'leftover-revs',
3076 args=['--exclude=BRANCH', '--default-eol=native'],
3078 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3079 raise Failure()
3082 def requires_internal_co():
3083 "test that internal co can do more than RCS"
3084 # See issues 4, 11 for the bugs whose regression we're testing for.
3085 # Unlike in requires_cvs above, issue 29 is not covered.
3086 conv = ensure_conversion('requires-cvs')
3088 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3090 if atsign_contents[-1:] == "@":
3091 raise Failure()
3093 if not (conv.logs[21].author == "William Lyon Phelps III" and
3094 conv.logs[20].author == "j random"):
3095 raise Failure()
3098 def internal_co_keywords():
3099 "test that internal co handles keywords correctly"
3100 conv_ic = ensure_conversion('internal-co-keywords',
3101 args=["--keywords-off"])
3102 conv_cvs = ensure_conversion('internal-co-keywords',
3103 args=["--use-cvs", "--keywords-off"])
3105 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3106 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3107 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3108 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3109 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3110 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3112 if ko_ic != ko_cvs:
3113 raise Failure()
3114 if kk_ic != kk_cvs:
3115 raise Failure()
3117 # The date format changed between cvs and co ('/' instead of '-').
3118 # Accept either one:
3119 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3120 if kv_ic != kv_cvs \
3121 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3122 raise Failure()
3125 def timestamp_chaos():
3126 "test timestamp adjustments"
3128 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3130 times = [
3131 '2007-01-01 21:00:00', # Initial commit
3132 '2007-01-01 21:00:00', # revision 1.1 of both files
3133 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3134 '2007-01-01 21:00:02', # revision 1.2 of file1.txt, adjusted backwards
3135 '2007-01-01 22:00:00', # revision 1.3 of both files
3137 for i in range(len(times)):
3138 if abs(conv.logs[i + 1].date - time.mktime(svn_strptime(times[i]))) > 0.1:
3139 raise Failure()
3142 def symlinks():
3143 "convert a repository that contains symlinks"
3145 # This is a test for issue #97.
3147 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3148 links = [
3150 os.path.join('..', 'file.txt,v'),
3151 os.path.join(proj, 'dir1', 'file.txt,v'),
3154 'dir1',
3155 os.path.join(proj, 'dir2'),
3159 try:
3160 os.symlink
3161 except AttributeError:
3162 # Apparently this OS doesn't support symlinks, so skip test.
3163 raise svntest.Skip()
3165 try:
3166 for (src,dst) in links:
3167 os.symlink(src, dst)
3169 conv = ensure_conversion('symlinks')
3170 conv.logs[2].check('', (
3171 ('/%(trunk)s/proj', 'A'),
3172 ('/%(trunk)s/proj/file.txt', 'A'),
3173 ('/%(trunk)s/proj/dir1', 'A'),
3174 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3175 ('/%(trunk)s/proj/dir2', 'A'),
3176 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3178 finally:
3179 for (src,dst) in links:
3180 os.remove(dst)
3183 def empty_trunk_path():
3184 "allow --trunk to be empty if --trunk-only"
3186 # This is a test for issue #53.
3188 conv = ensure_conversion(
3189 'main', args=['--trunk-only', '--trunk='],
3193 def preferred_parent_cycle():
3194 "handle a cycle in branch parent preferences"
3196 conv = ensure_conversion('preferred-parent-cycle')
3199 def branch_from_empty_dir():
3200 "branch from an empty directory"
3202 conv = ensure_conversion('branch-from-empty-dir')
3205 def trunk_readd():
3206 "add a file on a branch then on trunk"
3208 conv = ensure_conversion('trunk-readd')
3211 def branch_from_deleted_1_1():
3212 "branch from a 1.1 revision that will be deleted"
3214 conv = ensure_conversion('branch-from-deleted-1-1')
3215 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3216 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3218 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3219 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3221 conv.logs[7].check('Adding b.txt:1.2', (
3222 ('/%(trunk)s/proj/b.txt', 'A'),
3225 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3226 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3228 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3229 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3233 def add_on_branch():
3234 "add a file on a branch using newer CVS"
3236 conv = ensure_conversion('add-on-branch')
3237 conv.logs[6].check('Adding b.txt:1.1', (
3238 ('/%(trunk)s/proj/b.txt', 'A'),
3240 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3241 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3243 conv.logs[8].check('Adding c.txt:1.1', (
3244 ('/%(trunk)s/proj/c.txt', 'A'),
3246 conv.logs[9].check('Removing c.txt:1.2', (
3247 ('/%(trunk)s/proj/c.txt', 'D'),
3249 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3250 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3252 conv.logs[11].check('Adding d.txt:1.1', (
3253 ('/%(trunk)s/proj/d.txt', 'A'),
3255 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3256 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3260 def main_git():
3261 "test output in git-fast-import format"
3263 # Note: To test importing into git, do
3265 # ./run-tests <test-number>
3266 # rm -rf .git
3267 # git-init
3268 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3270 # Or, to load the dumpfiles separately:
3272 # cat cvs2svn-tmp/git-blob.dat \
3273 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3274 # cat cvs2svn-tmp/git-dump.dat \
3275 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3277 # Then use "gitk --all", "git log", etc. to test the contents of the
3278 # repository.
3280 # We don't have the infrastructure to check that the resulting git
3281 # repository is correct, so we just check that the conversion runs
3282 # to completion:
3283 conv = GitConversion('main', None, [
3284 '--blobfile=cvs2svn-tmp/blobfile.out',
3285 '--dumpfile=cvs2svn-tmp/dumpfile.out',
3286 '--username=cvs2git',
3287 'test-data/main-cvsrepos',
3291 def git_options():
3292 "test cvs2git using options file"
3294 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3297 def main_hg():
3298 "output in git-fast-import format with inline data"
3300 # The output should be suitable for import by Mercurial.
3302 # Note: To test importing into git, do
3304 # ./run-tests <test-number>
3305 # rm -rf .git
3306 # git-init
3307 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3309 # Then use "gitk --all", "git log", etc. to test the contents of the
3310 # repository.
3312 # We don't have the infrastructure to check that the resulting
3313 # Mercurial repository is correct, so we just check that the
3314 # conversion runs to completion:
3315 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3318 def invalid_symbol():
3319 "a symbol with the incorrect format"
3321 conv = ensure_conversion('invalid-symbol')
3322 if not conv.output_found(
3323 r".*branch 'SYMBOL' references invalid revision 1$"
3325 raise Failure()
3328 def invalid_symbol_ignore():
3329 "ignore a symbol with the incorrect format"
3331 conv = ensure_conversion(
3332 'invalid-symbol', options_file='cvs2svn-ignore.options'
3336 class EOLVariants(Cvs2SvnTestCase):
3337 "handle various --eol-style options"
3339 eol_style_strings = {
3340 'LF' : '\n',
3341 'CR' : '\r',
3342 'CRLF' : '\r\n',
3343 'native' : '\n',
3346 def __init__(self, eol_style):
3347 self.eol_style = eol_style
3348 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3349 Cvs2SvnTestCase.__init__(
3350 self, 'eol-variants', variant=self.eol_style,
3351 dumpfile=self.dumpfile,
3352 args=[
3353 '--default-eol=%s' % (self.eol_style,),
3357 def run(self):
3358 conv = self.ensure_conversion()
3359 dump_contents = open(conv.dumpfile, 'rb').read()
3360 expected_text = self.eol_style_strings[self.eol_style].join(
3361 ['line 1', 'line 2', '\n\n']
3363 if not dump_contents.endswith(expected_text):
3364 raise Failure()
3367 def no_revs_file():
3368 "handle a file with no revisions (issue #80)"
3370 conv = ensure_conversion('no-revs-file')
3373 def mirror_keyerror_test():
3374 "a case that gave KeyError in SVNRepositoryMirror"
3376 conv = ensure_conversion('mirror-keyerror')
3379 def exclude_ntdb_test():
3380 "exclude a non-trunk default branch"
3382 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3383 conv = ensure_conversion(
3384 'exclude-ntdb',
3385 args=[
3386 '--write-symbol-info=%s' % (symbol_info_file,),
3387 '--exclude=branch3',
3388 '--exclude=tag3',
3389 '--exclude=vendortag3',
3390 '--exclude=vendorbranch',
3395 def mirror_keyerror2_test():
3396 "a case that gave KeyError in RepositoryMirror"
3398 conv = ensure_conversion('mirror-keyerror2')
3401 def mirror_keyerror3_test():
3402 "a case that gave KeyError in RepositoryMirror"
3404 conv = ensure_conversion('mirror-keyerror3')
3407 def add_cvsignore_to_branch_test():
3408 "check adding .cvsignore to an existing branch"
3410 # This a test for issue #122.
3412 conv = ensure_conversion('add-cvsignore-to-branch')
3413 wc_tree = conv.get_wc_tree()
3414 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3415 if trunk_props['svn:ignore'] != '*.o\n\n':
3416 raise Failure()
3418 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3419 if branch_props['svn:ignore'] != '*.o\n\n':
3420 raise Failure()
3423 def missing_deltatext():
3424 "a revision's deltatext is missing"
3426 # This is a type of RCS file corruption that has been observed.
3427 conv = ensure_conversion(
3428 'missing-deltatext',
3429 error_re=(
3430 r"ERROR\: .* has no deltatext section for revision 1\.1\.4\.4"
3435 ########################################################################
3436 # Run the tests
3438 # list all tests here, starting with None:
3439 test_list = [
3440 None,
3441 # 1:
3442 show_usage,
3443 attr_exec,
3444 space_fname,
3445 two_quick,
3446 PruneWithCare(),
3447 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3448 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3449 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3450 interleaved_commits,
3451 # 10:
3452 simple_commits,
3453 SimpleTags(),
3454 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3455 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3456 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3457 simple_branch_commits,
3458 mixed_time_tag,
3459 mixed_time_branch_with_added_file,
3460 mixed_commit,
3461 split_time_branch,
3462 # 20:
3463 bogus_tag,
3464 overlapping_branch,
3465 PhoenixBranch(),
3466 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3467 ctrl_char_in_log,
3468 overdead,
3469 NoTrunkPrune(),
3470 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3471 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3472 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3473 # 30:
3474 double_delete,
3475 split_branch,
3476 resync_misgroups,
3477 TaggedBranchAndTrunk(),
3478 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3479 enroot_race,
3480 enroot_race_obo,
3481 BranchDeleteFirst(),
3482 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3483 nonascii_filenames,
3484 # 40:
3485 UnicodeAuthor(
3486 warning_expected=1),
3487 UnicodeAuthor(
3488 warning_expected=0,
3489 variant='encoding', args=['--encoding=utf_8']),
3490 UnicodeAuthor(
3491 warning_expected=0,
3492 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3493 UnicodeLog(
3494 warning_expected=1),
3495 UnicodeLog(
3496 warning_expected=0,
3497 variant='encoding', args=['--encoding=utf_8']),
3498 UnicodeLog(
3499 warning_expected=0,
3500 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3501 vendor_branch_sameness,
3502 vendor_branch_trunk_only,
3503 default_branches,
3504 default_branches_trunk_only,
3505 # 50:
3506 default_branch_and_1_2,
3507 compose_tag_three_sources,
3508 pass5_when_to_fill,
3509 PeerPathPruning(),
3510 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3511 EmptyTrunk(),
3512 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3513 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3514 no_spurious_svn_commits,
3515 invalid_closings_on_trunk,
3516 # 60:
3517 individual_passes,
3518 resync_bug,
3519 branch_from_default_branch,
3520 file_in_attic_too,
3521 retain_file_in_attic_too,
3522 symbolic_name_filling_guide,
3523 eol_mime1,
3524 eol_mime2,
3525 eol_mime3,
3526 eol_mime4,
3527 # 70:
3528 cvs_revnums_off,
3529 cvs_revnums_on,
3530 keywords,
3531 ignore,
3532 requires_cvs,
3533 questionable_branch_names,
3534 questionable_tag_names,
3535 revision_reorder_bug,
3536 exclude,
3537 vendor_branch_delete_add,
3538 # 80:
3539 resync_pass2_pull_forward,
3540 native_eol,
3541 double_fill,
3542 XFail(double_fill2),
3543 resync_pass2_push_backward,
3544 double_add,
3545 bogus_branch_copy,
3546 nested_ttb_directories,
3547 auto_props_ignore_case,
3548 ctrl_char_in_filename,
3549 # 90:
3550 commit_dependencies,
3551 show_help_passes,
3552 multiple_tags,
3553 multiply_defined_symbols,
3554 multiply_defined_symbols_renamed,
3555 multiply_defined_symbols_ignored,
3556 repeatedly_defined_symbols,
3557 double_branch_delete,
3558 symbol_mismatches,
3559 overlook_symbol_mismatches,
3560 # 100:
3561 force_symbols,
3562 commit_blocks_tags,
3563 blocked_excludes,
3564 unblock_blocked_excludes,
3565 regexp_force_symbols,
3566 heuristic_symbol_default,
3567 branch_symbol_default,
3568 tag_symbol_default,
3569 symbol_transform,
3570 write_symbol_info,
3571 # 110:
3572 symbol_hints,
3573 parent_hints,
3574 parent_hints_invalid,
3575 parent_hints_wildcards,
3576 path_hints,
3577 issue_99,
3578 issue_100,
3579 issue_106,
3580 options_option,
3581 multiproject,
3582 # 120:
3583 crossproject,
3584 tag_with_no_revision,
3585 delete_cvsignore,
3586 repeated_deltatext,
3587 nasty_graphs,
3588 XFail(tagging_after_delete),
3589 crossed_branches,
3590 file_directory_conflict,
3591 attic_directory_conflict,
3592 internal_co,
3593 # 130:
3594 internal_co_exclude,
3595 internal_co_trunk_only,
3596 internal_co_keywords,
3597 leftover_revs,
3598 requires_internal_co,
3599 timestamp_chaos,
3600 symlinks,
3601 empty_trunk_path,
3602 preferred_parent_cycle,
3603 branch_from_empty_dir,
3604 # 140:
3605 trunk_readd,
3606 branch_from_deleted_1_1,
3607 add_on_branch,
3608 main_git,
3609 git_options,
3610 main_hg,
3611 invalid_symbol,
3612 invalid_symbol_ignore,
3613 EOLVariants('LF'),
3614 EOLVariants('CR'),
3615 # 150:
3616 EOLVariants('CRLF'),
3617 EOLVariants('native'),
3618 no_revs_file,
3619 mirror_keyerror_test,
3620 exclude_ntdb_test,
3621 mirror_keyerror2_test,
3622 mirror_keyerror3_test,
3623 XFail(add_cvsignore_to_branch_test),
3624 missing_deltatext,
3627 if __name__ == '__main__':
3629 # Configure the environment for reproducable output from svn, etc.
3630 # I have no idea if this works on Windows too.
3631 os.environ["LC_ALL"] = "C"
3632 os.environ["TZ"] = "UTC"
3634 # The Subversion test suite code assumes it's being invoked from
3635 # within a working copy of the Subversion sources, and tries to use
3636 # the binaries in that tree. Since the cvs2svn tree never contains
3637 # a Subversion build, we just use the system's installed binaries.
3638 svntest.main.svn_binary = 'svn'
3639 svntest.main.svnlook_binary = 'svnlook'
3640 svntest.main.svnadmin_binary = 'svnadmin'
3641 svntest.main.svnversion_binary = 'svnversion'
3643 run_tests(test_list)
3644 # NOTREACHED
3647 ### End of file.