Add a warning to the FAQ that cvs2svn doesn't work under psyco.
[cvs2svn.git] / run-tests.py
blob25f88f59aaa075d81eab8acdf45d91355e646c8d
1 #!/usr/bin/env python
3 # run_tests.py: test suite for cvs2svn
5 # Usage: run_tests.py [-v | --verbose] [list | <num>]
7 # Options:
8 # -v, --verbose
9 # enable verbose output
11 # Arguments (at most one argument is allowed):
12 # list
13 # If the word "list" is passed as an argument, the list of
14 # available tests is printed (but no tests are run).
16 # <num>
17 # If a number is passed as an argument, then only the test
18 # with that number is run.
20 # If no argument is specified, then all tests are run.
22 # Subversion is a tool for revision control.
23 # See http://subversion.tigris.org for more information.
25 # ====================================================================
26 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
28 # This software is licensed as described in the file COPYING, which
29 # you should have received as part of this distribution. The terms
30 # are also available at http://subversion.tigris.org/license-1.html.
31 # If newer versions of this license are posted there, you may use a
32 # newer version instead, at your option.
34 ######################################################################
36 # General modules
37 import sys
38 import shutil
39 import stat
40 import re
41 import os
42 import time
43 import os.path
44 import locale
45 import textwrap
46 import calendar
47 try:
48 from hashlib import md5
49 except ImportError:
50 from md5 import md5
51 from difflib import Differ
53 # Make sure that a supported version of Python is being used:
54 if not (0x02040000 <= sys.hexversion < 0x03000000):
55 sys.stderr.write(
56 'error: Python 2, version 2.4 or higher required.\n'
58 sys.exit(1)
60 # This script needs to run in the correct directory. Make sure we're there.
61 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
62 sys.stderr.write("error: I need to be run in the directory containing "
63 "'cvs2svn' and 'test-data'.\n")
64 sys.exit(1)
66 # Load the Subversion test framework.
67 import svntest
68 from svntest import Failure
69 from svntest.main import run_command
70 from svntest.main import run_tests
71 from svntest.main import safe_rmtree
72 from svntest.testcase import TestCase
73 from svntest.testcase import Skip
74 from svntest.testcase import XFail
75 from svntest.tree import build_tree_from_wc
76 from svntest.tree import get_child
78 cvs2svn = os.path.abspath('cvs2svn')
79 cvs2git = os.path.abspath('cvs2git')
81 # We use the installed svn and svnlook binaries, instead of using
82 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
83 # behavior -- or even existence -- of local builds shouldn't affect
84 # the cvs2svn test suite.
85 svn = 'svn'
86 svnlook = 'svnlook'
88 test_data_dir = 'test-data'
89 tmp_dir = 'cvs2svn-tmp'
92 #----------------------------------------------------------------------
93 # Helpers.
94 #----------------------------------------------------------------------
97 # The value to expect for svn:keywords if it is set:
98 KEYWORDS = 'Author Date Id Revision'
101 class RunProgramException(Failure):
102 pass
105 class MissingErrorException(Failure):
106 def __init__(self, error_re):
107 Failure.__init__(
108 self, "Test failed because no error matched '%s'" % (error_re,)
112 def run_program(program, error_re, *varargs):
113 """Run PROGRAM with VARARGS, return stdout as a list of lines.
115 If there is any stderr and ERROR_RE is None, raise
116 RunProgramException, and print the stderr lines if
117 svntest.main.verbose_mode is true.
119 If ERROR_RE is not None, it is a string regular expression that must
120 match some line of stderr. If it fails to match, raise
121 MissingErrorExpection."""
123 # FIXME: exit_code is currently ignored.
124 exit_code, out, err = run_command(program, 1, 0, *varargs)
126 if error_re:
127 # Specified error expected on stderr.
128 if not err:
129 raise MissingErrorException(error_re)
130 else:
131 for line in err:
132 if re.match(error_re, line):
133 return out
134 raise MissingErrorException(error_re)
135 else:
136 # No stderr allowed.
137 if err:
138 if svntest.main.verbose_mode:
139 print '\n%s said:\n' % program
140 for line in err:
141 print ' ' + line,
142 print
143 raise RunProgramException()
145 return out
148 def run_cvs2svn(error_re, *varargs):
149 """Run cvs2svn with VARARGS, return stdout as a list of lines.
151 If there is any stderr and ERROR_RE is None, raise
152 RunProgramException, and print the stderr lines if
153 svntest.main.verbose_mode is true.
155 If ERROR_RE is not None, it is a string regular expression that must
156 match some line of stderr. If it fails to match, raise
157 MissingErrorException."""
159 # Use the same python that is running this script
160 return run_program(sys.executable, error_re, cvs2svn, *varargs)
161 # On Windows, for an unknown reason, the cmd.exe process invoked by
162 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
163 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
164 # this. Therefore, the redirection of the output to the .s-revs file fails.
165 # We no longer use the problematic invocation on any system, but this
166 # comment remains to warn about this problem.
169 def run_cvs2git(error_re, *varargs):
170 """Run cvs2git with VARARGS, returning stdout as a list of lines.
172 If there is any stderr and ERROR_RE is None, raise
173 RunProgramException, and print the stderr lines if
174 svntest.main.verbose_mode is true.
176 If ERROR_RE is not None, it is a string regular expression that must
177 match some line of stderr. If it fails to match, raise
178 MissingErrorException."""
180 # Use the same python that is running this script
181 return run_program(sys.executable, error_re, cvs2git, *varargs)
182 # On Windows, for an unknown reason, the cmd.exe process invoked by
183 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
184 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
185 # this. Therefore, the redirection of the output to the .s-revs file fails.
186 # We no longer use the problematic invocation on any system, but this
187 # comment remains to warn about this problem.
190 def run_svn(*varargs):
191 """Run svn with VARARGS; return stdout as a list of lines.
192 If there is any stderr, raise RunProgramException, and print the
193 stderr lines if svntest.main.verbose_mode is true."""
194 return run_program(svn, None, *varargs)
197 def repos_to_url(path_to_svn_repos):
198 """This does what you think it does."""
199 rpath = os.path.abspath(path_to_svn_repos)
200 if rpath[0] != '/':
201 rpath = '/' + rpath
202 return 'file://%s' % rpath.replace(os.sep, '/')
205 def svn_strptime(timestr):
206 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
209 class Log:
210 def __init__(self, revision, author, date, symbols):
211 self.revision = revision
212 self.author = author
214 # Internally, we represent the date as seconds since epoch (UTC).
215 # Since standard subversion log output shows dates in localtime
217 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
219 # and time.mktime() converts from localtime, it all works out very
220 # happily.
221 self.date = time.mktime(svn_strptime(date[0:19]))
223 # The following symbols are used for string interpolation when
224 # checking paths:
225 self.symbols = symbols
227 # The changed paths will be accumulated later, as log data is read.
228 # Keys here are paths such as '/trunk/foo/bar', values are letter
229 # codes such as 'M', 'A', and 'D'.
230 self.changed_paths = { }
232 # The msg will be accumulated later, as log data is read.
233 self.msg = ''
235 def absorb_changed_paths(self, out):
236 'Read changed paths from OUT into self, until no more.'
237 while 1:
238 line = out.readline()
239 if len(line) == 1: return
240 line = line[:-1]
241 op_portion = line[3:4]
242 path_portion = line[5:]
243 # If we're running on Windows we get backslashes instead of
244 # forward slashes.
245 path_portion = path_portion.replace('\\', '/')
246 # # We could parse out history information, but currently we
247 # # just leave it in the path portion because that's how some
248 # # tests expect it.
250 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
251 # if m:
252 # path_portion = m.group(1)
253 self.changed_paths[path_portion] = op_portion
255 def __cmp__(self, other):
256 return cmp(self.revision, other.revision) or \
257 cmp(self.author, other.author) or cmp(self.date, other.date) or \
258 cmp(self.changed_paths, other.changed_paths) or \
259 cmp(self.msg, other.msg)
261 def get_path_op(self, path):
262 """Return the operator for the change involving PATH.
264 PATH is allowed to include string interpolation directives (e.g.,
265 '%(trunk)s'), which are interpolated against self.symbols. Return
266 None if there is no record for PATH."""
267 return self.changed_paths.get(path % self.symbols)
269 def check_msg(self, msg):
270 """Verify that this Log's message starts with the specified MSG."""
271 if self.msg.find(msg) != 0:
272 raise Failure(
273 "Revision %d log message was:\n%s\n\n"
274 "It should have begun with:\n%s\n\n"
275 % (self.revision, self.msg, msg,)
278 def check_change(self, path, op):
279 """Verify that this Log includes a change for PATH with operator OP.
281 PATH is allowed to include string interpolation directives (e.g.,
282 '%(trunk)s'), which are interpolated against self.symbols."""
284 path = path % self.symbols
285 found_op = self.changed_paths.get(path, None)
286 if found_op is None:
287 raise Failure(
288 "Revision %d does not include change for path %s "
289 "(it should have been %s).\n"
290 % (self.revision, path, op,)
292 if found_op != op:
293 raise Failure(
294 "Revision %d path %s had op %s (it should have been %s)\n"
295 % (self.revision, path, found_op, op,)
298 def check_changes(self, changed_paths):
299 """Verify that this Log has precisely the CHANGED_PATHS specified.
301 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
302 strings are allowed to include string interpolation directives
303 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
305 cp = {}
306 for (path, op) in changed_paths:
307 cp[path % self.symbols] = op
309 if self.changed_paths != cp:
310 raise Failure(
311 "Revision %d changed paths list was:\n%s\n\n"
312 "It should have been:\n%s\n\n"
313 % (self.revision, self.changed_paths, cp,)
316 def check(self, msg, changed_paths):
317 """Verify that this Log has the MSG and CHANGED_PATHS specified.
319 Convenience function to check two things at once. MSG is passed
320 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
322 self.check_msg(msg)
323 self.check_changes(changed_paths)
326 def parse_log(svn_repos, symbols):
327 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
329 Initialize the Logs' symbols with SYMBOLS."""
331 class LineFeeder:
332 'Make a list of lines behave like an open file handle.'
333 def __init__(self, lines):
334 self.lines = lines
335 def readline(self):
336 if len(self.lines) > 0:
337 return self.lines.pop(0)
338 else:
339 return None
341 def absorb_message_body(out, num_lines, log):
342 """Read NUM_LINES of log message body from OUT into Log item LOG."""
344 for i in range(num_lines):
345 log.msg += out.readline()
347 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
348 '(?P<author>[^\|]+) \| '
349 '(?P<date>[^\|]+) '
350 '\| (?P<lines>[0-9]+) (line|lines)$')
352 log_separator = '-' * 72
354 logs = { }
356 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
358 while 1:
359 this_log = None
360 line = out.readline()
361 if not line: break
362 line = line[:-1]
364 if line.find(log_separator) == 0:
365 line = out.readline()
366 if not line: break
367 line = line[:-1]
368 m = log_start_re.match(line)
369 if m:
370 this_log = Log(
371 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
372 line = out.readline()
373 if not line.find('Changed paths:') == 0:
374 print 'unexpected log output (missing changed paths)'
375 print "Line: '%s'" % line
376 sys.exit(1)
377 this_log.absorb_changed_paths(out)
378 absorb_message_body(out, int(m.group('lines')), this_log)
379 logs[this_log.revision] = this_log
380 elif len(line) == 0:
381 break # We've reached the end of the log output.
382 else:
383 print 'unexpected log output (missing revision line)'
384 print "Line: '%s'" % line
385 sys.exit(1)
386 else:
387 print 'unexpected log output (missing log separator)'
388 print "Line: '%s'" % line
389 sys.exit(1)
391 return logs
394 def erase(path):
395 """Unconditionally remove PATH and its subtree, if any. PATH may be
396 non-existent, a file or symlink, or a directory."""
397 if os.path.isdir(path):
398 safe_rmtree(path)
399 elif os.path.exists(path):
400 os.remove(path)
403 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
405 def sym_log_msg(symbolic_name, is_tag=None):
406 """Return the expected log message for a cvs2svn-synthesized revision
407 creating branch or tag SYMBOLIC_NAME."""
409 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
410 if is_tag:
411 type = 'tag'
412 else:
413 type = 'branch'
415 return log_msg_text_wrapper.fill(
416 "This commit was manufactured by cvs2svn to create %s '%s'."
417 % (type, symbolic_name)
421 def make_conversion_id(
422 name, args, passbypass, options_file=None, symbol_hints_file=None
424 """Create an identifying tag for a conversion.
426 The return value can also be used as part of a filesystem path.
428 NAME is the name of the CVS repository.
430 ARGS are the extra arguments to be passed to cvs2svn.
432 PASSBYPASS is a boolean indicating whether the conversion is to be
433 run one pass at a time.
435 If OPTIONS_FILE is specified, it is an options file that will be
436 used for the conversion.
438 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
439 will be used for the conversion.
441 The 1-to-1 mapping between cvs2svn command parameters and
442 conversion_ids allows us to avoid running the same conversion more
443 than once, when multiple tests use exactly the same conversion."""
445 conv_id = name
447 args = args[:]
449 if passbypass:
450 args.append('--passbypass')
452 if symbol_hints_file is not None:
453 args.append('--symbol-hints=%s' % (symbol_hints_file,))
455 # There are some characters that are forbidden in filenames, and
456 # there is a limit on the total length of a path to a file. So use
457 # a hash of the parameters rather than concatenating the parameters
458 # into a string.
459 if args:
460 conv_id += "-" + md5('\0'.join(args)).hexdigest()
462 # Some options-file based tests rely on knowing the paths to which
463 # the repository should be written, so we handle that option as a
464 # predictable string:
465 if options_file is not None:
466 conv_id += '--options=%s' % (options_file,)
468 return conv_id
471 class Conversion:
472 """A record of a cvs2svn conversion.
474 Fields:
476 conv_id -- the conversion id for this Conversion.
478 name -- a one-word name indicating the involved repositories.
480 dumpfile -- the name of the SVN dumpfile created by the conversion
481 (if the DUMPFILE constructor argument was used); otherwise,
482 None.
484 repos -- the path to the svn repository. Unset if DUMPFILE was
485 specified.
487 logs -- a dictionary of Log instances, as returned by parse_log().
488 Unset if DUMPFILE was specified.
490 symbols -- a dictionary of symbols used for string interpolation
491 in path names.
493 stdout -- a list of lines written by cvs2svn to stdout
495 _wc -- the basename of the svn working copy (within tmp_dir).
496 Unset if DUMPFILE was specified.
498 _wc_path -- the path to the svn working copy, if it has already
499 been created; otherwise, None. (The working copy is created
500 lazily when get_wc() is called.) Unset if DUMPFILE was
501 specified.
503 _wc_tree -- the tree built from the svn working copy, if it has
504 already been created; otherwise, None. The tree is created
505 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
506 specified.
508 _svnrepos -- the basename of the svn repository (within tmp_dir).
509 Unset if DUMPFILE was specified."""
511 # The number of the last cvs2svn pass (determined lazily by
512 # get_last_pass()).
513 last_pass = None
515 @classmethod
516 def get_last_pass(cls):
517 """Return the number of cvs2svn's last pass."""
519 if cls.last_pass is None:
520 out = run_cvs2svn(None, '--help-passes')
521 cls.last_pass = int(out[-1].split()[0])
522 return cls.last_pass
524 def __init__(
525 self, conv_id, name, error_re, passbypass, symbols, args,
526 options_file=None, symbol_hints_file=None, dumpfile=None,
528 self.conv_id = conv_id
529 self.name = name
530 self.symbols = symbols
531 if not os.path.isdir(tmp_dir):
532 os.mkdir(tmp_dir)
534 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
536 if dumpfile:
537 self.dumpfile = os.path.join(tmp_dir, dumpfile)
538 # Clean up from any previous invocations of this script.
539 erase(self.dumpfile)
540 else:
541 self.dumpfile = None
542 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
543 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
544 self._wc_path = None
545 self._wc_tree = None
547 # Clean up from any previous invocations of this script.
548 erase(self.repos)
549 erase(self._wc)
551 args = list(args)
552 if options_file:
553 self.options_file = os.path.join(cvsrepos, options_file)
554 args.extend([
555 '--options=%s' % self.options_file,
557 assert not symbol_hints_file
558 else:
559 self.options_file = None
560 if tmp_dir != 'cvs2svn-tmp':
561 # Only include this argument if it differs from cvs2svn's default:
562 args.extend([
563 '--tmpdir=%s' % tmp_dir,
566 if symbol_hints_file:
567 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
568 args.extend([
569 '--symbol-hints=%s' % self.symbol_hints_file,
572 if self.dumpfile:
573 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
574 else:
575 args.extend(['-s', self.repos])
576 args.extend([cvsrepos])
578 if passbypass:
579 self.stdout = []
580 for p in range(1, self.get_last_pass() + 1):
581 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
582 else:
583 self.stdout = run_cvs2svn(error_re, *args)
585 if self.dumpfile:
586 if not os.path.isfile(self.dumpfile):
587 raise Failure(
588 "Dumpfile not created: '%s'"
589 % os.path.join(os.getcwd(), self.dumpfile)
591 else:
592 if os.path.isdir(self.repos):
593 self.logs = parse_log(self.repos, self.symbols)
594 elif error_re is None:
595 raise Failure(
596 "Repository not created: '%s'"
597 % os.path.join(os.getcwd(), self.repos)
600 def output_found(self, pattern):
601 """Return True if PATTERN matches any line in self.stdout.
603 PATTERN is a regular expression pattern as a string.
606 pattern_re = re.compile(pattern)
608 for line in self.stdout:
609 if pattern_re.match(line):
610 # We found the pattern that we were looking for.
611 return 1
612 else:
613 return 0
615 def find_tag_log(self, tagname):
616 """Search LOGS for a log message containing 'TAGNAME' and return the
617 log in which it was found."""
618 for i in xrange(len(self.logs), 0, -1):
619 if self.logs[i].msg.find("'"+tagname+"'") != -1:
620 return self.logs[i]
621 raise ValueError("Tag %s not found in logs" % tagname)
623 def get_wc(self, *args):
624 """Return the path to the svn working copy, or a path within the WC.
626 If a working copy has not been created yet, create it now.
628 If ARGS are specified, then they should be strings that form
629 fragments of a path within the WC. They are joined using
630 os.path.join() and appended to the WC path."""
632 if self._wc_path is None:
633 run_svn('co', repos_to_url(self.repos), self._wc)
634 self._wc_path = self._wc
635 return os.path.join(self._wc_path, *args)
637 def get_wc_tree(self):
638 if self._wc_tree is None:
639 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
640 return self._wc_tree
642 def path_exists(self, *args):
643 """Return True if the specified path exists within the repository.
645 (The strings in ARGS are first joined into a path using
646 os.path.join().)"""
648 return os.path.exists(self.get_wc(*args))
650 def check_props(self, keys, checks):
651 """Helper function for checking lots of properties. For a list of
652 files in the conversion, check that the values of the properties
653 listed in KEYS agree with those listed in CHECKS. CHECKS is a
654 list of tuples: [ (filename, [value, value, ...]), ...], where the
655 values are listed in the same order as the key names are listed in
656 KEYS."""
658 for (file, values) in checks:
659 assert len(values) == len(keys)
660 props = props_for_path(self.get_wc_tree(), file)
661 for i in range(len(keys)):
662 if props.get(keys[i]) != values[i]:
663 raise Failure(
664 "File %s has property %s set to \"%s\" "
665 "(it should have been \"%s\").\n"
666 % (file, keys[i], props.get(keys[i]), values[i],)
670 class GitConversion:
671 """A record of a cvs2svn conversion.
673 Fields:
675 name -- a one-word name indicating the CVS repository to be converted.
677 stdout -- a list of lines written by cvs2svn to stdout."""
679 def __init__(self, name, error_re, args, options_file=None):
680 self.name = name
681 if not os.path.isdir(tmp_dir):
682 os.mkdir(tmp_dir)
684 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
686 args = list(args)
687 if options_file:
688 self.options_file = os.path.join(cvsrepos, options_file)
689 args.extend([
690 '--options=%s' % self.options_file,
692 else:
693 self.options_file = None
695 self.stdout = run_cvs2git(error_re, *args)
698 # Cache of conversions that have already been done. Keys are conv_id;
699 # values are Conversion instances.
700 already_converted = { }
702 def ensure_conversion(
703 name, error_re=None, passbypass=None,
704 trunk=None, branches=None, tags=None,
705 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
707 """Convert CVS repository NAME to Subversion, but only if it has not
708 been converted before by this invocation of this script. If it has
709 been converted before, return the Conversion object from the
710 previous invocation.
712 If no error, return a Conversion instance.
714 If ERROR_RE is a string, it is a regular expression expected to
715 match some line of stderr printed by the conversion. If there is an
716 error and ERROR_RE is not set, then raise Failure.
718 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
719 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
721 NAME is just one word. For example, 'main' would mean to convert
722 './test-data/main-cvsrepos', and after the conversion, the resulting
723 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
724 a checked out head working copy in './cvs2svn-tmp/main-wc'.
726 Any other options to pass to cvs2svn should be in ARGS, each element
727 being one option, e.g., '--trunk-only'. If the option takes an
728 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
729 are passed to cvs2svn in the order that they appear in ARGS.
731 If OPTIONS_FILE is specified, then it should be the name of a file
732 within the main directory of the cvs repository associated with this
733 test. It is passed to cvs2svn using the --options option (which
734 suppresses some other options that are incompatible with --options).
736 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
737 file within the main directory of the cvs repository associated with
738 this test. It is passed to cvs2svn using the --symbol-hints option.
740 If DUMPFILE is specified, then it is the name of a dumpfile within
741 the temporary directory to which the conversion output should be
742 written."""
744 if args is None:
745 args = []
746 else:
747 args = list(args)
749 if trunk is None:
750 trunk = 'trunk'
751 else:
752 args.append('--trunk=%s' % (trunk,))
754 if branches is None:
755 branches = 'branches'
756 else:
757 args.append('--branches=%s' % (branches,))
759 if tags is None:
760 tags = 'tags'
761 else:
762 args.append('--tags=%s' % (tags,))
764 conv_id = make_conversion_id(
765 name, args, passbypass, options_file, symbol_hints_file
768 if conv_id not in already_converted:
769 try:
770 # Run the conversion and store the result for the rest of this
771 # session:
772 already_converted[conv_id] = Conversion(
773 conv_id, name, error_re, passbypass,
774 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
775 args, options_file, symbol_hints_file, dumpfile,
777 except Failure:
778 # Remember the failure so that a future attempt to run this conversion
779 # does not bother to retry, but fails immediately.
780 already_converted[conv_id] = None
781 raise
783 conv = already_converted[conv_id]
784 if conv is None:
785 raise Failure()
786 return conv
789 class Cvs2SvnTestCase(TestCase):
790 def __init__(
791 self, name, description=None, variant=None,
792 error_re=None, passbypass=None,
793 trunk=None, branches=None, tags=None,
794 args=None,
795 options_file=None, symbol_hints_file=None, dumpfile=None,
797 TestCase.__init__(self)
798 self.name = name
800 if description is not None:
801 self._description = description
802 else:
803 # By default, use the first line of the class docstring as the
804 # description:
805 self._description = self.__doc__.splitlines()[0]
807 # Check that the original description is OK before we tinker with
808 # it:
809 self.check_description()
811 if variant is not None:
812 # Modify description to show the variant. Trim description
813 # first if necessary to stay within the 50-character limit.
814 suffix = '...variant %s' % (variant,)
815 self._description = self._description[:50 - len(suffix)] + suffix
816 # Check that the description is still OK:
817 self.check_description()
819 self.error_re = error_re
820 self.passbypass = passbypass
821 self.trunk = trunk
822 self.branches = branches
823 self.tags = tags
824 self.args = args
825 self.options_file = options_file
826 self.symbol_hints_file = symbol_hints_file
827 self.dumpfile = dumpfile
829 def get_description(self):
830 return self._description
832 def ensure_conversion(self):
833 return ensure_conversion(
834 self.name,
835 error_re=self.error_re, passbypass=self.passbypass,
836 trunk=self.trunk, branches=self.branches, tags=self.tags,
837 args=self.args,
838 options_file=self.options_file,
839 symbol_hints_file=self.symbol_hints_file,
840 dumpfile=self.dumpfile,
844 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
845 """Test properties resulting from a conversion."""
847 def __init__(self, name, props_to_test, expected_props, **kw):
848 """Initialize an instance of Cvs2SvnPropertiesTestCase.
850 NAME is the name of the test, passed to Cvs2SvnTestCase.
851 PROPS_TO_TEST is a list of the names of svn properties that should
852 be tested. EXPECTED_PROPS is a list of tuples [(filename,
853 [value,...])], where the second item in each tuple is a list of
854 values expected for the properties listed in PROPS_TO_TEST for the
855 specified filename. If a property must *not* be set, then its
856 value should be listed as None."""
858 Cvs2SvnTestCase.__init__(self, name, **kw)
859 self.props_to_test = props_to_test
860 self.expected_props = expected_props
862 def run(self):
863 conv = self.ensure_conversion()
864 conv.check_props(self.props_to_test, self.expected_props)
867 #----------------------------------------------------------------------
868 # Tests.
869 #----------------------------------------------------------------------
872 def show_usage():
873 "cvs2svn with no arguments shows usage"
874 out = run_cvs2svn(None)
875 if (len(out) > 2 and out[0].find('ERROR:') == 0
876 and out[1].find('DBM module')):
877 print 'cvs2svn cannot execute due to lack of proper DBM module.'
878 print 'Exiting without running any further tests.'
879 sys.exit(1)
880 if out[0].find('Usage:') < 0:
881 raise Failure('Basic cvs2svn invocation failed.')
884 def cvs2svn_manpage():
885 "generate a manpage for cvs2svn"
886 out = run_cvs2svn(None, '--man')
889 def cvs2git_manpage():
890 "generate a manpage for cvs2git"
891 out = run_cvs2git(None, '--man')
894 def show_help_passes():
895 "cvs2svn --help-passes shows pass information"
896 out = run_cvs2svn(None, '--help-passes')
897 if out[0].find('PASSES') < 0:
898 raise Failure('cvs2svn --help-passes failed.')
901 def attr_exec():
902 "detection of the executable flag"
903 if sys.platform == 'win32':
904 raise svntest.Skip()
905 conv = ensure_conversion('main')
906 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
907 if not st[0] & stat.S_IXUSR:
908 raise Failure()
911 def space_fname():
912 "conversion of filename with a space"
913 conv = ensure_conversion('main')
914 if not conv.path_exists('trunk', 'single-files', 'space fname'):
915 raise Failure()
918 def two_quick():
919 "two commits in quick succession"
920 conv = ensure_conversion('main')
921 logs = parse_log(
922 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
923 if len(logs) != 2:
924 raise Failure()
927 class PruneWithCare(Cvs2SvnTestCase):
928 "prune, but never too much"
930 def __init__(self, **kw):
931 Cvs2SvnTestCase.__init__(self, 'main', **kw)
933 def run(self):
934 # Robert Pluim encountered this lovely one while converting the
935 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
936 # repository (see issue #1302). Step 4 is the doozy:
938 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
939 # revision 2: adds trunk/blah/NEWS
940 # revision 3: deletes trunk/blah/cookie
941 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
942 # revision 5: does nothing
944 # After fixing cvs2svn, the sequence (correctly) looks like this:
946 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
947 # revision 2: adds trunk/blah/NEWS
948 # revision 3: deletes trunk/blah/cookie
949 # revision 4: does nothing [because trunk/blah/cookie already deleted]
950 # revision 5: deletes blah
952 # The difference is in 4 and 5. In revision 4, it's not correct to
953 # prune blah/, because NEWS is still in there, so revision 4 does
954 # nothing now. But when we delete NEWS in 5, that should bubble up
955 # and prune blah/ instead.
957 # ### Note that empty revisions like 4 are probably going to become
958 # ### at least optional, if not banished entirely from cvs2svn's
959 # ### output. Hmmm, or they may stick around, with an extra
960 # ### revision property explaining what happened. Need to think
961 # ### about that. In some sense, it's a bug in Subversion itself,
962 # ### that such revisions don't show up in 'svn log' output.
964 # In the test below, 'trunk/full-prune/first' represents
965 # cookie, and 'trunk/full-prune/second' represents NEWS.
967 conv = self.ensure_conversion()
969 # Confirm that revision 4 removes '/trunk/full-prune/first',
970 # and that revision 6 removes '/trunk/full-prune'.
972 # Also confirm similar things about '/full-prune-reappear/...',
973 # which is similar, except that later on it reappears, restored
974 # from pruneland, because a file gets added to it.
976 # And finally, a similar thing for '/partial-prune/...', except that
977 # in its case, a permanent file on the top level prevents the
978 # pruning from going farther than the subdirectory containing first
979 # and second.
981 for path in ('full-prune/first',
982 'full-prune-reappear/sub/first',
983 'partial-prune/sub/first'):
984 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
986 for path in ('full-prune',
987 'full-prune-reappear',
988 'partial-prune/sub'):
989 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
991 for path in ('full-prune-reappear',
992 'full-prune-reappear/appears-later'):
993 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
996 def interleaved_commits():
997 "two interleaved trunk commits, different log msgs"
998 # See test-data/main-cvsrepos/proj/README.
999 conv = ensure_conversion('main')
1001 # The initial import.
1002 rev = 26
1003 conv.logs[rev].check('Initial import.', (
1004 ('/%(trunk)s/interleaved', 'A'),
1005 ('/%(trunk)s/interleaved/1', 'A'),
1006 ('/%(trunk)s/interleaved/2', 'A'),
1007 ('/%(trunk)s/interleaved/3', 'A'),
1008 ('/%(trunk)s/interleaved/4', 'A'),
1009 ('/%(trunk)s/interleaved/5', 'A'),
1010 ('/%(trunk)s/interleaved/a', 'A'),
1011 ('/%(trunk)s/interleaved/b', 'A'),
1012 ('/%(trunk)s/interleaved/c', 'A'),
1013 ('/%(trunk)s/interleaved/d', 'A'),
1014 ('/%(trunk)s/interleaved/e', 'A'),
1017 def check_letters(rev):
1018 """Check if REV is the rev where only letters were committed."""
1020 conv.logs[rev].check('Committing letters only.', (
1021 ('/%(trunk)s/interleaved/a', 'M'),
1022 ('/%(trunk)s/interleaved/b', 'M'),
1023 ('/%(trunk)s/interleaved/c', 'M'),
1024 ('/%(trunk)s/interleaved/d', 'M'),
1025 ('/%(trunk)s/interleaved/e', 'M'),
1028 def check_numbers(rev):
1029 """Check if REV is the rev where only numbers were committed."""
1031 conv.logs[rev].check('Committing numbers only.', (
1032 ('/%(trunk)s/interleaved/1', 'M'),
1033 ('/%(trunk)s/interleaved/2', 'M'),
1034 ('/%(trunk)s/interleaved/3', 'M'),
1035 ('/%(trunk)s/interleaved/4', 'M'),
1036 ('/%(trunk)s/interleaved/5', 'M'),
1039 # One of the commits was letters only, the other was numbers only.
1040 # But they happened "simultaneously", so we don't assume anything
1041 # about which commit appeared first, so we just try both ways.
1042 rev += 1
1043 try:
1044 check_letters(rev)
1045 check_numbers(rev + 1)
1046 except Failure:
1047 check_numbers(rev)
1048 check_letters(rev + 1)
1051 def simple_commits():
1052 "simple trunk commits"
1053 # See test-data/main-cvsrepos/proj/README.
1054 conv = ensure_conversion('main')
1056 # The initial import.
1057 conv.logs[13].check('Initial import.', (
1058 ('/%(trunk)s/proj', 'A'),
1059 ('/%(trunk)s/proj/default', 'A'),
1060 ('/%(trunk)s/proj/sub1', 'A'),
1061 ('/%(trunk)s/proj/sub1/default', 'A'),
1062 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1063 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1064 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1065 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1066 ('/%(trunk)s/proj/sub2', 'A'),
1067 ('/%(trunk)s/proj/sub2/default', 'A'),
1068 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1069 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1070 ('/%(trunk)s/proj/sub3', 'A'),
1071 ('/%(trunk)s/proj/sub3/default', 'A'),
1074 # The first commit.
1075 conv.logs[18].check('First commit to proj, affecting two files.', (
1076 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1077 ('/%(trunk)s/proj/sub3/default', 'M'),
1080 # The second commit.
1081 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1082 ('/%(trunk)s/proj/default', 'M'),
1083 ('/%(trunk)s/proj/sub1/default', 'M'),
1084 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1085 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1086 ('/%(trunk)s/proj/sub2/default', 'M'),
1087 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1088 ('/%(trunk)s/proj/sub3/default', 'M')
1092 class SimpleTags(Cvs2SvnTestCase):
1093 "simple tags and branches, no commits"
1095 def __init__(self, **kw):
1096 # See test-data/main-cvsrepos/proj/README.
1097 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1099 def run(self):
1100 conv = self.ensure_conversion()
1102 # Verify the copy source for the tags we are about to check
1103 # No need to verify the copyfrom revision, as simple_commits did that
1104 conv.logs[13].check('Initial import.', (
1105 ('/%(trunk)s/proj', 'A'),
1106 ('/%(trunk)s/proj/default', 'A'),
1107 ('/%(trunk)s/proj/sub1', 'A'),
1108 ('/%(trunk)s/proj/sub1/default', 'A'),
1109 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1110 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1111 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1112 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1113 ('/%(trunk)s/proj/sub2', 'A'),
1114 ('/%(trunk)s/proj/sub2/default', 'A'),
1115 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1116 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1117 ('/%(trunk)s/proj/sub3', 'A'),
1118 ('/%(trunk)s/proj/sub3/default', 'A'),
1121 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1123 # Tag on rev 1.1.1.1 of all files in proj
1124 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1125 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1126 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1127 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1130 # The same, as a tag
1131 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1132 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1133 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1136 # Tag on rev 1.1.1.1 of all files in proj, except one
1137 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1138 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1139 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1140 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1143 # The same, as a branch
1144 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1145 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1146 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1150 def simple_branch_commits():
1151 "simple branch commits"
1152 # See test-data/main-cvsrepos/proj/README.
1153 conv = ensure_conversion('main')
1155 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1156 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1157 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1158 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1162 def mixed_time_tag():
1163 "mixed-time tag"
1164 # See test-data/main-cvsrepos/proj/README.
1165 conv = ensure_conversion('main')
1167 log = conv.find_tag_log('T_MIXED')
1168 log.check_changes((
1169 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1173 def mixed_time_branch_with_added_file():
1174 "mixed-time branch, and a file added to the branch"
1175 # See test-data/main-cvsrepos/proj/README.
1176 conv = ensure_conversion('main')
1178 # A branch from the same place as T_MIXED in the previous test,
1179 # plus a file added directly to the branch
1180 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1181 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1182 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1183 ('/%(branches)s/B_MIXED/single-files', 'D'),
1184 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1185 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1186 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1189 conv.logs[22].check('Add a file on branch B_MIXED.', (
1190 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1194 def mixed_commit():
1195 "a commit affecting both trunk and a branch"
1196 # See test-data/main-cvsrepos/proj/README.
1197 conv = ensure_conversion('main')
1199 conv.logs[24].check(
1200 'A single commit affecting one file on branch B_MIXED '
1201 'and one on trunk.', (
1202 ('/%(trunk)s/proj/sub2/default', 'M'),
1203 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1207 def split_time_branch():
1208 "branch some trunk files, and later branch the rest"
1209 # See test-data/main-cvsrepos/proj/README.
1210 conv = ensure_conversion('main')
1212 # First change on the branch, creating it
1213 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1214 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1215 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1216 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1217 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1220 conv.logs[29].check('First change on branch B_SPLIT.', (
1221 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1222 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1223 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1224 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1225 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1228 # A trunk commit for the file which was not branched
1229 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1230 'This was committed about an', (
1231 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1234 # Add the file not already branched to the branch, with modification:w
1235 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1236 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1237 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1240 conv.logs[32].check('This change affects sub3/default and '
1241 'sub1/subsubB/default, on branch', (
1242 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1243 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1247 def multiple_tags():
1248 "multiple tags referring to same revision"
1249 conv = ensure_conversion('main')
1250 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1251 raise Failure()
1252 if not conv.path_exists(
1253 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1254 raise Failure()
1257 def multiply_defined_symbols():
1258 "multiple definitions of symbol names"
1260 # We can only check one line of the error output at a time, so test
1261 # twice. (The conversion only have to be done once because the
1262 # results are cached.)
1263 conv = ensure_conversion(
1264 'multiply-defined-symbols',
1265 error_re=(
1266 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1267 r"1\.2\.4 1\.2\.2"
1270 conv = ensure_conversion(
1271 'multiply-defined-symbols',
1272 error_re=(
1273 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1274 r"1\.2 1\.1"
1279 def multiply_defined_symbols_renamed():
1280 "rename multiply defined symbols"
1282 conv = ensure_conversion(
1283 'multiply-defined-symbols',
1284 options_file='cvs2svn-rename.options',
1288 def multiply_defined_symbols_ignored():
1289 "ignore multiply defined symbols"
1291 conv = ensure_conversion(
1292 'multiply-defined-symbols',
1293 options_file='cvs2svn-ignore.options',
1297 def repeatedly_defined_symbols():
1298 "multiple identical definitions of symbol names"
1300 # If a symbol is defined multiple times but has the same value each
1301 # time, that should not be an error.
1303 conv = ensure_conversion('repeatedly-defined-symbols')
1306 def bogus_tag():
1307 "conversion of invalid symbolic names"
1308 conv = ensure_conversion('bogus-tag')
1311 def overlapping_branch():
1312 "ignore a file with a branch with two names"
1313 conv = ensure_conversion('overlapping-branch')
1315 if not conv.output_found('.*cannot also have name \'vendorB\''):
1316 raise Failure()
1318 conv.logs[2].check('imported', (
1319 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1320 ('/%(trunk)s/overlapping-branch', 'A'),
1323 if len(conv.logs) != 2:
1324 raise Failure()
1327 class PhoenixBranch(Cvs2SvnTestCase):
1328 "convert a branch file rooted in a 'dead' revision"
1330 def __init__(self, **kw):
1331 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1333 def run(self):
1334 conv = self.ensure_conversion()
1335 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1336 ('/%(branches)s/volsung_20010721', 'A'),
1337 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1339 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1340 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1344 ###TODO: We check for 4 changed paths here to accomodate creating tags
1345 ###and branches in rev 1, but that will change, so this will
1346 ###eventually change back.
1347 def ctrl_char_in_log():
1348 "handle a control char in a log message"
1349 # This was issue #1106.
1350 rev = 2
1351 conv = ensure_conversion('ctrl-char-in-log')
1352 conv.logs[rev].check_changes((
1353 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1355 if conv.logs[rev].msg.find('\x04') < 0:
1356 raise Failure(
1357 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1360 def overdead():
1361 "handle tags rooted in a redeleted revision"
1362 conv = ensure_conversion('overdead')
1365 class NoTrunkPrune(Cvs2SvnTestCase):
1366 "ensure that trunk doesn't get pruned"
1368 def __init__(self, **kw):
1369 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1371 def run(self):
1372 conv = self.ensure_conversion()
1373 for rev in conv.logs.keys():
1374 rev_logs = conv.logs[rev]
1375 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1376 raise Failure()
1379 def double_delete():
1380 "file deleted twice, in the root of the repository"
1381 # This really tests several things: how we handle a file that's
1382 # removed (state 'dead') in two successive revisions; how we
1383 # handle a file in the root of the repository (there were some
1384 # bugs in cvs2svn's svn path construction for top-level files); and
1385 # the --no-prune option.
1386 conv = ensure_conversion(
1387 'double-delete', args=['--trunk-only', '--no-prune'])
1389 path = '/%(trunk)s/twice-removed'
1390 rev = 2
1391 conv.logs[rev].check('Updated CVS', (
1392 (path, 'A'),
1394 conv.logs[rev + 1].check('Remove this file for the first time.', (
1395 (path, 'D'),
1397 conv.logs[rev + 2].check('Remove this file for the second time,', (
1401 def split_branch():
1402 "branch created from both trunk and another branch"
1403 # See test-data/split-branch-cvsrepos/README.
1405 # The conversion will fail if the bug is present, and
1406 # ensure_conversion will raise Failure.
1407 conv = ensure_conversion('split-branch')
1410 def resync_misgroups():
1411 "resyncing should not misorder commit groups"
1412 # See test-data/resync-misgroups-cvsrepos/README.
1414 # The conversion will fail if the bug is present, and
1415 # ensure_conversion will raise Failure.
1416 conv = ensure_conversion('resync-misgroups')
1419 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1420 "allow tags with mixed trunk and branch sources"
1422 def __init__(self, **kw):
1423 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1425 def run(self):
1426 conv = self.ensure_conversion()
1428 tags = conv.symbols.get('tags', 'tags')
1430 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1431 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1432 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1433 raise Failure()
1434 if (open(a_path, 'r').read().find('1.24') == -1) \
1435 or (open(b_path, 'r').read().find('1.5') == -1):
1436 raise Failure()
1439 def enroot_race():
1440 "never use the rev-in-progress as a copy source"
1442 # See issue #1427 and r8544.
1443 conv = ensure_conversion('enroot-race')
1444 rev = 6
1445 conv.logs[rev].check_changes((
1446 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1447 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1448 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1450 conv.logs[rev + 1].check_changes((
1451 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1452 ('/%(trunk)s/proj/a.txt', 'M'),
1453 ('/%(trunk)s/proj/b.txt', 'M'),
1457 def enroot_race_obo():
1458 "do use the last completed rev as a copy source"
1459 conv = ensure_conversion('enroot-race-obo')
1460 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1461 if not len(conv.logs) == 3:
1462 raise Failure()
1465 class BranchDeleteFirst(Cvs2SvnTestCase):
1466 "correctly handle deletion as initial branch action"
1468 def __init__(self, **kw):
1469 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1471 def run(self):
1472 # See test-data/branch-delete-first-cvsrepos/README.
1474 # The conversion will fail if the bug is present, and
1475 # ensure_conversion would raise Failure.
1476 conv = self.ensure_conversion()
1478 branches = conv.symbols.get('branches', 'branches')
1480 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1481 if conv.path_exists(branches, 'branch-1', 'file'):
1482 raise Failure()
1483 if conv.path_exists(branches, 'branch-2', 'file'):
1484 raise Failure()
1485 if not conv.path_exists(branches, 'branch-3', 'file'):
1486 raise Failure()
1489 def nonascii_filenames():
1490 "non ascii files converted incorrectly"
1491 # see issue #1255
1493 # on a en_US.iso-8859-1 machine this test fails with
1494 # svn: Can't recode ...
1496 # as described in the issue
1498 # on a en_US.UTF-8 machine this test fails with
1499 # svn: Malformed XML ...
1501 # which means at least it fails. Unfortunately it won't fail
1502 # with the same error...
1504 # mangle current locale settings so we know we're not running
1505 # a UTF-8 locale (which does not exhibit this problem)
1506 current_locale = locale.getlocale()
1507 new_locale = 'en_US.ISO8859-1'
1508 locale_changed = None
1510 # From http://docs.python.org/lib/module-sys.html
1512 # getfilesystemencoding():
1514 # Return the name of the encoding used to convert Unicode filenames
1515 # into system file names, or None if the system default encoding is
1516 # used. The result value depends on the operating system:
1518 # - On Windows 9x, the encoding is ``mbcs''.
1519 # - On Mac OS X, the encoding is ``utf-8''.
1520 # - On Unix, the encoding is the user's preference according to the
1521 # result of nl_langinfo(CODESET), or None if the
1522 # nl_langinfo(CODESET) failed.
1523 # - On Windows NT+, file names are Unicode natively, so no conversion is
1524 # performed.
1526 # So we're going to skip this test on Mac OS X for now.
1527 if sys.platform == "darwin":
1528 raise svntest.Skip()
1530 try:
1531 # change locale to non-UTF-8 locale to generate latin1 names
1532 locale.setlocale(locale.LC_ALL, # this might be too broad?
1533 new_locale)
1534 locale_changed = 1
1535 except locale.Error:
1536 raise svntest.Skip()
1538 try:
1539 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1540 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1541 if not os.path.exists(dstrepos_path):
1542 # create repos from existing main repos
1543 shutil.copytree(srcrepos_path, dstrepos_path)
1544 base_path = os.path.join(dstrepos_path, 'single-files')
1545 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1546 os.path.join(base_path, 'two\366uick,v'))
1547 new_path = os.path.join(dstrepos_path, 'single\366files')
1548 os.rename(base_path, new_path)
1550 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1551 finally:
1552 if locale_changed:
1553 locale.setlocale(locale.LC_ALL, current_locale)
1554 safe_rmtree(dstrepos_path)
1557 class UnicodeTest(Cvs2SvnTestCase):
1558 "metadata contains unicode"
1560 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1562 def __init__(self, name, warning_expected, **kw):
1563 if warning_expected:
1564 error_re = self.warning_pattern
1565 else:
1566 error_re = None
1568 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1569 self.warning_expected = warning_expected
1571 def run(self):
1572 try:
1573 # ensure the availability of the "utf_8" encoding:
1574 u'a'.encode('utf_8').decode('utf_8')
1575 except LookupError:
1576 raise svntest.Skip()
1578 self.ensure_conversion()
1581 class UnicodeAuthor(UnicodeTest):
1582 "author name contains unicode"
1584 def __init__(self, warning_expected, **kw):
1585 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1588 class UnicodeLog(UnicodeTest):
1589 "log message contains unicode"
1591 def __init__(self, warning_expected, **kw):
1592 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1595 def vendor_branch_sameness():
1596 "avoid spurious changes for initial revs"
1597 conv = ensure_conversion(
1598 'vendor-branch-sameness', args=['--keep-trivial-imports']
1601 # The following files are in this repository:
1603 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1604 # the same contents, the file's default branch is 1.1.1,
1605 # and both revisions are in state 'Exp'.
1607 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1608 # 1.1 (the addition of a line of text).
1610 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1612 # d.txt: This file was created by 'cvs add' instead of import, so
1613 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1614 # The timestamp on the add is exactly the same as for the
1615 # imports of the other files.
1617 # e.txt: Like a.txt, except that the log message for revision 1.1
1618 # is not the standard import log message.
1620 # (Aside from e.txt, the log messages for the same revisions are the
1621 # same in all files.)
1623 # We expect that only a.txt is recognized as an import whose 1.1
1624 # revision can be omitted. The other files should be added on trunk
1625 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1626 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1627 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1628 # and c.txt should be 'D'eleted.
1630 rev = 2
1631 conv.logs[rev].check('Initial revision', (
1632 ('/%(trunk)s/proj', 'A'),
1633 ('/%(trunk)s/proj/b.txt', 'A'),
1634 ('/%(trunk)s/proj/c.txt', 'A'),
1635 ('/%(trunk)s/proj/d.txt', 'A'),
1638 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1639 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1640 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1643 conv.logs[rev + 2].check('First vendor branch revision.', (
1644 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1645 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1646 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1649 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1650 'to compensate for changes in r4,', (
1651 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1652 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1653 ('/%(trunk)s/proj/c.txt', 'D'),
1656 rev = 7
1657 conv.logs[rev].check('This log message is not the standard', (
1658 ('/%(trunk)s/proj/e.txt', 'A'),
1661 conv.logs[rev + 2].check('First vendor branch revision', (
1662 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1665 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1666 'to compensate for changes in r9,', (
1667 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1671 def vendor_branch_trunk_only():
1672 "handle vendor branches with --trunk-only"
1673 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1675 rev = 2
1676 conv.logs[rev].check('Initial revision', (
1677 ('/%(trunk)s/proj', 'A'),
1678 ('/%(trunk)s/proj/b.txt', 'A'),
1679 ('/%(trunk)s/proj/c.txt', 'A'),
1680 ('/%(trunk)s/proj/d.txt', 'A'),
1683 conv.logs[rev + 1].check('First vendor branch revision', (
1684 ('/%(trunk)s/proj/a.txt', 'A'),
1685 ('/%(trunk)s/proj/b.txt', 'M'),
1686 ('/%(trunk)s/proj/c.txt', 'D'),
1689 conv.logs[rev + 2].check('This log message is not the standard', (
1690 ('/%(trunk)s/proj/e.txt', 'A'),
1693 conv.logs[rev + 3].check('First vendor branch revision', (
1694 ('/%(trunk)s/proj/e.txt', 'M'),
1698 def default_branches():
1699 "handle default branches correctly"
1700 conv = ensure_conversion('default-branches')
1702 # There are seven files in the repository:
1704 # a.txt:
1705 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1706 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1707 # committed (thus losing the default branch "1.1.1"), then
1708 # 1.1.1.4 was imported. All vendor import release tags are
1709 # still present.
1711 # b.txt:
1712 # Like a.txt, but without rev 1.2.
1714 # c.txt:
1715 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1717 # d.txt:
1718 # Same as the previous two, but 1.1.1 branch is unlabeled.
1720 # e.txt:
1721 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1723 # deleted-on-vendor-branch.txt,v:
1724 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1726 # added-then-imported.txt,v:
1727 # Added with 'cvs add' to create 1.1, then imported with
1728 # completely different contents to create 1.1.1.1, therefore
1729 # never had a default branch.
1732 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1733 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1734 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1735 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1736 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1737 ('/%(branches)s/vbranchA', 'A'),
1738 ('/%(branches)s/vbranchA/proj', 'A'),
1739 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1740 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1741 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1742 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1745 conv.logs[3].check("This commit was generated by cvs2svn "
1746 "to compensate for changes in r2,", (
1747 ('/%(trunk)s/proj', 'A'),
1748 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1749 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1750 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1751 ('/%(trunk)s/proj/d.txt '
1752 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1753 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1754 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1755 ('/%(trunk)s/proj/e.txt '
1756 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1759 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1760 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1761 ('/%(tags)s/vtag-1/proj/d.txt '
1762 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1765 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1766 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1767 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1768 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1769 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1770 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1771 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1774 conv.logs[6].check("This commit was generated by cvs2svn "
1775 "to compensate for changes in r5,", (
1776 ('/%(trunk)s/proj/a.txt '
1777 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1778 ('/%(trunk)s/proj/b.txt '
1779 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1780 ('/%(trunk)s/proj/c.txt '
1781 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1782 ('/%(trunk)s/proj/d.txt '
1783 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1784 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1785 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1786 'R'),
1787 ('/%(trunk)s/proj/e.txt '
1788 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1791 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1792 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1793 ('/%(tags)s/vtag-2/proj/d.txt '
1794 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1797 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1798 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1799 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1800 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1801 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1802 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1803 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1806 conv.logs[9].check("This commit was generated by cvs2svn "
1807 "to compensate for changes in r8,", (
1808 ('/%(trunk)s/proj/a.txt '
1809 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1810 ('/%(trunk)s/proj/b.txt '
1811 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1812 ('/%(trunk)s/proj/c.txt '
1813 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1814 ('/%(trunk)s/proj/d.txt '
1815 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1816 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1817 ('/%(trunk)s/proj/e.txt '
1818 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1821 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1822 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1823 ('/%(tags)s/vtag-3/proj/d.txt '
1824 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1825 ('/%(tags)s/vtag-3/proj/e.txt '
1826 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1829 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1830 ('/%(trunk)s/proj/a.txt', 'M'),
1833 conv.logs[12].check("Add a file to the working copy.", (
1834 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1837 conv.logs[13].check(sym_log_msg('vbranchA'), (
1838 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1839 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1842 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1843 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1844 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1845 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1846 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1847 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1848 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1849 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1852 conv.logs[15].check("This commit was generated by cvs2svn "
1853 "to compensate for changes in r14,", (
1854 ('/%(trunk)s/proj/b.txt '
1855 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1856 ('/%(trunk)s/proj/c.txt '
1857 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1858 ('/%(trunk)s/proj/d.txt '
1859 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1860 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1861 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1862 'A'),
1863 ('/%(trunk)s/proj/e.txt '
1864 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1867 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1868 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1869 ('/%(tags)s/vtag-4/proj/d.txt '
1870 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1874 def default_branches_trunk_only():
1875 "handle default branches with --trunk-only"
1877 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1879 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1880 ('/%(trunk)s/proj', 'A'),
1881 ('/%(trunk)s/proj/a.txt', 'A'),
1882 ('/%(trunk)s/proj/b.txt', 'A'),
1883 ('/%(trunk)s/proj/c.txt', 'A'),
1884 ('/%(trunk)s/proj/d.txt', 'A'),
1885 ('/%(trunk)s/proj/e.txt', 'A'),
1886 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1889 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1890 ('/%(trunk)s/proj/a.txt', 'M'),
1891 ('/%(trunk)s/proj/b.txt', 'M'),
1892 ('/%(trunk)s/proj/c.txt', 'M'),
1893 ('/%(trunk)s/proj/d.txt', 'M'),
1894 ('/%(trunk)s/proj/e.txt', 'M'),
1895 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1898 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1899 ('/%(trunk)s/proj/a.txt', 'M'),
1900 ('/%(trunk)s/proj/b.txt', 'M'),
1901 ('/%(trunk)s/proj/c.txt', 'M'),
1902 ('/%(trunk)s/proj/d.txt', 'M'),
1903 ('/%(trunk)s/proj/e.txt', 'M'),
1904 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1907 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1908 ('/%(trunk)s/proj/a.txt', 'M'),
1911 conv.logs[6].check("Add a file to the working copy.", (
1912 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1915 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1916 ('/%(trunk)s/proj/b.txt', 'M'),
1917 ('/%(trunk)s/proj/c.txt', 'M'),
1918 ('/%(trunk)s/proj/d.txt', 'M'),
1919 ('/%(trunk)s/proj/e.txt', 'M'),
1920 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1924 def default_branch_and_1_2():
1925 "do not allow 1.2 revision with default branch"
1927 conv = ensure_conversion(
1928 'default-branch-and-1-2',
1929 error_re=(
1930 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
1935 def compose_tag_three_sources():
1936 "compose a tag from three sources"
1937 conv = ensure_conversion('compose-tag-three-sources')
1939 conv.logs[2].check("Add on trunk", (
1940 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
1941 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
1942 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
1943 ('/%(trunk)s/tagged-on-b1', 'A'),
1944 ('/%(trunk)s/tagged-on-b2', 'A'),
1947 conv.logs[3].check(sym_log_msg('b1'), (
1948 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
1951 conv.logs[4].check(sym_log_msg('b2'), (
1952 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
1955 conv.logs[5].check("Commit on branch b1", (
1956 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
1957 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
1958 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
1959 ('/%(branches)s/b1/tagged-on-b1', 'M'),
1960 ('/%(branches)s/b1/tagged-on-b2', 'M'),
1963 conv.logs[6].check("Commit on branch b2", (
1964 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
1965 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
1966 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
1967 ('/%(branches)s/b2/tagged-on-b1', 'M'),
1968 ('/%(branches)s/b2/tagged-on-b2', 'M'),
1971 conv.logs[7].check("Commit again on trunk", (
1972 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
1973 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
1974 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
1975 ('/%(trunk)s/tagged-on-b1', 'M'),
1976 ('/%(trunk)s/tagged-on-b2', 'M'),
1979 conv.logs[8].check(sym_log_msg('T',1), (
1980 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
1981 ('/%(tags)s/T/tagged-on-trunk-1.1 '
1982 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
1983 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
1984 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
1988 def pass5_when_to_fill():
1989 "reserve a svn revnum for a fill only when required"
1990 # The conversion will fail if the bug is present, and
1991 # ensure_conversion would raise Failure.
1992 conv = ensure_conversion('pass5-when-to-fill')
1995 class EmptyTrunk(Cvs2SvnTestCase):
1996 "don't break when the trunk is empty"
1998 def __init__(self, **kw):
1999 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
2001 def run(self):
2002 # The conversion will fail if the bug is present, and
2003 # ensure_conversion would raise Failure.
2004 conv = self.ensure_conversion()
2007 def no_spurious_svn_commits():
2008 "ensure that we don't create any spurious commits"
2009 conv = ensure_conversion('phoenix')
2011 # Check spurious commit that could be created in
2012 # SVNCommitCreator._pre_commit()
2014 # (When you add a file on a branch, CVS creates a trunk revision
2015 # in state 'dead'. If the log message of that commit is equal to
2016 # the one that CVS generates, we do not ever create a 'fill'
2017 # SVNCommit for it.)
2019 # and spurious commit that could be created in
2020 # SVNCommitCreator._commit()
2022 # (When you add a file on a branch, CVS creates a trunk revision
2023 # in state 'dead'. If the log message of that commit is equal to
2024 # the one that CVS generates, we do not create a primary SVNCommit
2025 # for it.)
2026 conv.logs[17].check('File added on branch xiphophorus', (
2027 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2030 # Check to make sure that a commit *is* generated:
2031 # (When you add a file on a branch, CVS creates a trunk revision
2032 # in state 'dead'. If the log message of that commit is NOT equal
2033 # to the one that CVS generates, we create a primary SVNCommit to
2034 # serve as a home for the log message in question.
2035 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2036 + 'branch xiphophorus,\nand this log message was tweaked', ())
2038 # Check spurious commit that could be created in
2039 # SVNCommitCreator._commit_symbols().
2040 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2041 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2045 class PeerPathPruning(Cvs2SvnTestCase):
2046 "make sure that filling prunes paths correctly"
2048 def __init__(self, **kw):
2049 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2051 def run(self):
2052 conv = self.ensure_conversion()
2053 conv.logs[6].check(sym_log_msg('BRANCH'), (
2054 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2055 ('/%(branches)s/BRANCH/bar', 'D'),
2056 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2060 def invalid_closings_on_trunk():
2061 "verify correct revs are copied to default branches"
2062 # The conversion will fail if the bug is present, and
2063 # ensure_conversion would raise Failure.
2064 conv = ensure_conversion('invalid-closings-on-trunk')
2067 def individual_passes():
2068 "run each pass individually"
2069 conv = ensure_conversion('main')
2070 conv2 = ensure_conversion('main', passbypass=1)
2072 if conv.logs != conv2.logs:
2073 raise Failure()
2076 def resync_bug():
2077 "reveal a big bug in our resync algorithm"
2078 # This will fail if the bug is present
2079 conv = ensure_conversion('resync-bug')
2082 def branch_from_default_branch():
2083 "reveal a bug in our default branch detection code"
2084 conv = ensure_conversion('branch-from-default-branch')
2086 # This revision will be a default branch synchronization only
2087 # if cvs2svn is correctly determining default branch revisions.
2089 # The bug was that cvs2svn was treating revisions on branches off of
2090 # default branches as default branch revisions, resulting in
2091 # incorrectly regarding the branch off of the default branch as a
2092 # non-trunk default branch. Crystal clear? I thought so. See
2093 # issue #42 for more incoherent blathering.
2094 conv.logs[5].check("This commit was generated by cvs2svn", (
2095 ('/%(trunk)s/proj/file.txt '
2096 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2100 def file_in_attic_too():
2101 "die if a file exists in and out of the attic"
2102 ensure_conversion(
2103 'file-in-attic-too',
2104 error_re=(
2105 r'.*A CVS repository cannot contain both '
2106 r'(.*)' + re.escape(os.sep) + r'(.*) '
2107 + r'and '
2108 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2113 def retain_file_in_attic_too():
2114 "test --retain-conflicting-attic-files option"
2115 conv = ensure_conversion(
2116 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2117 if not conv.path_exists('trunk', 'file.txt'):
2118 raise Failure()
2119 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2120 raise Failure()
2123 def symbolic_name_filling_guide():
2124 "reveal a big bug in our SymbolFillingGuide"
2125 # This will fail if the bug is present
2126 conv = ensure_conversion('symbolic-name-overfill')
2129 # Helpers for tests involving file contents and properties.
2131 class NodeTreeWalkException:
2132 "Exception class for node tree traversals."
2133 pass
2135 def node_for_path(node, path):
2136 "In the tree rooted under SVNTree NODE, return the node at PATH."
2137 if node.name != '__SVN_ROOT_NODE':
2138 raise NodeTreeWalkException()
2139 path = path.strip('/')
2140 components = path.split('/')
2141 for component in components:
2142 node = get_child(node, component)
2143 return node
2145 # Helper for tests involving properties.
2146 def props_for_path(node, path):
2147 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2148 return node_for_path(node, path).props
2151 class EOLMime(Cvs2SvnPropertiesTestCase):
2152 """eol settings and mime types together
2154 The files are as follows:
2156 trunk/foo.txt: no -kb, mime file says nothing.
2157 trunk/foo.xml: no -kb, mime file says text.
2158 trunk/foo.zip: no -kb, mime file says non-text.
2159 trunk/foo.bin: has -kb, mime file says nothing.
2160 trunk/foo.csv: has -kb, mime file says text.
2161 trunk/foo.dbf: has -kb, mime file says non-text.
2164 def __init__(self, args, **kw):
2165 # TODO: It's a bit klugey to construct this path here. But so far
2166 # there's only one test with a mime.types file. If we have more,
2167 # we should abstract this into some helper, which would be located
2168 # near ensure_conversion(). Note that it is a convention of this
2169 # test suite for a mime.types file to be located in the top level
2170 # of the CVS repository to which it applies.
2171 self.mime_path = os.path.join(
2172 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2174 Cvs2SvnPropertiesTestCase.__init__(
2175 self, 'eol-mime',
2176 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2177 args=['--mime-types=%s' % self.mime_path] + args,
2178 **kw)
2181 # We do four conversions. Each time, we pass --mime-types=FILE with
2182 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2183 # Thus there's one conversion with neither flag, one with just the
2184 # former, one with just the latter, and one with both.
2187 # Neither --no-default-eol nor --eol-from-mime-type:
2188 eol_mime1 = EOLMime(
2189 variant=1,
2190 args=[],
2191 expected_props=[
2192 ('trunk/foo.txt', [None, None, None]),
2193 ('trunk/foo.xml', [None, 'text/xml', None]),
2194 ('trunk/foo.zip', [None, 'application/zip', None]),
2195 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2196 ('trunk/foo.csv', [None, 'text/csv', None]),
2197 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2201 # Just --no-default-eol, not --eol-from-mime-type:
2202 eol_mime2 = EOLMime(
2203 variant=2,
2204 args=['--default-eol=native'],
2205 expected_props=[
2206 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2207 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2208 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2209 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2210 ('trunk/foo.csv', [None, 'text/csv', None]),
2211 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2215 # Just --eol-from-mime-type, not --no-default-eol:
2216 eol_mime3 = EOLMime(
2217 variant=3,
2218 args=['--eol-from-mime-type'],
2219 expected_props=[
2220 ('trunk/foo.txt', [None, None, None]),
2221 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2222 ('trunk/foo.zip', [None, 'application/zip', None]),
2223 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2224 ('trunk/foo.csv', [None, 'text/csv', None]),
2225 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2229 # Both --no-default-eol and --eol-from-mime-type:
2230 eol_mime4 = EOLMime(
2231 variant=4,
2232 args=['--eol-from-mime-type', '--default-eol=native'],
2233 expected_props=[
2234 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2235 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2236 ('trunk/foo.zip', [None, 'application/zip', None]),
2237 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2238 ('trunk/foo.csv', [None, 'text/csv', None]),
2239 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2243 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2244 'eol-mime',
2245 description='test non-setting of cvs2svn:cvs-rev property',
2246 args=[],
2247 props_to_test=['cvs2svn:cvs-rev'],
2248 expected_props=[
2249 ('trunk/foo.txt', [None]),
2250 ('trunk/foo.xml', [None]),
2251 ('trunk/foo.zip', [None]),
2252 ('trunk/foo.bin', [None]),
2253 ('trunk/foo.csv', [None]),
2254 ('trunk/foo.dbf', [None]),
2258 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2259 'eol-mime',
2260 description='test setting of cvs2svn:cvs-rev property',
2261 args=['--cvs-revnums'],
2262 props_to_test=['cvs2svn:cvs-rev'],
2263 expected_props=[
2264 ('trunk/foo.txt', ['1.2']),
2265 ('trunk/foo.xml', ['1.2']),
2266 ('trunk/foo.zip', ['1.2']),
2267 ('trunk/foo.bin', ['1.2']),
2268 ('trunk/foo.csv', ['1.2']),
2269 ('trunk/foo.dbf', ['1.2']),
2273 keywords = Cvs2SvnPropertiesTestCase(
2274 'keywords',
2275 description='test setting of svn:keywords property among others',
2276 args=['--default-eol=native'],
2277 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2278 expected_props=[
2279 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2280 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2281 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2282 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2283 ('trunk/foo.kk', [None, 'native', None]),
2284 ('trunk/foo.ko', [None, 'native', None]),
2285 ('trunk/foo.kv', [None, 'native', None]),
2289 def ignore():
2290 "test setting of svn:ignore property"
2291 conv = ensure_conversion('cvsignore')
2292 wc_tree = conv.get_wc_tree()
2293 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2294 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2296 if topdir_props['svn:ignore'] != \
2297 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2298 raise Failure()
2300 if subdir_props['svn:ignore'] != \
2301 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2302 raise Failure()
2305 def requires_cvs():
2306 "test that CVS can still do what RCS can't"
2307 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2308 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2310 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2311 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2313 if atsign_contents[-1:] == "@":
2314 raise Failure()
2315 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2316 raise Failure()
2318 if not (conv.logs[21].author == "William Lyon Phelps III" and
2319 conv.logs[20].author == "j random"):
2320 raise Failure()
2323 def questionable_branch_names():
2324 "test that we can handle weird branch names"
2325 conv = ensure_conversion('questionable-symbols')
2326 # If the conversion succeeds, then we're okay. We could check the
2327 # actual branch paths, too, but the main thing is to know that the
2328 # conversion doesn't fail.
2331 def questionable_tag_names():
2332 "test that we can handle weird tag names"
2333 conv = ensure_conversion('questionable-symbols')
2334 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2335 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2337 conv.find_tag_log('TagWith/Backslash_E').check(
2338 sym_log_msg('TagWith/Backslash_E',1),
2340 ('/%(tags)s/TagWith', 'A'),
2341 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2344 conv.find_tag_log('TagWith/Slash_Z').check(
2345 sym_log_msg('TagWith/Slash_Z',1),
2347 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2352 def revision_reorder_bug():
2353 "reveal a bug that reorders file revisions"
2354 conv = ensure_conversion('revision-reorder-bug')
2355 # If the conversion succeeds, then we're okay. We could check the
2356 # actual revisions, too, but the main thing is to know that the
2357 # conversion doesn't fail.
2360 def exclude():
2361 "test that exclude really excludes everything"
2362 conv = ensure_conversion('main', args=['--exclude=.*'])
2363 for log in conv.logs.values():
2364 for item in log.changed_paths.keys():
2365 if item.startswith('/branches/') or item.startswith('/tags/'):
2366 raise Failure()
2369 def vendor_branch_delete_add():
2370 "add trunk file that was deleted on vendor branch"
2371 # This will error if the bug is present
2372 conv = ensure_conversion('vendor-branch-delete-add')
2375 def resync_pass2_pull_forward():
2376 "ensure pass2 doesn't pull rev too far forward"
2377 conv = ensure_conversion('resync-pass2-pull-forward')
2378 # If the conversion succeeds, then we're okay. We could check the
2379 # actual revisions, too, but the main thing is to know that the
2380 # conversion doesn't fail.
2383 def native_eol():
2384 "only LFs for svn:eol-style=native files"
2385 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2386 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2387 conv.repos)
2388 # Verify that all files in the dump have LF EOLs. We're actually
2389 # testing the whole dump file, but the dump file itself only uses
2390 # LF EOLs, so we're safe.
2391 for line in lines:
2392 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2393 raise Failure()
2396 def double_fill():
2397 "reveal a bug that created a branch twice"
2398 conv = ensure_conversion('double-fill')
2399 # If the conversion succeeds, then we're okay. We could check the
2400 # actual revisions, too, but the main thing is to know that the
2401 # conversion doesn't fail.
2404 def double_fill2():
2405 "reveal a second bug that created a branch twice"
2406 conv = ensure_conversion('double-fill2')
2407 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2408 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2409 try:
2410 # This check should fail:
2411 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2412 except Failure:
2413 pass
2414 else:
2415 raise Failure('Symbol filled twice in a row')
2418 def resync_pass2_push_backward():
2419 "ensure pass2 doesn't push rev too far backward"
2420 conv = ensure_conversion('resync-pass2-push-backward')
2421 # If the conversion succeeds, then we're okay. We could check the
2422 # actual revisions, too, but the main thing is to know that the
2423 # conversion doesn't fail.
2426 def double_add():
2427 "reveal a bug that added a branch file twice"
2428 conv = ensure_conversion('double-add')
2429 # If the conversion succeeds, then we're okay. We could check the
2430 # actual revisions, too, but the main thing is to know that the
2431 # conversion doesn't fail.
2434 def bogus_branch_copy():
2435 "reveal a bug that copies a branch file wrongly"
2436 conv = ensure_conversion('bogus-branch-copy')
2437 # If the conversion succeeds, then we're okay. We could check the
2438 # actual revisions, too, but the main thing is to know that the
2439 # conversion doesn't fail.
2442 def nested_ttb_directories():
2443 "require error if ttb directories are not disjoint"
2444 opts_list = [
2445 {'trunk' : 'a', 'branches' : 'a',},
2446 {'trunk' : 'a', 'tags' : 'a',},
2447 {'branches' : 'a', 'tags' : 'a',},
2448 # This option conflicts with the default trunk path:
2449 {'branches' : 'trunk',},
2450 # Try some nested directories:
2451 {'trunk' : 'a', 'branches' : 'a/b',},
2452 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2453 {'branches' : 'a', 'tags' : 'a/b',},
2456 for opts in opts_list:
2457 ensure_conversion(
2458 'main', error_re=r'The following paths are not disjoint\:', **opts
2462 class AutoProps(Cvs2SvnPropertiesTestCase):
2463 """Test auto-props.
2465 The files are as follows:
2467 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2468 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2469 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2470 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2471 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2472 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2473 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2474 trunk/foo.UPCASE1: no -kb, no mime type.
2475 trunk/foo.UPCASE2: no -kb, no mime type.
2478 def __init__(self, args, **kw):
2479 ### TODO: It's a bit klugey to construct this path here. See also
2480 ### the comment in eol_mime().
2481 auto_props_path = os.path.join(
2482 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2484 Cvs2SvnPropertiesTestCase.__init__(
2485 self, 'eol-mime',
2486 props_to_test=[
2487 'myprop',
2488 'svn:eol-style',
2489 'svn:mime-type',
2490 'svn:keywords',
2491 'svn:executable',
2493 args=[
2494 '--auto-props=%s' % auto_props_path,
2495 '--eol-from-mime-type'
2496 ] + args,
2497 **kw)
2500 auto_props_ignore_case = AutoProps(
2501 description="test auto-props",
2502 args=['--default-eol=native'],
2503 expected_props=[
2504 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2505 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2506 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2507 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2508 ('trunk/foo.bin',
2509 ['bin', None, 'application/octet-stream', None, '']),
2510 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2511 ('trunk/foo.dbf',
2512 ['dbf', None, 'application/what-is-dbf', None, None]),
2513 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2514 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2518 def ctrl_char_in_filename():
2519 "do not allow control characters in filenames"
2521 try:
2522 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2523 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2524 if os.path.exists(dstrepos_path):
2525 safe_rmtree(dstrepos_path)
2527 # create repos from existing main repos
2528 shutil.copytree(srcrepos_path, dstrepos_path)
2529 base_path = os.path.join(dstrepos_path, 'single-files')
2530 try:
2531 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2532 os.path.join(base_path, 'two\rquick,v'))
2533 except:
2534 # Operating systems that don't allow control characters in
2535 # filenames will hopefully have thrown an exception; in that
2536 # case, just skip this test.
2537 raise svntest.Skip()
2539 conv = ensure_conversion(
2540 'ctrl-char-filename',
2541 error_re=(r'.*Character .* in filename .* '
2542 r'is not supported by Subversion\.'),
2544 finally:
2545 safe_rmtree(dstrepos_path)
2548 def commit_dependencies():
2549 "interleaved and multi-branch commits to same files"
2550 conv = ensure_conversion("commit-dependencies")
2551 conv.logs[2].check('adding', (
2552 ('/%(trunk)s/interleaved', 'A'),
2553 ('/%(trunk)s/interleaved/file1', 'A'),
2554 ('/%(trunk)s/interleaved/file2', 'A'),
2556 conv.logs[3].check('big commit', (
2557 ('/%(trunk)s/interleaved/file1', 'M'),
2558 ('/%(trunk)s/interleaved/file2', 'M'),
2560 conv.logs[4].check('dependant small commit', (
2561 ('/%(trunk)s/interleaved/file1', 'M'),
2563 conv.logs[5].check('adding', (
2564 ('/%(trunk)s/multi-branch', 'A'),
2565 ('/%(trunk)s/multi-branch/file1', 'A'),
2566 ('/%(trunk)s/multi-branch/file2', 'A'),
2568 conv.logs[6].check(sym_log_msg("branch"), (
2569 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2570 ('/%(branches)s/branch/interleaved', 'D'),
2572 conv.logs[7].check('multi-branch-commit', (
2573 ('/%(trunk)s/multi-branch/file1', 'M'),
2574 ('/%(trunk)s/multi-branch/file2', 'M'),
2575 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2576 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2580 def double_branch_delete():
2581 "fill branches before modifying files on them"
2582 conv = ensure_conversion('double-branch-delete')
2584 # Test for issue #102. The file IMarshalledValue.java is branched,
2585 # deleted, readded on the branch, and then deleted again. If the
2586 # fill for the file on the branch is postponed until after the
2587 # modification, the file will end up live on the branch instead of
2588 # dead! Make sure it happens at the right time.
2590 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2591 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2594 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2595 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2599 def symbol_mismatches():
2600 "error for conflicting tag/branch"
2602 ensure_conversion(
2603 'symbol-mess',
2604 args=['--symbol-default=strict'],
2605 error_re=r'.*Problems determining how symbols should be converted',
2609 def overlook_symbol_mismatches():
2610 "overlook conflicting tag/branch when --trunk-only"
2612 # This is a test for issue #85.
2614 ensure_conversion('symbol-mess', args=['--trunk-only'])
2617 def force_symbols():
2618 "force symbols to be tags/branches"
2620 conv = ensure_conversion(
2621 'symbol-mess',
2622 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2623 if conv.path_exists('tags', 'BRANCH') \
2624 or not conv.path_exists('branches', 'BRANCH'):
2625 raise Failure()
2626 if not conv.path_exists('tags', 'TAG') \
2627 or conv.path_exists('branches', 'TAG'):
2628 raise Failure()
2629 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2630 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2631 raise Failure()
2632 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2633 or conv.path_exists('branches', 'MOSTLY_TAG'):
2634 raise Failure()
2637 def commit_blocks_tags():
2638 "commit prevents forced tag"
2640 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2641 ensure_conversion(
2642 'symbol-mess',
2643 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2644 error_re=(
2645 r'.*The following branches cannot be forced to be tags '
2646 r'because they have commits'
2651 def blocked_excludes():
2652 "error for blocked excludes"
2654 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2655 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2656 try:
2657 ensure_conversion(
2658 'symbol-mess',
2659 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2660 raise MissingErrorException()
2661 except Failure:
2662 pass
2665 def unblock_blocked_excludes():
2666 "excluding blocker removes blockage"
2668 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2669 for blocker in ['BRANCH', 'COMMIT']:
2670 ensure_conversion(
2671 'symbol-mess',
2672 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2673 '--exclude=BLOCKING_%s' % blocker]))
2676 def regexp_force_symbols():
2677 "force symbols via regular expressions"
2679 conv = ensure_conversion(
2680 'symbol-mess',
2681 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2682 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2683 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2684 raise Failure()
2685 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2686 or conv.path_exists('branches', 'MOSTLY_TAG'):
2687 raise Failure()
2690 def heuristic_symbol_default():
2691 "test 'heuristic' symbol default"
2693 conv = ensure_conversion(
2694 'symbol-mess', args=['--symbol-default=heuristic'])
2695 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2696 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2697 raise Failure()
2698 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2699 or conv.path_exists('branches', 'MOSTLY_TAG'):
2700 raise Failure()
2703 def branch_symbol_default():
2704 "test 'branch' symbol default"
2706 conv = ensure_conversion(
2707 'symbol-mess', args=['--symbol-default=branch'])
2708 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2709 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2710 raise Failure()
2711 if conv.path_exists('tags', 'MOSTLY_TAG') \
2712 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2713 raise Failure()
2716 def tag_symbol_default():
2717 "test 'tag' symbol default"
2719 conv = ensure_conversion(
2720 'symbol-mess', args=['--symbol-default=tag'])
2721 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2722 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2723 raise Failure()
2724 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2725 or conv.path_exists('branches', 'MOSTLY_TAG'):
2726 raise Failure()
2729 def symbol_transform():
2730 "test --symbol-transform"
2732 conv = ensure_conversion(
2733 'symbol-mess',
2734 args=[
2735 '--symbol-default=heuristic',
2736 '--symbol-transform=BRANCH:branch',
2737 '--symbol-transform=TAG:tag',
2738 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2740 if not conv.path_exists('branches', 'branch'):
2741 raise Failure()
2742 if not conv.path_exists('tags', 'tag'):
2743 raise Failure()
2744 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2745 raise Failure()
2746 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2747 raise Failure()
2750 def write_symbol_info():
2751 "test --write-symbol-info"
2753 expected_lines = [
2754 ['0', '.trunk.',
2755 'trunk', 'trunk', '.'],
2756 ['0', 'BLOCKED_BY_UNNAMED',
2757 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2758 ['0', 'BLOCKING_COMMIT',
2759 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2760 ['0', 'BLOCKED_BY_COMMIT',
2761 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2762 ['0', 'BLOCKING_BRANCH',
2763 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2764 ['0', 'BLOCKED_BY_BRANCH',
2765 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2766 ['0', 'MOSTLY_BRANCH',
2767 '.', '.', '.'],
2768 ['0', 'MOSTLY_TAG',
2769 '.', '.', '.'],
2770 ['0', 'BRANCH_WITH_COMMIT',
2771 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2772 ['0', 'BRANCH',
2773 'branch', 'branches/BRANCH', '.trunk.'],
2774 ['0', 'TAG',
2775 'tag', 'tags/TAG', '.trunk.'],
2776 ['0', 'unlabeled-1.1.12.1.2',
2777 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2779 expected_lines.sort()
2781 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2782 try:
2783 ensure_conversion(
2784 'symbol-mess',
2785 args=[
2786 '--symbol-default=strict',
2787 '--write-symbol-info=%s' % (symbol_info_file,),
2788 '--passes=:CollateSymbolsPass',
2791 raise MissingErrorException()
2792 except Failure:
2793 pass
2794 lines = []
2795 comment_re = re.compile(r'^\s*\#')
2796 for l in open(symbol_info_file, 'r'):
2797 if comment_re.match(l):
2798 continue
2799 lines.append(l.strip().split())
2800 lines.sort()
2801 if lines != expected_lines:
2802 s = ['Symbol info incorrect\n']
2803 differ = Differ()
2804 for diffline in differ.compare(
2805 [' '.join(line) + '\n' for line in expected_lines],
2806 [' '.join(line) + '\n' for line in lines],
2808 s.append(diffline)
2809 raise Failure(''.join(s))
2812 def symbol_hints():
2813 "test --symbol-hints for setting branch/tag"
2815 conv = ensure_conversion(
2816 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2818 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2819 raise Failure()
2820 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2821 raise Failure()
2822 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2823 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2825 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2826 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2828 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2829 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2833 def parent_hints():
2834 "test --symbol-hints for setting parent"
2836 conv = ensure_conversion(
2837 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2839 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2840 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2844 def parent_hints_invalid():
2845 "test --symbol-hints with an invalid parent"
2847 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2848 # this symbol hints file sets the preferred parent to BRANCH
2849 # instead:
2850 conv = ensure_conversion(
2851 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2852 error_re=(
2853 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2858 def parent_hints_wildcards():
2859 "test --symbol-hints wildcards"
2861 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2862 # this symbol hints file sets the preferred parent to BRANCH
2863 # instead:
2864 conv = ensure_conversion(
2865 'symbol-mess',
2866 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2868 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2869 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2873 def path_hints():
2874 "test --symbol-hints for setting svn paths"
2876 conv = ensure_conversion(
2877 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
2879 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2880 ('/trunk', 'A'),
2881 ('/a', 'A'),
2882 ('/a/strange', 'A'),
2883 ('/a/strange/trunk', 'A'),
2884 ('/a/strange/trunk/path', 'A'),
2885 ('/branches', 'A'),
2886 ('/tags', 'A'),
2888 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2889 ('/special', 'A'),
2890 ('/special/tag', 'A'),
2891 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
2893 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2894 ('/special/other', 'A'),
2895 ('/special/other/branch', 'A'),
2896 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
2898 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2899 ('/special/branch', 'A'),
2900 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
2904 def issue_99():
2905 "test problem from issue 99"
2907 conv = ensure_conversion('issue-99')
2910 def issue_100():
2911 "test problem from issue 100"
2913 conv = ensure_conversion('issue-100')
2914 file1 = conv.get_wc('trunk', 'file1.txt')
2915 if file(file1).read() != 'file1.txt<1.2>\n':
2916 raise Failure()
2919 def issue_106():
2920 "test problem from issue 106"
2922 conv = ensure_conversion('issue-106')
2925 def options_option():
2926 "use of the --options option"
2928 conv = ensure_conversion('main', options_file='cvs2svn.options')
2931 def multiproject():
2932 "multiproject conversion"
2934 conv = ensure_conversion(
2935 'main', options_file='cvs2svn-multiproject.options'
2937 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2938 ('/partial-prune', 'A'),
2939 ('/partial-prune/trunk', 'A'),
2940 ('/partial-prune/branches', 'A'),
2941 ('/partial-prune/tags', 'A'),
2942 ('/partial-prune/releases', 'A'),
2946 def crossproject():
2947 "multiproject conversion with cross-project commits"
2949 conv = ensure_conversion(
2950 'main', options_file='cvs2svn-crossproject.options'
2954 def tag_with_no_revision():
2955 "tag defined but revision is deleted"
2957 conv = ensure_conversion('tag-with-no-revision')
2960 def delete_cvsignore():
2961 "svn:ignore should vanish when .cvsignore does"
2963 # This is issue #81.
2965 conv = ensure_conversion('delete-cvsignore')
2967 wc_tree = conv.get_wc_tree()
2968 props = props_for_path(wc_tree, 'trunk/proj')
2970 if props.has_key('svn:ignore'):
2971 raise Failure()
2974 def repeated_deltatext():
2975 "ignore repeated deltatext blocks with warning"
2977 conv = ensure_conversion('repeated-deltatext')
2978 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
2979 if not conv.output_found(warning_re):
2980 raise Failure()
2983 def nasty_graphs():
2984 "process some nasty dependency graphs"
2986 # It's not how well the bear can dance, but that the bear can dance
2987 # at all:
2988 conv = ensure_conversion('nasty-graphs')
2991 def tagging_after_delete():
2992 "optimal tag after deleting files"
2994 conv = ensure_conversion('tagging-after-delete')
2996 # tag should be 'clean', no deletes
2997 log = conv.find_tag_log('tag1')
2998 expected = (
2999 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
3001 log.check_changes(expected)
3004 def crossed_branches():
3005 "branches created in inconsistent orders"
3007 conv = ensure_conversion('crossed-branches')
3010 def file_directory_conflict():
3011 "error when filename conflicts with directory name"
3013 conv = ensure_conversion(
3014 'file-directory-conflict',
3015 error_re=r'.*Directory name conflicts with filename',
3019 def attic_directory_conflict():
3020 "error when attic filename conflicts with dirname"
3022 # This tests the problem reported in issue #105.
3024 conv = ensure_conversion(
3025 'attic-directory-conflict',
3026 error_re=r'.*Directory name conflicts with filename',
3030 def internal_co():
3031 "verify that --use-internal-co works"
3033 rcs_conv = ensure_conversion(
3034 'main', args=['--use-rcs', '--default-eol=native'],
3036 conv = ensure_conversion(
3037 'main', args=['--default-eol=native'],
3039 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3040 raise Failure()
3041 rcs_lines = run_program(
3042 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3043 rcs_conv.repos)
3044 lines = run_program(
3045 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3046 conv.repos)
3047 # Compare all lines following the repository UUID:
3048 if lines[3:] != rcs_lines[3:]:
3049 raise Failure()
3052 def internal_co_exclude():
3053 "verify that --use-internal-co --exclude=... works"
3055 rcs_conv = ensure_conversion(
3056 'internal-co',
3057 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3059 conv = ensure_conversion(
3060 'internal-co',
3061 args=['--exclude=BRANCH', '--default-eol=native'],
3063 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3064 raise Failure()
3065 rcs_lines = run_program(
3066 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3067 rcs_conv.repos)
3068 lines = run_program(
3069 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3070 conv.repos)
3071 # Compare all lines following the repository UUID:
3072 if lines[3:] != rcs_lines[3:]:
3073 raise Failure()
3076 def internal_co_trunk_only():
3077 "verify that --use-internal-co --trunk-only works"
3079 conv = ensure_conversion(
3080 'internal-co',
3081 args=['--trunk-only', '--default-eol=native'],
3083 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3084 raise Failure()
3087 def leftover_revs():
3088 "check for leftover checked-out revisions"
3090 conv = ensure_conversion(
3091 'leftover-revs',
3092 args=['--exclude=BRANCH', '--default-eol=native'],
3094 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3095 raise Failure()
3098 def requires_internal_co():
3099 "test that internal co can do more than RCS"
3100 # See issues 4, 11 for the bugs whose regression we're testing for.
3101 # Unlike in requires_cvs above, issue 29 is not covered.
3102 conv = ensure_conversion('requires-cvs')
3104 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3106 if atsign_contents[-1:] == "@":
3107 raise Failure()
3109 if not (conv.logs[21].author == "William Lyon Phelps III" and
3110 conv.logs[20].author == "j random"):
3111 raise Failure()
3114 def internal_co_keywords():
3115 "test that internal co handles keywords correctly"
3116 conv_ic = ensure_conversion('internal-co-keywords',
3117 args=["--keywords-off"])
3118 conv_cvs = ensure_conversion('internal-co-keywords',
3119 args=["--use-cvs", "--keywords-off"])
3121 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3122 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3123 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3124 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3125 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3126 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3128 if ko_ic != ko_cvs:
3129 raise Failure()
3130 if kk_ic != kk_cvs:
3131 raise Failure()
3133 # The date format changed between cvs and co ('/' instead of '-').
3134 # Accept either one:
3135 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3136 if kv_ic != kv_cvs \
3137 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3138 raise Failure()
3141 def timestamp_chaos():
3142 "test timestamp adjustments"
3144 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3146 # The times are expressed here in UTC:
3147 times = [
3148 '2007-01-01 21:00:00', # Initial commit
3149 '2007-01-01 21:00:00', # revision 1.1 of both files
3150 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3151 '2007-01-01 21:00:02', # revision 1.2 of file2.txt, adjusted backwards
3152 '2007-01-01 22:00:00', # revision 1.3 of both files
3155 # Convert the times to seconds since the epoch, in UTC:
3156 times = [calendar.timegm(svn_strptime(t)) for t in times]
3158 for i in range(len(times)):
3159 if abs(conv.logs[i + 1].date - times[i]) > 0.1:
3160 raise Failure()
3163 def symlinks():
3164 "convert a repository that contains symlinks"
3166 # This is a test for issue #97.
3168 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3169 links = [
3171 os.path.join('..', 'file.txt,v'),
3172 os.path.join(proj, 'dir1', 'file.txt,v'),
3175 'dir1',
3176 os.path.join(proj, 'dir2'),
3180 try:
3181 os.symlink
3182 except AttributeError:
3183 # Apparently this OS doesn't support symlinks, so skip test.
3184 raise svntest.Skip()
3186 try:
3187 for (src,dst) in links:
3188 os.symlink(src, dst)
3190 conv = ensure_conversion('symlinks')
3191 conv.logs[2].check('', (
3192 ('/%(trunk)s/proj', 'A'),
3193 ('/%(trunk)s/proj/file.txt', 'A'),
3194 ('/%(trunk)s/proj/dir1', 'A'),
3195 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3196 ('/%(trunk)s/proj/dir2', 'A'),
3197 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3199 finally:
3200 for (src,dst) in links:
3201 os.remove(dst)
3204 def empty_trunk_path():
3205 "allow --trunk to be empty if --trunk-only"
3207 # This is a test for issue #53.
3209 conv = ensure_conversion(
3210 'main', args=['--trunk-only', '--trunk='],
3214 def preferred_parent_cycle():
3215 "handle a cycle in branch parent preferences"
3217 conv = ensure_conversion('preferred-parent-cycle')
3220 def branch_from_empty_dir():
3221 "branch from an empty directory"
3223 conv = ensure_conversion('branch-from-empty-dir')
3226 def trunk_readd():
3227 "add a file on a branch then on trunk"
3229 conv = ensure_conversion('trunk-readd')
3232 def branch_from_deleted_1_1():
3233 "branch from a 1.1 revision that will be deleted"
3235 conv = ensure_conversion('branch-from-deleted-1-1')
3236 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3237 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3239 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3240 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3242 conv.logs[7].check('Adding b.txt:1.2', (
3243 ('/%(trunk)s/proj/b.txt', 'A'),
3246 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3247 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3249 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3250 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3254 def add_on_branch():
3255 "add a file on a branch using newer CVS"
3257 conv = ensure_conversion('add-on-branch')
3258 conv.logs[6].check('Adding b.txt:1.1', (
3259 ('/%(trunk)s/proj/b.txt', 'A'),
3261 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3262 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3264 conv.logs[8].check('Adding c.txt:1.1', (
3265 ('/%(trunk)s/proj/c.txt', 'A'),
3267 conv.logs[9].check('Removing c.txt:1.2', (
3268 ('/%(trunk)s/proj/c.txt', 'D'),
3270 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3271 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3273 conv.logs[11].check('Adding d.txt:1.1', (
3274 ('/%(trunk)s/proj/d.txt', 'A'),
3276 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3277 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3281 def main_git():
3282 "test output in git-fast-import format"
3284 # Note: To test importing into git, do
3286 # ./run-tests <test-number>
3287 # rm -rf .git
3288 # git-init
3289 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3291 # Or, to load the dumpfiles separately:
3293 # cat cvs2svn-tmp/git-blob.dat \
3294 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3295 # cat cvs2svn-tmp/git-dump.dat \
3296 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3298 # Then use "gitk --all", "git log", etc. to test the contents of the
3299 # repository.
3301 # We don't have the infrastructure to check that the resulting git
3302 # repository is correct, so we just check that the conversion runs
3303 # to completion:
3304 conv = GitConversion('main', None, [
3305 '--blobfile=cvs2svn-tmp/blobfile.out',
3306 '--dumpfile=cvs2svn-tmp/dumpfile.out',
3307 '--username=cvs2git',
3308 'test-data/main-cvsrepos',
3312 def git_options():
3313 "test cvs2git using options file"
3315 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3318 def main_hg():
3319 "output in git-fast-import format with inline data"
3321 # The output should be suitable for import by Mercurial.
3323 # Note: To test importing into git, do
3325 # ./run-tests <test-number>
3326 # rm -rf .git
3327 # git-init
3328 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3330 # Then use "gitk --all", "git log", etc. to test the contents of the
3331 # repository.
3333 # We don't have the infrastructure to check that the resulting
3334 # Mercurial repository is correct, so we just check that the
3335 # conversion runs to completion:
3336 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3339 def invalid_symbol():
3340 "a symbol with the incorrect format"
3342 conv = ensure_conversion('invalid-symbol')
3343 if not conv.output_found(
3344 r".*branch 'SYMBOL' references invalid revision 1$"
3346 raise Failure()
3349 def invalid_symbol_ignore():
3350 "ignore a symbol using a SymbolMapper"
3352 conv = ensure_conversion(
3353 'invalid-symbol', options_file='cvs2svn-ignore.options'
3357 def invalid_symbol_ignore2():
3358 "ignore a symbol using an IgnoreSymbolTransform"
3360 conv = ensure_conversion(
3361 'invalid-symbol', options_file='cvs2svn-ignore2.options'
3365 class EOLVariants(Cvs2SvnTestCase):
3366 "handle various --eol-style options"
3368 eol_style_strings = {
3369 'LF' : '\n',
3370 'CR' : '\r',
3371 'CRLF' : '\r\n',
3372 'native' : '\n',
3375 def __init__(self, eol_style):
3376 self.eol_style = eol_style
3377 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3378 Cvs2SvnTestCase.__init__(
3379 self, 'eol-variants', variant=self.eol_style,
3380 dumpfile=self.dumpfile,
3381 args=[
3382 '--default-eol=%s' % (self.eol_style,),
3386 def run(self):
3387 conv = self.ensure_conversion()
3388 dump_contents = open(conv.dumpfile, 'rb').read()
3389 expected_text = self.eol_style_strings[self.eol_style].join(
3390 ['line 1', 'line 2', '\n\n']
3392 if not dump_contents.endswith(expected_text):
3393 raise Failure()
3396 def no_revs_file():
3397 "handle a file with no revisions (issue #80)"
3399 conv = ensure_conversion('no-revs-file')
3402 def mirror_keyerror_test():
3403 "a case that gave KeyError in SVNRepositoryMirror"
3405 conv = ensure_conversion('mirror-keyerror')
3408 def exclude_ntdb_test():
3409 "exclude a non-trunk default branch"
3411 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3412 conv = ensure_conversion(
3413 'exclude-ntdb',
3414 args=[
3415 '--write-symbol-info=%s' % (symbol_info_file,),
3416 '--exclude=branch3',
3417 '--exclude=tag3',
3418 '--exclude=vendortag3',
3419 '--exclude=vendorbranch',
3424 def mirror_keyerror2_test():
3425 "a case that gave KeyError in RepositoryMirror"
3427 conv = ensure_conversion('mirror-keyerror2')
3430 def mirror_keyerror3_test():
3431 "a case that gave KeyError in RepositoryMirror"
3433 conv = ensure_conversion('mirror-keyerror3')
3436 def add_cvsignore_to_branch_test():
3437 "check adding .cvsignore to an existing branch"
3439 # This a test for issue #122.
3441 conv = ensure_conversion('add-cvsignore-to-branch')
3442 wc_tree = conv.get_wc_tree()
3443 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3444 if trunk_props['svn:ignore'] != '*.o\n\n':
3445 raise Failure()
3447 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3448 if branch_props['svn:ignore'] != '*.o\n\n':
3449 raise Failure()
3452 def missing_deltatext():
3453 "a revision's deltatext is missing"
3455 # This is a type of RCS file corruption that has been observed.
3456 conv = ensure_conversion(
3457 'missing-deltatext',
3458 error_re=(
3459 r"ERROR\: .* has no deltatext section for revision 1\.1\.4\.4"
3464 ########################################################################
3465 # Run the tests
3467 # list all tests here, starting with None:
3468 test_list = [
3469 None,
3470 # 1:
3471 show_usage,
3472 cvs2svn_manpage,
3473 cvs2git_manpage,
3474 attr_exec,
3475 space_fname,
3476 two_quick,
3477 PruneWithCare(),
3478 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3479 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3480 # 10:
3481 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3482 interleaved_commits,
3483 simple_commits,
3484 SimpleTags(),
3485 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3486 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3487 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3488 simple_branch_commits,
3489 mixed_time_tag,
3490 mixed_time_branch_with_added_file,
3491 # 20:
3492 mixed_commit,
3493 split_time_branch,
3494 bogus_tag,
3495 overlapping_branch,
3496 PhoenixBranch(),
3497 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3498 ctrl_char_in_log,
3499 overdead,
3500 NoTrunkPrune(),
3501 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3502 # 30:
3503 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3504 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3505 double_delete,
3506 split_branch,
3507 resync_misgroups,
3508 TaggedBranchAndTrunk(),
3509 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3510 enroot_race,
3511 enroot_race_obo,
3512 BranchDeleteFirst(),
3513 # 40:
3514 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3515 nonascii_filenames,
3516 UnicodeAuthor(
3517 warning_expected=1),
3518 UnicodeAuthor(
3519 warning_expected=0,
3520 variant='encoding', args=['--encoding=utf_8']),
3521 UnicodeAuthor(
3522 warning_expected=0,
3523 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3524 UnicodeLog(
3525 warning_expected=1),
3526 UnicodeLog(
3527 warning_expected=0,
3528 variant='encoding', args=['--encoding=utf_8']),
3529 UnicodeLog(
3530 warning_expected=0,
3531 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3532 vendor_branch_sameness,
3533 vendor_branch_trunk_only,
3534 # 50:
3535 default_branches,
3536 default_branches_trunk_only,
3537 default_branch_and_1_2,
3538 compose_tag_three_sources,
3539 pass5_when_to_fill,
3540 PeerPathPruning(),
3541 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3542 EmptyTrunk(),
3543 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3544 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3545 # 60:
3546 no_spurious_svn_commits,
3547 invalid_closings_on_trunk,
3548 individual_passes,
3549 resync_bug,
3550 branch_from_default_branch,
3551 file_in_attic_too,
3552 retain_file_in_attic_too,
3553 symbolic_name_filling_guide,
3554 eol_mime1,
3555 eol_mime2,
3556 # 70:
3557 eol_mime3,
3558 eol_mime4,
3559 cvs_revnums_off,
3560 cvs_revnums_on,
3561 keywords,
3562 ignore,
3563 requires_cvs,
3564 questionable_branch_names,
3565 questionable_tag_names,
3566 revision_reorder_bug,
3567 # 80:
3568 exclude,
3569 vendor_branch_delete_add,
3570 resync_pass2_pull_forward,
3571 native_eol,
3572 double_fill,
3573 XFail(double_fill2),
3574 resync_pass2_push_backward,
3575 double_add,
3576 bogus_branch_copy,
3577 nested_ttb_directories,
3578 # 90:
3579 auto_props_ignore_case,
3580 ctrl_char_in_filename,
3581 commit_dependencies,
3582 show_help_passes,
3583 multiple_tags,
3584 multiply_defined_symbols,
3585 multiply_defined_symbols_renamed,
3586 multiply_defined_symbols_ignored,
3587 repeatedly_defined_symbols,
3588 double_branch_delete,
3589 # 100:
3590 symbol_mismatches,
3591 overlook_symbol_mismatches,
3592 force_symbols,
3593 commit_blocks_tags,
3594 blocked_excludes,
3595 unblock_blocked_excludes,
3596 regexp_force_symbols,
3597 heuristic_symbol_default,
3598 branch_symbol_default,
3599 tag_symbol_default,
3600 # 110:
3601 symbol_transform,
3602 write_symbol_info,
3603 symbol_hints,
3604 parent_hints,
3605 parent_hints_invalid,
3606 parent_hints_wildcards,
3607 path_hints,
3608 issue_99,
3609 issue_100,
3610 issue_106,
3611 # 120:
3612 options_option,
3613 multiproject,
3614 crossproject,
3615 tag_with_no_revision,
3616 delete_cvsignore,
3617 repeated_deltatext,
3618 nasty_graphs,
3619 XFail(tagging_after_delete),
3620 crossed_branches,
3621 file_directory_conflict,
3622 # 130:
3623 attic_directory_conflict,
3624 internal_co,
3625 internal_co_exclude,
3626 internal_co_trunk_only,
3627 internal_co_keywords,
3628 leftover_revs,
3629 requires_internal_co,
3630 timestamp_chaos,
3631 symlinks,
3632 empty_trunk_path,
3633 # 140:
3634 preferred_parent_cycle,
3635 branch_from_empty_dir,
3636 trunk_readd,
3637 branch_from_deleted_1_1,
3638 add_on_branch,
3639 main_git,
3640 git_options,
3641 main_hg,
3642 invalid_symbol,
3643 invalid_symbol_ignore,
3644 # 150:
3645 invalid_symbol_ignore2,
3646 EOLVariants('LF'),
3647 EOLVariants('CR'),
3648 EOLVariants('CRLF'),
3649 EOLVariants('native'),
3650 no_revs_file,
3651 mirror_keyerror_test,
3652 exclude_ntdb_test,
3653 mirror_keyerror2_test,
3654 mirror_keyerror3_test,
3655 # 160:
3656 XFail(add_cvsignore_to_branch_test),
3657 missing_deltatext,
3660 if __name__ == '__main__':
3662 # Configure the environment for reproducable output from svn, etc.
3663 os.environ["LC_ALL"] = "C"
3665 # Unfortunately, there is no way under Windows to make Subversion
3666 # think that the local time zone is UTC, so we just work in the
3667 # local time zone.
3669 # The Subversion test suite code assumes it's being invoked from
3670 # within a working copy of the Subversion sources, and tries to use
3671 # the binaries in that tree. Since the cvs2svn tree never contains
3672 # a Subversion build, we just use the system's installed binaries.
3673 svntest.main.svn_binary = 'svn'
3674 svntest.main.svnlook_binary = 'svnlook'
3675 svntest.main.svnadmin_binary = 'svnadmin'
3676 svntest.main.svnversion_binary = 'svnversion'
3678 run_tests(test_list)
3679 # NOTREACHED
3682 ### End of file.