* cvs2svn_lib/svn_run_options.py (SVNRunOptions): Reorder method definitions.
[cvs2svn.git] / run-tests.py
blobaca7f017eeaadeaa44ad71c1d2f6c6401af40493
1 #!/usr/bin/env python
3 # run_tests.py: test suite for cvs2svn
5 # Usage: run_tests.py [-v | --verbose] [list | <num>]
7 # Options:
8 # -v, --verbose
9 # enable verbose output
11 # Arguments (at most one argument is allowed):
12 # list
13 # If the word "list" is passed as an argument, the list of
14 # available tests is printed (but no tests are run).
16 # <num>
17 # If a number is passed as an argument, then only the test
18 # with that number is run.
20 # If no argument is specified, then all tests are run.
22 # Subversion is a tool for revision control.
23 # See http://subversion.tigris.org for more information.
25 # ====================================================================
26 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
28 # This software is licensed as described in the file COPYING, which
29 # you should have received as part of this distribution. The terms
30 # are also available at http://subversion.tigris.org/license-1.html.
31 # If newer versions of this license are posted there, you may use a
32 # newer version instead, at your option.
34 ######################################################################
36 # General modules
37 import sys
38 import shutil
39 import stat
40 import re
41 import os
42 import time
43 import os.path
44 import locale
45 import textwrap
46 from difflib import Differ
48 # Make sure that a supported version of Python is being used:
49 if not (0x02040000 <= sys.hexversion < 0x03000000):
50 sys.stderr.write(
51 'error: Python 2, version 2.4 or higher required.\n'
53 sys.exit(1)
55 # This script needs to run in the correct directory. Make sure we're there.
56 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
57 sys.stderr.write("error: I need to be run in the directory containing "
58 "'cvs2svn' and 'test-data'.\n")
59 sys.exit(1)
61 # Load the Subversion test framework.
62 import svntest
63 from svntest import Failure
64 from svntest.main import run_command
65 from svntest.main import run_tests
66 from svntest.main import safe_rmtree
67 from svntest.testcase import TestCase
68 from svntest.testcase import Skip
69 from svntest.testcase import XFail
70 from svntest.tree import build_tree_from_wc
71 from svntest.tree import get_child
73 cvs2svn = os.path.abspath('cvs2svn')
74 cvs2git = os.path.abspath('cvs2git')
76 # We use the installed svn and svnlook binaries, instead of using
77 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
78 # behavior -- or even existence -- of local builds shouldn't affect
79 # the cvs2svn test suite.
80 svn = 'svn'
81 svnlook = 'svnlook'
83 test_data_dir = 'test-data'
84 tmp_dir = 'cvs2svn-tmp'
87 #----------------------------------------------------------------------
88 # Helpers.
89 #----------------------------------------------------------------------
92 # The value to expect for svn:keywords if it is set:
93 KEYWORDS = 'Author Date Id Revision'
96 class RunProgramException(Failure):
97 pass
100 class MissingErrorException(Failure):
101 def __init__(self, error_re):
102 Failure.__init__(
103 self, "Test failed because no error matched '%s'" % (error_re,)
107 def run_program(program, error_re, *varargs):
108 """Run PROGRAM with VARARGS, return stdout as a list of lines.
110 If there is any stderr and ERROR_RE is None, raise
111 RunProgramException, and print the stderr lines if
112 svntest.main.verbose_mode is true.
114 If ERROR_RE is not None, it is a string regular expression that must
115 match some line of stderr. If it fails to match, raise
116 MissingErrorExpection."""
118 # FIXME: exit_code is currently ignored.
119 exit_code, out, err = run_command(program, 1, 0, *varargs)
121 if error_re:
122 # Specified error expected on stderr.
123 if not err:
124 raise MissingErrorException(error_re)
125 else:
126 for line in err:
127 if re.match(error_re, line):
128 return out
129 raise MissingErrorException(error_re)
130 else:
131 # No stderr allowed.
132 if err:
133 if svntest.main.verbose_mode:
134 print '\n%s said:\n' % program
135 for line in err:
136 print ' ' + line,
137 print
138 raise RunProgramException()
140 return out
143 def run_cvs2svn(error_re, *varargs):
144 """Run cvs2svn with VARARGS, return stdout as a list of lines.
146 If there is any stderr and ERROR_RE is None, raise
147 RunProgramException, and print the stderr lines if
148 svntest.main.verbose_mode is true.
150 If ERROR_RE is not None, it is a string regular expression that must
151 match some line of stderr. If it fails to match, raise
152 MissingErrorException."""
154 # Use the same python that is running this script
155 return run_program(sys.executable, error_re, cvs2svn, *varargs)
156 # On Windows, for an unknown reason, the cmd.exe process invoked by
157 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
158 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
159 # this. Therefore, the redirection of the output to the .s-revs file fails.
160 # We no longer use the problematic invocation on any system, but this
161 # comment remains to warn about this problem.
164 def run_cvs2git(error_re, *varargs):
165 """Run cvs2git with VARARGS, returning stdout as a list of lines.
167 If there is any stderr and ERROR_RE is None, raise
168 RunProgramException, and print the stderr lines if
169 svntest.main.verbose_mode is true.
171 If ERROR_RE is not None, it is a string regular expression that must
172 match some line of stderr. If it fails to match, raise
173 MissingErrorException."""
175 # Use the same python that is running this script
176 return run_program(sys.executable, error_re, cvs2git, *varargs)
177 # On Windows, for an unknown reason, the cmd.exe process invoked by
178 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
179 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
180 # this. Therefore, the redirection of the output to the .s-revs file fails.
181 # We no longer use the problematic invocation on any system, but this
182 # comment remains to warn about this problem.
185 def run_svn(*varargs):
186 """Run svn with VARARGS; return stdout as a list of lines.
187 If there is any stderr, raise RunProgramException, and print the
188 stderr lines if svntest.main.verbose_mode is true."""
189 return run_program(svn, None, *varargs)
192 def repos_to_url(path_to_svn_repos):
193 """This does what you think it does."""
194 rpath = os.path.abspath(path_to_svn_repos)
195 if rpath[0] != '/':
196 rpath = '/' + rpath
197 return 'file://%s' % rpath.replace(os.sep, '/')
200 def svn_strptime(timestr):
201 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
204 class Log:
205 def __init__(self, revision, author, date, symbols):
206 self.revision = revision
207 self.author = author
209 # Internally, we represent the date as seconds since epoch (UTC).
210 # Since standard subversion log output shows dates in localtime
212 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
214 # and time.mktime() converts from localtime, it all works out very
215 # happily.
216 self.date = time.mktime(svn_strptime(date[0:19]))
218 # The following symbols are used for string interpolation when
219 # checking paths:
220 self.symbols = symbols
222 # The changed paths will be accumulated later, as log data is read.
223 # Keys here are paths such as '/trunk/foo/bar', values are letter
224 # codes such as 'M', 'A', and 'D'.
225 self.changed_paths = { }
227 # The msg will be accumulated later, as log data is read.
228 self.msg = ''
230 def absorb_changed_paths(self, out):
231 'Read changed paths from OUT into self, until no more.'
232 while 1:
233 line = out.readline()
234 if len(line) == 1: return
235 line = line[:-1]
236 op_portion = line[3:4]
237 path_portion = line[5:]
238 # If we're running on Windows we get backslashes instead of
239 # forward slashes.
240 path_portion = path_portion.replace('\\', '/')
241 # # We could parse out history information, but currently we
242 # # just leave it in the path portion because that's how some
243 # # tests expect it.
245 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
246 # if m:
247 # path_portion = m.group(1)
248 self.changed_paths[path_portion] = op_portion
250 def __cmp__(self, other):
251 return cmp(self.revision, other.revision) or \
252 cmp(self.author, other.author) or cmp(self.date, other.date) or \
253 cmp(self.changed_paths, other.changed_paths) or \
254 cmp(self.msg, other.msg)
256 def get_path_op(self, path):
257 """Return the operator for the change involving PATH.
259 PATH is allowed to include string interpolation directives (e.g.,
260 '%(trunk)s'), which are interpolated against self.symbols. Return
261 None if there is no record for PATH."""
262 return self.changed_paths.get(path % self.symbols)
264 def check_msg(self, msg):
265 """Verify that this Log's message starts with the specified MSG."""
266 if self.msg.find(msg) != 0:
267 raise Failure(
268 "Revision %d log message was:\n%s\n\n"
269 "It should have begun with:\n%s\n\n"
270 % (self.revision, self.msg, msg,)
273 def check_change(self, path, op):
274 """Verify that this Log includes a change for PATH with operator OP.
276 PATH is allowed to include string interpolation directives (e.g.,
277 '%(trunk)s'), which are interpolated against self.symbols."""
279 path = path % self.symbols
280 found_op = self.changed_paths.get(path, None)
281 if found_op is None:
282 raise Failure(
283 "Revision %d does not include change for path %s "
284 "(it should have been %s).\n"
285 % (self.revision, path, op,)
287 if found_op != op:
288 raise Failure(
289 "Revision %d path %s had op %s (it should have been %s)\n"
290 % (self.revision, path, found_op, op,)
293 def check_changes(self, changed_paths):
294 """Verify that this Log has precisely the CHANGED_PATHS specified.
296 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
297 strings are allowed to include string interpolation directives
298 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
300 cp = {}
301 for (path, op) in changed_paths:
302 cp[path % self.symbols] = op
304 if self.changed_paths != cp:
305 raise Failure(
306 "Revision %d changed paths list was:\n%s\n\n"
307 "It should have been:\n%s\n\n"
308 % (self.revision, self.changed_paths, cp,)
311 def check(self, msg, changed_paths):
312 """Verify that this Log has the MSG and CHANGED_PATHS specified.
314 Convenience function to check two things at once. MSG is passed
315 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
317 self.check_msg(msg)
318 self.check_changes(changed_paths)
321 def parse_log(svn_repos, symbols):
322 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
324 Initialize the Logs' symbols with SYMBOLS."""
326 class LineFeeder:
327 'Make a list of lines behave like an open file handle.'
328 def __init__(self, lines):
329 self.lines = lines
330 def readline(self):
331 if len(self.lines) > 0:
332 return self.lines.pop(0)
333 else:
334 return None
336 def absorb_message_body(out, num_lines, log):
337 """Read NUM_LINES of log message body from OUT into Log item LOG."""
339 for i in range(num_lines):
340 log.msg += out.readline()
342 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
343 '(?P<author>[^\|]+) \| '
344 '(?P<date>[^\|]+) '
345 '\| (?P<lines>[0-9]+) (line|lines)$')
347 log_separator = '-' * 72
349 logs = { }
351 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
353 while 1:
354 this_log = None
355 line = out.readline()
356 if not line: break
357 line = line[:-1]
359 if line.find(log_separator) == 0:
360 line = out.readline()
361 if not line: break
362 line = line[:-1]
363 m = log_start_re.match(line)
364 if m:
365 this_log = Log(
366 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
367 line = out.readline()
368 if not line.find('Changed paths:') == 0:
369 print 'unexpected log output (missing changed paths)'
370 print "Line: '%s'" % line
371 sys.exit(1)
372 this_log.absorb_changed_paths(out)
373 absorb_message_body(out, int(m.group('lines')), this_log)
374 logs[this_log.revision] = this_log
375 elif len(line) == 0:
376 break # We've reached the end of the log output.
377 else:
378 print 'unexpected log output (missing revision line)'
379 print "Line: '%s'" % line
380 sys.exit(1)
381 else:
382 print 'unexpected log output (missing log separator)'
383 print "Line: '%s'" % line
384 sys.exit(1)
386 return logs
389 def erase(path):
390 """Unconditionally remove PATH and its subtree, if any. PATH may be
391 non-existent, a file or symlink, or a directory."""
392 if os.path.isdir(path):
393 safe_rmtree(path)
394 elif os.path.exists(path):
395 os.remove(path)
398 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
400 def sym_log_msg(symbolic_name, is_tag=None):
401 """Return the expected log message for a cvs2svn-synthesized revision
402 creating branch or tag SYMBOLIC_NAME."""
404 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
405 if is_tag:
406 type = 'tag'
407 else:
408 type = 'branch'
410 return log_msg_text_wrapper.fill(
411 "This commit was manufactured by cvs2svn to create %s '%s'."
412 % (type, symbolic_name)
416 def make_conversion_id(
417 name, args, passbypass, options_file=None, symbol_hints_file=None
419 """Create an identifying tag for a conversion.
421 The return value can also be used as part of a filesystem path.
423 NAME is the name of the CVS repository.
425 ARGS are the extra arguments to be passed to cvs2svn.
427 PASSBYPASS is a boolean indicating whether the conversion is to be
428 run one pass at a time.
430 If OPTIONS_FILE is specified, it is an options file that will be
431 used for the conversion.
433 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
434 will be used for the conversion.
436 The 1-to-1 mapping between cvs2svn command parameters and
437 conversion_ids allows us to avoid running the same conversion more
438 than once, when multiple tests use exactly the same conversion."""
440 conv_id = name
442 _win32_fname_mapping = { '/': '_sl_', '\\': '_bs_', ':': '_co_',
443 '*': '_st_', '?': '_qm_', '"': '_qq_',
444 '<': '_lt_', '>': '_gt_', '|': '_pi_', }
445 for arg in args:
446 # Replace some characters that Win32 isn't happy about having in a
447 # filename (which was causing the eol_mime test to fail).
448 sanitized_arg = arg
449 for a, b in _win32_fname_mapping.items():
450 sanitized_arg = sanitized_arg.replace(a, b)
451 conv_id += sanitized_arg
453 if passbypass:
454 conv_id += '-passbypass'
456 if options_file is not None:
457 conv_id += '--options=%s' % options_file
459 if symbol_hints_file is not None:
460 conv_id += '--symbol-hints=%s' % symbol_hints_file
462 return conv_id
465 class Conversion:
466 """A record of a cvs2svn conversion.
468 Fields:
470 conv_id -- the conversion id for this Conversion.
472 name -- a one-word name indicating the involved repositories.
474 dumpfile -- the name of the SVN dumpfile created by the conversion
475 (if the DUMPFILE constructor argument was used); otherwise,
476 None.
478 repos -- the path to the svn repository. Unset if DUMPFILE was
479 specified.
481 logs -- a dictionary of Log instances, as returned by parse_log().
482 Unset if DUMPFILE was specified.
484 symbols -- a dictionary of symbols used for string interpolation
485 in path names.
487 stdout -- a list of lines written by cvs2svn to stdout
489 _wc -- the basename of the svn working copy (within tmp_dir).
490 Unset if DUMPFILE was specified.
492 _wc_path -- the path to the svn working copy, if it has already
493 been created; otherwise, None. (The working copy is created
494 lazily when get_wc() is called.) Unset if DUMPFILE was
495 specified.
497 _wc_tree -- the tree built from the svn working copy, if it has
498 already been created; otherwise, None. The tree is created
499 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
500 specified.
502 _svnrepos -- the basename of the svn repository (within tmp_dir).
503 Unset if DUMPFILE was specified."""
505 # The number of the last cvs2svn pass (determined lazily by
506 # get_last_pass()).
507 last_pass = None
509 @classmethod
510 def get_last_pass(cls):
511 """Return the number of cvs2svn's last pass."""
513 if cls.last_pass is None:
514 out = run_cvs2svn(None, '--help-passes')
515 cls.last_pass = int(out[-1].split()[0])
516 return cls.last_pass
518 def __init__(
519 self, conv_id, name, error_re, passbypass, symbols, args,
520 options_file=None, symbol_hints_file=None, dumpfile=None,
522 self.conv_id = conv_id
523 self.name = name
524 self.symbols = symbols
525 if not os.path.isdir(tmp_dir):
526 os.mkdir(tmp_dir)
528 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
530 if dumpfile:
531 self.dumpfile = os.path.join(tmp_dir, dumpfile)
532 # Clean up from any previous invocations of this script.
533 erase(self.dumpfile)
534 else:
535 self.dumpfile = None
536 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
537 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
538 self._wc_path = None
539 self._wc_tree = None
541 # Clean up from any previous invocations of this script.
542 erase(self.repos)
543 erase(self._wc)
545 args = list(args)
546 if options_file:
547 self.options_file = os.path.join(cvsrepos, options_file)
548 args.extend([
549 '--options=%s' % self.options_file,
551 assert not symbol_hints_file
552 else:
553 self.options_file = None
554 if tmp_dir != 'cvs2svn-tmp':
555 # Only include this argument if it differs from cvs2svn's default:
556 args.extend([
557 '--tmpdir=%s' % tmp_dir,
560 if symbol_hints_file:
561 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
562 args.extend([
563 '--symbol-hints=%s' % self.symbol_hints_file,
566 if self.dumpfile:
567 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
568 else:
569 args.extend(['-s', self.repos])
570 args.extend([cvsrepos])
572 if passbypass:
573 self.stdout = []
574 for p in range(1, self.get_last_pass() + 1):
575 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
576 else:
577 self.stdout = run_cvs2svn(error_re, *args)
579 if self.dumpfile:
580 if not os.path.isfile(self.dumpfile):
581 raise Failure(
582 "Dumpfile not created: '%s'"
583 % os.path.join(os.getcwd(), self.dumpfile)
585 else:
586 if os.path.isdir(self.repos):
587 self.logs = parse_log(self.repos, self.symbols)
588 elif error_re is None:
589 raise Failure(
590 "Repository not created: '%s'"
591 % os.path.join(os.getcwd(), self.repos)
594 def output_found(self, pattern):
595 """Return True if PATTERN matches any line in self.stdout.
597 PATTERN is a regular expression pattern as a string.
600 pattern_re = re.compile(pattern)
602 for line in self.stdout:
603 if pattern_re.match(line):
604 # We found the pattern that we were looking for.
605 return 1
606 else:
607 return 0
609 def find_tag_log(self, tagname):
610 """Search LOGS for a log message containing 'TAGNAME' and return the
611 log in which it was found."""
612 for i in xrange(len(self.logs), 0, -1):
613 if self.logs[i].msg.find("'"+tagname+"'") != -1:
614 return self.logs[i]
615 raise ValueError("Tag %s not found in logs" % tagname)
617 def get_wc(self, *args):
618 """Return the path to the svn working copy, or a path within the WC.
620 If a working copy has not been created yet, create it now.
622 If ARGS are specified, then they should be strings that form
623 fragments of a path within the WC. They are joined using
624 os.path.join() and appended to the WC path."""
626 if self._wc_path is None:
627 run_svn('co', repos_to_url(self.repos), self._wc)
628 self._wc_path = self._wc
629 return os.path.join(self._wc_path, *args)
631 def get_wc_tree(self):
632 if self._wc_tree is None:
633 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
634 return self._wc_tree
636 def path_exists(self, *args):
637 """Return True if the specified path exists within the repository.
639 (The strings in ARGS are first joined into a path using
640 os.path.join().)"""
642 return os.path.exists(self.get_wc(*args))
644 def check_props(self, keys, checks):
645 """Helper function for checking lots of properties. For a list of
646 files in the conversion, check that the values of the properties
647 listed in KEYS agree with those listed in CHECKS. CHECKS is a
648 list of tuples: [ (filename, [value, value, ...]), ...], where the
649 values are listed in the same order as the key names are listed in
650 KEYS."""
652 for (file, values) in checks:
653 assert len(values) == len(keys)
654 props = props_for_path(self.get_wc_tree(), file)
655 for i in range(len(keys)):
656 if props.get(keys[i]) != values[i]:
657 raise Failure(
658 "File %s has property %s set to \"%s\" "
659 "(it should have been \"%s\").\n"
660 % (file, keys[i], props.get(keys[i]), values[i],)
664 class GitConversion:
665 """A record of a cvs2svn conversion.
667 Fields:
669 name -- a one-word name indicating the CVS repository to be converted.
671 stdout -- a list of lines written by cvs2svn to stdout."""
673 def __init__(self, name, error_re, args, options_file):
674 self.name = name
675 if not os.path.isdir(tmp_dir):
676 os.mkdir(tmp_dir)
678 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
680 args = list(args)
681 assert options_file
682 self.options_file = os.path.join(cvsrepos, options_file)
683 args.extend([
684 '--options=%s' % self.options_file,
687 self.stdout = run_cvs2git(error_re, *args)
690 # Cache of conversions that have already been done. Keys are conv_id;
691 # values are Conversion instances.
692 already_converted = { }
694 def ensure_conversion(
695 name, error_re=None, passbypass=None,
696 trunk=None, branches=None, tags=None,
697 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
699 """Convert CVS repository NAME to Subversion, but only if it has not
700 been converted before by this invocation of this script. If it has
701 been converted before, return the Conversion object from the
702 previous invocation.
704 If no error, return a Conversion instance.
706 If ERROR_RE is a string, it is a regular expression expected to
707 match some line of stderr printed by the conversion. If there is an
708 error and ERROR_RE is not set, then raise Failure.
710 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
711 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
713 NAME is just one word. For example, 'main' would mean to convert
714 './test-data/main-cvsrepos', and after the conversion, the resulting
715 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
716 a checked out head working copy in './cvs2svn-tmp/main-wc'.
718 Any other options to pass to cvs2svn should be in ARGS, each element
719 being one option, e.g., '--trunk-only'. If the option takes an
720 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
721 are passed to cvs2svn in the order that they appear in ARGS.
723 If OPTIONS_FILE is specified, then it should be the name of a file
724 within the main directory of the cvs repository associated with this
725 test. It is passed to cvs2svn using the --options option (which
726 suppresses some other options that are incompatible with --options).
728 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
729 file within the main directory of the cvs repository associated with
730 this test. It is passed to cvs2svn using the --symbol-hints option.
732 If DUMPFILE is specified, then it is the name of a dumpfile within
733 the temporary directory to which the conversion output should be
734 written."""
736 if args is None:
737 args = []
738 else:
739 args = list(args)
741 if trunk is None:
742 trunk = 'trunk'
743 else:
744 args.append('--trunk=%s' % (trunk,))
746 if branches is None:
747 branches = 'branches'
748 else:
749 args.append('--branches=%s' % (branches,))
751 if tags is None:
752 tags = 'tags'
753 else:
754 args.append('--tags=%s' % (tags,))
756 conv_id = make_conversion_id(
757 name, args, passbypass, options_file, symbol_hints_file
760 if conv_id not in already_converted:
761 try:
762 # Run the conversion and store the result for the rest of this
763 # session:
764 already_converted[conv_id] = Conversion(
765 conv_id, name, error_re, passbypass,
766 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
767 args, options_file, symbol_hints_file, dumpfile,
769 except Failure:
770 # Remember the failure so that a future attempt to run this conversion
771 # does not bother to retry, but fails immediately.
772 already_converted[conv_id] = None
773 raise
775 conv = already_converted[conv_id]
776 if conv is None:
777 raise Failure()
778 return conv
781 class Cvs2SvnTestCase(TestCase):
782 def __init__(
783 self, name, description=None, variant=None,
784 error_re=None, passbypass=None,
785 trunk=None, branches=None, tags=None,
786 args=None,
787 options_file=None, symbol_hints_file=None, dumpfile=None,
789 TestCase.__init__(self)
790 self.name = name
792 if description is not None:
793 self._description = description
794 else:
795 # By default, use the first line of the class docstring as the
796 # description:
797 self._description = self.__doc__.splitlines()[0]
799 # Check that the original description is OK before we tinker with
800 # it:
801 self.check_description()
803 if variant is not None:
804 # Modify description to show the variant. Trim description
805 # first if necessary to stay within the 50-character limit.
806 suffix = '...variant %s' % (variant,)
807 self._description = self._description[:50 - len(suffix)] + suffix
808 # Check that the description is still OK:
809 self.check_description()
811 self.error_re = error_re
812 self.passbypass = passbypass
813 self.trunk = trunk
814 self.branches = branches
815 self.tags = tags
816 self.args = args
817 self.options_file = options_file
818 self.symbol_hints_file = symbol_hints_file
819 self.dumpfile = dumpfile
821 def get_description(self):
822 return self._description
824 def ensure_conversion(self):
825 return ensure_conversion(
826 self.name,
827 error_re=self.error_re, passbypass=self.passbypass,
828 trunk=self.trunk, branches=self.branches, tags=self.tags,
829 args=self.args,
830 options_file=self.options_file,
831 symbol_hints_file=self.symbol_hints_file,
832 dumpfile=self.dumpfile,
836 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
837 """Test properties resulting from a conversion."""
839 def __init__(self, name, props_to_test, expected_props, **kw):
840 """Initialize an instance of Cvs2SvnPropertiesTestCase.
842 NAME is the name of the test, passed to Cvs2SvnTestCase.
843 PROPS_TO_TEST is a list of the names of svn properties that should
844 be tested. EXPECTED_PROPS is a list of tuples [(filename,
845 [value,...])], where the second item in each tuple is a list of
846 values expected for the properties listed in PROPS_TO_TEST for the
847 specified filename. If a property must *not* be set, then its
848 value should be listed as None."""
850 Cvs2SvnTestCase.__init__(self, name, **kw)
851 self.props_to_test = props_to_test
852 self.expected_props = expected_props
854 def run(self):
855 conv = self.ensure_conversion()
856 conv.check_props(self.props_to_test, self.expected_props)
859 #----------------------------------------------------------------------
860 # Tests.
861 #----------------------------------------------------------------------
864 def show_usage():
865 "cvs2svn with no arguments shows usage"
866 out = run_cvs2svn(None)
867 if (len(out) > 2 and out[0].find('ERROR:') == 0
868 and out[1].find('DBM module')):
869 print 'cvs2svn cannot execute due to lack of proper DBM module.'
870 print 'Exiting without running any further tests.'
871 sys.exit(1)
872 if out[0].find('Usage:') < 0:
873 raise Failure('Basic cvs2svn invocation failed.')
876 def show_help_passes():
877 "cvs2svn --help-passes shows pass information"
878 out = run_cvs2svn(None, '--help-passes')
879 if out[0].find('PASSES') < 0:
880 raise Failure('cvs2svn --help-passes failed.')
883 def attr_exec():
884 "detection of the executable flag"
885 if sys.platform == 'win32':
886 raise svntest.Skip()
887 conv = ensure_conversion('main')
888 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
889 if not st[0] & stat.S_IXUSR:
890 raise Failure()
893 def space_fname():
894 "conversion of filename with a space"
895 conv = ensure_conversion('main')
896 if not conv.path_exists('trunk', 'single-files', 'space fname'):
897 raise Failure()
900 def two_quick():
901 "two commits in quick succession"
902 conv = ensure_conversion('main')
903 logs = parse_log(
904 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
905 if len(logs) != 2:
906 raise Failure()
909 class PruneWithCare(Cvs2SvnTestCase):
910 "prune, but never too much"
912 def __init__(self, **kw):
913 Cvs2SvnTestCase.__init__(self, 'main', **kw)
915 def run(self):
916 # Robert Pluim encountered this lovely one while converting the
917 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
918 # repository (see issue #1302). Step 4 is the doozy:
920 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
921 # revision 2: adds trunk/blah/NEWS
922 # revision 3: deletes trunk/blah/cookie
923 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
924 # revision 5: does nothing
926 # After fixing cvs2svn, the sequence (correctly) looks like this:
928 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
929 # revision 2: adds trunk/blah/NEWS
930 # revision 3: deletes trunk/blah/cookie
931 # revision 4: does nothing [because trunk/blah/cookie already deleted]
932 # revision 5: deletes blah
934 # The difference is in 4 and 5. In revision 4, it's not correct to
935 # prune blah/, because NEWS is still in there, so revision 4 does
936 # nothing now. But when we delete NEWS in 5, that should bubble up
937 # and prune blah/ instead.
939 # ### Note that empty revisions like 4 are probably going to become
940 # ### at least optional, if not banished entirely from cvs2svn's
941 # ### output. Hmmm, or they may stick around, with an extra
942 # ### revision property explaining what happened. Need to think
943 # ### about that. In some sense, it's a bug in Subversion itself,
944 # ### that such revisions don't show up in 'svn log' output.
946 # In the test below, 'trunk/full-prune/first' represents
947 # cookie, and 'trunk/full-prune/second' represents NEWS.
949 conv = self.ensure_conversion()
951 # Confirm that revision 4 removes '/trunk/full-prune/first',
952 # and that revision 6 removes '/trunk/full-prune'.
954 # Also confirm similar things about '/full-prune-reappear/...',
955 # which is similar, except that later on it reappears, restored
956 # from pruneland, because a file gets added to it.
958 # And finally, a similar thing for '/partial-prune/...', except that
959 # in its case, a permanent file on the top level prevents the
960 # pruning from going farther than the subdirectory containing first
961 # and second.
963 for path in ('full-prune/first',
964 'full-prune-reappear/sub/first',
965 'partial-prune/sub/first'):
966 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
968 for path in ('full-prune',
969 'full-prune-reappear',
970 'partial-prune/sub'):
971 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
973 for path in ('full-prune-reappear',
974 'full-prune-reappear/appears-later'):
975 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
978 def interleaved_commits():
979 "two interleaved trunk commits, different log msgs"
980 # See test-data/main-cvsrepos/proj/README.
981 conv = ensure_conversion('main')
983 # The initial import.
984 rev = 26
985 conv.logs[rev].check('Initial import.', (
986 ('/%(trunk)s/interleaved', 'A'),
987 ('/%(trunk)s/interleaved/1', 'A'),
988 ('/%(trunk)s/interleaved/2', 'A'),
989 ('/%(trunk)s/interleaved/3', 'A'),
990 ('/%(trunk)s/interleaved/4', 'A'),
991 ('/%(trunk)s/interleaved/5', 'A'),
992 ('/%(trunk)s/interleaved/a', 'A'),
993 ('/%(trunk)s/interleaved/b', 'A'),
994 ('/%(trunk)s/interleaved/c', 'A'),
995 ('/%(trunk)s/interleaved/d', 'A'),
996 ('/%(trunk)s/interleaved/e', 'A'),
999 def check_letters(rev):
1000 """Check if REV is the rev where only letters were committed."""
1002 conv.logs[rev].check('Committing letters only.', (
1003 ('/%(trunk)s/interleaved/a', 'M'),
1004 ('/%(trunk)s/interleaved/b', 'M'),
1005 ('/%(trunk)s/interleaved/c', 'M'),
1006 ('/%(trunk)s/interleaved/d', 'M'),
1007 ('/%(trunk)s/interleaved/e', 'M'),
1010 def check_numbers(rev):
1011 """Check if REV is the rev where only numbers were committed."""
1013 conv.logs[rev].check('Committing numbers only.', (
1014 ('/%(trunk)s/interleaved/1', 'M'),
1015 ('/%(trunk)s/interleaved/2', 'M'),
1016 ('/%(trunk)s/interleaved/3', 'M'),
1017 ('/%(trunk)s/interleaved/4', 'M'),
1018 ('/%(trunk)s/interleaved/5', 'M'),
1021 # One of the commits was letters only, the other was numbers only.
1022 # But they happened "simultaneously", so we don't assume anything
1023 # about which commit appeared first, so we just try both ways.
1024 rev += 1
1025 try:
1026 check_letters(rev)
1027 check_numbers(rev + 1)
1028 except Failure:
1029 check_numbers(rev)
1030 check_letters(rev + 1)
1033 def simple_commits():
1034 "simple trunk commits"
1035 # See test-data/main-cvsrepos/proj/README.
1036 conv = ensure_conversion('main')
1038 # The initial import.
1039 conv.logs[13].check('Initial import.', (
1040 ('/%(trunk)s/proj', 'A'),
1041 ('/%(trunk)s/proj/default', 'A'),
1042 ('/%(trunk)s/proj/sub1', 'A'),
1043 ('/%(trunk)s/proj/sub1/default', 'A'),
1044 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1045 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1046 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1047 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1048 ('/%(trunk)s/proj/sub2', 'A'),
1049 ('/%(trunk)s/proj/sub2/default', 'A'),
1050 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1051 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1052 ('/%(trunk)s/proj/sub3', 'A'),
1053 ('/%(trunk)s/proj/sub3/default', 'A'),
1056 # The first commit.
1057 conv.logs[18].check('First commit to proj, affecting two files.', (
1058 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1059 ('/%(trunk)s/proj/sub3/default', 'M'),
1062 # The second commit.
1063 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1064 ('/%(trunk)s/proj/default', 'M'),
1065 ('/%(trunk)s/proj/sub1/default', 'M'),
1066 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1067 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1068 ('/%(trunk)s/proj/sub2/default', 'M'),
1069 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1070 ('/%(trunk)s/proj/sub3/default', 'M')
1074 class SimpleTags(Cvs2SvnTestCase):
1075 "simple tags and branches, no commits"
1077 def __init__(self, **kw):
1078 # See test-data/main-cvsrepos/proj/README.
1079 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1081 def run(self):
1082 conv = self.ensure_conversion()
1084 # Verify the copy source for the tags we are about to check
1085 # No need to verify the copyfrom revision, as simple_commits did that
1086 conv.logs[13].check('Initial import.', (
1087 ('/%(trunk)s/proj', 'A'),
1088 ('/%(trunk)s/proj/default', 'A'),
1089 ('/%(trunk)s/proj/sub1', 'A'),
1090 ('/%(trunk)s/proj/sub1/default', 'A'),
1091 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1092 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1093 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1094 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1095 ('/%(trunk)s/proj/sub2', 'A'),
1096 ('/%(trunk)s/proj/sub2/default', 'A'),
1097 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1098 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1099 ('/%(trunk)s/proj/sub3', 'A'),
1100 ('/%(trunk)s/proj/sub3/default', 'A'),
1103 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1105 # Tag on rev 1.1.1.1 of all files in proj
1106 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1107 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1108 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1109 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1112 # The same, as a tag
1113 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1114 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1115 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1118 # Tag on rev 1.1.1.1 of all files in proj, except one
1119 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1120 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1121 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1122 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1125 # The same, as a branch
1126 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1127 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1128 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1132 def simple_branch_commits():
1133 "simple branch commits"
1134 # See test-data/main-cvsrepos/proj/README.
1135 conv = ensure_conversion('main')
1137 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1138 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1139 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1140 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1144 def mixed_time_tag():
1145 "mixed-time tag"
1146 # See test-data/main-cvsrepos/proj/README.
1147 conv = ensure_conversion('main')
1149 log = conv.find_tag_log('T_MIXED')
1150 log.check_changes((
1151 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1155 def mixed_time_branch_with_added_file():
1156 "mixed-time branch, and a file added to the branch"
1157 # See test-data/main-cvsrepos/proj/README.
1158 conv = ensure_conversion('main')
1160 # A branch from the same place as T_MIXED in the previous test,
1161 # plus a file added directly to the branch
1162 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1163 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1164 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1165 ('/%(branches)s/B_MIXED/single-files', 'D'),
1166 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1167 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1168 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1171 conv.logs[22].check('Add a file on branch B_MIXED.', (
1172 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1176 def mixed_commit():
1177 "a commit affecting both trunk and a branch"
1178 # See test-data/main-cvsrepos/proj/README.
1179 conv = ensure_conversion('main')
1181 conv.logs[24].check(
1182 'A single commit affecting one file on branch B_MIXED '
1183 'and one on trunk.', (
1184 ('/%(trunk)s/proj/sub2/default', 'M'),
1185 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1189 def split_time_branch():
1190 "branch some trunk files, and later branch the rest"
1191 # See test-data/main-cvsrepos/proj/README.
1192 conv = ensure_conversion('main')
1194 # First change on the branch, creating it
1195 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1196 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1197 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1198 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1199 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1202 conv.logs[29].check('First change on branch B_SPLIT.', (
1203 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1204 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1205 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1206 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1207 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1210 # A trunk commit for the file which was not branched
1211 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1212 'This was committed about an', (
1213 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1216 # Add the file not already branched to the branch, with modification:w
1217 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1218 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1219 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1222 conv.logs[32].check('This change affects sub3/default and '
1223 'sub1/subsubB/default, on branch', (
1224 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1225 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1229 def multiple_tags():
1230 "multiple tags referring to same revision"
1231 conv = ensure_conversion('main')
1232 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1233 raise Failure()
1234 if not conv.path_exists(
1235 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1236 raise Failure()
1239 def multiply_defined_symbols():
1240 "multiple definitions of symbol names"
1242 # We can only check one line of the error output at a time, so test
1243 # twice. (The conversion only have to be done once because the
1244 # results are cached.)
1245 conv = ensure_conversion(
1246 'multiply-defined-symbols',
1247 error_re=(
1248 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1249 r"1\.2\.4 1\.2\.2"
1252 conv = ensure_conversion(
1253 'multiply-defined-symbols',
1254 error_re=(
1255 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1256 r"1\.2 1\.1"
1261 def multiply_defined_symbols_renamed():
1262 "rename multiply defined symbols"
1264 conv = ensure_conversion(
1265 'multiply-defined-symbols',
1266 options_file='cvs2svn-rename.options',
1270 def multiply_defined_symbols_ignored():
1271 "ignore multiply defined symbols"
1273 conv = ensure_conversion(
1274 'multiply-defined-symbols',
1275 options_file='cvs2svn-ignore.options',
1279 def repeatedly_defined_symbols():
1280 "multiple identical definitions of symbol names"
1282 # If a symbol is defined multiple times but has the same value each
1283 # time, that should not be an error.
1285 conv = ensure_conversion('repeatedly-defined-symbols')
1288 def bogus_tag():
1289 "conversion of invalid symbolic names"
1290 conv = ensure_conversion('bogus-tag')
1293 def overlapping_branch():
1294 "ignore a file with a branch with two names"
1295 conv = ensure_conversion('overlapping-branch')
1297 if not conv.output_found('.*cannot also have name \'vendorB\''):
1298 raise Failure()
1300 conv.logs[2].check('imported', (
1301 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1302 ('/%(trunk)s/overlapping-branch', 'A'),
1305 if len(conv.logs) != 2:
1306 raise Failure()
1309 class PhoenixBranch(Cvs2SvnTestCase):
1310 "convert a branch file rooted in a 'dead' revision"
1312 def __init__(self, **kw):
1313 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1315 def run(self):
1316 conv = self.ensure_conversion()
1317 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1318 ('/%(branches)s/volsung_20010721', 'A'),
1319 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1321 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1322 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1326 ###TODO: We check for 4 changed paths here to accomodate creating tags
1327 ###and branches in rev 1, but that will change, so this will
1328 ###eventually change back.
1329 def ctrl_char_in_log():
1330 "handle a control char in a log message"
1331 # This was issue #1106.
1332 rev = 2
1333 conv = ensure_conversion('ctrl-char-in-log')
1334 conv.logs[rev].check_changes((
1335 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1337 if conv.logs[rev].msg.find('\x04') < 0:
1338 raise Failure(
1339 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1342 def overdead():
1343 "handle tags rooted in a redeleted revision"
1344 conv = ensure_conversion('overdead')
1347 class NoTrunkPrune(Cvs2SvnTestCase):
1348 "ensure that trunk doesn't get pruned"
1350 def __init__(self, **kw):
1351 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1353 def run(self):
1354 conv = self.ensure_conversion()
1355 for rev in conv.logs.keys():
1356 rev_logs = conv.logs[rev]
1357 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1358 raise Failure()
1361 def double_delete():
1362 "file deleted twice, in the root of the repository"
1363 # This really tests several things: how we handle a file that's
1364 # removed (state 'dead') in two successive revisions; how we
1365 # handle a file in the root of the repository (there were some
1366 # bugs in cvs2svn's svn path construction for top-level files); and
1367 # the --no-prune option.
1368 conv = ensure_conversion(
1369 'double-delete', args=['--trunk-only', '--no-prune'])
1371 path = '/%(trunk)s/twice-removed'
1372 rev = 2
1373 conv.logs[rev].check('Updated CVS', (
1374 (path, 'A'),
1376 conv.logs[rev + 1].check('Remove this file for the first time.', (
1377 (path, 'D'),
1379 conv.logs[rev + 2].check('Remove this file for the second time,', (
1383 def split_branch():
1384 "branch created from both trunk and another branch"
1385 # See test-data/split-branch-cvsrepos/README.
1387 # The conversion will fail if the bug is present, and
1388 # ensure_conversion will raise Failure.
1389 conv = ensure_conversion('split-branch')
1392 def resync_misgroups():
1393 "resyncing should not misorder commit groups"
1394 # See test-data/resync-misgroups-cvsrepos/README.
1396 # The conversion will fail if the bug is present, and
1397 # ensure_conversion will raise Failure.
1398 conv = ensure_conversion('resync-misgroups')
1401 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1402 "allow tags with mixed trunk and branch sources"
1404 def __init__(self, **kw):
1405 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1407 def run(self):
1408 conv = self.ensure_conversion()
1410 tags = conv.symbols.get('tags', 'tags')
1412 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1413 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1414 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1415 raise Failure()
1416 if (open(a_path, 'r').read().find('1.24') == -1) \
1417 or (open(b_path, 'r').read().find('1.5') == -1):
1418 raise Failure()
1421 def enroot_race():
1422 "never use the rev-in-progress as a copy source"
1424 # See issue #1427 and r8544.
1425 conv = ensure_conversion('enroot-race')
1426 rev = 6
1427 conv.logs[rev].check_changes((
1428 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1429 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1430 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1432 conv.logs[rev + 1].check_changes((
1433 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1434 ('/%(trunk)s/proj/a.txt', 'M'),
1435 ('/%(trunk)s/proj/b.txt', 'M'),
1439 def enroot_race_obo():
1440 "do use the last completed rev as a copy source"
1441 conv = ensure_conversion('enroot-race-obo')
1442 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1443 if not len(conv.logs) == 3:
1444 raise Failure()
1447 class BranchDeleteFirst(Cvs2SvnTestCase):
1448 "correctly handle deletion as initial branch action"
1450 def __init__(self, **kw):
1451 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1453 def run(self):
1454 # See test-data/branch-delete-first-cvsrepos/README.
1456 # The conversion will fail if the bug is present, and
1457 # ensure_conversion would raise Failure.
1458 conv = self.ensure_conversion()
1460 branches = conv.symbols.get('branches', 'branches')
1462 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1463 if conv.path_exists(branches, 'branch-1', 'file'):
1464 raise Failure()
1465 if conv.path_exists(branches, 'branch-2', 'file'):
1466 raise Failure()
1467 if not conv.path_exists(branches, 'branch-3', 'file'):
1468 raise Failure()
1471 def nonascii_filenames():
1472 "non ascii files converted incorrectly"
1473 # see issue #1255
1475 # on a en_US.iso-8859-1 machine this test fails with
1476 # svn: Can't recode ...
1478 # as described in the issue
1480 # on a en_US.UTF-8 machine this test fails with
1481 # svn: Malformed XML ...
1483 # which means at least it fails. Unfortunately it won't fail
1484 # with the same error...
1486 # mangle current locale settings so we know we're not running
1487 # a UTF-8 locale (which does not exhibit this problem)
1488 current_locale = locale.getlocale()
1489 new_locale = 'en_US.ISO8859-1'
1490 locale_changed = None
1492 # From http://docs.python.org/lib/module-sys.html
1494 # getfilesystemencoding():
1496 # Return the name of the encoding used to convert Unicode filenames
1497 # into system file names, or None if the system default encoding is
1498 # used. The result value depends on the operating system:
1500 # - On Windows 9x, the encoding is ``mbcs''.
1501 # - On Mac OS X, the encoding is ``utf-8''.
1502 # - On Unix, the encoding is the user's preference according to the
1503 # result of nl_langinfo(CODESET), or None if the
1504 # nl_langinfo(CODESET) failed.
1505 # - On Windows NT+, file names are Unicode natively, so no conversion is
1506 # performed.
1508 # So we're going to skip this test on Mac OS X for now.
1509 if sys.platform == "darwin":
1510 raise svntest.Skip()
1512 try:
1513 # change locale to non-UTF-8 locale to generate latin1 names
1514 locale.setlocale(locale.LC_ALL, # this might be too broad?
1515 new_locale)
1516 locale_changed = 1
1517 except locale.Error:
1518 raise svntest.Skip()
1520 try:
1521 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1522 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1523 if not os.path.exists(dstrepos_path):
1524 # create repos from existing main repos
1525 shutil.copytree(srcrepos_path, dstrepos_path)
1526 base_path = os.path.join(dstrepos_path, 'single-files')
1527 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1528 os.path.join(base_path, 'two\366uick,v'))
1529 new_path = os.path.join(dstrepos_path, 'single\366files')
1530 os.rename(base_path, new_path)
1532 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1533 finally:
1534 if locale_changed:
1535 locale.setlocale(locale.LC_ALL, current_locale)
1536 safe_rmtree(dstrepos_path)
1539 class UnicodeTest(Cvs2SvnTestCase):
1540 "metadata contains unicode"
1542 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1544 def __init__(self, name, warning_expected, **kw):
1545 if warning_expected:
1546 error_re = self.warning_pattern
1547 else:
1548 error_re = None
1550 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1551 self.warning_expected = warning_expected
1553 def run(self):
1554 try:
1555 # ensure the availability of the "utf_8" encoding:
1556 u'a'.encode('utf_8').decode('utf_8')
1557 except LookupError:
1558 raise svntest.Skip()
1560 self.ensure_conversion()
1563 class UnicodeAuthor(UnicodeTest):
1564 "author name contains unicode"
1566 def __init__(self, warning_expected, **kw):
1567 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1570 class UnicodeLog(UnicodeTest):
1571 "log message contains unicode"
1573 def __init__(self, warning_expected, **kw):
1574 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1577 def vendor_branch_sameness():
1578 "avoid spurious changes for initial revs"
1579 conv = ensure_conversion(
1580 'vendor-branch-sameness', args=['--keep-trivial-imports']
1583 # The following files are in this repository:
1585 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1586 # the same contents, the file's default branch is 1.1.1,
1587 # and both revisions are in state 'Exp'.
1589 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1590 # 1.1 (the addition of a line of text).
1592 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1594 # d.txt: This file was created by 'cvs add' instead of import, so
1595 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1596 # The timestamp on the add is exactly the same as for the
1597 # imports of the other files.
1599 # e.txt: Like a.txt, except that the log message for revision 1.1
1600 # is not the standard import log message.
1602 # (Aside from e.txt, the log messages for the same revisions are the
1603 # same in all files.)
1605 # We expect that only a.txt is recognized as an import whose 1.1
1606 # revision can be omitted. The other files should be added on trunk
1607 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1608 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1609 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1610 # and c.txt should be 'D'eleted.
1612 rev = 2
1613 conv.logs[rev].check('Initial revision', (
1614 ('/%(trunk)s/proj', 'A'),
1615 ('/%(trunk)s/proj/b.txt', 'A'),
1616 ('/%(trunk)s/proj/c.txt', 'A'),
1617 ('/%(trunk)s/proj/d.txt', 'A'),
1620 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1621 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1622 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1625 conv.logs[rev + 2].check('First vendor branch revision.', (
1626 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1627 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1628 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1631 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1632 'to compensate for changes in r4,', (
1633 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1634 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1635 ('/%(trunk)s/proj/c.txt', 'D'),
1638 rev = 7
1639 conv.logs[rev].check('This log message is not the standard', (
1640 ('/%(trunk)s/proj/e.txt', 'A'),
1643 conv.logs[rev + 2].check('First vendor branch revision', (
1644 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1647 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1648 'to compensate for changes in r9,', (
1649 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1653 def vendor_branch_trunk_only():
1654 "handle vendor branches with --trunk-only"
1655 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1657 rev = 2
1658 conv.logs[rev].check('Initial revision', (
1659 ('/%(trunk)s/proj', 'A'),
1660 ('/%(trunk)s/proj/b.txt', 'A'),
1661 ('/%(trunk)s/proj/c.txt', 'A'),
1662 ('/%(trunk)s/proj/d.txt', 'A'),
1665 conv.logs[rev + 1].check('First vendor branch revision', (
1666 ('/%(trunk)s/proj/a.txt', 'A'),
1667 ('/%(trunk)s/proj/b.txt', 'M'),
1668 ('/%(trunk)s/proj/c.txt', 'D'),
1671 conv.logs[rev + 2].check('This log message is not the standard', (
1672 ('/%(trunk)s/proj/e.txt', 'A'),
1675 conv.logs[rev + 3].check('First vendor branch revision', (
1676 ('/%(trunk)s/proj/e.txt', 'M'),
1680 def default_branches():
1681 "handle default branches correctly"
1682 conv = ensure_conversion('default-branches')
1684 # There are seven files in the repository:
1686 # a.txt:
1687 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1688 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1689 # committed (thus losing the default branch "1.1.1"), then
1690 # 1.1.1.4 was imported. All vendor import release tags are
1691 # still present.
1693 # b.txt:
1694 # Like a.txt, but without rev 1.2.
1696 # c.txt:
1697 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1699 # d.txt:
1700 # Same as the previous two, but 1.1.1 branch is unlabeled.
1702 # e.txt:
1703 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1705 # deleted-on-vendor-branch.txt,v:
1706 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1708 # added-then-imported.txt,v:
1709 # Added with 'cvs add' to create 1.1, then imported with
1710 # completely different contents to create 1.1.1.1, therefore
1711 # never had a default branch.
1714 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1715 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1716 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1717 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1718 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1719 ('/%(branches)s/vbranchA', 'A'),
1720 ('/%(branches)s/vbranchA/proj', 'A'),
1721 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1722 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1723 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1724 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1727 conv.logs[3].check("This commit was generated by cvs2svn "
1728 "to compensate for changes in r2,", (
1729 ('/%(trunk)s/proj', 'A'),
1730 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1731 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1732 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1733 ('/%(trunk)s/proj/d.txt '
1734 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1735 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1736 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1737 ('/%(trunk)s/proj/e.txt '
1738 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1741 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1742 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1743 ('/%(tags)s/vtag-1/proj/d.txt '
1744 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1747 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1748 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1749 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1750 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1751 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1752 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1753 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1756 conv.logs[6].check("This commit was generated by cvs2svn "
1757 "to compensate for changes in r5,", (
1758 ('/%(trunk)s/proj/a.txt '
1759 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1760 ('/%(trunk)s/proj/b.txt '
1761 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1762 ('/%(trunk)s/proj/c.txt '
1763 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1764 ('/%(trunk)s/proj/d.txt '
1765 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1766 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1767 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1768 'R'),
1769 ('/%(trunk)s/proj/e.txt '
1770 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1773 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1774 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1775 ('/%(tags)s/vtag-2/proj/d.txt '
1776 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1779 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1780 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1781 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1782 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1783 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1784 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1785 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1788 conv.logs[9].check("This commit was generated by cvs2svn "
1789 "to compensate for changes in r8,", (
1790 ('/%(trunk)s/proj/a.txt '
1791 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1792 ('/%(trunk)s/proj/b.txt '
1793 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1794 ('/%(trunk)s/proj/c.txt '
1795 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1796 ('/%(trunk)s/proj/d.txt '
1797 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1798 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1799 ('/%(trunk)s/proj/e.txt '
1800 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1803 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1804 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1805 ('/%(tags)s/vtag-3/proj/d.txt '
1806 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1807 ('/%(tags)s/vtag-3/proj/e.txt '
1808 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1811 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1812 ('/%(trunk)s/proj/a.txt', 'M'),
1815 conv.logs[12].check("Add a file to the working copy.", (
1816 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1819 conv.logs[13].check(sym_log_msg('vbranchA'), (
1820 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1821 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1824 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1825 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1826 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1827 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1828 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1829 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1830 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1831 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1834 conv.logs[15].check("This commit was generated by cvs2svn "
1835 "to compensate for changes in r14,", (
1836 ('/%(trunk)s/proj/b.txt '
1837 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1838 ('/%(trunk)s/proj/c.txt '
1839 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1840 ('/%(trunk)s/proj/d.txt '
1841 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1842 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1843 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1844 'A'),
1845 ('/%(trunk)s/proj/e.txt '
1846 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1849 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1850 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1851 ('/%(tags)s/vtag-4/proj/d.txt '
1852 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1856 def default_branches_trunk_only():
1857 "handle default branches with --trunk-only"
1859 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1861 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1862 ('/%(trunk)s/proj', 'A'),
1863 ('/%(trunk)s/proj/a.txt', 'A'),
1864 ('/%(trunk)s/proj/b.txt', 'A'),
1865 ('/%(trunk)s/proj/c.txt', 'A'),
1866 ('/%(trunk)s/proj/d.txt', 'A'),
1867 ('/%(trunk)s/proj/e.txt', 'A'),
1868 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1871 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1872 ('/%(trunk)s/proj/a.txt', 'M'),
1873 ('/%(trunk)s/proj/b.txt', 'M'),
1874 ('/%(trunk)s/proj/c.txt', 'M'),
1875 ('/%(trunk)s/proj/d.txt', 'M'),
1876 ('/%(trunk)s/proj/e.txt', 'M'),
1877 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1880 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1881 ('/%(trunk)s/proj/a.txt', 'M'),
1882 ('/%(trunk)s/proj/b.txt', 'M'),
1883 ('/%(trunk)s/proj/c.txt', 'M'),
1884 ('/%(trunk)s/proj/d.txt', 'M'),
1885 ('/%(trunk)s/proj/e.txt', 'M'),
1886 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1889 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1890 ('/%(trunk)s/proj/a.txt', 'M'),
1893 conv.logs[6].check("Add a file to the working copy.", (
1894 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1897 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1898 ('/%(trunk)s/proj/b.txt', 'M'),
1899 ('/%(trunk)s/proj/c.txt', 'M'),
1900 ('/%(trunk)s/proj/d.txt', 'M'),
1901 ('/%(trunk)s/proj/e.txt', 'M'),
1902 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1906 def default_branch_and_1_2():
1907 "do not allow 1.2 revision with default branch"
1909 conv = ensure_conversion(
1910 'default-branch-and-1-2',
1911 error_re=(
1912 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
1917 def compose_tag_three_sources():
1918 "compose a tag from three sources"
1919 conv = ensure_conversion('compose-tag-three-sources')
1921 conv.logs[2].check("Add on trunk", (
1922 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
1923 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
1924 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
1925 ('/%(trunk)s/tagged-on-b1', 'A'),
1926 ('/%(trunk)s/tagged-on-b2', 'A'),
1929 conv.logs[3].check(sym_log_msg('b1'), (
1930 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
1933 conv.logs[4].check(sym_log_msg('b2'), (
1934 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
1937 conv.logs[5].check("Commit on branch b1", (
1938 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
1939 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
1940 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
1941 ('/%(branches)s/b1/tagged-on-b1', 'M'),
1942 ('/%(branches)s/b1/tagged-on-b2', 'M'),
1945 conv.logs[6].check("Commit on branch b2", (
1946 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
1947 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
1948 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
1949 ('/%(branches)s/b2/tagged-on-b1', 'M'),
1950 ('/%(branches)s/b2/tagged-on-b2', 'M'),
1953 conv.logs[7].check("Commit again on trunk", (
1954 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
1955 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
1956 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
1957 ('/%(trunk)s/tagged-on-b1', 'M'),
1958 ('/%(trunk)s/tagged-on-b2', 'M'),
1961 conv.logs[8].check(sym_log_msg('T',1), (
1962 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
1963 ('/%(tags)s/T/tagged-on-trunk-1.1 '
1964 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
1965 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
1966 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
1970 def pass5_when_to_fill():
1971 "reserve a svn revnum for a fill only when required"
1972 # The conversion will fail if the bug is present, and
1973 # ensure_conversion would raise Failure.
1974 conv = ensure_conversion('pass5-when-to-fill')
1977 class EmptyTrunk(Cvs2SvnTestCase):
1978 "don't break when the trunk is empty"
1980 def __init__(self, **kw):
1981 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
1983 def run(self):
1984 # The conversion will fail if the bug is present, and
1985 # ensure_conversion would raise Failure.
1986 conv = self.ensure_conversion()
1989 def no_spurious_svn_commits():
1990 "ensure that we don't create any spurious commits"
1991 conv = ensure_conversion('phoenix')
1993 # Check spurious commit that could be created in
1994 # SVNCommitCreator._pre_commit()
1996 # (When you add a file on a branch, CVS creates a trunk revision
1997 # in state 'dead'. If the log message of that commit is equal to
1998 # the one that CVS generates, we do not ever create a 'fill'
1999 # SVNCommit for it.)
2001 # and spurious commit that could be created in
2002 # SVNCommitCreator._commit()
2004 # (When you add a file on a branch, CVS creates a trunk revision
2005 # in state 'dead'. If the log message of that commit is equal to
2006 # the one that CVS generates, we do not create a primary SVNCommit
2007 # for it.)
2008 conv.logs[17].check('File added on branch xiphophorus', (
2009 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2012 # Check to make sure that a commit *is* generated:
2013 # (When you add a file on a branch, CVS creates a trunk revision
2014 # in state 'dead'. If the log message of that commit is NOT equal
2015 # to the one that CVS generates, we create a primary SVNCommit to
2016 # serve as a home for the log message in question.
2017 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2018 + 'branch xiphophorus,\nand this log message was tweaked', ())
2020 # Check spurious commit that could be created in
2021 # SVNCommitCreator._commit_symbols().
2022 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2023 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2027 class PeerPathPruning(Cvs2SvnTestCase):
2028 "make sure that filling prunes paths correctly"
2030 def __init__(self, **kw):
2031 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2033 def run(self):
2034 conv = self.ensure_conversion()
2035 conv.logs[6].check(sym_log_msg('BRANCH'), (
2036 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2037 ('/%(branches)s/BRANCH/bar', 'D'),
2038 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2042 def invalid_closings_on_trunk():
2043 "verify correct revs are copied to default branches"
2044 # The conversion will fail if the bug is present, and
2045 # ensure_conversion would raise Failure.
2046 conv = ensure_conversion('invalid-closings-on-trunk')
2049 def individual_passes():
2050 "run each pass individually"
2051 conv = ensure_conversion('main')
2052 conv2 = ensure_conversion('main', passbypass=1)
2054 if conv.logs != conv2.logs:
2055 raise Failure()
2058 def resync_bug():
2059 "reveal a big bug in our resync algorithm"
2060 # This will fail if the bug is present
2061 conv = ensure_conversion('resync-bug')
2064 def branch_from_default_branch():
2065 "reveal a bug in our default branch detection code"
2066 conv = ensure_conversion('branch-from-default-branch')
2068 # This revision will be a default branch synchronization only
2069 # if cvs2svn is correctly determining default branch revisions.
2071 # The bug was that cvs2svn was treating revisions on branches off of
2072 # default branches as default branch revisions, resulting in
2073 # incorrectly regarding the branch off of the default branch as a
2074 # non-trunk default branch. Crystal clear? I thought so. See
2075 # issue #42 for more incoherent blathering.
2076 conv.logs[5].check("This commit was generated by cvs2svn", (
2077 ('/%(trunk)s/proj/file.txt '
2078 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2082 def file_in_attic_too():
2083 "die if a file exists in and out of the attic"
2084 ensure_conversion(
2085 'file-in-attic-too',
2086 error_re=(
2087 r'.*A CVS repository cannot contain both '
2088 r'(.*)' + re.escape(os.sep) + r'(.*) '
2089 + r'and '
2090 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2095 def retain_file_in_attic_too():
2096 "test --retain-conflicting-attic-files option"
2097 conv = ensure_conversion(
2098 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2099 if not conv.path_exists('trunk', 'file.txt'):
2100 raise Failure()
2101 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2102 raise Failure()
2105 def symbolic_name_filling_guide():
2106 "reveal a big bug in our SymbolFillingGuide"
2107 # This will fail if the bug is present
2108 conv = ensure_conversion('symbolic-name-overfill')
2111 # Helpers for tests involving file contents and properties.
2113 class NodeTreeWalkException:
2114 "Exception class for node tree traversals."
2115 pass
2117 def node_for_path(node, path):
2118 "In the tree rooted under SVNTree NODE, return the node at PATH."
2119 if node.name != '__SVN_ROOT_NODE':
2120 raise NodeTreeWalkException()
2121 path = path.strip('/')
2122 components = path.split('/')
2123 for component in components:
2124 node = get_child(node, component)
2125 return node
2127 # Helper for tests involving properties.
2128 def props_for_path(node, path):
2129 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2130 return node_for_path(node, path).props
2133 class EOLMime(Cvs2SvnPropertiesTestCase):
2134 """eol settings and mime types together
2136 The files are as follows:
2138 trunk/foo.txt: no -kb, mime file says nothing.
2139 trunk/foo.xml: no -kb, mime file says text.
2140 trunk/foo.zip: no -kb, mime file says non-text.
2141 trunk/foo.bin: has -kb, mime file says nothing.
2142 trunk/foo.csv: has -kb, mime file says text.
2143 trunk/foo.dbf: has -kb, mime file says non-text.
2146 def __init__(self, args, **kw):
2147 # TODO: It's a bit klugey to construct this path here. But so far
2148 # there's only one test with a mime.types file. If we have more,
2149 # we should abstract this into some helper, which would be located
2150 # near ensure_conversion(). Note that it is a convention of this
2151 # test suite for a mime.types file to be located in the top level
2152 # of the CVS repository to which it applies.
2153 self.mime_path = os.path.join(
2154 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2156 Cvs2SvnPropertiesTestCase.__init__(
2157 self, 'eol-mime',
2158 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2159 args=['--mime-types=%s' % self.mime_path] + args,
2160 **kw)
2163 # We do four conversions. Each time, we pass --mime-types=FILE with
2164 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2165 # Thus there's one conversion with neither flag, one with just the
2166 # former, one with just the latter, and one with both.
2169 # Neither --no-default-eol nor --eol-from-mime-type:
2170 eol_mime1 = EOLMime(
2171 variant=1,
2172 args=[],
2173 expected_props=[
2174 ('trunk/foo.txt', [None, None, None]),
2175 ('trunk/foo.xml', [None, 'text/xml', None]),
2176 ('trunk/foo.zip', [None, 'application/zip', None]),
2177 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2178 ('trunk/foo.csv', [None, 'text/csv', None]),
2179 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2183 # Just --no-default-eol, not --eol-from-mime-type:
2184 eol_mime2 = EOLMime(
2185 variant=2,
2186 args=['--default-eol=native'],
2187 expected_props=[
2188 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2189 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2190 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2191 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2192 ('trunk/foo.csv', [None, 'text/csv', None]),
2193 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2197 # Just --eol-from-mime-type, not --no-default-eol:
2198 eol_mime3 = EOLMime(
2199 variant=3,
2200 args=['--eol-from-mime-type'],
2201 expected_props=[
2202 ('trunk/foo.txt', [None, None, None]),
2203 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2204 ('trunk/foo.zip', [None, 'application/zip', None]),
2205 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2206 ('trunk/foo.csv', [None, 'text/csv', None]),
2207 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2211 # Both --no-default-eol and --eol-from-mime-type:
2212 eol_mime4 = EOLMime(
2213 variant=4,
2214 args=['--eol-from-mime-type', '--default-eol=native'],
2215 expected_props=[
2216 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2217 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2218 ('trunk/foo.zip', [None, 'application/zip', None]),
2219 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2220 ('trunk/foo.csv', [None, 'text/csv', None]),
2221 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2225 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2226 'eol-mime',
2227 description='test non-setting of cvs2svn:cvs-rev property',
2228 args=[],
2229 props_to_test=['cvs2svn:cvs-rev'],
2230 expected_props=[
2231 ('trunk/foo.txt', [None]),
2232 ('trunk/foo.xml', [None]),
2233 ('trunk/foo.zip', [None]),
2234 ('trunk/foo.bin', [None]),
2235 ('trunk/foo.csv', [None]),
2236 ('trunk/foo.dbf', [None]),
2240 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2241 'eol-mime',
2242 description='test setting of cvs2svn:cvs-rev property',
2243 args=['--cvs-revnums'],
2244 props_to_test=['cvs2svn:cvs-rev'],
2245 expected_props=[
2246 ('trunk/foo.txt', ['1.2']),
2247 ('trunk/foo.xml', ['1.2']),
2248 ('trunk/foo.zip', ['1.2']),
2249 ('trunk/foo.bin', ['1.2']),
2250 ('trunk/foo.csv', ['1.2']),
2251 ('trunk/foo.dbf', ['1.2']),
2255 keywords = Cvs2SvnPropertiesTestCase(
2256 'keywords',
2257 description='test setting of svn:keywords property among others',
2258 args=['--default-eol=native'],
2259 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2260 expected_props=[
2261 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2262 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2263 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2264 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2265 ('trunk/foo.kk', [None, 'native', None]),
2266 ('trunk/foo.ko', [None, 'native', None]),
2267 ('trunk/foo.kv', [None, 'native', None]),
2271 def ignore():
2272 "test setting of svn:ignore property"
2273 conv = ensure_conversion('cvsignore')
2274 wc_tree = conv.get_wc_tree()
2275 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2276 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2278 if topdir_props['svn:ignore'] != \
2279 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2280 raise Failure()
2282 if subdir_props['svn:ignore'] != \
2283 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2284 raise Failure()
2287 def requires_cvs():
2288 "test that CVS can still do what RCS can't"
2289 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2290 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2292 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2293 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2295 if atsign_contents[-1:] == "@":
2296 raise Failure()
2297 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2298 raise Failure()
2300 if not (conv.logs[21].author == "William Lyon Phelps III" and
2301 conv.logs[20].author == "j random"):
2302 raise Failure()
2305 def questionable_branch_names():
2306 "test that we can handle weird branch names"
2307 conv = ensure_conversion('questionable-symbols')
2308 # If the conversion succeeds, then we're okay. We could check the
2309 # actual branch paths, too, but the main thing is to know that the
2310 # conversion doesn't fail.
2313 def questionable_tag_names():
2314 "test that we can handle weird tag names"
2315 conv = ensure_conversion('questionable-symbols')
2316 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2317 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2319 conv.find_tag_log('TagWith/Backslash_E').check(
2320 sym_log_msg('TagWith/Backslash_E',1),
2322 ('/%(tags)s/TagWith', 'A'),
2323 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2326 conv.find_tag_log('TagWith/Slash_Z').check(
2327 sym_log_msg('TagWith/Slash_Z',1),
2329 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2334 def revision_reorder_bug():
2335 "reveal a bug that reorders file revisions"
2336 conv = ensure_conversion('revision-reorder-bug')
2337 # If the conversion succeeds, then we're okay. We could check the
2338 # actual revisions, too, but the main thing is to know that the
2339 # conversion doesn't fail.
2342 def exclude():
2343 "test that exclude really excludes everything"
2344 conv = ensure_conversion('main', args=['--exclude=.*'])
2345 for log in conv.logs.values():
2346 for item in log.changed_paths.keys():
2347 if item.startswith('/branches/') or item.startswith('/tags/'):
2348 raise Failure()
2351 def vendor_branch_delete_add():
2352 "add trunk file that was deleted on vendor branch"
2353 # This will error if the bug is present
2354 conv = ensure_conversion('vendor-branch-delete-add')
2357 def resync_pass2_pull_forward():
2358 "ensure pass2 doesn't pull rev too far forward"
2359 conv = ensure_conversion('resync-pass2-pull-forward')
2360 # If the conversion succeeds, then we're okay. We could check the
2361 # actual revisions, too, but the main thing is to know that the
2362 # conversion doesn't fail.
2365 def native_eol():
2366 "only LFs for svn:eol-style=native files"
2367 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2368 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2369 conv.repos)
2370 # Verify that all files in the dump have LF EOLs. We're actually
2371 # testing the whole dump file, but the dump file itself only uses
2372 # LF EOLs, so we're safe.
2373 for line in lines:
2374 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2375 raise Failure()
2378 def double_fill():
2379 "reveal a bug that created a branch twice"
2380 conv = ensure_conversion('double-fill')
2381 # If the conversion succeeds, then we're okay. We could check the
2382 # actual revisions, too, but the main thing is to know that the
2383 # conversion doesn't fail.
2386 def double_fill2():
2387 "reveal a second bug that created a branch twice"
2388 conv = ensure_conversion('double-fill2')
2389 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2390 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2391 try:
2392 # This check should fail:
2393 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2394 except Failure:
2395 pass
2396 else:
2397 raise Failure('Symbol filled twice in a row')
2400 def resync_pass2_push_backward():
2401 "ensure pass2 doesn't push rev too far backward"
2402 conv = ensure_conversion('resync-pass2-push-backward')
2403 # If the conversion succeeds, then we're okay. We could check the
2404 # actual revisions, too, but the main thing is to know that the
2405 # conversion doesn't fail.
2408 def double_add():
2409 "reveal a bug that added a branch file twice"
2410 conv = ensure_conversion('double-add')
2411 # If the conversion succeeds, then we're okay. We could check the
2412 # actual revisions, too, but the main thing is to know that the
2413 # conversion doesn't fail.
2416 def bogus_branch_copy():
2417 "reveal a bug that copies a branch file wrongly"
2418 conv = ensure_conversion('bogus-branch-copy')
2419 # If the conversion succeeds, then we're okay. We could check the
2420 # actual revisions, too, but the main thing is to know that the
2421 # conversion doesn't fail.
2424 def nested_ttb_directories():
2425 "require error if ttb directories are not disjoint"
2426 opts_list = [
2427 {'trunk' : 'a', 'branches' : 'a',},
2428 {'trunk' : 'a', 'tags' : 'a',},
2429 {'branches' : 'a', 'tags' : 'a',},
2430 # This option conflicts with the default trunk path:
2431 {'branches' : 'trunk',},
2432 # Try some nested directories:
2433 {'trunk' : 'a', 'branches' : 'a/b',},
2434 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2435 {'branches' : 'a', 'tags' : 'a/b',},
2438 for opts in opts_list:
2439 ensure_conversion(
2440 'main', error_re=r'The following paths are not disjoint\:', **opts
2444 class AutoProps(Cvs2SvnPropertiesTestCase):
2445 """Test auto-props.
2447 The files are as follows:
2449 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2450 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2451 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2452 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2453 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2454 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2455 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2456 trunk/foo.UPCASE1: no -kb, no mime type.
2457 trunk/foo.UPCASE2: no -kb, no mime type.
2460 def __init__(self, args, **kw):
2461 ### TODO: It's a bit klugey to construct this path here. See also
2462 ### the comment in eol_mime().
2463 auto_props_path = os.path.join(
2464 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2466 Cvs2SvnPropertiesTestCase.__init__(
2467 self, 'eol-mime',
2468 props_to_test=[
2469 'myprop',
2470 'svn:eol-style',
2471 'svn:mime-type',
2472 'svn:keywords',
2473 'svn:executable',
2475 args=[
2476 '--auto-props=%s' % auto_props_path,
2477 '--eol-from-mime-type'
2478 ] + args,
2479 **kw)
2482 auto_props_ignore_case = AutoProps(
2483 description="test auto-props",
2484 args=['--default-eol=native'],
2485 expected_props=[
2486 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2487 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2488 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2489 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2490 ('trunk/foo.bin',
2491 ['bin', None, 'application/octet-stream', None, '']),
2492 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2493 ('trunk/foo.dbf',
2494 ['dbf', None, 'application/what-is-dbf', None, None]),
2495 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2496 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2500 def ctrl_char_in_filename():
2501 "do not allow control characters in filenames"
2503 try:
2504 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2505 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2506 if os.path.exists(dstrepos_path):
2507 safe_rmtree(dstrepos_path)
2509 # create repos from existing main repos
2510 shutil.copytree(srcrepos_path, dstrepos_path)
2511 base_path = os.path.join(dstrepos_path, 'single-files')
2512 try:
2513 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2514 os.path.join(base_path, 'two\rquick,v'))
2515 except:
2516 # Operating systems that don't allow control characters in
2517 # filenames will hopefully have thrown an exception; in that
2518 # case, just skip this test.
2519 raise svntest.Skip()
2521 conv = ensure_conversion(
2522 'ctrl-char-filename',
2523 error_re=(r'.*Character .* in filename .* '
2524 r'is not supported by Subversion\.'),
2526 finally:
2527 safe_rmtree(dstrepos_path)
2530 def commit_dependencies():
2531 "interleaved and multi-branch commits to same files"
2532 conv = ensure_conversion("commit-dependencies")
2533 conv.logs[2].check('adding', (
2534 ('/%(trunk)s/interleaved', 'A'),
2535 ('/%(trunk)s/interleaved/file1', 'A'),
2536 ('/%(trunk)s/interleaved/file2', 'A'),
2538 conv.logs[3].check('big commit', (
2539 ('/%(trunk)s/interleaved/file1', 'M'),
2540 ('/%(trunk)s/interleaved/file2', 'M'),
2542 conv.logs[4].check('dependant small commit', (
2543 ('/%(trunk)s/interleaved/file1', 'M'),
2545 conv.logs[5].check('adding', (
2546 ('/%(trunk)s/multi-branch', 'A'),
2547 ('/%(trunk)s/multi-branch/file1', 'A'),
2548 ('/%(trunk)s/multi-branch/file2', 'A'),
2550 conv.logs[6].check(sym_log_msg("branch"), (
2551 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2552 ('/%(branches)s/branch/interleaved', 'D'),
2554 conv.logs[7].check('multi-branch-commit', (
2555 ('/%(trunk)s/multi-branch/file1', 'M'),
2556 ('/%(trunk)s/multi-branch/file2', 'M'),
2557 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2558 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2562 def double_branch_delete():
2563 "fill branches before modifying files on them"
2564 conv = ensure_conversion('double-branch-delete')
2566 # Test for issue #102. The file IMarshalledValue.java is branched,
2567 # deleted, readded on the branch, and then deleted again. If the
2568 # fill for the file on the branch is postponed until after the
2569 # modification, the file will end up live on the branch instead of
2570 # dead! Make sure it happens at the right time.
2572 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2573 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2576 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2577 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2581 def symbol_mismatches():
2582 "error for conflicting tag/branch"
2584 ensure_conversion(
2585 'symbol-mess',
2586 args=['--symbol-default=strict'],
2587 error_re=r'.*Problems determining how symbols should be converted',
2591 def overlook_symbol_mismatches():
2592 "overlook conflicting tag/branch when --trunk-only"
2594 # This is a test for issue #85.
2596 ensure_conversion('symbol-mess', args=['--trunk-only'])
2599 def force_symbols():
2600 "force symbols to be tags/branches"
2602 conv = ensure_conversion(
2603 'symbol-mess',
2604 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2605 if conv.path_exists('tags', 'BRANCH') \
2606 or not conv.path_exists('branches', 'BRANCH'):
2607 raise Failure()
2608 if not conv.path_exists('tags', 'TAG') \
2609 or conv.path_exists('branches', 'TAG'):
2610 raise Failure()
2611 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2612 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2613 raise Failure()
2614 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2615 or conv.path_exists('branches', 'MOSTLY_TAG'):
2616 raise Failure()
2619 def commit_blocks_tags():
2620 "commit prevents forced tag"
2622 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2623 ensure_conversion(
2624 'symbol-mess',
2625 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2626 error_re=(
2627 r'.*The following branches cannot be forced to be tags '
2628 r'because they have commits'
2633 def blocked_excludes():
2634 "error for blocked excludes"
2636 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2637 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2638 try:
2639 ensure_conversion(
2640 'symbol-mess',
2641 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2642 raise MissingErrorException()
2643 except Failure:
2644 pass
2647 def unblock_blocked_excludes():
2648 "excluding blocker removes blockage"
2650 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2651 for blocker in ['BRANCH', 'COMMIT']:
2652 ensure_conversion(
2653 'symbol-mess',
2654 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2655 '--exclude=BLOCKING_%s' % blocker]))
2658 def regexp_force_symbols():
2659 "force symbols via regular expressions"
2661 conv = ensure_conversion(
2662 'symbol-mess',
2663 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2664 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2665 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2666 raise Failure()
2667 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2668 or conv.path_exists('branches', 'MOSTLY_TAG'):
2669 raise Failure()
2672 def heuristic_symbol_default():
2673 "test 'heuristic' symbol default"
2675 conv = ensure_conversion(
2676 'symbol-mess', args=['--symbol-default=heuristic'])
2677 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2678 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2679 raise Failure()
2680 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2681 or conv.path_exists('branches', 'MOSTLY_TAG'):
2682 raise Failure()
2685 def branch_symbol_default():
2686 "test 'branch' symbol default"
2688 conv = ensure_conversion(
2689 'symbol-mess', args=['--symbol-default=branch'])
2690 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2691 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2692 raise Failure()
2693 if conv.path_exists('tags', 'MOSTLY_TAG') \
2694 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2695 raise Failure()
2698 def tag_symbol_default():
2699 "test 'tag' symbol default"
2701 conv = ensure_conversion(
2702 'symbol-mess', args=['--symbol-default=tag'])
2703 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2704 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2705 raise Failure()
2706 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2707 or conv.path_exists('branches', 'MOSTLY_TAG'):
2708 raise Failure()
2711 def symbol_transform():
2712 "test --symbol-transform"
2714 conv = ensure_conversion(
2715 'symbol-mess',
2716 args=[
2717 '--symbol-default=heuristic',
2718 '--symbol-transform=BRANCH:branch',
2719 '--symbol-transform=TAG:tag',
2720 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2722 if not conv.path_exists('branches', 'branch'):
2723 raise Failure()
2724 if not conv.path_exists('tags', 'tag'):
2725 raise Failure()
2726 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2727 raise Failure()
2728 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2729 raise Failure()
2732 def write_symbol_info():
2733 "test --write-symbol-info"
2735 expected_lines = [
2736 ['0', '.trunk.',
2737 'trunk', 'trunk', '.'],
2738 ['0', 'BLOCKED_BY_UNNAMED',
2739 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2740 ['0', 'BLOCKING_COMMIT',
2741 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2742 ['0', 'BLOCKED_BY_COMMIT',
2743 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2744 ['0', 'BLOCKING_BRANCH',
2745 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2746 ['0', 'BLOCKED_BY_BRANCH',
2747 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2748 ['0', 'MOSTLY_BRANCH',
2749 '.', '.', '.'],
2750 ['0', 'MOSTLY_TAG',
2751 '.', '.', '.'],
2752 ['0', 'BRANCH_WITH_COMMIT',
2753 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2754 ['0', 'BRANCH',
2755 'branch', 'branches/BRANCH', '.trunk.'],
2756 ['0', 'TAG',
2757 'tag', 'tags/TAG', '.trunk.'],
2758 ['0', 'unlabeled-1.1.12.1.2',
2759 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2761 expected_lines.sort()
2763 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2764 try:
2765 ensure_conversion(
2766 'symbol-mess',
2767 args=[
2768 '--symbol-default=strict',
2769 '--write-symbol-info=%s' % (symbol_info_file,),
2770 '--passes=:CollateSymbolsPass',
2773 raise MissingErrorException()
2774 except Failure:
2775 pass
2776 lines = []
2777 comment_re = re.compile(r'^\s*\#')
2778 for l in open(symbol_info_file, 'r'):
2779 if comment_re.match(l):
2780 continue
2781 lines.append(l.strip().split())
2782 lines.sort()
2783 if lines != expected_lines:
2784 s = ['Symbol info incorrect\n']
2785 differ = Differ()
2786 for diffline in differ.compare(
2787 [' '.join(line) + '\n' for line in expected_lines],
2788 [' '.join(line) + '\n' for line in lines],
2790 s.append(diffline)
2791 raise Failure(''.join(s))
2794 def symbol_hints():
2795 "test --symbol-hints for setting branch/tag"
2797 conv = ensure_conversion(
2798 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2800 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2801 raise Failure()
2802 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2803 raise Failure()
2804 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2805 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2807 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2808 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2810 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2811 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2815 def parent_hints():
2816 "test --symbol-hints for setting parent"
2818 conv = ensure_conversion(
2819 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2821 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2822 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2826 def parent_hints_invalid():
2827 "test --symbol-hints with an invalid parent"
2829 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2830 # this symbol hints file sets the preferred parent to BRANCH
2831 # instead:
2832 conv = ensure_conversion(
2833 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2834 error_re=(
2835 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2840 def parent_hints_wildcards():
2841 "test --symbol-hints wildcards"
2843 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2844 # this symbol hints file sets the preferred parent to BRANCH
2845 # instead:
2846 conv = ensure_conversion(
2847 'symbol-mess',
2848 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2850 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2851 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2855 def path_hints():
2856 "test --symbol-hints for setting svn paths"
2858 conv = ensure_conversion(
2859 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
2861 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2862 ('/trunk', 'A'),
2863 ('/a', 'A'),
2864 ('/a/strange', 'A'),
2865 ('/a/strange/trunk', 'A'),
2866 ('/a/strange/trunk/path', 'A'),
2867 ('/branches', 'A'),
2868 ('/tags', 'A'),
2870 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2871 ('/special', 'A'),
2872 ('/special/tag', 'A'),
2873 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
2875 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2876 ('/special/other', 'A'),
2877 ('/special/other/branch', 'A'),
2878 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
2880 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2881 ('/special/branch', 'A'),
2882 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
2886 def issue_99():
2887 "test problem from issue 99"
2889 conv = ensure_conversion('issue-99')
2892 def issue_100():
2893 "test problem from issue 100"
2895 conv = ensure_conversion('issue-100')
2896 file1 = conv.get_wc('trunk', 'file1.txt')
2897 if file(file1).read() != 'file1.txt<1.2>\n':
2898 raise Failure()
2901 def issue_106():
2902 "test problem from issue 106"
2904 conv = ensure_conversion('issue-106')
2907 def options_option():
2908 "use of the --options option"
2910 conv = ensure_conversion('main', options_file='cvs2svn.options')
2913 def multiproject():
2914 "multiproject conversion"
2916 conv = ensure_conversion(
2917 'main', options_file='cvs2svn-multiproject.options'
2919 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2920 ('/partial-prune', 'A'),
2921 ('/partial-prune/trunk', 'A'),
2922 ('/partial-prune/branches', 'A'),
2923 ('/partial-prune/tags', 'A'),
2924 ('/partial-prune/releases', 'A'),
2928 def crossproject():
2929 "multiproject conversion with cross-project commits"
2931 conv = ensure_conversion(
2932 'main', options_file='cvs2svn-crossproject.options'
2936 def tag_with_no_revision():
2937 "tag defined but revision is deleted"
2939 conv = ensure_conversion('tag-with-no-revision')
2942 def delete_cvsignore():
2943 "svn:ignore should vanish when .cvsignore does"
2945 # This is issue #81.
2947 conv = ensure_conversion('delete-cvsignore')
2949 wc_tree = conv.get_wc_tree()
2950 props = props_for_path(wc_tree, 'trunk/proj')
2952 if props.has_key('svn:ignore'):
2953 raise Failure()
2956 def repeated_deltatext():
2957 "ignore repeated deltatext blocks with warning"
2959 conv = ensure_conversion('repeated-deltatext')
2960 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
2961 if not conv.output_found(warning_re):
2962 raise Failure()
2965 def nasty_graphs():
2966 "process some nasty dependency graphs"
2968 # It's not how well the bear can dance, but that the bear can dance
2969 # at all:
2970 conv = ensure_conversion('nasty-graphs')
2973 def tagging_after_delete():
2974 "optimal tag after deleting files"
2976 conv = ensure_conversion('tagging-after-delete')
2978 # tag should be 'clean', no deletes
2979 log = conv.find_tag_log('tag1')
2980 expected = (
2981 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
2983 log.check_changes(expected)
2986 def crossed_branches():
2987 "branches created in inconsistent orders"
2989 conv = ensure_conversion('crossed-branches')
2992 def file_directory_conflict():
2993 "error when filename conflicts with directory name"
2995 conv = ensure_conversion(
2996 'file-directory-conflict',
2997 error_re=r'.*Directory name conflicts with filename',
3001 def attic_directory_conflict():
3002 "error when attic filename conflicts with dirname"
3004 # This tests the problem reported in issue #105.
3006 conv = ensure_conversion(
3007 'attic-directory-conflict',
3008 error_re=r'.*Directory name conflicts with filename',
3012 def internal_co():
3013 "verify that --use-internal-co works"
3015 rcs_conv = ensure_conversion(
3016 'main', args=['--use-rcs', '--default-eol=native'],
3018 conv = ensure_conversion(
3019 'main', args=['--default-eol=native'],
3021 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3022 raise Failure()
3023 rcs_lines = run_program(
3024 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3025 rcs_conv.repos)
3026 lines = run_program(
3027 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3028 conv.repos)
3029 # Compare all lines following the repository UUID:
3030 if lines[3:] != rcs_lines[3:]:
3031 raise Failure()
3034 def internal_co_exclude():
3035 "verify that --use-internal-co --exclude=... works"
3037 rcs_conv = ensure_conversion(
3038 'internal-co',
3039 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3041 conv = ensure_conversion(
3042 'internal-co',
3043 args=['--exclude=BRANCH', '--default-eol=native'],
3045 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3046 raise Failure()
3047 rcs_lines = run_program(
3048 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3049 rcs_conv.repos)
3050 lines = run_program(
3051 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3052 conv.repos)
3053 # Compare all lines following the repository UUID:
3054 if lines[3:] != rcs_lines[3:]:
3055 raise Failure()
3058 def internal_co_trunk_only():
3059 "verify that --use-internal-co --trunk-only works"
3061 conv = ensure_conversion(
3062 'internal-co',
3063 args=['--trunk-only', '--default-eol=native'],
3065 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3066 raise Failure()
3069 def leftover_revs():
3070 "check for leftover checked-out revisions"
3072 conv = ensure_conversion(
3073 'leftover-revs',
3074 args=['--exclude=BRANCH', '--default-eol=native'],
3076 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3077 raise Failure()
3080 def requires_internal_co():
3081 "test that internal co can do more than RCS"
3082 # See issues 4, 11 for the bugs whose regression we're testing for.
3083 # Unlike in requires_cvs above, issue 29 is not covered.
3084 conv = ensure_conversion('requires-cvs')
3086 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3088 if atsign_contents[-1:] == "@":
3089 raise Failure()
3091 if not (conv.logs[21].author == "William Lyon Phelps III" and
3092 conv.logs[20].author == "j random"):
3093 raise Failure()
3096 def internal_co_keywords():
3097 "test that internal co handles keywords correctly"
3098 conv_ic = ensure_conversion('internal-co-keywords',
3099 args=["--keywords-off"])
3100 conv_cvs = ensure_conversion('internal-co-keywords',
3101 args=["--use-cvs", "--keywords-off"])
3103 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3104 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3105 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3106 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3107 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3108 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3110 if ko_ic != ko_cvs:
3111 raise Failure()
3112 if kk_ic != kk_cvs:
3113 raise Failure()
3115 # The date format changed between cvs and co ('/' instead of '-').
3116 # Accept either one:
3117 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3118 if kv_ic != kv_cvs \
3119 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3120 raise Failure()
3123 def timestamp_chaos():
3124 "test timestamp adjustments"
3126 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3128 times = [
3129 '2007-01-01 21:00:00', # Initial commit
3130 '2007-01-01 21:00:00', # revision 1.1 of both files
3131 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3132 '2007-01-01 21:00:02', # revision 1.2 of file1.txt, adjusted backwards
3133 '2007-01-01 22:00:00', # revision 1.3 of both files
3135 for i in range(len(times)):
3136 if abs(conv.logs[i + 1].date - time.mktime(svn_strptime(times[i]))) > 0.1:
3137 raise Failure()
3140 def symlinks():
3141 "convert a repository that contains symlinks"
3143 # This is a test for issue #97.
3145 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3146 links = [
3148 os.path.join('..', 'file.txt,v'),
3149 os.path.join(proj, 'dir1', 'file.txt,v'),
3152 'dir1',
3153 os.path.join(proj, 'dir2'),
3157 try:
3158 os.symlink
3159 except AttributeError:
3160 # Apparently this OS doesn't support symlinks, so skip test.
3161 raise svntest.Skip()
3163 try:
3164 for (src,dst) in links:
3165 os.symlink(src, dst)
3167 conv = ensure_conversion('symlinks')
3168 conv.logs[2].check('', (
3169 ('/%(trunk)s/proj', 'A'),
3170 ('/%(trunk)s/proj/file.txt', 'A'),
3171 ('/%(trunk)s/proj/dir1', 'A'),
3172 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3173 ('/%(trunk)s/proj/dir2', 'A'),
3174 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3176 finally:
3177 for (src,dst) in links:
3178 os.remove(dst)
3181 def empty_trunk_path():
3182 "allow --trunk to be empty if --trunk-only"
3184 # This is a test for issue #53.
3186 conv = ensure_conversion(
3187 'main', args=['--trunk-only', '--trunk='],
3191 def preferred_parent_cycle():
3192 "handle a cycle in branch parent preferences"
3194 conv = ensure_conversion('preferred-parent-cycle')
3197 def branch_from_empty_dir():
3198 "branch from an empty directory"
3200 conv = ensure_conversion('branch-from-empty-dir')
3203 def trunk_readd():
3204 "add a file on a branch then on trunk"
3206 conv = ensure_conversion('trunk-readd')
3209 def branch_from_deleted_1_1():
3210 "branch from a 1.1 revision that will be deleted"
3212 conv = ensure_conversion('branch-from-deleted-1-1')
3213 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3214 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3216 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3217 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3219 conv.logs[7].check('Adding b.txt:1.2', (
3220 ('/%(trunk)s/proj/b.txt', 'A'),
3223 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3224 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3226 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3227 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3231 def add_on_branch():
3232 "add a file on a branch using newer CVS"
3234 conv = ensure_conversion('add-on-branch')
3235 conv.logs[6].check('Adding b.txt:1.1', (
3236 ('/%(trunk)s/proj/b.txt', 'A'),
3238 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3239 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3241 conv.logs[8].check('Adding c.txt:1.1', (
3242 ('/%(trunk)s/proj/c.txt', 'A'),
3244 conv.logs[9].check('Removing c.txt:1.2', (
3245 ('/%(trunk)s/proj/c.txt', 'D'),
3247 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3248 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3250 conv.logs[11].check('Adding d.txt:1.1', (
3251 ('/%(trunk)s/proj/d.txt', 'A'),
3253 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3254 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3258 def main_git():
3259 "test output in git-fast-import format"
3261 # Note: To test importing into git, do
3263 # ./run-tests <test-number>
3264 # rm -rf .git
3265 # git-init
3266 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3268 # Or, to load the dumpfiles separately:
3270 # cat cvs2svn-tmp/git-blob.dat \
3271 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3272 # cat cvs2svn-tmp/git-dump.dat \
3273 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3275 # Then use "gitk --all", "git log", etc. to test the contents of the
3276 # repository.
3278 # We don't have the infrastructure to check that the resulting git
3279 # repository is correct, so we just check that the conversion runs
3280 # to completion:
3281 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3284 def main_hg():
3285 "output in git-fast-import format with inline data"
3287 # The output should be suitable for import by Mercurial.
3289 # Note: To test importing into git, do
3291 # ./run-tests <test-number>
3292 # rm -rf .git
3293 # git-init
3294 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3296 # Then use "gitk --all", "git log", etc. to test the contents of the
3297 # repository.
3299 # We don't have the infrastructure to check that the resulting
3300 # Mercurial repository is correct, so we just check that the
3301 # conversion runs to completion:
3302 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3305 def invalid_symbol():
3306 "a symbol with the incorrect format"
3308 conv = ensure_conversion('invalid-symbol')
3309 if not conv.output_found(
3310 r".*branch 'SYMBOL' references invalid revision 1$"
3312 raise Failure()
3315 def invalid_symbol_ignore():
3316 "ignore a symbol with the incorrect format"
3318 conv = ensure_conversion(
3319 'invalid-symbol', options_file='cvs2svn-ignore.options'
3323 class EOLVariants(Cvs2SvnTestCase):
3324 "handle various --eol-style options"
3326 eol_style_strings = {
3327 'LF' : '\n',
3328 'CR' : '\r',
3329 'CRLF' : '\r\n',
3330 'native' : '\n',
3333 def __init__(self, eol_style):
3334 self.eol_style = eol_style
3335 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3336 Cvs2SvnTestCase.__init__(
3337 self, 'eol-variants', variant=self.eol_style,
3338 dumpfile=self.dumpfile,
3339 args=[
3340 '--default-eol=%s' % (self.eol_style,),
3344 def run(self):
3345 conv = self.ensure_conversion()
3346 dump_contents = open(conv.dumpfile, 'rb').read()
3347 expected_text = self.eol_style_strings[self.eol_style].join(
3348 ['line 1', 'line 2', '\n\n']
3350 if not dump_contents.endswith(expected_text):
3351 raise Failure()
3354 def no_revs_file():
3355 "handle a file with no revisions (issue #80)"
3357 conv = ensure_conversion('no-revs-file')
3360 def mirror_keyerror_test():
3361 "a case that gave KeyError in SVNRepositoryMirror"
3363 conv = ensure_conversion('mirror-keyerror')
3366 def exclude_ntdb_test():
3367 "exclude a non-trunk default branch"
3369 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3370 conv = ensure_conversion(
3371 'exclude-ntdb',
3372 args=[
3373 '--write-symbol-info=%s' % (symbol_info_file,),
3374 '--exclude=branch3',
3375 '--exclude=tag3',
3376 '--exclude=vendortag3',
3377 '--exclude=vendorbranch',
3382 def mirror_keyerror2_test():
3383 "a case that gave KeyError in RepositoryMirror"
3385 conv = ensure_conversion('mirror-keyerror2')
3388 def mirror_keyerror3_test():
3389 "a case that gave KeyError in RepositoryMirror"
3391 conv = ensure_conversion('mirror-keyerror3')
3394 def add_cvsignore_to_branch_test():
3395 "check adding .cvsignore to an existing branch"
3397 # This a test for issue #122.
3399 conv = ensure_conversion('add-cvsignore-to-branch')
3400 wc_tree = conv.get_wc_tree()
3401 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3402 if trunk_props['svn:ignore'] != '*.o\n\n':
3403 raise Failure()
3405 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3406 if branch_props['svn:ignore'] != '*.o\n\n':
3407 raise Failure()
3410 ########################################################################
3411 # Run the tests
3413 # list all tests here, starting with None:
3414 test_list = [
3415 None,
3416 # 1:
3417 show_usage,
3418 attr_exec,
3419 space_fname,
3420 two_quick,
3421 PruneWithCare(),
3422 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3423 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3424 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3425 interleaved_commits,
3426 # 10:
3427 simple_commits,
3428 SimpleTags(),
3429 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3430 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3431 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3432 simple_branch_commits,
3433 mixed_time_tag,
3434 mixed_time_branch_with_added_file,
3435 mixed_commit,
3436 split_time_branch,
3437 # 20:
3438 bogus_tag,
3439 overlapping_branch,
3440 PhoenixBranch(),
3441 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3442 ctrl_char_in_log,
3443 overdead,
3444 NoTrunkPrune(),
3445 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3446 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3447 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3448 # 30:
3449 double_delete,
3450 split_branch,
3451 resync_misgroups,
3452 TaggedBranchAndTrunk(),
3453 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3454 enroot_race,
3455 enroot_race_obo,
3456 BranchDeleteFirst(),
3457 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3458 nonascii_filenames,
3459 # 40:
3460 UnicodeAuthor(
3461 warning_expected=1),
3462 UnicodeAuthor(
3463 warning_expected=0,
3464 variant='encoding', args=['--encoding=utf_8']),
3465 UnicodeAuthor(
3466 warning_expected=0,
3467 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3468 UnicodeLog(
3469 warning_expected=1),
3470 UnicodeLog(
3471 warning_expected=0,
3472 variant='encoding', args=['--encoding=utf_8']),
3473 UnicodeLog(
3474 warning_expected=0,
3475 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3476 vendor_branch_sameness,
3477 vendor_branch_trunk_only,
3478 default_branches,
3479 default_branches_trunk_only,
3480 # 50:
3481 default_branch_and_1_2,
3482 compose_tag_three_sources,
3483 pass5_when_to_fill,
3484 PeerPathPruning(),
3485 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3486 EmptyTrunk(),
3487 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3488 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3489 no_spurious_svn_commits,
3490 invalid_closings_on_trunk,
3491 # 60:
3492 individual_passes,
3493 resync_bug,
3494 branch_from_default_branch,
3495 file_in_attic_too,
3496 retain_file_in_attic_too,
3497 symbolic_name_filling_guide,
3498 eol_mime1,
3499 eol_mime2,
3500 eol_mime3,
3501 eol_mime4,
3502 # 70:
3503 cvs_revnums_off,
3504 cvs_revnums_on,
3505 keywords,
3506 ignore,
3507 requires_cvs,
3508 questionable_branch_names,
3509 questionable_tag_names,
3510 revision_reorder_bug,
3511 exclude,
3512 vendor_branch_delete_add,
3513 # 80:
3514 resync_pass2_pull_forward,
3515 native_eol,
3516 double_fill,
3517 XFail(double_fill2),
3518 resync_pass2_push_backward,
3519 double_add,
3520 bogus_branch_copy,
3521 nested_ttb_directories,
3522 auto_props_ignore_case,
3523 ctrl_char_in_filename,
3524 # 90:
3525 commit_dependencies,
3526 show_help_passes,
3527 multiple_tags,
3528 multiply_defined_symbols,
3529 multiply_defined_symbols_renamed,
3530 multiply_defined_symbols_ignored,
3531 repeatedly_defined_symbols,
3532 double_branch_delete,
3533 symbol_mismatches,
3534 overlook_symbol_mismatches,
3535 # 100:
3536 force_symbols,
3537 commit_blocks_tags,
3538 blocked_excludes,
3539 unblock_blocked_excludes,
3540 regexp_force_symbols,
3541 heuristic_symbol_default,
3542 branch_symbol_default,
3543 tag_symbol_default,
3544 symbol_transform,
3545 write_symbol_info,
3546 # 110:
3547 symbol_hints,
3548 parent_hints,
3549 parent_hints_invalid,
3550 parent_hints_wildcards,
3551 path_hints,
3552 issue_99,
3553 issue_100,
3554 issue_106,
3555 options_option,
3556 multiproject,
3557 # 120:
3558 crossproject,
3559 tag_with_no_revision,
3560 delete_cvsignore,
3561 repeated_deltatext,
3562 nasty_graphs,
3563 XFail(tagging_after_delete),
3564 crossed_branches,
3565 file_directory_conflict,
3566 attic_directory_conflict,
3567 internal_co,
3568 # 130:
3569 internal_co_exclude,
3570 internal_co_trunk_only,
3571 internal_co_keywords,
3572 leftover_revs,
3573 requires_internal_co,
3574 timestamp_chaos,
3575 symlinks,
3576 empty_trunk_path,
3577 preferred_parent_cycle,
3578 branch_from_empty_dir,
3579 # 140:
3580 trunk_readd,
3581 branch_from_deleted_1_1,
3582 add_on_branch,
3583 main_git,
3584 main_hg,
3585 invalid_symbol,
3586 invalid_symbol_ignore,
3587 EOLVariants('LF'),
3588 EOLVariants('CR'),
3589 EOLVariants('CRLF'),
3590 # 150:
3591 EOLVariants('native'),
3592 no_revs_file,
3593 mirror_keyerror_test,
3594 exclude_ntdb_test,
3595 mirror_keyerror2_test,
3596 mirror_keyerror3_test,
3597 XFail(add_cvsignore_to_branch_test),
3600 if __name__ == '__main__':
3602 # Configure the environment for reproducable output from svn, etc.
3603 # I have no idea if this works on Windows too.
3604 os.environ["LC_ALL"] = "C"
3605 os.environ["TZ"] = "UTC"
3607 # The Subversion test suite code assumes it's being invoked from
3608 # within a working copy of the Subversion sources, and tries to use
3609 # the binaries in that tree. Since the cvs2svn tree never contains
3610 # a Subversion build, we just use the system's installed binaries.
3611 svntest.main.svn_binary = 'svn'
3612 svntest.main.svnlook_binary = 'svnlook'
3613 svntest.main.svnadmin_binary = 'svnadmin'
3614 svntest.main.svnversion_binary = 'svnversion'
3616 run_tests(test_list)
3617 # NOTREACHED
3620 ### End of file.