Use a for loop instead of a while loop.
[cvs2svn.git] / run-tests.py
blob4fef2dbcd3d009cb9143a2f471381b389d892afc
1 #!/usr/bin/env python
3 # run_tests.py: test suite for cvs2svn
5 # Usage: run_tests.py [-v | --verbose] [list | <num>]
7 # Options:
8 # -v, --verbose
9 # enable verbose output
11 # Arguments (at most one argument is allowed):
12 # list
13 # If the word "list" is passed as an argument, the list of
14 # available tests is printed (but no tests are run).
16 # <num>
17 # If a number is passed as an argument, then only the test
18 # with that number is run.
20 # If no argument is specified, then all tests are run.
22 # Subversion is a tool for revision control.
23 # See http://subversion.tigris.org for more information.
25 # ====================================================================
26 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
28 # This software is licensed as described in the file COPYING, which
29 # you should have received as part of this distribution. The terms
30 # are also available at http://subversion.tigris.org/license-1.html.
31 # If newer versions of this license are posted there, you may use a
32 # newer version instead, at your option.
34 ######################################################################
36 # General modules
37 import sys
38 import shutil
39 import stat
40 import re
41 import os
42 import time
43 import os.path
44 import locale
45 import textwrap
46 from difflib import Differ
48 # Make sure this Python is recent enough.
49 if sys.hexversion < 0x02040000:
50 sys.stderr.write(
51 'error: Python 2.4 or higher required; see www.python.org.\n'
53 sys.exit(1)
55 # This script needs to run in the correct directory. Make sure we're there.
56 if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
57 sys.stderr.write("error: I need to be run in the directory containing "
58 "'cvs2svn' and 'test-data'.\n")
59 sys.exit(1)
61 # Load the Subversion test framework.
62 import svntest
63 from svntest import Failure
64 from svntest.main import run_command
65 from svntest.main import run_tests
66 from svntest.main import safe_rmtree
67 from svntest.testcase import TestCase
68 from svntest.testcase import Skip
69 from svntest.testcase import XFail
70 from svntest.tree import build_tree_from_wc
71 from svntest.tree import get_child
73 cvs2svn = os.path.abspath('cvs2svn')
75 # We use the installed svn and svnlook binaries, instead of using
76 # svntest.main.run_svn() and svntest.main.run_svnlook(), because the
77 # behavior -- or even existence -- of local builds shouldn't affect
78 # the cvs2svn test suite.
79 svn = 'svn'
80 svnlook = 'svnlook'
82 test_data_dir = 'test-data'
83 tmp_dir = 'cvs2svn-tmp'
86 #----------------------------------------------------------------------
87 # Helpers.
88 #----------------------------------------------------------------------
91 # The value to expect for svn:keywords if it is set:
92 KEYWORDS = 'Author Date Id Revision'
95 class RunProgramException(Failure):
96 pass
99 class MissingErrorException(Failure):
100 def __init__(self, error_re):
101 Failure.__init__(
102 self, "Test failed because no error matched '%s'" % (error_re,)
106 def run_program(program, error_re, *varargs):
107 """Run PROGRAM with VARARGS, return stdout as a list of lines.
109 If there is any stderr and ERROR_RE is None, raise
110 RunProgramException, and print the stderr lines if
111 svntest.main.verbose_mode is true.
113 If ERROR_RE is not None, it is a string regular expression that must
114 match some line of stderr. If it fails to match, raise
115 MissingErrorExpection."""
117 # FIXME: exit_code is currently ignored.
118 exit_code, out, err = run_command(program, 1, 0, *varargs)
120 if error_re:
121 # Specified error expected on stderr.
122 if not err:
123 raise MissingErrorException(error_re)
124 else:
125 for line in err:
126 if re.match(error_re, line):
127 return out
128 raise MissingErrorException(error_re)
129 else:
130 # No stderr allowed.
131 if err:
132 if svntest.main.verbose_mode:
133 print '\n%s said:\n' % program
134 for line in err:
135 print ' ' + line,
136 print
137 raise RunProgramException()
139 return out
142 def run_cvs2svn(error_re, *varargs):
143 """Run cvs2svn with VARARGS, return stdout as a list of lines.
145 If there is any stderr and ERROR_RE is None, raise
146 RunProgramException, and print the stderr lines if
147 svntest.main.verbose_mode is true.
149 If ERROR_RE is not None, it is a string regular expression that must
150 match some line of stderr. If it fails to match, raise
151 MissingErrorException."""
153 # Use the same python that is running this script
154 return run_program(sys.executable, error_re, cvs2svn, *varargs)
155 # On Windows, for an unknown reason, the cmd.exe process invoked by
156 # os.system('sort ...') in cvs2svn receives invalid stdio handles, if
157 # cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
158 # this. Therefore, the redirection of the output to the .s-revs file fails.
159 # We no longer use the problematic invocation on any system, but this
160 # comment remains to warn about this problem.
163 def run_svn(*varargs):
164 """Run svn with VARARGS; return stdout as a list of lines.
165 If there is any stderr, raise RunProgramException, and print the
166 stderr lines if svntest.main.verbose_mode is true."""
167 return run_program(svn, None, *varargs)
170 def repos_to_url(path_to_svn_repos):
171 """This does what you think it does."""
172 rpath = os.path.abspath(path_to_svn_repos)
173 if rpath[0] != '/':
174 rpath = '/' + rpath
175 return 'file://%s' % rpath.replace(os.sep, '/')
178 def svn_strptime(timestr):
179 return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
182 class Log:
183 def __init__(self, revision, author, date, symbols):
184 self.revision = revision
185 self.author = author
187 # Internally, we represent the date as seconds since epoch (UTC).
188 # Since standard subversion log output shows dates in localtime
190 # "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
192 # and time.mktime() converts from localtime, it all works out very
193 # happily.
194 self.date = time.mktime(svn_strptime(date[0:19]))
196 # The following symbols are used for string interpolation when
197 # checking paths:
198 self.symbols = symbols
200 # The changed paths will be accumulated later, as log data is read.
201 # Keys here are paths such as '/trunk/foo/bar', values are letter
202 # codes such as 'M', 'A', and 'D'.
203 self.changed_paths = { }
205 # The msg will be accumulated later, as log data is read.
206 self.msg = ''
208 def absorb_changed_paths(self, out):
209 'Read changed paths from OUT into self, until no more.'
210 while 1:
211 line = out.readline()
212 if len(line) == 1: return
213 line = line[:-1]
214 op_portion = line[3:4]
215 path_portion = line[5:]
216 # If we're running on Windows we get backslashes instead of
217 # forward slashes.
218 path_portion = path_portion.replace('\\', '/')
219 # # We could parse out history information, but currently we
220 # # just leave it in the path portion because that's how some
221 # # tests expect it.
223 # m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
224 # if m:
225 # path_portion = m.group(1)
226 self.changed_paths[path_portion] = op_portion
228 def __cmp__(self, other):
229 return cmp(self.revision, other.revision) or \
230 cmp(self.author, other.author) or cmp(self.date, other.date) or \
231 cmp(self.changed_paths, other.changed_paths) or \
232 cmp(self.msg, other.msg)
234 def get_path_op(self, path):
235 """Return the operator for the change involving PATH.
237 PATH is allowed to include string interpolation directives (e.g.,
238 '%(trunk)s'), which are interpolated against self.symbols. Return
239 None if there is no record for PATH."""
240 return self.changed_paths.get(path % self.symbols)
242 def check_msg(self, msg):
243 """Verify that this Log's message starts with the specified MSG."""
244 if self.msg.find(msg) != 0:
245 raise Failure(
246 "Revision %d log message was:\n%s\n\n"
247 "It should have begun with:\n%s\n\n"
248 % (self.revision, self.msg, msg,)
251 def check_change(self, path, op):
252 """Verify that this Log includes a change for PATH with operator OP.
254 PATH is allowed to include string interpolation directives (e.g.,
255 '%(trunk)s'), which are interpolated against self.symbols."""
257 path = path % self.symbols
258 found_op = self.changed_paths.get(path, None)
259 if found_op is None:
260 raise Failure(
261 "Revision %d does not include change for path %s "
262 "(it should have been %s).\n"
263 % (self.revision, path, op,)
265 if found_op != op:
266 raise Failure(
267 "Revision %d path %s had op %s (it should have been %s)\n"
268 % (self.revision, path, found_op, op,)
271 def check_changes(self, changed_paths):
272 """Verify that this Log has precisely the CHANGED_PATHS specified.
274 CHANGED_PATHS is a sequence of tuples (path, op), where the paths
275 strings are allowed to include string interpolation directives
276 (e.g., '%(trunk)s'), which are interpolated against self.symbols."""
278 cp = {}
279 for (path, op) in changed_paths:
280 cp[path % self.symbols] = op
282 if self.changed_paths != cp:
283 raise Failure(
284 "Revision %d changed paths list was:\n%s\n\n"
285 "It should have been:\n%s\n\n"
286 % (self.revision, self.changed_paths, cp,)
289 def check(self, msg, changed_paths):
290 """Verify that this Log has the MSG and CHANGED_PATHS specified.
292 Convenience function to check two things at once. MSG is passed
293 to check_msg(); CHANGED_PATHS is passed to check_changes()."""
295 self.check_msg(msg)
296 self.check_changes(changed_paths)
299 def parse_log(svn_repos, symbols):
300 """Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
302 Initialize the Logs' symbols with SYMBOLS."""
304 class LineFeeder:
305 'Make a list of lines behave like an open file handle.'
306 def __init__(self, lines):
307 self.lines = lines
308 def readline(self):
309 if len(self.lines) > 0:
310 return self.lines.pop(0)
311 else:
312 return None
314 def absorb_message_body(out, num_lines, log):
315 """Read NUM_LINES of log message body from OUT into Log item LOG."""
317 for i in range(num_lines):
318 log.msg += out.readline()
320 log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
321 '(?P<author>[^\|]+) \| '
322 '(?P<date>[^\|]+) '
323 '\| (?P<lines>[0-9]+) (line|lines)$')
325 log_separator = '-' * 72
327 logs = { }
329 out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
331 while 1:
332 this_log = None
333 line = out.readline()
334 if not line: break
335 line = line[:-1]
337 if line.find(log_separator) == 0:
338 line = out.readline()
339 if not line: break
340 line = line[:-1]
341 m = log_start_re.match(line)
342 if m:
343 this_log = Log(
344 int(m.group('rev')), m.group('author'), m.group('date'), symbols)
345 line = out.readline()
346 if not line.find('Changed paths:') == 0:
347 print 'unexpected log output (missing changed paths)'
348 print "Line: '%s'" % line
349 sys.exit(1)
350 this_log.absorb_changed_paths(out)
351 absorb_message_body(out, int(m.group('lines')), this_log)
352 logs[this_log.revision] = this_log
353 elif len(line) == 0:
354 break # We've reached the end of the log output.
355 else:
356 print 'unexpected log output (missing revision line)'
357 print "Line: '%s'" % line
358 sys.exit(1)
359 else:
360 print 'unexpected log output (missing log separator)'
361 print "Line: '%s'" % line
362 sys.exit(1)
364 return logs
367 def erase(path):
368 """Unconditionally remove PATH and its subtree, if any. PATH may be
369 non-existent, a file or symlink, or a directory."""
370 if os.path.isdir(path):
371 safe_rmtree(path)
372 elif os.path.exists(path):
373 os.remove(path)
376 log_msg_text_wrapper = textwrap.TextWrapper(width=76)
378 def sym_log_msg(symbolic_name, is_tag=None):
379 """Return the expected log message for a cvs2svn-synthesized revision
380 creating branch or tag SYMBOLIC_NAME."""
382 # This reproduces the logic in SVNSymbolCommit.get_log_msg().
383 if is_tag:
384 type = 'tag'
385 else:
386 type = 'branch'
388 return log_msg_text_wrapper.fill(
389 "This commit was manufactured by cvs2svn to create %s '%s'."
390 % (type, symbolic_name)
394 def make_conversion_id(
395 name, args, passbypass, options_file=None, symbol_hints_file=None
397 """Create an identifying tag for a conversion.
399 The return value can also be used as part of a filesystem path.
401 NAME is the name of the CVS repository.
403 ARGS are the extra arguments to be passed to cvs2svn.
405 PASSBYPASS is a boolean indicating whether the conversion is to be
406 run one pass at a time.
408 If OPTIONS_FILE is specified, it is an options file that will be
409 used for the conversion.
411 If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
412 will be used for the conversion.
414 The 1-to-1 mapping between cvs2svn command parameters and
415 conversion_ids allows us to avoid running the same conversion more
416 than once, when multiple tests use exactly the same conversion."""
418 conv_id = name
420 _win32_fname_mapping = { '/': '_sl_', '\\': '_bs_', ':': '_co_',
421 '*': '_st_', '?': '_qm_', '"': '_qq_',
422 '<': '_lt_', '>': '_gt_', '|': '_pi_', }
423 for arg in args:
424 # Replace some characters that Win32 isn't happy about having in a
425 # filename (which was causing the eol_mime test to fail).
426 sanitized_arg = arg
427 for a, b in _win32_fname_mapping.items():
428 sanitized_arg = sanitized_arg.replace(a, b)
429 conv_id += sanitized_arg
431 if passbypass:
432 conv_id += '-passbypass'
434 if options_file is not None:
435 conv_id += '--options=%s' % options_file
437 if symbol_hints_file is not None:
438 conv_id += '--symbol-hints=%s' % symbol_hints_file
440 return conv_id
443 class Conversion:
444 """A record of a cvs2svn conversion.
446 Fields:
448 conv_id -- the conversion id for this Conversion.
450 name -- a one-word name indicating the involved repositories.
452 dumpfile -- the name of the SVN dumpfile created by the conversion
453 (if the DUMPFILE constructor argument was used); otherwise,
454 None.
456 repos -- the path to the svn repository. Unset if DUMPFILE was
457 specified.
459 logs -- a dictionary of Log instances, as returned by parse_log().
460 Unset if DUMPFILE was specified.
462 symbols -- a dictionary of symbols used for string interpolation
463 in path names.
465 stdout -- a list of lines written by cvs2svn to stdout
467 _wc -- the basename of the svn working copy (within tmp_dir).
468 Unset if DUMPFILE was specified.
470 _wc_path -- the path to the svn working copy, if it has already
471 been created; otherwise, None. (The working copy is created
472 lazily when get_wc() is called.) Unset if DUMPFILE was
473 specified.
475 _wc_tree -- the tree built from the svn working copy, if it has
476 already been created; otherwise, None. The tree is created
477 lazily when get_wc_tree() is called.) Unset if DUMPFILE was
478 specified.
480 _svnrepos -- the basename of the svn repository (within tmp_dir).
481 Unset if DUMPFILE was specified."""
483 # The number of the last cvs2svn pass (determined lazily by
484 # get_last_pass()).
485 last_pass = None
487 @classmethod
488 def get_last_pass(cls):
489 """Return the number of cvs2svn's last pass."""
491 if cls.last_pass is None:
492 out = run_cvs2svn(None, '--help-passes')
493 cls.last_pass = int(out[-1].split()[0])
494 return cls.last_pass
496 def __init__(
497 self, conv_id, name, error_re, passbypass, symbols, args,
498 options_file=None, symbol_hints_file=None, dumpfile=None,
500 self.conv_id = conv_id
501 self.name = name
502 self.symbols = symbols
503 if not os.path.isdir(tmp_dir):
504 os.mkdir(tmp_dir)
506 cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
508 if dumpfile:
509 self.dumpfile = os.path.join(tmp_dir, dumpfile)
510 # Clean up from any previous invocations of this script.
511 erase(self.dumpfile)
512 else:
513 self.dumpfile = None
514 self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
515 self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
516 self._wc_path = None
517 self._wc_tree = None
519 # Clean up from any previous invocations of this script.
520 erase(self.repos)
521 erase(self._wc)
523 args = list(args)
524 if options_file:
525 self.options_file = os.path.join(cvsrepos, options_file)
526 args.extend([
527 '--options=%s' % self.options_file,
529 assert not symbol_hints_file
530 else:
531 self.options_file = None
532 if tmp_dir != 'cvs2svn-tmp':
533 # Only include this argument if it differs from cvs2svn's default:
534 args.extend([
535 '--tmpdir=%s' % tmp_dir,
538 if symbol_hints_file:
539 self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
540 args.extend([
541 '--symbol-hints=%s' % self.symbol_hints_file,
544 if self.dumpfile:
545 args.extend(['--dumpfile=%s' % (self.dumpfile,)])
546 else:
547 args.extend(['-s', self.repos])
548 args.extend([cvsrepos])
550 if passbypass:
551 self.stdout = []
552 for p in range(1, self.get_last_pass() + 1):
553 self.stdout += run_cvs2svn(error_re, '-p', str(p), *args)
554 else:
555 self.stdout = run_cvs2svn(error_re, *args)
557 if self.dumpfile:
558 if not os.path.isfile(self.dumpfile):
559 raise Failure(
560 "Dumpfile not created: '%s'"
561 % os.path.join(os.getcwd(), self.dumpfile)
563 else:
564 if os.path.isdir(self.repos):
565 self.logs = parse_log(self.repos, self.symbols)
566 elif error_re is None:
567 raise Failure(
568 "Repository not created: '%s'"
569 % os.path.join(os.getcwd(), self.repos)
572 def output_found(self, pattern):
573 """Return True if PATTERN matches any line in self.stdout.
575 PATTERN is a regular expression pattern as a string.
578 pattern_re = re.compile(pattern)
580 for line in self.stdout:
581 if pattern_re.match(line):
582 # We found the pattern that we were looking for.
583 return 1
584 else:
585 return 0
587 def find_tag_log(self, tagname):
588 """Search LOGS for a log message containing 'TAGNAME' and return the
589 log in which it was found."""
590 for i in xrange(len(self.logs), 0, -1):
591 if self.logs[i].msg.find("'"+tagname+"'") != -1:
592 return self.logs[i]
593 raise ValueError("Tag %s not found in logs" % tagname)
595 def get_wc(self, *args):
596 """Return the path to the svn working copy, or a path within the WC.
598 If a working copy has not been created yet, create it now.
600 If ARGS are specified, then they should be strings that form
601 fragments of a path within the WC. They are joined using
602 os.path.join() and appended to the WC path."""
604 if self._wc_path is None:
605 run_svn('co', repos_to_url(self.repos), self._wc)
606 self._wc_path = self._wc
607 return os.path.join(self._wc_path, *args)
609 def get_wc_tree(self):
610 if self._wc_tree is None:
611 self._wc_tree = build_tree_from_wc(self.get_wc(), 1)
612 return self._wc_tree
614 def path_exists(self, *args):
615 """Return True if the specified path exists within the repository.
617 (The strings in ARGS are first joined into a path using
618 os.path.join().)"""
620 return os.path.exists(self.get_wc(*args))
622 def check_props(self, keys, checks):
623 """Helper function for checking lots of properties. For a list of
624 files in the conversion, check that the values of the properties
625 listed in KEYS agree with those listed in CHECKS. CHECKS is a
626 list of tuples: [ (filename, [value, value, ...]), ...], where the
627 values are listed in the same order as the key names are listed in
628 KEYS."""
630 for (file, values) in checks:
631 assert len(values) == len(keys)
632 props = props_for_path(self.get_wc_tree(), file)
633 for i in range(len(keys)):
634 if props.get(keys[i]) != values[i]:
635 raise Failure(
636 "File %s has property %s set to \"%s\" "
637 "(it should have been \"%s\").\n"
638 % (file, keys[i], props.get(keys[i]), values[i],)
642 # Cache of conversions that have already been done. Keys are conv_id;
643 # values are Conversion instances.
644 already_converted = { }
646 def ensure_conversion(
647 name, error_re=None, passbypass=None,
648 trunk=None, branches=None, tags=None,
649 args=None, options_file=None, symbol_hints_file=None, dumpfile=None,
651 """Convert CVS repository NAME to Subversion, but only if it has not
652 been converted before by this invocation of this script. If it has
653 been converted before, return the Conversion object from the
654 previous invocation.
656 If no error, return a Conversion instance.
658 If ERROR_RE is a string, it is a regular expression expected to
659 match some line of stderr printed by the conversion. If there is an
660 error and ERROR_RE is not set, then raise Failure.
662 If PASSBYPASS is set, then cvs2svn is run multiple times, each time
663 with a -p option starting at 1 and increasing to a (hardcoded) maximum.
665 NAME is just one word. For example, 'main' would mean to convert
666 './test-data/main-cvsrepos', and after the conversion, the resulting
667 Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
668 a checked out head working copy in './cvs2svn-tmp/main-wc'.
670 Any other options to pass to cvs2svn should be in ARGS, each element
671 being one option, e.g., '--trunk-only'. If the option takes an
672 argument, include it directly, e.g., '--mime-types=PATH'. Arguments
673 are passed to cvs2svn in the order that they appear in ARGS.
675 If OPTIONS_FILE is specified, then it should be the name of a file
676 within the main directory of the cvs repository associated with this
677 test. It is passed to cvs2svn using the --options option (which
678 suppresses some other options that are incompatible with --options).
680 If SYMBOL_HINTS_FILE is specified, then it should be the name of a
681 file within the main directory of the cvs repository associated with
682 this test. It is passed to cvs2svn using the --symbol-hints option.
684 If DUMPFILE is specified, then it is the name of a dumpfile within
685 the temporary directory to which the conversion output should be
686 written."""
688 if args is None:
689 args = []
690 else:
691 args = list(args)
693 if trunk is None:
694 trunk = 'trunk'
695 else:
696 args.append('--trunk=%s' % (trunk,))
698 if branches is None:
699 branches = 'branches'
700 else:
701 args.append('--branches=%s' % (branches,))
703 if tags is None:
704 tags = 'tags'
705 else:
706 args.append('--tags=%s' % (tags,))
708 conv_id = make_conversion_id(
709 name, args, passbypass, options_file, symbol_hints_file
712 if conv_id not in already_converted:
713 try:
714 # Run the conversion and store the result for the rest of this
715 # session:
716 already_converted[conv_id] = Conversion(
717 conv_id, name, error_re, passbypass,
718 {'trunk' : trunk, 'branches' : branches, 'tags' : tags},
719 args, options_file, symbol_hints_file, dumpfile,
721 except Failure:
722 # Remember the failure so that a future attempt to run this conversion
723 # does not bother to retry, but fails immediately.
724 already_converted[conv_id] = None
725 raise
727 conv = already_converted[conv_id]
728 if conv is None:
729 raise Failure()
730 return conv
733 class Cvs2SvnTestCase(TestCase):
734 def __init__(
735 self, name, description=None, variant=None,
736 error_re=None, passbypass=None,
737 trunk=None, branches=None, tags=None,
738 args=None,
739 options_file=None, symbol_hints_file=None, dumpfile=None,
741 TestCase.__init__(self)
742 self.name = name
744 if description is not None:
745 self._description = description
746 else:
747 # By default, use the first line of the class docstring as the
748 # description:
749 self._description = self.__doc__.splitlines()[0]
751 # Check that the original description is OK before we tinker with
752 # it:
753 self.check_description()
755 if variant is not None:
756 # Modify description to show the variant. Trim description
757 # first if necessary to stay within the 50-character limit.
758 suffix = '...variant %s' % (variant,)
759 self._description = self._description[:50 - len(suffix)] + suffix
760 # Check that the description is still OK:
761 self.check_description()
763 self.error_re = error_re
764 self.passbypass = passbypass
765 self.trunk = trunk
766 self.branches = branches
767 self.tags = tags
768 self.args = args
769 self.options_file = options_file
770 self.symbol_hints_file = symbol_hints_file
771 self.dumpfile = dumpfile
773 def get_description(self):
774 return self._description
776 def ensure_conversion(self):
777 return ensure_conversion(
778 self.name,
779 error_re=self.error_re, passbypass=self.passbypass,
780 trunk=self.trunk, branches=self.branches, tags=self.tags,
781 args=self.args,
782 options_file=self.options_file,
783 symbol_hints_file=self.symbol_hints_file,
784 dumpfile=self.dumpfile,
788 class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
789 """Test properties resulting from a conversion."""
791 def __init__(self, name, props_to_test, expected_props, **kw):
792 """Initialize an instance of Cvs2SvnPropertiesTestCase.
794 NAME is the name of the test, passed to Cvs2SvnTestCase.
795 PROPS_TO_TEST is a list of the names of svn properties that should
796 be tested. EXPECTED_PROPS is a list of tuples [(filename,
797 [value,...])], where the second item in each tuple is a list of
798 values expected for the properties listed in PROPS_TO_TEST for the
799 specified filename. If a property must *not* be set, then its
800 value should be listed as None."""
802 Cvs2SvnTestCase.__init__(self, name, **kw)
803 self.props_to_test = props_to_test
804 self.expected_props = expected_props
806 def run(self):
807 conv = self.ensure_conversion()
808 conv.check_props(self.props_to_test, self.expected_props)
811 #----------------------------------------------------------------------
812 # Tests.
813 #----------------------------------------------------------------------
816 def show_usage():
817 "cvs2svn with no arguments shows usage"
818 out = run_cvs2svn(None)
819 if (len(out) > 2 and out[0].find('ERROR:') == 0
820 and out[1].find('DBM module')):
821 print 'cvs2svn cannot execute due to lack of proper DBM module.'
822 print 'Exiting without running any further tests.'
823 sys.exit(1)
824 if out[0].find('Usage:') < 0:
825 raise Failure('Basic cvs2svn invocation failed.')
828 def show_help_passes():
829 "cvs2svn --help-passes shows pass information"
830 out = run_cvs2svn(None, '--help-passes')
831 if out[0].find('PASSES') < 0:
832 raise Failure('cvs2svn --help-passes failed.')
835 def attr_exec():
836 "detection of the executable flag"
837 if sys.platform == 'win32':
838 raise svntest.Skip()
839 conv = ensure_conversion('main')
840 st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
841 if not st[0] & stat.S_IXUSR:
842 raise Failure()
845 def space_fname():
846 "conversion of filename with a space"
847 conv = ensure_conversion('main')
848 if not conv.path_exists('trunk', 'single-files', 'space fname'):
849 raise Failure()
852 def two_quick():
853 "two commits in quick succession"
854 conv = ensure_conversion('main')
855 logs = parse_log(
856 os.path.join(conv.repos, 'trunk', 'single-files', 'twoquick'), {})
857 if len(logs) != 2:
858 raise Failure()
861 class PruneWithCare(Cvs2SvnTestCase):
862 "prune, but never too much"
864 def __init__(self, **kw):
865 Cvs2SvnTestCase.__init__(self, 'main', **kw)
867 def run(self):
868 # Robert Pluim encountered this lovely one while converting the
869 # directory src/gnu/usr.bin/cvs/contrib/pcl-cvs/ in FreeBSD's CVS
870 # repository (see issue #1302). Step 4 is the doozy:
872 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
873 # revision 2: adds trunk/blah/NEWS
874 # revision 3: deletes trunk/blah/cookie
875 # revision 4: deletes blah [re-deleting trunk/blah/cookie pruned blah!]
876 # revision 5: does nothing
878 # After fixing cvs2svn, the sequence (correctly) looks like this:
880 # revision 1: adds trunk/blah/, adds trunk/blah/cookie
881 # revision 2: adds trunk/blah/NEWS
882 # revision 3: deletes trunk/blah/cookie
883 # revision 4: does nothing [because trunk/blah/cookie already deleted]
884 # revision 5: deletes blah
886 # The difference is in 4 and 5. In revision 4, it's not correct to
887 # prune blah/, because NEWS is still in there, so revision 4 does
888 # nothing now. But when we delete NEWS in 5, that should bubble up
889 # and prune blah/ instead.
891 # ### Note that empty revisions like 4 are probably going to become
892 # ### at least optional, if not banished entirely from cvs2svn's
893 # ### output. Hmmm, or they may stick around, with an extra
894 # ### revision property explaining what happened. Need to think
895 # ### about that. In some sense, it's a bug in Subversion itself,
896 # ### that such revisions don't show up in 'svn log' output.
898 # In the test below, 'trunk/full-prune/first' represents
899 # cookie, and 'trunk/full-prune/second' represents NEWS.
901 conv = self.ensure_conversion()
903 # Confirm that revision 4 removes '/trunk/full-prune/first',
904 # and that revision 6 removes '/trunk/full-prune'.
906 # Also confirm similar things about '/full-prune-reappear/...',
907 # which is similar, except that later on it reappears, restored
908 # from pruneland, because a file gets added to it.
910 # And finally, a similar thing for '/partial-prune/...', except that
911 # in its case, a permanent file on the top level prevents the
912 # pruning from going farther than the subdirectory containing first
913 # and second.
915 for path in ('full-prune/first',
916 'full-prune-reappear/sub/first',
917 'partial-prune/sub/first'):
918 conv.logs[5].check_change('/%(trunk)s/' + path, 'D')
920 for path in ('full-prune',
921 'full-prune-reappear',
922 'partial-prune/sub'):
923 conv.logs[7].check_change('/%(trunk)s/' + path, 'D')
925 for path in ('full-prune-reappear',
926 'full-prune-reappear/appears-later'):
927 conv.logs[33].check_change('/%(trunk)s/' + path, 'A')
930 def interleaved_commits():
931 "two interleaved trunk commits, different log msgs"
932 # See test-data/main-cvsrepos/proj/README.
933 conv = ensure_conversion('main')
935 # The initial import.
936 rev = 26
937 conv.logs[rev].check('Initial import.', (
938 ('/%(trunk)s/interleaved', 'A'),
939 ('/%(trunk)s/interleaved/1', 'A'),
940 ('/%(trunk)s/interleaved/2', 'A'),
941 ('/%(trunk)s/interleaved/3', 'A'),
942 ('/%(trunk)s/interleaved/4', 'A'),
943 ('/%(trunk)s/interleaved/5', 'A'),
944 ('/%(trunk)s/interleaved/a', 'A'),
945 ('/%(trunk)s/interleaved/b', 'A'),
946 ('/%(trunk)s/interleaved/c', 'A'),
947 ('/%(trunk)s/interleaved/d', 'A'),
948 ('/%(trunk)s/interleaved/e', 'A'),
951 # This PEP explains why we pass the 'log' parameter to these two
952 # nested functions, instead of just inheriting it from the enclosing
953 # scope: http://www.python.org/peps/pep-0227.html
955 def check_letters(log):
956 """Check if REV is the rev where only letters were committed."""
957 log.check('Committing letters only.', (
958 ('/%(trunk)s/interleaved/a', 'M'),
959 ('/%(trunk)s/interleaved/b', 'M'),
960 ('/%(trunk)s/interleaved/c', 'M'),
961 ('/%(trunk)s/interleaved/d', 'M'),
962 ('/%(trunk)s/interleaved/e', 'M'),
965 def check_numbers(log):
966 """Check if REV is the rev where only numbers were committed."""
967 log.check('Committing numbers only.', (
968 ('/%(trunk)s/interleaved/1', 'M'),
969 ('/%(trunk)s/interleaved/2', 'M'),
970 ('/%(trunk)s/interleaved/3', 'M'),
971 ('/%(trunk)s/interleaved/4', 'M'),
972 ('/%(trunk)s/interleaved/5', 'M'),
975 # One of the commits was letters only, the other was numbers only.
976 # But they happened "simultaneously", so we don't assume anything
977 # about which commit appeared first, so we just try both ways.
978 rev += 1
979 try:
980 check_letters(conv.logs[rev])
981 check_numbers(conv.logs[rev + 1])
982 except Failure:
983 check_numbers(conv.logs[rev])
984 check_letters(conv.logs[rev + 1])
987 def simple_commits():
988 "simple trunk commits"
989 # See test-data/main-cvsrepos/proj/README.
990 conv = ensure_conversion('main')
992 # The initial import.
993 conv.logs[13].check('Initial import.', (
994 ('/%(trunk)s/proj', 'A'),
995 ('/%(trunk)s/proj/default', 'A'),
996 ('/%(trunk)s/proj/sub1', 'A'),
997 ('/%(trunk)s/proj/sub1/default', 'A'),
998 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
999 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1000 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1001 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1002 ('/%(trunk)s/proj/sub2', 'A'),
1003 ('/%(trunk)s/proj/sub2/default', 'A'),
1004 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1005 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1006 ('/%(trunk)s/proj/sub3', 'A'),
1007 ('/%(trunk)s/proj/sub3/default', 'A'),
1010 # The first commit.
1011 conv.logs[18].check('First commit to proj, affecting two files.', (
1012 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1013 ('/%(trunk)s/proj/sub3/default', 'M'),
1016 # The second commit.
1017 conv.logs[19].check('Second commit to proj, affecting all 7 files.', (
1018 ('/%(trunk)s/proj/default', 'M'),
1019 ('/%(trunk)s/proj/sub1/default', 'M'),
1020 ('/%(trunk)s/proj/sub1/subsubA/default', 'M'),
1021 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1022 ('/%(trunk)s/proj/sub2/default', 'M'),
1023 ('/%(trunk)s/proj/sub2/subsubA/default', 'M'),
1024 ('/%(trunk)s/proj/sub3/default', 'M')
1028 class SimpleTags(Cvs2SvnTestCase):
1029 "simple tags and branches, no commits"
1031 def __init__(self, **kw):
1032 # See test-data/main-cvsrepos/proj/README.
1033 Cvs2SvnTestCase.__init__(self, 'main', **kw)
1035 def run(self):
1036 conv = self.ensure_conversion()
1038 # Verify the copy source for the tags we are about to check
1039 # No need to verify the copyfrom revision, as simple_commits did that
1040 conv.logs[13].check('Initial import.', (
1041 ('/%(trunk)s/proj', 'A'),
1042 ('/%(trunk)s/proj/default', 'A'),
1043 ('/%(trunk)s/proj/sub1', 'A'),
1044 ('/%(trunk)s/proj/sub1/default', 'A'),
1045 ('/%(trunk)s/proj/sub1/subsubA', 'A'),
1046 ('/%(trunk)s/proj/sub1/subsubA/default', 'A'),
1047 ('/%(trunk)s/proj/sub1/subsubB', 'A'),
1048 ('/%(trunk)s/proj/sub1/subsubB/default', 'A'),
1049 ('/%(trunk)s/proj/sub2', 'A'),
1050 ('/%(trunk)s/proj/sub2/default', 'A'),
1051 ('/%(trunk)s/proj/sub2/subsubA', 'A'),
1052 ('/%(trunk)s/proj/sub2/subsubA/default', 'A'),
1053 ('/%(trunk)s/proj/sub3', 'A'),
1054 ('/%(trunk)s/proj/sub3/default', 'A'),
1057 fromstr = ' (from /%(branches)s/B_FROM_INITIALS:14)'
1059 # Tag on rev 1.1.1.1 of all files in proj
1060 conv.logs[14].check(sym_log_msg('B_FROM_INITIALS'), (
1061 ('/%(branches)s/B_FROM_INITIALS (from /%(trunk)s:13)', 'A'),
1062 ('/%(branches)s/B_FROM_INITIALS/single-files', 'D'),
1063 ('/%(branches)s/B_FROM_INITIALS/partial-prune', 'D'),
1066 # The same, as a tag
1067 log = conv.find_tag_log('T_ALL_INITIAL_FILES')
1068 log.check(sym_log_msg('T_ALL_INITIAL_FILES',1), (
1069 ('/%(tags)s/T_ALL_INITIAL_FILES'+fromstr, 'A'),
1072 # Tag on rev 1.1.1.1 of all files in proj, except one
1073 log = conv.find_tag_log('T_ALL_INITIAL_FILES_BUT_ONE')
1074 log.check(sym_log_msg('T_ALL_INITIAL_FILES_BUT_ONE',1), (
1075 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE'+fromstr, 'A'),
1076 ('/%(tags)s/T_ALL_INITIAL_FILES_BUT_ONE/proj/sub1/subsubB', 'D'),
1079 # The same, as a branch
1080 conv.logs[17].check(sym_log_msg('B_FROM_INITIALS_BUT_ONE'), (
1081 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE'+fromstr, 'A'),
1082 ('/%(branches)s/B_FROM_INITIALS_BUT_ONE/proj/sub1/subsubB', 'D'),
1086 def simple_branch_commits():
1087 "simple branch commits"
1088 # See test-data/main-cvsrepos/proj/README.
1089 conv = ensure_conversion('main')
1091 conv.logs[23].check('Modify three files, on branch B_MIXED.', (
1092 ('/%(branches)s/B_MIXED/proj/default', 'M'),
1093 ('/%(branches)s/B_MIXED/proj/sub1/default', 'M'),
1094 ('/%(branches)s/B_MIXED/proj/sub2/subsubA/default', 'M'),
1098 def mixed_time_tag():
1099 "mixed-time tag"
1100 # See test-data/main-cvsrepos/proj/README.
1101 conv = ensure_conversion('main')
1103 log = conv.find_tag_log('T_MIXED')
1104 log.check_changes((
1105 ('/%(tags)s/T_MIXED (from /%(branches)s/B_MIXED:20)', 'A'),
1109 def mixed_time_branch_with_added_file():
1110 "mixed-time branch, and a file added to the branch"
1111 # See test-data/main-cvsrepos/proj/README.
1112 conv = ensure_conversion('main')
1114 # A branch from the same place as T_MIXED in the previous test,
1115 # plus a file added directly to the branch
1116 conv.logs[20].check(sym_log_msg('B_MIXED'), (
1117 ('/%(branches)s/B_MIXED (from /%(trunk)s:19)', 'A'),
1118 ('/%(branches)s/B_MIXED/partial-prune', 'D'),
1119 ('/%(branches)s/B_MIXED/single-files', 'D'),
1120 ('/%(branches)s/B_MIXED/proj/sub2/subsubA '
1121 '(from /%(trunk)s/proj/sub2/subsubA:13)', 'R'),
1122 ('/%(branches)s/B_MIXED/proj/sub3 (from /%(trunk)s/proj/sub3:18)', 'R'),
1125 conv.logs[22].check('Add a file on branch B_MIXED.', (
1126 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'A'),
1130 def mixed_commit():
1131 "a commit affecting both trunk and a branch"
1132 # See test-data/main-cvsrepos/proj/README.
1133 conv = ensure_conversion('main')
1135 conv.logs[24].check(
1136 'A single commit affecting one file on branch B_MIXED '
1137 'and one on trunk.', (
1138 ('/%(trunk)s/proj/sub2/default', 'M'),
1139 ('/%(branches)s/B_MIXED/proj/sub2/branch_B_MIXED_only', 'M'),
1143 def split_time_branch():
1144 "branch some trunk files, and later branch the rest"
1145 # See test-data/main-cvsrepos/proj/README.
1146 conv = ensure_conversion('main')
1148 # First change on the branch, creating it
1149 conv.logs[25].check(sym_log_msg('B_SPLIT'), (
1150 ('/%(branches)s/B_SPLIT (from /%(trunk)s:24)', 'A'),
1151 ('/%(branches)s/B_SPLIT/partial-prune', 'D'),
1152 ('/%(branches)s/B_SPLIT/single-files', 'D'),
1153 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB', 'D'),
1156 conv.logs[29].check('First change on branch B_SPLIT.', (
1157 ('/%(branches)s/B_SPLIT/proj/default', 'M'),
1158 ('/%(branches)s/B_SPLIT/proj/sub1/default', 'M'),
1159 ('/%(branches)s/B_SPLIT/proj/sub1/subsubA/default', 'M'),
1160 ('/%(branches)s/B_SPLIT/proj/sub2/default', 'M'),
1161 ('/%(branches)s/B_SPLIT/proj/sub2/subsubA/default', 'M'),
1164 # A trunk commit for the file which was not branched
1165 conv.logs[30].check('A trunk change to sub1/subsubB/default. '
1166 'This was committed about an', (
1167 ('/%(trunk)s/proj/sub1/subsubB/default', 'M'),
1170 # Add the file not already branched to the branch, with modification:w
1171 conv.logs[31].check(sym_log_msg('B_SPLIT'), (
1172 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB '
1173 '(from /%(trunk)s/proj/sub1/subsubB:30)', 'A'),
1176 conv.logs[32].check('This change affects sub3/default and '
1177 'sub1/subsubB/default, on branch', (
1178 ('/%(branches)s/B_SPLIT/proj/sub1/subsubB/default', 'M'),
1179 ('/%(branches)s/B_SPLIT/proj/sub3/default', 'M'),
1183 def multiple_tags():
1184 "multiple tags referring to same revision"
1185 conv = ensure_conversion('main')
1186 if not conv.path_exists('tags', 'T_ALL_INITIAL_FILES', 'proj', 'default'):
1187 raise Failure()
1188 if not conv.path_exists(
1189 'tags', 'T_ALL_INITIAL_FILES_BUT_ONE', 'proj', 'default'):
1190 raise Failure()
1193 def multiply_defined_symbols():
1194 "multiple definitions of symbol names"
1196 # We can only check one line of the error output at a time, so test
1197 # twice. (The conversion only have to be done once because the
1198 # results are cached.)
1199 conv = ensure_conversion(
1200 'multiply-defined-symbols',
1201 error_re=(
1202 r"ERROR\: Multiple definitions of the symbol \'BRANCH\' .*\: "
1203 r"1\.2\.4 1\.2\.2"
1206 conv = ensure_conversion(
1207 'multiply-defined-symbols',
1208 error_re=(
1209 r"ERROR\: Multiple definitions of the symbol \'TAG\' .*\: "
1210 r"1\.2 1\.1"
1215 def multiply_defined_symbols_renamed():
1216 "rename multiply defined symbols"
1218 conv = ensure_conversion(
1219 'multiply-defined-symbols',
1220 options_file='cvs2svn-rename.options',
1224 def multiply_defined_symbols_ignored():
1225 "ignore multiply defined symbols"
1227 conv = ensure_conversion(
1228 'multiply-defined-symbols',
1229 options_file='cvs2svn-ignore.options',
1233 def repeatedly_defined_symbols():
1234 "multiple identical definitions of symbol names"
1236 # If a symbol is defined multiple times but has the same value each
1237 # time, that should not be an error.
1239 conv = ensure_conversion('repeatedly-defined-symbols')
1242 def bogus_tag():
1243 "conversion of invalid symbolic names"
1244 conv = ensure_conversion('bogus-tag')
1247 def overlapping_branch():
1248 "ignore a file with a branch with two names"
1249 conv = ensure_conversion('overlapping-branch')
1251 if not conv.output_found('.*cannot also have name \'vendorB\''):
1252 raise Failure()
1254 conv.logs[2].check('imported', (
1255 ('/%(trunk)s/nonoverlapping-branch', 'A'),
1256 ('/%(trunk)s/overlapping-branch', 'A'),
1259 if len(conv.logs) != 2:
1260 raise Failure()
1263 class PhoenixBranch(Cvs2SvnTestCase):
1264 "convert a branch file rooted in a 'dead' revision"
1266 def __init__(self, **kw):
1267 Cvs2SvnTestCase.__init__(self, 'phoenix', **kw)
1269 def run(self):
1270 conv = self.ensure_conversion()
1271 conv.logs[8].check('This file was supplied by Jack Moffitt', (
1272 ('/%(branches)s/volsung_20010721', 'A'),
1273 ('/%(branches)s/volsung_20010721/phoenix', 'A'),
1275 conv.logs[9].check('This file was supplied by Jack Moffitt', (
1276 ('/%(branches)s/volsung_20010721/phoenix', 'M'),
1280 ###TODO: We check for 4 changed paths here to accomodate creating tags
1281 ###and branches in rev 1, but that will change, so this will
1282 ###eventually change back.
1283 def ctrl_char_in_log():
1284 "handle a control char in a log message"
1285 # This was issue #1106.
1286 rev = 2
1287 conv = ensure_conversion('ctrl-char-in-log')
1288 conv.logs[rev].check_changes((
1289 ('/%(trunk)s/ctrl-char-in-log', 'A'),
1291 if conv.logs[rev].msg.find('\x04') < 0:
1292 raise Failure(
1293 "Log message of 'ctrl-char-in-log,v' (rev 2) is wrong.")
1296 def overdead():
1297 "handle tags rooted in a redeleted revision"
1298 conv = ensure_conversion('overdead')
1301 class NoTrunkPrune(Cvs2SvnTestCase):
1302 "ensure that trunk doesn't get pruned"
1304 def __init__(self, **kw):
1305 Cvs2SvnTestCase.__init__(self, 'overdead', **kw)
1307 def run(self):
1308 conv = self.ensure_conversion()
1309 for rev in conv.logs.keys():
1310 rev_logs = conv.logs[rev]
1311 if rev_logs.get_path_op('/%(trunk)s') == 'D':
1312 raise Failure()
1315 def double_delete():
1316 "file deleted twice, in the root of the repository"
1317 # This really tests several things: how we handle a file that's
1318 # removed (state 'dead') in two successive revisions; how we
1319 # handle a file in the root of the repository (there were some
1320 # bugs in cvs2svn's svn path construction for top-level files); and
1321 # the --no-prune option.
1322 conv = ensure_conversion(
1323 'double-delete', args=['--trunk-only', '--no-prune'])
1325 path = '/%(trunk)s/twice-removed'
1326 rev = 2
1327 conv.logs[rev].check('Updated CVS', (
1328 (path, 'A'),
1330 conv.logs[rev + 1].check('Remove this file for the first time.', (
1331 (path, 'D'),
1333 conv.logs[rev + 2].check('Remove this file for the second time,', (
1337 def split_branch():
1338 "branch created from both trunk and another branch"
1339 # See test-data/split-branch-cvsrepos/README.
1341 # The conversion will fail if the bug is present, and
1342 # ensure_conversion will raise Failure.
1343 conv = ensure_conversion('split-branch')
1346 def resync_misgroups():
1347 "resyncing should not misorder commit groups"
1348 # See test-data/resync-misgroups-cvsrepos/README.
1350 # The conversion will fail if the bug is present, and
1351 # ensure_conversion will raise Failure.
1352 conv = ensure_conversion('resync-misgroups')
1355 class TaggedBranchAndTrunk(Cvs2SvnTestCase):
1356 "allow tags with mixed trunk and branch sources"
1358 def __init__(self, **kw):
1359 Cvs2SvnTestCase.__init__(self, 'tagged-branch-n-trunk', **kw)
1361 def run(self):
1362 conv = self.ensure_conversion()
1364 tags = conv.symbols.get('tags', 'tags')
1366 a_path = conv.get_wc(tags, 'some-tag', 'a.txt')
1367 b_path = conv.get_wc(tags, 'some-tag', 'b.txt')
1368 if not (os.path.exists(a_path) and os.path.exists(b_path)):
1369 raise Failure()
1370 if (open(a_path, 'r').read().find('1.24') == -1) \
1371 or (open(b_path, 'r').read().find('1.5') == -1):
1372 raise Failure()
1375 def enroot_race():
1376 "never use the rev-in-progress as a copy source"
1378 # See issue #1427 and r8544.
1379 conv = ensure_conversion('enroot-race')
1380 rev = 6
1381 conv.logs[rev].check_changes((
1382 ('/%(branches)s/mybranch (from /%(trunk)s:5)', 'A'),
1383 ('/%(branches)s/mybranch/proj/a.txt', 'D'),
1384 ('/%(branches)s/mybranch/proj/b.txt', 'D'),
1386 conv.logs[rev + 1].check_changes((
1387 ('/%(branches)s/mybranch/proj/c.txt', 'M'),
1388 ('/%(trunk)s/proj/a.txt', 'M'),
1389 ('/%(trunk)s/proj/b.txt', 'M'),
1393 def enroot_race_obo():
1394 "do use the last completed rev as a copy source"
1395 conv = ensure_conversion('enroot-race-obo')
1396 conv.logs[3].check_change('/%(branches)s/BRANCH (from /%(trunk)s:2)', 'A')
1397 if not len(conv.logs) == 3:
1398 raise Failure()
1401 class BranchDeleteFirst(Cvs2SvnTestCase):
1402 "correctly handle deletion as initial branch action"
1404 def __init__(self, **kw):
1405 Cvs2SvnTestCase.__init__(self, 'branch-delete-first', **kw)
1407 def run(self):
1408 # See test-data/branch-delete-first-cvsrepos/README.
1410 # The conversion will fail if the bug is present, and
1411 # ensure_conversion would raise Failure.
1412 conv = self.ensure_conversion()
1414 branches = conv.symbols.get('branches', 'branches')
1416 # 'file' was deleted from branch-1 and branch-2, but not branch-3
1417 if conv.path_exists(branches, 'branch-1', 'file'):
1418 raise Failure()
1419 if conv.path_exists(branches, 'branch-2', 'file'):
1420 raise Failure()
1421 if not conv.path_exists(branches, 'branch-3', 'file'):
1422 raise Failure()
1425 def nonascii_filenames():
1426 "non ascii files converted incorrectly"
1427 # see issue #1255
1429 # on a en_US.iso-8859-1 machine this test fails with
1430 # svn: Can't recode ...
1432 # as described in the issue
1434 # on a en_US.UTF-8 machine this test fails with
1435 # svn: Malformed XML ...
1437 # which means at least it fails. Unfortunately it won't fail
1438 # with the same error...
1440 # mangle current locale settings so we know we're not running
1441 # a UTF-8 locale (which does not exhibit this problem)
1442 current_locale = locale.getlocale()
1443 new_locale = 'en_US.ISO8859-1'
1444 locale_changed = None
1446 # From http://docs.python.org/lib/module-sys.html
1448 # getfilesystemencoding():
1450 # Return the name of the encoding used to convert Unicode filenames
1451 # into system file names, or None if the system default encoding is
1452 # used. The result value depends on the operating system:
1454 # - On Windows 9x, the encoding is ``mbcs''.
1455 # - On Mac OS X, the encoding is ``utf-8''.
1456 # - On Unix, the encoding is the user's preference according to the
1457 # result of nl_langinfo(CODESET), or None if the
1458 # nl_langinfo(CODESET) failed.
1459 # - On Windows NT+, file names are Unicode natively, so no conversion is
1460 # performed.
1462 # So we're going to skip this test on Mac OS X for now.
1463 if sys.platform == "darwin":
1464 raise svntest.Skip()
1466 try:
1467 # change locale to non-UTF-8 locale to generate latin1 names
1468 locale.setlocale(locale.LC_ALL, # this might be too broad?
1469 new_locale)
1470 locale_changed = 1
1471 except locale.Error:
1472 raise svntest.Skip()
1474 try:
1475 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
1476 dstrepos_path = os.path.join(test_data_dir,'non-ascii-cvsrepos')
1477 if not os.path.exists(dstrepos_path):
1478 # create repos from existing main repos
1479 shutil.copytree(srcrepos_path, dstrepos_path)
1480 base_path = os.path.join(dstrepos_path, 'single-files')
1481 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
1482 os.path.join(base_path, 'two\366uick,v'))
1483 new_path = os.path.join(dstrepos_path, 'single\366files')
1484 os.rename(base_path, new_path)
1486 # if ensure_conversion can generate a
1487 conv = ensure_conversion('non-ascii', args=['--encoding=latin1'])
1488 finally:
1489 if locale_changed:
1490 locale.setlocale(locale.LC_ALL, current_locale)
1491 safe_rmtree(dstrepos_path)
1494 class UnicodeTest(Cvs2SvnTestCase):
1495 "metadata contains unicode"
1497 warning_pattern = r'ERROR\: There were warnings converting .* messages'
1499 def __init__(self, name, warning_expected, **kw):
1500 if warning_expected:
1501 error_re = self.warning_pattern
1502 else:
1503 error_re = None
1505 Cvs2SvnTestCase.__init__(self, name, error_re=error_re, **kw)
1506 self.warning_expected = warning_expected
1508 def run(self):
1509 try:
1510 # ensure the availability of the "utf_8" encoding:
1511 u'a'.encode('utf_8').decode('utf_8')
1512 except LookupError:
1513 raise svntest.Skip()
1515 self.ensure_conversion()
1518 class UnicodeAuthor(UnicodeTest):
1519 "author name contains unicode"
1521 def __init__(self, warning_expected, **kw):
1522 UnicodeTest.__init__(self, 'unicode-author', warning_expected, **kw)
1525 class UnicodeLog(UnicodeTest):
1526 "log message contains unicode"
1528 def __init__(self, warning_expected, **kw):
1529 UnicodeTest.__init__(self, 'unicode-log', warning_expected, **kw)
1532 def vendor_branch_sameness():
1533 "avoid spurious changes for initial revs"
1534 conv = ensure_conversion(
1535 'vendor-branch-sameness', args=['--keep-trivial-imports']
1538 # The following files are in this repository:
1540 # a.txt: Imported in the traditional way; 1.1 and 1.1.1.1 have
1541 # the same contents, the file's default branch is 1.1.1,
1542 # and both revisions are in state 'Exp'.
1544 # b.txt: Like a.txt, except that 1.1.1.1 has a real change from
1545 # 1.1 (the addition of a line of text).
1547 # c.txt: Like a.txt, except that 1.1.1.1 is in state 'dead'.
1549 # d.txt: This file was created by 'cvs add' instead of import, so
1550 # it has only 1.1 -- no 1.1.1.1, and no default branch.
1551 # The timestamp on the add is exactly the same as for the
1552 # imports of the other files.
1554 # e.txt: Like a.txt, except that the log message for revision 1.1
1555 # is not the standard import log message.
1557 # (Aside from e.txt, the log messages for the same revisions are the
1558 # same in all files.)
1560 # We expect that only a.txt is recognized as an import whose 1.1
1561 # revision can be omitted. The other files should be added on trunk
1562 # then filled to vbranchA, whereas a.txt should be added to vbranchA
1563 # then copied to trunk. In the copy of 1.1.1.1 back to trunk, a.txt
1564 # and e.txt should be copied untouched; b.txt should be 'M'odified,
1565 # and c.txt should be 'D'eleted.
1567 rev = 2
1568 conv.logs[rev].check('Initial revision', (
1569 ('/%(trunk)s/proj', 'A'),
1570 ('/%(trunk)s/proj/b.txt', 'A'),
1571 ('/%(trunk)s/proj/c.txt', 'A'),
1572 ('/%(trunk)s/proj/d.txt', 'A'),
1575 conv.logs[rev + 1].check(sym_log_msg('vbranchA'), (
1576 ('/%(branches)s/vbranchA (from /%(trunk)s:2)', 'A'),
1577 ('/%(branches)s/vbranchA/proj/d.txt', 'D'),
1580 conv.logs[rev + 2].check('First vendor branch revision.', (
1581 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1582 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1583 ('/%(branches)s/vbranchA/proj/c.txt', 'D'),
1586 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1587 'to compensate for changes in r4,', (
1588 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:4)', 'A'),
1589 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:4)', 'R'),
1590 ('/%(trunk)s/proj/c.txt', 'D'),
1593 rev = 7
1594 conv.logs[rev].check('This log message is not the standard', (
1595 ('/%(trunk)s/proj/e.txt', 'A'),
1598 conv.logs[rev + 2].check('First vendor branch revision', (
1599 ('/%(branches)s/vbranchB/proj/e.txt', 'M'),
1602 conv.logs[rev + 3].check('This commit was generated by cvs2svn '
1603 'to compensate for changes in r9,', (
1604 ('/%(trunk)s/proj/e.txt (from /%(branches)s/vbranchB/proj/e.txt:9)', 'R'),
1608 def vendor_branch_trunk_only():
1609 "handle vendor branches with --trunk-only"
1610 conv = ensure_conversion('vendor-branch-sameness', args=['--trunk-only'])
1612 rev = 2
1613 conv.logs[rev].check('Initial revision', (
1614 ('/%(trunk)s/proj', 'A'),
1615 ('/%(trunk)s/proj/b.txt', 'A'),
1616 ('/%(trunk)s/proj/c.txt', 'A'),
1617 ('/%(trunk)s/proj/d.txt', 'A'),
1620 conv.logs[rev + 1].check('First vendor branch revision', (
1621 ('/%(trunk)s/proj/a.txt', 'A'),
1622 ('/%(trunk)s/proj/b.txt', 'M'),
1623 ('/%(trunk)s/proj/c.txt', 'D'),
1626 conv.logs[rev + 2].check('This log message is not the standard', (
1627 ('/%(trunk)s/proj/e.txt', 'A'),
1630 conv.logs[rev + 3].check('First vendor branch revision', (
1631 ('/%(trunk)s/proj/e.txt', 'M'),
1635 def default_branches():
1636 "handle default branches correctly"
1637 conv = ensure_conversion('default-branches')
1639 # There are seven files in the repository:
1641 # a.txt:
1642 # Imported in the traditional way, so 1.1 and 1.1.1.1 are the
1643 # same. Then 1.1.1.2 and 1.1.1.3 were imported, then 1.2
1644 # committed (thus losing the default branch "1.1.1"), then
1645 # 1.1.1.4 was imported. All vendor import release tags are
1646 # still present.
1648 # b.txt:
1649 # Like a.txt, but without rev 1.2.
1651 # c.txt:
1652 # Exactly like b.txt, just s/b.txt/c.txt/ in content.
1654 # d.txt:
1655 # Same as the previous two, but 1.1.1 branch is unlabeled.
1657 # e.txt:
1658 # Same, but missing 1.1.1 label and all tags but 1.1.1.3.
1660 # deleted-on-vendor-branch.txt,v:
1661 # Like b.txt and c.txt, except that 1.1.1.3 is state 'dead'.
1663 # added-then-imported.txt,v:
1664 # Added with 'cvs add' to create 1.1, then imported with
1665 # completely different contents to create 1.1.1.1, therefore
1666 # never had a default branch.
1669 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1670 ('/%(branches)s/unlabeled-1.1.1', 'A'),
1671 ('/%(branches)s/unlabeled-1.1.1/proj', 'A'),
1672 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'A'),
1673 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'A'),
1674 ('/%(branches)s/vbranchA', 'A'),
1675 ('/%(branches)s/vbranchA/proj', 'A'),
1676 ('/%(branches)s/vbranchA/proj/a.txt', 'A'),
1677 ('/%(branches)s/vbranchA/proj/b.txt', 'A'),
1678 ('/%(branches)s/vbranchA/proj/c.txt', 'A'),
1679 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1682 conv.logs[3].check("This commit was generated by cvs2svn "
1683 "to compensate for changes in r2,", (
1684 ('/%(trunk)s/proj', 'A'),
1685 ('/%(trunk)s/proj/a.txt (from /%(branches)s/vbranchA/proj/a.txt:2)', 'A'),
1686 ('/%(trunk)s/proj/b.txt (from /%(branches)s/vbranchA/proj/b.txt:2)', 'A'),
1687 ('/%(trunk)s/proj/c.txt (from /%(branches)s/vbranchA/proj/c.txt:2)', 'A'),
1688 ('/%(trunk)s/proj/d.txt '
1689 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1690 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1691 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:2)', 'A'),
1692 ('/%(trunk)s/proj/e.txt '
1693 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:2)', 'A'),
1696 conv.logs[4].check(sym_log_msg('vtag-1',1), (
1697 ('/%(tags)s/vtag-1 (from /%(branches)s/vbranchA:2)', 'A'),
1698 ('/%(tags)s/vtag-1/proj/d.txt '
1699 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:2)', 'A'),
1702 conv.logs[5].check("Import (vbranchA, vtag-2).", (
1703 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1704 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1705 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1706 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1707 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1708 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'M'),
1711 conv.logs[6].check("This commit was generated by cvs2svn "
1712 "to compensate for changes in r5,", (
1713 ('/%(trunk)s/proj/a.txt '
1714 '(from /%(branches)s/vbranchA/proj/a.txt:5)', 'R'),
1715 ('/%(trunk)s/proj/b.txt '
1716 '(from /%(branches)s/vbranchA/proj/b.txt:5)', 'R'),
1717 ('/%(trunk)s/proj/c.txt '
1718 '(from /%(branches)s/vbranchA/proj/c.txt:5)', 'R'),
1719 ('/%(trunk)s/proj/d.txt '
1720 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'R'),
1721 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1722 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:5)',
1723 'R'),
1724 ('/%(trunk)s/proj/e.txt '
1725 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:5)', 'R'),
1728 conv.logs[7].check(sym_log_msg('vtag-2',1), (
1729 ('/%(tags)s/vtag-2 (from /%(branches)s/vbranchA:5)', 'A'),
1730 ('/%(tags)s/vtag-2/proj/d.txt '
1731 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:5)', 'A'),
1734 conv.logs[8].check("Import (vbranchA, vtag-3).", (
1735 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1736 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1737 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1738 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1739 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1740 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'D'),
1743 conv.logs[9].check("This commit was generated by cvs2svn "
1744 "to compensate for changes in r8,", (
1745 ('/%(trunk)s/proj/a.txt '
1746 '(from /%(branches)s/vbranchA/proj/a.txt:8)', 'R'),
1747 ('/%(trunk)s/proj/b.txt '
1748 '(from /%(branches)s/vbranchA/proj/b.txt:8)', 'R'),
1749 ('/%(trunk)s/proj/c.txt '
1750 '(from /%(branches)s/vbranchA/proj/c.txt:8)', 'R'),
1751 ('/%(trunk)s/proj/d.txt '
1752 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'R'),
1753 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1754 ('/%(trunk)s/proj/e.txt '
1755 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'R'),
1758 conv.logs[10].check(sym_log_msg('vtag-3',1), (
1759 ('/%(tags)s/vtag-3 (from /%(branches)s/vbranchA:8)', 'A'),
1760 ('/%(tags)s/vtag-3/proj/d.txt '
1761 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:8)', 'A'),
1762 ('/%(tags)s/vtag-3/proj/e.txt '
1763 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:8)', 'A'),
1766 conv.logs[11].check("First regular commit, to a.txt, on vtag-3.", (
1767 ('/%(trunk)s/proj/a.txt', 'M'),
1770 conv.logs[12].check("Add a file to the working copy.", (
1771 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1774 conv.logs[13].check(sym_log_msg('vbranchA'), (
1775 ('/%(branches)s/vbranchA/proj/added-then-imported.txt '
1776 '(from /%(trunk)s/proj/added-then-imported.txt:12)', 'A'),
1779 conv.logs[14].check("Import (vbranchA, vtag-4).", (
1780 ('/%(branches)s/unlabeled-1.1.1/proj/d.txt', 'M'),
1781 ('/%(branches)s/unlabeled-1.1.1/proj/e.txt', 'M'),
1782 ('/%(branches)s/vbranchA/proj/a.txt', 'M'),
1783 ('/%(branches)s/vbranchA/proj/added-then-imported.txt', 'M'), # CHECK!!!
1784 ('/%(branches)s/vbranchA/proj/b.txt', 'M'),
1785 ('/%(branches)s/vbranchA/proj/c.txt', 'M'),
1786 ('/%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt', 'A'),
1789 conv.logs[15].check("This commit was generated by cvs2svn "
1790 "to compensate for changes in r14,", (
1791 ('/%(trunk)s/proj/b.txt '
1792 '(from /%(branches)s/vbranchA/proj/b.txt:14)', 'R'),
1793 ('/%(trunk)s/proj/c.txt '
1794 '(from /%(branches)s/vbranchA/proj/c.txt:14)', 'R'),
1795 ('/%(trunk)s/proj/d.txt '
1796 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'R'),
1797 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt '
1798 '(from /%(branches)s/vbranchA/proj/deleted-on-vendor-branch.txt:14)',
1799 'A'),
1800 ('/%(trunk)s/proj/e.txt '
1801 '(from /%(branches)s/unlabeled-1.1.1/proj/e.txt:14)', 'R'),
1804 conv.logs[16].check(sym_log_msg('vtag-4',1), (
1805 ('/%(tags)s/vtag-4 (from /%(branches)s/vbranchA:14)', 'A'),
1806 ('/%(tags)s/vtag-4/proj/d.txt '
1807 '(from /%(branches)s/unlabeled-1.1.1/proj/d.txt:14)', 'A'),
1811 def default_branches_trunk_only():
1812 "handle default branches with --trunk-only"
1814 conv = ensure_conversion('default-branches', args=['--trunk-only'])
1816 conv.logs[2].check("Import (vbranchA, vtag-1).", (
1817 ('/%(trunk)s/proj', 'A'),
1818 ('/%(trunk)s/proj/a.txt', 'A'),
1819 ('/%(trunk)s/proj/b.txt', 'A'),
1820 ('/%(trunk)s/proj/c.txt', 'A'),
1821 ('/%(trunk)s/proj/d.txt', 'A'),
1822 ('/%(trunk)s/proj/e.txt', 'A'),
1823 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1826 conv.logs[3].check("Import (vbranchA, vtag-2).", (
1827 ('/%(trunk)s/proj/a.txt', 'M'),
1828 ('/%(trunk)s/proj/b.txt', 'M'),
1829 ('/%(trunk)s/proj/c.txt', 'M'),
1830 ('/%(trunk)s/proj/d.txt', 'M'),
1831 ('/%(trunk)s/proj/e.txt', 'M'),
1832 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'M'),
1835 conv.logs[4].check("Import (vbranchA, vtag-3).", (
1836 ('/%(trunk)s/proj/a.txt', 'M'),
1837 ('/%(trunk)s/proj/b.txt', 'M'),
1838 ('/%(trunk)s/proj/c.txt', 'M'),
1839 ('/%(trunk)s/proj/d.txt', 'M'),
1840 ('/%(trunk)s/proj/e.txt', 'M'),
1841 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'D'),
1844 conv.logs[5].check("First regular commit, to a.txt, on vtag-3.", (
1845 ('/%(trunk)s/proj/a.txt', 'M'),
1848 conv.logs[6].check("Add a file to the working copy.", (
1849 ('/%(trunk)s/proj/added-then-imported.txt', 'A'),
1852 conv.logs[7].check("Import (vbranchA, vtag-4).", (
1853 ('/%(trunk)s/proj/b.txt', 'M'),
1854 ('/%(trunk)s/proj/c.txt', 'M'),
1855 ('/%(trunk)s/proj/d.txt', 'M'),
1856 ('/%(trunk)s/proj/e.txt', 'M'),
1857 ('/%(trunk)s/proj/deleted-on-vendor-branch.txt', 'A'),
1861 def default_branch_and_1_2():
1862 "do not allow 1.2 revision with default branch"
1864 conv = ensure_conversion(
1865 'default-branch-and-1-2',
1866 error_re=(
1867 r'.*File \'.*\' has default branch=1\.1\.1 but also a revision 1\.2'
1872 def compose_tag_three_sources():
1873 "compose a tag from three sources"
1874 conv = ensure_conversion('compose-tag-three-sources')
1876 conv.logs[2].check("Add on trunk", (
1877 ('/%(trunk)s/tagged-on-trunk-1.1', 'A'),
1878 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'A'),
1879 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'A'),
1880 ('/%(trunk)s/tagged-on-b1', 'A'),
1881 ('/%(trunk)s/tagged-on-b2', 'A'),
1884 conv.logs[3].check(sym_log_msg('b1'), (
1885 ('/%(branches)s/b1 (from /%(trunk)s:2)', 'A'),
1888 conv.logs[4].check(sym_log_msg('b2'), (
1889 ('/%(branches)s/b2 (from /%(trunk)s:2)', 'A'),
1892 conv.logs[5].check("Commit on branch b1", (
1893 ('/%(branches)s/b1/tagged-on-trunk-1.1', 'M'),
1894 ('/%(branches)s/b1/tagged-on-trunk-1.2-a', 'M'),
1895 ('/%(branches)s/b1/tagged-on-trunk-1.2-b', 'M'),
1896 ('/%(branches)s/b1/tagged-on-b1', 'M'),
1897 ('/%(branches)s/b1/tagged-on-b2', 'M'),
1900 conv.logs[6].check("Commit on branch b2", (
1901 ('/%(branches)s/b2/tagged-on-trunk-1.1', 'M'),
1902 ('/%(branches)s/b2/tagged-on-trunk-1.2-a', 'M'),
1903 ('/%(branches)s/b2/tagged-on-trunk-1.2-b', 'M'),
1904 ('/%(branches)s/b2/tagged-on-b1', 'M'),
1905 ('/%(branches)s/b2/tagged-on-b2', 'M'),
1908 conv.logs[7].check("Commit again on trunk", (
1909 ('/%(trunk)s/tagged-on-trunk-1.2-a', 'M'),
1910 ('/%(trunk)s/tagged-on-trunk-1.2-b', 'M'),
1911 ('/%(trunk)s/tagged-on-trunk-1.1', 'M'),
1912 ('/%(trunk)s/tagged-on-b1', 'M'),
1913 ('/%(trunk)s/tagged-on-b2', 'M'),
1916 conv.logs[8].check(sym_log_msg('T',1), (
1917 ('/%(tags)s/T (from /%(trunk)s:7)', 'A'),
1918 ('/%(tags)s/T/tagged-on-trunk-1.1 '
1919 '(from /%(trunk)s/tagged-on-trunk-1.1:2)', 'R'),
1920 ('/%(tags)s/T/tagged-on-b1 (from /%(branches)s/b1/tagged-on-b1:5)', 'R'),
1921 ('/%(tags)s/T/tagged-on-b2 (from /%(branches)s/b2/tagged-on-b2:6)', 'R'),
1925 def pass5_when_to_fill():
1926 "reserve a svn revnum for a fill only when required"
1927 # The conversion will fail if the bug is present, and
1928 # ensure_conversion would raise Failure.
1929 conv = ensure_conversion('pass5-when-to-fill')
1932 class EmptyTrunk(Cvs2SvnTestCase):
1933 "don't break when the trunk is empty"
1935 def __init__(self, **kw):
1936 Cvs2SvnTestCase.__init__(self, 'empty-trunk', **kw)
1938 def run(self):
1939 # The conversion will fail if the bug is present, and
1940 # ensure_conversion would raise Failure.
1941 conv = self.ensure_conversion()
1944 def no_spurious_svn_commits():
1945 "ensure that we don't create any spurious commits"
1946 conv = ensure_conversion('phoenix')
1948 # Check spurious commit that could be created in
1949 # SVNCommitCreator._pre_commit()
1951 # (When you add a file on a branch, CVS creates a trunk revision
1952 # in state 'dead'. If the log message of that commit is equal to
1953 # the one that CVS generates, we do not ever create a 'fill'
1954 # SVNCommit for it.)
1956 # and spurious commit that could be created in
1957 # SVNCommitCreator._commit()
1959 # (When you add a file on a branch, CVS creates a trunk revision
1960 # in state 'dead'. If the log message of that commit is equal to
1961 # the one that CVS generates, we do not create a primary SVNCommit
1962 # for it.)
1963 conv.logs[17].check('File added on branch xiphophorus', (
1964 ('/%(branches)s/xiphophorus/added-on-branch.txt', 'A'),
1967 # Check to make sure that a commit *is* generated:
1968 # (When you add a file on a branch, CVS creates a trunk revision
1969 # in state 'dead'. If the log message of that commit is NOT equal
1970 # to the one that CVS generates, we create a primary SVNCommit to
1971 # serve as a home for the log message in question.
1972 conv.logs[18].check('file added-on-branch2.txt was initially added on '
1973 + 'branch xiphophorus,\nand this log message was tweaked', ())
1975 # Check spurious commit that could be created in
1976 # SVNCommitCreator._commit_symbols().
1977 conv.logs[19].check('This file was also added on branch xiphophorus,', (
1978 ('/%(branches)s/xiphophorus/added-on-branch2.txt', 'A'),
1982 class PeerPathPruning(Cvs2SvnTestCase):
1983 "make sure that filling prunes paths correctly"
1985 def __init__(self, **kw):
1986 Cvs2SvnTestCase.__init__(self, 'peer-path-pruning', **kw)
1988 def run(self):
1989 conv = self.ensure_conversion()
1990 conv.logs[6].check(sym_log_msg('BRANCH'), (
1991 ('/%(branches)s/BRANCH (from /%(trunk)s:4)', 'A'),
1992 ('/%(branches)s/BRANCH/bar', 'D'),
1993 ('/%(branches)s/BRANCH/foo (from /%(trunk)s/foo:5)', 'R'),
1997 def invalid_closings_on_trunk():
1998 "verify correct revs are copied to default branches"
1999 # The conversion will fail if the bug is present, and
2000 # ensure_conversion would raise Failure.
2001 conv = ensure_conversion('invalid-closings-on-trunk')
2004 def individual_passes():
2005 "run each pass individually"
2006 conv = ensure_conversion('main')
2007 conv2 = ensure_conversion('main', passbypass=1)
2009 if conv.logs != conv2.logs:
2010 raise Failure()
2013 def resync_bug():
2014 "reveal a big bug in our resync algorithm"
2015 # This will fail if the bug is present
2016 conv = ensure_conversion('resync-bug')
2019 def branch_from_default_branch():
2020 "reveal a bug in our default branch detection code"
2021 conv = ensure_conversion('branch-from-default-branch')
2023 # This revision will be a default branch synchronization only
2024 # if cvs2svn is correctly determining default branch revisions.
2026 # The bug was that cvs2svn was treating revisions on branches off of
2027 # default branches as default branch revisions, resulting in
2028 # incorrectly regarding the branch off of the default branch as a
2029 # non-trunk default branch. Crystal clear? I thought so. See
2030 # issue #42 for more incoherent blathering.
2031 conv.logs[5].check("This commit was generated by cvs2svn", (
2032 ('/%(trunk)s/proj/file.txt '
2033 '(from /%(branches)s/upstream/proj/file.txt:4)', 'R'),
2037 def file_in_attic_too():
2038 "die if a file exists in and out of the attic"
2039 ensure_conversion(
2040 'file-in-attic-too',
2041 error_re=(
2042 r'.*A CVS repository cannot contain both '
2043 r'(.*)' + re.escape(os.sep) + r'(.*) '
2044 + r'and '
2045 r'\1' + re.escape(os.sep) + r'Attic' + re.escape(os.sep) + r'\2'
2050 def retain_file_in_attic_too():
2051 "test --retain-conflicting-attic-files option"
2052 conv = ensure_conversion(
2053 'file-in-attic-too', args=['--retain-conflicting-attic-files'])
2054 if not conv.path_exists('trunk', 'file.txt'):
2055 raise Failure()
2056 if not conv.path_exists('trunk', 'Attic', 'file.txt'):
2057 raise Failure()
2060 def symbolic_name_filling_guide():
2061 "reveal a big bug in our SymbolFillingGuide"
2062 # This will fail if the bug is present
2063 conv = ensure_conversion('symbolic-name-overfill')
2066 # Helpers for tests involving file contents and properties.
2068 class NodeTreeWalkException:
2069 "Exception class for node tree traversals."
2070 pass
2072 def node_for_path(node, path):
2073 "In the tree rooted under SVNTree NODE, return the node at PATH."
2074 if node.name != '__SVN_ROOT_NODE':
2075 raise NodeTreeWalkException()
2076 path = path.strip('/')
2077 components = path.split('/')
2078 for component in components:
2079 node = get_child(node, component)
2080 return node
2082 # Helper for tests involving properties.
2083 def props_for_path(node, path):
2084 "In the tree rooted under SVNTree NODE, return the prop dict for PATH."
2085 return node_for_path(node, path).props
2088 class EOLMime(Cvs2SvnPropertiesTestCase):
2089 """eol settings and mime types together
2091 The files are as follows:
2093 trunk/foo.txt: no -kb, mime file says nothing.
2094 trunk/foo.xml: no -kb, mime file says text.
2095 trunk/foo.zip: no -kb, mime file says non-text.
2096 trunk/foo.bin: has -kb, mime file says nothing.
2097 trunk/foo.csv: has -kb, mime file says text.
2098 trunk/foo.dbf: has -kb, mime file says non-text.
2101 def __init__(self, args, **kw):
2102 # TODO: It's a bit klugey to construct this path here. But so far
2103 # there's only one test with a mime.types file. If we have more,
2104 # we should abstract this into some helper, which would be located
2105 # near ensure_conversion(). Note that it is a convention of this
2106 # test suite for a mime.types file to be located in the top level
2107 # of the CVS repository to which it applies.
2108 self.mime_path = os.path.join(
2109 test_data_dir, 'eol-mime-cvsrepos', 'mime.types')
2111 Cvs2SvnPropertiesTestCase.__init__(
2112 self, 'eol-mime',
2113 props_to_test=['svn:eol-style', 'svn:mime-type', 'svn:keywords'],
2114 args=['--mime-types=%s' % self.mime_path] + args,
2115 **kw)
2118 # We do four conversions. Each time, we pass --mime-types=FILE with
2119 # the same FILE, but vary --default-eol and --eol-from-mime-type.
2120 # Thus there's one conversion with neither flag, one with just the
2121 # former, one with just the latter, and one with both.
2124 # Neither --no-default-eol nor --eol-from-mime-type:
2125 eol_mime1 = EOLMime(
2126 variant=1,
2127 args=[],
2128 expected_props=[
2129 ('trunk/foo.txt', [None, None, None]),
2130 ('trunk/foo.xml', [None, 'text/xml', None]),
2131 ('trunk/foo.zip', [None, 'application/zip', None]),
2132 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2133 ('trunk/foo.csv', [None, 'text/csv', None]),
2134 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2138 # Just --no-default-eol, not --eol-from-mime-type:
2139 eol_mime2 = EOLMime(
2140 variant=2,
2141 args=['--default-eol=native'],
2142 expected_props=[
2143 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2144 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2145 ('trunk/foo.zip', ['native', 'application/zip', KEYWORDS]),
2146 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2147 ('trunk/foo.csv', [None, 'text/csv', None]),
2148 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2152 # Just --eol-from-mime-type, not --no-default-eol:
2153 eol_mime3 = EOLMime(
2154 variant=3,
2155 args=['--eol-from-mime-type'],
2156 expected_props=[
2157 ('trunk/foo.txt', [None, None, None]),
2158 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2159 ('trunk/foo.zip', [None, 'application/zip', None]),
2160 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2161 ('trunk/foo.csv', [None, 'text/csv', None]),
2162 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2166 # Both --no-default-eol and --eol-from-mime-type:
2167 eol_mime4 = EOLMime(
2168 variant=4,
2169 args=['--eol-from-mime-type', '--default-eol=native'],
2170 expected_props=[
2171 ('trunk/foo.txt', ['native', None, KEYWORDS]),
2172 ('trunk/foo.xml', ['native', 'text/xml', KEYWORDS]),
2173 ('trunk/foo.zip', [None, 'application/zip', None]),
2174 ('trunk/foo.bin', [None, 'application/octet-stream', None]),
2175 ('trunk/foo.csv', [None, 'text/csv', None]),
2176 ('trunk/foo.dbf', [None, 'application/what-is-dbf', None]),
2180 cvs_revnums_off = Cvs2SvnPropertiesTestCase(
2181 'eol-mime',
2182 description='test non-setting of cvs2svn:cvs-rev property',
2183 args=[],
2184 props_to_test=['cvs2svn:cvs-rev'],
2185 expected_props=[
2186 ('trunk/foo.txt', [None]),
2187 ('trunk/foo.xml', [None]),
2188 ('trunk/foo.zip', [None]),
2189 ('trunk/foo.bin', [None]),
2190 ('trunk/foo.csv', [None]),
2191 ('trunk/foo.dbf', [None]),
2195 cvs_revnums_on = Cvs2SvnPropertiesTestCase(
2196 'eol-mime',
2197 description='test setting of cvs2svn:cvs-rev property',
2198 args=['--cvs-revnums'],
2199 props_to_test=['cvs2svn:cvs-rev'],
2200 expected_props=[
2201 ('trunk/foo.txt', ['1.2']),
2202 ('trunk/foo.xml', ['1.2']),
2203 ('trunk/foo.zip', ['1.2']),
2204 ('trunk/foo.bin', ['1.2']),
2205 ('trunk/foo.csv', ['1.2']),
2206 ('trunk/foo.dbf', ['1.2']),
2210 keywords = Cvs2SvnPropertiesTestCase(
2211 'keywords',
2212 description='test setting of svn:keywords property among others',
2213 args=['--default-eol=native'],
2214 props_to_test=['svn:keywords', 'svn:eol-style', 'svn:mime-type'],
2215 expected_props=[
2216 ('trunk/foo.default', [KEYWORDS, 'native', None]),
2217 ('trunk/foo.kkvl', [KEYWORDS, 'native', None]),
2218 ('trunk/foo.kkv', [KEYWORDS, 'native', None]),
2219 ('trunk/foo.kb', [None, None, 'application/octet-stream']),
2220 ('trunk/foo.kk', [None, 'native', None]),
2221 ('trunk/foo.ko', [None, 'native', None]),
2222 ('trunk/foo.kv', [None, 'native', None]),
2226 def ignore():
2227 "test setting of svn:ignore property"
2228 conv = ensure_conversion('cvsignore')
2229 wc_tree = conv.get_wc_tree()
2230 topdir_props = props_for_path(wc_tree, 'trunk/proj')
2231 subdir_props = props_for_path(wc_tree, '/trunk/proj/subdir')
2233 if topdir_props['svn:ignore'] != \
2234 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2235 raise Failure()
2237 if subdir_props['svn:ignore'] != \
2238 '*.idx\n*.aux\n*.dvi\n*.log\nfoo\nbar\nbaz\nqux\n\n':
2239 raise Failure()
2242 def requires_cvs():
2243 "test that CVS can still do what RCS can't"
2244 # See issues 4, 11, 29 for the bugs whose regression we're testing for.
2245 conv = ensure_conversion('requires-cvs', args=["--use-cvs"])
2247 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
2248 cl_contents = file(conv.get_wc("trunk", "client_lock.idl")).read()
2250 if atsign_contents[-1:] == "@":
2251 raise Failure()
2252 if cl_contents.find("gregh\n//\n//Integration for locks") < 0:
2253 raise Failure()
2255 if not (conv.logs[21].author == "William Lyon Phelps III" and
2256 conv.logs[20].author == "j random"):
2257 raise Failure()
2260 def questionable_branch_names():
2261 "test that we can handle weird branch names"
2262 conv = ensure_conversion('questionable-symbols')
2263 # If the conversion succeeds, then we're okay. We could check the
2264 # actual branch paths, too, but the main thing is to know that the
2265 # conversion doesn't fail.
2268 def questionable_tag_names():
2269 "test that we can handle weird tag names"
2270 conv = ensure_conversion('questionable-symbols')
2271 conv.find_tag_log('Tag_A').check(sym_log_msg('Tag_A', 1), (
2272 ('/%(tags)s/Tag_A (from /trunk:8)', 'A'),
2274 conv.find_tag_log('TagWith/Backslash_E').check(
2275 sym_log_msg('TagWith/Backslash_E',1),
2277 ('/%(tags)s/TagWith', 'A'),
2278 ('/%(tags)s/TagWith/Backslash_E (from /trunk:8)', 'A'),
2281 conv.find_tag_log('TagWith/Slash_Z').check(
2282 sym_log_msg('TagWith/Slash_Z',1),
2284 ('/%(tags)s/TagWith/Slash_Z (from /trunk:8)', 'A'),
2289 def revision_reorder_bug():
2290 "reveal a bug that reorders file revisions"
2291 conv = ensure_conversion('revision-reorder-bug')
2292 # If the conversion succeeds, then we're okay. We could check the
2293 # actual revisions, too, but the main thing is to know that the
2294 # conversion doesn't fail.
2297 def exclude():
2298 "test that exclude really excludes everything"
2299 conv = ensure_conversion('main', args=['--exclude=.*'])
2300 for log in conv.logs.values():
2301 for item in log.changed_paths.keys():
2302 if item.startswith('/branches/') or item.startswith('/tags/'):
2303 raise Failure()
2306 def vendor_branch_delete_add():
2307 "add trunk file that was deleted on vendor branch"
2308 # This will error if the bug is present
2309 conv = ensure_conversion('vendor-branch-delete-add')
2312 def resync_pass2_pull_forward():
2313 "ensure pass2 doesn't pull rev too far forward"
2314 conv = ensure_conversion('resync-pass2-pull-forward')
2315 # If the conversion succeeds, then we're okay. We could check the
2316 # actual revisions, too, but the main thing is to know that the
2317 # conversion doesn't fail.
2320 def native_eol():
2321 "only LFs for svn:eol-style=native files"
2322 conv = ensure_conversion('native-eol', args=['--default-eol=native'])
2323 lines = run_program(svntest.main.svnadmin_binary, None, 'dump', '-q',
2324 conv.repos)
2325 # Verify that all files in the dump have LF EOLs. We're actually
2326 # testing the whole dump file, but the dump file itself only uses
2327 # LF EOLs, so we're safe.
2328 for line in lines:
2329 if line[-1] != '\n' or line[:-1].find('\r') != -1:
2330 raise Failure()
2333 def double_fill():
2334 "reveal a bug that created a branch twice"
2335 conv = ensure_conversion('double-fill')
2336 # If the conversion succeeds, then we're okay. We could check the
2337 # actual revisions, too, but the main thing is to know that the
2338 # conversion doesn't fail.
2341 def double_fill2():
2342 "reveal a second bug that created a branch twice"
2343 conv = ensure_conversion('double-fill2')
2344 conv.logs[6].check_msg(sym_log_msg('BRANCH1'))
2345 conv.logs[7].check_msg(sym_log_msg('BRANCH2'))
2346 try:
2347 # This check should fail:
2348 conv.logs[8].check_msg(sym_log_msg('BRANCH2'))
2349 except Failure:
2350 pass
2351 else:
2352 raise Failure('Symbol filled twice in a row')
2355 def resync_pass2_push_backward():
2356 "ensure pass2 doesn't push rev too far backward"
2357 conv = ensure_conversion('resync-pass2-push-backward')
2358 # If the conversion succeeds, then we're okay. We could check the
2359 # actual revisions, too, but the main thing is to know that the
2360 # conversion doesn't fail.
2363 def double_add():
2364 "reveal a bug that added a branch file twice"
2365 conv = ensure_conversion('double-add')
2366 # If the conversion succeeds, then we're okay. We could check the
2367 # actual revisions, too, but the main thing is to know that the
2368 # conversion doesn't fail.
2371 def bogus_branch_copy():
2372 "reveal a bug that copies a branch file wrongly"
2373 conv = ensure_conversion('bogus-branch-copy')
2374 # If the conversion succeeds, then we're okay. We could check the
2375 # actual revisions, too, but the main thing is to know that the
2376 # conversion doesn't fail.
2379 def nested_ttb_directories():
2380 "require error if ttb directories are not disjoint"
2381 opts_list = [
2382 {'trunk' : 'a', 'branches' : 'a',},
2383 {'trunk' : 'a', 'tags' : 'a',},
2384 {'branches' : 'a', 'tags' : 'a',},
2385 # This option conflicts with the default trunk path:
2386 {'branches' : 'trunk',},
2387 # Try some nested directories:
2388 {'trunk' : 'a', 'branches' : 'a/b',},
2389 {'trunk' : 'a/b', 'tags' : 'a/b/c/d',},
2390 {'branches' : 'a', 'tags' : 'a/b',},
2393 for opts in opts_list:
2394 ensure_conversion(
2395 'main', error_re=r'The following paths are not disjoint\:', **opts
2399 class AutoProps(Cvs2SvnPropertiesTestCase):
2400 """Test auto-props.
2402 The files are as follows:
2404 trunk/foo.txt: no -kb, mime auto-prop says nothing.
2405 trunk/foo.xml: no -kb, mime auto-prop says text and eol-style=CRLF.
2406 trunk/foo.zip: no -kb, mime auto-prop says non-text.
2407 trunk/foo.asc: no -kb, mime auto-prop says text and eol-style=<unset>.
2408 trunk/foo.bin: has -kb, mime auto-prop says nothing.
2409 trunk/foo.csv: has -kb, mime auto-prop says text and eol-style=CRLF.
2410 trunk/foo.dbf: has -kb, mime auto-prop says non-text.
2411 trunk/foo.UPCASE1: no -kb, no mime type.
2412 trunk/foo.UPCASE2: no -kb, no mime type.
2415 def __init__(self, args, **kw):
2416 ### TODO: It's a bit klugey to construct this path here. See also
2417 ### the comment in eol_mime().
2418 auto_props_path = os.path.join(
2419 test_data_dir, 'eol-mime-cvsrepos', 'auto-props')
2421 Cvs2SvnPropertiesTestCase.__init__(
2422 self, 'eol-mime',
2423 props_to_test=[
2424 'myprop',
2425 'svn:eol-style',
2426 'svn:mime-type',
2427 'svn:keywords',
2428 'svn:executable',
2430 args=[
2431 '--auto-props=%s' % auto_props_path,
2432 '--eol-from-mime-type'
2433 ] + args,
2434 **kw)
2437 auto_props_ignore_case = AutoProps(
2438 description="test auto-props",
2439 args=['--default-eol=native'],
2440 expected_props=[
2441 ('trunk/foo.txt', ['txt', 'native', None, KEYWORDS, None]),
2442 ('trunk/foo.xml', ['xml', 'CRLF', 'text/xml', KEYWORDS, None]),
2443 ('trunk/foo.zip', ['zip', None, 'application/zip', None, None]),
2444 ('trunk/foo.asc', ['asc', None, 'text/plain', None, None]),
2445 ('trunk/foo.bin',
2446 ['bin', None, 'application/octet-stream', None, '']),
2447 ('trunk/foo.csv', ['csv', 'CRLF', 'text/csv', None, None]),
2448 ('trunk/foo.dbf',
2449 ['dbf', None, 'application/what-is-dbf', None, None]),
2450 ('trunk/foo.UPCASE1', ['UPCASE1', 'native', None, KEYWORDS, None]),
2451 ('trunk/foo.UPCASE2', ['UPCASE2', 'native', None, KEYWORDS, None]),
2455 def ctrl_char_in_filename():
2456 "do not allow control characters in filenames"
2458 try:
2459 srcrepos_path = os.path.join(test_data_dir,'main-cvsrepos')
2460 dstrepos_path = os.path.join(test_data_dir,'ctrl-char-filename-cvsrepos')
2461 if os.path.exists(dstrepos_path):
2462 safe_rmtree(dstrepos_path)
2464 # create repos from existing main repos
2465 shutil.copytree(srcrepos_path, dstrepos_path)
2466 base_path = os.path.join(dstrepos_path, 'single-files')
2467 try:
2468 shutil.copyfile(os.path.join(base_path, 'twoquick,v'),
2469 os.path.join(base_path, 'two\rquick,v'))
2470 except:
2471 # Operating systems that don't allow control characters in
2472 # filenames will hopefully have thrown an exception; in that
2473 # case, just skip this test.
2474 raise svntest.Skip()
2476 conv = ensure_conversion(
2477 'ctrl-char-filename',
2478 error_re=(r'.*Character .* in filename .* '
2479 r'is not supported by Subversion\.'),
2481 finally:
2482 safe_rmtree(dstrepos_path)
2485 def commit_dependencies():
2486 "interleaved and multi-branch commits to same files"
2487 conv = ensure_conversion("commit-dependencies")
2488 conv.logs[2].check('adding', (
2489 ('/%(trunk)s/interleaved', 'A'),
2490 ('/%(trunk)s/interleaved/file1', 'A'),
2491 ('/%(trunk)s/interleaved/file2', 'A'),
2493 conv.logs[3].check('big commit', (
2494 ('/%(trunk)s/interleaved/file1', 'M'),
2495 ('/%(trunk)s/interleaved/file2', 'M'),
2497 conv.logs[4].check('dependant small commit', (
2498 ('/%(trunk)s/interleaved/file1', 'M'),
2500 conv.logs[5].check('adding', (
2501 ('/%(trunk)s/multi-branch', 'A'),
2502 ('/%(trunk)s/multi-branch/file1', 'A'),
2503 ('/%(trunk)s/multi-branch/file2', 'A'),
2505 conv.logs[6].check(sym_log_msg("branch"), (
2506 ('/%(branches)s/branch (from /%(trunk)s:5)', 'A'),
2507 ('/%(branches)s/branch/interleaved', 'D'),
2509 conv.logs[7].check('multi-branch-commit', (
2510 ('/%(trunk)s/multi-branch/file1', 'M'),
2511 ('/%(trunk)s/multi-branch/file2', 'M'),
2512 ('/%(branches)s/branch/multi-branch/file1', 'M'),
2513 ('/%(branches)s/branch/multi-branch/file2', 'M'),
2517 def double_branch_delete():
2518 "fill branches before modifying files on them"
2519 conv = ensure_conversion('double-branch-delete')
2521 # Test for issue #102. The file IMarshalledValue.java is branched,
2522 # deleted, readded on the branch, and then deleted again. If the
2523 # fill for the file on the branch is postponed until after the
2524 # modification, the file will end up live on the branch instead of
2525 # dead! Make sure it happens at the right time.
2527 conv.logs[6].check('JBAS-2436 - Adding LGPL Header2', (
2528 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'A'),
2531 conv.logs[7].check('JBAS-3025 - Removing dependency', (
2532 ('/%(branches)s/Branch_4_0/IMarshalledValue.java', 'D'),
2536 def symbol_mismatches():
2537 "error for conflicting tag/branch"
2539 ensure_conversion(
2540 'symbol-mess',
2541 args=['--symbol-default=strict'],
2542 error_re=r'.*Problems determining how symbols should be converted',
2546 def overlook_symbol_mismatches():
2547 "overlook conflicting tag/branch when --trunk-only"
2549 # This is a test for issue #85.
2551 ensure_conversion('symbol-mess', args=['--trunk-only'])
2554 def force_symbols():
2555 "force symbols to be tags/branches"
2557 conv = ensure_conversion(
2558 'symbol-mess',
2559 args=['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG'])
2560 if conv.path_exists('tags', 'BRANCH') \
2561 or not conv.path_exists('branches', 'BRANCH'):
2562 raise Failure()
2563 if not conv.path_exists('tags', 'TAG') \
2564 or conv.path_exists('branches', 'TAG'):
2565 raise Failure()
2566 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2567 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2568 raise Failure()
2569 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2570 or conv.path_exists('branches', 'MOSTLY_TAG'):
2571 raise Failure()
2574 def commit_blocks_tags():
2575 "commit prevents forced tag"
2577 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2578 ensure_conversion(
2579 'symbol-mess',
2580 args=(basic_args + ['--force-tag=BRANCH_WITH_COMMIT']),
2581 error_re=(
2582 r'.*The following branches cannot be forced to be tags '
2583 r'because they have commits'
2588 def blocked_excludes():
2589 "error for blocked excludes"
2591 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2592 for blocker in ['BRANCH', 'COMMIT', 'UNNAMED']:
2593 try:
2594 ensure_conversion(
2595 'symbol-mess',
2596 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker]))
2597 raise MissingErrorException()
2598 except Failure:
2599 pass
2602 def unblock_blocked_excludes():
2603 "excluding blocker removes blockage"
2605 basic_args = ['--force-branch=MOSTLY_BRANCH', '--force-tag=MOSTLY_TAG']
2606 for blocker in ['BRANCH', 'COMMIT']:
2607 ensure_conversion(
2608 'symbol-mess',
2609 args=(basic_args + ['--exclude=BLOCKED_BY_%s' % blocker,
2610 '--exclude=BLOCKING_%s' % blocker]))
2613 def regexp_force_symbols():
2614 "force symbols via regular expressions"
2616 conv = ensure_conversion(
2617 'symbol-mess',
2618 args=['--force-branch=MOST.*_BRANCH', '--force-tag=MOST.*_TAG'])
2619 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2620 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2621 raise Failure()
2622 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2623 or conv.path_exists('branches', 'MOSTLY_TAG'):
2624 raise Failure()
2627 def heuristic_symbol_default():
2628 "test 'heuristic' symbol default"
2630 conv = ensure_conversion(
2631 'symbol-mess', args=['--symbol-default=heuristic'])
2632 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2633 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2634 raise Failure()
2635 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2636 or conv.path_exists('branches', 'MOSTLY_TAG'):
2637 raise Failure()
2640 def branch_symbol_default():
2641 "test 'branch' symbol default"
2643 conv = ensure_conversion(
2644 'symbol-mess', args=['--symbol-default=branch'])
2645 if conv.path_exists('tags', 'MOSTLY_BRANCH') \
2646 or not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2647 raise Failure()
2648 if conv.path_exists('tags', 'MOSTLY_TAG') \
2649 or not conv.path_exists('branches', 'MOSTLY_TAG'):
2650 raise Failure()
2653 def tag_symbol_default():
2654 "test 'tag' symbol default"
2656 conv = ensure_conversion(
2657 'symbol-mess', args=['--symbol-default=tag'])
2658 if not conv.path_exists('tags', 'MOSTLY_BRANCH') \
2659 or conv.path_exists('branches', 'MOSTLY_BRANCH'):
2660 raise Failure()
2661 if not conv.path_exists('tags', 'MOSTLY_TAG') \
2662 or conv.path_exists('branches', 'MOSTLY_TAG'):
2663 raise Failure()
2666 def symbol_transform():
2667 "test --symbol-transform"
2669 conv = ensure_conversion(
2670 'symbol-mess',
2671 args=[
2672 '--symbol-default=heuristic',
2673 '--symbol-transform=BRANCH:branch',
2674 '--symbol-transform=TAG:tag',
2675 '--symbol-transform=MOSTLY_(BRANCH|TAG):MOSTLY.\\1',
2677 if not conv.path_exists('branches', 'branch'):
2678 raise Failure()
2679 if not conv.path_exists('tags', 'tag'):
2680 raise Failure()
2681 if not conv.path_exists('branches', 'MOSTLY.BRANCH'):
2682 raise Failure()
2683 if not conv.path_exists('tags', 'MOSTLY.TAG'):
2684 raise Failure()
2687 def write_symbol_info():
2688 "test --write-symbol-info"
2690 expected_lines = [
2691 ['0', '.trunk.',
2692 'trunk', 'trunk', '.'],
2693 ['0', 'BLOCKED_BY_UNNAMED',
2694 'branch', 'branches/BLOCKED_BY_UNNAMED', '.trunk.'],
2695 ['0', 'BLOCKING_COMMIT',
2696 'branch', 'branches/BLOCKING_COMMIT', 'BLOCKED_BY_COMMIT'],
2697 ['0', 'BLOCKED_BY_COMMIT',
2698 'branch', 'branches/BLOCKED_BY_COMMIT', '.trunk.'],
2699 ['0', 'BLOCKING_BRANCH',
2700 'branch', 'branches/BLOCKING_BRANCH', 'BLOCKED_BY_BRANCH'],
2701 ['0', 'BLOCKED_BY_BRANCH',
2702 'branch', 'branches/BLOCKED_BY_BRANCH', '.trunk.'],
2703 ['0', 'MOSTLY_BRANCH',
2704 '.', '.', '.'],
2705 ['0', 'MOSTLY_TAG',
2706 '.', '.', '.'],
2707 ['0', 'BRANCH_WITH_COMMIT',
2708 'branch', 'branches/BRANCH_WITH_COMMIT', '.trunk.'],
2709 ['0', 'BRANCH',
2710 'branch', 'branches/BRANCH', '.trunk.'],
2711 ['0', 'TAG',
2712 'tag', 'tags/TAG', '.trunk.'],
2713 ['0', 'unlabeled-1.1.12.1.2',
2714 'branch', 'branches/unlabeled-1.1.12.1.2', 'BLOCKED_BY_UNNAMED'],
2716 expected_lines.sort()
2718 symbol_info_file = os.path.join(tmp_dir, 'symbol-mess-symbol-info.txt')
2719 try:
2720 ensure_conversion(
2721 'symbol-mess',
2722 args=[
2723 '--symbol-default=strict',
2724 '--write-symbol-info=%s' % (symbol_info_file,),
2725 '--passes=:CollateSymbolsPass',
2728 raise MissingErrorException()
2729 except Failure:
2730 pass
2731 lines = []
2732 comment_re = re.compile(r'^\s*\#')
2733 for l in open(symbol_info_file, 'r'):
2734 if comment_re.match(l):
2735 continue
2736 lines.append(l.strip().split())
2737 lines.sort()
2738 if lines != expected_lines:
2739 s = ['Symbol info incorrect\n']
2740 differ = Differ()
2741 for diffline in differ.compare(
2742 [' '.join(line) + '\n' for line in expected_lines],
2743 [' '.join(line) + '\n' for line in lines],
2745 s.append(diffline)
2746 raise Failure(''.join(s))
2749 def symbol_hints():
2750 "test --symbol-hints for setting branch/tag"
2752 conv = ensure_conversion(
2753 'symbol-mess', symbol_hints_file='symbol-mess-symbol-hints.txt',
2755 if not conv.path_exists('branches', 'MOSTLY_BRANCH'):
2756 raise Failure()
2757 if not conv.path_exists('tags', 'MOSTLY_TAG'):
2758 raise Failure()
2759 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2760 ('/tags/MOSTLY_TAG (from /trunk:2)', 'A'),
2762 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2763 ('/branches/BRANCH_WITH_COMMIT (from /trunk:2)', 'A'),
2765 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2766 ('/branches/MOSTLY_BRANCH (from /trunk:2)', 'A'),
2770 def parent_hints():
2771 "test --symbol-hints for setting parent"
2773 conv = ensure_conversion(
2774 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints.txt',
2776 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2777 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2781 def parent_hints_invalid():
2782 "test --symbol-hints with an invalid parent"
2784 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2785 # this symbol hints file sets the preferred parent to BRANCH
2786 # instead:
2787 conv = ensure_conversion(
2788 'symbol-mess', symbol_hints_file='symbol-mess-parent-hints-invalid.txt',
2789 error_re=(
2790 r"BLOCKED_BY_BRANCH is not a valid parent for BRANCH_WITH_COMMIT"
2795 def parent_hints_wildcards():
2796 "test --symbol-hints wildcards"
2798 # BRANCH_WITH_COMMIT is usually determined to branch from .trunk.;
2799 # this symbol hints file sets the preferred parent to BRANCH
2800 # instead:
2801 conv = ensure_conversion(
2802 'symbol-mess',
2803 symbol_hints_file='symbol-mess-parent-hints-wildcards.txt',
2805 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2806 ('/%(branches)s/BRANCH_WITH_COMMIT (from /branches/BRANCH:8)', 'A'),
2810 def path_hints():
2811 "test --symbol-hints for setting svn paths"
2813 conv = ensure_conversion(
2814 'symbol-mess', symbol_hints_file='symbol-mess-path-hints.txt',
2816 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2817 ('/trunk', 'A'),
2818 ('/a', 'A'),
2819 ('/a/strange', 'A'),
2820 ('/a/strange/trunk', 'A'),
2821 ('/a/strange/trunk/path', 'A'),
2822 ('/branches', 'A'),
2823 ('/tags', 'A'),
2825 conv.logs[3].check(sym_log_msg('MOSTLY_TAG', 1), (
2826 ('/special', 'A'),
2827 ('/special/tag', 'A'),
2828 ('/special/tag/path (from /a/strange/trunk/path:2)', 'A'),
2830 conv.logs[9].check(sym_log_msg('BRANCH_WITH_COMMIT'), (
2831 ('/special/other', 'A'),
2832 ('/special/other/branch', 'A'),
2833 ('/special/other/branch/path (from /a/strange/trunk/path:2)', 'A'),
2835 conv.logs[10].check(sym_log_msg('MOSTLY_BRANCH'), (
2836 ('/special/branch', 'A'),
2837 ('/special/branch/path (from /a/strange/trunk/path:2)', 'A'),
2841 def issue_99():
2842 "test problem from issue 99"
2844 conv = ensure_conversion('issue-99')
2847 def issue_100():
2848 "test problem from issue 100"
2850 conv = ensure_conversion('issue-100')
2851 file1 = conv.get_wc('trunk', 'file1.txt')
2852 if file(file1).read() != 'file1.txt<1.2>\n':
2853 raise Failure()
2856 def issue_106():
2857 "test problem from issue 106"
2859 conv = ensure_conversion('issue-106')
2862 def options_option():
2863 "use of the --options option"
2865 conv = ensure_conversion('main', options_file='cvs2svn.options')
2868 def multiproject():
2869 "multiproject conversion"
2871 conv = ensure_conversion(
2872 'main', options_file='cvs2svn-multiproject.options'
2874 conv.logs[1].check('Standard project directories initialized by cvs2svn.', (
2875 ('/partial-prune', 'A'),
2876 ('/partial-prune/trunk', 'A'),
2877 ('/partial-prune/branches', 'A'),
2878 ('/partial-prune/tags', 'A'),
2879 ('/partial-prune/releases', 'A'),
2883 def crossproject():
2884 "multiproject conversion with cross-project commits"
2886 conv = ensure_conversion(
2887 'main', options_file='cvs2svn-crossproject.options'
2891 def tag_with_no_revision():
2892 "tag defined but revision is deleted"
2894 conv = ensure_conversion('tag-with-no-revision')
2897 def delete_cvsignore():
2898 "svn:ignore should vanish when .cvsignore does"
2900 # This is issue #81.
2902 conv = ensure_conversion('delete-cvsignore')
2904 wc_tree = conv.get_wc_tree()
2905 props = props_for_path(wc_tree, 'trunk/proj')
2907 if props.has_key('svn:ignore'):
2908 raise Failure()
2911 def repeated_deltatext():
2912 "ignore repeated deltatext blocks with warning"
2914 conv = ensure_conversion('repeated-deltatext')
2915 warning_re = r'.*Deltatext block for revision 1.1 appeared twice'
2916 if not conv.output_found(warning_re):
2917 raise Failure()
2920 def nasty_graphs():
2921 "process some nasty dependency graphs"
2923 # It's not how well the bear can dance, but that the bear can dance
2924 # at all:
2925 conv = ensure_conversion('nasty-graphs')
2928 def tagging_after_delete():
2929 "optimal tag after deleting files"
2931 conv = ensure_conversion('tagging-after-delete')
2933 # tag should be 'clean', no deletes
2934 log = conv.find_tag_log('tag1')
2935 expected = (
2936 ('/%(tags)s/tag1 (from /%(trunk)s:3)', 'A'),
2938 log.check_changes(expected)
2941 def crossed_branches():
2942 "branches created in inconsistent orders"
2944 conv = ensure_conversion('crossed-branches')
2947 def file_directory_conflict():
2948 "error when filename conflicts with directory name"
2950 conv = ensure_conversion(
2951 'file-directory-conflict',
2952 error_re=r'.*Directory name conflicts with filename',
2956 def attic_directory_conflict():
2957 "error when attic filename conflicts with dirname"
2959 # This tests the problem reported in issue #105.
2961 conv = ensure_conversion(
2962 'attic-directory-conflict',
2963 error_re=r'.*Directory name conflicts with filename',
2967 def internal_co():
2968 "verify that --use-internal-co works"
2970 rcs_conv = ensure_conversion(
2971 'main', args=['--use-rcs', '--default-eol=native'],
2973 conv = ensure_conversion(
2974 'main', args=['--default-eol=native'],
2976 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
2977 raise Failure()
2978 rcs_lines = run_program(
2979 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
2980 rcs_conv.repos)
2981 lines = run_program(
2982 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
2983 conv.repos)
2984 # Compare all lines following the repository UUID:
2985 if lines[3:] != rcs_lines[3:]:
2986 raise Failure()
2989 def internal_co_exclude():
2990 "verify that --use-internal-co --exclude=... works"
2992 rcs_conv = ensure_conversion(
2993 'internal-co',
2994 args=['--use-rcs', '--exclude=BRANCH', '--default-eol=native'],
2996 conv = ensure_conversion(
2997 'internal-co',
2998 args=['--exclude=BRANCH', '--default-eol=native'],
3000 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3001 raise Failure()
3002 rcs_lines = run_program(
3003 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3004 rcs_conv.repos)
3005 lines = run_program(
3006 svntest.main.svnadmin_binary, None, 'dump', '-q', '-r', '1:HEAD',
3007 conv.repos)
3008 # Compare all lines following the repository UUID:
3009 if lines[3:] != rcs_lines[3:]:
3010 raise Failure()
3013 def internal_co_trunk_only():
3014 "verify that --use-internal-co --trunk-only works"
3016 conv = ensure_conversion(
3017 'internal-co',
3018 args=['--trunk-only', '--default-eol=native'],
3020 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3021 raise Failure()
3024 def leftover_revs():
3025 "check for leftover checked-out revisions"
3027 conv = ensure_conversion(
3028 'leftover-revs',
3029 args=['--exclude=BRANCH', '--default-eol=native'],
3031 if conv.output_found(r'WARNING\: internal problem\: leftover revisions'):
3032 raise Failure()
3035 def requires_internal_co():
3036 "test that internal co can do more than RCS"
3037 # See issues 4, 11 for the bugs whose regression we're testing for.
3038 # Unlike in requires_cvs above, issue 29 is not covered.
3039 conv = ensure_conversion('requires-cvs')
3041 atsign_contents = file(conv.get_wc("trunk", "atsign-add")).read()
3043 if atsign_contents[-1:] == "@":
3044 raise Failure()
3046 if not (conv.logs[21].author == "William Lyon Phelps III" and
3047 conv.logs[20].author == "j random"):
3048 raise Failure()
3051 def internal_co_keywords():
3052 "test that internal co handles keywords correctly"
3053 conv_ic = ensure_conversion('internal-co-keywords',
3054 args=["--keywords-off"])
3055 conv_cvs = ensure_conversion('internal-co-keywords',
3056 args=["--use-cvs", "--keywords-off"])
3058 ko_ic = file(conv_ic.get_wc('trunk', 'dir', 'ko.txt')).read()
3059 ko_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'ko.txt')).read()
3060 kk_ic = file(conv_ic.get_wc('trunk', 'dir', 'kk.txt')).read()
3061 kk_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kk.txt')).read()
3062 kv_ic = file(conv_ic.get_wc('trunk', 'dir', 'kv.txt')).read()
3063 kv_cvs = file(conv_cvs.get_wc('trunk', 'dir', 'kv.txt')).read()
3065 if ko_ic != ko_cvs:
3066 raise Failure()
3067 if kk_ic != kk_cvs:
3068 raise Failure()
3070 # The date format changed between cvs and co ('/' instead of '-').
3071 # Accept either one:
3072 date_substitution_re = re.compile(r' ([0-9]*)-([0-9]*)-([0-9]*) ')
3073 if kv_ic != kv_cvs \
3074 and date_substitution_re.sub(r' \1/\2/\3 ', kv_ic) != kv_cvs:
3075 raise Failure()
3078 def timestamp_chaos():
3079 "test timestamp adjustments"
3081 conv = ensure_conversion('timestamp-chaos', args=["-v"])
3083 times = [
3084 '2007-01-01 21:00:00', # Initial commit
3085 '2007-01-01 21:00:00', # revision 1.1 of both files
3086 '2007-01-01 21:00:01', # revision 1.2 of file1.txt, adjusted forwards
3087 '2007-01-01 21:00:02', # revision 1.2 of file1.txt, adjusted backwards
3088 '2007-01-01 22:00:00', # revision 1.3 of both files
3090 for i in range(len(times)):
3091 if abs(conv.logs[i + 1].date - time.mktime(svn_strptime(times[i]))) > 0.1:
3092 raise Failure()
3095 def symlinks():
3096 "convert a repository that contains symlinks"
3098 # This is a test for issue #97.
3100 proj = os.path.join(test_data_dir, 'symlinks-cvsrepos', 'proj')
3101 links = [
3103 os.path.join('..', 'file.txt,v'),
3104 os.path.join(proj, 'dir1', 'file.txt,v'),
3107 'dir1',
3108 os.path.join(proj, 'dir2'),
3112 try:
3113 os.symlink
3114 except AttributeError:
3115 # Apparently this OS doesn't support symlinks, so skip test.
3116 raise svntest.Skip()
3118 try:
3119 for (src,dst) in links:
3120 os.symlink(src, dst)
3122 conv = ensure_conversion('symlinks')
3123 conv.logs[2].check('', (
3124 ('/%(trunk)s/proj', 'A'),
3125 ('/%(trunk)s/proj/file.txt', 'A'),
3126 ('/%(trunk)s/proj/dir1', 'A'),
3127 ('/%(trunk)s/proj/dir1/file.txt', 'A'),
3128 ('/%(trunk)s/proj/dir2', 'A'),
3129 ('/%(trunk)s/proj/dir2/file.txt', 'A'),
3131 finally:
3132 for (src,dst) in links:
3133 os.remove(dst)
3136 def empty_trunk_path():
3137 "allow --trunk to be empty if --trunk-only"
3139 # This is a test for issue #53.
3141 conv = ensure_conversion(
3142 'main', args=['--trunk-only', '--trunk='],
3146 def preferred_parent_cycle():
3147 "handle a cycle in branch parent preferences"
3149 conv = ensure_conversion('preferred-parent-cycle')
3152 def branch_from_empty_dir():
3153 "branch from an empty directory"
3155 conv = ensure_conversion('branch-from-empty-dir')
3158 def trunk_readd():
3159 "add a file on a branch then on trunk"
3161 conv = ensure_conversion('trunk-readd')
3164 def branch_from_deleted_1_1():
3165 "branch from a 1.1 revision that will be deleted"
3167 conv = ensure_conversion('branch-from-deleted-1-1')
3168 conv.logs[5].check('Adding b.txt:1.1.2.1', (
3169 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3171 conv.logs[6].check('Adding b.txt:1.1.4.1', (
3172 ('/%(branches)s/BRANCH2/proj/b.txt', 'A'),
3174 conv.logs[7].check('Adding b.txt:1.2', (
3175 ('/%(trunk)s/proj/b.txt', 'A'),
3178 conv.logs[8].check('Adding c.txt:1.1.2.1', (
3179 ('/%(branches)s/BRANCH1/proj/c.txt', 'A'),
3181 conv.logs[9].check('Adding c.txt:1.1.4.1', (
3182 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3186 def add_on_branch():
3187 "add a file on a branch using newer CVS"
3189 conv = ensure_conversion('add-on-branch')
3190 conv.logs[6].check('Adding b.txt:1.1', (
3191 ('/%(trunk)s/proj/b.txt', 'A'),
3193 conv.logs[7].check('Adding b.txt:1.1.2.2', (
3194 ('/%(branches)s/BRANCH1/proj/b.txt', 'A'),
3196 conv.logs[8].check('Adding c.txt:1.1', (
3197 ('/%(trunk)s/proj/c.txt', 'A'),
3199 conv.logs[9].check('Removing c.txt:1.2', (
3200 ('/%(trunk)s/proj/c.txt', 'D'),
3202 conv.logs[10].check('Adding c.txt:1.2.2.2', (
3203 ('/%(branches)s/BRANCH2/proj/c.txt', 'A'),
3205 conv.logs[11].check('Adding d.txt:1.1', (
3206 ('/%(trunk)s/proj/d.txt', 'A'),
3208 conv.logs[12].check('Adding d.txt:1.1.2.2', (
3209 ('/%(branches)s/BRANCH3/proj/d.txt', 'A'),
3213 def main_git():
3214 "test output in git-fast-import format"
3216 # Note: To test importing into git, do
3218 # ./run-tests <test-number>
3219 # rm -rf .git
3220 # git-init
3221 # cat cvs2svn-tmp/git-{blob,dump}.dat | git-fast-import
3223 # Or, to load the dumpfiles separately:
3225 # cat cvs2svn-tmp/git-blob.dat \
3226 # | git-fast-import --export-marks=cvs2svn-tmp/git-marks.dat
3227 # cat cvs2svn-tmp/git-dump.dat \
3228 # | git-fast-import --import-marks=cvs2svn-tmp/git-marks.dat
3230 # Then use "gitk --all", "git log", etc. to test the contents of the
3231 # repository.
3233 conv = ensure_conversion('main', options_file='cvs2svn-git.options')
3236 def main_git_inline():
3237 "output in git-fast-import format with inline data"
3239 # Note: To test importing into git, do
3241 # ./run-tests <test-number>
3242 # rm -rf .git
3243 # git-init
3244 # cat cvs2svn-tmp/git-dump.dat | git-fast-import
3246 # Then use "gitk --all", "git log", etc. to test the contents of the
3247 # repository.
3249 conv = ensure_conversion('main', options_file='cvs2svn-git-inline.options')
3252 def invalid_symbol():
3253 "a symbol with the incorrect format"
3255 conv = ensure_conversion('invalid-symbol')
3256 if not conv.output_found(
3257 r".*branch 'SYMBOL' references invalid revision 1$"
3259 raise Failure()
3262 def invalid_symbol_ignore():
3263 "ignore a symbol with the incorrect format"
3265 conv = ensure_conversion(
3266 'invalid-symbol', options_file='cvs2svn-ignore.options'
3270 class EOLVariants(Cvs2SvnTestCase):
3271 "handle various --eol-style options"
3273 eol_style_strings = {
3274 'LF' : '\n',
3275 'CR' : '\r',
3276 'CRLF' : '\r\n',
3277 'native' : '\n',
3280 def __init__(self, eol_style):
3281 self.eol_style = eol_style
3282 self.dumpfile = 'eol-variants-%s.dump' % (self.eol_style,)
3283 Cvs2SvnTestCase.__init__(
3284 self, 'eol-variants', variant=self.eol_style,
3285 dumpfile=self.dumpfile,
3286 args=[
3287 '--default-eol=%s' % (self.eol_style,),
3291 def run(self):
3292 conv = self.ensure_conversion()
3293 dump_contents = open(conv.dumpfile, 'rb').read()
3294 expected_text = self.eol_style_strings[self.eol_style].join(
3295 ['line 1', 'line 2', '\n\n']
3297 if not dump_contents.endswith(expected_text):
3298 raise Failure()
3301 def no_revs_file():
3302 "handle a file with no revisions (issue #80)"
3304 conv = ensure_conversion('no-revs-file')
3307 def mirror_keyerror_test():
3308 "a case that gave KeyError in SVNRepositoryMirror"
3310 conv = ensure_conversion('mirror-keyerror')
3313 def exclude_ntdb_test():
3314 "exclude a non-trunk default branch"
3316 symbol_info_file = os.path.join(tmp_dir, 'exclude-ntdb-symbol-info.txt')
3317 conv = ensure_conversion(
3318 'exclude-ntdb',
3319 args=[
3320 '--write-symbol-info=%s' % (symbol_info_file,),
3321 '--exclude=branch3',
3322 '--exclude=tag3',
3323 '--exclude=vendortag3',
3324 '--exclude=vendorbranch',
3329 def mirror_keyerror2_test():
3330 "a case that gave KeyError in RepositoryMirror"
3332 conv = ensure_conversion('mirror-keyerror2')
3335 def mirror_keyerror3_test():
3336 "a case that gave KeyError in RepositoryMirror"
3338 conv = ensure_conversion('mirror-keyerror3')
3341 def add_cvsignore_to_branch_test():
3342 "check adding .cvsignore to an existing branch"
3344 # This a test for issue #122.
3346 conv = ensure_conversion('add-cvsignore-to-branch')
3347 wc_tree = conv.get_wc_tree()
3348 trunk_props = props_for_path(wc_tree, 'trunk/dir')
3349 if trunk_props['svn:ignore'] != '*.o\n\n':
3350 raise Failure()
3352 branch_props = props_for_path(wc_tree, 'branches/BRANCH/dir')
3353 if branch_props['svn:ignore'] != '*.o\n\n':
3354 raise Failure()
3357 ########################################################################
3358 # Run the tests
3360 # list all tests here, starting with None:
3361 test_list = [
3362 None,
3363 # 1:
3364 show_usage,
3365 attr_exec,
3366 space_fname,
3367 two_quick,
3368 PruneWithCare(),
3369 PruneWithCare(variant=1, trunk='a', branches='b', tags='c'),
3370 PruneWithCare(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3371 PruneWithCare(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3372 interleaved_commits,
3373 # 10:
3374 simple_commits,
3375 SimpleTags(),
3376 SimpleTags(variant=1, trunk='a', branches='b', tags='c'),
3377 SimpleTags(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3378 SimpleTags(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3379 simple_branch_commits,
3380 mixed_time_tag,
3381 mixed_time_branch_with_added_file,
3382 mixed_commit,
3383 split_time_branch,
3384 # 20:
3385 bogus_tag,
3386 overlapping_branch,
3387 PhoenixBranch(),
3388 PhoenixBranch(variant=1, trunk='a/1', branches='b/1', tags='c/1'),
3389 ctrl_char_in_log,
3390 overdead,
3391 NoTrunkPrune(),
3392 NoTrunkPrune(variant=1, trunk='a', branches='b', tags='c'),
3393 NoTrunkPrune(variant=2, trunk='a/1', branches='b/1', tags='c/1'),
3394 NoTrunkPrune(variant=3, trunk='a/1', branches='a/2', tags='a/3'),
3395 # 30:
3396 double_delete,
3397 split_branch,
3398 resync_misgroups,
3399 TaggedBranchAndTrunk(),
3400 TaggedBranchAndTrunk(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3401 enroot_race,
3402 enroot_race_obo,
3403 BranchDeleteFirst(),
3404 BranchDeleteFirst(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3405 nonascii_filenames,
3406 # 40:
3407 UnicodeAuthor(
3408 warning_expected=1),
3409 UnicodeAuthor(
3410 warning_expected=0,
3411 variant='encoding', args=['--encoding=utf_8']),
3412 UnicodeAuthor(
3413 warning_expected=0,
3414 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3415 UnicodeLog(
3416 warning_expected=1),
3417 UnicodeLog(
3418 warning_expected=0,
3419 variant='encoding', args=['--encoding=utf_8']),
3420 UnicodeLog(
3421 warning_expected=0,
3422 variant='fallback-encoding', args=['--fallback-encoding=utf_8']),
3423 vendor_branch_sameness,
3424 vendor_branch_trunk_only,
3425 default_branches,
3426 default_branches_trunk_only,
3427 # 50:
3428 default_branch_and_1_2,
3429 compose_tag_three_sources,
3430 pass5_when_to_fill,
3431 PeerPathPruning(),
3432 PeerPathPruning(variant=1, trunk='a/1', branches='a/2', tags='a/3'),
3433 EmptyTrunk(),
3434 EmptyTrunk(variant=1, trunk='a', branches='b', tags='c'),
3435 EmptyTrunk(variant=2, trunk='a/1', branches='a/2', tags='a/3'),
3436 no_spurious_svn_commits,
3437 invalid_closings_on_trunk,
3438 # 60:
3439 individual_passes,
3440 resync_bug,
3441 branch_from_default_branch,
3442 file_in_attic_too,
3443 retain_file_in_attic_too,
3444 symbolic_name_filling_guide,
3445 eol_mime1,
3446 eol_mime2,
3447 eol_mime3,
3448 eol_mime4,
3449 # 70:
3450 cvs_revnums_off,
3451 cvs_revnums_on,
3452 keywords,
3453 ignore,
3454 requires_cvs,
3455 questionable_branch_names,
3456 questionable_tag_names,
3457 revision_reorder_bug,
3458 exclude,
3459 vendor_branch_delete_add,
3460 # 80:
3461 resync_pass2_pull_forward,
3462 native_eol,
3463 double_fill,
3464 XFail(double_fill2),
3465 resync_pass2_push_backward,
3466 double_add,
3467 bogus_branch_copy,
3468 nested_ttb_directories,
3469 auto_props_ignore_case,
3470 ctrl_char_in_filename,
3471 # 90:
3472 commit_dependencies,
3473 show_help_passes,
3474 multiple_tags,
3475 multiply_defined_symbols,
3476 multiply_defined_symbols_renamed,
3477 multiply_defined_symbols_ignored,
3478 repeatedly_defined_symbols,
3479 double_branch_delete,
3480 symbol_mismatches,
3481 overlook_symbol_mismatches,
3482 # 100:
3483 force_symbols,
3484 commit_blocks_tags,
3485 blocked_excludes,
3486 unblock_blocked_excludes,
3487 regexp_force_symbols,
3488 heuristic_symbol_default,
3489 branch_symbol_default,
3490 tag_symbol_default,
3491 symbol_transform,
3492 write_symbol_info,
3493 # 110:
3494 symbol_hints,
3495 parent_hints,
3496 parent_hints_invalid,
3497 parent_hints_wildcards,
3498 path_hints,
3499 issue_99,
3500 issue_100,
3501 issue_106,
3502 options_option,
3503 multiproject,
3504 # 120:
3505 crossproject,
3506 tag_with_no_revision,
3507 delete_cvsignore,
3508 repeated_deltatext,
3509 nasty_graphs,
3510 XFail(tagging_after_delete),
3511 crossed_branches,
3512 file_directory_conflict,
3513 attic_directory_conflict,
3514 internal_co,
3515 # 130:
3516 internal_co_exclude,
3517 internal_co_trunk_only,
3518 internal_co_keywords,
3519 leftover_revs,
3520 requires_internal_co,
3521 timestamp_chaos,
3522 symlinks,
3523 empty_trunk_path,
3524 preferred_parent_cycle,
3525 branch_from_empty_dir,
3526 # 140:
3527 trunk_readd,
3528 branch_from_deleted_1_1,
3529 add_on_branch,
3530 XFail(main_git),
3531 XFail(main_git_inline),
3532 invalid_symbol,
3533 invalid_symbol_ignore,
3534 EOLVariants('LF'),
3535 EOLVariants('CR'),
3536 EOLVariants('CRLF'),
3537 # 150:
3538 EOLVariants('native'),
3539 no_revs_file,
3540 mirror_keyerror_test,
3541 exclude_ntdb_test,
3542 mirror_keyerror2_test,
3543 mirror_keyerror3_test,
3544 XFail(add_cvsignore_to_branch_test),
3547 if __name__ == '__main__':
3549 # Configure the environment for reproducable output from svn, etc.
3550 # I have no idea if this works on Windows too.
3551 os.environ["LC_ALL"] = "C"
3552 os.environ["TZ"] = "UTC"
3554 # The Subversion test suite code assumes it's being invoked from
3555 # within a working copy of the Subversion sources, and tries to use
3556 # the binaries in that tree. Since the cvs2svn tree never contains
3557 # a Subversion build, we just use the system's installed binaries.
3558 svntest.main.svn_binary = 'svn'
3559 svntest.main.svnlook_binary = 'svnlook'
3560 svntest.main.svnadmin_binary = 'svnadmin'
3561 svntest.main.svnversion_binary = 'svnversion'
3563 run_tests(test_list)
3564 # NOTREACHED
3567 ### End of file.