Fix references to the --write-symbol-info option.
[cvs2svn.git] / run-tests.py
blob565bc4fd26bcfabdbeae7299fccd26db5fe97b21
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 run_command
71 from svntest.main import run_tests
72 from svntest.main import safe_rmtree
73 from svntest.testcase import TestCase
74 from svntest.testcase import Skip
75 from svntest.testcase import XFail
76 from svntest.tree import build_tree_from_wc
77 from svntest.tree import get_child
79 cvs2svn = os.path.abspath('cvs2svn')
80 cvs2git = os.path.abspath('cvs2git')
82 # We use the installed svn and svnlook binaries, instead of using
83 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
84 # behavior -- or even existence -- of local builds shouldn't affect
85 # the cvs2svn test suite.
86 svn_binary = 'svn'
87 svnlook_binary = 'svnlook'
88 svnadmin_binary = 'svnadmin'
89 svnversion_binary = 'svnversion'
91 test_data_dir = 'test-data'
92 tmp_dir = 'cvs2svn-tmp'
95 #----------------------------------------------------------------------
96 # Helpers.
97 #----------------------------------------------------------------------
100 # The value to expect for svn:keywords if it is set:
101 KEYWORDS = 'Author Date Id Revision'
104 class RunProgramException(Failure):
105 pass
108 class MissingErrorException(Failure):
109 def __init__(self, error_re):
110 Failure.__init__(
111 self, "Test failed because no error matched '%s'" % (error_re,)
115 def run_program(program, error_re, *varargs):
116 """Run PROGRAM with VARARGS, return stdout as a list of lines.
118 If there is any stderr and ERROR_RE is None, raise
119 RunProgramException, and print the stderr lines if
120 svntest.main.verbose_mode is true.
122 If ERROR_RE is not None, it is a string regular expression that must
123 match some line of stderr. If it fails to match, raise
124 MissingErrorExpection."""
126 # FIXME: exit_code is currently ignored.
127 exit_code, out, err = run_command(program, 1, 0, *varargs)
129 if error_re:
130 # Specified error expected on stderr.
131 if not err:
132 raise MissingErrorException(error_re)
133 else:
134 for line in err:
135 if re.match(error_re, line):
136 return out
137 raise MissingErrorException(error_re)
138 else:
139 # No stderr allowed.
140 if err:
141 if svntest.main.verbose_mode:
142 print '\n%s said:\n' % program
143 for line in err:
144 print ' ' + line,
145 print
146 raise RunProgramException()
148 return out
151 def run_cvs2svn(error_re, *varargs):
152 """Run cvs2svn with VARARGS, return stdout as a list of lines.
154 If there is any stderr and ERROR_RE is None, raise
155 RunProgramException, and print the stderr lines if
156 svntest.main.verbose_mode is true.
158 If ERROR_RE is not None, it is a string regular expression that must
159 match some line of stderr. If it fails to match, raise
160 MissingErrorException."""
162 # Use the same python that is running this script
163 return run_program(sys.executable, error_re, cvs2svn, *varargs)
164 # On Windows, for an unknown reason, the cmd.exe process invoked by
165 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
166 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
167 # this. Therefore, the redirection of the output to the .s-revs file fails.
168 # We no longer use the problematic invocation on any system, but this
169 # comment remains to warn about this problem.
172 def run_cvs2git(error_re, *varargs):
173 """Run cvs2git with VARARGS, returning stdout as a list of lines.
175 If there is any stderr and ERROR_RE is None, raise
176 RunProgramException, and print the stderr lines if
177 svntest.main.verbose_mode is true.
179 If ERROR_RE is not None, it is a string regular expression that must
180 match some line of stderr. If it fails to match, raise
181 MissingErrorException."""
183 # Use the same python that is running this script
184 return run_program(sys.executable, error_re, cvs2git, *varargs)
185 # On Windows, for an unknown reason, the cmd.exe process invoked by
186 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
187 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
188 # this. Therefore, the redirection of the output to the .s-revs file fails.
189 # We no longer use the problematic invocation on any system, but this
190 # comment remains to warn about this problem.
193 def run_svn(*varargs):
194 """Run svn with VARARGS; return stdout as a list of lines.
195 If there is any stderr, raise RunProgramException, and print the
196 stderr lines if svntest.main.verbose_mode is true."""
197 return run_program(svn_binary, None, *varargs)
200 def repos_to_url(path_to_svn_repos):
201 """This does what you think it does."""
202 rpath = os.path.abspath(path_to_svn_repos)
203 if rpath[0] != '/':
204 rpath = '/' + rpath
205 return 'file://%s' % rpath.replace(os.sep, '/')
208 def svn_strptime(timestr):
209 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
212 class Log:
213 def __init__(self, revision, author, date, symbols):
214 self.revision = revision
215 self.author = author
217 # Internally, we represent the date as seconds since epoch (UTC).
218 # Since standard subversion log output shows dates in localtime
220 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
222 # and time.mktime() converts from localtime, it all works out very
223 # happily.
224 self.date = time.mktime(svn_strptime(date[0:19]))
226 # The following symbols are used for string interpolation when
227 # checking paths:
228 self.symbols = symbols
230 # The changed paths will be accumulated later, as log data is read.
231 # Keys here are paths such as '/trunk/foo/bar', values are letter
232 # codes such as 'M', 'A', and 'D'.
233 self.changed_paths = { }
235 # The msg will be accumulated later, as log data is read.
236 self.msg = ''
238 def absorb_changed_paths(self, out):
239 'Read changed paths from OUT into self, until no more.'
240 while 1:
241 line = out.readline()
242 if len(line) == 1: return
243 line = line[:-1]
244 op_portion = line[3:4]
245 path_portion = line[5:]
246 # If we're running on Windows we get backslashes instead of
247 # forward slashes.
248 path_portion = path_portion.replace('\\', '/')
249 # # We could parse out history information, but currently we
250 # # just leave it in the path portion because that's how some
251 # # tests expect it.
253 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
254 # if m:
255 # path_portion = m.group(1)
256 self.changed_paths[path_portion] = op_portion
258 def __cmp__(self, other):
259 return cmp(self.revision, other.revision) or \
260 cmp(self.author, other.author) or cmp(self.date, other.date) or \
261 cmp(self.changed_paths, other.changed_paths) or \
262 cmp(self.msg, other.msg)
264 def get_path_op(self, path):
265 """Return the operator for the change involving PATH.
267 PATH is allowed to include string interpolation directives (e.g.,
268 '%(trunk)s'), which are interpolated against self.symbols. Return
269 None if there is no record for PATH."""
270 return self.changed_paths.get(path % self.symbols)
272 def check_msg(self, msg):
273 """Verify that this Log's message starts with the specified MSG."""
274 if self.msg.find(msg) != 0:
275 raise Failure(
276 "Revision %d log message was:\n%s\n\n"
277 "It should have begun with:\n%s\n\n"
278 % (self.revision, self.msg, msg,)
281 def check_change(self, path, op):
282 """Verify that this Log includes a change for PATH with operator OP.
284 PATH is allowed to include string interpolation directives (e.g.,
285 '%(trunk)s'), which are interpolated against self.symbols."""
287 path = path % self.symbols
288 found_op = self.changed_paths.get(path, None)
289 if found_op is None:
290 raise Failure(
291 "Revision %d does not include change for path %s "
292 "(it should have been %s).\n"
293 % (self.revision, path, op,)
295 if found_op != op:
296 raise Failure(
297 "Revision %d path %s had op %s (it should have been %s)\n"
298 % (self.revision, path, found_op, op,)
301 def check_changes(self, changed_paths):
302 """Verify that this Log has precisely the CHANGED_PATHS specified.
304 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
305 strings are allowed to include string interpolation directives
306 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
308 cp = {}
309 for (path, op) in changed_paths:
310 cp[path % self.symbols] = op
312 if self.changed_paths != cp:
313 raise Failure(
314 "Revision %d changed paths list was:\n%s\n\n"
315 "It should have been:\n%s\n\n"
316 % (self.revision, self.changed_paths, cp,)
319 def check(self, msg, changed_paths):
320 """Verify that this Log has the MSG and CHANGED_PATHS specified.
322 Convenience function to check two things at once. MSG is passed
323 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
325 self.check_msg(msg)
326 self.check_changes(changed_paths)
329 def parse_log(svn_repos, symbols):
330 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
332 Initialize the Logs' symbols with SYMBOLS."""
334 class LineFeeder:
335 'Make a list of lines behave like an open file handle.'
336 def __init__(self, lines):
337 self.lines = lines
338 def readline(self):
339 if len(self.lines) > 0:
340 return self.lines.pop(0)
341 else:
342 return None
344 def absorb_message_body(out, num_lines, log):
345 """Read NUM_LINES of log message body from OUT into Log item LOG."""
347 for i in range(num_lines):
348 log.msg += out.readline()
350 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
351 '(?P<author>[^\|]+) \| '
352 '(?P<date>[^\|]+) '
353 '\| (?P<lines>[0-9]+) (line|lines)$')
355 log_separator = '-' * 72
357 logs = { }
359 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
361 while 1:
362 this_log = None
363 line = out.readline()
364 if not line: break
365 line = line[:-1]
367 if line.find(log_separator) == 0:
368 line = out.readline()
369 if not line: break
370 line = line[:-1]
371 m = log_start_re.match(line)
372 if m:
373 this_log = Log(
374 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
375 line = out.readline()
376 if not line.find('Changed paths:') == 0:
377 print 'unexpected log output (missing changed paths)'
378 print "Line: '%s'" % line
379 sys.exit(1)
380 this_log.absorb_changed_paths(out)
381 absorb_message_body(out, int(m.group('lines')), this_log)
382 logs[this_log.revision] = this_log
383 elif len(line) == 0:
384 break # We've reached the end of the log output.
385 else:
386 print 'unexpected log output (missing revision line)'
387 print "Line: '%s'" % line
388 sys.exit(1)
389 else:
390 print 'unexpected log output (missing log separator)'
391 print "Line: '%s'" % line
392 sys.exit(1)
394 return logs
397 def erase(path):
398 """Unconditionally remove PATH and its subtree, if any. PATH may be
399 non-existent, a file or symlink, or a directory."""
400 if os.path.isdir(path):
401 safe_rmtree(path)
402 elif os.path.exists(path):
403 os.remove(path)
406 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
408 def sym_log_msg(symbolic_name, is_tag=None):
409 """Return the expected log message for a cvs2svn-synthesized revision
410 creating branch or tag SYMBOLIC_NAME."""
412 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
413 if is_tag:
414 type = 'tag'
415 else:
416 type = 'branch'
418 return log_msg_text_wrapper.fill(
419 "This commit was manufactured by cvs2svn to create %s '%s'."
420 % (type, symbolic_name)
424 def make_conversion_id(
425 name, args, passbypass, options_file=None, symbol_hints_file=None
427 """Create an identifying tag for a conversion.
429 The return value can also be used as part of a filesystem path.
431 NAME is the name of the CVS repository.
433 ARGS are the extra arguments to be passed to cvs2svn.
435 PASSBYPASS is a boolean indicating whether the conversion is to be
436 run one pass at a time.
438 If OPTIONS_FILE is specified, it is an options file that will be
439 used for the conversion.
441 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
442 will be used for the conversion.
444 The 1-to-1 mapping between cvs2svn command parameters and
445 conversion_ids allows us to avoid running the same conversion more
446 than once, when multiple tests use exactly the same conversion."""
448 conv_id = name
450 args = args[:]
452 if passbypass:
453 args.append('--passbypass')
455 if symbol_hints_file is not None:
456 args.append('--symbol-hints=%s' % (symbol_hints_file,))
458 # There are some characters that are forbidden in filenames, and
459 # there is a limit on the total length of a path to a file. So use
460 # a hash of the parameters rather than concatenating the parameters
461 # into a string.
462 if args:
463 conv_id += "-" + md5('\0'.join(args)).hexdigest()
465 # Some options-file based tests rely on knowing the paths to which
466 # the repository should be written, so we handle that option as a
467 # predictable string:
468 if options_file is not None:
469 conv_id += '--options=%s' % (options_file,)
471 return conv_id
474 class Conversion:
475 """A record of a cvs2svn conversion.
477 Fields:
479 conv_id -- the conversion id for this Conversion.
481 name -- a one-word name indicating the involved repositories.
483 dumpfile -- the name of the SVN dumpfile created by the conversion
484 (if the DUMPFILE constructor argument was used); otherwise,
485 None.
487 repos -- the path to the svn repository. Unset if DUMPFILE was
488 specified.
490 logs -- a dictionary of Log instances, as returned by parse_log().
491 Unset if DUMPFILE was specified.
493 symbols -- a dictionary of symbols used for string interpolation
494 in path names.
496 stdout -- a list of lines written by cvs2svn to stdout
498 _wc -- the basename of the svn working copy (within tmp_dir).
499 Unset if DUMPFILE was specified.
501 _wc_path -- the path to the svn working copy, if it has already
502 been created; otherwise, None. (The working copy is created
503 lazily when get_wc() is called.) Unset if DUMPFILE was
504 specified.
506 _wc_tree -- the tree built from the svn working copy, if it has
507 already been created; otherwise, None. The tree is created
508 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
509 specified.
511 _svnrepos -- the basename of the svn repository (within tmp_dir).
512 Unset if DUMPFILE was specified."""
514 # The number of the last cvs2svn pass (determined lazily by
515 # get_last_pass()).
516 last_pass = None
518 @classmethod
519 def get_last_pass(cls):
520 """Return the number of cvs2svn's last pass."""
522 if cls.last_pass is None:
523 out = run_cvs2svn(None, '--help-passes')
524 cls.last_pass = int(out[-1].split()[0])
525 return cls.last_pass
527 def __init__(
528 self, conv_id, name, error_re, passbypass, symbols, args,
529 options_file=None, symbol_hints_file=None, dumpfile=None,
531 self.conv_id = conv_id
532 self.name = name
533 self.symbols = symbols
534 if not os.path.isdir(tmp_dir):
535 os.mkdir(tmp_dir)
537 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
539 if dumpfile:
540 self.dumpfile = os.path.join(tmp_dir, dumpfile)
541 # Clean up from any previous invocations of this script.
542 erase(self.dumpfile)
543 else:
544 self.dumpfile = None
545 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
546 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
547 self._wc_path = None
548 self._wc_tree = None
550 # Clean up from any previous invocations of this script.
551 erase(self.repos)
552 erase(self._wc)
554 args = list(args)
555 if options_file:
556 self.options_file = os.path.join(cvsrepos, options_file)
557 args.extend([
558 '--options=%s' % self.options_file,
560 assert not symbol_hints_file
561 else:
562 self.options_file = None
563 if tmp_dir != 'cvs2svn-tmp':
564 # Only include this argument if it differs from cvs2svn's default:
565 args.extend([
566 '--tmpdir=%s' % tmp_dir,
569 if symbol_hints_file:
570 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
571 args.extend([
572 '--symbol-hints=%s' % self.symbol_hints_file,
575 if self.dumpfile:
576 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
577 else:
578 args.extend(['-s', self.repos])
579 args.extend([cvsrepos])
581 if passbypass:
582 self.stdout = []
583 for p in range(1, self.get_last_pass() + 1):
584 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
585 else:
586 self.stdout = run_cvs2svn(error_re, *args)
588 if self.dumpfile:
589 if not os.path.isfile(self.dumpfile):
590 raise Failure(
591 "Dumpfile not created: '%s'"
592 % os.path.join(os.getcwd(), self.dumpfile)
594 else:
595 if os.path.isdir(self.repos):
596 self.logs = parse_log(self.repos, self.symbols)
597 elif error_re is None:
598 raise Failure(
599 "Repository not created: '%s'"
600 % os.path.join(os.getcwd(), self.repos)
603 def output_found(self, pattern):
604 """Return True if PATTERN matches any line in self.stdout.
606 PATTERN is a regular expression pattern as a string.
609 pattern_re = re.compile(pattern)
611 for line in self.stdout:
612 if pattern_re.match(line):
613 # We found the pattern that we were looking for.
614 return 1
615 else:
616 return 0
618 def find_tag_log(self, tagname):
619 """Search LOGS for a log message containing 'TAGNAME' and return the
620 log in which it was found."""
621 for i in xrange(len(self.logs), 0, -1):
622 if self.logs[i].msg.find("'"+tagname+"'") != -1:
623 return self.logs[i]
624 raise ValueError("Tag %s not found in logs" % tagname)
626 def get_wc(self, *args):
627 """Return the path to the svn working copy, or a path within the WC.
629 If a working copy has not been created yet, create it now.
631 If ARGS are specified, then they should be strings that form
632 fragments of a path within the WC. They are joined using
633 os.path.join() and appended to the WC path."""
635 if self._wc_path is None:
636 run_svn('co', repos_to_url(self.repos), self._wc)
637 self._wc_path = self._wc
638 return os.path.join(self._wc_path, *args)
640 def get_wc_tree(self):
641 if self._wc_tree is None:
642 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
643 return self._wc_tree
645 def path_exists(self, *args):
646 """Return True if the specified path exists within the repository.
648 (The strings in ARGS are first joined into a path using
649 os.path.join().)"""
651 return os.path.exists(self.get_wc(*args))
653 def check_props(self, keys, checks):
654 """Helper function for checking lots of properties. For a list of
655 files in the conversion, check that the values of the properties
656 listed in KEYS agree with those listed in CHECKS. CHECKS is a
657 list of tuples: [ (filename, [value, value, ...]), ...], where the
658 values are listed in the same order as the key names are listed in
659 KEYS."""
661 for (file, values) in checks:
662 assert len(values) == len(keys)
663 props = props_for_path(self.get_wc_tree(), file)
664 for i in range(len(keys)):
665 if props.get(keys[i]) != values[i]:
666 raise Failure(
667 "File %s has property %s set to \"%s\" "
668 "(it should have been \"%s\").\n"
669 % (file, keys[i], props.get(keys[i]), values[i],)
673 class GitConversion:
674 """A record of a cvs2svn conversion.
676 Fields:
678 name -- a one-word name indicating the CVS repository to be converted.
680 stdout -- a list of lines written by cvs2svn to stdout."""
682 def __init__(self, name, error_re, args, options_file=None):
683 self.name = name
684 if not os.path.isdir(tmp_dir):
685 os.mkdir(tmp_dir)
687 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
689 args = list(args)
690 if options_file:
691 self.options_file = os.path.join(cvsrepos, options_file)
692 args.extend([
693 '--options=%s' % self.options_file,
695 else:
696 self.options_file = None
698 self.stdout = run_cvs2git(error_re, *args)
701 # Cache of conversions that have already been done. Keys are conv_id;
702 # values are Conversion instances.
703 already_converted = { }
705 def ensure_conversion(
706 name, error_re=None, passbypass=None,
707 trunk=None, branches=None, tags=None,
708 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
710 """Convert CVS repository NAME to Subversion, but only if it has not
711 been converted before by this invocation of this script. If it has
712 been converted before, return the Conversion object from the
713 previous invocation.
715 If no error, return a Conversion instance.
717 If ERROR_RE is a string, it is a regular expression expected to
718 match some line of stderr printed by the conversion. If there is an
719 error and ERROR_RE is not set, then raise Failure.
721 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
722 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
724 NAME is just one word. For example, 'main' would mean to convert
725 './test-data/main-cvsrepos', and after the conversion, the resulting
726 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
727 a checked out head working copy in './cvs2svn-tmp/main-wc'.
729 Any other options to pass to cvs2svn should be in ARGS, each element
730 being one option, e.g., '--trunk-only'. If the option takes an
731 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
732 are passed to cvs2svn in the order that they appear in ARGS.
734 If OPTIONS_FILE is specified, then it should be the name of a file
735 within the main directory of the cvs repository associated with this
736 test. It is passed to cvs2svn using the --options option (which
737 suppresses some other options that are incompatible with --options).
739 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
740 file within the main directory of the cvs repository associated with
741 this test. It is passed to cvs2svn using the --symbol-hints option.
743 If DUMPFILE is specified, then it is the name of a dumpfile within
744 the temporary directory to which the conversion output should be
745 written."""
747 if args is None:
748 args = []
749 else:
750 args = list(args)
752 if trunk is None:
753 trunk = 'trunk'
754 else:
755 args.append('--trunk=%s' % (trunk,))
757 if branches is None:
758 branches = 'branches'
759 else:
760 args.append('--branches=%s' % (branches,))
762 if tags is None:
763 tags = 'tags'
764 else:
765 args.append('--tags=%s' % (tags,))
767 conv_id = make_conversion_id(
768 name, args, passbypass, options_file, symbol_hints_file
771 if conv_id not in already_converted:
772 try:
773 # Run the conversion and store the result for the rest of this
774 # session:
775 already_converted[conv_id] = Conversion(
776 conv_id, name, error_re, passbypass,
777 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
778 args, options_file, symbol_hints_file, dumpfile,
780 except Failure:
781 # Remember the failure so that a future attempt to run this conversion
782 # does not bother to retry, but fails immediately.
783 already_converted[conv_id] = None
784 raise
786 conv = already_converted[conv_id]
787 if conv is None:
788 raise Failure()
789 return conv
792 class Cvs2SvnTestFunction(TestCase):
793 """A TestCase based on a naked Python function object.
795 FUNC should be a function that returns None on success and throws an
796 svntest.Failure exception on failure. It should have a brief
797 docstring describing what it does (and fulfilling certain
798 conditions). FUNC must take no arguments.
800 This class is almost identical to svntest.testcase.FunctionTestCase,
801 except that the test function does not require a sandbox and does
802 not accept any parameter (not even sandbox=None).
804 This class can be used as an annotation on a Python function.
808 def __init__(self, func):
809 # it better be a function that accepts no parameters and has a
810 # docstring on it.
811 assert isinstance(func, types.FunctionType)
813 name = func.func_name
815 assert func.func_code.co_argcount == 0, \
816 '%s must not take any arguments' % name
818 doc = func.__doc__.strip()
819 assert doc, '%s must have a docstring' % name
821 # enforce stylistic guidelines for the function docstrings:
822 # - no longer than 50 characters
823 # - should not end in a period
824 # - should not be capitalized
825 assert len(doc) <= 50, \
826 "%s's docstring must be 50 characters or less" % name
827 assert doc[-1] != '.', \
828 "%s's docstring should not end in a period" % name
829 assert doc[0].lower() == doc[0], \
830 "%s's docstring should not be capitalized" % name
832 TestCase.__init__(self, doc=doc)
833 self.func = func
835 def get_function_name(self):
836 return self.func.func_name
838 def get_sandbox_name(self):
839 return None
841 def run(self, sandbox):
842 return self.func()
845 class Cvs2SvnTestCase(TestCase):
846 def __init__(
847 self, name, doc=None, variant=None,
848 error_re=None, passbypass=None,
849 trunk=None, branches=None, tags=None,
850 args=None,
851 options_file=None, symbol_hints_file=None, dumpfile=None,
853 self.name = name
855 if doc is None:
856 # By default, use the first line of the class docstring as the
857 # doc:
858 doc = self.__doc__.splitlines()[0]
860 if variant is not None:
861 # Modify doc to show the variant. Trim doc first if necessary
862 # to stay within the 50-character limit.
863 suffix = '...variant %s' % (variant,)
864 doc = doc[:50 - len(suffix)] + suffix
866 TestCase.__init__(self, doc=doc)
868 self.error_re = error_re
869 self.passbypass = passbypass
870 self.trunk = trunk
871 self.branches = branches
872 self.tags = tags
873 self.args = args
874 self.options_file = options_file
875 self.symbol_hints_file = symbol_hints_file
876 self.dumpfile = dumpfile
878 def ensure_conversion(self):
879 return ensure_conversion(
880 self.name,
881 error_re=self.error_re, passbypass=self.passbypass,
882 trunk=self.trunk, branches=self.branches, tags=self.tags,
883 args=self.args,
884 options_file=self.options_file,
885 symbol_hints_file=self.symbol_hints_file,
886 dumpfile=self.dumpfile,
889 def get_sandbox_name(self):
890 return None
893 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
894 """Test properties resulting from a conversion."""
896 def __init__(self, name, props_to_test, expected_props, **kw):
897 """Initialize an instance of Cvs2SvnPropertiesTestCase.
899 NAME is the name of the test, passed to Cvs2SvnTestCase.
900 PROPS_TO_TEST is a list of the names of svn properties that should
901 be tested. EXPECTED_PROPS is a list of tuples [(filename,
902 [value,...])], where the second item in each tuple is a list of
903 values expected for the properties listed in PROPS_TO_TEST for the
904 specified filename. If a property must *not* be set, then its
905 value should be listed as None."""
907 Cvs2SvnTestCase.__init__(self, name, **kw)
908 self.props_to_test = props_to_test
909 self.expected_props = expected_props
911 def run(self, sbox):
912 conv = self.ensure_conversion()
913 conv.check_props(self.props_to_test, self.expected_props)
916 #----------------------------------------------------------------------
917 # Tests.
918 #----------------------------------------------------------------------
921 @Cvs2SvnTestFunction
922 def show_usage():
923 "cvs2svn with no arguments shows usage"
924 out = run_cvs2svn(None)
925 if (len(out) > 2 and out[0].find('ERROR:') == 0
926 and out[1].find('DBM module')):
927 print 'cvs2svn cannot execute due to lack of proper DBM module.'
928 print 'Exiting without running any further tests.'
929 sys.exit(1)
930 if out[0].find('Usage:') < 0:
931 raise Failure('Basic cvs2svn invocation failed.')
934 @Cvs2SvnTestFunction
935 def cvs2svn_manpage():
936 "generate a manpage for cvs2svn"
937 out = run_cvs2svn(None, '--man')
940 @Cvs2SvnTestFunction
941 def cvs2git_manpage():
942 "generate a manpage for cvs2git"
943 out = run_cvs2git(None, '--man')
946 @Cvs2SvnTestFunction
947 def show_help_passes():
948 "cvs2svn --help-passes shows pass information"
949 out = run_cvs2svn(None, '--help-passes')
950 if out[0].find('PASSES') < 0:
951 raise Failure('cvs2svn --help-passes failed.')
954 @Cvs2SvnTestFunction
955 def attr_exec():
956 "detection of the executable flag"
957 if sys.platform == 'win32':
958 raise svntest.Skip()
959 conv = ensure_conversion('main')
960 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
961 if not st[0] & stat.S_IXUSR:
962 raise Failure()
965 @Cvs2SvnTestFunction
966 def space_fname():
967 "conversion of filename with a space"
968 conv = ensure_conversion('main')
969 if not conv.path_exists('trunk', 'single-files', 'space fname'):
970 raise Failure()
973 @Cvs2SvnTestFunction
974 def two_quick():
975 "two commits in quick succession"
976 conv = ensure_conversion('main')
977 logs = parse_log(
978 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
979 if len(logs) != 2:
980 raise Failure()
983 class PruneWithCare(Cvs2SvnTestCase):
984 "prune, but never too much"
986 def __init__(self, **kw):
987 Cvs2SvnTestCase.__init__(self, 'main', **kw)
989 def run(self, sbox):
990 # Robert Pluim encountered this lovely one while converting the
991 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
992 # repository (see issue #1302). Step 4 is the doozy:
994 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
995 # revision 2: adds trunk/blah/NEWS
996 # revision 3: deletes trunk/blah/cookie
997 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
998 # revision 5: does nothing
1000 # After fixing cvs2svn, the sequence (correctly) looks like this:
1002 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
1003 # revision 2: adds trunk/blah/NEWS
1004 # revision 3: deletes trunk/blah/cookie
1005 # revision 4: does nothing [because trunk/blah/cookie already deleted]
1006 # revision 5: deletes blah
1008 # The difference is in 4 and 5. In revision 4, it's not correct to
1009 # prune blah/, because NEWS is still in there, so revision 4 does
1010 # nothing now. But when we delete NEWS in 5, that should bubble up
1011 # and prune blah/ instead.
1013 # ### Note that empty revisions like 4 are probably going to become
1014 # ### at least optional, if not banished entirely from cvs2svn's
1015 # ### output. Hmmm, or they may stick around, with an extra
1016 # ### revision property explaining what happened. Need to think
1017 # ### about that. In some sense, it's a bug in Subversion itself,
1018 # ### that such revisions don't show up in 'svn log' output.
1020 # In the test below, 'trunk/full-prune/first' represents
1021 # cookie, and 'trunk/full-prune/second' represents NEWS.
1023 conv = self.ensure_conversion()
1025 # Confirm that revision 4 removes '/trunk/full-prune/first',
1026 # and that revision 6 removes '/trunk/full-prune'.
1028 # Also confirm similar things about '/full-prune-reappear/...',
1029 # which is similar, except that later on it reappears, restored
1030 # from pruneland, because a file gets added to it.
1032 # And finally, a similar thing for '/partial-prune/...', except that
1033 # in its case, a permanent file on the top level prevents the
1034 # pruning from going farther than the subdirectory containing first
1035 # and second.
1037 for path in ('full-prune/first',
1038 'full-prune-reappear/sub/first',
1039 'partial-prune/sub/first'):
1040 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
1042 for path in ('full-prune',
1043 'full-prune-reappear',
1044 'partial-prune/sub'):
1045 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
1047 for path in ('full-prune-reappear',
1048 'full-prune-reappear/appears-later'):
1049 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
1052 @Cvs2SvnTestFunction
1053 def interleaved_commits():
1054 "two interleaved trunk commits, different log msgs"
1055 # See test-data/main-cvsrepos/proj/README.
1056 conv = ensure_conversion('main')
1058 # The initial import.
1059 rev = 26
1060 conv.logs[rev].check('Initial import.', (
1061 ('/%(trunk)s/interleaved', 'A'),
1062 ('/%(trunk)s/interleaved/1', 'A'),
1063 ('/%(trunk)s/interleaved/2', 'A'),
1064 ('/%(trunk)s/interleaved/3', 'A'),
1065 ('/%(trunk)s/interleaved/4', 'A'),
1066 ('/%(trunk)s/interleaved/5', 'A'),
1067 ('/%(trunk)s/interleaved/a', 'A'),
1068 ('/%(trunk)s/interleaved/b', 'A'),
1069 ('/%(trunk)s/interleaved/c', 'A'),
1070 ('/%(trunk)s/interleaved/d', 'A'),
1071 ('/%(trunk)s/interleaved/e', 'A'),
1074 def check_letters(rev):
1075 """Check if REV is the rev where only letters were committed."""
1077 conv.logs[rev].check('Committing letters only.', (
1078 ('/%(trunk)s/interleaved/a', 'M'),
1079 ('/%(trunk)s/interleaved/b', 'M'),
1080 ('/%(trunk)s/interleaved/c', 'M'),
1081 ('/%(trunk)s/interleaved/d', 'M'),
1082 ('/%(trunk)s/interleaved/e', 'M'),
1085 def check_numbers(rev):
1086 """Check if REV is the rev where only numbers were committed."""
1088 conv.logs[rev].check('Committing numbers only.', (
1089 ('/%(trunk)s/interleaved/1', 'M'),
1090 ('/%(trunk)s/interleaved/2', 'M'),
1091 ('/%(trunk)s/interleaved/3', 'M'),
1092 ('/%(trunk)s/interleaved/4', 'M'),
1093 ('/%(trunk)s/interleaved/5', 'M'),
1096 # One of the commits was letters only, the other was numbers only.
1097 # But they happened "simultaneously", so we don't assume anything
1098 # about which commit appeared first, so we just try both ways.
1099 rev += 1
1100 try:
1101 check_letters(rev)
1102 check_numbers(rev + 1)
1103 except Failure:
1104 check_numbers(rev)
1105 check_letters(rev + 1)
1108 @Cvs2SvnTestFunction
1109 def simple_commits():
1110 "simple trunk commits"
1111 # See test-data/main-cvsrepos/proj/README.
1112 conv = ensure_conversion('main')
1114 # The initial import.
1115 conv.logs[13].check('Initial import.', (
1116 ('/%(trunk)s/proj', 'A'),
1117 ('/%(trunk)s/proj/default', 'A'),
1118 ('/%(trunk)s/proj/sub1', 'A'),
1119 ('/%(trunk)s/proj/sub1/default', 'A'),
1120 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1121 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1122 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1123 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1124 ('/%(trunk)s/proj/sub2', 'A'),
1125 ('/%(trunk)s/proj/sub2/default', 'A'),
1126 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1127 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1128 ('/%(trunk)s/proj/sub3', 'A'),
1129 ('/%(trunk)s/proj/sub3/default', 'A'),
1132 # The first commit.
1133 conv.logs[18].check('First commit to proj, affecting two files.', (
1134 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1135 ('/%(trunk)s/proj/sub3/default', 'M'),
1138 # The second commit.
1139 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1140 ('/%(trunk)s/proj/default', 'M'),
1141 ('/%(trunk)s/proj/sub1/default', 'M'),
1142 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1143 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1144 ('/%(trunk)s/proj/sub2/default', 'M'),
1145 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1146 ('/%(trunk)s/proj/sub3/default', 'M')
1150 class SimpleTags(Cvs2SvnTestCase):
1151 "simple tags and branches, no commits"
1153 def __init__(self, **kw):
1154 # See test-data/main-cvsrepos/proj/README.
1155 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1157 def run(self, sbox):
1158 conv = self.ensure_conversion()
1160 # Verify the copy source for the tags we are about to check
1161 # No need to verify the copyfrom revision, as simple_commits did that
1162 conv.logs[13].check('Initial import.', (
1163 ('/%(trunk)s/proj', 'A'),
1164 ('/%(trunk)s/proj/default', 'A'),
1165 ('/%(trunk)s/proj/sub1', 'A'),
1166 ('/%(trunk)s/proj/sub1/default', 'A'),
1167 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1168 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1169 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1170 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1171 ('/%(trunk)s/proj/sub2', 'A'),
1172 ('/%(trunk)s/proj/sub2/default', 'A'),
1173 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1174 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1175 ('/%(trunk)s/proj/sub3', 'A'),
1176 ('/%(trunk)s/proj/sub3/default', 'A'),
1179 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1181 # Tag on rev 1.1.1.1 of all files in proj
1182 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1183 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1184 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1185 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1188 # The same, as a tag
1189 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1190 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1191 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1194 # Tag on rev 1.1.1.1 of all files in proj, except one
1195 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1196 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1197 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1198 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1201 # The same, as a branch
1202 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1203 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1204 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1208 @Cvs2SvnTestFunction
1209 def simple_branch_commits():
1210 "simple branch commits"
1211 # See test-data/main-cvsrepos/proj/README.
1212 conv = ensure_conversion('main')
1214 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1215 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1216 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1217 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1221 @Cvs2SvnTestFunction
1222 def mixed_time_tag():
1223 "mixed-time tag"
1224 # See test-data/main-cvsrepos/proj/README.
1225 conv = ensure_conversion('main')
1227 log = conv.find_tag_log('T_MIXED')
1228 log.check_changes((
1229 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1233 @Cvs2SvnTestFunction
1234 def mixed_time_branch_with_added_file():
1235 "mixed-time branch, and a file added to the branch"
1236 # See test-data/main-cvsrepos/proj/README.
1237 conv = ensure_conversion('main')
1239 # A branch from the same place as T_MIXED in the previous test,
1240 # plus a file added directly to the branch
1241 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1242 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1243 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1244 ('/%(branches)s/B_MIXED/single-files', 'D'),
1245 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1246 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1247 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1250 conv.logs[22].check('Add a file on branch B_MIXED.', (
1251 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1255 @Cvs2SvnTestFunction
1256 def mixed_commit():
1257 "a commit affecting both trunk and a branch"
1258 # See test-data/main-cvsrepos/proj/README.
1259 conv = ensure_conversion('main')
1261 conv.logs[24].check(
1262 'A single commit affecting one file on branch B_MIXED '
1263 'and one on trunk.', (
1264 ('/%(trunk)s/proj/sub2/default', 'M'),
1265 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1269 @Cvs2SvnTestFunction
1270 def split_time_branch():
1271 "branch some trunk files, and later branch the rest"
1272 # See test-data/main-cvsrepos/proj/README.
1273 conv = ensure_conversion('main')
1275 # First change on the branch, creating it
1276 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1277 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1278 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1279 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1280 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1283 conv.logs[29].check('First change on branch B_SPLIT.', (
1284 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1285 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1286 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1287 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1288 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1291 # A trunk commit for the file which was not branched
1292 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1293 'This was committed about an', (
1294 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1297 # Add the file not already branched to the branch, with modification:w
1298 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1299 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1300 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1303 conv.logs[32].check('This change affects sub3/default and '
1304 'sub1/subsubB/default, on branch', (
1305 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1306 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1310 @Cvs2SvnTestFunction
1311 def multiple_tags():
1312 "multiple tags referring to same revision"
1313 conv = ensure_conversion('main')
1314 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1315 raise Failure()
1316 if not conv.path_exists(
1317 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1318 raise Failure()
1321 @Cvs2SvnTestFunction
1322 def multiply_defined_symbols():
1323 "multiple definitions of symbol names"
1325 # We can only check one line of the error output at a time, so test
1326 # twice. (The conversion only have to be done once because the
1327 # results are cached.)
1328 conv = ensure_conversion(
1329 'multiply-defined-symbols',
1330 error_re=(
1331 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1332 r"1\.2\.4 1\.2\.2"
1335 conv = ensure_conversion(
1336 'multiply-defined-symbols',
1337 error_re=(
1338 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1339 r"1\.2 1\.1"
1344 @Cvs2SvnTestFunction
1345 def multiply_defined_symbols_renamed():
1346 "rename multiply defined symbols"
1348 conv = ensure_conversion(
1349 'multiply-defined-symbols',
1350 options_file='cvs2svn-rename.options',
1354 @Cvs2SvnTestFunction
1355 def multiply_defined_symbols_ignored():
1356 "ignore multiply defined symbols"
1358 conv = ensure_conversion(
1359 'multiply-defined-symbols',
1360 options_file='cvs2svn-ignore.options',
1364 @Cvs2SvnTestFunction
1365 def repeatedly_defined_symbols():
1366 "multiple identical definitions of symbol names"
1368 # If a symbol is defined multiple times but has the same value each
1369 # time, that should not be an error.
1371 conv = ensure_conversion('repeatedly-defined-symbols')
1374 @Cvs2SvnTestFunction
1375 def bogus_tag():
1376 "conversion of invalid symbolic names"
1377 conv = ensure_conversion('bogus-tag')
1380 @Cvs2SvnTestFunction
1381 def overlapping_branch():
1382 "ignore a file with a branch with two names"
1383 conv = ensure_conversion('overlapping-branch')
1385 if not conv.output_found('.*cannot also have name \'vendorB\''):
1386 raise Failure()
1388 conv.logs[2].check('imported', (
1389 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1390 ('/%(trunk)s/overlapping-branch', 'A'),
1393 if len(conv.logs) != 2:
1394 raise Failure()
1397 class PhoenixBranch(Cvs2SvnTestCase):
1398 "convert a branch file rooted in a 'dead' revision"
1400 def __init__(self, **kw):
1401 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1403 def run(self, sbox):
1404 conv = self.ensure_conversion()
1405 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1406 ('/%(branches)s/volsung_20010721', 'A'),
1407 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1409 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1410 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1414 ###TODO: We check for 4 changed paths here to accomodate creating tags
1415 ###and branches in rev 1, but that will change, so this will
1416 ###eventually change back.
1417 @Cvs2SvnTestFunction
1418 def ctrl_char_in_log():
1419 "handle a control char in a log message"
1420 # This was issue #1106.
1421 rev = 2
1422 conv = ensure_conversion('ctrl-char-in-log')
1423 conv.logs[rev].check_changes((
1424 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1426 if conv.logs[rev].msg.find('\x04') < 0:
1427 raise Failure(
1428 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1431 @Cvs2SvnTestFunction
1432 def overdead():
1433 "handle tags rooted in a redeleted revision"
1434 conv = ensure_conversion('overdead')
1437 class NoTrunkPrune(Cvs2SvnTestCase):
1438 "ensure that trunk doesn't get pruned"
1440 def __init__(self, **kw):
1441 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1443 def run(self, sbox):
1444 conv = self.ensure_conversion()
1445 for rev in conv.logs.keys():
1446 rev_logs = conv.logs[rev]
1447 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1448 raise Failure()
1451 @Cvs2SvnTestFunction
1452 def double_delete():
1453 "file deleted twice, in the root of the repository"
1454 # This really tests several things: how we handle a file that's
1455 # removed (state 'dead') in two successive revisions; how we
1456 # handle a file in the root of the repository (there were some
1457 # bugs in cvs2svn's svn path construction for top-level files); and
1458 # the --no-prune option.
1459 conv = ensure_conversion(
1460 'double-delete', args=['--trunk-only', '--no-prune'])
1462 path = '/%(trunk)s/twice-removed'
1463 rev = 2
1464 conv.logs[rev].check('Updated CVS', (
1465 (path, 'A'),
1467 conv.logs[rev + 1].check('Remove this file for the first time.', (
1468 (path, 'D'),
1470 conv.logs[rev + 2].check('Remove this file for the second time,', (
1474 @Cvs2SvnTestFunction
1475 def split_branch():
1476 "branch created from both trunk and another branch"
1477 # See test-data/split-branch-cvsrepos/README.
1479 # The conversion will fail if the bug is present, and
1480 # ensure_conversion will raise Failure.
1481 conv = ensure_conversion('split-branch')
1484 @Cvs2SvnTestFunction
1485 def resync_misgroups():
1486 "resyncing should not misorder commit groups"
1487 # See test-data/resync-misgroups-cvsrepos/README.
1489 # The conversion will fail if the bug is present, and
1490 # ensure_conversion will raise Failure.
1491 conv = ensure_conversion('resync-misgroups')
1494 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1495 "allow tags with mixed trunk and branch sources"
1497 def __init__(self, **kw):
1498 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1500 def run(self, sbox):
1501 conv = self.ensure_conversion()
1503 tags = conv.symbols.get('tags', 'tags')
1505 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1506 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1507 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1508 raise Failure()
1509 if (open(a_path, 'r').read().find('1.24') == -1) \
1510 or (open(b_path, 'r').read().find('1.5') == -1):
1511 raise Failure()
1514 @Cvs2SvnTestFunction
1515 def enroot_race():
1516 "never use the rev-in-progress as a copy source"
1518 # See issue #1427 and r8544.
1519 conv = ensure_conversion('enroot-race')
1520 rev = 6
1521 conv.logs[rev].check_changes((
1522 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1523 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1524 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1526 conv.logs[rev + 1].check_changes((
1527 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1528 ('/%(trunk)s/proj/a.txt', 'M'),
1529 ('/%(trunk)s/proj/b.txt', 'M'),
1533 @Cvs2SvnTestFunction
1534 def enroot_race_obo():
1535 "do use the last completed rev as a copy source"
1536 conv = ensure_conversion('enroot-race-obo')
1537 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1538 if not len(conv.logs) == 3:
1539 raise Failure()
1542 class BranchDeleteFirst(Cvs2SvnTestCase):
1543 "correctly handle deletion as initial branch action"
1545 def __init__(self, **kw):
1546 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1548 def run(self, sbox):
1549 # See test-data/branch-delete-first-cvsrepos/README.
1551 # The conversion will fail if the bug is present, and
1552 # ensure_conversion would raise Failure.
1553 conv = self.ensure_conversion()
1555 branches = conv.symbols.get('branches', 'branches')
1557 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1558 if conv.path_exists(branches, 'branch-1', 'file'):
1559 raise Failure()
1560 if conv.path_exists(branches, 'branch-2', 'file'):
1561 raise Failure()
1562 if not conv.path_exists(branches, 'branch-3', 'file'):
1563 raise Failure()
1566 @Cvs2SvnTestFunction
1567 def nonascii_filenames():
1568 "non ascii files converted incorrectly"
1569 # see issue #1255
1571 # on a en_US.iso-8859-1 machine this test fails with
1572 # svn: Can't recode ...
1574 # as described in the issue
1576 # on a en_US.UTF-8 machine this test fails with
1577 # svn: Malformed XML ...
1579 # which means at least it fails. Unfortunately it won't fail
1580 # with the same error...
1582 # mangle current locale settings so we know we're not running
1583 # a UTF-8 locale (which does not exhibit this problem)
1584 current_locale = locale.getlocale()
1585 new_locale = 'en_US.ISO8859-1'
1586 locale_changed = None
1588 # From http://docs.python.org/lib/module-sys.html
1590 # getfilesystemencoding():
1592 # Return the name of the encoding used to convert Unicode filenames
1593 # into system file names, or None if the system default encoding is
1594 # used. The result value depends on the operating system:
1596 # - On Windows 9x, the encoding is ``mbcs''.
1597 # - On Mac OS X, the encoding is ``utf-8''.
1598 # - On Unix, the encoding is the user's preference according to the
1599 # result of nl_langinfo(CODESET), or None if the
1600 # nl_langinfo(CODESET) failed.
1601 # - On Windows NT+, file names are Unicode natively, so no conversion is
1602 # performed.
1604 # So we're going to skip this test on Mac OS X for now.
1605 if sys.platform == "darwin":
1606 raise svntest.Skip()
1608 try:
1609 # change locale to non-UTF-8 locale to generate latin1 names
1610 locale.setlocale(locale.LC_ALL, # this might be too broad?
1611 new_locale)
1612 locale_changed = 1
1613 except locale.Error:
1614 raise svntest.Skip()
1616 try:
1617 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1618 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1619 if not os.path.exists(dstrepos_path):
1620 # create repos from existing main repos
1621 shutil.copytree(srcrepos_path, dstrepos_path)
1622 base_path = os.path.join(dstrepos_path, 'single-files')
1623 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1624 os.path.join(base_path, 'two\366uick,v'))
1625 new_path = os.path.join(dstrepos_path, 'single\366files')
1626 os.rename(base_path, new_path)
1628 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1629 finally:
1630 if locale_changed:
1631 locale.setlocale(locale.LC_ALL, current_locale)
1632 safe_rmtree(dstrepos_path)
1635 class UnicodeTest(Cvs2SvnTestCase):
1636 "metadata contains Unicode"
1638 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1640 def __init__(self, name, warning_expected, **kw):
1641 if warning_expected:
1642 error_re = self.warning_pattern
1643 else:
1644 error_re = None
1646 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1647 self.warning_expected = warning_expected
1649 def run(self, sbox):
1650 try:
1651 # ensure the availability of the "utf_8" encoding:
1652 u'a'.encode('utf_8').decode('utf_8')
1653 except LookupError:
1654 raise svntest.Skip()
1656 self.ensure_conversion()
1659 class UnicodeAuthor(UnicodeTest):
1660 "author name contains Unicode"
1662 def __init__(self, warning_expected, **kw):
1663 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1666 class UnicodeLog(UnicodeTest):
1667 "log message contains Unicode"
1669 def __init__(self, warning_expected, **kw):
1670 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1673 @Cvs2SvnTestFunction
1674 def vendor_branch_sameness():
1675 "avoid spurious changes for initial revs"
1676 conv = ensure_conversion(
1677 'vendor-branch-sameness', args=['--keep-trivial-imports']
1680 # The following files are in this repository:
1682 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1683 # the same contents, the file's default branch is 1.1.1,
1684 # and both revisions are in state 'Exp'.
1686 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1687 # 1.1 (the addition of a line of text).
1689 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1691 # d.txt: This file was created by 'cvs add' instead of import, so
1692 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1693 # The timestamp on the add is exactly the same as for the
1694 # imports of the other files.
1696 # e.txt: Like a.txt, except that the log message for revision 1.1
1697 # is not the standard import log message.
1699 # (Aside from e.txt, the log messages for the same revisions are the
1700 # same in all files.)
1702 # We expect that only a.txt is recognized as an import whose 1.1
1703 # revision can be omitted. The other files should be added on trunk
1704 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1705 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1706 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1707 # and c.txt should be 'D'eleted.
1709 rev = 2
1710 conv.logs[rev].check('Initial revision', (
1711 ('/%(trunk)s/proj', 'A'),
1712 ('/%(trunk)s/proj/b.txt', 'A'),
1713 ('/%(trunk)s/proj/c.txt', 'A'),
1714 ('/%(trunk)s/proj/d.txt', 'A'),
1717 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1718 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1719 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1722 conv.logs[rev + 2].check('First vendor branch revision.', (
1723 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1724 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1725 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1728 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1729 'to compensate for changes in r4,', (
1730 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1731 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1732 ('/%(trunk)s/proj/c.txt', 'D'),
1735 rev = 7
1736 conv.logs[rev].check('This log message is not the standard', (
1737 ('/%(trunk)s/proj/e.txt', 'A'),
1740 conv.logs[rev + 2].check('First vendor branch revision', (
1741 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1744 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1745 'to compensate for changes in r9,', (
1746 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1750 @Cvs2SvnTestFunction
1751 def vendor_branch_trunk_only():
1752 "handle vendor branches with --trunk-only"
1753 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1755 rev = 2
1756 conv.logs[rev].check('Initial revision', (
1757 ('/%(trunk)s/proj', 'A'),
1758 ('/%(trunk)s/proj/b.txt', 'A'),
1759 ('/%(trunk)s/proj/c.txt', 'A'),
1760 ('/%(trunk)s/proj/d.txt', 'A'),
1763 conv.logs[rev + 1].check('First vendor branch revision', (
1764 ('/%(trunk)s/proj/a.txt', 'A'),
1765 ('/%(trunk)s/proj/b.txt', 'M'),
1766 ('/%(trunk)s/proj/c.txt', 'D'),
1769 conv.logs[rev + 2].check('This log message is not the standard', (
1770 ('/%(trunk)s/proj/e.txt', 'A'),
1773 conv.logs[rev + 3].check('First vendor branch revision', (
1774 ('/%(trunk)s/proj/e.txt', 'M'),
1778 @Cvs2SvnTestFunction
1779 def default_branches():
1780 "handle default branches correctly"
1781 conv = ensure_conversion('default-branches')
1783 # There are seven files in the repository:
1785 # a.txt:
1786 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1787 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1788 # committed (thus losing the default branch "1.1.1"), then
1789 # 1.1.1.4 was imported. All vendor import release tags are
1790 # still present.
1792 # b.txt:
1793 # Like a.txt, but without rev 1.2.
1795 # c.txt:
1796 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1798 # d.txt:
1799 # Same as the previous two, but 1.1.1 branch is unlabeled.
1801 # e.txt:
1802 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1804 # deleted-on-vendor-branch.txt,v:
1805 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1807 # added-then-imported.txt,v:
1808 # Added with 'cvs add' to create 1.1, then imported with
1809 # completely different contents to create 1.1.1.1, therefore
1810 # never had a default branch.
1813 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1814 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1815 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1816 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1817 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1818 ('/%(branches)s/vbranchA', 'A'),
1819 ('/%(branches)s/vbranchA/proj', 'A'),
1820 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1821 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1822 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1823 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1826 conv.logs[3].check("This commit was generated by cvs2svn "
1827 "to compensate for changes in r2,", (
1828 ('/%(trunk)s/proj', 'A'),
1829 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1830 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1831 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1832 ('/%(trunk)s/proj/d.txt '
1833 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1834 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1835 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1836 ('/%(trunk)s/proj/e.txt '
1837 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1840 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1841 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1842 ('/%(tags)s/vtag-1/proj/d.txt '
1843 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1846 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1847 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1848 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1849 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1850 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1851 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1852 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1855 conv.logs[6].check("This commit was generated by cvs2svn "
1856 "to compensate for changes in r5,", (
1857 ('/%(trunk)s/proj/a.txt '
1858 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1859 ('/%(trunk)s/proj/b.txt '
1860 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1861 ('/%(trunk)s/proj/c.txt '
1862 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1863 ('/%(trunk)s/proj/d.txt '
1864 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1865 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1866 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1867 'R'),
1868 ('/%(trunk)s/proj/e.txt '
1869 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1872 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1873 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1874 ('/%(tags)s/vtag-2/proj/d.txt '
1875 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1878 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1879 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1880 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1881 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1882 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1883 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1884 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1887 conv.logs[9].check("This commit was generated by cvs2svn "
1888 "to compensate for changes in r8,", (
1889 ('/%(trunk)s/proj/a.txt '
1890 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1891 ('/%(trunk)s/proj/b.txt '
1892 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1893 ('/%(trunk)s/proj/c.txt '
1894 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1895 ('/%(trunk)s/proj/d.txt '
1896 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1897 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1898 ('/%(trunk)s/proj/e.txt '
1899 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1902 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1903 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1904 ('/%(tags)s/vtag-3/proj/d.txt '
1905 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1906 ('/%(tags)s/vtag-3/proj/e.txt '
1907 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1910 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1911 ('/%(trunk)s/proj/a.txt', 'M'),
1914 conv.logs[12].check("Add a file to the working copy.", (
1915 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1918 conv.logs[13].check(sym_log_msg('vbranchA'), (
1919 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1920 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1923 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1924 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1925 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1926 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1927 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1928 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1929 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1930 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1933 conv.logs[15].check("This commit was generated by cvs2svn "
1934 "to compensate for changes in r14,", (
1935 ('/%(trunk)s/proj/b.txt '
1936 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1937 ('/%(trunk)s/proj/c.txt '
1938 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1939 ('/%(trunk)s/proj/d.txt '
1940 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1941 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1942 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1943 'A'),
1944 ('/%(trunk)s/proj/e.txt '
1945 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1948 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1949 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1950 ('/%(tags)s/vtag-4/proj/d.txt '
1951 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1955 @Cvs2SvnTestFunction
1956 def default_branches_trunk_only():
1957 "handle default branches with --trunk-only"
1959 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1961 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1962 ('/%(trunk)s/proj', 'A'),
1963 ('/%(trunk)s/proj/a.txt', 'A'),
1964 ('/%(trunk)s/proj/b.txt', 'A'),
1965 ('/%(trunk)s/proj/c.txt', 'A'),
1966 ('/%(trunk)s/proj/d.txt', 'A'),
1967 ('/%(trunk)s/proj/e.txt', 'A'),
1968 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1971 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1972 ('/%(trunk)s/proj/a.txt', 'M'),
1973 ('/%(trunk)s/proj/b.txt', 'M'),
1974 ('/%(trunk)s/proj/c.txt', 'M'),
1975 ('/%(trunk)s/proj/d.txt', 'M'),
1976 ('/%(trunk)s/proj/e.txt', 'M'),
1977 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1980 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1981 ('/%(trunk)s/proj/a.txt', 'M'),
1982 ('/%(trunk)s/proj/b.txt', 'M'),
1983 ('/%(trunk)s/proj/c.txt', 'M'),
1984 ('/%(trunk)s/proj/d.txt', 'M'),
1985 ('/%(trunk)s/proj/e.txt', 'M'),
1986 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1989 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1990 ('/%(trunk)s/proj/a.txt', 'M'),
1993 conv.logs[6].check("Add a file to the working copy.", (
1994 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1997 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1998 ('/%(trunk)s/proj/b.txt', 'M'),
1999 ('/%(trunk)s/proj/c.txt', 'M'),
2000 ('/%(trunk)s/proj/d.txt', 'M'),
2001 ('/%(trunk)s/proj/e.txt', 'M'),
2002 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
2006 @Cvs2SvnTestFunction
2007 def default_branch_and_1_2():
2008 "do not allow 1.2 revision with default branch"
2010 conv = ensure_conversion(
2011 'default-branch-and-1-2',
2012 error_re=(
2013 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
2018 @Cvs2SvnTestFunction
2019 def compose_tag_three_sources():
2020 "compose a tag from three sources"
2021 conv = ensure_conversion('compose-tag-three-sources')
2023 conv.logs[2].check("Add on trunk", (
2024 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
2025 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
2026 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
2027 ('/%(trunk)s/tagged-on-b1', 'A'),
2028 ('/%(trunk)s/tagged-on-b2', 'A'),
2031 conv.logs[3].check(sym_log_msg('b1'), (
2032 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
2035 conv.logs[4].check(sym_log_msg('b2'), (
2036 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
2039 conv.logs[5].check("Commit on branch b1", (
2040 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
2041 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
2042 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
2043 ('/%(branches)s/b1/tagged-on-b1', 'M'),
2044 ('/%(branches)s/b1/tagged-on-b2', 'M'),
2047 conv.logs[6].check("Commit on branch b2", (
2048 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
2049 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
2050 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
2051 ('/%(branches)s/b2/tagged-on-b1', 'M'),
2052 ('/%(branches)s/b2/tagged-on-b2', 'M'),
2055 conv.logs[7].check("Commit again on trunk", (
2056 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
2057 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
2058 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
2059 ('/%(trunk)s/tagged-on-b1', 'M'),
2060 ('/%(trunk)s/tagged-on-b2', 'M'),
2063 conv.logs[8].check(sym_log_msg('T',1), (
2064 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
2065 ('/%(tags)s/T/tagged-on-trunk-1.1 '
2066 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
2067 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
2068 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
2072 @Cvs2SvnTestFunction
2073 def pass5_when_to_fill():
2074 "reserve a svn revnum for a fill only when required"
2075 # The conversion will fail if the bug is present, and
2076 # ensure_conversion would raise Failure.
2077 conv = ensure_conversion('pass5-when-to-fill')
2080 class EmptyTrunk(Cvs2SvnTestCase):
2081 "don't break when the trunk is empty"
2083 def __init__(self, **kw):
2084 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
2086 def run(self, sbox):
2087 # The conversion will fail if the bug is present, and
2088 # ensure_conversion would raise Failure.
2089 conv = self.ensure_conversion()
2092 @Cvs2SvnTestFunction
2093 def no_spurious_svn_commits():
2094 "ensure that we don't create any spurious commits"
2095 conv = ensure_conversion('phoenix')
2097 # Check spurious commit that could be created in
2098 # SVNCommitCreator._pre_commit()
2100 # (When you add a file on a branch, CVS creates a trunk revision
2101 # in state 'dead'. If the log message of that commit is equal to
2102 # the one that CVS generates, we do not ever create a 'fill'
2103 # SVNCommit for it.)
2105 # and spurious commit that could be created in
2106 # SVNCommitCreator._commit()
2108 # (When you add a file on a branch, CVS creates a trunk revision
2109 # in state 'dead'. If the log message of that commit is equal to
2110 # the one that CVS generates, we do not create a primary SVNCommit
2111 # for it.)
2112 conv.logs[17].check('File added on branch xiphophorus', (
2113 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2116 # Check to make sure that a commit *is* generated:
2117 # (When you add a file on a branch, CVS creates a trunk revision
2118 # in state 'dead'. If the log message of that commit is NOT equal
2119 # to the one that CVS generates, we create a primary SVNCommit to
2120 # serve as a home for the log message in question.
2121 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2122 + 'branch xiphophorus,\nand this log message was tweaked', ())
2124 # Check spurious commit that could be created in
2125 # SVNCommitCreator._commit_symbols().
2126 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2127 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2131 class PeerPathPruning(Cvs2SvnTestCase):
2132 "make sure that filling prunes paths correctly"
2134 def __init__(self, **kw):
2135 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2137 def run(self, sbox):
2138 conv = self.ensure_conversion()
2139 conv.logs[6].check(sym_log_msg('BRANCH'), (
2140 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2141 ('/%(branches)s/BRANCH/bar', 'D'),
2142 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2146 @Cvs2SvnTestFunction
2147 def invalid_closings_on_trunk():
2148 "verify correct revs are copied to default branches"
2149 # The conversion will fail if the bug is present, and
2150 # ensure_conversion would raise Failure.
2151 conv = ensure_conversion('invalid-closings-on-trunk')
2154 @Cvs2SvnTestFunction
2155 def individual_passes():
2156 "run each pass individually"
2157 conv = ensure_conversion('main')
2158 conv2 = ensure_conversion('main', passbypass=1)
2160 if conv.logs != conv2.logs:
2161 raise Failure()
2164 @Cvs2SvnTestFunction
2165 def resync_bug():
2166 "reveal a big bug in our resync algorithm"
2167 # This will fail if the bug is present
2168 conv = ensure_conversion('resync-bug')
2171 @Cvs2SvnTestFunction
2172 def branch_from_default_branch():
2173 "reveal a bug in our default branch detection code"
2174 conv = ensure_conversion('branch-from-default-branch')
2176 # This revision will be a default branch synchronization only
2177 # if cvs2svn is correctly determining default branch revisions.
2179 # The bug was that cvs2svn was treating revisions on branches off of
2180 # default branches as default branch revisions, resulting in
2181 # incorrectly regarding the branch off of the default branch as a
2182 # non-trunk default branch. Crystal clear? I thought so. See
2183 # issue #42 for more incoherent blathering.
2184 conv.logs[5].check("This commit was generated by cvs2svn", (
2185 ('/%(trunk)s/proj/file.txt '
2186 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2190 @Cvs2SvnTestFunction
2191 def file_in_attic_too():
2192 "die if a file exists in and out of the attic"
2193 ensure_conversion(
2194 'file-in-attic-too',
2195 error_re=(
2196 r'.*A CVS repository cannot contain both '
2197 r'(.*)' + re.escape(os.sep) + r'(.*) '
2198 + r'and '
2199 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2204 @Cvs2SvnTestFunction
2205 def retain_file_in_attic_too():
2206 "test --retain-conflicting-attic-files option"
2207 conv = ensure_conversion(
2208 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2209 if not conv.path_exists('trunk', 'file.txt'):
2210 raise Failure()
2211 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2212 raise Failure()
2215 @Cvs2SvnTestFunction
2216 def symbolic_name_filling_guide():
2217 "reveal a big bug in our SymbolFillingGuide"
2218 # This will fail if the bug is present
2219 conv = ensure_conversion('symbolic-name-overfill')
2222 # Helpers for tests involving file contents and properties.
2224 class NodeTreeWalkException:
2225 "Exception class for node tree traversals."
2226 pass
2228 def node_for_path(node, path):
2229 "In the tree rooted under SVNTree NODE, return the node at PATH."
2230 if node.name != '__SVN_ROOT_NODE':
2231 raise NodeTreeWalkException()
2232 path = path.strip('/')
2233 components = path.split('/')
2234 for component in components:
2235 node = get_child(node, component)
2236 return node
2238 # Helper for tests involving properties.
2239 def props_for_path(node, path):
2240 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2241 return node_for_path(node, path).props
2244 class EOLMime(Cvs2SvnPropertiesTestCase):
2245 """eol settings and mime types together
2247 The files are as follows:
2249 trunk/foo.txt: no -kb, mime file says nothing.
2250 trunk/foo.xml: no -kb, mime file says text.
2251 trunk/foo.zip: no -kb, mime file says non-text.
2252 trunk/foo.bin: has -kb, mime file says nothing.
2253 trunk/foo.csv: has -kb, mime file says text.
2254 trunk/foo.dbf: has -kb, mime file says non-text.
2257 def __init__(self, args, **kw):
2258 # TODO: It's a bit klugey to construct this path here. But so far
2259 # there's only one test with a mime.types file. If we have more,
2260 # we should abstract this into some helper, which would be located
2261 # near ensure_conversion(). Note that it is a convention of this
2262 # test suite for a mime.types file to be located in the top level
2263 # of the CVS repository to which it applies.
2264 self.mime_path = os.path.join(
2265 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2267 Cvs2SvnPropertiesTestCase.__init__(
2268 self, 'eol-mime',
2269 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2270 args=['--mime-types=%s' % self.mime_path] + args,
2271 **kw)
2274 # We do four conversions. Each time, we pass --mime-types=FILE with
2275 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2276 # Thus there's one conversion with neither flag, one with just the
2277 # former, one with just the latter, and one with both.
2280 # Neither --no-default-eol nor --eol-from-mime-type:
2281 eol_mime1 = EOLMime(
2282 variant=1,
2283 args=[],
2284 expected_props=[
2285 ('trunk/foo.txt', [None, None, None]),
2286 ('trunk/foo.xml', [None, 'text/xml', None]),
2287 ('trunk/foo.zip', [None, 'application/zip', None]),
2288 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2289 ('trunk/foo.csv', [None, 'text/csv', None]),
2290 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2294 # Just --no-default-eol, not --eol-from-mime-type:
2295 eol_mime2 = EOLMime(
2296 variant=2,
2297 args=['--default-eol=native'],
2298 expected_props=[
2299 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2300 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2301 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2302 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2303 ('trunk/foo.csv', [None, 'text/csv', None]),
2304 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2308 # Just --eol-from-mime-type, not --no-default-eol:
2309 eol_mime3 = EOLMime(
2310 variant=3,
2311 args=['--eol-from-mime-type'],
2312 expected_props=[
2313 ('trunk/foo.txt', [None, None, None]),
2314 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2315 ('trunk/foo.zip', [None, 'application/zip', None]),
2316 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2317 ('trunk/foo.csv', [None, 'text/csv', None]),
2318 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2322 # Both --no-default-eol and --eol-from-mime-type:
2323 eol_mime4 = EOLMime(
2324 variant=4,
2325 args=['--eol-from-mime-type', '--default-eol=native'],
2326 expected_props=[
2327 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2328 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2329 ('trunk/foo.zip', [None, 'application/zip', None]),
2330 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2331 ('trunk/foo.csv', [None, 'text/csv', None]),
2332 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2336 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2337 'eol-mime',
2338 doc='test non-setting of cvs2svn:cvs-rev property',
2339 args=[],
2340 props_to_test=['cvs2svn:cvs-rev'],
2341 expected_props=[
2342 ('trunk/foo.txt', [None]),
2343 ('trunk/foo.xml', [None]),
2344 ('trunk/foo.zip', [None]),
2345 ('trunk/foo.bin', [None]),
2346 ('trunk/foo.csv', [None]),
2347 ('trunk/foo.dbf', [None]),
2351 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2352 'eol-mime',
2353 doc='test setting of cvs2svn:cvs-rev property',
2354 args=['--cvs-revnums'],
2355 props_to_test=['cvs2svn:cvs-rev'],
2356 expected_props=[
2357 ('trunk/foo.txt', ['1.2']),
2358 ('trunk/foo.xml', ['1.2']),
2359 ('trunk/foo.zip', ['1.2']),
2360 ('trunk/foo.bin', ['1.2']),
2361 ('trunk/foo.csv', ['1.2']),
2362 ('trunk/foo.dbf', ['1.2']),
2366 keywords = Cvs2SvnPropertiesTestCase(
2367 'keywords',
2368 doc='test setting of svn:keywords property among others',
2369 args=['--default-eol=native'],
2370 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2371 expected_props=[
2372 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2373 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2374 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2375 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2376 ('trunk/foo.kk', [None, 'native', None]),
2377 ('trunk/foo.ko', [None, 'native', None]),
2378 ('trunk/foo.kv', [None, 'native', None]),
2382 @Cvs2SvnTestFunction
2383 def ignore():
2384 "test setting of svn:ignore property"
2385 conv = ensure_conversion('cvsignore')
2386 wc_tree = conv.get_wc_tree()
2387 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2388 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2390 if topdir_props['svn:ignore'] != \
2391 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n':
2392 raise Failure()
2394 if subdir_props['svn:ignore'] != \
2395 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n':
2396 raise Failure()
2399 @Cvs2SvnTestFunction
2400 def requires_cvs():
2401 "test that CVS can still do what RCS can't"
2402 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2403 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2405 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2406 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2408 if atsign_contents[-1:] == "@":
2409 raise Failure()
2410 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2411 raise Failure()
2413 if not (conv.logs[21].author == "William Lyon Phelps III" and
2414 conv.logs[20].author == "j random"):
2415 raise Failure()
2418 @Cvs2SvnTestFunction
2419 def questionable_branch_names():
2420 "test that we can handle weird branch names"
2421 conv = ensure_conversion('questionable-symbols')
2422 # If the conversion succeeds, then we're okay. We could check the
2423 # actual branch paths, too, but the main thing is to know that the
2424 # conversion doesn't fail.
2427 @Cvs2SvnTestFunction
2428 def questionable_tag_names():
2429 "test that we can handle weird tag names"
2430 conv = ensure_conversion('questionable-symbols')
2431 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2432 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2434 conv.find_tag_log('TagWith/Backslash_E').check(
2435 sym_log_msg('TagWith/Backslash_E',1),
2437 ('/%(tags)s/TagWith', 'A'),
2438 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2441 conv.find_tag_log('TagWith/Slash_Z').check(
2442 sym_log_msg('TagWith/Slash_Z',1),
2444 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2449 @Cvs2SvnTestFunction
2450 def revision_reorder_bug():
2451 "reveal a bug that reorders file revisions"
2452 conv = ensure_conversion('revision-reorder-bug')
2453 # If the conversion succeeds, then we're okay. We could check the
2454 # actual revisions, too, but the main thing is to know that the
2455 # conversion doesn't fail.
2458 @Cvs2SvnTestFunction
2459 def exclude():
2460 "test that exclude really excludes everything"
2461 conv = ensure_conversion('main', args=['--exclude=.*'])
2462 for log in conv.logs.values():
2463 for item in log.changed_paths.keys():
2464 if item.startswith('/branches/') or item.startswith('/tags/'):
2465 raise Failure()
2468 @Cvs2SvnTestFunction
2469 def vendor_branch_delete_add():
2470 "add trunk file that was deleted on vendor branch"
2471 # This will error if the bug is present
2472 conv = ensure_conversion('vendor-branch-delete-add')
2475 @Cvs2SvnTestFunction
2476 def resync_pass2_pull_forward():
2477 "ensure pass2 doesn't pull rev too far forward"
2478 conv = ensure_conversion('resync-pass2-pull-forward')
2479 # If the conversion succeeds, then we're okay. We could check the
2480 # actual revisions, too, but the main thing is to know that the
2481 # conversion doesn't fail.
2484 @Cvs2SvnTestFunction
2485 def native_eol():
2486 "only LFs for svn:eol-style=native files"
2487 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2488 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2489 conv.repos)
2490 # Verify that all files in the dump have LF EOLs. We're actually
2491 # testing the whole dump file, but the dump file itself only uses
2492 # LF EOLs, so we're safe.
2493 for line in lines:
2494 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2495 raise Failure()
2498 @Cvs2SvnTestFunction
2499 def double_fill():
2500 "reveal a bug that created a branch twice"
2501 conv = ensure_conversion('double-fill')
2502 # If the conversion succeeds, then we're okay. We could check the
2503 # actual revisions, too, but the main thing is to know that the
2504 # conversion doesn't fail.
2507 @Cvs2SvnTestFunction
2508 def double_fill2():
2509 "reveal a second bug that created a branch twice"
2510 conv = ensure_conversion('double-fill2')
2511 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2512 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2513 try:
2514 # This check should fail:
2515 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2516 except Failure:
2517 pass
2518 else:
2519 raise Failure('Symbol filled twice in a row')
2522 @Cvs2SvnTestFunction
2523 def resync_pass2_push_backward():
2524 "ensure pass2 doesn't push rev too far backward"
2525 conv = ensure_conversion('resync-pass2-push-backward')
2526 # If the conversion succeeds, then we're okay. We could check the
2527 # actual revisions, too, but the main thing is to know that the
2528 # conversion doesn't fail.
2531 @Cvs2SvnTestFunction
2532 def double_add():
2533 "reveal a bug that added a branch file twice"
2534 conv = ensure_conversion('double-add')
2535 # If the conversion succeeds, then we're okay. We could check the
2536 # actual revisions, too, but the main thing is to know that the
2537 # conversion doesn't fail.
2540 @Cvs2SvnTestFunction
2541 def bogus_branch_copy():
2542 "reveal a bug that copies a branch file wrongly"
2543 conv = ensure_conversion('bogus-branch-copy')
2544 # If the conversion succeeds, then we're okay. We could check the
2545 # actual revisions, too, but the main thing is to know that the
2546 # conversion doesn't fail.
2549 @Cvs2SvnTestFunction
2550 def nested_ttb_directories():
2551 "require error if ttb directories are not disjoint"
2552 opts_list = [
2553 {'trunk' : 'a', 'branches' : 'a',},
2554 {'trunk' : 'a', 'tags' : 'a',},
2555 {'branches' : 'a', 'tags' : 'a',},
2556 # This option conflicts with the default trunk path:
2557 {'branches' : 'trunk',},
2558 # Try some nested directories:
2559 {'trunk' : 'a', 'branches' : 'a/b',},
2560 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2561 {'branches' : 'a', 'tags' : 'a/b',},
2564 for opts in opts_list:
2565 ensure_conversion(
2566 'main', error_re=r'The following paths are not disjoint\:', **opts
2570 class AutoProps(Cvs2SvnPropertiesTestCase):
2571 """Test auto-props.
2573 The files are as follows:
2575 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2576 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2577 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2578 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2579 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2580 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2581 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2582 trunk/foo.UPCASE1: no -kb, no mime type.
2583 trunk/foo.UPCASE2: no -kb, no mime type.
2586 def __init__(self, args, **kw):
2587 ### TODO: It's a bit klugey to construct this path here. See also
2588 ### the comment in eol_mime().
2589 auto_props_path = os.path.join(
2590 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2592 Cvs2SvnPropertiesTestCase.__init__(
2593 self, 'eol-mime',
2594 props_to_test=[
2595 'myprop',
2596 'svn:eol-style',
2597 'svn:mime-type',
2598 'svn:keywords',
2599 'svn:executable',
2601 args=[
2602 '--auto-props=%s' % auto_props_path,
2603 '--eol-from-mime-type'
2604 ] + args,
2605 **kw)
2608 auto_props_ignore_case = AutoProps(
2609 doc="test auto-props",
2610 args=['--default-eol=native'],
2611 expected_props=[
2612 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2613 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2614 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2615 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2616 ('trunk/foo.bin',
2617 ['bin', None, 'application/octet-stream', None, '']),
2618 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2619 ('trunk/foo.dbf',
2620 ['dbf', None, 'application/what-is-dbf', None, None]),
2621 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2622 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2626 @Cvs2SvnTestFunction
2627 def ctrl_char_in_filename():
2628 "do not allow control characters in filenames"
2630 try:
2631 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2632 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2633 if os.path.exists(dstrepos_path):
2634 safe_rmtree(dstrepos_path)
2636 # create repos from existing main repos
2637 shutil.copytree(srcrepos_path, dstrepos_path)
2638 base_path = os.path.join(dstrepos_path, 'single-files')
2639 try:
2640 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2641 os.path.join(base_path, 'two\rquick,v'))
2642 except:
2643 # Operating systems that don't allow control characters in
2644 # filenames will hopefully have thrown an exception; in that
2645 # case, just skip this test.
2646 raise svntest.Skip()
2648 conv = ensure_conversion(
2649 'ctrl-char-filename',
2650 error_re=(r'.*Character .* in filename .* '
2651 r'is not supported by Subversion\.'),
2653 finally:
2654 safe_rmtree(dstrepos_path)
2657 @Cvs2SvnTestFunction
2658 def commit_dependencies():
2659 "interleaved and multi-branch commits to same files"
2660 conv = ensure_conversion("commit-dependencies")
2661 conv.logs[2].check('adding', (
2662 ('/%(trunk)s/interleaved', 'A'),
2663 ('/%(trunk)s/interleaved/file1', 'A'),
2664 ('/%(trunk)s/interleaved/file2', 'A'),
2666 conv.logs[3].check('big commit', (
2667 ('/%(trunk)s/interleaved/file1', 'M'),
2668 ('/%(trunk)s/interleaved/file2', 'M'),
2670 conv.logs[4].check('dependant small commit', (
2671 ('/%(trunk)s/interleaved/file1', 'M'),
2673 conv.logs[5].check('adding', (
2674 ('/%(trunk)s/multi-branch', 'A'),
2675 ('/%(trunk)s/multi-branch/file1', 'A'),
2676 ('/%(trunk)s/multi-branch/file2', 'A'),
2678 conv.logs[6].check(sym_log_msg("branch"), (
2679 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2680 ('/%(branches)s/branch/interleaved', 'D'),
2682 conv.logs[7].check('multi-branch-commit', (
2683 ('/%(trunk)s/multi-branch/file1', 'M'),
2684 ('/%(trunk)s/multi-branch/file2', 'M'),
2685 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2686 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2690 @Cvs2SvnTestFunction
2691 def double_branch_delete():
2692 "fill branches before modifying files on them"
2693 conv = ensure_conversion('double-branch-delete')
2695 # Test for issue #102. The file IMarshalledValue.java is branched,
2696 # deleted, readded on the branch, and then deleted again. If the
2697 # fill for the file on the branch is postponed until after the
2698 # modification, the file will end up live on the branch instead of
2699 # dead! Make sure it happens at the right time.
2701 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2702 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2705 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2706 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2710 @Cvs2SvnTestFunction
2711 def symbol_mismatches():
2712 "error for conflicting tag/branch"
2714 ensure_conversion(
2715 'symbol-mess',
2716 args=['--symbol-default=strict'],
2717 error_re=r'.*Problems determining how symbols should be converted',
2721 @Cvs2SvnTestFunction
2722 def overlook_symbol_mismatches():
2723 "overlook conflicting tag/branch when --trunk-only"
2725 # This is a test for issue #85.
2727 ensure_conversion('symbol-mess', args=['--trunk-only'])
2730 @Cvs2SvnTestFunction
2731 def force_symbols():
2732 "force symbols to be tags/branches"
2734 conv = ensure_conversion(
2735 'symbol-mess',
2736 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2737 if conv.path_exists('tags', 'BRANCH') \
2738 or not conv.path_exists('branches', 'BRANCH'):
2739 raise Failure()
2740 if not conv.path_exists('tags', 'TAG') \
2741 or conv.path_exists('branches', 'TAG'):
2742 raise Failure()
2743 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2744 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2745 raise Failure()
2746 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2747 or conv.path_exists('branches', 'MOSTLY_TAG'):
2748 raise Failure()
2751 @Cvs2SvnTestFunction
2752 def commit_blocks_tags():
2753 "commit prevents forced tag"
2755 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2756 ensure_conversion(
2757 'symbol-mess',
2758 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2759 error_re=(
2760 r'.*The following branches cannot be forced to be tags '
2761 r'because they have commits'
2766 @Cvs2SvnTestFunction
2767 def blocked_excludes():
2768 "error for blocked excludes"
2770 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2771 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2772 try:
2773 ensure_conversion(
2774 'symbol-mess',
2775 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2776 raise MissingErrorException()
2777 except Failure:
2778 pass
2781 @Cvs2SvnTestFunction
2782 def unblock_blocked_excludes():
2783 "excluding blocker removes blockage"
2785 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2786 for blocker in ['BRANCH', 'COMMIT']:
2787 ensure_conversion(
2788 'symbol-mess',
2789 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2790 '--exclude=BLOCKING_%s' % blocker]))
2793 @Cvs2SvnTestFunction
2794 def regexp_force_symbols():
2795 "force symbols via regular expressions"
2797 conv = ensure_conversion(
2798 'symbol-mess',
2799 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2800 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2801 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2802 raise Failure()
2803 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2804 or conv.path_exists('branches', 'MOSTLY_TAG'):
2805 raise Failure()
2808 @Cvs2SvnTestFunction
2809 def heuristic_symbol_default():
2810 "test 'heuristic' symbol default"
2812 conv = ensure_conversion(
2813 'symbol-mess', args=['--symbol-default=heuristic'])
2814 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2815 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2816 raise Failure()
2817 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2818 or conv.path_exists('branches', 'MOSTLY_TAG'):
2819 raise Failure()
2822 @Cvs2SvnTestFunction
2823 def branch_symbol_default():
2824 "test 'branch' symbol default"
2826 conv = ensure_conversion(
2827 'symbol-mess', args=['--symbol-default=branch'])
2828 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2829 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2830 raise Failure()
2831 if conv.path_exists('tags', 'MOSTLY_TAG') \
2832 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2833 raise Failure()
2836 @Cvs2SvnTestFunction
2837 def tag_symbol_default():
2838 "test 'tag' symbol default"
2840 conv = ensure_conversion(
2841 'symbol-mess', args=['--symbol-default=tag'])
2842 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2843 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2844 raise Failure()
2845 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2846 or conv.path_exists('branches', 'MOSTLY_TAG'):
2847 raise Failure()
2850 @Cvs2SvnTestFunction
2851 def symbol_transform():
2852 "test --symbol-transform"
2854 conv = ensure_conversion(
2855 'symbol-mess',
2856 args=[
2857 '--symbol-default=heuristic',
2858 '--symbol-transform=BRANCH:branch',
2859 '--symbol-transform=TAG:tag',
2860 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2862 if not conv.path_exists('branches', 'branch'):
2863 raise Failure()
2864 if not conv.path_exists('tags', 'tag'):
2865 raise Failure()
2866 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2867 raise Failure()
2868 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2869 raise Failure()
2872 @Cvs2SvnTestFunction
2873 def write_symbol_info():
2874 "test --write-symbol-info"
2876 expected_lines = [
2877 ['0', '.trunk.',
2878 'trunk', 'trunk', '.'],
2879 ['0', 'BLOCKED_BY_UNNAMED',
2880 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2881 ['0', 'BLOCKING_COMMIT',
2882 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2883 ['0', 'BLOCKED_BY_COMMIT',
2884 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2885 ['0', 'BLOCKING_BRANCH',
2886 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2887 ['0', 'BLOCKED_BY_BRANCH',
2888 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2889 ['0', 'MOSTLY_BRANCH',
2890 '.', '.', '.'],
2891 ['0', 'MOSTLY_TAG',
2892 '.', '.', '.'],
2893 ['0', 'BRANCH_WITH_COMMIT',
2894 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2895 ['0', 'BRANCH',
2896 'branch', 'branches/BRANCH', '.trunk.'],
2897 ['0', 'TAG',
2898 'tag', 'tags/TAG', '.trunk.'],
2899 ['0', 'unlabeled-1.1.12.1.2',
2900 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2902 expected_lines.sort()
2904 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2905 try:
2906 ensure_conversion(
2907 'symbol-mess',
2908 args=[
2909 '--symbol-default=strict',
2910 '--write-symbol-info=%s' % (symbol_info_file,),
2911 '--passes=:CollateSymbolsPass',
2914 raise MissingErrorException()
2915 except Failure:
2916 pass
2917 lines = []
2918 comment_re = re.compile(r'^\s*\#')
2919 for l in open(symbol_info_file, 'r'):
2920 if comment_re.match(l):
2921 continue
2922 lines.append(l.strip().split())
2923 lines.sort()
2924 if lines != expected_lines:
2925 s = ['Symbol info incorrect\n']
2926 differ = Differ()
2927 for diffline in differ.compare(
2928 [' '.join(line) + '\n' for line in expected_lines],
2929 [' '.join(line) + '\n' for line in lines],
2931 s.append(diffline)
2932 raise Failure(''.join(s))
2935 @Cvs2SvnTestFunction
2936 def symbol_hints():
2937 "test --symbol-hints for setting branch/tag"
2939 conv = ensure_conversion(
2940 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2942 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2943 raise Failure()
2944 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2945 raise Failure()
2946 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2947 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2949 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2950 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2952 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2953 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2957 @Cvs2SvnTestFunction
2958 def parent_hints():
2959 "test --symbol-hints for setting parent"
2961 conv = ensure_conversion(
2962 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2964 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2965 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2969 @Cvs2SvnTestFunction
2970 def parent_hints_invalid():
2971 "test --symbol-hints with an invalid parent"
2973 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2974 # this symbol hints file sets the preferred parent to BRANCH
2975 # instead:
2976 conv = ensure_conversion(
2977 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2978 error_re=(
2979 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2984 @Cvs2SvnTestFunction
2985 def parent_hints_wildcards():
2986 "test --symbol-hints wildcards"
2988 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2989 # this symbol hints file sets the preferred parent to BRANCH
2990 # instead:
2991 conv = ensure_conversion(
2992 'symbol-mess',
2993 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2995 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2996 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
3000 @Cvs2SvnTestFunction
3001 def path_hints():
3002 "test --symbol-hints for setting svn paths"
3004 conv = ensure_conversion(
3005 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
3007 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
3008 ('/trunk', 'A'),
3009 ('/a', 'A'),
3010 ('/a/strange', 'A'),
3011 ('/a/strange/trunk', 'A'),
3012 ('/a/strange/trunk/path', 'A'),
3013 ('/branches', 'A'),
3014 ('/tags', 'A'),
3016 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
3017 ('/special', 'A'),
3018 ('/special/tag', 'A'),
3019 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
3021 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
3022 ('/special/other', 'A'),
3023 ('/special/other/branch', 'A'),
3024 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
3026 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
3027 ('/special/branch', 'A'),
3028 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
3032 @Cvs2SvnTestFunction
3033 def issue_99():
3034 "test problem from issue 99"
3036 conv = ensure_conversion('issue-99')
3039 @Cvs2SvnTestFunction
3040 def issue_100():
3041 "test problem from issue 100"
3043 conv = ensure_conversion('issue-100')
3044 file1 = conv.get_wc('trunk', 'file1.txt')
3045 if file(file1).read() != 'file1.txt<1.2>\n':
3046 raise Failure()
3049 @Cvs2SvnTestFunction
3050 def issue_106():
3051 "test problem from issue 106"
3053 conv = ensure_conversion('issue-106')
3056 @Cvs2SvnTestFunction
3057 def options_option():
3058 "use of the --options option"
3060 conv = ensure_conversion('main', options_file='cvs2svn.options')
3063 @Cvs2SvnTestFunction
3064 def multiproject():
3065 "multiproject conversion"
3067 conv = ensure_conversion(
3068 'main', options_file='cvs2svn-multiproject.options'
3070 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
3071 ('/partial-prune', 'A'),
3072 ('/partial-prune/trunk', 'A'),
3073 ('/partial-prune/branches', 'A'),
3074 ('/partial-prune/tags', 'A'),
3075 ('/partial-prune/releases', 'A'),
3079 @Cvs2SvnTestFunction
3080 def crossproject():
3081 "multiproject conversion with cross-project commits"
3083 conv = ensure_conversion(
3084 'main', options_file='cvs2svn-crossproject.options'
3088 @Cvs2SvnTestFunction
3089 def tag_with_no_revision():
3090 "tag defined but revision is deleted"
3092 conv = ensure_conversion('tag-with-no-revision')
3095 @Cvs2SvnTestFunction
3096 def delete_cvsignore():
3097 "svn:ignore should vanish when .cvsignore does"
3099 # This is issue #81.
3101 conv = ensure_conversion('delete-cvsignore')
3103 wc_tree = conv.get_wc_tree()
3104 props = props_for_path(wc_tree, 'trunk/proj')
3106 if props.has_key('svn:ignore'):
3107 raise Failure()
3110 @Cvs2SvnTestFunction
3111 def repeated_deltatext():
3112 "ignore repeated deltatext blocks with warning"
3114 conv = ensure_conversion('repeated-deltatext')
3115 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
3116 if not conv.output_found(warning_re):
3117 raise Failure()
3120 @Cvs2SvnTestFunction
3121 def nasty_graphs():
3122 "process some nasty dependency graphs"
3124 # It's not how well the bear can dance, but that the bear can dance
3125 # at all:
3126 conv = ensure_conversion('nasty-graphs')
3129 @Cvs2SvnTestFunction
3130 def tagging_after_delete():
3131 "optimal tag after deleting files"
3133 conv = ensure_conversion('tagging-after-delete')
3135 # tag should be 'clean', no deletes
3136 log = conv.find_tag_log('tag1')
3137 expected = (
3138 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
3140 log.check_changes(expected)
3143 @Cvs2SvnTestFunction
3144 def crossed_branches():
3145 "branches created in inconsistent orders"
3147 conv = ensure_conversion('crossed-branches')
3150 @Cvs2SvnTestFunction
3151 def file_directory_conflict():
3152 "error when filename conflicts with directory name"
3154 conv = ensure_conversion(
3155 'file-directory-conflict',
3156 error_re=r'.*Directory name conflicts with filename',
3160 @Cvs2SvnTestFunction
3161 def attic_directory_conflict():
3162 "error when attic filename conflicts with dirname"
3164 # This tests the problem reported in issue #105.
3166 conv = ensure_conversion(
3167 'attic-directory-conflict',
3168 error_re=r'.*Directory name conflicts with filename',
3172 @Cvs2SvnTestFunction
3173 def internal_co():
3174 "verify that --use-internal-co works"
3176 rcs_conv = ensure_conversion(
3177 'main', args=['--use-rcs', '--default-eol=native'],
3179 conv = ensure_conversion(
3180 'main', args=['--default-eol=native'],
3182 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3183 raise Failure()
3184 rcs_lines = run_program(
3185 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3186 rcs_conv.repos)
3187 lines = run_program(
3188 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3189 conv.repos)
3190 # Compare all lines following the repository UUID:
3191 if lines[3:] != rcs_lines[3:]:
3192 raise Failure()
3195 @Cvs2SvnTestFunction
3196 def internal_co_exclude():
3197 "verify that --use-internal-co --exclude=... works"
3199 rcs_conv = ensure_conversion(
3200 'internal-co',
3201 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3203 conv = ensure_conversion(
3204 'internal-co',
3205 args=['--exclude=BRANCH', '--default-eol=native'],
3207 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3208 raise Failure()
3209 rcs_lines = run_program(
3210 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3211 rcs_conv.repos)
3212 lines = run_program(
3213 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3214 conv.repos)
3215 # Compare all lines following the repository UUID:
3216 if lines[3:] != rcs_lines[3:]:
3217 raise Failure()
3220 @Cvs2SvnTestFunction
3221 def internal_co_trunk_only():
3222 "verify that --use-internal-co --trunk-only works"
3224 conv = ensure_conversion(
3225 'internal-co',
3226 args=['--trunk-only', '--default-eol=native'],
3228 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3229 raise Failure()
3232 @Cvs2SvnTestFunction
3233 def leftover_revs():
3234 "check for leftover checked-out revisions"
3236 conv = ensure_conversion(
3237 'leftover-revs',
3238 args=['--exclude=BRANCH', '--default-eol=native'],
3240 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3241 raise Failure()
3244 @Cvs2SvnTestFunction
3245 def requires_internal_co():
3246 "test that internal co can do more than RCS"
3247 # See issues 4, 11 for the bugs whose regression we're testing for.
3248 # Unlike in requires_cvs above, issue 29 is not covered.
3249 conv = ensure_conversion('requires-cvs')
3251 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3253 if atsign_contents[-1:] == "@":
3254 raise Failure()
3256 if not (conv.logs[21].author == "William Lyon Phelps III" and
3257 conv.logs[20].author == "j random"):
3258 raise Failure()
3261 @Cvs2SvnTestFunction
3262 def internal_co_keywords():
3263 "test that internal co handles keywords correctly"
3264 conv_ic = ensure_conversion('internal-co-keywords',
3265 args=["--keywords-off"])
3266 conv_cvs = ensure_conversion('internal-co-keywords',
3267 args=["--use-cvs", "--keywords-off"])
3269 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3270 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3271 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3272 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3273 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3274 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3276 if ko_ic != ko_cvs:
3277 raise Failure()
3278 if kk_ic != kk_cvs:
3279 raise Failure()
3281 # The date format changed between cvs and co ('/' instead of '-').
3282 # Accept either one:
3283 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3284 if kv_ic != kv_cvs \
3285 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3286 raise Failure()
3289 @Cvs2SvnTestFunction
3290 def timestamp_chaos():
3291 "test timestamp adjustments"
3293 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3295 # The times are expressed here in UTC:
3296 times = [
3297 '2007-01-01 21:00:00', # Initial commit
3298 '2007-01-01 21:00:00', # revision 1.1 of both files
3299 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3300 '2007-01-01 21:00:02', # revision 1.2 of file2.txt, adjusted backwards
3301 '2007-01-01 22:00:00', # revision 1.3 of both files
3304 # Convert the times to seconds since the epoch, in UTC:
3305 times = [calendar.timegm(svn_strptime(t)) for t in times]
3307 for i in range(len(times)):
3308 if abs(conv.logs[i + 1].date - times[i]) > 0.1:
3309 raise Failure()
3312 @Cvs2SvnTestFunction
3313 def symlinks():
3314 "convert a repository that contains symlinks"
3316 # This is a test for issue #97.
3318 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3319 links = [
3321 os.path.join('..', 'file.txt,v'),
3322 os.path.join(proj, 'dir1', 'file.txt,v'),
3325 'dir1',
3326 os.path.join(proj, 'dir2'),
3330 try:
3331 os.symlink
3332 except AttributeError:
3333 # Apparently this OS doesn't support symlinks, so skip test.
3334 raise svntest.Skip()
3336 try:
3337 for (src,dst) in links:
3338 os.symlink(src, dst)
3340 conv = ensure_conversion('symlinks')
3341 conv.logs[2].check('', (
3342 ('/%(trunk)s/proj', 'A'),
3343 ('/%(trunk)s/proj/file.txt', 'A'),
3344 ('/%(trunk)s/proj/dir1', 'A'),
3345 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3346 ('/%(trunk)s/proj/dir2', 'A'),
3347 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3349 finally:
3350 for (src,dst) in links:
3351 os.remove(dst)
3354 @Cvs2SvnTestFunction
3355 def empty_trunk_path():
3356 "allow --trunk to be empty if --trunk-only"
3358 # This is a test for issue #53.
3360 conv = ensure_conversion(
3361 'main', args=['--trunk-only', '--trunk='],
3365 @Cvs2SvnTestFunction
3366 def preferred_parent_cycle():
3367 "handle a cycle in branch parent preferences"
3369 conv = ensure_conversion('preferred-parent-cycle')
3372 @Cvs2SvnTestFunction
3373 def branch_from_empty_dir():
3374 "branch from an empty directory"
3376 conv = ensure_conversion('branch-from-empty-dir')
3379 @Cvs2SvnTestFunction
3380 def trunk_readd():
3381 "add a file on a branch then on trunk"
3383 conv = ensure_conversion('trunk-readd')
3386 @Cvs2SvnTestFunction
3387 def branch_from_deleted_1_1():
3388 "branch from a 1.1 revision that will be deleted"
3390 conv = ensure_conversion('branch-from-deleted-1-1')
3391 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3392 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3394 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3395 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3397 conv.logs[7].check('Adding b.txt:1.2', (
3398 ('/%(trunk)s/proj/b.txt', 'A'),
3401 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3402 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3404 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3405 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3409 @Cvs2SvnTestFunction
3410 def add_on_branch():
3411 "add a file on a branch using newer CVS"
3413 conv = ensure_conversion('add-on-branch')
3414 conv.logs[6].check('Adding b.txt:1.1', (
3415 ('/%(trunk)s/proj/b.txt', 'A'),
3417 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3418 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3420 conv.logs[8].check('Adding c.txt:1.1', (
3421 ('/%(trunk)s/proj/c.txt', 'A'),
3423 conv.logs[9].check('Removing c.txt:1.2', (
3424 ('/%(trunk)s/proj/c.txt', 'D'),
3426 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3427 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3429 conv.logs[11].check('Adding d.txt:1.1', (
3430 ('/%(trunk)s/proj/d.txt', 'A'),
3432 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3433 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3437 @Cvs2SvnTestFunction
3438 def main_git():
3439 "test output in git-fast-import format"
3441 # Note: To test importing into git, do
3443 # ./run-tests <test-number>
3444 # rm -rf .git
3445 # git-init
3446 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3448 # Or, to load the dumpfiles separately:
3450 # cat cvs2svn-tmp/git-blob.dat \
3451 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3452 # cat cvs2svn-tmp/git-dump.dat \
3453 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3455 # Then use "gitk --all", "git log", etc. to test the contents of the
3456 # repository.
3458 # We don't have the infrastructure to check that the resulting git
3459 # repository is correct, so we just check that the conversion runs
3460 # to completion:
3461 conv = GitConversion('main', None, [
3462 '--blobfile=cvs2svn-tmp/blobfile.out',
3463 '--dumpfile=cvs2svn-tmp/dumpfile.out',
3464 '--username=cvs2git',
3465 'test-data/main-cvsrepos',
3469 @Cvs2SvnTestFunction
3470 def git_options():
3471 "test cvs2git using options file"
3473 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3476 @Cvs2SvnTestFunction
3477 def main_hg():
3478 "output in git-fast-import format with inline data"
3480 # The output should be suitable for import by Mercurial.
3482 # Note: To test importing into git, do
3484 # ./run-tests <test-number>
3485 # rm -rf .git
3486 # git-init
3487 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3489 # Then use "gitk --all", "git log", etc. to test the contents of the
3490 # repository.
3492 # We don't have the infrastructure to check that the resulting
3493 # Mercurial repository is correct, so we just check that the
3494 # conversion runs to completion:
3495 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3498 @Cvs2SvnTestFunction
3499 def invalid_symbol():
3500 "a symbol with the incorrect format"
3502 conv = ensure_conversion('invalid-symbol')
3503 if not conv.output_found(
3504 r".*branch 'SYMBOL' references invalid revision 1$"
3506 raise Failure()
3509 @Cvs2SvnTestFunction
3510 def invalid_symbol_ignore():
3511 "ignore a symbol using a SymbolMapper"
3513 conv = ensure_conversion(
3514 'invalid-symbol', options_file='cvs2svn-ignore.options'
3518 @Cvs2SvnTestFunction
3519 def invalid_symbol_ignore2():
3520 "ignore a symbol using an IgnoreSymbolTransform"
3522 conv = ensure_conversion(
3523 'invalid-symbol', options_file='cvs2svn-ignore2.options'
3527 class EOLVariants(Cvs2SvnTestCase):
3528 "handle various --eol-style options"
3530 eol_style_strings = {
3531 'LF' : '\n',
3532 'CR' : '\r',
3533 'CRLF' : '\r\n',
3534 'native' : '\n',
3537 def __init__(self, eol_style):
3538 self.eol_style = eol_style
3539 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3540 Cvs2SvnTestCase.__init__(
3541 self, 'eol-variants', variant=self.eol_style,
3542 dumpfile=self.dumpfile,
3543 args=[
3544 '--default-eol=%s' % (self.eol_style,),
3548 def run(self, sbox):
3549 conv = self.ensure_conversion()
3550 dump_contents = open(conv.dumpfile, 'rb').read()
3551 expected_text = self.eol_style_strings[self.eol_style].join(
3552 ['line 1', 'line 2', '\n\n']
3554 if not dump_contents.endswith(expected_text):
3555 raise Failure()
3558 @Cvs2SvnTestFunction
3559 def no_revs_file():
3560 "handle a file with no revisions (issue #80)"
3562 conv = ensure_conversion('no-revs-file')
3565 @Cvs2SvnTestFunction
3566 def mirror_keyerror_test():
3567 "a case that gave KeyError in SVNRepositoryMirror"
3569 conv = ensure_conversion('mirror-keyerror')
3572 @Cvs2SvnTestFunction
3573 def exclude_ntdb_test():
3574 "exclude a non-trunk default branch"
3576 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3577 conv = ensure_conversion(
3578 'exclude-ntdb',
3579 args=[
3580 '--write-symbol-info=%s' % (symbol_info_file,),
3581 '--exclude=branch3',
3582 '--exclude=tag3',
3583 '--exclude=vendortag3',
3584 '--exclude=vendorbranch',
3589 @Cvs2SvnTestFunction
3590 def mirror_keyerror2_test():
3591 "a case that gave KeyError in RepositoryMirror"
3593 conv = ensure_conversion('mirror-keyerror2')
3596 @Cvs2SvnTestFunction
3597 def mirror_keyerror3_test():
3598 "a case that gave KeyError in RepositoryMirror"
3600 conv = ensure_conversion('mirror-keyerror3')
3603 @Cvs2SvnTestFunction
3604 def add_cvsignore_to_branch_test():
3605 "check adding .cvsignore to an existing branch"
3607 # This a test for issue #122.
3609 conv = ensure_conversion('add-cvsignore-to-branch')
3610 wc_tree = conv.get_wc_tree()
3611 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3612 if trunk_props['svn:ignore'] != '*.o\n\n':
3613 raise Failure()
3615 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3616 if branch_props['svn:ignore'] != '*.o\n\n':
3617 raise Failure()
3620 @Cvs2SvnTestFunction
3621 def missing_deltatext():
3622 "a revision's deltatext is missing"
3624 # This is a type of RCS file corruption that has been observed.
3625 conv = ensure_conversion(
3626 'missing-deltatext',
3627 error_re=(
3628 r"ERROR\: .* has no deltatext section for revision 1\.1\.4\.4"
3633 @Cvs2SvnTestFunction
3634 def transform_unlabeled_branch_name():
3635 "transform name of unlabeled branch"
3637 conv = ensure_conversion(
3638 'unlabeled-branch',
3639 args=[
3640 '--symbol-transform=unlabeled-1.1.4:BRANCH2',
3645 @Cvs2SvnTestFunction
3646 def ignore_unlabeled_branch():
3647 "ignoring an unlabeled branch is not allowed"
3649 conv = ensure_conversion(
3650 'unlabeled-branch',
3651 options_file='cvs2svn-ignore.options',
3652 error_re=(
3653 r"ERROR\: The unlabeled branch \'unlabeled\-1\.1\.4\' "
3654 r"in \'.*\' contains commits"
3659 @Cvs2SvnTestFunction
3660 def unlabeled_branch_name_collision():
3661 "transform branch to same name as unlabeled branch"
3663 conv = ensure_conversion(
3664 'unlabeled-branch',
3665 args=[
3666 '--symbol-transform=unlabeled-1.1.4:BRANCH',
3668 error_re=(
3669 r"ERROR\: Symbol name \'BRANCH\' is already used"
3674 @Cvs2SvnTestFunction
3675 def collision_with_unlabeled_branch_name():
3676 "transform unlabeled branch to same name as branch"
3678 conv = ensure_conversion(
3679 'unlabeled-branch',
3680 args=[
3681 '--symbol-transform=BRANCH:unlabeled-1.1.4',
3683 error_re=(
3684 r"ERROR\: Symbol name \'unlabeled\-1\.1\.4\' is already used"
3689 ########################################################################
3690 # Run the tests
3692 # list all tests here, starting with None:
3693 test_list = [
3694 None,
3695 # 1:
3696 show_usage,
3697 cvs2svn_manpage,
3698 cvs2git_manpage,
3699 attr_exec,
3700 space_fname,
3701 two_quick,
3702 PruneWithCare(),
3703 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3704 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3705 # 10:
3706 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3707 interleaved_commits,
3708 simple_commits,
3709 SimpleTags(),
3710 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3711 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3712 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3713 simple_branch_commits,
3714 mixed_time_tag,
3715 mixed_time_branch_with_added_file,
3716 # 20:
3717 mixed_commit,
3718 split_time_branch,
3719 bogus_tag,
3720 overlapping_branch,
3721 PhoenixBranch(),
3722 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3723 ctrl_char_in_log,
3724 overdead,
3725 NoTrunkPrune(),
3726 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3727 # 30:
3728 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3729 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3730 double_delete,
3731 split_branch,
3732 resync_misgroups,
3733 TaggedBranchAndTrunk(),
3734 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3735 enroot_race,
3736 enroot_race_obo,
3737 BranchDeleteFirst(),
3738 # 40:
3739 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3740 nonascii_filenames,
3741 UnicodeAuthor(
3742 warning_expected=1),
3743 UnicodeAuthor(
3744 warning_expected=0,
3745 variant='encoding', args=['--encoding=utf_8']),
3746 UnicodeAuthor(
3747 warning_expected=0,
3748 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3749 UnicodeLog(
3750 warning_expected=1),
3751 UnicodeLog(
3752 warning_expected=0,
3753 variant='encoding', args=['--encoding=utf_8']),
3754 UnicodeLog(
3755 warning_expected=0,
3756 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3757 vendor_branch_sameness,
3758 vendor_branch_trunk_only,
3759 # 50:
3760 default_branches,
3761 default_branches_trunk_only,
3762 default_branch_and_1_2,
3763 compose_tag_three_sources,
3764 pass5_when_to_fill,
3765 PeerPathPruning(),
3766 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3767 EmptyTrunk(),
3768 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3769 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3770 # 60:
3771 no_spurious_svn_commits,
3772 invalid_closings_on_trunk,
3773 individual_passes,
3774 resync_bug,
3775 branch_from_default_branch,
3776 file_in_attic_too,
3777 retain_file_in_attic_too,
3778 symbolic_name_filling_guide,
3779 eol_mime1,
3780 eol_mime2,
3781 # 70:
3782 eol_mime3,
3783 eol_mime4,
3784 cvs_revnums_off,
3785 cvs_revnums_on,
3786 keywords,
3787 ignore,
3788 requires_cvs,
3789 questionable_branch_names,
3790 questionable_tag_names,
3791 revision_reorder_bug,
3792 # 80:
3793 exclude,
3794 vendor_branch_delete_add,
3795 resync_pass2_pull_forward,
3796 native_eol,
3797 double_fill,
3798 XFail(double_fill2),
3799 resync_pass2_push_backward,
3800 double_add,
3801 bogus_branch_copy,
3802 nested_ttb_directories,
3803 # 90:
3804 auto_props_ignore_case,
3805 ctrl_char_in_filename,
3806 commit_dependencies,
3807 show_help_passes,
3808 multiple_tags,
3809 multiply_defined_symbols,
3810 multiply_defined_symbols_renamed,
3811 multiply_defined_symbols_ignored,
3812 repeatedly_defined_symbols,
3813 double_branch_delete,
3814 # 100:
3815 symbol_mismatches,
3816 overlook_symbol_mismatches,
3817 force_symbols,
3818 commit_blocks_tags,
3819 blocked_excludes,
3820 unblock_blocked_excludes,
3821 regexp_force_symbols,
3822 heuristic_symbol_default,
3823 branch_symbol_default,
3824 tag_symbol_default,
3825 # 110:
3826 symbol_transform,
3827 write_symbol_info,
3828 symbol_hints,
3829 parent_hints,
3830 parent_hints_invalid,
3831 parent_hints_wildcards,
3832 path_hints,
3833 issue_99,
3834 issue_100,
3835 issue_106,
3836 # 120:
3837 options_option,
3838 multiproject,
3839 crossproject,
3840 tag_with_no_revision,
3841 delete_cvsignore,
3842 repeated_deltatext,
3843 nasty_graphs,
3844 XFail(tagging_after_delete),
3845 crossed_branches,
3846 file_directory_conflict,
3847 # 130:
3848 attic_directory_conflict,
3849 internal_co,
3850 internal_co_exclude,
3851 internal_co_trunk_only,
3852 internal_co_keywords,
3853 leftover_revs,
3854 requires_internal_co,
3855 timestamp_chaos,
3856 symlinks,
3857 empty_trunk_path,
3858 # 140:
3859 preferred_parent_cycle,
3860 branch_from_empty_dir,
3861 trunk_readd,
3862 branch_from_deleted_1_1,
3863 add_on_branch,
3864 main_git,
3865 git_options,
3866 main_hg,
3867 invalid_symbol,
3868 invalid_symbol_ignore,
3869 # 150:
3870 invalid_symbol_ignore2,
3871 EOLVariants('LF'),
3872 EOLVariants('CR'),
3873 EOLVariants('CRLF'),
3874 EOLVariants('native'),
3875 no_revs_file,
3876 mirror_keyerror_test,
3877 exclude_ntdb_test,
3878 mirror_keyerror2_test,
3879 mirror_keyerror3_test,
3880 # 160:
3881 XFail(add_cvsignore_to_branch_test),
3882 missing_deltatext,
3883 transform_unlabeled_branch_name,
3884 ignore_unlabeled_branch,
3885 unlabeled_branch_name_collision,
3886 collision_with_unlabeled_branch_name,
3889 if __name__ == '__main__':
3891 # Configure the environment for reproducable output from svn, etc.
3892 os.environ["LC_ALL"] = "C"
3894 # Unfortunately, there is no way under Windows to make Subversion
3895 # think that the local time zone is UTC, so we just work in the
3896 # local time zone.
3898 # The Subversion test suite code assumes it's being invoked from
3899 # within a working copy of the Subversion sources, and tries to use
3900 # the binaries in that tree. Since the cvs2svn tree never contains
3901 # a Subversion build, we just use the system's installed binaries.
3902 svntest.main.svn_binary = svn_binary
3903 svntest.main.svnlook_binary = svnlook_binary
3904 svntest.main.svnadmin_binary = svnadmin_binary
3905 svntest.main.svnversion_binary = svnversion_binary
3907 run_tests(test_list)
3908 # NOTREACHED
3911 ### End of file.