Convert the time strings to seconds since the epoch in one swoop.
[cvs2svn.git] / run-tests.py
blob5a4acdff703014cb52f10b4ab08d3c06a5a3a92a
1 #!/usr/bin/env python
3 # run_tests.py: test suite for cvs2svn
5 # Usage: run_tests.py [-v | --verbose] [list | <num>]
7 # Options:
8 # -v, --verbose
9 # enable verbose output
11 # Arguments (at most one argument is allowed):
12 # list
13 # If the word "list" is passed as an argument, the list of
14 # available tests is printed (but no tests are run).
16 # <num>
17 # If a number is passed as an argument, then only the test
18 # with that number is run.
20 # If no argument is specified, then all tests are run.
22 # Subversion is a tool for revision control.
23 # See http://subversion.tigris.org for more information.
25 # ====================================================================
26 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
28 # This software is licensed as described in the file COPYING, which
29 # you should have received as part of this distribution. The terms
30 # are also available at http://subversion.tigris.org/license-1.html.
31 # If newer versions of this license are posted there, you may use a
32 # newer version instead, at your option.
34 ######################################################################
36 # General modules
37 import sys
38 import shutil
39 import stat
40 import re
41 import os
42 import time
43 import os.path
44 import locale
45 import textwrap
46 import calendar
47 from difflib import Differ
49 # Make sure that a supported version of Python is being used:
50 if not (0x02040000 <= sys.hexversion < 0x03000000):
51 sys.stderr.write(
52 'error: Python 2, version 2.4 or higher required.\n'
54 sys.exit(1)
56 # This script needs to run in the correct directory. Make sure we're there.
57 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
58 sys.stderr.write("error: I need to be run in the directory containing "
59 "'cvs2svn' and 'test-data'.\n")
60 sys.exit(1)
62 # Load the Subversion test framework.
63 import svntest
64 from svntest import Failure
65 from svntest.main import run_command
66 from svntest.main import run_tests
67 from svntest.main import safe_rmtree
68 from svntest.testcase import TestCase
69 from svntest.testcase import Skip
70 from svntest.testcase import XFail
71 from svntest.tree import build_tree_from_wc
72 from svntest.tree import get_child
74 cvs2svn = os.path.abspath('cvs2svn')
75 cvs2git = os.path.abspath('cvs2git')
77 # We use the installed svn and svnlook binaries, instead of using
78 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
79 # behavior -- or even existence -- of local builds shouldn't affect
80 # the cvs2svn test suite.
81 svn = 'svn'
82 svnlook = 'svnlook'
84 test_data_dir = 'test-data'
85 tmp_dir = 'cvs2svn-tmp'
88 #----------------------------------------------------------------------
89 # Helpers.
90 #----------------------------------------------------------------------
93 # The value to expect for svn:keywords if it is set:
94 KEYWORDS = 'Author Date Id Revision'
97 class RunProgramException(Failure):
98 pass
101 class MissingErrorException(Failure):
102 def __init__(self, error_re):
103 Failure.__init__(
104 self, "Test failed because no error matched '%s'" % (error_re,)
108 def run_program(program, error_re, *varargs):
109 """Run PROGRAM with VARARGS, return stdout as a list of lines.
111 If there is any stderr and ERROR_RE is None, raise
112 RunProgramException, and print the stderr lines if
113 svntest.main.verbose_mode is true.
115 If ERROR_RE is not None, it is a string regular expression that must
116 match some line of stderr. If it fails to match, raise
117 MissingErrorExpection."""
119 # FIXME: exit_code is currently ignored.
120 exit_code, out, err = run_command(program, 1, 0, *varargs)
122 if error_re:
123 # Specified error expected on stderr.
124 if not err:
125 raise MissingErrorException(error_re)
126 else:
127 for line in err:
128 if re.match(error_re, line):
129 return out
130 raise MissingErrorException(error_re)
131 else:
132 # No stderr allowed.
133 if err:
134 if svntest.main.verbose_mode:
135 print '\n%s said:\n' % program
136 for line in err:
137 print ' ' + line,
138 print
139 raise RunProgramException()
141 return out
144 def run_cvs2svn(error_re, *varargs):
145 """Run cvs2svn with VARARGS, return stdout as a list of lines.
147 If there is any stderr and ERROR_RE is None, raise
148 RunProgramException, and print the stderr lines if
149 svntest.main.verbose_mode is true.
151 If ERROR_RE is not None, it is a string regular expression that must
152 match some line of stderr. If it fails to match, raise
153 MissingErrorException."""
155 # Use the same python that is running this script
156 return run_program(sys.executable, error_re, cvs2svn, *varargs)
157 # On Windows, for an unknown reason, the cmd.exe process invoked by
158 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
159 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
160 # this. Therefore, the redirection of the output to the .s-revs file fails.
161 # We no longer use the problematic invocation on any system, but this
162 # comment remains to warn about this problem.
165 def run_cvs2git(error_re, *varargs):
166 """Run cvs2git with VARARGS, returning stdout as a list of lines.
168 If there is any stderr and ERROR_RE is None, raise
169 RunProgramException, and print the stderr lines if
170 svntest.main.verbose_mode is true.
172 If ERROR_RE is not None, it is a string regular expression that must
173 match some line of stderr. If it fails to match, raise
174 MissingErrorException."""
176 # Use the same python that is running this script
177 return run_program(sys.executable, error_re, cvs2git, *varargs)
178 # On Windows, for an unknown reason, the cmd.exe process invoked by
179 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
180 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
181 # this. Therefore, the redirection of the output to the .s-revs file fails.
182 # We no longer use the problematic invocation on any system, but this
183 # comment remains to warn about this problem.
186 def run_svn(*varargs):
187 """Run svn with VARARGS; return stdout as a list of lines.
188 If there is any stderr, raise RunProgramException, and print the
189 stderr lines if svntest.main.verbose_mode is true."""
190 return run_program(svn, None, *varargs)
193 def repos_to_url(path_to_svn_repos):
194 """This does what you think it does."""
195 rpath = os.path.abspath(path_to_svn_repos)
196 if rpath[0] != '/':
197 rpath = '/' + rpath
198 return 'file://%s' % rpath.replace(os.sep, '/')
201 def svn_strptime(timestr):
202 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
205 class Log:
206 def __init__(self, revision, author, date, symbols):
207 self.revision = revision
208 self.author = author
210 # Internally, we represent the date as seconds since epoch (UTC).
211 # Since standard subversion log output shows dates in localtime
213 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
215 # and time.mktime() converts from localtime, it all works out very
216 # happily.
217 self.date = time.mktime(svn_strptime(date[0:19]))
219 # The following symbols are used for string interpolation when
220 # checking paths:
221 self.symbols = symbols
223 # The changed paths will be accumulated later, as log data is read.
224 # Keys here are paths such as '/trunk/foo/bar', values are letter
225 # codes such as 'M', 'A', and 'D'.
226 self.changed_paths = { }
228 # The msg will be accumulated later, as log data is read.
229 self.msg = ''
231 def absorb_changed_paths(self, out):
232 'Read changed paths from OUT into self, until no more.'
233 while 1:
234 line = out.readline()
235 if len(line) == 1: return
236 line = line[:-1]
237 op_portion = line[3:4]
238 path_portion = line[5:]
239 # If we're running on Windows we get backslashes instead of
240 # forward slashes.
241 path_portion = path_portion.replace('\\', '/')
242 # # We could parse out history information, but currently we
243 # # just leave it in the path portion because that's how some
244 # # tests expect it.
246 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
247 # if m:
248 # path_portion = m.group(1)
249 self.changed_paths[path_portion] = op_portion
251 def __cmp__(self, other):
252 return cmp(self.revision, other.revision) or \
253 cmp(self.author, other.author) or cmp(self.date, other.date) or \
254 cmp(self.changed_paths, other.changed_paths) or \
255 cmp(self.msg, other.msg)
257 def get_path_op(self, path):
258 """Return the operator for the change involving PATH.
260 PATH is allowed to include string interpolation directives (e.g.,
261 '%(trunk)s'), which are interpolated against self.symbols. Return
262 None if there is no record for PATH."""
263 return self.changed_paths.get(path % self.symbols)
265 def check_msg(self, msg):
266 """Verify that this Log's message starts with the specified MSG."""
267 if self.msg.find(msg) != 0:
268 raise Failure(
269 "Revision %d log message was:\n%s\n\n"
270 "It should have begun with:\n%s\n\n"
271 % (self.revision, self.msg, msg,)
274 def check_change(self, path, op):
275 """Verify that this Log includes a change for PATH with operator OP.
277 PATH is allowed to include string interpolation directives (e.g.,
278 '%(trunk)s'), which are interpolated against self.symbols."""
280 path = path % self.symbols
281 found_op = self.changed_paths.get(path, None)
282 if found_op is None:
283 raise Failure(
284 "Revision %d does not include change for path %s "
285 "(it should have been %s).\n"
286 % (self.revision, path, op,)
288 if found_op != op:
289 raise Failure(
290 "Revision %d path %s had op %s (it should have been %s)\n"
291 % (self.revision, path, found_op, op,)
294 def check_changes(self, changed_paths):
295 """Verify that this Log has precisely the CHANGED_PATHS specified.
297 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
298 strings are allowed to include string interpolation directives
299 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
301 cp = {}
302 for (path, op) in changed_paths:
303 cp[path % self.symbols] = op
305 if self.changed_paths != cp:
306 raise Failure(
307 "Revision %d changed paths list was:\n%s\n\n"
308 "It should have been:\n%s\n\n"
309 % (self.revision, self.changed_paths, cp,)
312 def check(self, msg, changed_paths):
313 """Verify that this Log has the MSG and CHANGED_PATHS specified.
315 Convenience function to check two things at once. MSG is passed
316 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
318 self.check_msg(msg)
319 self.check_changes(changed_paths)
322 def parse_log(svn_repos, symbols):
323 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
325 Initialize the Logs' symbols with SYMBOLS."""
327 class LineFeeder:
328 'Make a list of lines behave like an open file handle.'
329 def __init__(self, lines):
330 self.lines = lines
331 def readline(self):
332 if len(self.lines) > 0:
333 return self.lines.pop(0)
334 else:
335 return None
337 def absorb_message_body(out, num_lines, log):
338 """Read NUM_LINES of log message body from OUT into Log item LOG."""
340 for i in range(num_lines):
341 log.msg += out.readline()
343 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
344 '(?P<author>[^\|]+) \| '
345 '(?P<date>[^\|]+) '
346 '\| (?P<lines>[0-9]+) (line|lines)$')
348 log_separator = '-' * 72
350 logs = { }
352 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
354 while 1:
355 this_log = None
356 line = out.readline()
357 if not line: break
358 line = line[:-1]
360 if line.find(log_separator) == 0:
361 line = out.readline()
362 if not line: break
363 line = line[:-1]
364 m = log_start_re.match(line)
365 if m:
366 this_log = Log(
367 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
368 line = out.readline()
369 if not line.find('Changed paths:') == 0:
370 print 'unexpected log output (missing changed paths)'
371 print "Line: '%s'" % line
372 sys.exit(1)
373 this_log.absorb_changed_paths(out)
374 absorb_message_body(out, int(m.group('lines')), this_log)
375 logs[this_log.revision] = this_log
376 elif len(line) == 0:
377 break # We've reached the end of the log output.
378 else:
379 print 'unexpected log output (missing revision line)'
380 print "Line: '%s'" % line
381 sys.exit(1)
382 else:
383 print 'unexpected log output (missing log separator)'
384 print "Line: '%s'" % line
385 sys.exit(1)
387 return logs
390 def erase(path):
391 """Unconditionally remove PATH and its subtree, if any. PATH may be
392 non-existent, a file or symlink, or a directory."""
393 if os.path.isdir(path):
394 safe_rmtree(path)
395 elif os.path.exists(path):
396 os.remove(path)
399 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
401 def sym_log_msg(symbolic_name, is_tag=None):
402 """Return the expected log message for a cvs2svn-synthesized revision
403 creating branch or tag SYMBOLIC_NAME."""
405 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
406 if is_tag:
407 type = 'tag'
408 else:
409 type = 'branch'
411 return log_msg_text_wrapper.fill(
412 "This commit was manufactured by cvs2svn to create %s '%s'."
413 % (type, symbolic_name)
417 def make_conversion_id(
418 name, args, passbypass, options_file=None, symbol_hints_file=None
420 """Create an identifying tag for a conversion.
422 The return value can also be used as part of a filesystem path.
424 NAME is the name of the CVS repository.
426 ARGS are the extra arguments to be passed to cvs2svn.
428 PASSBYPASS is a boolean indicating whether the conversion is to be
429 run one pass at a time.
431 If OPTIONS_FILE is specified, it is an options file that will be
432 used for the conversion.
434 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
435 will be used for the conversion.
437 The 1-to-1 mapping between cvs2svn command parameters and
438 conversion_ids allows us to avoid running the same conversion more
439 than once, when multiple tests use exactly the same conversion."""
441 conv_id = name
443 _win32_fname_mapping = { '/': '_sl_', '\\': '_bs_', ':': '_co_',
444 '*': '_st_', '?': '_qm_', '"': '_qq_',
445 '<': '_lt_', '>': '_gt_', '|': '_pi_', }
446 for arg in args:
447 # Replace some characters that Win32 isn't happy about having in a
448 # filename (which was causing the eol_mime test to fail).
449 sanitized_arg = arg
450 for a, b in _win32_fname_mapping.items():
451 sanitized_arg = sanitized_arg.replace(a, b)
452 conv_id += sanitized_arg
454 if passbypass:
455 conv_id += '-passbypass'
457 if options_file is not None:
458 conv_id += '--options=%s' % options_file
460 if symbol_hints_file is not None:
461 conv_id += '--symbol-hints=%s' % symbol_hints_file
463 return conv_id
466 class Conversion:
467 """A record of a cvs2svn conversion.
469 Fields:
471 conv_id -- the conversion id for this Conversion.
473 name -- a one-word name indicating the involved repositories.
475 dumpfile -- the name of the SVN dumpfile created by the conversion
476 (if the DUMPFILE constructor argument was used); otherwise,
477 None.
479 repos -- the path to the svn repository. Unset if DUMPFILE was
480 specified.
482 logs -- a dictionary of Log instances, as returned by parse_log().
483 Unset if DUMPFILE was specified.
485 symbols -- a dictionary of symbols used for string interpolation
486 in path names.
488 stdout -- a list of lines written by cvs2svn to stdout
490 _wc -- the basename of the svn working copy (within tmp_dir).
491 Unset if DUMPFILE was specified.
493 _wc_path -- the path to the svn working copy, if it has already
494 been created; otherwise, None. (The working copy is created
495 lazily when get_wc() is called.) Unset if DUMPFILE was
496 specified.
498 _wc_tree -- the tree built from the svn working copy, if it has
499 already been created; otherwise, None. The tree is created
500 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
501 specified.
503 _svnrepos -- the basename of the svn repository (within tmp_dir).
504 Unset if DUMPFILE was specified."""
506 # The number of the last cvs2svn pass (determined lazily by
507 # get_last_pass()).
508 last_pass = None
510 @classmethod
511 def get_last_pass(cls):
512 """Return the number of cvs2svn's last pass."""
514 if cls.last_pass is None:
515 out = run_cvs2svn(None, '--help-passes')
516 cls.last_pass = int(out[-1].split()[0])
517 return cls.last_pass
519 def __init__(
520 self, conv_id, name, error_re, passbypass, symbols, args,
521 options_file=None, symbol_hints_file=None, dumpfile=None,
523 self.conv_id = conv_id
524 self.name = name
525 self.symbols = symbols
526 if not os.path.isdir(tmp_dir):
527 os.mkdir(tmp_dir)
529 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
531 if dumpfile:
532 self.dumpfile = os.path.join(tmp_dir, dumpfile)
533 # Clean up from any previous invocations of this script.
534 erase(self.dumpfile)
535 else:
536 self.dumpfile = None
537 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
538 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
539 self._wc_path = None
540 self._wc_tree = None
542 # Clean up from any previous invocations of this script.
543 erase(self.repos)
544 erase(self._wc)
546 args = list(args)
547 if options_file:
548 self.options_file = os.path.join(cvsrepos, options_file)
549 args.extend([
550 '--options=%s' % self.options_file,
552 assert not symbol_hints_file
553 else:
554 self.options_file = None
555 if tmp_dir != 'cvs2svn-tmp':
556 # Only include this argument if it differs from cvs2svn's default:
557 args.extend([
558 '--tmpdir=%s' % tmp_dir,
561 if symbol_hints_file:
562 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
563 args.extend([
564 '--symbol-hints=%s' % self.symbol_hints_file,
567 if self.dumpfile:
568 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
569 else:
570 args.extend(['-s', self.repos])
571 args.extend([cvsrepos])
573 if passbypass:
574 self.stdout = []
575 for p in range(1, self.get_last_pass() + 1):
576 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
577 else:
578 self.stdout = run_cvs2svn(error_re, *args)
580 if self.dumpfile:
581 if not os.path.isfile(self.dumpfile):
582 raise Failure(
583 "Dumpfile not created: '%s'"
584 % os.path.join(os.getcwd(), self.dumpfile)
586 else:
587 if os.path.isdir(self.repos):
588 self.logs = parse_log(self.repos, self.symbols)
589 elif error_re is None:
590 raise Failure(
591 "Repository not created: '%s'"
592 % os.path.join(os.getcwd(), self.repos)
595 def output_found(self, pattern):
596 """Return True if PATTERN matches any line in self.stdout.
598 PATTERN is a regular expression pattern as a string.
601 pattern_re = re.compile(pattern)
603 for line in self.stdout:
604 if pattern_re.match(line):
605 # We found the pattern that we were looking for.
606 return 1
607 else:
608 return 0
610 def find_tag_log(self, tagname):
611 """Search LOGS for a log message containing 'TAGNAME' and return the
612 log in which it was found."""
613 for i in xrange(len(self.logs), 0, -1):
614 if self.logs[i].msg.find("'"+tagname+"'") != -1:
615 return self.logs[i]
616 raise ValueError("Tag %s not found in logs" % tagname)
618 def get_wc(self, *args):
619 """Return the path to the svn working copy, or a path within the WC.
621 If a working copy has not been created yet, create it now.
623 If ARGS are specified, then they should be strings that form
624 fragments of a path within the WC. They are joined using
625 os.path.join() and appended to the WC path."""
627 if self._wc_path is None:
628 run_svn('co', repos_to_url(self.repos), self._wc)
629 self._wc_path = self._wc
630 return os.path.join(self._wc_path, *args)
632 def get_wc_tree(self):
633 if self._wc_tree is None:
634 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
635 return self._wc_tree
637 def path_exists(self, *args):
638 """Return True if the specified path exists within the repository.
640 (The strings in ARGS are first joined into a path using
641 os.path.join().)"""
643 return os.path.exists(self.get_wc(*args))
645 def check_props(self, keys, checks):
646 """Helper function for checking lots of properties. For a list of
647 files in the conversion, check that the values of the properties
648 listed in KEYS agree with those listed in CHECKS. CHECKS is a
649 list of tuples: [ (filename, [value, value, ...]), ...], where the
650 values are listed in the same order as the key names are listed in
651 KEYS."""
653 for (file, values) in checks:
654 assert len(values) == len(keys)
655 props = props_for_path(self.get_wc_tree(), file)
656 for i in range(len(keys)):
657 if props.get(keys[i]) != values[i]:
658 raise Failure(
659 "File %s has property %s set to \"%s\" "
660 "(it should have been \"%s\").\n"
661 % (file, keys[i], props.get(keys[i]), values[i],)
665 class GitConversion:
666 """A record of a cvs2svn conversion.
668 Fields:
670 name -- a one-word name indicating the CVS repository to be converted.
672 stdout -- a list of lines written by cvs2svn to stdout."""
674 def __init__(self, name, error_re, args, options_file=None):
675 self.name = name
676 if not os.path.isdir(tmp_dir):
677 os.mkdir(tmp_dir)
679 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
681 args = list(args)
682 if options_file:
683 self.options_file = os.path.join(cvsrepos, options_file)
684 args.extend([
685 '--options=%s' % self.options_file,
687 else:
688 self.options_file = None
690 self.stdout = run_cvs2git(error_re, *args)
693 # Cache of conversions that have already been done. Keys are conv_id;
694 # values are Conversion instances.
695 already_converted = { }
697 def ensure_conversion(
698 name, error_re=None, passbypass=None,
699 trunk=None, branches=None, tags=None,
700 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
702 """Convert CVS repository NAME to Subversion, but only if it has not
703 been converted before by this invocation of this script. If it has
704 been converted before, return the Conversion object from the
705 previous invocation.
707 If no error, return a Conversion instance.
709 If ERROR_RE is a string, it is a regular expression expected to
710 match some line of stderr printed by the conversion. If there is an
711 error and ERROR_RE is not set, then raise Failure.
713 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
714 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
716 NAME is just one word. For example, 'main' would mean to convert
717 './test-data/main-cvsrepos', and after the conversion, the resulting
718 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
719 a checked out head working copy in './cvs2svn-tmp/main-wc'.
721 Any other options to pass to cvs2svn should be in ARGS, each element
722 being one option, e.g., '--trunk-only'. If the option takes an
723 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
724 are passed to cvs2svn in the order that they appear in ARGS.
726 If OPTIONS_FILE is specified, then it should be the name of a file
727 within the main directory of the cvs repository associated with this
728 test. It is passed to cvs2svn using the --options option (which
729 suppresses some other options that are incompatible with --options).
731 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
732 file within the main directory of the cvs repository associated with
733 this test. It is passed to cvs2svn using the --symbol-hints option.
735 If DUMPFILE is specified, then it is the name of a dumpfile within
736 the temporary directory to which the conversion output should be
737 written."""
739 if args is None:
740 args = []
741 else:
742 args = list(args)
744 if trunk is None:
745 trunk = 'trunk'
746 else:
747 args.append('--trunk=%s' % (trunk,))
749 if branches is None:
750 branches = 'branches'
751 else:
752 args.append('--branches=%s' % (branches,))
754 if tags is None:
755 tags = 'tags'
756 else:
757 args.append('--tags=%s' % (tags,))
759 conv_id = make_conversion_id(
760 name, args, passbypass, options_file, symbol_hints_file
763 if conv_id not in already_converted:
764 try:
765 # Run the conversion and store the result for the rest of this
766 # session:
767 already_converted[conv_id] = Conversion(
768 conv_id, name, error_re, passbypass,
769 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
770 args, options_file, symbol_hints_file, dumpfile,
772 except Failure:
773 # Remember the failure so that a future attempt to run this conversion
774 # does not bother to retry, but fails immediately.
775 already_converted[conv_id] = None
776 raise
778 conv = already_converted[conv_id]
779 if conv is None:
780 raise Failure()
781 return conv
784 class Cvs2SvnTestCase(TestCase):
785 def __init__(
786 self, name, description=None, variant=None,
787 error_re=None, passbypass=None,
788 trunk=None, branches=None, tags=None,
789 args=None,
790 options_file=None, symbol_hints_file=None, dumpfile=None,
792 TestCase.__init__(self)
793 self.name = name
795 if description is not None:
796 self._description = description
797 else:
798 # By default, use the first line of the class docstring as the
799 # description:
800 self._description = self.__doc__.splitlines()[0]
802 # Check that the original description is OK before we tinker with
803 # it:
804 self.check_description()
806 if variant is not None:
807 # Modify description to show the variant. Trim description
808 # first if necessary to stay within the 50-character limit.
809 suffix = '...variant %s' % (variant,)
810 self._description = self._description[:50 - len(suffix)] + suffix
811 # Check that the description is still OK:
812 self.check_description()
814 self.error_re = error_re
815 self.passbypass = passbypass
816 self.trunk = trunk
817 self.branches = branches
818 self.tags = tags
819 self.args = args
820 self.options_file = options_file
821 self.symbol_hints_file = symbol_hints_file
822 self.dumpfile = dumpfile
824 def get_description(self):
825 return self._description
827 def ensure_conversion(self):
828 return ensure_conversion(
829 self.name,
830 error_re=self.error_re, passbypass=self.passbypass,
831 trunk=self.trunk, branches=self.branches, tags=self.tags,
832 args=self.args,
833 options_file=self.options_file,
834 symbol_hints_file=self.symbol_hints_file,
835 dumpfile=self.dumpfile,
839 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
840 """Test properties resulting from a conversion."""
842 def __init__(self, name, props_to_test, expected_props, **kw):
843 """Initialize an instance of Cvs2SvnPropertiesTestCase.
845 NAME is the name of the test, passed to Cvs2SvnTestCase.
846 PROPS_TO_TEST is a list of the names of svn properties that should
847 be tested. EXPECTED_PROPS is a list of tuples [(filename,
848 [value,...])], where the second item in each tuple is a list of
849 values expected for the properties listed in PROPS_TO_TEST for the
850 specified filename. If a property must *not* be set, then its
851 value should be listed as None."""
853 Cvs2SvnTestCase.__init__(self, name, **kw)
854 self.props_to_test = props_to_test
855 self.expected_props = expected_props
857 def run(self):
858 conv = self.ensure_conversion()
859 conv.check_props(self.props_to_test, self.expected_props)
862 #----------------------------------------------------------------------
863 # Tests.
864 #----------------------------------------------------------------------
867 def show_usage():
868 "cvs2svn with no arguments shows usage"
869 out = run_cvs2svn(None)
870 if (len(out) > 2 and out[0].find('ERROR:') == 0
871 and out[1].find('DBM module')):
872 print 'cvs2svn cannot execute due to lack of proper DBM module.'
873 print 'Exiting without running any further tests.'
874 sys.exit(1)
875 if out[0].find('Usage:') < 0:
876 raise Failure('Basic cvs2svn invocation failed.')
879 def cvs2svn_manpage():
880 "generate a manpage for cvs2svn"
881 out = run_cvs2svn(None, '--man')
884 def cvs2git_manpage():
885 "generate a manpage for cvs2git"
886 out = run_cvs2git(None, '--man')
889 def show_help_passes():
890 "cvs2svn --help-passes shows pass information"
891 out = run_cvs2svn(None, '--help-passes')
892 if out[0].find('PASSES') < 0:
893 raise Failure('cvs2svn --help-passes failed.')
896 def attr_exec():
897 "detection of the executable flag"
898 if sys.platform == 'win32':
899 raise svntest.Skip()
900 conv = ensure_conversion('main')
901 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
902 if not st[0] & stat.S_IXUSR:
903 raise Failure()
906 def space_fname():
907 "conversion of filename with a space"
908 conv = ensure_conversion('main')
909 if not conv.path_exists('trunk', 'single-files', 'space fname'):
910 raise Failure()
913 def two_quick():
914 "two commits in quick succession"
915 conv = ensure_conversion('main')
916 logs = parse_log(
917 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
918 if len(logs) != 2:
919 raise Failure()
922 class PruneWithCare(Cvs2SvnTestCase):
923 "prune, but never too much"
925 def __init__(self, **kw):
926 Cvs2SvnTestCase.__init__(self, 'main', **kw)
928 def run(self):
929 # Robert Pluim encountered this lovely one while converting the
930 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
931 # repository (see issue #1302). Step 4 is the doozy:
933 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
934 # revision 2: adds trunk/blah/NEWS
935 # revision 3: deletes trunk/blah/cookie
936 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
937 # revision 5: does nothing
939 # After fixing cvs2svn, the sequence (correctly) looks like this:
941 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
942 # revision 2: adds trunk/blah/NEWS
943 # revision 3: deletes trunk/blah/cookie
944 # revision 4: does nothing [because trunk/blah/cookie already deleted]
945 # revision 5: deletes blah
947 # The difference is in 4 and 5. In revision 4, it's not correct to
948 # prune blah/, because NEWS is still in there, so revision 4 does
949 # nothing now. But when we delete NEWS in 5, that should bubble up
950 # and prune blah/ instead.
952 # ### Note that empty revisions like 4 are probably going to become
953 # ### at least optional, if not banished entirely from cvs2svn's
954 # ### output. Hmmm, or they may stick around, with an extra
955 # ### revision property explaining what happened. Need to think
956 # ### about that. In some sense, it's a bug in Subversion itself,
957 # ### that such revisions don't show up in 'svn log' output.
959 # In the test below, 'trunk/full-prune/first' represents
960 # cookie, and 'trunk/full-prune/second' represents NEWS.
962 conv = self.ensure_conversion()
964 # Confirm that revision 4 removes '/trunk/full-prune/first',
965 # and that revision 6 removes '/trunk/full-prune'.
967 # Also confirm similar things about '/full-prune-reappear/...',
968 # which is similar, except that later on it reappears, restored
969 # from pruneland, because a file gets added to it.
971 # And finally, a similar thing for '/partial-prune/...', except that
972 # in its case, a permanent file on the top level prevents the
973 # pruning from going farther than the subdirectory containing first
974 # and second.
976 for path in ('full-prune/first',
977 'full-prune-reappear/sub/first',
978 'partial-prune/sub/first'):
979 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
981 for path in ('full-prune',
982 'full-prune-reappear',
983 'partial-prune/sub'):
984 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
986 for path in ('full-prune-reappear',
987 'full-prune-reappear/appears-later'):
988 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
991 def interleaved_commits():
992 "two interleaved trunk commits, different log msgs"
993 # See test-data/main-cvsrepos/proj/README.
994 conv = ensure_conversion('main')
996 # The initial import.
997 rev = 26
998 conv.logs[rev].check('Initial import.', (
999 ('/%(trunk)s/interleaved', 'A'),
1000 ('/%(trunk)s/interleaved/1', 'A'),
1001 ('/%(trunk)s/interleaved/2', 'A'),
1002 ('/%(trunk)s/interleaved/3', 'A'),
1003 ('/%(trunk)s/interleaved/4', 'A'),
1004 ('/%(trunk)s/interleaved/5', 'A'),
1005 ('/%(trunk)s/interleaved/a', 'A'),
1006 ('/%(trunk)s/interleaved/b', 'A'),
1007 ('/%(trunk)s/interleaved/c', 'A'),
1008 ('/%(trunk)s/interleaved/d', 'A'),
1009 ('/%(trunk)s/interleaved/e', 'A'),
1012 def check_letters(rev):
1013 """Check if REV is the rev where only letters were committed."""
1015 conv.logs[rev].check('Committing letters only.', (
1016 ('/%(trunk)s/interleaved/a', 'M'),
1017 ('/%(trunk)s/interleaved/b', 'M'),
1018 ('/%(trunk)s/interleaved/c', 'M'),
1019 ('/%(trunk)s/interleaved/d', 'M'),
1020 ('/%(trunk)s/interleaved/e', 'M'),
1023 def check_numbers(rev):
1024 """Check if REV is the rev where only numbers were committed."""
1026 conv.logs[rev].check('Committing numbers only.', (
1027 ('/%(trunk)s/interleaved/1', 'M'),
1028 ('/%(trunk)s/interleaved/2', 'M'),
1029 ('/%(trunk)s/interleaved/3', 'M'),
1030 ('/%(trunk)s/interleaved/4', 'M'),
1031 ('/%(trunk)s/interleaved/5', 'M'),
1034 # One of the commits was letters only, the other was numbers only.
1035 # But they happened "simultaneously", so we don't assume anything
1036 # about which commit appeared first, so we just try both ways.
1037 rev += 1
1038 try:
1039 check_letters(rev)
1040 check_numbers(rev + 1)
1041 except Failure:
1042 check_numbers(rev)
1043 check_letters(rev + 1)
1046 def simple_commits():
1047 "simple trunk commits"
1048 # See test-data/main-cvsrepos/proj/README.
1049 conv = ensure_conversion('main')
1051 # The initial import.
1052 conv.logs[13].check('Initial import.', (
1053 ('/%(trunk)s/proj', 'A'),
1054 ('/%(trunk)s/proj/default', 'A'),
1055 ('/%(trunk)s/proj/sub1', 'A'),
1056 ('/%(trunk)s/proj/sub1/default', 'A'),
1057 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1058 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1059 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1060 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1061 ('/%(trunk)s/proj/sub2', 'A'),
1062 ('/%(trunk)s/proj/sub2/default', 'A'),
1063 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1064 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1065 ('/%(trunk)s/proj/sub3', 'A'),
1066 ('/%(trunk)s/proj/sub3/default', 'A'),
1069 # The first commit.
1070 conv.logs[18].check('First commit to proj, affecting two files.', (
1071 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1072 ('/%(trunk)s/proj/sub3/default', 'M'),
1075 # The second commit.
1076 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1077 ('/%(trunk)s/proj/default', 'M'),
1078 ('/%(trunk)s/proj/sub1/default', 'M'),
1079 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1080 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1081 ('/%(trunk)s/proj/sub2/default', 'M'),
1082 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1083 ('/%(trunk)s/proj/sub3/default', 'M')
1087 class SimpleTags(Cvs2SvnTestCase):
1088 "simple tags and branches, no commits"
1090 def __init__(self, **kw):
1091 # See test-data/main-cvsrepos/proj/README.
1092 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1094 def run(self):
1095 conv = self.ensure_conversion()
1097 # Verify the copy source for the tags we are about to check
1098 # No need to verify the copyfrom revision, as simple_commits did that
1099 conv.logs[13].check('Initial import.', (
1100 ('/%(trunk)s/proj', 'A'),
1101 ('/%(trunk)s/proj/default', 'A'),
1102 ('/%(trunk)s/proj/sub1', 'A'),
1103 ('/%(trunk)s/proj/sub1/default', 'A'),
1104 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1105 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1106 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1107 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1108 ('/%(trunk)s/proj/sub2', 'A'),
1109 ('/%(trunk)s/proj/sub2/default', 'A'),
1110 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1111 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1112 ('/%(trunk)s/proj/sub3', 'A'),
1113 ('/%(trunk)s/proj/sub3/default', 'A'),
1116 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1118 # Tag on rev 1.1.1.1 of all files in proj
1119 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1120 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1121 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1122 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1125 # The same, as a tag
1126 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1127 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1128 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1131 # Tag on rev 1.1.1.1 of all files in proj, except one
1132 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1133 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1134 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1135 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1138 # The same, as a branch
1139 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1140 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1141 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1145 def simple_branch_commits():
1146 "simple branch commits"
1147 # See test-data/main-cvsrepos/proj/README.
1148 conv = ensure_conversion('main')
1150 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1151 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1152 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1153 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1157 def mixed_time_tag():
1158 "mixed-time tag"
1159 # See test-data/main-cvsrepos/proj/README.
1160 conv = ensure_conversion('main')
1162 log = conv.find_tag_log('T_MIXED')
1163 log.check_changes((
1164 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1168 def mixed_time_branch_with_added_file():
1169 "mixed-time branch, and a file added to the branch"
1170 # See test-data/main-cvsrepos/proj/README.
1171 conv = ensure_conversion('main')
1173 # A branch from the same place as T_MIXED in the previous test,
1174 # plus a file added directly to the branch
1175 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1176 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1177 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1178 ('/%(branches)s/B_MIXED/single-files', 'D'),
1179 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1180 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1181 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1184 conv.logs[22].check('Add a file on branch B_MIXED.', (
1185 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1189 def mixed_commit():
1190 "a commit affecting both trunk and a branch"
1191 # See test-data/main-cvsrepos/proj/README.
1192 conv = ensure_conversion('main')
1194 conv.logs[24].check(
1195 'A single commit affecting one file on branch B_MIXED '
1196 'and one on trunk.', (
1197 ('/%(trunk)s/proj/sub2/default', 'M'),
1198 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1202 def split_time_branch():
1203 "branch some trunk files, and later branch the rest"
1204 # See test-data/main-cvsrepos/proj/README.
1205 conv = ensure_conversion('main')
1207 # First change on the branch, creating it
1208 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1209 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1210 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1211 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1212 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1215 conv.logs[29].check('First change on branch B_SPLIT.', (
1216 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1217 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1218 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1219 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1220 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1223 # A trunk commit for the file which was not branched
1224 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1225 'This was committed about an', (
1226 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1229 # Add the file not already branched to the branch, with modification:w
1230 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1231 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1232 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1235 conv.logs[32].check('This change affects sub3/default and '
1236 'sub1/subsubB/default, on branch', (
1237 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1238 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1242 def multiple_tags():
1243 "multiple tags referring to same revision"
1244 conv = ensure_conversion('main')
1245 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1246 raise Failure()
1247 if not conv.path_exists(
1248 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1249 raise Failure()
1252 def multiply_defined_symbols():
1253 "multiple definitions of symbol names"
1255 # We can only check one line of the error output at a time, so test
1256 # twice. (The conversion only have to be done once because the
1257 # results are cached.)
1258 conv = ensure_conversion(
1259 'multiply-defined-symbols',
1260 error_re=(
1261 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1262 r"1\.2\.4 1\.2\.2"
1265 conv = ensure_conversion(
1266 'multiply-defined-symbols',
1267 error_re=(
1268 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1269 r"1\.2 1\.1"
1274 def multiply_defined_symbols_renamed():
1275 "rename multiply defined symbols"
1277 conv = ensure_conversion(
1278 'multiply-defined-symbols',
1279 options_file='cvs2svn-rename.options',
1283 def multiply_defined_symbols_ignored():
1284 "ignore multiply defined symbols"
1286 conv = ensure_conversion(
1287 'multiply-defined-symbols',
1288 options_file='cvs2svn-ignore.options',
1292 def repeatedly_defined_symbols():
1293 "multiple identical definitions of symbol names"
1295 # If a symbol is defined multiple times but has the same value each
1296 # time, that should not be an error.
1298 conv = ensure_conversion('repeatedly-defined-symbols')
1301 def bogus_tag():
1302 "conversion of invalid symbolic names"
1303 conv = ensure_conversion('bogus-tag')
1306 def overlapping_branch():
1307 "ignore a file with a branch with two names"
1308 conv = ensure_conversion('overlapping-branch')
1310 if not conv.output_found('.*cannot also have name \'vendorB\''):
1311 raise Failure()
1313 conv.logs[2].check('imported', (
1314 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1315 ('/%(trunk)s/overlapping-branch', 'A'),
1318 if len(conv.logs) != 2:
1319 raise Failure()
1322 class PhoenixBranch(Cvs2SvnTestCase):
1323 "convert a branch file rooted in a 'dead' revision"
1325 def __init__(self, **kw):
1326 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1328 def run(self):
1329 conv = self.ensure_conversion()
1330 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1331 ('/%(branches)s/volsung_20010721', 'A'),
1332 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1334 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1335 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1339 ###TODO: We check for 4 changed paths here to accomodate creating tags
1340 ###and branches in rev 1, but that will change, so this will
1341 ###eventually change back.
1342 def ctrl_char_in_log():
1343 "handle a control char in a log message"
1344 # This was issue #1106.
1345 rev = 2
1346 conv = ensure_conversion('ctrl-char-in-log')
1347 conv.logs[rev].check_changes((
1348 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1350 if conv.logs[rev].msg.find('\x04') < 0:
1351 raise Failure(
1352 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1355 def overdead():
1356 "handle tags rooted in a redeleted revision"
1357 conv = ensure_conversion('overdead')
1360 class NoTrunkPrune(Cvs2SvnTestCase):
1361 "ensure that trunk doesn't get pruned"
1363 def __init__(self, **kw):
1364 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1366 def run(self):
1367 conv = self.ensure_conversion()
1368 for rev in conv.logs.keys():
1369 rev_logs = conv.logs[rev]
1370 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1371 raise Failure()
1374 def double_delete():
1375 "file deleted twice, in the root of the repository"
1376 # This really tests several things: how we handle a file that's
1377 # removed (state 'dead') in two successive revisions; how we
1378 # handle a file in the root of the repository (there were some
1379 # bugs in cvs2svn's svn path construction for top-level files); and
1380 # the --no-prune option.
1381 conv = ensure_conversion(
1382 'double-delete', args=['--trunk-only', '--no-prune'])
1384 path = '/%(trunk)s/twice-removed'
1385 rev = 2
1386 conv.logs[rev].check('Updated CVS', (
1387 (path, 'A'),
1389 conv.logs[rev + 1].check('Remove this file for the first time.', (
1390 (path, 'D'),
1392 conv.logs[rev + 2].check('Remove this file for the second time,', (
1396 def split_branch():
1397 "branch created from both trunk and another branch"
1398 # See test-data/split-branch-cvsrepos/README.
1400 # The conversion will fail if the bug is present, and
1401 # ensure_conversion will raise Failure.
1402 conv = ensure_conversion('split-branch')
1405 def resync_misgroups():
1406 "resyncing should not misorder commit groups"
1407 # See test-data/resync-misgroups-cvsrepos/README.
1409 # The conversion will fail if the bug is present, and
1410 # ensure_conversion will raise Failure.
1411 conv = ensure_conversion('resync-misgroups')
1414 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1415 "allow tags with mixed trunk and branch sources"
1417 def __init__(self, **kw):
1418 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1420 def run(self):
1421 conv = self.ensure_conversion()
1423 tags = conv.symbols.get('tags', 'tags')
1425 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1426 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1427 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1428 raise Failure()
1429 if (open(a_path, 'r').read().find('1.24') == -1) \
1430 or (open(b_path, 'r').read().find('1.5') == -1):
1431 raise Failure()
1434 def enroot_race():
1435 "never use the rev-in-progress as a copy source"
1437 # See issue #1427 and r8544.
1438 conv = ensure_conversion('enroot-race')
1439 rev = 6
1440 conv.logs[rev].check_changes((
1441 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1442 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1443 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1445 conv.logs[rev + 1].check_changes((
1446 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1447 ('/%(trunk)s/proj/a.txt', 'M'),
1448 ('/%(trunk)s/proj/b.txt', 'M'),
1452 def enroot_race_obo():
1453 "do use the last completed rev as a copy source"
1454 conv = ensure_conversion('enroot-race-obo')
1455 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1456 if not len(conv.logs) == 3:
1457 raise Failure()
1460 class BranchDeleteFirst(Cvs2SvnTestCase):
1461 "correctly handle deletion as initial branch action"
1463 def __init__(self, **kw):
1464 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1466 def run(self):
1467 # See test-data/branch-delete-first-cvsrepos/README.
1469 # The conversion will fail if the bug is present, and
1470 # ensure_conversion would raise Failure.
1471 conv = self.ensure_conversion()
1473 branches = conv.symbols.get('branches', 'branches')
1475 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1476 if conv.path_exists(branches, 'branch-1', 'file'):
1477 raise Failure()
1478 if conv.path_exists(branches, 'branch-2', 'file'):
1479 raise Failure()
1480 if not conv.path_exists(branches, 'branch-3', 'file'):
1481 raise Failure()
1484 def nonascii_filenames():
1485 "non ascii files converted incorrectly"
1486 # see issue #1255
1488 # on a en_US.iso-8859-1 machine this test fails with
1489 # svn: Can't recode ...
1491 # as described in the issue
1493 # on a en_US.UTF-8 machine this test fails with
1494 # svn: Malformed XML ...
1496 # which means at least it fails. Unfortunately it won't fail
1497 # with the same error...
1499 # mangle current locale settings so we know we're not running
1500 # a UTF-8 locale (which does not exhibit this problem)
1501 current_locale = locale.getlocale()
1502 new_locale = 'en_US.ISO8859-1'
1503 locale_changed = None
1505 # From http://docs.python.org/lib/module-sys.html
1507 # getfilesystemencoding():
1509 # Return the name of the encoding used to convert Unicode filenames
1510 # into system file names, or None if the system default encoding is
1511 # used. The result value depends on the operating system:
1513 # - On Windows 9x, the encoding is ``mbcs''.
1514 # - On Mac OS X, the encoding is ``utf-8''.
1515 # - On Unix, the encoding is the user's preference according to the
1516 # result of nl_langinfo(CODESET), or None if the
1517 # nl_langinfo(CODESET) failed.
1518 # - On Windows NT+, file names are Unicode natively, so no conversion is
1519 # performed.
1521 # So we're going to skip this test on Mac OS X for now.
1522 if sys.platform == "darwin":
1523 raise svntest.Skip()
1525 try:
1526 # change locale to non-UTF-8 locale to generate latin1 names
1527 locale.setlocale(locale.LC_ALL, # this might be too broad?
1528 new_locale)
1529 locale_changed = 1
1530 except locale.Error:
1531 raise svntest.Skip()
1533 try:
1534 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1535 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1536 if not os.path.exists(dstrepos_path):
1537 # create repos from existing main repos
1538 shutil.copytree(srcrepos_path, dstrepos_path)
1539 base_path = os.path.join(dstrepos_path, 'single-files')
1540 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1541 os.path.join(base_path, 'two\366uick,v'))
1542 new_path = os.path.join(dstrepos_path, 'single\366files')
1543 os.rename(base_path, new_path)
1545 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1546 finally:
1547 if locale_changed:
1548 locale.setlocale(locale.LC_ALL, current_locale)
1549 safe_rmtree(dstrepos_path)
1552 class UnicodeTest(Cvs2SvnTestCase):
1553 "metadata contains unicode"
1555 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1557 def __init__(self, name, warning_expected, **kw):
1558 if warning_expected:
1559 error_re = self.warning_pattern
1560 else:
1561 error_re = None
1563 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1564 self.warning_expected = warning_expected
1566 def run(self):
1567 try:
1568 # ensure the availability of the "utf_8" encoding:
1569 u'a'.encode('utf_8').decode('utf_8')
1570 except LookupError:
1571 raise svntest.Skip()
1573 self.ensure_conversion()
1576 class UnicodeAuthor(UnicodeTest):
1577 "author name contains unicode"
1579 def __init__(self, warning_expected, **kw):
1580 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1583 class UnicodeLog(UnicodeTest):
1584 "log message contains unicode"
1586 def __init__(self, warning_expected, **kw):
1587 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1590 def vendor_branch_sameness():
1591 "avoid spurious changes for initial revs"
1592 conv = ensure_conversion(
1593 'vendor-branch-sameness', args=['--keep-trivial-imports']
1596 # The following files are in this repository:
1598 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1599 # the same contents, the file's default branch is 1.1.1,
1600 # and both revisions are in state 'Exp'.
1602 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1603 # 1.1 (the addition of a line of text).
1605 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1607 # d.txt: This file was created by 'cvs add' instead of import, so
1608 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1609 # The timestamp on the add is exactly the same as for the
1610 # imports of the other files.
1612 # e.txt: Like a.txt, except that the log message for revision 1.1
1613 # is not the standard import log message.
1615 # (Aside from e.txt, the log messages for the same revisions are the
1616 # same in all files.)
1618 # We expect that only a.txt is recognized as an import whose 1.1
1619 # revision can be omitted. The other files should be added on trunk
1620 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1621 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1622 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1623 # and c.txt should be 'D'eleted.
1625 rev = 2
1626 conv.logs[rev].check('Initial revision', (
1627 ('/%(trunk)s/proj', 'A'),
1628 ('/%(trunk)s/proj/b.txt', 'A'),
1629 ('/%(trunk)s/proj/c.txt', 'A'),
1630 ('/%(trunk)s/proj/d.txt', 'A'),
1633 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1634 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1635 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1638 conv.logs[rev + 2].check('First vendor branch revision.', (
1639 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1640 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1641 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1644 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1645 'to compensate for changes in r4,', (
1646 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1647 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1648 ('/%(trunk)s/proj/c.txt', 'D'),
1651 rev = 7
1652 conv.logs[rev].check('This log message is not the standard', (
1653 ('/%(trunk)s/proj/e.txt', 'A'),
1656 conv.logs[rev + 2].check('First vendor branch revision', (
1657 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1660 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1661 'to compensate for changes in r9,', (
1662 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1666 def vendor_branch_trunk_only():
1667 "handle vendor branches with --trunk-only"
1668 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1670 rev = 2
1671 conv.logs[rev].check('Initial revision', (
1672 ('/%(trunk)s/proj', 'A'),
1673 ('/%(trunk)s/proj/b.txt', 'A'),
1674 ('/%(trunk)s/proj/c.txt', 'A'),
1675 ('/%(trunk)s/proj/d.txt', 'A'),
1678 conv.logs[rev + 1].check('First vendor branch revision', (
1679 ('/%(trunk)s/proj/a.txt', 'A'),
1680 ('/%(trunk)s/proj/b.txt', 'M'),
1681 ('/%(trunk)s/proj/c.txt', 'D'),
1684 conv.logs[rev + 2].check('This log message is not the standard', (
1685 ('/%(trunk)s/proj/e.txt', 'A'),
1688 conv.logs[rev + 3].check('First vendor branch revision', (
1689 ('/%(trunk)s/proj/e.txt', 'M'),
1693 def default_branches():
1694 "handle default branches correctly"
1695 conv = ensure_conversion('default-branches')
1697 # There are seven files in the repository:
1699 # a.txt:
1700 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1701 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1702 # committed (thus losing the default branch "1.1.1"), then
1703 # 1.1.1.4 was imported. All vendor import release tags are
1704 # still present.
1706 # b.txt:
1707 # Like a.txt, but without rev 1.2.
1709 # c.txt:
1710 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1712 # d.txt:
1713 # Same as the previous two, but 1.1.1 branch is unlabeled.
1715 # e.txt:
1716 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1718 # deleted-on-vendor-branch.txt,v:
1719 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1721 # added-then-imported.txt,v:
1722 # Added with 'cvs add' to create 1.1, then imported with
1723 # completely different contents to create 1.1.1.1, therefore
1724 # never had a default branch.
1727 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1728 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1729 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1730 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1731 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1732 ('/%(branches)s/vbranchA', 'A'),
1733 ('/%(branches)s/vbranchA/proj', 'A'),
1734 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1735 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1736 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1737 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1740 conv.logs[3].check("This commit was generated by cvs2svn "
1741 "to compensate for changes in r2,", (
1742 ('/%(trunk)s/proj', 'A'),
1743 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1744 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1745 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1746 ('/%(trunk)s/proj/d.txt '
1747 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1748 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1749 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1750 ('/%(trunk)s/proj/e.txt '
1751 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1754 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1755 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1756 ('/%(tags)s/vtag-1/proj/d.txt '
1757 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1760 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1761 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1762 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1763 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1764 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1765 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1766 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1769 conv.logs[6].check("This commit was generated by cvs2svn "
1770 "to compensate for changes in r5,", (
1771 ('/%(trunk)s/proj/a.txt '
1772 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1773 ('/%(trunk)s/proj/b.txt '
1774 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1775 ('/%(trunk)s/proj/c.txt '
1776 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1777 ('/%(trunk)s/proj/d.txt '
1778 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1779 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1780 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1781 'R'),
1782 ('/%(trunk)s/proj/e.txt '
1783 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1786 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1787 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1788 ('/%(tags)s/vtag-2/proj/d.txt '
1789 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1792 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1793 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1794 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1795 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1796 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1797 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1798 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1801 conv.logs[9].check("This commit was generated by cvs2svn "
1802 "to compensate for changes in r8,", (
1803 ('/%(trunk)s/proj/a.txt '
1804 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1805 ('/%(trunk)s/proj/b.txt '
1806 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1807 ('/%(trunk)s/proj/c.txt '
1808 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1809 ('/%(trunk)s/proj/d.txt '
1810 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1811 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1812 ('/%(trunk)s/proj/e.txt '
1813 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1816 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1817 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1818 ('/%(tags)s/vtag-3/proj/d.txt '
1819 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1820 ('/%(tags)s/vtag-3/proj/e.txt '
1821 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1824 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1825 ('/%(trunk)s/proj/a.txt', 'M'),
1828 conv.logs[12].check("Add a file to the working copy.", (
1829 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1832 conv.logs[13].check(sym_log_msg('vbranchA'), (
1833 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1834 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1837 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1838 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1839 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1840 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1841 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1842 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1843 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1844 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1847 conv.logs[15].check("This commit was generated by cvs2svn "
1848 "to compensate for changes in r14,", (
1849 ('/%(trunk)s/proj/b.txt '
1850 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1851 ('/%(trunk)s/proj/c.txt '
1852 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1853 ('/%(trunk)s/proj/d.txt '
1854 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1855 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1856 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1857 'A'),
1858 ('/%(trunk)s/proj/e.txt '
1859 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1862 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1863 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1864 ('/%(tags)s/vtag-4/proj/d.txt '
1865 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1869 def default_branches_trunk_only():
1870 "handle default branches with --trunk-only"
1872 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1874 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1875 ('/%(trunk)s/proj', 'A'),
1876 ('/%(trunk)s/proj/a.txt', 'A'),
1877 ('/%(trunk)s/proj/b.txt', 'A'),
1878 ('/%(trunk)s/proj/c.txt', 'A'),
1879 ('/%(trunk)s/proj/d.txt', 'A'),
1880 ('/%(trunk)s/proj/e.txt', 'A'),
1881 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1884 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1885 ('/%(trunk)s/proj/a.txt', 'M'),
1886 ('/%(trunk)s/proj/b.txt', 'M'),
1887 ('/%(trunk)s/proj/c.txt', 'M'),
1888 ('/%(trunk)s/proj/d.txt', 'M'),
1889 ('/%(trunk)s/proj/e.txt', 'M'),
1890 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1893 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1894 ('/%(trunk)s/proj/a.txt', 'M'),
1895 ('/%(trunk)s/proj/b.txt', 'M'),
1896 ('/%(trunk)s/proj/c.txt', 'M'),
1897 ('/%(trunk)s/proj/d.txt', 'M'),
1898 ('/%(trunk)s/proj/e.txt', 'M'),
1899 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1902 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1903 ('/%(trunk)s/proj/a.txt', 'M'),
1906 conv.logs[6].check("Add a file to the working copy.", (
1907 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1910 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1911 ('/%(trunk)s/proj/b.txt', 'M'),
1912 ('/%(trunk)s/proj/c.txt', 'M'),
1913 ('/%(trunk)s/proj/d.txt', 'M'),
1914 ('/%(trunk)s/proj/e.txt', 'M'),
1915 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1919 def default_branch_and_1_2():
1920 "do not allow 1.2 revision with default branch"
1922 conv = ensure_conversion(
1923 'default-branch-and-1-2',
1924 error_re=(
1925 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
1930 def compose_tag_three_sources():
1931 "compose a tag from three sources"
1932 conv = ensure_conversion('compose-tag-three-sources')
1934 conv.logs[2].check("Add on trunk", (
1935 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
1936 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
1937 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
1938 ('/%(trunk)s/tagged-on-b1', 'A'),
1939 ('/%(trunk)s/tagged-on-b2', 'A'),
1942 conv.logs[3].check(sym_log_msg('b1'), (
1943 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
1946 conv.logs[4].check(sym_log_msg('b2'), (
1947 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
1950 conv.logs[5].check("Commit on branch b1", (
1951 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
1952 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
1953 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
1954 ('/%(branches)s/b1/tagged-on-b1', 'M'),
1955 ('/%(branches)s/b1/tagged-on-b2', 'M'),
1958 conv.logs[6].check("Commit on branch b2", (
1959 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
1960 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
1961 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
1962 ('/%(branches)s/b2/tagged-on-b1', 'M'),
1963 ('/%(branches)s/b2/tagged-on-b2', 'M'),
1966 conv.logs[7].check("Commit again on trunk", (
1967 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
1968 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
1969 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
1970 ('/%(trunk)s/tagged-on-b1', 'M'),
1971 ('/%(trunk)s/tagged-on-b2', 'M'),
1974 conv.logs[8].check(sym_log_msg('T',1), (
1975 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
1976 ('/%(tags)s/T/tagged-on-trunk-1.1 '
1977 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
1978 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
1979 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
1983 def pass5_when_to_fill():
1984 "reserve a svn revnum for a fill only when required"
1985 # The conversion will fail if the bug is present, and
1986 # ensure_conversion would raise Failure.
1987 conv = ensure_conversion('pass5-when-to-fill')
1990 class EmptyTrunk(Cvs2SvnTestCase):
1991 "don't break when the trunk is empty"
1993 def __init__(self, **kw):
1994 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
1996 def run(self):
1997 # The conversion will fail if the bug is present, and
1998 # ensure_conversion would raise Failure.
1999 conv = self.ensure_conversion()
2002 def no_spurious_svn_commits():
2003 "ensure that we don't create any spurious commits"
2004 conv = ensure_conversion('phoenix')
2006 # Check spurious commit that could be created in
2007 # SVNCommitCreator._pre_commit()
2009 # (When you add a file on a branch, CVS creates a trunk revision
2010 # in state 'dead'. If the log message of that commit is equal to
2011 # the one that CVS generates, we do not ever create a 'fill'
2012 # SVNCommit for it.)
2014 # and spurious commit that could be created in
2015 # SVNCommitCreator._commit()
2017 # (When you add a file on a branch, CVS creates a trunk revision
2018 # in state 'dead'. If the log message of that commit is equal to
2019 # the one that CVS generates, we do not create a primary SVNCommit
2020 # for it.)
2021 conv.logs[17].check('File added on branch xiphophorus', (
2022 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
2025 # Check to make sure that a commit *is* generated:
2026 # (When you add a file on a branch, CVS creates a trunk revision
2027 # in state 'dead'. If the log message of that commit is NOT equal
2028 # to the one that CVS generates, we create a primary SVNCommit to
2029 # serve as a home for the log message in question.
2030 conv.logs[18].check('file added-on-branch2.txt was initially added on '
2031 + 'branch xiphophorus,\nand this log message was tweaked', ())
2033 # Check spurious commit that could be created in
2034 # SVNCommitCreator._commit_symbols().
2035 conv.logs[19].check('This file was also added on branch xiphophorus,', (
2036 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
2040 class PeerPathPruning(Cvs2SvnTestCase):
2041 "make sure that filling prunes paths correctly"
2043 def __init__(self, **kw):
2044 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
2046 def run(self):
2047 conv = self.ensure_conversion()
2048 conv.logs[6].check(sym_log_msg('BRANCH'), (
2049 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
2050 ('/%(branches)s/BRANCH/bar', 'D'),
2051 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
2055 def invalid_closings_on_trunk():
2056 "verify correct revs are copied to default branches"
2057 # The conversion will fail if the bug is present, and
2058 # ensure_conversion would raise Failure.
2059 conv = ensure_conversion('invalid-closings-on-trunk')
2062 def individual_passes():
2063 "run each pass individually"
2064 conv = ensure_conversion('main')
2065 conv2 = ensure_conversion('main', passbypass=1)
2067 if conv.logs != conv2.logs:
2068 raise Failure()
2071 def resync_bug():
2072 "reveal a big bug in our resync algorithm"
2073 # This will fail if the bug is present
2074 conv = ensure_conversion('resync-bug')
2077 def branch_from_default_branch():
2078 "reveal a bug in our default branch detection code"
2079 conv = ensure_conversion('branch-from-default-branch')
2081 # This revision will be a default branch synchronization only
2082 # if cvs2svn is correctly determining default branch revisions.
2084 # The bug was that cvs2svn was treating revisions on branches off of
2085 # default branches as default branch revisions, resulting in
2086 # incorrectly regarding the branch off of the default branch as a
2087 # non-trunk default branch. Crystal clear? I thought so. See
2088 # issue #42 for more incoherent blathering.
2089 conv.logs[5].check("This commit was generated by cvs2svn", (
2090 ('/%(trunk)s/proj/file.txt '
2091 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2095 def file_in_attic_too():
2096 "die if a file exists in and out of the attic"
2097 ensure_conversion(
2098 'file-in-attic-too',
2099 error_re=(
2100 r'.*A CVS repository cannot contain both '
2101 r'(.*)' + re.escape(os.sep) + r'(.*) '
2102 + r'and '
2103 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2108 def retain_file_in_attic_too():
2109 "test --retain-conflicting-attic-files option"
2110 conv = ensure_conversion(
2111 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2112 if not conv.path_exists('trunk', 'file.txt'):
2113 raise Failure()
2114 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2115 raise Failure()
2118 def symbolic_name_filling_guide():
2119 "reveal a big bug in our SymbolFillingGuide"
2120 # This will fail if the bug is present
2121 conv = ensure_conversion('symbolic-name-overfill')
2124 # Helpers for tests involving file contents and properties.
2126 class NodeTreeWalkException:
2127 "Exception class for node tree traversals."
2128 pass
2130 def node_for_path(node, path):
2131 "In the tree rooted under SVNTree NODE, return the node at PATH."
2132 if node.name != '__SVN_ROOT_NODE':
2133 raise NodeTreeWalkException()
2134 path = path.strip('/')
2135 components = path.split('/')
2136 for component in components:
2137 node = get_child(node, component)
2138 return node
2140 # Helper for tests involving properties.
2141 def props_for_path(node, path):
2142 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2143 return node_for_path(node, path).props
2146 class EOLMime(Cvs2SvnPropertiesTestCase):
2147 """eol settings and mime types together
2149 The files are as follows:
2151 trunk/foo.txt: no -kb, mime file says nothing.
2152 trunk/foo.xml: no -kb, mime file says text.
2153 trunk/foo.zip: no -kb, mime file says non-text.
2154 trunk/foo.bin: has -kb, mime file says nothing.
2155 trunk/foo.csv: has -kb, mime file says text.
2156 trunk/foo.dbf: has -kb, mime file says non-text.
2159 def __init__(self, args, **kw):
2160 # TODO: It's a bit klugey to construct this path here. But so far
2161 # there's only one test with a mime.types file. If we have more,
2162 # we should abstract this into some helper, which would be located
2163 # near ensure_conversion(). Note that it is a convention of this
2164 # test suite for a mime.types file to be located in the top level
2165 # of the CVS repository to which it applies.
2166 self.mime_path = os.path.join(
2167 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2169 Cvs2SvnPropertiesTestCase.__init__(
2170 self, 'eol-mime',
2171 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2172 args=['--mime-types=%s' % self.mime_path] + args,
2173 **kw)
2176 # We do four conversions. Each time, we pass --mime-types=FILE with
2177 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2178 # Thus there's one conversion with neither flag, one with just the
2179 # former, one with just the latter, and one with both.
2182 # Neither --no-default-eol nor --eol-from-mime-type:
2183 eol_mime1 = EOLMime(
2184 variant=1,
2185 args=[],
2186 expected_props=[
2187 ('trunk/foo.txt', [None, None, None]),
2188 ('trunk/foo.xml', [None, 'text/xml', None]),
2189 ('trunk/foo.zip', [None, 'application/zip', None]),
2190 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2191 ('trunk/foo.csv', [None, 'text/csv', None]),
2192 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2196 # Just --no-default-eol, not --eol-from-mime-type:
2197 eol_mime2 = EOLMime(
2198 variant=2,
2199 args=['--default-eol=native'],
2200 expected_props=[
2201 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2202 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2203 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2204 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2205 ('trunk/foo.csv', [None, 'text/csv', None]),
2206 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2210 # Just --eol-from-mime-type, not --no-default-eol:
2211 eol_mime3 = EOLMime(
2212 variant=3,
2213 args=['--eol-from-mime-type'],
2214 expected_props=[
2215 ('trunk/foo.txt', [None, None, None]),
2216 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2217 ('trunk/foo.zip', [None, 'application/zip', None]),
2218 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2219 ('trunk/foo.csv', [None, 'text/csv', None]),
2220 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2224 # Both --no-default-eol and --eol-from-mime-type:
2225 eol_mime4 = EOLMime(
2226 variant=4,
2227 args=['--eol-from-mime-type', '--default-eol=native'],
2228 expected_props=[
2229 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2230 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2231 ('trunk/foo.zip', [None, 'application/zip', None]),
2232 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2233 ('trunk/foo.csv', [None, 'text/csv', None]),
2234 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2238 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2239 'eol-mime',
2240 description='test non-setting of cvs2svn:cvs-rev property',
2241 args=[],
2242 props_to_test=['cvs2svn:cvs-rev'],
2243 expected_props=[
2244 ('trunk/foo.txt', [None]),
2245 ('trunk/foo.xml', [None]),
2246 ('trunk/foo.zip', [None]),
2247 ('trunk/foo.bin', [None]),
2248 ('trunk/foo.csv', [None]),
2249 ('trunk/foo.dbf', [None]),
2253 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2254 'eol-mime',
2255 description='test setting of cvs2svn:cvs-rev property',
2256 args=['--cvs-revnums'],
2257 props_to_test=['cvs2svn:cvs-rev'],
2258 expected_props=[
2259 ('trunk/foo.txt', ['1.2']),
2260 ('trunk/foo.xml', ['1.2']),
2261 ('trunk/foo.zip', ['1.2']),
2262 ('trunk/foo.bin', ['1.2']),
2263 ('trunk/foo.csv', ['1.2']),
2264 ('trunk/foo.dbf', ['1.2']),
2268 keywords = Cvs2SvnPropertiesTestCase(
2269 'keywords',
2270 description='test setting of svn:keywords property among others',
2271 args=['--default-eol=native'],
2272 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2273 expected_props=[
2274 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2275 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2276 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2277 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2278 ('trunk/foo.kk', [None, 'native', None]),
2279 ('trunk/foo.ko', [None, 'native', None]),
2280 ('trunk/foo.kv', [None, 'native', None]),
2284 def ignore():
2285 "test setting of svn:ignore property"
2286 conv = ensure_conversion('cvsignore')
2287 wc_tree = conv.get_wc_tree()
2288 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2289 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2291 if topdir_props['svn:ignore'] != \
2292 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2293 raise Failure()
2295 if subdir_props['svn:ignore'] != \
2296 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2297 raise Failure()
2300 def requires_cvs():
2301 "test that CVS can still do what RCS can't"
2302 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2303 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2305 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2306 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2308 if atsign_contents[-1:] == "@":
2309 raise Failure()
2310 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2311 raise Failure()
2313 if not (conv.logs[21].author == "William Lyon Phelps III" and
2314 conv.logs[20].author == "j random"):
2315 raise Failure()
2318 def questionable_branch_names():
2319 "test that we can handle weird branch names"
2320 conv = ensure_conversion('questionable-symbols')
2321 # If the conversion succeeds, then we're okay. We could check the
2322 # actual branch paths, too, but the main thing is to know that the
2323 # conversion doesn't fail.
2326 def questionable_tag_names():
2327 "test that we can handle weird tag names"
2328 conv = ensure_conversion('questionable-symbols')
2329 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2330 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2332 conv.find_tag_log('TagWith/Backslash_E').check(
2333 sym_log_msg('TagWith/Backslash_E',1),
2335 ('/%(tags)s/TagWith', 'A'),
2336 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2339 conv.find_tag_log('TagWith/Slash_Z').check(
2340 sym_log_msg('TagWith/Slash_Z',1),
2342 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2347 def revision_reorder_bug():
2348 "reveal a bug that reorders file revisions"
2349 conv = ensure_conversion('revision-reorder-bug')
2350 # If the conversion succeeds, then we're okay. We could check the
2351 # actual revisions, too, but the main thing is to know that the
2352 # conversion doesn't fail.
2355 def exclude():
2356 "test that exclude really excludes everything"
2357 conv = ensure_conversion('main', args=['--exclude=.*'])
2358 for log in conv.logs.values():
2359 for item in log.changed_paths.keys():
2360 if item.startswith('/branches/') or item.startswith('/tags/'):
2361 raise Failure()
2364 def vendor_branch_delete_add():
2365 "add trunk file that was deleted on vendor branch"
2366 # This will error if the bug is present
2367 conv = ensure_conversion('vendor-branch-delete-add')
2370 def resync_pass2_pull_forward():
2371 "ensure pass2 doesn't pull rev too far forward"
2372 conv = ensure_conversion('resync-pass2-pull-forward')
2373 # If the conversion succeeds, then we're okay. We could check the
2374 # actual revisions, too, but the main thing is to know that the
2375 # conversion doesn't fail.
2378 def native_eol():
2379 "only LFs for svn:eol-style=native files"
2380 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2381 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2382 conv.repos)
2383 # Verify that all files in the dump have LF EOLs. We're actually
2384 # testing the whole dump file, but the dump file itself only uses
2385 # LF EOLs, so we're safe.
2386 for line in lines:
2387 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2388 raise Failure()
2391 def double_fill():
2392 "reveal a bug that created a branch twice"
2393 conv = ensure_conversion('double-fill')
2394 # If the conversion succeeds, then we're okay. We could check the
2395 # actual revisions, too, but the main thing is to know that the
2396 # conversion doesn't fail.
2399 def double_fill2():
2400 "reveal a second bug that created a branch twice"
2401 conv = ensure_conversion('double-fill2')
2402 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2403 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2404 try:
2405 # This check should fail:
2406 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2407 except Failure:
2408 pass
2409 else:
2410 raise Failure('Symbol filled twice in a row')
2413 def resync_pass2_push_backward():
2414 "ensure pass2 doesn't push rev too far backward"
2415 conv = ensure_conversion('resync-pass2-push-backward')
2416 # If the conversion succeeds, then we're okay. We could check the
2417 # actual revisions, too, but the main thing is to know that the
2418 # conversion doesn't fail.
2421 def double_add():
2422 "reveal a bug that added a branch file twice"
2423 conv = ensure_conversion('double-add')
2424 # If the conversion succeeds, then we're okay. We could check the
2425 # actual revisions, too, but the main thing is to know that the
2426 # conversion doesn't fail.
2429 def bogus_branch_copy():
2430 "reveal a bug that copies a branch file wrongly"
2431 conv = ensure_conversion('bogus-branch-copy')
2432 # If the conversion succeeds, then we're okay. We could check the
2433 # actual revisions, too, but the main thing is to know that the
2434 # conversion doesn't fail.
2437 def nested_ttb_directories():
2438 "require error if ttb directories are not disjoint"
2439 opts_list = [
2440 {'trunk' : 'a', 'branches' : 'a',},
2441 {'trunk' : 'a', 'tags' : 'a',},
2442 {'branches' : 'a', 'tags' : 'a',},
2443 # This option conflicts with the default trunk path:
2444 {'branches' : 'trunk',},
2445 # Try some nested directories:
2446 {'trunk' : 'a', 'branches' : 'a/b',},
2447 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2448 {'branches' : 'a', 'tags' : 'a/b',},
2451 for opts in opts_list:
2452 ensure_conversion(
2453 'main', error_re=r'The following paths are not disjoint\:', **opts
2457 class AutoProps(Cvs2SvnPropertiesTestCase):
2458 """Test auto-props.
2460 The files are as follows:
2462 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2463 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2464 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2465 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2466 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2467 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2468 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2469 trunk/foo.UPCASE1: no -kb, no mime type.
2470 trunk/foo.UPCASE2: no -kb, no mime type.
2473 def __init__(self, args, **kw):
2474 ### TODO: It's a bit klugey to construct this path here. See also
2475 ### the comment in eol_mime().
2476 auto_props_path = os.path.join(
2477 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2479 Cvs2SvnPropertiesTestCase.__init__(
2480 self, 'eol-mime',
2481 props_to_test=[
2482 'myprop',
2483 'svn:eol-style',
2484 'svn:mime-type',
2485 'svn:keywords',
2486 'svn:executable',
2488 args=[
2489 '--auto-props=%s' % auto_props_path,
2490 '--eol-from-mime-type'
2491 ] + args,
2492 **kw)
2495 auto_props_ignore_case = AutoProps(
2496 description="test auto-props",
2497 args=['--default-eol=native'],
2498 expected_props=[
2499 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2500 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2501 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2502 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2503 ('trunk/foo.bin',
2504 ['bin', None, 'application/octet-stream', None, '']),
2505 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2506 ('trunk/foo.dbf',
2507 ['dbf', None, 'application/what-is-dbf', None, None]),
2508 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2509 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2513 def ctrl_char_in_filename():
2514 "do not allow control characters in filenames"
2516 try:
2517 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2518 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2519 if os.path.exists(dstrepos_path):
2520 safe_rmtree(dstrepos_path)
2522 # create repos from existing main repos
2523 shutil.copytree(srcrepos_path, dstrepos_path)
2524 base_path = os.path.join(dstrepos_path, 'single-files')
2525 try:
2526 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2527 os.path.join(base_path, 'two\rquick,v'))
2528 except:
2529 # Operating systems that don't allow control characters in
2530 # filenames will hopefully have thrown an exception; in that
2531 # case, just skip this test.
2532 raise svntest.Skip()
2534 conv = ensure_conversion(
2535 'ctrl-char-filename',
2536 error_re=(r'.*Character .* in filename .* '
2537 r'is not supported by Subversion\.'),
2539 finally:
2540 safe_rmtree(dstrepos_path)
2543 def commit_dependencies():
2544 "interleaved and multi-branch commits to same files"
2545 conv = ensure_conversion("commit-dependencies")
2546 conv.logs[2].check('adding', (
2547 ('/%(trunk)s/interleaved', 'A'),
2548 ('/%(trunk)s/interleaved/file1', 'A'),
2549 ('/%(trunk)s/interleaved/file2', 'A'),
2551 conv.logs[3].check('big commit', (
2552 ('/%(trunk)s/interleaved/file1', 'M'),
2553 ('/%(trunk)s/interleaved/file2', 'M'),
2555 conv.logs[4].check('dependant small commit', (
2556 ('/%(trunk)s/interleaved/file1', 'M'),
2558 conv.logs[5].check('adding', (
2559 ('/%(trunk)s/multi-branch', 'A'),
2560 ('/%(trunk)s/multi-branch/file1', 'A'),
2561 ('/%(trunk)s/multi-branch/file2', 'A'),
2563 conv.logs[6].check(sym_log_msg("branch"), (
2564 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2565 ('/%(branches)s/branch/interleaved', 'D'),
2567 conv.logs[7].check('multi-branch-commit', (
2568 ('/%(trunk)s/multi-branch/file1', 'M'),
2569 ('/%(trunk)s/multi-branch/file2', 'M'),
2570 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2571 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2575 def double_branch_delete():
2576 "fill branches before modifying files on them"
2577 conv = ensure_conversion('double-branch-delete')
2579 # Test for issue #102. The file IMarshalledValue.java is branched,
2580 # deleted, readded on the branch, and then deleted again. If the
2581 # fill for the file on the branch is postponed until after the
2582 # modification, the file will end up live on the branch instead of
2583 # dead! Make sure it happens at the right time.
2585 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2586 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2589 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2590 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2594 def symbol_mismatches():
2595 "error for conflicting tag/branch"
2597 ensure_conversion(
2598 'symbol-mess',
2599 args=['--symbol-default=strict'],
2600 error_re=r'.*Problems determining how symbols should be converted',
2604 def overlook_symbol_mismatches():
2605 "overlook conflicting tag/branch when --trunk-only"
2607 # This is a test for issue #85.
2609 ensure_conversion('symbol-mess', args=['--trunk-only'])
2612 def force_symbols():
2613 "force symbols to be tags/branches"
2615 conv = ensure_conversion(
2616 'symbol-mess',
2617 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2618 if conv.path_exists('tags', 'BRANCH') \
2619 or not conv.path_exists('branches', 'BRANCH'):
2620 raise Failure()
2621 if not conv.path_exists('tags', 'TAG') \
2622 or conv.path_exists('branches', 'TAG'):
2623 raise Failure()
2624 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2625 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2626 raise Failure()
2627 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2628 or conv.path_exists('branches', 'MOSTLY_TAG'):
2629 raise Failure()
2632 def commit_blocks_tags():
2633 "commit prevents forced tag"
2635 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2636 ensure_conversion(
2637 'symbol-mess',
2638 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2639 error_re=(
2640 r'.*The following branches cannot be forced to be tags '
2641 r'because they have commits'
2646 def blocked_excludes():
2647 "error for blocked excludes"
2649 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2650 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2651 try:
2652 ensure_conversion(
2653 'symbol-mess',
2654 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2655 raise MissingErrorException()
2656 except Failure:
2657 pass
2660 def unblock_blocked_excludes():
2661 "excluding blocker removes blockage"
2663 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2664 for blocker in ['BRANCH', 'COMMIT']:
2665 ensure_conversion(
2666 'symbol-mess',
2667 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2668 '--exclude=BLOCKING_%s' % blocker]))
2671 def regexp_force_symbols():
2672 "force symbols via regular expressions"
2674 conv = ensure_conversion(
2675 'symbol-mess',
2676 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
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 heuristic_symbol_default():
2686 "test 'heuristic' symbol default"
2688 conv = ensure_conversion(
2689 'symbol-mess', args=['--symbol-default=heuristic'])
2690 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2691 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2692 raise Failure()
2693 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2694 or conv.path_exists('branches', 'MOSTLY_TAG'):
2695 raise Failure()
2698 def branch_symbol_default():
2699 "test 'branch' symbol default"
2701 conv = ensure_conversion(
2702 'symbol-mess', args=['--symbol-default=branch'])
2703 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2704 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2705 raise Failure()
2706 if conv.path_exists('tags', 'MOSTLY_TAG') \
2707 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2708 raise Failure()
2711 def tag_symbol_default():
2712 "test 'tag' symbol default"
2714 conv = ensure_conversion(
2715 'symbol-mess', args=['--symbol-default=tag'])
2716 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2717 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2718 raise Failure()
2719 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2720 or conv.path_exists('branches', 'MOSTLY_TAG'):
2721 raise Failure()
2724 def symbol_transform():
2725 "test --symbol-transform"
2727 conv = ensure_conversion(
2728 'symbol-mess',
2729 args=[
2730 '--symbol-default=heuristic',
2731 '--symbol-transform=BRANCH:branch',
2732 '--symbol-transform=TAG:tag',
2733 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2735 if not conv.path_exists('branches', 'branch'):
2736 raise Failure()
2737 if not conv.path_exists('tags', 'tag'):
2738 raise Failure()
2739 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2740 raise Failure()
2741 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2742 raise Failure()
2745 def write_symbol_info():
2746 "test --write-symbol-info"
2748 expected_lines = [
2749 ['0', '.trunk.',
2750 'trunk', 'trunk', '.'],
2751 ['0', 'BLOCKED_BY_UNNAMED',
2752 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2753 ['0', 'BLOCKING_COMMIT',
2754 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2755 ['0', 'BLOCKED_BY_COMMIT',
2756 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2757 ['0', 'BLOCKING_BRANCH',
2758 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2759 ['0', 'BLOCKED_BY_BRANCH',
2760 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2761 ['0', 'MOSTLY_BRANCH',
2762 '.', '.', '.'],
2763 ['0', 'MOSTLY_TAG',
2764 '.', '.', '.'],
2765 ['0', 'BRANCH_WITH_COMMIT',
2766 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2767 ['0', 'BRANCH',
2768 'branch', 'branches/BRANCH', '.trunk.'],
2769 ['0', 'TAG',
2770 'tag', 'tags/TAG', '.trunk.'],
2771 ['0', 'unlabeled-1.1.12.1.2',
2772 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2774 expected_lines.sort()
2776 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2777 try:
2778 ensure_conversion(
2779 'symbol-mess',
2780 args=[
2781 '--symbol-default=strict',
2782 '--write-symbol-info=%s' % (symbol_info_file,),
2783 '--passes=:CollateSymbolsPass',
2786 raise MissingErrorException()
2787 except Failure:
2788 pass
2789 lines = []
2790 comment_re = re.compile(r'^\s*\#')
2791 for l in open(symbol_info_file, 'r'):
2792 if comment_re.match(l):
2793 continue
2794 lines.append(l.strip().split())
2795 lines.sort()
2796 if lines != expected_lines:
2797 s = ['Symbol info incorrect\n']
2798 differ = Differ()
2799 for diffline in differ.compare(
2800 [' '.join(line) + '\n' for line in expected_lines],
2801 [' '.join(line) + '\n' for line in lines],
2803 s.append(diffline)
2804 raise Failure(''.join(s))
2807 def symbol_hints():
2808 "test --symbol-hints for setting branch/tag"
2810 conv = ensure_conversion(
2811 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2813 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2814 raise Failure()
2815 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2816 raise Failure()
2817 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2818 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2820 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2821 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2823 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2824 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2828 def parent_hints():
2829 "test --symbol-hints for setting parent"
2831 conv = ensure_conversion(
2832 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2834 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2835 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2839 def parent_hints_invalid():
2840 "test --symbol-hints with an invalid parent"
2842 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2843 # this symbol hints file sets the preferred parent to BRANCH
2844 # instead:
2845 conv = ensure_conversion(
2846 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2847 error_re=(
2848 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2853 def parent_hints_wildcards():
2854 "test --symbol-hints wildcards"
2856 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2857 # this symbol hints file sets the preferred parent to BRANCH
2858 # instead:
2859 conv = ensure_conversion(
2860 'symbol-mess',
2861 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2863 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2864 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2868 def path_hints():
2869 "test --symbol-hints for setting svn paths"
2871 conv = ensure_conversion(
2872 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
2874 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2875 ('/trunk', 'A'),
2876 ('/a', 'A'),
2877 ('/a/strange', 'A'),
2878 ('/a/strange/trunk', 'A'),
2879 ('/a/strange/trunk/path', 'A'),
2880 ('/branches', 'A'),
2881 ('/tags', 'A'),
2883 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2884 ('/special', 'A'),
2885 ('/special/tag', 'A'),
2886 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
2888 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2889 ('/special/other', 'A'),
2890 ('/special/other/branch', 'A'),
2891 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
2893 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2894 ('/special/branch', 'A'),
2895 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
2899 def issue_99():
2900 "test problem from issue 99"
2902 conv = ensure_conversion('issue-99')
2905 def issue_100():
2906 "test problem from issue 100"
2908 conv = ensure_conversion('issue-100')
2909 file1 = conv.get_wc('trunk', 'file1.txt')
2910 if file(file1).read() != 'file1.txt<1.2>\n':
2911 raise Failure()
2914 def issue_106():
2915 "test problem from issue 106"
2917 conv = ensure_conversion('issue-106')
2920 def options_option():
2921 "use of the --options option"
2923 conv = ensure_conversion('main', options_file='cvs2svn.options')
2926 def multiproject():
2927 "multiproject conversion"
2929 conv = ensure_conversion(
2930 'main', options_file='cvs2svn-multiproject.options'
2932 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2933 ('/partial-prune', 'A'),
2934 ('/partial-prune/trunk', 'A'),
2935 ('/partial-prune/branches', 'A'),
2936 ('/partial-prune/tags', 'A'),
2937 ('/partial-prune/releases', 'A'),
2941 def crossproject():
2942 "multiproject conversion with cross-project commits"
2944 conv = ensure_conversion(
2945 'main', options_file='cvs2svn-crossproject.options'
2949 def tag_with_no_revision():
2950 "tag defined but revision is deleted"
2952 conv = ensure_conversion('tag-with-no-revision')
2955 def delete_cvsignore():
2956 "svn:ignore should vanish when .cvsignore does"
2958 # This is issue #81.
2960 conv = ensure_conversion('delete-cvsignore')
2962 wc_tree = conv.get_wc_tree()
2963 props = props_for_path(wc_tree, 'trunk/proj')
2965 if props.has_key('svn:ignore'):
2966 raise Failure()
2969 def repeated_deltatext():
2970 "ignore repeated deltatext blocks with warning"
2972 conv = ensure_conversion('repeated-deltatext')
2973 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
2974 if not conv.output_found(warning_re):
2975 raise Failure()
2978 def nasty_graphs():
2979 "process some nasty dependency graphs"
2981 # It's not how well the bear can dance, but that the bear can dance
2982 # at all:
2983 conv = ensure_conversion('nasty-graphs')
2986 def tagging_after_delete():
2987 "optimal tag after deleting files"
2989 conv = ensure_conversion('tagging-after-delete')
2991 # tag should be 'clean', no deletes
2992 log = conv.find_tag_log('tag1')
2993 expected = (
2994 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
2996 log.check_changes(expected)
2999 def crossed_branches():
3000 "branches created in inconsistent orders"
3002 conv = ensure_conversion('crossed-branches')
3005 def file_directory_conflict():
3006 "error when filename conflicts with directory name"
3008 conv = ensure_conversion(
3009 'file-directory-conflict',
3010 error_re=r'.*Directory name conflicts with filename',
3014 def attic_directory_conflict():
3015 "error when attic filename conflicts with dirname"
3017 # This tests the problem reported in issue #105.
3019 conv = ensure_conversion(
3020 'attic-directory-conflict',
3021 error_re=r'.*Directory name conflicts with filename',
3025 def internal_co():
3026 "verify that --use-internal-co works"
3028 rcs_conv = ensure_conversion(
3029 'main', args=['--use-rcs', '--default-eol=native'],
3031 conv = ensure_conversion(
3032 'main', args=['--default-eol=native'],
3034 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3035 raise Failure()
3036 rcs_lines = run_program(
3037 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3038 rcs_conv.repos)
3039 lines = run_program(
3040 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3041 conv.repos)
3042 # Compare all lines following the repository UUID:
3043 if lines[3:] != rcs_lines[3:]:
3044 raise Failure()
3047 def internal_co_exclude():
3048 "verify that --use-internal-co --exclude=... works"
3050 rcs_conv = ensure_conversion(
3051 'internal-co',
3052 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
3054 conv = ensure_conversion(
3055 'internal-co',
3056 args=['--exclude=BRANCH', '--default-eol=native'],
3058 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3059 raise Failure()
3060 rcs_lines = run_program(
3061 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3062 rcs_conv.repos)
3063 lines = run_program(
3064 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3065 conv.repos)
3066 # Compare all lines following the repository UUID:
3067 if lines[3:] != rcs_lines[3:]:
3068 raise Failure()
3071 def internal_co_trunk_only():
3072 "verify that --use-internal-co --trunk-only works"
3074 conv = ensure_conversion(
3075 'internal-co',
3076 args=['--trunk-only', '--default-eol=native'],
3078 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3079 raise Failure()
3082 def leftover_revs():
3083 "check for leftover checked-out revisions"
3085 conv = ensure_conversion(
3086 'leftover-revs',
3087 args=['--exclude=BRANCH', '--default-eol=native'],
3089 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3090 raise Failure()
3093 def requires_internal_co():
3094 "test that internal co can do more than RCS"
3095 # See issues 4, 11 for the bugs whose regression we're testing for.
3096 # Unlike in requires_cvs above, issue 29 is not covered.
3097 conv = ensure_conversion('requires-cvs')
3099 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3101 if atsign_contents[-1:] == "@":
3102 raise Failure()
3104 if not (conv.logs[21].author == "William Lyon Phelps III" and
3105 conv.logs[20].author == "j random"):
3106 raise Failure()
3109 def internal_co_keywords():
3110 "test that internal co handles keywords correctly"
3111 conv_ic = ensure_conversion('internal-co-keywords',
3112 args=["--keywords-off"])
3113 conv_cvs = ensure_conversion('internal-co-keywords',
3114 args=["--use-cvs", "--keywords-off"])
3116 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3117 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3118 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3119 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3120 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3121 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3123 if ko_ic != ko_cvs:
3124 raise Failure()
3125 if kk_ic != kk_cvs:
3126 raise Failure()
3128 # The date format changed between cvs and co ('/' instead of '-').
3129 # Accept either one:
3130 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3131 if kv_ic != kv_cvs \
3132 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3133 raise Failure()
3136 def timestamp_chaos():
3137 "test timestamp adjustments"
3139 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3141 # The times are expressed here in UTC:
3142 times = [
3143 '2007-01-01 21:00:00', # Initial commit
3144 '2007-01-01 21:00:00', # revision 1.1 of both files
3145 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3146 '2007-01-01 21:00:02', # revision 1.2 of file2.txt, adjusted backwards
3147 '2007-01-01 22:00:00', # revision 1.3 of both files
3150 # Convert the times to seconds since the epoch, in UTC:
3151 times = [calendar.timegm(svn_strptime(t)) for t in times]
3153 for i in range(len(times)):
3154 if abs(conv.logs[i + 1].date - times[i]) > 0.1:
3155 raise Failure()
3158 def symlinks():
3159 "convert a repository that contains symlinks"
3161 # This is a test for issue #97.
3163 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3164 links = [
3166 os.path.join('..', 'file.txt,v'),
3167 os.path.join(proj, 'dir1', 'file.txt,v'),
3170 'dir1',
3171 os.path.join(proj, 'dir2'),
3175 try:
3176 os.symlink
3177 except AttributeError:
3178 # Apparently this OS doesn't support symlinks, so skip test.
3179 raise svntest.Skip()
3181 try:
3182 for (src,dst) in links:
3183 os.symlink(src, dst)
3185 conv = ensure_conversion('symlinks')
3186 conv.logs[2].check('', (
3187 ('/%(trunk)s/proj', 'A'),
3188 ('/%(trunk)s/proj/file.txt', 'A'),
3189 ('/%(trunk)s/proj/dir1', 'A'),
3190 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3191 ('/%(trunk)s/proj/dir2', 'A'),
3192 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3194 finally:
3195 for (src,dst) in links:
3196 os.remove(dst)
3199 def empty_trunk_path():
3200 "allow --trunk to be empty if --trunk-only"
3202 # This is a test for issue #53.
3204 conv = ensure_conversion(
3205 'main', args=['--trunk-only', '--trunk='],
3209 def preferred_parent_cycle():
3210 "handle a cycle in branch parent preferences"
3212 conv = ensure_conversion('preferred-parent-cycle')
3215 def branch_from_empty_dir():
3216 "branch from an empty directory"
3218 conv = ensure_conversion('branch-from-empty-dir')
3221 def trunk_readd():
3222 "add a file on a branch then on trunk"
3224 conv = ensure_conversion('trunk-readd')
3227 def branch_from_deleted_1_1():
3228 "branch from a 1.1 revision that will be deleted"
3230 conv = ensure_conversion('branch-from-deleted-1-1')
3231 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3232 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3234 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3235 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3237 conv.logs[7].check('Adding b.txt:1.2', (
3238 ('/%(trunk)s/proj/b.txt', 'A'),
3241 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3242 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3244 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3245 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3249 def add_on_branch():
3250 "add a file on a branch using newer CVS"
3252 conv = ensure_conversion('add-on-branch')
3253 conv.logs[6].check('Adding b.txt:1.1', (
3254 ('/%(trunk)s/proj/b.txt', 'A'),
3256 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3257 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3259 conv.logs[8].check('Adding c.txt:1.1', (
3260 ('/%(trunk)s/proj/c.txt', 'A'),
3262 conv.logs[9].check('Removing c.txt:1.2', (
3263 ('/%(trunk)s/proj/c.txt', 'D'),
3265 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3266 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3268 conv.logs[11].check('Adding d.txt:1.1', (
3269 ('/%(trunk)s/proj/d.txt', 'A'),
3271 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3272 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3276 def main_git():
3277 "test output in git-fast-import format"
3279 # Note: To test importing into git, do
3281 # ./run-tests <test-number>
3282 # rm -rf .git
3283 # git-init
3284 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3286 # Or, to load the dumpfiles separately:
3288 # cat cvs2svn-tmp/git-blob.dat \
3289 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3290 # cat cvs2svn-tmp/git-dump.dat \
3291 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3293 # Then use "gitk --all", "git log", etc. to test the contents of the
3294 # repository.
3296 # We don't have the infrastructure to check that the resulting git
3297 # repository is correct, so we just check that the conversion runs
3298 # to completion:
3299 conv = GitConversion('main', None, [
3300 '--blobfile=cvs2svn-tmp/blobfile.out',
3301 '--dumpfile=cvs2svn-tmp/dumpfile.out',
3302 '--username=cvs2git',
3303 'test-data/main-cvsrepos',
3307 def git_options():
3308 "test cvs2git using options file"
3310 conv = GitConversion('main', None, [], options_file='cvs2git.options')
3313 def main_hg():
3314 "output in git-fast-import format with inline data"
3316 # The output should be suitable for import by Mercurial.
3318 # Note: To test importing into git, do
3320 # ./run-tests <test-number>
3321 # rm -rf .git
3322 # git-init
3323 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3325 # Then use "gitk --all", "git log", etc. to test the contents of the
3326 # repository.
3328 # We don't have the infrastructure to check that the resulting
3329 # Mercurial repository is correct, so we just check that the
3330 # conversion runs to completion:
3331 conv = GitConversion('main', None, [], options_file='cvs2hg.options')
3334 def invalid_symbol():
3335 "a symbol with the incorrect format"
3337 conv = ensure_conversion('invalid-symbol')
3338 if not conv.output_found(
3339 r".*branch 'SYMBOL' references invalid revision 1$"
3341 raise Failure()
3344 def invalid_symbol_ignore():
3345 "ignore a symbol using a SymbolMapper"
3347 conv = ensure_conversion(
3348 'invalid-symbol', options_file='cvs2svn-ignore.options'
3352 def invalid_symbol_ignore2():
3353 "ignore a symbol using an IgnoreSymbolTransform"
3355 conv = ensure_conversion(
3356 'invalid-symbol', options_file='cvs2svn-ignore2.options'
3360 class EOLVariants(Cvs2SvnTestCase):
3361 "handle various --eol-style options"
3363 eol_style_strings = {
3364 'LF' : '\n',
3365 'CR' : '\r',
3366 'CRLF' : '\r\n',
3367 'native' : '\n',
3370 def __init__(self, eol_style):
3371 self.eol_style = eol_style
3372 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3373 Cvs2SvnTestCase.__init__(
3374 self, 'eol-variants', variant=self.eol_style,
3375 dumpfile=self.dumpfile,
3376 args=[
3377 '--default-eol=%s' % (self.eol_style,),
3381 def run(self):
3382 conv = self.ensure_conversion()
3383 dump_contents = open(conv.dumpfile, 'rb').read()
3384 expected_text = self.eol_style_strings[self.eol_style].join(
3385 ['line 1', 'line 2', '\n\n']
3387 if not dump_contents.endswith(expected_text):
3388 raise Failure()
3391 def no_revs_file():
3392 "handle a file with no revisions (issue #80)"
3394 conv = ensure_conversion('no-revs-file')
3397 def mirror_keyerror_test():
3398 "a case that gave KeyError in SVNRepositoryMirror"
3400 conv = ensure_conversion('mirror-keyerror')
3403 def exclude_ntdb_test():
3404 "exclude a non-trunk default branch"
3406 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3407 conv = ensure_conversion(
3408 'exclude-ntdb',
3409 args=[
3410 '--write-symbol-info=%s' % (symbol_info_file,),
3411 '--exclude=branch3',
3412 '--exclude=tag3',
3413 '--exclude=vendortag3',
3414 '--exclude=vendorbranch',
3419 def mirror_keyerror2_test():
3420 "a case that gave KeyError in RepositoryMirror"
3422 conv = ensure_conversion('mirror-keyerror2')
3425 def mirror_keyerror3_test():
3426 "a case that gave KeyError in RepositoryMirror"
3428 conv = ensure_conversion('mirror-keyerror3')
3431 def add_cvsignore_to_branch_test():
3432 "check adding .cvsignore to an existing branch"
3434 # This a test for issue #122.
3436 conv = ensure_conversion('add-cvsignore-to-branch')
3437 wc_tree = conv.get_wc_tree()
3438 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3439 if trunk_props['svn:ignore'] != '*.o\n\n':
3440 raise Failure()
3442 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3443 if branch_props['svn:ignore'] != '*.o\n\n':
3444 raise Failure()
3447 def missing_deltatext():
3448 "a revision's deltatext is missing"
3450 # This is a type of RCS file corruption that has been observed.
3451 conv = ensure_conversion(
3452 'missing-deltatext',
3453 error_re=(
3454 r"ERROR\: .* has no deltatext section for revision 1\.1\.4\.4"
3459 ########################################################################
3460 # Run the tests
3462 # list all tests here, starting with None:
3463 test_list = [
3464 None,
3465 # 1:
3466 show_usage,
3467 cvs2svn_manpage,
3468 cvs2git_manpage,
3469 attr_exec,
3470 space_fname,
3471 two_quick,
3472 PruneWithCare(),
3473 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3474 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3475 # 10:
3476 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3477 interleaved_commits,
3478 simple_commits,
3479 SimpleTags(),
3480 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3481 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3482 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3483 simple_branch_commits,
3484 mixed_time_tag,
3485 mixed_time_branch_with_added_file,
3486 # 20:
3487 mixed_commit,
3488 split_time_branch,
3489 bogus_tag,
3490 overlapping_branch,
3491 PhoenixBranch(),
3492 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3493 ctrl_char_in_log,
3494 overdead,
3495 NoTrunkPrune(),
3496 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3497 # 30:
3498 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3499 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3500 double_delete,
3501 split_branch,
3502 resync_misgroups,
3503 TaggedBranchAndTrunk(),
3504 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3505 enroot_race,
3506 enroot_race_obo,
3507 BranchDeleteFirst(),
3508 # 40:
3509 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3510 nonascii_filenames,
3511 UnicodeAuthor(
3512 warning_expected=1),
3513 UnicodeAuthor(
3514 warning_expected=0,
3515 variant='encoding', args=['--encoding=utf_8']),
3516 UnicodeAuthor(
3517 warning_expected=0,
3518 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3519 UnicodeLog(
3520 warning_expected=1),
3521 UnicodeLog(
3522 warning_expected=0,
3523 variant='encoding', args=['--encoding=utf_8']),
3524 UnicodeLog(
3525 warning_expected=0,
3526 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3527 vendor_branch_sameness,
3528 vendor_branch_trunk_only,
3529 # 50:
3530 default_branches,
3531 default_branches_trunk_only,
3532 default_branch_and_1_2,
3533 compose_tag_three_sources,
3534 pass5_when_to_fill,
3535 PeerPathPruning(),
3536 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3537 EmptyTrunk(),
3538 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3539 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3540 # 60:
3541 no_spurious_svn_commits,
3542 invalid_closings_on_trunk,
3543 individual_passes,
3544 resync_bug,
3545 branch_from_default_branch,
3546 file_in_attic_too,
3547 retain_file_in_attic_too,
3548 symbolic_name_filling_guide,
3549 eol_mime1,
3550 eol_mime2,
3551 # 70:
3552 eol_mime3,
3553 eol_mime4,
3554 cvs_revnums_off,
3555 cvs_revnums_on,
3556 keywords,
3557 ignore,
3558 requires_cvs,
3559 questionable_branch_names,
3560 questionable_tag_names,
3561 revision_reorder_bug,
3562 # 80:
3563 exclude,
3564 vendor_branch_delete_add,
3565 resync_pass2_pull_forward,
3566 native_eol,
3567 double_fill,
3568 XFail(double_fill2),
3569 resync_pass2_push_backward,
3570 double_add,
3571 bogus_branch_copy,
3572 nested_ttb_directories,
3573 # 90:
3574 auto_props_ignore_case,
3575 ctrl_char_in_filename,
3576 commit_dependencies,
3577 show_help_passes,
3578 multiple_tags,
3579 multiply_defined_symbols,
3580 multiply_defined_symbols_renamed,
3581 multiply_defined_symbols_ignored,
3582 repeatedly_defined_symbols,
3583 double_branch_delete,
3584 # 100:
3585 symbol_mismatches,
3586 overlook_symbol_mismatches,
3587 force_symbols,
3588 commit_blocks_tags,
3589 blocked_excludes,
3590 unblock_blocked_excludes,
3591 regexp_force_symbols,
3592 heuristic_symbol_default,
3593 branch_symbol_default,
3594 tag_symbol_default,
3595 # 110:
3596 symbol_transform,
3597 write_symbol_info,
3598 symbol_hints,
3599 parent_hints,
3600 parent_hints_invalid,
3601 parent_hints_wildcards,
3602 path_hints,
3603 issue_99,
3604 issue_100,
3605 issue_106,
3606 # 120:
3607 options_option,
3608 multiproject,
3609 crossproject,
3610 tag_with_no_revision,
3611 delete_cvsignore,
3612 repeated_deltatext,
3613 nasty_graphs,
3614 XFail(tagging_after_delete),
3615 crossed_branches,
3616 file_directory_conflict,
3617 # 130:
3618 attic_directory_conflict,
3619 internal_co,
3620 internal_co_exclude,
3621 internal_co_trunk_only,
3622 internal_co_keywords,
3623 leftover_revs,
3624 requires_internal_co,
3625 timestamp_chaos,
3626 symlinks,
3627 empty_trunk_path,
3628 # 140:
3629 preferred_parent_cycle,
3630 branch_from_empty_dir,
3631 trunk_readd,
3632 branch_from_deleted_1_1,
3633 add_on_branch,
3634 main_git,
3635 git_options,
3636 main_hg,
3637 invalid_symbol,
3638 invalid_symbol_ignore,
3639 # 150:
3640 invalid_symbol_ignore2,
3641 EOLVariants('LF'),
3642 EOLVariants('CR'),
3643 EOLVariants('CRLF'),
3644 EOLVariants('native'),
3645 no_revs_file,
3646 mirror_keyerror_test,
3647 exclude_ntdb_test,
3648 mirror_keyerror2_test,
3649 mirror_keyerror3_test,
3650 # 160:
3651 XFail(add_cvsignore_to_branch_test),
3652 missing_deltatext,
3655 if __name__ == '__main__':
3657 # Configure the environment for reproducable output from svn, etc.
3658 os.environ["LC_ALL"] = "C"
3660 # Unfortunately, there is no way under Windows to make Subversion
3661 # think that the local time zone is UTC, so we just work in the
3662 # local time zone.
3664 # The Subversion test suite code assumes it's being invoked from
3665 # within a working copy of the Subversion sources, and tries to use
3666 # the binaries in that tree. Since the cvs2svn tree never contains
3667 # a Subversion build, we just use the system's installed binaries.
3668 svntest.main.svn_binary = 'svn'
3669 svntest.main.svnlook_binary = 'svnlook'
3670 svntest.main.svnadmin_binary = 'svnadmin'
3671 svntest.main.svnversion_binary = 'svnversion'
3673 run_tests(test_list)
3674 # NOTREACHED
3677 ### End of file.