Bring CHANGES file up to date.
[cvs2svn.git] / run-tests.py
blobd53e123fce631293b8dde5d9a7aa97b885cc229f
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-2009 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 import calendar
47 import types
48 try:
49 from hashlib import md5
50 except ImportError:
51 from md5 import md5
52 from difflib import Differ
54 # Make sure that a supported version of Python is being used:
55 if not (0x02040000 <= sys.hexversion < 0x03000000):
56 sys.stderr.write(
57 'error: Python 2, version 2.4 or higher required.\n'
59 sys.exit(1)
61 # This script needs to run in the correct directory. Make sure we're there.
62 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
63 sys.stderr.write("error: I need to be run in the directory containing "
64 "'cvs2svn' and 'test-data'.\n")
65 sys.exit(1)
67 # Load the Subversion test framework.
68 import svntest
69 from svntest import Failure
70 from svntest.main import safe_rmtree
71 from svntest.testcase import TestCase
72 from svntest.testcase import XFail_deco
74 # Test if Mercurial >= 1.1 is available.
75 try:
76 from mercurial import context
77 context.memctx
78 have_hg = True
79 except (ImportError, AttributeError):
80 have_hg = False
82 cvs2svn = os.path.abspath('cvs2svn')
83 cvs2git = os.path.abspath('cvs2git')
84 cvs2hg = os.path.abspath('cvs2hg')
86 # We use the installed svn and svnlook binaries, instead of using
87 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
88 # behavior -- or even existence -- of local builds shouldn't affect
89 # the cvs2svn test suite.
90 svn_binary = 'svn'
91 svnlook_binary = 'svnlook'
92 svnadmin_binary = 'svnadmin'
93 svnversion_binary = 'svnversion'
95 test_data_dir = 'test-data'
96 tmp_dir = 'cvs2svn-tmp'
99 #----------------------------------------------------------------------
100 # Helpers.
101 #----------------------------------------------------------------------
104 # The value to expect for svn:keywords if it is set:
105 KEYWORDS = 'Author Date Id Revision'
108 class RunProgramException(Failure):
109 pass
112 class MissingErrorException(Failure):
113 def __init__(self, error_re):
114 Failure.__init__(
115 self, "Test failed because no error matched '%s'" % (error_re,)
119 def run_program(program, error_re, *varargs):
120 """Run PROGRAM with VARARGS, return stdout as a list of lines.
122 If there is any stderr and ERROR_RE is None, raise
123 RunProgramException, and log the stderr lines via
124 svntest.main.logger.info().
126 If ERROR_RE is not None, it is a string regular expression that must
127 match some line of stderr. If it fails to match, raise
128 MissingErrorExpection."""
130 # FIXME: exit_code is currently ignored.
131 exit_code, out, err = svntest.main.run_command(program, 1, 0, *varargs)
133 if error_re:
134 # Specified error expected on stderr.
135 if not err:
136 raise MissingErrorException(error_re)
137 else:
138 for line in err:
139 if re.match(error_re, line):
140 return out
141 raise MissingErrorException(error_re)
142 else:
143 # No stderr allowed.
144 if err:
145 log = svntest.main.logger.info
146 log('%s said:' % program)
147 for line in err:
148 log(' ' + line.rstrip())
149 raise RunProgramException()
151 return out
154 def run_script(script, error_re, *varargs):
155 """Run Python script SCRIPT with VARARGS, returning stdout as a list
156 of lines.
158 If there is any stderr and ERROR_RE is None, raise
159 RunProgramException, and log the stderr lines via
160 svntest.main.logger.info().
162 If ERROR_RE is not None, it is a string regular expression that must
163 match some line of stderr. If it fails to match, raise
164 MissingErrorException."""
166 # Use the same python that is running this script
167 return run_program(sys.executable, error_re, script, *varargs)
168 # On Windows, for an unknown reason, the cmd.exe process invoked by
169 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
170 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
171 # this. Therefore, the redirection of the output to the .s-revs file fails.
172 # We no longer use the problematic invocation on any system, but this
173 # comment remains to warn about this problem.
176 def run_svn(*varargs):
177 """Run svn with VARARGS; return stdout as a list of lines.
178 If there is any stderr, raise RunProgramException, and log the
179 stderr lines via svntest.main.logger.info()."""
180 return run_program(svn_binary, None, *varargs)
183 def repos_to_url(path_to_svn_repos):
184 """This does what you think it does."""
185 rpath = os.path.abspath(path_to_svn_repos)
186 if rpath[0] != '/':
187 rpath = '/' + rpath
188 return 'file://%s' % rpath.replace(os.sep, '/')
191 def svn_strptime(timestr):
192 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
195 class Log:
196 def __init__(self, revision, author, date, symbols):
197 self.revision = revision
198 self.author = author
200 # Internally, we represent the date as seconds since epoch (UTC).
201 # Since standard subversion log output shows dates in localtime
203 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
205 # and time.mktime() converts from localtime, it all works out very
206 # happily.
207 self.date = time.mktime(svn_strptime(date[0:19]))
209 # The following symbols are used for string interpolation when
210 # checking paths:
211 self.symbols = symbols
213 # The changed paths will be accumulated later, as log data is read.
214 # Keys here are paths such as '/trunk/foo/bar', values are letter
215 # codes such as 'M', 'A', and 'D'.
216 self.changed_paths = { }
218 # The msg will be accumulated later, as log data is read.
219 self.msg = ''
221 def absorb_changed_paths(self, out):
222 'Read changed paths from OUT into self, until no more.'
223 while 1:
224 line = out.readline()
225 if len(line) == 1: return
226 line = line[:-1]
227 op_portion = line[3:4]
228 path_portion = line[5:]
229 # If we're running on Windows we get backslashes instead of
230 # forward slashes.
231 path_portion = path_portion.replace('\\', '/')
232 # # We could parse out history information, but currently we
233 # # just leave it in the path portion because that's how some
234 # # tests expect it.
236 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
237 # if m:
238 # path_portion = m.group(1)
239 self.changed_paths[path_portion] = op_portion
241 def __cmp__(self, other):
242 return cmp(self.revision, other.revision) or \
243 cmp(self.author, other.author) or cmp(self.date, other.date) or \
244 cmp(self.changed_paths, other.changed_paths) or \
245 cmp(self.msg, other.msg)
247 def get_path_op(self, path):
248 """Return the operator for the change involving PATH.
250 PATH is allowed to include string interpolation directives (e.g.,
251 '%(trunk)s'), which are interpolated against self.symbols. Return
252 None if there is no record for PATH."""
253 return self.changed_paths.get(path % self.symbols)
255 def check_msg(self, msg):
256 """Verify that this Log's message starts with the specified MSG."""
257 if self.msg.find(msg) != 0:
258 raise Failure(
259 "Revision %d log message was:\n%s\n\n"
260 "It should have begun with:\n%s\n\n"
261 % (self.revision, self.msg, msg,)
264 def check_change(self, path, op):
265 """Verify that this Log includes a change for PATH with operator OP.
267 PATH is allowed to include string interpolation directives (e.g.,
268 '%(trunk)s'), which are interpolated against self.symbols."""
270 path = path % self.symbols
271 found_op = self.changed_paths.get(path, None)
272 if found_op is None:
273 raise Failure(
274 "Revision %d does not include change for path %s "
275 "(it should have been %s).\n"
276 % (self.revision, path, op,)
278 if found_op != op:
279 raise Failure(
280 "Revision %d path %s had op %s (it should have been %s)\n"
281 % (self.revision, path, found_op, op,)
284 def check_changes(self, changed_paths):
285 """Verify that this Log has precisely the CHANGED_PATHS specified.
287 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
288 strings are allowed to include string interpolation directives
289 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
291 cp = {}
292 for (path, op) in changed_paths:
293 cp[path % self.symbols] = op
295 if self.changed_paths != cp:
296 raise Failure(
297 "Revision %d changed paths list was:\n%s\n\n"
298 "It should have been:\n%s\n\n"
299 % (self.revision, self.changed_paths, cp,)
302 def check(self, msg, changed_paths):
303 """Verify that this Log has the MSG and CHANGED_PATHS specified.
305 Convenience function to check two things at once. MSG is passed
306 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
308 self.check_msg(msg)
309 self.check_changes(changed_paths)
312 def parse_log(svn_repos, symbols):
313 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
315 Initialize the Logs' symbols with SYMBOLS."""
317 class LineFeeder:
318 'Make a list of lines behave like an open file handle.'
319 def __init__(self, lines):
320 self.lines = lines
321 def readline(self):
322 if len(self.lines) > 0:
323 return self.lines.pop(0)
324 else:
325 return None
327 def absorb_message_body(out, num_lines, log):
328 """Read NUM_LINES of log message body from OUT into Log item LOG."""
330 for i in range(num_lines):
331 log.msg += out.readline()
333 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
334 '(?P<author>[^\|]+) \| '
335 '(?P<date>[^\|]+) '
336 '\| (?P<lines>[0-9]+) (line|lines)$')
338 log_separator = '-' * 72
340 logs = { }
342 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
344 while 1:
345 this_log = None
346 line = out.readline()
347 if not line: break
348 line = line[:-1]
350 if line.find(log_separator) == 0:
351 line = out.readline()
352 if not line: break
353 line = line[:-1]
354 m = log_start_re.match(line)
355 if m:
356 this_log = Log(
357 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
358 line = out.readline()
359 if not line.find('Changed paths:') == 0:
360 print 'unexpected log output (missing changed paths)'
361 print "Line: '%s'" % line
362 sys.exit(1)
363 this_log.absorb_changed_paths(out)
364 absorb_message_body(out, int(m.group('lines')), this_log)
365 logs[this_log.revision] = this_log
366 elif len(line) == 0:
367 break # We've reached the end of the log output.
368 else:
369 print 'unexpected log output (missing revision line)'
370 print "Line: '%s'" % line
371 sys.exit(1)
372 else:
373 print 'unexpected log output (missing log separator)'
374 print "Line: '%s'" % line
375 sys.exit(1)
377 return logs
380 def erase(path):
381 """Unconditionally remove PATH and its subtree, if any. PATH may be
382 non-existent, a file or symlink, or a directory."""
383 if os.path.isdir(path):
384 safe_rmtree(path)
385 elif os.path.exists(path):
386 os.remove(path)
389 log_msg_text_wrapper = textwrap.TextWrapper(width=76, break_long_words=False)
391 def sym_log_msg(symbolic_name, is_tag=None):
392 """Return the expected log message for a cvs2svn-synthesized revision
393 creating branch or tag SYMBOLIC_NAME."""
395 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
396 if is_tag:
397 type = 'tag'
398 else:
399 type = 'branch'
401 return log_msg_text_wrapper.fill(
402 "This commit was manufactured by cvs2svn to create %s '%s'."
403 % (type, symbolic_name)
407 def make_conversion_id(
408 name, args, passbypass, options_file=None, symbol_hints_file=None
410 """Create an identifying tag for a conversion.
412 The return value can also be used as part of a filesystem path.
414 NAME is the name of the CVS repository.
416 ARGS are the extra arguments to be passed to cvs2svn.
418 PASSBYPASS is a boolean indicating whether the conversion is to be
419 run one pass at a time.
421 If OPTIONS_FILE is specified, it is an options file that will be
422 used for the conversion.
424 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
425 will be used for the conversion.
427 The 1-to-1 mapping between cvs2svn command parameters and
428 conversion_ids allows us to avoid running the same conversion more
429 than once, when multiple tests use exactly the same conversion."""
431 conv_id = name
433 args = args[:]
435 if passbypass:
436 args.append('--passbypass')
438 if symbol_hints_file is not None:
439 args.append('--symbol-hints=%s' % (symbol_hints_file,))
441 # There are some characters that are forbidden in filenames, and
442 # there is a limit on the total length of a path to a file. So use
443 # a hash of the parameters rather than concatenating the parameters
444 # into a string.
445 if args:
446 conv_id += "-" + md5('\0'.join(args)).hexdigest()
448 # Some options-file based tests rely on knowing the paths to which
449 # the repository should be written, so we handle that option as a
450 # predictable string:
451 if options_file is not None:
452 conv_id += '--options=%s' % (options_file,)
454 return conv_id
457 class Conversion:
458 """A record of a cvs2svn conversion.
460 Fields:
462 conv_id -- the conversion id for this Conversion.
464 name -- a one-word name indicating the involved repositories.
466 dumpfile -- the name of the SVN dumpfile created by the conversion
467 (if the DUMPFILE constructor argument was used); otherwise,
468 None.
470 repos -- the path to the svn repository. Unset if DUMPFILE was
471 specified.
473 logs -- a dictionary of Log instances, as returned by parse_log().
474 Unset if DUMPFILE was specified.
476 symbols -- a dictionary of symbols used for string interpolation
477 in path names.
479 stdout -- a list of lines written by cvs2svn to stdout
481 _wc -- the basename of the svn working copy (within tmp_dir).
482 Unset if DUMPFILE was specified.
484 _wc_path -- the path to the svn working copy, if it has already
485 been created; otherwise, None. (The working copy is created
486 lazily when get_wc() is called.) Unset if DUMPFILE was
487 specified.
489 _wc_tree -- the tree built from the svn working copy, if it has
490 already been created; otherwise, None. The tree is created
491 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
492 specified.
494 _svnrepos -- the basename of the svn repository (within tmp_dir).
495 Unset if DUMPFILE was specified."""
497 # The number of the last cvs2svn pass (determined lazily by
498 # get_last_pass()).
499 last_pass = None
501 @classmethod
502 def get_last_pass(cls):
503 """Return the number of cvs2svn's last pass."""
505 if cls.last_pass is None:
506 out = run_script(cvs2svn, None, '--help-passes')
507 cls.last_pass = int(out[-1].split()[0])
508 return cls.last_pass
510 def __init__(
511 self, conv_id, name, error_re, passbypass, symbols, args,
512 verbosity=None, options_file=None, symbol_hints_file=None, dumpfile=None,
514 self.conv_id = conv_id
515 self.name = name
516 self.symbols = symbols
517 if not os.path.isdir(tmp_dir):
518 os.mkdir(tmp_dir)
520 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
522 if dumpfile:
523 self.dumpfile = os.path.join(tmp_dir, dumpfile)
524 # Clean up from any previous invocations of this script.
525 erase(self.dumpfile)
526 else:
527 self.dumpfile = None
528 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
529 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
530 self._wc_path = None
531 self._wc_tree = None
533 # Clean up from any previous invocations of this script.
534 erase(self.repos)
535 erase(self._wc)
537 args = list(args)
538 if svntest.main.svnadmin_binary != 'svnadmin':
539 args.extend([
540 '--svnadmin=%s' % (svntest.main.svnadmin_binary,),
542 if options_file:
543 self.options_file = os.path.join(cvsrepos, options_file)
544 args.extend([
545 '--options=%s' % self.options_file,
547 args.append(verbosity or '-qqqqqq')
548 assert not symbol_hints_file
549 else:
550 self.options_file = None
551 args.extend([
552 '--tmpdir=%s' % tmp_dir,
555 args.append(verbosity or '-qqqqqq')
557 if symbol_hints_file:
558 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
559 args.extend([
560 '--symbol-hints=%s' % self.symbol_hints_file,
563 if self.dumpfile:
564 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
565 else:
566 args.extend(['-s', self.repos])
567 args.extend([cvsrepos])
569 if passbypass:
570 self.stdout = []
571 for p in range(1, self.get_last_pass() + 1):
572 self.stdout += run_script(cvs2svn, error_re, '-p', str(p), *args)
573 else:
574 self.stdout = run_script(cvs2svn, error_re, *args)
576 if self.dumpfile:
577 if not os.path.isfile(self.dumpfile):
578 raise Failure(
579 "Dumpfile not created: '%s'"
580 % os.path.join(os.getcwd(), self.dumpfile)
582 else:
583 if os.path.isdir(self.repos):
584 self.logs = parse_log(self.repos, self.symbols)
585 elif error_re is None:
586 raise Failure(
587 "Repository not created: '%s'"
588 % os.path.join(os.getcwd(), self.repos)
591 def output_found(self, pattern):
592 """Return True if PATTERN matches any line in self.stdout.
594 PATTERN is a regular expression pattern as a string.
597 pattern_re = re.compile(pattern)
599 for line in self.stdout:
600 if pattern_re.match(line):
601 # We found the pattern that we were looking for.
602 return 1
603 else:
604 return 0
606 def find_tag_log(self, tagname):
607 """Search LOGS for a log message containing 'TAGNAME' and return the
608 log in which it was found."""
609 for i in xrange(len(self.logs), 0, -1):
610 if self.logs[i].msg.find("'"+tagname+"'") != -1:
611 return self.logs[i]
612 raise ValueError("Tag %s not found in logs" % tagname)
614 def get_wc(self, *args):
615 """Return the path to the svn working copy, or a path within the WC.
617 If a working copy has not been created yet, create it now.
619 If ARGS are specified, then they should be strings that form
620 fragments of a path within the WC. They are joined using
621 os.path.join() and appended to the WC path."""
623 if self._wc_path is None:
624 run_svn('co', repos_to_url(self.repos), self._wc)
625 self._wc_path = self._wc
626 return os.path.join(self._wc_path, *args)
628 def get_wc_tree(self):
629 if self._wc_tree is None:
630 self._wc_tree = svntest.tree.build_tree_from_wc(self.get_wc(), 1)
631 return self._wc_tree
633 def path_exists(self, *args):
634 """Return True if the specified path exists within the repository.
636 (The strings in ARGS are first joined into a path using
637 os.path.join().)"""
639 return os.path.exists(self.get_wc(*args))
641 def check_props(self, keys, checks):
642 """Helper function for checking lots of properties. For a list of
643 files in the conversion, check that the values of the properties
644 listed in KEYS agree with those listed in CHECKS. CHECKS is a
645 list of tuples: [ (filename, [value, value, ...]), ...], where the
646 values are listed in the same order as the key names are listed in
647 KEYS."""
649 for (file, values) in checks:
650 assert len(values) == len(keys)
651 props = props_for_path(self.get_wc_tree(), file)
652 for i in range(len(keys)):
653 if props.get(keys[i]) != values[i]:
654 raise Failure(
655 "File %s has property %s set to \"%s\" "
656 "(it should have been \"%s\").\n"
657 % (file, keys[i], props.get(keys[i]), values[i],)
661 class GitConversion:
662 """A record of a cvs2svn conversion.
664 Fields:
666 name -- a one-word name indicating the CVS repository to be converted.
668 stdout -- a list of lines written by cvs2svn to stdout."""
670 def __init__(self, name, error_re, args, verbosity=None, options_file=None):
671 self.name = name
672 if not os.path.isdir(tmp_dir):
673 os.mkdir(tmp_dir)
675 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
677 args = list(args)
678 if options_file:
679 self.options_file = os.path.join(cvsrepos, options_file)
680 args.extend([
681 '--options=%s' % self.options_file,
683 else:
684 self.options_file = None
686 args.append(verbosity or '-qqqqqq')
688 self.stdout = run_script(cvs2git, error_re, *args)
691 # Cache of conversions that have already been done. Keys are conv_id;
692 # values are Conversion instances.
693 already_converted = { }
695 def ensure_conversion(
696 name, error_re=None, passbypass=None,
697 trunk=None, branches=None, tags=None,
698 args=None, verbosity=None,
699 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 VERBOSITY is set, then it is passed to cvs2svn as an option.
726 Otherwise, the verbosity is turned way down so that only error
727 messages are emitted.
729 If OPTIONS_FILE is specified, then it should be the name of a file
730 within the main directory of the cvs repository associated with this
731 test. It is passed to cvs2svn using the --options option (which
732 suppresses some other options that are incompatible with --options).
734 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
735 file within the main directory of the cvs repository associated with
736 this test. It is passed to cvs2svn using the --symbol-hints option.
738 If DUMPFILE is specified, then it is the name of a dumpfile within
739 the temporary directory to which the conversion output should be
740 written."""
742 if args is None:
743 args = []
744 else:
745 args = list(args)
747 if trunk is None:
748 trunk = 'trunk'
749 else:
750 args.append('--trunk=%s' % (trunk,))
752 if branches is None:
753 branches = 'branches'
754 else:
755 args.append('--branches=%s' % (branches,))
757 if tags is None:
758 tags = 'tags'
759 else:
760 args.append('--tags=%s' % (tags,))
762 conv_id = make_conversion_id(
763 name, args, passbypass, options_file, symbol_hints_file
766 if conv_id not in already_converted:
767 try:
768 # Run the conversion and store the result for the rest of this
769 # session:
770 already_converted[conv_id] = Conversion(
771 conv_id, name, error_re, passbypass,
772 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
773 args, verbosity, options_file, symbol_hints_file, dumpfile,
775 except Failure:
776 # Remember the failure so that a future attempt to run this conversion
777 # does not bother to retry, but fails immediately.
778 already_converted[conv_id] = None
779 raise
781 conv = already_converted[conv_id]
782 if conv is None:
783 raise Failure()
784 return conv
787 class Cvs2SvnTestFunction(TestCase):
788 """A TestCase based on a naked Python function object.
790 FUNC should be a function that returns None on success and throws an
791 svntest.Failure exception on failure. It should have a brief
792 docstring describing what it does (and fulfilling certain
793 conditions). FUNC must take no arguments.
795 This class is almost identical to svntest.testcase.FunctionTestCase,
796 except that the test function does not require a sandbox and does
797 not accept any parameter (not even sandbox=None).
799 This class can be used as an annotation on a Python function.
803 def __init__(self, func):
804 # it better be a function that accepts no parameters and has a
805 # docstring on it.
806 assert isinstance(func, types.FunctionType)
808 name = func.func_name
810 assert func.func_code.co_argcount == 0, \
811 '%s must not take any arguments' % name
813 doc = func.__doc__.strip()
814 assert doc, '%s must have a docstring' % name
816 # enforce stylistic guidelines for the function docstrings:
817 # - no longer than 50 characters
818 # - should not end in a period
819 # - should not be capitalized
820 assert len(doc) <= 50, \
821 "%s's docstring must be 50 characters or less" % name
822 assert doc[-1] != '.', \
823 "%s's docstring should not end in a period" % name
824 assert doc[0].lower() == doc[0], \
825 "%s's docstring should not be capitalized" % name
827 TestCase.__init__(self, doc=doc)
828 self.func = func
830 def get_function_name(self):
831 return self.func.func_name
833 def get_sandbox_name(self):
834 return None
836 def run(self, sandbox):
837 return self.func()
840 class Cvs2HgTestFunction(Cvs2SvnTestFunction):
841 """Same as Cvs2SvnTestFunction, but for test cases that should be
842 skipped if Mercurial is not available.
844 def run(self, sandbox):
845 if not have_hg:
846 raise svntest.Skip()
847 else:
848 return self.func()
851 class Cvs2SvnTestCase(TestCase):
852 def __init__(
853 self, name, doc=None, variant=None,
854 error_re=None, passbypass=None,
855 trunk=None, branches=None, tags=None,
856 args=None,
857 options_file=None, symbol_hints_file=None, dumpfile=None,
859 self.name = name
861 if doc is None:
862 # By default, use the first line of the class docstring as the
863 # doc:
864 doc = self.__doc__.splitlines()[0]
866 if variant is not None:
867 # Modify doc to show the variant. Trim doc first if necessary
868 # to stay within the 50-character limit.
869 suffix = '...variant %s' % (variant,)
870 doc = doc[:50 - len(suffix)] + suffix
872 TestCase.__init__(self, doc=doc)
874 self.error_re = error_re
875 self.passbypass = passbypass
876 self.trunk = trunk
877 self.branches = branches
878 self.tags = tags
879 self.args = args
880 self.options_file = options_file
881 self.symbol_hints_file = symbol_hints_file
882 self.dumpfile = dumpfile
884 def ensure_conversion(self):
885 return ensure_conversion(
886 self.name,
887 error_re=self.error_re, passbypass=self.passbypass,
888 trunk=self.trunk, branches=self.branches, tags=self.tags,
889 args=self.args,
890 options_file=self.options_file,
891 symbol_hints_file=self.symbol_hints_file,
892 dumpfile=self.dumpfile,
895 def get_sandbox_name(self):
896 return None
899 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
900 """Test properties resulting from a conversion."""
902 def __init__(self, name, props_to_test, expected_props, **kw):
903 """Initialize an instance of Cvs2SvnPropertiesTestCase.
905 NAME is the name of the test, passed to Cvs2SvnTestCase.
906 PROPS_TO_TEST is a list of the names of svn properties that should
907 be tested. EXPECTED_PROPS is a list of tuples [(filename,
908 [value,...])], where the second item in each tuple is a list of
909 values expected for the properties listed in PROPS_TO_TEST for the
910 specified filename. If a property must *not* be set, then its
911 value should be listed as None."""
913 Cvs2SvnTestCase.__init__(self, name, **kw)
914 self.props_to_test = props_to_test
915 self.expected_props = expected_props
917 def run(self, sbox):
918 conv = self.ensure_conversion()
919 conv.check_props(self.props_to_test, self.expected_props)
922 #----------------------------------------------------------------------
923 # Tests.
924 #----------------------------------------------------------------------
927 @Cvs2SvnTestFunction
928 def show_usage():
929 "cvs2svn with no arguments shows usage"
930 out = run_script(cvs2svn, None)
931 if (len(out) > 2 and out[0].find('ERROR:') == 0
932 and out[1].find('DBM module')):
933 print 'cvs2svn cannot execute due to lack of proper DBM module.'
934 print 'Exiting without running any further tests.'
935 sys.exit(1)
936 if out[0].find('Usage:') < 0:
937 raise Failure('Basic cvs2svn invocation failed.')
940 @Cvs2SvnTestFunction
941 def cvs2svn_manpage():
942 "generate a manpage for cvs2svn"
943 out = run_script(cvs2svn, None, '--man')
946 @Cvs2SvnTestFunction
947 def cvs2git_manpage():
948 "generate a manpage for cvs2git"
949 out = run_script(cvs2git, None, '--man')
952 @XFail_deco()
953 @Cvs2HgTestFunction
954 def cvs2hg_manpage():
955 "generate a manpage for cvs2hg"
956 out = run_script(cvs2hg, None, '--man')
959 @Cvs2SvnTestFunction
960 def show_help_passes():
961 "cvs2svn --help-passes shows pass information"
962 out = run_script(cvs2svn, None, '--help-passes')
963 if out[0].find('PASSES') < 0:
964 raise Failure('cvs2svn --help-passes failed.')
967 @Cvs2SvnTestFunction
968 def attr_exec():
969 "detection of the executable flag"
970 if sys.platform == 'win32':
971 raise svntest.Skip()
972 conv = ensure_conversion('main')
973 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
974 if not st.st_mode & stat.S_IXUSR:
975 raise Failure()
978 @Cvs2SvnTestFunction
979 def space_fname():
980 "conversion of filename with a space"
981 conv = ensure_conversion('main')
982 if not conv.path_exists('trunk', 'single-files', 'space fname'):
983 raise Failure()
986 @Cvs2SvnTestFunction
987 def two_quick():
988 "two commits in quick succession"
989 conv = ensure_conversion('main')
990 logs = parse_log(
991 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
992 if len(logs) != 2:
993 raise Failure()
996 class PruneWithCare(Cvs2SvnTestCase):
997 "prune, but never too much"
999 def __init__(self, **kw):
1000 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1002 def run(self, sbox):
1003 # Robert Pluim encountered this lovely one while converting the
1004 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
1005 # repository (see issue #1302). Step 4 is the doozy:
1007 # revision 1: adds trunk/blah/, adds trunk/blah/first
1008 # revision 2: adds trunk/blah/second
1009 # revision 3: deletes trunk/blah/first
1010 # revision 4: deletes blah [re-deleting trunk/blah/first pruned blah!]
1011 # revision 5: does nothing
1013 # After fixing cvs2svn, the sequence (correctly) looks like this:
1015 # revision 1: adds trunk/blah/, adds trunk/blah/first
1016 # revision 2: adds trunk/blah/second
1017 # revision 3: deletes trunk/blah/first
1018 # revision 4: does nothing [because trunk/blah/first already deleted]
1019 # revision 5: deletes blah
1021 # The difference is in 4 and 5. In revision 4, it's not correct
1022 # to prune blah/, because second is still in there, so revision 4
1023 # does nothing now. But when we delete second in 5, that should
1024 # bubble up and prune blah/ instead.
1026 # ### Note that empty revisions like 4 are probably going to become
1027 # ### at least optional, if not banished entirely from cvs2svn's
1028 # ### output. Hmmm, or they may stick around, with an extra
1029 # ### revision property explaining what happened. Need to think
1030 # ### about that. In some sense, it's a bug in Subversion itself,
1031 # ### that such revisions don't show up in 'svn log' output.
1033 conv = self.ensure_conversion()
1035 # Confirm that revision 4 removes '/trunk/full-prune/first',
1036 # and that revision 6 removes '/trunk/full-prune'.
1038 # Also confirm similar things about '/full-prune-reappear/...',
1039 # which is similar, except that later on it reappears, restored
1040 # from pruneland, because a file gets added to it.
1042 # And finally, a similar thing for '/partial-prune/...', except that
1043 # in its case, a permanent file on the top level prevents the
1044 # pruning from going farther than the subdirectory containing first
1045 # and second.
1047 for path in ('full-prune/first',
1048 'full-prune-reappear/sub/first',
1049 'partial-prune/sub/first'):
1050 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
1052 for path in ('full-prune',
1053 'full-prune-reappear',
1054 'partial-prune/sub'):
1055 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
1057 for path in ('full-prune-reappear',
1058 'full-prune-reappear/appears-later'):
1059 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
1062 @Cvs2SvnTestFunction
1063 def interleaved_commits():
1064 "two interleaved trunk commits, different log msgs"
1065 # See test-data/main-cvsrepos/proj/README.
1066 conv = ensure_conversion('main')
1068 # The initial import.
1069 rev = 26
1070 conv.logs[rev].check('Initial import.', (
1071 ('/%(trunk)s/interleaved', 'A'),
1072 ('/%(trunk)s/interleaved/1', 'A'),
1073 ('/%(trunk)s/interleaved/2', 'A'),
1074 ('/%(trunk)s/interleaved/3', 'A'),
1075 ('/%(trunk)s/interleaved/4', 'A'),
1076 ('/%(trunk)s/interleaved/5', 'A'),
1077 ('/%(trunk)s/interleaved/a', 'A'),
1078 ('/%(trunk)s/interleaved/b', 'A'),
1079 ('/%(trunk)s/interleaved/c', 'A'),
1080 ('/%(trunk)s/interleaved/d', 'A'),
1081 ('/%(trunk)s/interleaved/e', 'A'),
1084 def check_letters(rev):
1085 """Check if REV is the rev where only letters were committed."""
1087 conv.logs[rev].check('Committing letters only.', (
1088 ('/%(trunk)s/interleaved/a', 'M'),
1089 ('/%(trunk)s/interleaved/b', 'M'),
1090 ('/%(trunk)s/interleaved/c', 'M'),
1091 ('/%(trunk)s/interleaved/d', 'M'),
1092 ('/%(trunk)s/interleaved/e', 'M'),
1095 def check_numbers(rev):
1096 """Check if REV is the rev where only numbers were committed."""
1098 conv.logs[rev].check('Committing numbers only.', (
1099 ('/%(trunk)s/interleaved/1', 'M'),
1100 ('/%(trunk)s/interleaved/2', 'M'),
1101 ('/%(trunk)s/interleaved/3', 'M'),
1102 ('/%(trunk)s/interleaved/4', 'M'),
1103 ('/%(trunk)s/interleaved/5', 'M'),
1106 # One of the commits was letters only, the other was numbers only.
1107 # But they happened "simultaneously", so we don't assume anything
1108 # about which commit appeared first, so we just try both ways.
1109 rev += 1
1110 try:
1111 check_letters(rev)
1112 check_numbers(rev + 1)
1113 except Failure:
1114 check_numbers(rev)
1115 check_letters(rev + 1)
1118 @Cvs2SvnTestFunction
1119 def simple_commits():
1120 "simple trunk commits"
1121 # See test-data/main-cvsrepos/proj/README.
1122 conv = ensure_conversion('main')
1124 # The initial import.
1125 conv.logs[13].check('Initial import.', (
1126 ('/%(trunk)s/proj', 'A'),
1127 ('/%(trunk)s/proj/default', 'A'),
1128 ('/%(trunk)s/proj/sub1', 'A'),
1129 ('/%(trunk)s/proj/sub1/default', 'A'),
1130 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1131 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1132 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1133 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1134 ('/%(trunk)s/proj/sub2', 'A'),
1135 ('/%(trunk)s/proj/sub2/default', 'A'),
1136 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1137 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1138 ('/%(trunk)s/proj/sub3', 'A'),
1139 ('/%(trunk)s/proj/sub3/default', 'A'),
1142 # The first commit.
1143 conv.logs[18].check('First commit to proj, affecting two files.', (
1144 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1145 ('/%(trunk)s/proj/sub3/default', 'M'),
1148 # The second commit.
1149 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1150 ('/%(trunk)s/proj/default', 'M'),
1151 ('/%(trunk)s/proj/sub1/default', 'M'),
1152 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1153 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1154 ('/%(trunk)s/proj/sub2/default', 'M'),
1155 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1156 ('/%(trunk)s/proj/sub3/default', 'M')
1160 class SimpleTags(Cvs2SvnTestCase):
1161 "simple tags and branches, no commits"
1163 def __init__(self, **kw):
1164 # See test-data/main-cvsrepos/proj/README.
1165 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1167 def run(self, sbox):
1168 conv = self.ensure_conversion()
1170 # Verify the copy source for the tags we are about to check
1171 # No need to verify the copyfrom revision, as simple_commits did that
1172 conv.logs[13].check('Initial import.', (
1173 ('/%(trunk)s/proj', 'A'),
1174 ('/%(trunk)s/proj/default', 'A'),
1175 ('/%(trunk)s/proj/sub1', 'A'),
1176 ('/%(trunk)s/proj/sub1/default', 'A'),
1177 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1178 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1179 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1180 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1181 ('/%(trunk)s/proj/sub2', 'A'),
1182 ('/%(trunk)s/proj/sub2/default', 'A'),
1183 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1184 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1185 ('/%(trunk)s/proj/sub3', 'A'),
1186 ('/%(trunk)s/proj/sub3/default', 'A'),
1189 # Tag on rev 1.1.1.1 of all files in proj
1190 conv.logs[16].check(sym_log_msg('B_FROM_INITIALS'), (
1191 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1192 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1193 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1196 # The same, as a tag
1197 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1198 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1199 ('/%(tags)s/T_ALL_INITIAL_FILES (from /%(trunk)s:13)', 'A'),
1200 ('/%(tags)s/T_ALL_INITIAL_FILES/single-files', 'D'),
1201 ('/%(tags)s/T_ALL_INITIAL_FILES/partial-prune', 'D'),
1204 # Tag on rev 1.1.1.1 of all files in proj, except one
1205 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1206 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1207 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE (from /%(trunk)s:13)', 'A'),
1208 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/single-files', 'D'),
1209 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/partial-prune', 'D'),
1210 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1213 # The same, as a branch
1214 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1215 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE (from /%(trunk)s:13)', 'A'),
1216 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1217 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/single-files', 'D'),
1218 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/partial-prune', 'D'),
1222 @Cvs2SvnTestFunction
1223 def simple_branch_commits():
1224 "simple branch commits"
1225 # See test-data/main-cvsrepos/proj/README.
1226 conv = ensure_conversion('main')
1228 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1229 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1230 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1231 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1235 @Cvs2SvnTestFunction
1236 def mixed_time_tag():
1237 "mixed-time tag"
1238 # See test-data/main-cvsrepos/proj/README.
1239 conv = ensure_conversion('main')
1241 log = conv.find_tag_log('T_MIXED')
1242 log.check_changes((
1243 ('/%(tags)s/T_MIXED (from /%(trunk)s:19)', 'A'),
1244 ('/%(tags)s/T_MIXED/single-files', 'D'),
1245 ('/%(tags)s/T_MIXED/partial-prune', 'D'),
1246 ('/%(tags)s/T_MIXED/proj/sub2/subsubA '
1247 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1248 ('/%(tags)s/T_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1252 @Cvs2SvnTestFunction
1253 def mixed_time_branch_with_added_file():
1254 "mixed-time branch, and a file added to the branch"
1255 # See test-data/main-cvsrepos/proj/README.
1256 conv = ensure_conversion('main')
1258 # A branch from the same place as T_MIXED in the previous test,
1259 # plus a file added directly to the branch
1260 conv.logs[21].check(sym_log_msg('B_MIXED'), (
1261 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1262 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1263 ('/%(branches)s/B_MIXED/single-files', 'D'),
1264 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1265 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1266 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1269 conv.logs[22].check('Add a file on branch B_MIXED.', (
1270 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1274 @Cvs2SvnTestFunction
1275 def mixed_commit():
1276 "a commit affecting both trunk and a branch"
1277 # See test-data/main-cvsrepos/proj/README.
1278 conv = ensure_conversion('main')
1280 conv.logs[24].check(
1281 'A single commit affecting one file on branch B_MIXED '
1282 'and one on trunk.', (
1283 ('/%(trunk)s/proj/sub2/default', 'M'),
1284 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1288 @Cvs2SvnTestFunction
1289 def split_time_branch():
1290 "branch some trunk files, and later branch the rest"
1291 # See test-data/main-cvsrepos/proj/README.
1292 conv = ensure_conversion('main')
1294 # First change on the branch, creating it
1295 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1296 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1297 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1298 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1299 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1302 conv.logs[29].check('First change on branch B_SPLIT.', (
1303 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1304 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1305 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1306 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1307 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1310 # A trunk commit for the file which was not branched
1311 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1312 'This was committed about an', (
1313 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1316 # Add the file not already branched to the branch, with modification:w
1317 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1318 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1319 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1322 conv.logs[32].check('This change affects sub3/default and '
1323 'sub1/subsubB/default, on branch', (
1324 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1325 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1329 @Cvs2SvnTestFunction
1330 def multiple_tags():
1331 "multiple tags referring to same revision"
1332 conv = ensure_conversion('main')
1333 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1334 raise Failure()
1335 if not conv.path_exists(
1336 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1337 raise Failure()
1340 @Cvs2SvnTestFunction
1341 def multiply_defined_symbols():
1342 "multiple definitions of symbol names"
1344 # We can only check one line of the error output at a time, so test
1345 # twice. (The conversion only have to be done once because the
1346 # results are cached.)
1347 conv = ensure_conversion(
1348 'multiply-defined-symbols',
1349 error_re=(
1350 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1351 r"1\.2\.4 1\.2\.2"
1354 conv = ensure_conversion(
1355 'multiply-defined-symbols',
1356 error_re=(
1357 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1358 r"1\.2 1\.1"
1363 @Cvs2SvnTestFunction
1364 def multiply_defined_symbols_renamed():
1365 "rename multiply defined symbols"
1367 conv = ensure_conversion(
1368 'multiply-defined-symbols',
1369 options_file='cvs2svn-rename.options',
1373 @Cvs2SvnTestFunction
1374 def multiply_defined_symbols_ignored():
1375 "ignore multiply defined symbols"
1377 conv = ensure_conversion(
1378 'multiply-defined-symbols',
1379 options_file='cvs2svn-ignore.options',
1383 @Cvs2SvnTestFunction
1384 def repeatedly_defined_symbols():
1385 "multiple identical definitions of symbol names"
1387 # If a symbol is defined multiple times but has the same value each
1388 # time, that should not be an error.
1390 conv = ensure_conversion('repeatedly-defined-symbols')
1393 @Cvs2SvnTestFunction
1394 def bogus_tag():
1395 "conversion of invalid symbolic names"
1396 conv = ensure_conversion('bogus-tag')
1399 @Cvs2SvnTestFunction
1400 def overlapping_branch():
1401 "ignore a file with a branch with two names"
1402 conv = ensure_conversion(
1403 'overlapping-branch',
1404 verbosity='-qq',
1405 error_re='.*cannot also have name \'vendorB\'',
1408 conv.logs[2].check('imported', (
1409 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1410 ('/%(trunk)s/overlapping-branch', 'A'),
1413 if len(conv.logs) != 2:
1414 raise Failure()
1417 class PhoenixBranch(Cvs2SvnTestCase):
1418 "convert a branch file rooted in a 'dead' revision"
1420 def __init__(self, **kw):
1421 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1423 def run(self, sbox):
1424 conv = self.ensure_conversion()
1425 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1426 ('/%(branches)s/volsung_20010721', 'A'),
1427 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1429 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1430 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1434 ###TODO: We check for 4 changed paths here to accomodate creating tags
1435 ###and branches in rev 1, but that will change, so this will
1436 ###eventually change back.
1437 @Cvs2SvnTestFunction
1438 def ctrl_char_in_log():
1439 "handle a control char in a log message"
1440 # This was issue #1106.
1441 rev = 2
1442 conv = ensure_conversion('ctrl-char-in-log')
1443 conv.logs[rev].check_changes((
1444 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1446 if conv.logs[rev].msg.find('\x04') < 0:
1447 raise Failure(
1448 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1451 @Cvs2SvnTestFunction
1452 def overdead():
1453 "handle tags rooted in a redeleted revision"
1454 conv = ensure_conversion('overdead')
1457 class NoTrunkPrune(Cvs2SvnTestCase):
1458 "ensure that trunk doesn't get pruned"
1460 def __init__(self, **kw):
1461 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1463 def run(self, sbox):
1464 conv = self.ensure_conversion()
1465 for rev in conv.logs.keys():
1466 rev_logs = conv.logs[rev]
1467 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1468 raise Failure()
1471 @Cvs2SvnTestFunction
1472 def double_delete():
1473 "file deleted twice, in the root of the repository"
1474 # This really tests several things: how we handle a file that's
1475 # removed (state 'dead') in two successive revisions; how we
1476 # handle a file in the root of the repository (there were some
1477 # bugs in cvs2svn's svn path construction for top-level files); and
1478 # the --no-prune option.
1479 conv = ensure_conversion(
1480 'double-delete', args=['--trunk-only', '--no-prune'])
1482 path = '/%(trunk)s/twice-removed'
1483 rev = 2
1484 conv.logs[rev].check('Updated CVS', (
1485 (path, 'A'),
1487 conv.logs[rev + 1].check('Remove this file for the first time.', (
1488 (path, 'D'),
1490 conv.logs[rev + 2].check('Remove this file for the second time,', (
1494 @Cvs2SvnTestFunction
1495 def split_branch():
1496 "branch created from both trunk and another branch"
1497 # See test-data/split-branch-cvsrepos/README.
1499 # The conversion will fail if the bug is present, and
1500 # ensure_conversion will raise Failure.
1501 conv = ensure_conversion('split-branch')
1504 @Cvs2SvnTestFunction
1505 def resync_misgroups():
1506 "resyncing should not misorder commit groups"
1507 # See test-data/resync-misgroups-cvsrepos/README.
1509 # The conversion will fail if the bug is present, and
1510 # ensure_conversion will raise Failure.
1511 conv = ensure_conversion('resync-misgroups')
1514 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1515 "allow tags with mixed trunk and branch sources"
1517 def __init__(self, **kw):
1518 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1520 def run(self, sbox):
1521 conv = self.ensure_conversion()
1523 tags = conv.symbols.get('tags', 'tags')
1525 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1526 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1527 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1528 raise Failure()
1529 if (open(a_path, 'r').read().find('1.24') == -1) \
1530 or (open(b_path, 'r').read().find('1.5') == -1):
1531 raise Failure()
1534 @Cvs2SvnTestFunction
1535 def enroot_race():
1536 "never use the rev-in-progress as a copy source"
1538 # See issue #1427 and r8544.
1539 conv = ensure_conversion('enroot-race')
1540 rev = 6
1541 conv.logs[rev].check_changes((
1542 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1543 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1544 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1546 conv.logs[rev + 1].check_changes((
1547 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1548 ('/%(trunk)s/proj/a.txt', 'M'),
1549 ('/%(trunk)s/proj/b.txt', 'M'),
1553 @Cvs2SvnTestFunction
1554 def enroot_race_obo():
1555 "do use the last completed rev as a copy source"
1556 conv = ensure_conversion('enroot-race-obo')
1557 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1558 if not len(conv.logs) == 3:
1559 raise Failure()
1562 class BranchDeleteFirst(Cvs2SvnTestCase):
1563 "correctly handle deletion as initial branch action"
1565 def __init__(self, **kw):
1566 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1568 def run(self, sbox):
1569 # See test-data/branch-delete-first-cvsrepos/README.
1571 # The conversion will fail if the bug is present, and
1572 # ensure_conversion would raise Failure.
1573 conv = self.ensure_conversion()
1575 branches = conv.symbols.get('branches', 'branches')
1577 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1578 if conv.path_exists(branches, 'branch-1', 'file'):
1579 raise Failure()
1580 if conv.path_exists(branches, 'branch-2', 'file'):
1581 raise Failure()
1582 if not conv.path_exists(branches, 'branch-3', 'file'):
1583 raise Failure()
1586 @Cvs2SvnTestFunction
1587 def nonascii_cvsignore():
1588 "non ascii files in .cvsignore"
1590 # The output seems to be in the C locale, where it looks like this
1591 # (at least on one test system):
1592 expected = (
1593 'Sp?\\195?\\164tzle\n'
1594 'Cr?\\195?\\168meBr?\\195?\\187l?\\195?\\169e\n'
1595 'Jam?\\195?\\179nIb?\\195?\\169rico\n'
1596 'Am?\\195?\\170ijoas?\\195?\\128Bulh?\\195?\\163oPato\n'
1599 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1600 props = props_for_path(conv.get_wc_tree(), 'trunk/single-files')
1602 if props['svn:ignore'] != expected:
1603 raise Failure()
1606 @Cvs2SvnTestFunction
1607 def nonascii_filenames():
1608 "non ascii files converted incorrectly"
1609 # see issue #1255
1611 # on a en_US.iso-8859-1 machine this test fails with
1612 # svn: Can't recode ...
1614 # as described in the issue
1616 # on a en_US.UTF-8 machine this test fails with
1617 # svn: Malformed XML ...
1619 # which means at least it fails. Unfortunately it won't fail
1620 # with the same error...
1622 # mangle current locale settings so we know we're not running
1623 # a UTF-8 locale (which does not exhibit this problem)
1624 current_locale = locale.getlocale()
1625 new_locale = 'en_US.ISO8859-1'
1626 locale_changed = None
1628 # From http://docs.python.org/lib/module-sys.html
1630 # getfilesystemencoding():
1632 # Return the name of the encoding used to convert Unicode filenames
1633 # into system file names, or None if the system default encoding is
1634 # used. The result value depends on the operating system:
1636 # - On Windows 9x, the encoding is ``mbcs''.
1637 # - On Mac OS X, the encoding is ``utf-8''.
1638 # - On Unix, the encoding is the user's preference according to the
1639 # result of nl_langinfo(CODESET), or None if the
1640 # nl_langinfo(CODESET) failed.
1641 # - On Windows NT+, file names are Unicode natively, so no conversion is
1642 # performed.
1644 # So we're going to skip this test on Mac OS X for now.
1645 if sys.platform == "darwin":
1646 raise svntest.Skip()
1648 try:
1649 # change locale to non-UTF-8 locale to generate latin1 names
1650 locale.setlocale(locale.LC_ALL, # this might be too broad?
1651 new_locale)
1652 locale_changed = 1
1653 except locale.Error:
1654 raise svntest.Skip()
1656 try:
1657 srcrepos_path = os.path.join(test_data_dir, 'non-ascii-cvsrepos')
1658 dstrepos_path = os.path.join(test_data_dir, 'non-ascii-copy-cvsrepos')
1659 if not os.path.exists(dstrepos_path):
1660 # create repos from existing main repos
1661 shutil.copytree(srcrepos_path, dstrepos_path)
1662 base_path = os.path.join(dstrepos_path, 'single-files')
1663 os.remove(os.path.join(base_path, '.cvsignore,v'))
1664 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1665 os.path.join(base_path, 'two\366uick,v'))
1666 new_path = os.path.join(dstrepos_path, 'single\366files')
1667 os.rename(base_path, new_path)
1669 conv = ensure_conversion('non-ascii-copy', args=['--encoding=latin1'])
1670 finally:
1671 if locale_changed:
1672 locale.setlocale(locale.LC_ALL, current_locale)
1673 safe_rmtree(dstrepos_path)
1676 class UnicodeTest(Cvs2SvnTestCase):
1677 "metadata contains Unicode"
1679 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1681 def __init__(self, name, warning_expected, **kw):
1682 if warning_expected:
1683 error_re = self.warning_pattern
1684 else:
1685 error_re = None
1687 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1688 self.warning_expected = warning_expected
1690 def run(self, sbox):
1691 try:
1692 # ensure the availability of the "utf_8" encoding:
1693 u'a'.encode('utf_8').decode('utf_8')
1694 except LookupError:
1695 raise svntest.Skip()
1697 self.ensure_conversion()
1700 class UnicodeAuthor(UnicodeTest):
1701 "author name contains Unicode"
1703 def __init__(self, warning_expected, **kw):
1704 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1707 class UnicodeLog(UnicodeTest):
1708 "log message contains Unicode"
1710 def __init__(self, warning_expected, **kw):
1711 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1714 @Cvs2SvnTestFunction
1715 def vendor_branch_sameness():
1716 "avoid spurious changes for initial revs"
1717 conv = ensure_conversion(
1718 'vendor-branch-sameness', args=['--keep-trivial-imports']
1721 # The following files are in this repository:
1723 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1724 # the same contents, the file's default branch is 1.1.1,
1725 # and both revisions are in state 'Exp'.
1727 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1728 # 1.1 (the addition of a line of text).
1730 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1732 # d.txt: This file was created by 'cvs add' instead of import, so
1733 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1734 # The timestamp on the add is exactly the same as for the
1735 # imports of the other files.
1737 # e.txt: Like a.txt, except that the log message for revision 1.1
1738 # is not the standard import log message.
1740 # (Aside from e.txt, the log messages for the same revisions are the
1741 # same in all files.)
1743 # We expect that only a.txt is recognized as an import whose 1.1
1744 # revision can be omitted. The other files should be added on trunk
1745 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1746 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1747 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1748 # and c.txt should be 'D'eleted.
1750 rev = 2
1751 conv.logs[rev].check('Initial revision', (
1752 ('/%(trunk)s/proj', 'A'),
1753 ('/%(trunk)s/proj/b.txt', 'A'),
1754 ('/%(trunk)s/proj/c.txt', 'A'),
1755 ('/%(trunk)s/proj/d.txt', 'A'),
1758 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1759 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1760 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1763 conv.logs[rev + 2].check('First vendor branch revision.', (
1764 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1765 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1766 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1769 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1770 'to compensate for changes in r4,', (
1771 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1772 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1773 ('/%(trunk)s/proj/c.txt', 'D'),
1776 rev = 7
1777 conv.logs[rev].check('This log message is not the standard', (
1778 ('/%(trunk)s/proj/e.txt', 'A'),
1781 conv.logs[rev + 2].check('First vendor branch revision', (
1782 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1785 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1786 'to compensate for changes in r9,', (
1787 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1791 @Cvs2SvnTestFunction
1792 def vendor_branch_trunk_only():
1793 "handle vendor branches with --trunk-only"
1794 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1796 rev = 2
1797 conv.logs[rev].check('Initial revision', (
1798 ('/%(trunk)s/proj', 'A'),
1799 ('/%(trunk)s/proj/b.txt', 'A'),
1800 ('/%(trunk)s/proj/c.txt', 'A'),
1801 ('/%(trunk)s/proj/d.txt', 'A'),
1804 conv.logs[rev + 1].check('First vendor branch revision', (
1805 ('/%(trunk)s/proj/a.txt', 'A'),
1806 ('/%(trunk)s/proj/b.txt', 'M'),
1807 ('/%(trunk)s/proj/c.txt', 'D'),
1810 conv.logs[rev + 2].check('This log message is not the standard', (
1811 ('/%(trunk)s/proj/e.txt', 'A'),
1814 conv.logs[rev + 3].check('First vendor branch revision', (
1815 ('/%(trunk)s/proj/e.txt', 'M'),
1819 @Cvs2SvnTestFunction
1820 def default_branches():
1821 "handle default branches correctly"
1822 conv = ensure_conversion('default-branches')
1824 # There are seven files in the repository:
1826 # a.txt:
1827 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1828 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1829 # committed (thus losing the default branch "1.1.1"), then
1830 # 1.1.1.4 was imported. All vendor import release tags are
1831 # still present.
1833 # b.txt:
1834 # Like a.txt, but without rev 1.2.
1836 # c.txt:
1837 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1839 # d.txt:
1840 # Same as the previous two, but 1.1.1 branch is unlabeled.
1842 # e.txt:
1843 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1845 # deleted-on-vendor-branch.txt,v:
1846 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1848 # added-then-imported.txt,v:
1849 # Added with 'cvs add' to create 1.1, then imported with
1850 # completely different contents to create 1.1.1.1, therefore
1851 # never had a default branch.
1854 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1855 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1856 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1857 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1858 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1859 ('/%(branches)s/vbranchA', 'A'),
1860 ('/%(branches)s/vbranchA/proj', 'A'),
1861 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1862 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1863 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1864 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1867 conv.logs[3].check("This commit was generated by cvs2svn "
1868 "to compensate for changes in r2,", (
1869 ('/%(trunk)s/proj', 'A'),
1870 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1871 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1872 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1873 ('/%(trunk)s/proj/d.txt '
1874 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1875 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1876 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1877 ('/%(trunk)s/proj/e.txt '
1878 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1881 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1882 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1883 ('/%(tags)s/vtag-1/proj/d.txt '
1884 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1887 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1888 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1889 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1890 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1891 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1892 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1893 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1896 conv.logs[6].check("This commit was generated by cvs2svn "
1897 "to compensate for changes in r5,", (
1898 ('/%(trunk)s/proj/a.txt '
1899 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1900 ('/%(trunk)s/proj/b.txt '
1901 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1902 ('/%(trunk)s/proj/c.txt '
1903 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1904 ('/%(trunk)s/proj/d.txt '
1905 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1906 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1907 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1908 'R'),
1909 ('/%(trunk)s/proj/e.txt '
1910 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1913 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1914 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1915 ('/%(tags)s/vtag-2/proj/d.txt '
1916 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1919 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1920 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1921 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1922 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1923 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1924 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1925 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1928 conv.logs[9].check("This commit was generated by cvs2svn "
1929 "to compensate for changes in r8,", (
1930 ('/%(trunk)s/proj/a.txt '
1931 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1932 ('/%(trunk)s/proj/b.txt '
1933 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1934 ('/%(trunk)s/proj/c.txt '
1935 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1936 ('/%(trunk)s/proj/d.txt '
1937 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1938 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1939 ('/%(trunk)s/proj/e.txt '
1940 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1943 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1944 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1945 ('/%(tags)s/vtag-3/proj/d.txt '
1946 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1947 ('/%(tags)s/vtag-3/proj/e.txt '
1948 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1951 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1952 ('/%(trunk)s/proj/a.txt', 'M'),
1955 conv.logs[12].check("Add a file to the working copy.", (
1956 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1959 conv.logs[13].check(sym_log_msg('vbranchA'), (
1960 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1961 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1964 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1965 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1966 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1967 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1968 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1969 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1970 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1971 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1974 conv.logs[15].check("This commit was generated by cvs2svn "
1975 "to compensate for changes in r14,", (
1976 ('/%(trunk)s/proj/b.txt '
1977 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1978 ('/%(trunk)s/proj/c.txt '
1979 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1980 ('/%(trunk)s/proj/d.txt '
1981 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1982 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1983 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1984 'A'),
1985 ('/%(trunk)s/proj/e.txt '
1986 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1989 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1990 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1991 ('/%(tags)s/vtag-4/proj/d.txt '
1992 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1996 @Cvs2SvnTestFunction
1997 def default_branches_trunk_only():
1998 "handle default branches with --trunk-only"
2000 conv = ensure_conversion('default-branches', args=['--trunk-only'])
2002 conv.logs[2].check("Import (vbranchA, vtag-1).", (
2003 ('/%(trunk)s/proj', 'A'),
2004 ('/%(trunk)s/proj/a.txt', 'A'),
2005 ('/%(trunk)s/proj/b.txt', 'A'),
2006 ('/%(trunk)s/proj/c.txt', 'A'),
2007 ('/%(trunk)s/proj/d.txt', 'A'),
2008 ('/%(trunk)s/proj/e.txt', 'A'),
2009 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
2012 conv.logs[3].check("Import (vbranchA, vtag-2).", (
2013 ('/%(trunk)s/proj/a.txt', 'M'),
2014 ('/%(trunk)s/proj/b.txt', 'M'),
2015 ('/%(trunk)s/proj/c.txt', 'M'),
2016 ('/%(trunk)s/proj/d.txt', 'M'),
2017 ('/%(trunk)s/proj/e.txt', 'M'),
2018 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
2021 conv.logs[4].check("Import (vbranchA, vtag-3).", (
2022 ('/%(trunk)s/proj/a.txt', 'M'),
2023 ('/%(trunk)s/proj/b.txt', 'M'),
2024 ('/%(trunk)s/proj/c.txt', 'M'),
2025 ('/%(trunk)s/proj/d.txt', 'M'),
2026 ('/%(trunk)s/proj/e.txt', 'M'),
2027 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
2030 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
2031 ('/%(trunk)s/proj/a.txt', 'M'),
2034 conv.logs[6].check("Add a file to the working copy.", (
2035 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
2038 conv.logs[7].check("Import (vbranchA, vtag-4).", (
2039 ('/%(trunk)s/proj/b.txt', 'M'),
2040 ('/%(trunk)s/proj/c.txt', 'M'),
2041 ('/%(trunk)s/proj/d.txt', 'M'),
2042 ('/%(trunk)s/proj/e.txt', 'M'),
2043 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
2047 @Cvs2SvnTestFunction
2048 def default_branch_and_1_2():
2049 "do not allow 1.2 revision with default branch"
2051 conv = ensure_conversion(
2052 'default-branch-and-1-2',
2053 error_re=(
2054 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
2059 @Cvs2SvnTestFunction
2060 def compose_tag_three_sources():
2061 "compose a tag from three sources"
2062 conv = ensure_conversion('compose-tag-three-sources')
2064 conv.logs[2].check("Add on trunk", (
2065 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
2066 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
2067 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
2068 ('/%(trunk)s/tagged-on-b1', 'A'),
2069 ('/%(trunk)s/tagged-on-b2', 'A'),
2072 conv.logs[3].check(sym_log_msg('b1'), (
2073 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
2076 conv.logs[4].check(sym_log_msg('b2'), (
2077 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
2080 conv.logs[5].check("Commit on branch b1", (
2081 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
2082 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
2083 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
2084 ('/%(branches)s/b1/tagged-on-b1', 'M'),
2085 ('/%(branches)s/b1/tagged-on-b2', 'M'),
2088 conv.logs[6].check("Commit on branch b2", (
2089 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
2090 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
2091 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
2092 ('/%(branches)s/b2/tagged-on-b1', 'M'),
2093 ('/%(branches)s/b2/tagged-on-b2', 'M'),
2096 conv.logs[7].check("Commit again on trunk", (
2097 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
2098 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
2099 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
2100 ('/%(trunk)s/tagged-on-b1', 'M'),
2101 ('/%(trunk)s/tagged-on-b2', 'M'),
2104 conv.logs[8].check(sym_log_msg('T',1), (
2105 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
2106 ('/%(tags)s/T/tagged-on-trunk-1.1 '
2107 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
2108 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
2109 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
2113 @Cvs2SvnTestFunction
2114 def pass5_when_to_fill():
2115 "reserve a svn revnum for a fill only when required"
2116 # The conversion will fail if the bug is present, and
2117 # ensure_conversion would raise Failure.
2118 conv = ensure_conversion('pass5-when-to-fill')
2121 class EmptyTrunk(Cvs2SvnTestCase):
2122 "don't break when the trunk is empty"
2124 def __init__(self, **kw):
2125 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
2127 def run(self, sbox):
2128 # The conversion will fail if the bug is present, and
2129 # ensure_conversion would raise Failure.
2130 conv = self.ensure_conversion()
2133 @Cvs2SvnTestFunction
2134 def no_spurious_svn_commits():
2135 "ensure that we don't create any spurious commits"
2136 conv = ensure_conversion('phoenix')
2138 # Check spurious commit that could be created in
2139 # SVNCommitCreator._pre_commit()
2141 # (When you add a file on a branch, CVS creates a trunk revision
2142 # in state 'dead'. If the log message of that commit is equal to
2143 # the one that CVS generates, we do not ever create a 'fill'
2144 # SVNCommit for it.)
2146 # and spurious commit that could be created in
2147 # SVNCommitCreator._commit()
2149 # (When you add a file on a branch, CVS creates a trunk revision
2150 # in state 'dead'. If the log message of that commit is equal to
2151 # the one that CVS generates, we do not create a primary SVNCommit
2152 # for it.)
2153 conv.logs[17].check('File added on branch xiphophorus', (
2154 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2157 # Check to make sure that a commit *is* generated:
2158 # (When you add a file on a branch, CVS creates a trunk revision
2159 # in state 'dead'. If the log message of that commit is NOT equal
2160 # to the one that CVS generates, we create a primary SVNCommit to
2161 # serve as a home for the log message in question.
2162 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2163 + 'branch xiphophorus,\nand this log message was tweaked', ())
2165 # Check spurious commit that could be created in
2166 # SVNCommitCreator._commit_symbols().
2167 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2168 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2172 class PeerPathPruning(Cvs2SvnTestCase):
2173 "make sure that filling prunes paths correctly"
2175 def __init__(self, **kw):
2176 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2178 def run(self, sbox):
2179 conv = self.ensure_conversion()
2180 conv.logs[6].check(sym_log_msg('BRANCH'), (
2181 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2182 ('/%(branches)s/BRANCH/bar', 'D'),
2183 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2187 @Cvs2SvnTestFunction
2188 def invalid_closings_on_trunk():
2189 "verify correct revs are copied to default branches"
2190 # The conversion will fail if the bug is present, and
2191 # ensure_conversion would raise Failure.
2192 conv = ensure_conversion('invalid-closings-on-trunk')
2195 @Cvs2SvnTestFunction
2196 def individual_passes():
2197 "run each pass individually"
2198 conv = ensure_conversion('main')
2199 conv2 = ensure_conversion('main', passbypass=1)
2201 if conv.logs != conv2.logs:
2202 raise Failure()
2205 @Cvs2SvnTestFunction
2206 def resync_bug():
2207 "reveal a big bug in our resync algorithm"
2208 # This will fail if the bug is present
2209 conv = ensure_conversion('resync-bug')
2212 @Cvs2SvnTestFunction
2213 def branch_from_default_branch():
2214 "reveal a bug in our default branch detection code"
2215 conv = ensure_conversion('branch-from-default-branch')
2217 # This revision will be a default branch synchronization only
2218 # if cvs2svn is correctly determining default branch revisions.
2220 # The bug was that cvs2svn was treating revisions on branches off of
2221 # default branches as default branch revisions, resulting in
2222 # incorrectly regarding the branch off of the default branch as a
2223 # non-trunk default branch. Crystal clear? I thought so. See
2224 # issue #42 for more incoherent blathering.
2225 conv.logs[5].check("This commit was generated by cvs2svn", (
2226 ('/%(trunk)s/proj/file.txt '
2227 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2231 @Cvs2SvnTestFunction
2232 def file_in_attic_too():
2233 "die if a file exists in and out of the attic"
2234 ensure_conversion(
2235 'file-in-attic-too',
2236 error_re=(
2237 r'.*A CVS repository cannot contain both '
2238 r'(.*)' + re.escape(os.sep) + r'(.*) '
2239 + r'and '
2240 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2245 @Cvs2SvnTestFunction
2246 def retain_file_in_attic_too():
2247 "test --retain-conflicting-attic-files option"
2248 conv = ensure_conversion(
2249 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2250 if not conv.path_exists('trunk', 'file.txt'):
2251 raise Failure()
2252 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2253 raise Failure()
2256 @Cvs2SvnTestFunction
2257 def symbolic_name_filling_guide():
2258 "reveal a big bug in our SymbolFillingGuide"
2259 # This will fail if the bug is present
2260 conv = ensure_conversion('symbolic-name-overfill')
2263 # Helpers for tests involving file contents and properties.
2265 class NodeTreeWalkException:
2266 "Exception class for node tree traversals."
2267 pass
2269 def node_for_path(node, path):
2270 "In the tree rooted under SVNTree NODE, return the node at PATH."
2271 if node.name != '__SVN_ROOT_NODE':
2272 raise NodeTreeWalkException()
2273 path = path.strip('/')
2274 components = path.split('/')
2275 for component in components:
2276 node = svntest.tree.get_child(node, component)
2277 return node
2279 # Helper for tests involving properties.
2280 def props_for_path(node, path):
2281 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2282 return node_for_path(node, path).props
2285 class EOLMime(Cvs2SvnPropertiesTestCase):
2286 """eol settings and mime types together
2288 The files are as follows:
2290 trunk/foo.txt: no -kb, mime file says nothing.
2291 trunk/foo.xml: no -kb, mime file says text.
2292 trunk/foo.zip: no -kb, mime file says non-text.
2293 trunk/foo.bin: has -kb, mime file says nothing.
2294 trunk/foo.csv: has -kb, mime file says text.
2295 trunk/foo.dbf: has -kb, mime file says non-text.
2298 def __init__(self, args, **kw):
2299 # TODO: It's a bit klugey to construct this path here. But so far
2300 # there's only one test with a mime.types file. If we have more,
2301 # we should abstract this into some helper, which would be located
2302 # near ensure_conversion(). Note that it is a convention of this
2303 # test suite for a mime.types file to be located in the top level
2304 # of the CVS repository to which it applies.
2305 self.mime_path = os.path.join(
2306 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2308 Cvs2SvnPropertiesTestCase.__init__(
2309 self, 'eol-mime',
2310 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2311 args=['--mime-types=%s' % self.mime_path] + args,
2312 **kw)
2315 # We do four conversions. Each time, we pass --mime-types=FILE with
2316 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2317 # Thus there's one conversion with neither flag, one with just the
2318 # former, one with just the latter, and one with both.
2321 # Neither --no-default-eol nor --eol-from-mime-type:
2322 eol_mime1 = EOLMime(
2323 variant=1,
2324 args=[],
2325 expected_props=[
2326 ('trunk/foo.txt', [None, None, None]),
2327 ('trunk/foo.xml', [None, 'text/xml', None]),
2328 ('trunk/foo.zip', [None, 'application/zip', None]),
2329 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2330 ('trunk/foo.csv', [None, 'text/csv', None]),
2331 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2335 # Just --no-default-eol, not --eol-from-mime-type:
2336 eol_mime2 = EOLMime(
2337 variant=2,
2338 args=['--default-eol=native'],
2339 expected_props=[
2340 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2341 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2342 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2343 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2344 ('trunk/foo.csv', [None, 'text/csv', None]),
2345 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2349 # Just --eol-from-mime-type, not --no-default-eol:
2350 eol_mime3 = EOLMime(
2351 variant=3,
2352 args=['--eol-from-mime-type'],
2353 expected_props=[
2354 ('trunk/foo.txt', [None, None, None]),
2355 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2356 ('trunk/foo.zip', [None, 'application/zip', None]),
2357 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2358 ('trunk/foo.csv', [None, 'text/csv', None]),
2359 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2363 # Both --no-default-eol and --eol-from-mime-type:
2364 eol_mime4 = EOLMime(
2365 variant=4,
2366 args=['--eol-from-mime-type', '--default-eol=native'],
2367 expected_props=[
2368 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2369 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2370 ('trunk/foo.zip', [None, 'application/zip', None]),
2371 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2372 ('trunk/foo.csv', [None, 'text/csv', None]),
2373 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2377 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2378 'eol-mime',
2379 doc='test non-setting of cvs2svn:cvs-rev property',
2380 args=[],
2381 props_to_test=['cvs2svn:cvs-rev'],
2382 expected_props=[
2383 ('trunk/foo.txt', [None]),
2384 ('trunk/foo.xml', [None]),
2385 ('trunk/foo.zip', [None]),
2386 ('trunk/foo.bin', [None]),
2387 ('trunk/foo.csv', [None]),
2388 ('trunk/foo.dbf', [None]),
2392 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2393 'eol-mime',
2394 doc='test setting of cvs2svn:cvs-rev property',
2395 args=['--cvs-revnums'],
2396 props_to_test=['cvs2svn:cvs-rev'],
2397 expected_props=[
2398 ('trunk/foo.txt', ['1.2']),
2399 ('trunk/foo.xml', ['1.2']),
2400 ('trunk/foo.zip', ['1.2']),
2401 ('trunk/foo.bin', ['1.2']),
2402 ('trunk/foo.csv', ['1.2']),
2403 ('trunk/foo.dbf', ['1.2']),
2407 keywords = Cvs2SvnPropertiesTestCase(
2408 'keywords',
2409 doc='test setting of svn:keywords property among others',
2410 args=['--default-eol=native'],
2411 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2412 expected_props=[
2413 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2414 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2415 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2416 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2417 ('trunk/foo.kk', [None, 'native', None]),
2418 ('trunk/foo.ko', [None, 'native', None]),
2419 ('trunk/foo.kv', [None, 'native', None]),
2423 @Cvs2SvnTestFunction
2424 def ignore():
2425 "test setting of svn:ignore property"
2426 conv = ensure_conversion('cvsignore')
2427 wc_tree = conv.get_wc_tree()
2428 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2429 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2431 if topdir_props['svn:ignore'] != \
2432 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n':
2433 raise Failure()
2435 if subdir_props['svn:ignore'] != \
2436 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n':
2437 raise Failure()
2440 @Cvs2SvnTestFunction
2441 def requires_cvs():
2442 "test that CVS can still do what RCS can't"
2443 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2444 conv = ensure_conversion(
2445 'requires-cvs', args=['--use-cvs', '--default-eol=native'],
2448 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2449 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2451 if atsign_contents[-1:] == "@":
2452 raise Failure()
2453 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2454 raise Failure()
2456 if not (conv.logs[6].author == "William Lyon Phelps III" and
2457 conv.logs[5].author == "j random"):
2458 raise Failure()
2461 @Cvs2SvnTestFunction
2462 def questionable_branch_names():
2463 "test that we can handle weird branch names"
2464 conv = ensure_conversion('questionable-symbols')
2465 # If the conversion succeeds, then we're okay. We could check the
2466 # actual branch paths, too, but the main thing is to know that the
2467 # conversion doesn't fail.
2470 @Cvs2SvnTestFunction
2471 def questionable_tag_names():
2472 "test that we can handle weird tag names"
2473 conv = ensure_conversion('questionable-symbols')
2474 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2475 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2477 conv.find_tag_log('TagWith/Backslash_E').check(
2478 sym_log_msg('TagWith/Backslash_E',1),
2480 ('/%(tags)s/TagWith', 'A'),
2481 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2484 conv.find_tag_log('TagWith/Slash_Z').check(
2485 sym_log_msg('TagWith/Slash_Z',1),
2487 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2492 @Cvs2SvnTestFunction
2493 def revision_reorder_bug():
2494 "reveal a bug that reorders file revisions"
2495 conv = ensure_conversion('revision-reorder-bug')
2496 # If the conversion succeeds, then we're okay. We could check the
2497 # actual revisions, too, but the main thing is to know that the
2498 # conversion doesn't fail.
2501 @Cvs2SvnTestFunction
2502 def exclude():
2503 "test that exclude really excludes everything"
2504 conv = ensure_conversion('main', args=['--exclude=.*'])
2505 for log in conv.logs.values():
2506 for item in log.changed_paths.keys():
2507 if item.startswith('/branches/') or item.startswith('/tags/'):
2508 raise Failure()
2511 @Cvs2SvnTestFunction
2512 def vendor_branch_delete_add():
2513 "add trunk file that was deleted on vendor branch"
2514 # This will error if the bug is present
2515 conv = ensure_conversion('vendor-branch-delete-add')
2518 @Cvs2SvnTestFunction
2519 def resync_pass2_pull_forward():
2520 "ensure pass2 doesn't pull rev too far forward"
2521 conv = ensure_conversion('resync-pass2-pull-forward')
2522 # If the conversion succeeds, then we're okay. We could check the
2523 # actual revisions, too, but the main thing is to know that the
2524 # conversion doesn't fail.
2527 @Cvs2SvnTestFunction
2528 def native_eol():
2529 "only LFs for svn:eol-style=native files"
2530 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2531 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2532 conv.repos)
2533 # Verify that all files in the dump have LF EOLs. We're actually
2534 # testing the whole dump file, but the dump file itself only uses
2535 # LF EOLs, so we're safe.
2536 for line in lines:
2537 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2538 raise Failure()
2541 @Cvs2SvnTestFunction
2542 def double_fill():
2543 "reveal a bug that created a branch twice"
2544 conv = ensure_conversion('double-fill')
2545 # If the conversion succeeds, then we're okay. We could check the
2546 # actual revisions, too, but the main thing is to know that the
2547 # conversion doesn't fail.
2550 @XFail_deco()
2551 @Cvs2SvnTestFunction
2552 def double_fill2():
2553 "reveal a second bug that created a branch twice"
2554 conv = ensure_conversion('double-fill2')
2555 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2556 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2557 try:
2558 # This check should fail:
2559 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2560 except Failure:
2561 pass
2562 else:
2563 raise Failure('Symbol filled twice in a row')
2566 @Cvs2SvnTestFunction
2567 def resync_pass2_push_backward():
2568 "ensure pass2 doesn't push rev too far backward"
2569 conv = ensure_conversion('resync-pass2-push-backward')
2570 # If the conversion succeeds, then we're okay. We could check the
2571 # actual revisions, too, but the main thing is to know that the
2572 # conversion doesn't fail.
2575 @Cvs2SvnTestFunction
2576 def double_add():
2577 "reveal a bug that added a branch file twice"
2578 conv = ensure_conversion('double-add')
2579 # If the conversion succeeds, then we're okay. We could check the
2580 # actual revisions, too, but the main thing is to know that the
2581 # conversion doesn't fail.
2584 @Cvs2SvnTestFunction
2585 def bogus_branch_copy():
2586 "reveal a bug that copies a branch file wrongly"
2587 conv = ensure_conversion('bogus-branch-copy')
2588 # If the conversion succeeds, then we're okay. We could check the
2589 # actual revisions, too, but the main thing is to know that the
2590 # conversion doesn't fail.
2593 @Cvs2SvnTestFunction
2594 def nested_ttb_directories():
2595 "require error if ttb directories are not disjoint"
2596 opts_list = [
2597 {'trunk' : 'a', 'branches' : 'a',},
2598 {'trunk' : 'a', 'tags' : 'a',},
2599 {'branches' : 'a', 'tags' : 'a',},
2600 # This option conflicts with the default trunk path:
2601 {'branches' : 'trunk',},
2602 # Try some nested directories:
2603 {'trunk' : 'a', 'branches' : 'a/b',},
2604 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2605 {'branches' : 'a', 'tags' : 'a/b',},
2608 for opts in opts_list:
2609 ensure_conversion(
2610 'main', error_re=r'The following paths are not disjoint\:', **opts
2614 class AutoProps(Cvs2SvnPropertiesTestCase):
2615 """Test auto-props.
2617 The files are as follows:
2619 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2620 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2621 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2622 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2623 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2624 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2625 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2626 trunk/foo.UPCASE1: no -kb, no mime type.
2627 trunk/foo.UPCASE2: no -kb, no mime type.
2630 def __init__(self, args, **kw):
2631 ### TODO: It's a bit klugey to construct this path here. See also
2632 ### the comment in eol_mime().
2633 auto_props_path = os.path.join(
2634 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2636 Cvs2SvnPropertiesTestCase.__init__(
2637 self, 'eol-mime',
2638 props_to_test=[
2639 'myprop',
2640 'svn:eol-style',
2641 'svn:mime-type',
2642 'svn:keywords',
2643 'svn:executable',
2645 args=[
2646 '--auto-props=%s' % auto_props_path,
2647 '--eol-from-mime-type'
2648 ] + args,
2649 **kw)
2652 auto_props_ignore_case = AutoProps(
2653 doc="test auto-props",
2654 args=['--default-eol=native'],
2655 expected_props=[
2656 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2657 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2658 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2659 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2660 ('trunk/foo.bin',
2661 ['bin', None, 'application/octet-stream', None, '']),
2662 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2663 ('trunk/foo.dbf',
2664 ['dbf', None, 'application/what-is-dbf', None, None]),
2665 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2666 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2670 @Cvs2SvnTestFunction
2671 def ctrl_char_in_filename():
2672 "do not allow control characters in filenames"
2674 try:
2675 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2676 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2677 if os.path.exists(dstrepos_path):
2678 safe_rmtree(dstrepos_path)
2680 # create repos from existing main repos
2681 shutil.copytree(srcrepos_path, dstrepos_path)
2682 base_path = os.path.join(dstrepos_path, 'single-files')
2683 try:
2684 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2685 os.path.join(base_path, 'two\rquick,v'))
2686 except:
2687 # Operating systems that don't allow control characters in
2688 # filenames will hopefully have thrown an exception; in that
2689 # case, just skip this test.
2690 raise svntest.Skip()
2692 conv = ensure_conversion(
2693 'ctrl-char-filename',
2694 error_re=(r'.*Subversion does not allow character .*.'),
2696 finally:
2697 safe_rmtree(dstrepos_path)
2700 @Cvs2SvnTestFunction
2701 def commit_dependencies():
2702 "interleaved and multi-branch commits to same files"
2703 conv = ensure_conversion("commit-dependencies")
2704 conv.logs[2].check('adding', (
2705 ('/%(trunk)s/interleaved', 'A'),
2706 ('/%(trunk)s/interleaved/file1', 'A'),
2707 ('/%(trunk)s/interleaved/file2', 'A'),
2709 conv.logs[3].check('big commit', (
2710 ('/%(trunk)s/interleaved/file1', 'M'),
2711 ('/%(trunk)s/interleaved/file2', 'M'),
2713 conv.logs[4].check('dependant small commit', (
2714 ('/%(trunk)s/interleaved/file1', 'M'),
2716 conv.logs[5].check('adding', (
2717 ('/%(trunk)s/multi-branch', 'A'),
2718 ('/%(trunk)s/multi-branch/file1', 'A'),
2719 ('/%(trunk)s/multi-branch/file2', 'A'),
2721 conv.logs[6].check(sym_log_msg("branch"), (
2722 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2723 ('/%(branches)s/branch/interleaved', 'D'),
2725 conv.logs[7].check('multi-branch-commit', (
2726 ('/%(trunk)s/multi-branch/file1', 'M'),
2727 ('/%(trunk)s/multi-branch/file2', 'M'),
2728 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2729 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2733 @Cvs2SvnTestFunction
2734 def double_branch_delete():
2735 "fill branches before modifying files on them"
2736 conv = ensure_conversion('double-branch-delete')
2738 # Test for issue #102. The file IMarshalledValue.java is branched,
2739 # deleted, readded on the branch, and then deleted again. If the
2740 # fill for the file on the branch is postponed until after the
2741 # modification, the file will end up live on the branch instead of
2742 # dead! Make sure it happens at the right time.
2744 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2745 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2748 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2749 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2753 @Cvs2SvnTestFunction
2754 def symbol_mismatches():
2755 "error for conflicting tag/branch"
2757 ensure_conversion(
2758 'symbol-mess',
2759 args=['--symbol-default=strict'],
2760 error_re=r'.*Problems determining how symbols should be converted',
2764 @Cvs2SvnTestFunction
2765 def overlook_symbol_mismatches():
2766 "overlook conflicting tag/branch when --trunk-only"
2768 # This is a test for issue #85.
2770 ensure_conversion('symbol-mess', args=['--trunk-only'])
2773 @Cvs2SvnTestFunction
2774 def force_symbols():
2775 "force symbols to be tags/branches"
2777 conv = ensure_conversion(
2778 'symbol-mess',
2779 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2780 if conv.path_exists('tags', 'BRANCH') \
2781 or not conv.path_exists('branches', 'BRANCH'):
2782 raise Failure()
2783 if not conv.path_exists('tags', 'TAG') \
2784 or conv.path_exists('branches', 'TAG'):
2785 raise Failure()
2786 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2787 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2788 raise Failure()
2789 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2790 or conv.path_exists('branches', 'MOSTLY_TAG'):
2791 raise Failure()
2794 @Cvs2SvnTestFunction
2795 def commit_blocks_tags():
2796 "commit prevents forced tag"
2798 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2799 ensure_conversion(
2800 'symbol-mess',
2801 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2802 error_re=(
2803 r'.*The following branches cannot be forced to be tags '
2804 r'because they have commits'
2809 @Cvs2SvnTestFunction
2810 def blocked_excludes():
2811 "error for blocked excludes"
2813 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2814 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2815 try:
2816 ensure_conversion(
2817 'symbol-mess',
2818 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2819 raise MissingErrorException()
2820 except Failure:
2821 pass
2824 @Cvs2SvnTestFunction
2825 def unblock_blocked_excludes():
2826 "excluding blocker removes blockage"
2828 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2829 for blocker in ['BRANCH', 'COMMIT']:
2830 ensure_conversion(
2831 'symbol-mess',
2832 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2833 '--exclude=BLOCKING_%s' % blocker]))
2836 @Cvs2SvnTestFunction
2837 def regexp_force_symbols():
2838 "force symbols via regular expressions"
2840 conv = ensure_conversion(
2841 'symbol-mess',
2842 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2843 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2844 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2845 raise Failure()
2846 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2847 or conv.path_exists('branches', 'MOSTLY_TAG'):
2848 raise Failure()
2851 @Cvs2SvnTestFunction
2852 def heuristic_symbol_default():
2853 "test 'heuristic' symbol default"
2855 conv = ensure_conversion(
2856 'symbol-mess', args=['--symbol-default=heuristic'])
2857 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2858 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2859 raise Failure()
2860 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2861 or conv.path_exists('branches', 'MOSTLY_TAG'):
2862 raise Failure()
2865 @Cvs2SvnTestFunction
2866 def branch_symbol_default():
2867 "test 'branch' symbol default"
2869 conv = ensure_conversion(
2870 'symbol-mess', args=['--symbol-default=branch'])
2871 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2872 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2873 raise Failure()
2874 if conv.path_exists('tags', 'MOSTLY_TAG') \
2875 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2876 raise Failure()
2879 @Cvs2SvnTestFunction
2880 def tag_symbol_default():
2881 "test 'tag' symbol default"
2883 conv = ensure_conversion(
2884 'symbol-mess', args=['--symbol-default=tag'])
2885 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2886 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2887 raise Failure()
2888 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2889 or conv.path_exists('branches', 'MOSTLY_TAG'):
2890 raise Failure()
2893 @Cvs2SvnTestFunction
2894 def symbol_transform():
2895 "test --symbol-transform"
2897 conv = ensure_conversion(
2898 'symbol-mess',
2899 args=[
2900 '--symbol-default=heuristic',
2901 '--symbol-transform=BRANCH:branch',
2902 '--symbol-transform=TAG:tag',
2903 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2905 if not conv.path_exists('branches', 'branch'):
2906 raise Failure()
2907 if not conv.path_exists('tags', 'tag'):
2908 raise Failure()
2909 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2910 raise Failure()
2911 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2912 raise Failure()
2915 @Cvs2SvnTestFunction
2916 def write_symbol_info():
2917 "test --write-symbol-info"
2919 expected_lines = [
2920 ['0', '.trunk.',
2921 'trunk', 'trunk', '.'],
2922 ['0', 'BLOCKED_BY_UNNAMED',
2923 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2924 ['0', 'BLOCKING_COMMIT',
2925 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2926 ['0', 'BLOCKED_BY_COMMIT',
2927 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2928 ['0', 'BLOCKING_BRANCH',
2929 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2930 ['0', 'BLOCKED_BY_BRANCH',
2931 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2932 ['0', 'MOSTLY_BRANCH',
2933 '.', '.', '.'],
2934 ['0', 'MOSTLY_TAG',
2935 '.', '.', '.'],
2936 ['0', 'BRANCH_WITH_COMMIT',
2937 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2938 ['0', 'BRANCH',
2939 'branch', 'branches/BRANCH', '.trunk.'],
2940 ['0', 'TAG',
2941 'tag', 'tags/TAG', '.trunk.'],
2942 ['0', 'unlabeled-1.1.12.1.2',
2943 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2945 expected_lines.sort()
2947 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2948 try:
2949 ensure_conversion(
2950 'symbol-mess',
2951 args=[
2952 '--symbol-default=strict',
2953 '--write-symbol-info=%s' % (symbol_info_file,),
2954 '--passes=:CollateSymbolsPass',
2957 raise MissingErrorException()
2958 except Failure:
2959 pass
2960 lines = []
2961 comment_re = re.compile(r'^\s*\#')
2962 for l in open(symbol_info_file, 'r'):
2963 if comment_re.match(l):
2964 continue
2965 lines.append(l.strip().split())
2966 lines.sort()
2967 if lines != expected_lines:
2968 s = ['Symbol info incorrect\n']
2969 differ = Differ()
2970 for diffline in differ.compare(
2971 [' '.join(line) + '\n' for line in expected_lines],
2972 [' '.join(line) + '\n' for line in lines],
2974 s.append(diffline)
2975 raise Failure(''.join(s))
2978 @Cvs2SvnTestFunction
2979 def symbol_hints():
2980 "test --symbol-hints for setting branch/tag"
2982 conv = ensure_conversion(
2983 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2985 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2986 raise Failure()
2987 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2988 raise Failure()
2989 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2990 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2992 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2993 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2995 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2996 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
3000 @Cvs2SvnTestFunction
3001 def parent_hints():
3002 "test --symbol-hints for setting parent"
3004 conv = ensure_conversion(
3005 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
3007 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
3008 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
3012 @Cvs2SvnTestFunction
3013 def parent_hints_invalid():
3014 "test --symbol-hints with an invalid parent"
3016 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
3017 # this symbol hints file sets the preferred parent to BRANCH
3018 # instead:
3019 conv = ensure_conversion(
3020 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
3021 error_re=(
3022 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
3027 @Cvs2SvnTestFunction
3028 def parent_hints_wildcards():
3029 "test --symbol-hints wildcards"
3031 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
3032 # this symbol hints file sets the preferred parent to BRANCH
3033 # instead:
3034 conv = ensure_conversion(
3035 'symbol-mess',
3036 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
3038 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
3039 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
3043 @Cvs2SvnTestFunction
3044 def path_hints():
3045 "test --symbol-hints for setting svn paths"
3047 conv = ensure_conversion(
3048 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
3050 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
3051 ('/trunk', 'A'),
3052 ('/a', 'A'),
3053 ('/a/strange', 'A'),
3054 ('/a/strange/trunk', 'A'),
3055 ('/a/strange/trunk/path', 'A'),
3056 ('/branches', 'A'),
3057 ('/tags', 'A'),
3059 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
3060 ('/special', 'A'),
3061 ('/special/tag', 'A'),
3062 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
3064 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
3065 ('/special/other', 'A'),
3066 ('/special/other/branch', 'A'),
3067 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
3069 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
3070 ('/special/branch', 'A'),
3071 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
3075 @Cvs2SvnTestFunction
3076 def issue_99():
3077 "test problem from issue 99"
3079 conv = ensure_conversion('issue-99')
3082 @Cvs2SvnTestFunction
3083 def issue_100():
3084 "test problem from issue 100"
3086 conv = ensure_conversion('issue-100')
3087 file1 = conv.get_wc('trunk', 'file1.txt')
3088 if file(file1).read() != 'file1.txt<1.2>\n':
3089 raise Failure()
3092 @Cvs2SvnTestFunction
3093 def issue_106():
3094 "test problem from issue 106"
3096 conv = ensure_conversion('issue-106')
3099 @Cvs2SvnTestFunction
3100 def options_option():
3101 "use of the --options option"
3103 conv = ensure_conversion('main', options_file='cvs2svn.options')
3106 @Cvs2SvnTestFunction
3107 def multiproject():
3108 "multiproject conversion"
3110 conv = ensure_conversion(
3111 'main', options_file='cvs2svn-multiproject.options'
3113 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
3114 ('/partial-prune', 'A'),
3115 ('/partial-prune/trunk', 'A'),
3116 ('/partial-prune/branches', 'A'),
3117 ('/partial-prune/tags', 'A'),
3118 ('/partial-prune/releases', 'A'),
3122 @Cvs2SvnTestFunction
3123 def crossproject():
3124 "multiproject conversion with cross-project commits"
3126 conv = ensure_conversion(
3127 'main', options_file='cvs2svn-crossproject.options'
3131 @Cvs2SvnTestFunction
3132 def tag_with_no_revision():
3133 "tag defined but revision is deleted"
3135 conv = ensure_conversion('tag-with-no-revision')
3138 @Cvs2SvnTestFunction
3139 def delete_cvsignore():
3140 "svn:ignore should vanish when .cvsignore does"
3142 # This is issue #81.
3144 conv = ensure_conversion('delete-cvsignore')
3146 wc_tree = conv.get_wc_tree()
3147 props = props_for_path(wc_tree, 'trunk/proj')
3149 if props.has_key('svn:ignore'):
3150 raise Failure()
3153 @Cvs2SvnTestFunction
3154 def repeated_deltatext():
3155 "ignore repeated deltatext blocks with warning"
3157 conv = ensure_conversion(
3158 'repeated-deltatext',
3159 verbosity='-qq',
3160 error_re=r'.*Deltatext block for revision 1.1 appeared twice',
3164 @Cvs2SvnTestFunction
3165 def nasty_graphs():
3166 "process some nasty dependency graphs"
3168 # It's not how well the bear can dance, but that the bear can dance
3169 # at all:
3170 conv = ensure_conversion('nasty-graphs')
3173 @XFail_deco()
3174 @Cvs2SvnTestFunction
3175 def tagging_after_delete():
3176 "optimal tag after deleting files"
3178 conv = ensure_conversion('tagging-after-delete')
3180 # tag should be 'clean', no deletes
3181 log = conv.find_tag_log('tag1')
3182 expected = (
3183 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
3185 log.check_changes(expected)
3188 @Cvs2SvnTestFunction
3189 def crossed_branches():
3190 "branches created in inconsistent orders"
3192 conv = ensure_conversion('crossed-branches')
3195 @Cvs2SvnTestFunction
3196 def file_directory_conflict():
3197 "error when filename conflicts with directory name"
3199 conv = ensure_conversion(
3200 'file-directory-conflict',
3201 error_re=r'.*Directory name conflicts with filename',
3205 @Cvs2SvnTestFunction
3206 def attic_directory_conflict():
3207 "error when attic filename conflicts with dirname"
3209 # This tests the problem reported in issue #105.
3211 conv = ensure_conversion(
3212 'attic-directory-conflict',
3213 error_re=r'.*Directory name conflicts with filename',
3217 @Cvs2SvnTestFunction
3218 def use_rcs():
3219 "verify that --use-rcs and --use-internal-co agree"
3221 rcs_conv = ensure_conversion(
3222 'main', args=['--use-rcs', '--default-eol=native'], dumpfile='use-rcs-rcs.dump',
3224 conv = ensure_conversion(
3225 'main', args=['--default-eol=native'], dumpfile='use-rcs-int.dump',
3227 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3228 raise Failure()
3229 rcs_lines = list(open(rcs_conv.dumpfile, 'rb'))
3230 lines = list(open(conv.dumpfile, 'rb'))
3231 # Compare all lines following the repository UUID:
3232 if lines[3:] != rcs_lines[3:]:
3233 raise Failure()
3236 @Cvs2SvnTestFunction
3237 def internal_co_exclude():
3238 "verify that --use-internal-co --exclude=... works"
3240 rcs_conv = ensure_conversion(
3241 'internal-co',
3242 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3243 dumpfile='internal-co-exclude-rcs.dump',
3245 conv = ensure_conversion(
3246 'internal-co',
3247 args=['--exclude=BRANCH', '--default-eol=native'],
3248 dumpfile='internal-co-exclude-int.dump',
3250 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3251 raise Failure()
3252 rcs_lines = list(open(rcs_conv.dumpfile, 'rb'))
3253 lines = list(open(conv.dumpfile, 'rb'))
3254 # Compare all lines following the repository UUID:
3255 if lines[3:] != rcs_lines[3:]:
3256 raise Failure()
3259 @Cvs2SvnTestFunction
3260 def internal_co_trunk_only():
3261 "verify that --use-internal-co --trunk-only works"
3263 conv = ensure_conversion(
3264 'internal-co',
3265 args=['--trunk-only', '--default-eol=native'],
3267 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3268 raise Failure()
3271 @Cvs2SvnTestFunction
3272 def leftover_revs():
3273 "check for leftover checked-out revisions"
3275 conv = ensure_conversion(
3276 'leftover-revs',
3277 args=['--exclude=BRANCH', '--default-eol=native'],
3279 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3280 raise Failure()
3283 @Cvs2SvnTestFunction
3284 def requires_internal_co():
3285 "test that internal co can do more than RCS"
3286 # See issues 4, 11 for the bugs whose regression we're testing for.
3287 # Unlike in requires_cvs above, issue 29 is not covered.
3288 conv = ensure_conversion('requires-cvs')
3290 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3292 if atsign_contents[-1:] == "@":
3293 raise Failure()
3295 if not (conv.logs[6].author == "William Lyon Phelps III" and
3296 conv.logs[5].author == "j random"):
3297 raise Failure()
3300 @Cvs2SvnTestFunction
3301 def internal_co_keywords():
3302 "test that internal co handles keywords correctly"
3303 conv_ic = ensure_conversion('internal-co-keywords',
3304 args=["--keywords-off"])
3305 conv_cvs = ensure_conversion('internal-co-keywords',
3306 args=["--use-cvs", "--keywords-off"])
3308 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3309 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3310 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3311 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3312 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3313 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3314 # Ensure proper "/Attic" expansion of $Source$ keyword in files
3315 # which are in a deleted state in trunk
3316 del_ic = file(conv_ic.get_wc('branches/b', 'dir', 'kv-deleted.txt')).read()
3317 del_cvs = file(conv_cvs.get_wc('branches/b', 'dir', 'kv-deleted.txt')).read()
3320 if ko_ic != ko_cvs:
3321 raise Failure()
3322 if kk_ic != kk_cvs:
3323 raise Failure()
3324 if del_ic != del_cvs:
3325 raise Failure()
3327 # The date format changed between cvs and co ('/' instead of '-').
3328 # Accept either one:
3329 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3330 if kv_ic != kv_cvs \
3331 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3332 raise Failure()
3335 @Cvs2SvnTestFunction
3336 def timestamp_chaos():
3337 "test timestamp adjustments"
3339 conv = ensure_conversion('timestamp-chaos')
3341 # The times are expressed here in UTC:
3342 times = [
3343 '2007-01-01 21:00:00', # Initial commit
3344 '2007-01-01 21:00:00', # revision 1.1 of both files
3345 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3346 '2007-01-01 21:00:02', # revision 1.2 of file2.txt, adjusted backwards
3347 '2007-01-01 22:00:00', # revision 1.3 of both files
3350 # Convert the times to seconds since the epoch, in UTC:
3351 times = [calendar.timegm(svn_strptime(t)) for t in times]
3353 for i in range(len(times)):
3354 if abs(conv.logs[i + 1].date - times[i]) > 0.1:
3355 raise Failure()
3358 @Cvs2SvnTestFunction
3359 def symlinks():
3360 "convert a repository that contains symlinks"
3362 # This is a test for issue #97.
3364 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3365 links = [
3367 os.path.join('..', 'file.txt,v'),
3368 os.path.join(proj, 'dir1', 'file.txt,v'),
3371 'dir1',
3372 os.path.join(proj, 'dir2'),
3376 try:
3377 os.symlink
3378 except AttributeError:
3379 # Apparently this OS doesn't support symlinks, so skip test.
3380 raise svntest.Skip()
3382 try:
3383 for (src,dst) in links:
3384 os.symlink(src, dst)
3386 conv = ensure_conversion('symlinks')
3387 conv.logs[2].check('', (
3388 ('/%(trunk)s/proj', 'A'),
3389 ('/%(trunk)s/proj/file.txt', 'A'),
3390 ('/%(trunk)s/proj/dir1', 'A'),
3391 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3392 ('/%(trunk)s/proj/dir2', 'A'),
3393 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3395 finally:
3396 for (src,dst) in links:
3397 os.remove(dst)
3400 @Cvs2SvnTestFunction
3401 def empty_trunk_path():
3402 "allow --trunk to be empty if --trunk-only"
3404 # This is a test for issue #53.
3406 conv = ensure_conversion(
3407 'main', args=['--trunk-only', '--trunk='],
3411 @Cvs2SvnTestFunction
3412 def preferred_parent_cycle():
3413 "handle a cycle in branch parent preferences"
3415 conv = ensure_conversion('preferred-parent-cycle')
3418 @Cvs2SvnTestFunction
3419 def branch_from_empty_dir():
3420 "branch from an empty directory"
3422 conv = ensure_conversion('branch-from-empty-dir')
3425 @Cvs2SvnTestFunction
3426 def trunk_readd():
3427 "add a file on a branch then on trunk"
3429 conv = ensure_conversion('trunk-readd')
3432 @Cvs2SvnTestFunction
3433 def branch_from_deleted_1_1():
3434 "branch from a 1.1 revision that will be deleted"
3436 conv = ensure_conversion('branch-from-deleted-1-1')
3437 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3438 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3440 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3441 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3443 conv.logs[7].check('Adding b.txt:1.2', (
3444 ('/%(trunk)s/proj/b.txt', 'A'),
3447 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3448 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3450 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3451 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3455 @Cvs2SvnTestFunction
3456 def add_on_branch():
3457 "add a file on a branch using newer CVS"
3459 conv = ensure_conversion('add-on-branch')
3460 conv.logs[6].check('Adding b.txt:1.1', (
3461 ('/%(trunk)s/proj/b.txt', 'A'),
3463 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3464 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3466 conv.logs[8].check('Adding c.txt:1.1', (
3467 ('/%(trunk)s/proj/c.txt', 'A'),
3469 conv.logs[9].check('Removing c.txt:1.2', (
3470 ('/%(trunk)s/proj/c.txt', 'D'),
3472 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3473 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3475 conv.logs[11].check('Adding d.txt:1.1', (
3476 ('/%(trunk)s/proj/d.txt', 'A'),
3478 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3479 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3483 @Cvs2SvnTestFunction
3484 def main_git():
3485 "test output in git-fast-import format"
3487 # Note: To test importing into git, do
3489 # ./run-tests <this-test-number>
3490 # rm -rf cvs2svn-tmp/main.git
3491 # git init --bare cvs2svn-tmp/main.git
3492 # cd cvs2svn-tmp/main.git
3493 # cat ../git-{blob,dump}.dat | git fast-import
3495 # Or, to load the dumpfiles separately:
3497 # cat ../git-blob.dat | git fast-import --export-marks=../git-marks.dat
3498 # cat ../git-dump.dat | git fast-import --import-marks=../git-marks.dat
3500 # Then use "gitk --all", "git log", etc. to test the contents of the
3501 # repository or "git clone" to make a non-bare clone.
3503 # We don't have the infrastructure to check that the resulting git
3504 # repository is correct, so we just check that the conversion runs
3505 # to completion:
3506 conv = GitConversion('main', None, [
3507 '--blobfile=cvs2svn-tmp/git-blob.dat',
3508 '--dumpfile=cvs2svn-tmp/git-dump.dat',
3509 '--username=cvs2git',
3510 'test-data/main-cvsrepos',
3514 @Cvs2SvnTestFunction
3515 def main_git2():
3516 "test cvs2git --use-external-blob-generator option"
3518 # See comment in main_git() for more information.
3520 conv = GitConversion('main', None, [
3521 '--use-external-blob-generator',
3522 '--blobfile=cvs2svn-tmp/blobfile.out',
3523 '--dumpfile=cvs2svn-tmp/dumpfile.out',
3524 '--username=cvs2git',
3525 'test-data/main-cvsrepos',
3529 @Cvs2SvnTestFunction
3530 def git_options():
3531 "test cvs2git using options file"
3533 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3536 @Cvs2SvnTestFunction
3537 def main_hg():
3538 "output in git-fast-import format with inline data"
3540 # The output should be suitable for import by Mercurial.
3542 # We don't have the infrastructure to check that the resulting
3543 # Mercurial repository is correct, so we just check that the
3544 # conversion runs to completion:
3545 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3548 @Cvs2SvnTestFunction
3549 def invalid_symbol():
3550 "a symbol with the incorrect format"
3552 conv = ensure_conversion(
3553 'invalid-symbol',
3554 verbosity='-qq',
3555 error_re=r".*branch 'SYMBOL' references invalid revision 1$",
3559 @Cvs2SvnTestFunction
3560 def invalid_symbol_ignore():
3561 "ignore a symbol using a SymbolMapper"
3563 conv = ensure_conversion(
3564 'invalid-symbol', options_file='cvs2svn-ignore.options'
3568 @Cvs2SvnTestFunction
3569 def invalid_symbol_ignore2():
3570 "ignore a symbol using an IgnoreSymbolTransform"
3572 conv = ensure_conversion(
3573 'invalid-symbol', options_file='cvs2svn-ignore2.options'
3577 class EOLVariants(Cvs2SvnTestCase):
3578 "handle various --eol-style options"
3580 eol_style_strings = {
3581 'LF' : '\n',
3582 'CR' : '\r',
3583 'CRLF' : '\r\n',
3584 'native' : '\n',
3587 def __init__(self, eol_style):
3588 self.eol_style = eol_style
3589 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3590 Cvs2SvnTestCase.__init__(
3591 self, 'eol-variants', variant=self.eol_style,
3592 dumpfile=self.dumpfile,
3593 args=[
3594 '--default-eol=%s' % (self.eol_style,),
3598 def run(self, sbox):
3599 conv = self.ensure_conversion()
3600 dump_contents = open(conv.dumpfile, 'rb').read()
3601 expected_text = self.eol_style_strings[self.eol_style].join(
3602 ['line 1', 'line 2', '\n\n']
3604 if not dump_contents.endswith(expected_text):
3605 raise Failure()
3608 @Cvs2SvnTestFunction
3609 def no_revs_file():
3610 "handle a file with no revisions (issue #80)"
3612 conv = ensure_conversion('no-revs-file')
3615 @Cvs2SvnTestFunction
3616 def mirror_keyerror_test():
3617 "a case that gave KeyError in SVNRepositoryMirror"
3619 conv = ensure_conversion('mirror-keyerror')
3622 @Cvs2SvnTestFunction
3623 def exclude_ntdb_test():
3624 "exclude a non-trunk default branch"
3626 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3627 conv = ensure_conversion(
3628 'exclude-ntdb',
3629 args=[
3630 '--write-symbol-info=%s' % (symbol_info_file,),
3631 '--exclude=branch3',
3632 '--exclude=tag3',
3633 '--exclude=vendortag3',
3634 '--exclude=vendorbranch',
3639 @Cvs2SvnTestFunction
3640 def mirror_keyerror2_test():
3641 "a case that gave KeyError in RepositoryMirror"
3643 conv = ensure_conversion('mirror-keyerror2')
3646 @Cvs2SvnTestFunction
3647 def mirror_keyerror3_test():
3648 "a case that gave KeyError in RepositoryMirror"
3650 conv = ensure_conversion('mirror-keyerror3')
3653 @XFail_deco()
3654 @Cvs2SvnTestFunction
3655 def add_cvsignore_to_branch_test():
3656 "check adding .cvsignore to an existing branch"
3658 # This a test for issue #122.
3660 conv = ensure_conversion('add-cvsignore-to-branch')
3661 wc_tree = conv.get_wc_tree()
3662 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3663 if trunk_props['svn:ignore'] != '*.o\n\n':
3664 raise Failure()
3666 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3667 if branch_props['svn:ignore'] != '*.o\n\n':
3668 raise Failure()
3671 @Cvs2SvnTestFunction
3672 def missing_deltatext():
3673 "a revision's deltatext is missing"
3675 # This is a type of RCS file corruption that has been observed.
3676 conv = ensure_conversion(
3677 'missing-deltatext',
3678 error_re=(
3679 r"ERROR\: .* has no deltatext section for revision 1\.1\.4\.4"
3684 @Cvs2SvnTestFunction
3685 def transform_unlabeled_branch_name():
3686 "transform name of unlabeled branch"
3688 conv = ensure_conversion(
3689 'unlabeled-branch',
3690 args=[
3691 '--symbol-transform=unlabeled-1.1.4:BRANCH2',
3694 if conv.path_exists('branches', 'unlabeled-1.1.4'):
3695 raise Failure('Branch unlabeled-1.1.4 not excluded')
3696 if not conv.path_exists('branches', 'BRANCH2'):
3697 raise Failure('Branch BRANCH2 not found')
3700 @Cvs2SvnTestFunction
3701 def ignore_unlabeled_branch():
3702 "ignoring an unlabeled branch is not allowed"
3704 conv = ensure_conversion(
3705 'unlabeled-branch',
3706 options_file='cvs2svn-ignore.options',
3707 error_re=(
3708 r"ERROR\: The unlabeled branch \'unlabeled\-1\.1\.4\' "
3709 r"in \'.*\' contains commits"
3714 @Cvs2SvnTestFunction
3715 def exclude_unlabeled_branch():
3716 "exclude unlabeled branch"
3718 conv = ensure_conversion(
3719 'unlabeled-branch',
3720 args=['--exclude=unlabeled-.*'],
3722 if conv.path_exists('branches', 'unlabeled-1.1.4'):
3723 raise Failure('Branch unlabeled-1.1.4 not excluded')
3726 @Cvs2SvnTestFunction
3727 def unlabeled_branch_name_collision():
3728 "transform unlabeled branch to same name as branch"
3730 conv = ensure_conversion(
3731 'unlabeled-branch',
3732 args=[
3733 '--symbol-transform=unlabeled-1.1.4:BRANCH',
3735 error_re=(
3736 r"ERROR\: Symbol name \'BRANCH\' is already used"
3741 @Cvs2SvnTestFunction
3742 def collision_with_unlabeled_branch_name():
3743 "transform branch to same name as unlabeled branch"
3745 conv = ensure_conversion(
3746 'unlabeled-branch',
3747 args=[
3748 '--symbol-transform=BRANCH:unlabeled-1.1.4',
3750 error_re=(
3751 r"ERROR\: Symbol name \'unlabeled\-1\.1\.4\' is already used"
3756 @Cvs2SvnTestFunction
3757 def many_deletes():
3758 "a repo with many removable dead revisions"
3760 conv = ensure_conversion('many-deletes')
3761 conv.logs[5].check('Add files on BRANCH', (
3762 ('/%(branches)s/BRANCH/proj/b.txt', 'A'),
3764 conv.logs[6].check('Add files on BRANCH2', (
3765 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3766 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3767 ('/%(branches)s/BRANCH2/proj/d.txt', 'A'),
3771 cvs_description = Cvs2SvnPropertiesTestCase(
3772 'main',
3773 doc='test handling of CVS file descriptions',
3774 props_to_test=['cvs:description'],
3775 expected_props=[
3776 ('trunk/proj/default', ['This is an example file description.']),
3777 ('trunk/proj/sub1/default', [None]),
3781 @Cvs2SvnTestFunction
3782 def include_empty_directories():
3783 "test --include-empty-directories option"
3785 conv = ensure_conversion(
3786 'empty-directories', args=['--include-empty-directories'],
3788 conv.logs[1].check('Standard project directories', (
3789 ('/%(trunk)s', 'A'),
3790 ('/%(branches)s', 'A'),
3791 ('/%(tags)s', 'A'),
3792 ('/%(trunk)s/root-empty-directory', 'A'),
3793 ('/%(trunk)s/root-empty-directory/empty-subdirectory', 'A'),
3795 conv.logs[3].check('Add b.txt.', (
3796 ('/%(trunk)s/direct', 'A'),
3797 ('/%(trunk)s/direct/b.txt', 'A'),
3798 ('/%(trunk)s/direct/empty-directory', 'A'),
3799 ('/%(trunk)s/direct/empty-directory/empty-subdirectory', 'A'),
3801 conv.logs[4].check('Add c.txt.', (
3802 ('/%(trunk)s/indirect', 'A'),
3803 ('/%(trunk)s/indirect/subdirectory', 'A'),
3804 ('/%(trunk)s/indirect/subdirectory/c.txt', 'A'),
3805 ('/%(trunk)s/indirect/empty-directory', 'A'),
3806 ('/%(trunk)s/indirect/empty-directory/empty-subdirectory', 'A'),
3808 conv.logs[5].check('Remove b.txt', (
3809 ('/%(trunk)s/direct', 'D'),
3811 conv.logs[6].check('Remove c.txt', (
3812 ('/%(trunk)s/indirect', 'D'),
3814 conv.logs[7].check('Re-add b.txt.', (
3815 ('/%(trunk)s/direct', 'A'),
3816 ('/%(trunk)s/direct/b.txt', 'A'),
3817 ('/%(trunk)s/direct/empty-directory', 'A'),
3818 ('/%(trunk)s/direct/empty-directory/empty-subdirectory', 'A'),
3820 conv.logs[8].check('Re-add c.txt.', (
3821 ('/%(trunk)s/indirect', 'A'),
3822 ('/%(trunk)s/indirect/subdirectory', 'A'),
3823 ('/%(trunk)s/indirect/subdirectory/c.txt', 'A'),
3824 ('/%(trunk)s/indirect/empty-directory', 'A'),
3825 ('/%(trunk)s/indirect/empty-directory/empty-subdirectory', 'A'),
3827 conv.logs[9].check('This commit was manufactured', (
3828 ('/%(tags)s/TAG (from /%(trunk)s:8)', 'A'),
3830 conv.logs[10].check('This commit was manufactured', (
3831 ('/%(branches)s/BRANCH (from /%(trunk)s:8)', 'A'),
3833 conv.logs[11].check('Import d.txt.', (
3834 ('/%(branches)s/VENDORBRANCH', 'A'),
3835 ('/%(branches)s/VENDORBRANCH/import', 'A'),
3836 ('/%(branches)s/VENDORBRANCH/import/d.txt', 'A'),
3837 ('/%(branches)s/VENDORBRANCH/root-empty-directory', 'A'),
3838 ('/%(branches)s/VENDORBRANCH/root-empty-directory/empty-subdirectory',
3839 'A'),
3840 ('/%(branches)s/VENDORBRANCH/import/empty-directory', 'A'),
3841 ('/%(branches)s/VENDORBRANCH/import/empty-directory/empty-subdirectory',
3842 'A'),
3844 conv.logs[12].check('This commit was generated', (
3845 ('/%(trunk)s/import', 'A'),
3846 ('/%(trunk)s/import/d.txt '
3847 '(from /%(branches)s/VENDORBRANCH/import/d.txt:11)', 'A'),
3848 ('/%(trunk)s/import/empty-directory', 'A'),
3849 ('/%(trunk)s/import/empty-directory/empty-subdirectory', 'A'),
3853 @Cvs2SvnTestFunction
3854 def include_empty_directories_no_prune():
3855 "test --include-empty-directories with --no-prune"
3857 conv = ensure_conversion(
3858 'empty-directories', args=['--include-empty-directories', '--no-prune'],
3860 conv.logs[1].check('Standard project directories', (
3861 ('/%(trunk)s', 'A'),
3862 ('/%(branches)s', 'A'),
3863 ('/%(tags)s', 'A'),
3864 ('/%(trunk)s/root-empty-directory', 'A'),
3865 ('/%(trunk)s/root-empty-directory/empty-subdirectory', 'A'),
3867 conv.logs[3].check('Add b.txt.', (
3868 ('/%(trunk)s/direct', 'A'),
3869 ('/%(trunk)s/direct/b.txt', 'A'),
3870 ('/%(trunk)s/direct/empty-directory', 'A'),
3871 ('/%(trunk)s/direct/empty-directory/empty-subdirectory', 'A'),
3873 conv.logs[4].check('Add c.txt.', (
3874 ('/%(trunk)s/indirect', 'A'),
3875 ('/%(trunk)s/indirect/subdirectory', 'A'),
3876 ('/%(trunk)s/indirect/subdirectory/c.txt', 'A'),
3877 ('/%(trunk)s/indirect/empty-directory', 'A'),
3878 ('/%(trunk)s/indirect/empty-directory/empty-subdirectory', 'A'),
3880 conv.logs[5].check('Remove b.txt', (
3881 ('/%(trunk)s/direct/b.txt', 'D'),
3883 conv.logs[6].check('Remove c.txt', (
3884 ('/%(trunk)s/indirect/subdirectory/c.txt', 'D'),
3886 conv.logs[7].check('Re-add b.txt.', (
3887 ('/%(trunk)s/direct/b.txt', 'A'),
3889 conv.logs[8].check('Re-add c.txt.', (
3890 ('/%(trunk)s/indirect/subdirectory/c.txt', 'A'),
3892 conv.logs[9].check('This commit was manufactured', (
3893 ('/%(tags)s/TAG (from /%(trunk)s:8)', 'A'),
3895 conv.logs[10].check('This commit was manufactured', (
3896 ('/%(branches)s/BRANCH (from /%(trunk)s:8)', 'A'),
3900 @Cvs2SvnTestFunction
3901 def exclude_symbol_default():
3902 "test 'exclude' symbol default"
3904 conv = ensure_conversion(
3905 'symbol-mess', args=['--symbol-default=exclude'])
3906 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
3907 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
3908 raise Failure()
3909 if conv.path_exists('tags', 'MOSTLY_TAG') \
3910 or conv.path_exists('branches', 'MOSTLY_TAG'):
3911 raise Failure()
3914 @Cvs2SvnTestFunction
3915 def add_on_branch2():
3916 "another add-on-branch test case"
3918 conv = ensure_conversion('add-on-branch2')
3919 if len(conv.logs) != 2:
3920 raise Failure()
3921 conv.logs[2].check('add file on branch', (
3922 ('/%(branches)s/BRANCH', 'A'),
3923 ('/%(branches)s/BRANCH/file1', 'A'),
3927 @Cvs2SvnTestFunction
3928 def branch_from_vendor_branch():
3929 "branch from vendor branch"
3931 ensure_conversion(
3932 'branch-from-vendor-branch',
3933 symbol_hints_file='branch-from-vendor-branch-symbol-hints.txt',
3937 @Cvs2SvnTestFunction
3938 def strange_default_branch():
3939 "default branch too deep in the hierarchy"
3941 ensure_conversion(
3942 'strange-default-branch',
3943 error_re=(
3944 r'ERROR\: The default branch 1\.2\.4\.3\.2\.1\.2 '
3945 r'in file .* is not a top-level branch'
3950 @Cvs2SvnTestFunction
3951 def move_parent():
3952 "graft onto preferred parent that was itself moved"
3954 conv = ensure_conversion(
3955 'move-parent',
3957 conv.logs[2].check('first', (
3958 ('/%(trunk)s/file1', 'A'),
3959 ('/%(trunk)s/file2', 'A'),
3961 conv.logs[3].check('This commit was manufactured', (
3962 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
3964 conv.logs[4].check('second', (
3965 ('/%(branches)s/b2/file1', 'M'),
3967 conv.logs[5].check('This commit was manufactured', (
3968 ('/%(branches)s/b1 (from /%(branches)s/b2:4)', 'A'),
3971 # b2 and b1 are equally good parents for b3, so accept either one.
3972 # (Currently, cvs2svn chooses b1 as the preferred parent because it
3973 # comes earlier than b2 in alphabetical order.)
3974 try:
3975 conv.logs[6].check('This commit was manufactured', (
3976 ('/%(branches)s/b3 (from /%(branches)s/b1:5)', 'A'),
3978 except Failure:
3979 conv.logs[6].check('This commit was manufactured', (
3980 ('/%(branches)s/b3 (from /%(branches)s/b2:4)', 'A'),
3984 @Cvs2SvnTestFunction
3985 def log_message_eols():
3986 "nonstandard EOLs in log messages"
3988 conv = ensure_conversion(
3989 'log-message-eols',
3991 conv.logs[2].check('The CRLF at the end of this line\nshould', (
3992 ('/%(trunk)s/lottalogs', 'A'),
3994 conv.logs[3].check('The CR at the end of this line\nshould', (
3995 ('/%(trunk)s/lottalogs', 'M'),
3999 @Cvs2SvnTestFunction
4000 def missing_vendor_branch():
4001 "default branch not present in RCS file"
4003 conv = ensure_conversion(
4004 'missing-vendor-branch',
4005 verbosity='-qq',
4006 error_re=r'.*vendor branch \'1\.1\.1\' is not present in file and will be ignored',
4010 @Cvs2SvnTestFunction
4011 def newphrases():
4012 "newphrases in RCS files"
4014 ensure_conversion(
4015 'newphrases',
4019 ########################################################################
4020 # Run the tests
4022 # list all tests here, starting with None:
4023 test_list = [
4024 None,
4025 # 1:
4026 show_usage,
4027 cvs2svn_manpage,
4028 cvs2git_manpage,
4029 cvs2hg_manpage,
4030 attr_exec,
4031 space_fname,
4032 two_quick,
4033 PruneWithCare(),
4034 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
4035 # 10:
4036 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
4037 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
4038 interleaved_commits,
4039 simple_commits,
4040 SimpleTags(),
4041 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
4042 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
4043 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
4044 simple_branch_commits,
4045 mixed_time_tag,
4046 # 20:
4047 mixed_time_branch_with_added_file,
4048 mixed_commit,
4049 split_time_branch,
4050 bogus_tag,
4051 overlapping_branch,
4052 PhoenixBranch(),
4053 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
4054 ctrl_char_in_log,
4055 overdead,
4056 NoTrunkPrune(),
4057 # 30:
4058 NoTrunkPrune(variant=1