-
Notifications
You must be signed in to change notification settings - Fork 43
/
run-tests.py
executable file
·4317 lines (3518 loc) · 131 KB
/
run-tests.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
#
# run_tests.py: test suite for cvs2svn
#
# Usage: run_tests.py [-v | --verbose] [list | <num>]
#
# Options:
# -v, --verbose
# enable verbose output
#
# Arguments (at most one argument is allowed):
# list
# If the word "list" is passed as an argument, the list of
# available tests is printed (but no tests are run).
#
# <num>
# If a number is passed as an argument, then only the test
# with that number is run.
#
# If no argument is specified, then all tests are run.
#
# Subversion is a tool for revision control.
# See http://subversion.tigris.org for more information.
#
# ====================================================================
# Copyright (c) 2000-2009 CollabNet. All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.
#
# This software consists of voluntary contributions made by many
# individuals. For exact contribution history, see the revision
# history and logs.
#
######################################################################
# General modules
import sys
import shutil
import stat
import re
import os
import time
import os.path
import locale
import textwrap
import calendar
import types
try:
from hashlib import md5
except ImportError:
from md5 import md5
from difflib import Differ
# Make sure that a supported version of Python is being used:
if not (0x02040000 <= sys.hexversion < 0x03000000):
sys.stderr.write(
'error: Python 2, version 2.4 or higher required.\n'
)
sys.exit(1)
# This script needs to run in the correct directory. Make sure we're there.
if not (os.path.exists('cvs2svn') and os.path.exists('test-data')):
sys.stderr.write("error: I need to be run in the directory containing "
"'cvs2svn' and 'test-data'.\n")
sys.exit(1)
# Load the Subversion test framework.
import svntest
from svntest import Failure
from svntest.main import safe_rmtree
from svntest.testcase import TestCase
from svntest.testcase import XFail_deco
# Test if Mercurial >= 1.1 is available.
try:
from mercurial import context
context.memctx
have_hg = True
except (ImportError, AttributeError):
have_hg = False
cvs2svn = os.path.abspath('cvs2svn')
cvs2git = os.path.abspath('cvs2git')
cvs2hg = os.path.abspath('cvs2hg')
# We use the installed svn and svnlook binaries, instead of using
# svntest.main.run_svn() and svntest.main.run_svnlook(), because the
# behavior -- or even existence -- of local builds shouldn't affect
# the cvs2svn test suite.
svn_binary = 'svn'
svnlook_binary = 'svnlook'
svnadmin_binary = 'svnadmin'
svnversion_binary = 'svnversion'
test_data_dir = 'test-data'
tmp_dir = 'cvs2svn-tmp'
#----------------------------------------------------------------------
# Helpers.
#----------------------------------------------------------------------
# The value to expect for svn:keywords if it is set:
KEYWORDS = 'Author Date Id Revision'
class RunProgramException(Failure):
pass
class MissingErrorException(Failure):
def __init__(self, error_re):
Failure.__init__(
self, "Test failed because no error matched '%s'" % (error_re,)
)
def run_program(program, error_re, *varargs):
"""Run PROGRAM with VARARGS, return stdout as a list of lines.
If there is any stderr and ERROR_RE is None, raise
RunProgramException, and log the stderr lines via
svntest.main.logger.info().
If ERROR_RE is not None, it is a string regular expression that must
match some line of stderr. If it fails to match, raise
MissingErrorExpection."""
# FIXME: exit_code is currently ignored.
exit_code, out, err = svntest.main.run_command(program, 1, 0, *varargs)
if error_re:
# Specified error expected on stderr.
if not err:
raise MissingErrorException(error_re)
else:
for line in err:
if re.match(error_re, line):
return out
raise MissingErrorException(error_re)
else:
# No stderr allowed.
if err:
log = svntest.main.logger.info
log('%s said:' % program)
for line in err:
log(' ' + line.rstrip())
raise RunProgramException()
return out
def run_script(script, error_re, *varargs):
"""Run Python script SCRIPT with VARARGS, returning stdout as a list
of lines.
If there is any stderr and ERROR_RE is None, raise
RunProgramException, and log the stderr lines via
svntest.main.logger.info().
If ERROR_RE is not None, it is a string regular expression that must
match some line of stderr. If it fails to match, raise
MissingErrorException."""
# Use the same python that is running this script
return run_program(sys.executable, error_re, script, *varargs)
# On Windows, for an unknown reason, the cmd.exe process invoked by
# os.system('sort ...') in cvs2svn receives invalid stdio handles, if
# cvs2svn is started as "cvs2svn ...". "python cvs2svn ..." avoids
# this. Therefore, the redirection of the output to the .s-revs file fails.
# We no longer use the problematic invocation on any system, but this
# comment remains to warn about this problem.
def run_svn(*varargs):
"""Run svn with VARARGS; return stdout as a list of lines.
If there is any stderr, raise RunProgramException, and log the
stderr lines via svntest.main.logger.info()."""
return run_program(svn_binary, None, *varargs)
def repos_to_url(path_to_svn_repos):
"""This does what you think it does."""
rpath = os.path.abspath(path_to_svn_repos)
if rpath[0] != '/':
rpath = '/' + rpath
return 'file://%s' % rpath.replace(os.sep, '/')
def svn_strptime(timestr):
return time.strptime(timestr, '%Y-%m-%d %H:%M:%S')
class Log:
def __init__(self, revision, author, date, symbols):
self.revision = revision
self.author = author
# Internally, we represent the date as seconds since epoch (UTC).
# Since standard subversion log output shows dates in localtime
#
# "1993-06-18 00:46:07 -0500 (Fri, 18 Jun 1993)"
#
# and time.mktime() converts from localtime, it all works out very
# happily.
self.date = time.mktime(svn_strptime(date[0:19]))
# The following symbols are used for string interpolation when
# checking paths:
self.symbols = symbols
# The changed paths will be accumulated later, as log data is read.
# Keys here are paths such as '/trunk/foo/bar', values are letter
# codes such as 'M', 'A', and 'D'.
self.changed_paths = { }
# The msg will be accumulated later, as log data is read.
self.msg = ''
def absorb_changed_paths(self, out):
'Read changed paths from OUT into self, until no more.'
while True:
line = out.readline()
if len(line) == 1: return
line = line[:-1]
op_portion = line[3:4]
path_portion = line[5:]
# If we're running on Windows we get backslashes instead of
# forward slashes.
path_portion = path_portion.replace('\\', '/')
# # We could parse out history information, but currently we
# # just leave it in the path portion because that's how some
# # tests expect it.
#
# m = re.match("(.*) \(from /.*:[0-9]+\)", path_portion)
# if m:
# path_portion = m.group(1)
self.changed_paths[path_portion] = op_portion
def __cmp__(self, other):
return cmp(self.revision, other.revision) or \
cmp(self.author, other.author) or cmp(self.date, other.date) or \
cmp(self.changed_paths, other.changed_paths) or \
cmp(self.msg, other.msg)
def get_path_op(self, path):
"""Return the operator for the change involving PATH.
PATH is allowed to include string interpolation directives (e.g.,
'%(trunk)s'), which are interpolated against self.symbols. Return
None if there is no record for PATH."""
return self.changed_paths.get(path % self.symbols)
def check_msg(self, msg):
"""Verify that this Log's message starts with the specified MSG."""
if self.msg.find(msg) != 0:
raise Failure(
"Revision %d log message was:\n%s\n\n"
"It should have begun with:\n%s\n\n"
% (self.revision, self.msg, msg,)
)
def check_change(self, path, op):
"""Verify that this Log includes a change for PATH with operator OP.
PATH is allowed to include string interpolation directives (e.g.,
'%(trunk)s'), which are interpolated against self.symbols."""
path = path % self.symbols
found_op = self.changed_paths.get(path, None)
if found_op is None:
raise Failure(
"Revision %d does not include change for path %s "
"(it should have been %s).\n"
% (self.revision, path, op,)
)
if found_op != op:
raise Failure(
"Revision %d path %s had op %s (it should have been %s)\n"
% (self.revision, path, found_op, op,)
)
def check_changes(self, changed_paths):
"""Verify that this Log has precisely the CHANGED_PATHS specified.
CHANGED_PATHS is a sequence of tuples (path, op), where the paths
strings are allowed to include string interpolation directives
(e.g., '%(trunk)s'), which are interpolated against self.symbols."""
cp = {}
for (path, op) in changed_paths:
cp[path % self.symbols] = op
if self.changed_paths != cp:
raise Failure(
"Revision %d changed paths list was:\n%s\n\n"
"It should have been:\n%s\n\n"
% (self.revision, self.changed_paths, cp,)
)
def check(self, msg, changed_paths):
"""Verify that this Log has the MSG and CHANGED_PATHS specified.
Convenience function to check two things at once. MSG is passed
to check_msg(); CHANGED_PATHS is passed to check_changes()."""
self.check_msg(msg)
self.check_changes(changed_paths)
def parse_log(svn_repos, symbols):
"""Return a dictionary of Logs, keyed on revision number, for SVN_REPOS.
Initialize the Logs' symbols with SYMBOLS."""
class LineFeeder:
'Make a list of lines behave like an open file handle.'
def __init__(self, lines):
self.lines = list(reversed(lines))
def readline(self):
if len(self.lines) > 0:
return self.lines.pop()
else:
return None
def absorb_message_body(out, num_lines, log):
"""Read NUM_LINES of log message body from OUT into Log item LOG."""
for i in range(num_lines):
log.msg += out.readline()
log_start_re = re.compile('^r(?P<rev>[0-9]+) \| '
'(?P<author>[^\|]+) \| '
'(?P<date>[^\|]+) '
'\| (?P<lines>[0-9]+) (line|lines)$')
log_separator = '-' * 72
logs = { }
out = LineFeeder(run_svn('log', '-v', repos_to_url(svn_repos)))
while True:
this_log = None
line = out.readline()
if not line: break
line = line[:-1]
if line.find(log_separator) == 0:
line = out.readline()
if not line: break
line = line[:-1]
m = log_start_re.match(line)
if m:
this_log = Log(
int(m.group('rev')), m.group('author'), m.group('date'), symbols)
line = out.readline()
if line == '\n':
# No changed paths
pass
elif line.startswith('Changed paths:'):
this_log.absorb_changed_paths(out)
else:
print 'unexpected log output'
print "Line: '%s'" % line
sys.exit(1)
absorb_message_body(out, int(m.group('lines')), this_log)
logs[this_log.revision] = this_log
elif len(line) == 0:
break # We've reached the end of the log output.
else:
print 'unexpected log output (missing revision line)'
print "Line: '%s'" % line
sys.exit(1)
else:
print 'unexpected log output (missing log separator)'
print "Line: '%s'" % line
sys.exit(1)
return logs
def erase(path):
"""Unconditionally remove PATH and its subtree, if any. PATH may be
non-existent, a file or symlink, or a directory."""
if os.path.isdir(path):
safe_rmtree(path)
elif os.path.exists(path):
os.remove(path)
log_msg_text_wrapper = textwrap.TextWrapper(width=76, break_long_words=False)
def sym_log_msg(symbolic_name, is_tag=None):
"""Return the expected log message for a cvs2svn-synthesized revision
creating branch or tag SYMBOLIC_NAME."""
# This reproduces the logic in SVNSymbolCommit.get_log_msg().
if is_tag:
type = 'tag'
else:
type = 'branch'
return log_msg_text_wrapper.fill(
"This commit was manufactured by cvs2svn to create %s '%s'."
% (type, symbolic_name)
)
def make_conversion_id(
name, args, passbypass, options_file=None, symbol_hints_file=None
):
"""Create an identifying tag for a conversion.
The return value can also be used as part of a filesystem path.
NAME is the name of the CVS repository.
ARGS are the extra arguments to be passed to cvs2svn.
PASSBYPASS is a boolean indicating whether the conversion is to be
run one pass at a time.
If OPTIONS_FILE is specified, it is an options file that will be
used for the conversion.
If SYMBOL_HINTS_FILE is specified, it is a symbol hints file that
will be used for the conversion.
The 1-to-1 mapping between cvs2svn command parameters and
conversion_ids allows us to avoid running the same conversion more
than once, when multiple tests use exactly the same conversion."""
conv_id = name
args = args[:]
if passbypass:
args.append('--passbypass')
if symbol_hints_file is not None:
args.append('--symbol-hints=%s' % (symbol_hints_file,))
# There are some characters that are forbidden in filenames, and
# there is a limit on the total length of a path to a file. So use
# a hash of the parameters rather than concatenating the parameters
# into a string.
if args:
conv_id += "-" + md5('\0'.join(args)).hexdigest()
# Some options-file based tests rely on knowing the paths to which
# the repository should be written, so we handle that option as a
# predictable string:
if options_file is not None:
conv_id += '--options=%s' % (options_file,)
return conv_id
class Conversion:
"""A record of a cvs2svn conversion.
Fields:
conv_id -- the conversion id for this Conversion.
name -- a one-word name indicating the involved repositories.
dumpfile -- the name of the SVN dumpfile created by the conversion
(if the DUMPFILE constructor argument was used); otherwise,
None.
repos -- the path to the svn repository. Unset if DUMPFILE was
specified.
logs -- a dictionary of Log instances, as returned by parse_log().
Unset if DUMPFILE was specified.
symbols -- a dictionary of symbols used for string interpolation
in path names.
stdout -- a list of lines written by cvs2svn to stdout
_wc -- the basename of the svn working copy (within tmp_dir).
Unset if DUMPFILE was specified.
_wc_path -- the path to the svn working copy, if it has already
been created; otherwise, None. (The working copy is created
lazily when get_wc() is called.) Unset if DUMPFILE was
specified.
_wc_tree -- the tree built from the svn working copy, if it has
already been created; otherwise, None. The tree is created
lazily when get_wc_tree() is called.) Unset if DUMPFILE was
specified.
_svnrepos -- the basename of the svn repository (within tmp_dir).
Unset if DUMPFILE was specified."""
# The number of the last cvs2svn pass (determined lazily by
# get_last_pass()).
last_pass = None
@classmethod
def get_last_pass(cls):
"""Return the number of cvs2svn's last pass."""
if cls.last_pass is None:
out = run_script(cvs2svn, None, '--help-passes')
cls.last_pass = int(out[-1].split()[0])
return cls.last_pass
def __init__(
self, conv_id, name, error_re, passbypass, symbols, args,
verbosity=None, options_file=None, symbol_hints_file=None, dumpfile=None,
):
self.conv_id = conv_id
self.name = name
self.symbols = symbols
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
if dumpfile:
self.dumpfile = os.path.join(tmp_dir, dumpfile)
# Clean up from any previous invocations of this script.
erase(self.dumpfile)
else:
self.dumpfile = None
self.repos = os.path.join(tmp_dir, '%s-svnrepos' % self.conv_id)
self._wc = os.path.join(tmp_dir, '%s-wc' % self.conv_id)
self._wc_path = None
self._wc_tree = None
# Clean up from any previous invocations of this script.
erase(self.repos)
erase(self._wc)
args = list(args)
if svntest.main.svnadmin_binary != 'svnadmin':
args.extend([
'--svnadmin=%s' % (svntest.main.svnadmin_binary,),
])
if options_file:
self.options_file = os.path.join(cvsrepos, options_file)
args.extend([
'--options=%s' % self.options_file,
])
args.append(verbosity or '-qqqqqq')
assert not symbol_hints_file
else:
self.options_file = None
args.extend([
'--tmpdir=%s' % tmp_dir,
])
args.append(verbosity or '-qqqqqq')
if symbol_hints_file:
self.symbol_hints_file = os.path.join(cvsrepos, symbol_hints_file)
args.extend([
'--symbol-hints=%s' % self.symbol_hints_file,
])
if self.dumpfile:
args.extend(['--dumpfile=%s' % (self.dumpfile,)])
else:
args.extend(['-s', self.repos])
args.extend([cvsrepos])
if passbypass:
self.stdout = []
for p in range(1, self.get_last_pass() + 1):
self.stdout += run_script(cvs2svn, error_re, '-p', str(p), *args)
else:
self.stdout = run_script(cvs2svn, error_re, *args)
if self.dumpfile:
if not os.path.isfile(self.dumpfile):
raise Failure(
"Dumpfile not created: '%s'"
% os.path.join(os.getcwd(), self.dumpfile)
)
else:
if os.path.isdir(self.repos):
self.logs = parse_log(self.repos, self.symbols)
elif error_re is None:
raise Failure(
"Repository not created: '%s'"
% os.path.join(os.getcwd(), self.repos)
)
def output_found(self, pattern):
"""Return True if PATTERN matches any line in self.stdout.
PATTERN is a regular expression pattern as a string.
"""
pattern_re = re.compile(pattern)
for line in self.stdout:
if pattern_re.match(line):
# We found the pattern that we were looking for.
return True
else:
return False
def find_tag_log(self, tagname):
"""Search LOGS for a log message containing 'TAGNAME' and return the
log in which it was found."""
for i in xrange(len(self.logs), 0, -1):
if self.logs[i].msg.find("'"+tagname+"'") != -1:
return self.logs[i]
raise ValueError("Tag %s not found in logs" % tagname)
def get_wc(self, *args):
"""Return the path to the svn working copy, or a path within the WC.
If a working copy has not been created yet, create it now.
If ARGS are specified, then they should be strings that form
fragments of a path within the WC. They are joined using
os.path.join() and appended to the WC path."""
if self._wc_path is None:
run_svn('co', repos_to_url(self.repos), self._wc)
self._wc_path = self._wc
return os.path.join(self._wc_path, *args)
def get_wc_tree(self):
if self._wc_tree is None:
self._wc_tree = svntest.tree.build_tree_from_wc(self.get_wc(), 1)
return self._wc_tree
def path_exists(self, *args):
"""Return True if the specified path exists within the repository.
(The strings in ARGS are first joined into a path using
os.path.join().)"""
return os.path.exists(self.get_wc(*args))
def check_props(self, keys, checks):
"""Helper function for checking lots of properties. For a list of
files in the conversion, check that the values of the properties
listed in KEYS agree with those listed in CHECKS. CHECKS is a
list of tuples: [ (filename, [value, value, ...]), ...], where the
values are listed in the same order as the key names are listed in
KEYS."""
for (file, values) in checks:
assert len(values) == len(keys)
props = props_for_path(self.get_wc_tree(), file)
for i in range(len(keys)):
if props.get(keys[i]) != values[i]:
raise Failure(
"File %s has property %s set to \"%s\" "
"(it should have been \"%s\").\n"
% (file, keys[i], props.get(keys[i]), values[i],)
)
class GitConversion:
"""A record of a cvs2svn conversion.
Fields:
name -- a one-word name indicating the CVS repository to be converted.
stdout -- a list of lines written by cvs2svn to stdout."""
def __init__(self, name, error_re, args, verbosity=None, options_file=None):
self.name = name
if not os.path.isdir(tmp_dir):
os.mkdir(tmp_dir)
cvsrepos = os.path.join(test_data_dir, '%s-cvsrepos' % self.name)
args = list(args)
if options_file:
self.options_file = os.path.join(cvsrepos, options_file)
args.extend([
'--options=%s' % self.options_file,
])
else:
self.options_file = None
args.append(verbosity or '-qqqqqq')
self.stdout = run_script(cvs2git, error_re, *args)
# Cache of conversions that have already been done. Keys are conv_id;
# values are Conversion instances.
already_converted = { }
def ensure_conversion(
name, error_re=None, passbypass=None,
trunk=None, branches=None, tags=None,
args=None, verbosity=None,
options_file=None, symbol_hints_file=None, dumpfile=None,
):
"""Convert CVS repository NAME to Subversion, but only if it has not
been converted before by this invocation of this script. If it has
been converted before, return the Conversion object from the
previous invocation.
If no error, return a Conversion instance.
If ERROR_RE is a string, it is a regular expression expected to
match some line of stderr printed by the conversion. If there is an
error and ERROR_RE is not set, then raise Failure.
If PASSBYPASS is set, then cvs2svn is run multiple times, each time
with a -p option starting at 1 and increasing to a (hardcoded) maximum.
NAME is just one word. For example, 'main' would mean to convert
'./test-data/main-cvsrepos', and after the conversion, the resulting
Subversion repository would be in './cvs2svn-tmp/main-svnrepos', and
a checked out head working copy in './cvs2svn-tmp/main-wc'.
Any other options to pass to cvs2svn should be in ARGS, each element
being one option, e.g., '--trunk-only'. If the option takes an
argument, include it directly, e.g., '--mime-types=PATH'. Arguments
are passed to cvs2svn in the order that they appear in ARGS.
If VERBOSITY is set, then it is passed to cvs2svn as an option.
Otherwise, the verbosity is turned way down so that only error
messages are emitted.
If OPTIONS_FILE is specified, then it should be the name of a file
within the main directory of the cvs repository associated with this
test. It is passed to cvs2svn using the --options option (which
suppresses some other options that are incompatible with --options).
If SYMBOL_HINTS_FILE is specified, then it should be the name of a
file within the main directory of the cvs repository associated with
this test. It is passed to cvs2svn using the --symbol-hints option.
If DUMPFILE is specified, then it is the name of a dumpfile within
the temporary directory to which the conversion output should be
written."""
if args is None:
args = []
else:
args = list(args)
if trunk is None:
trunk = 'trunk'
else:
args.append('--trunk=%s' % (trunk,))
if branches is None:
branches = 'branches'
else:
args.append('--branches=%s' % (branches,))
if tags is None:
tags = 'tags'
else:
args.append('--tags=%s' % (tags,))
conv_id = make_conversion_id(
name, args, passbypass, options_file, symbol_hints_file
)
if conv_id not in already_converted:
try:
# Run the conversion and store the result for the rest of this
# session:
already_converted[conv_id] = Conversion(
conv_id, name, error_re, passbypass,
{'trunk' : trunk, 'branches' : branches, 'tags' : tags},
args, verbosity, options_file, symbol_hints_file, dumpfile,
)
except Failure:
# Remember the failure so that a future attempt to run this conversion
# does not bother to retry, but fails immediately.
already_converted[conv_id] = None
raise
conv = already_converted[conv_id]
if conv is None:
raise Failure()
return conv
class Cvs2SvnTestFunction(TestCase):
"""A TestCase based on a naked Python function object.
FUNC should be a function that returns None on success and throws an
svntest.Failure exception on failure. It should have a brief
docstring describing what it does (and fulfilling certain
conditions). FUNC must take no arguments.
This class is almost identical to svntest.testcase.FunctionTestCase,
except that the test function does not require a sandbox and does
not accept any parameter (not even sandbox=None).
This class can be used as an annotation on a Python function.
"""
def __init__(self, func):
# it better be a function that accepts no parameters and has a
# docstring on it.
assert isinstance(func, types.FunctionType)
name = func.func_name
assert func.func_code.co_argcount == 0, \
'%s must not take any arguments' % name
doc = func.__doc__.strip()
assert doc, '%s must have a docstring' % name
# enforce stylistic guidelines for the function docstrings:
# - no longer than 50 characters
# - should not end in a period
# - should not be capitalized
assert len(doc) <= 50, \
"%s's docstring must be 50 characters or less" % name
assert doc[-1] != '.', \
"%s's docstring should not end in a period" % name
assert doc[0].lower() == doc[0], \
"%s's docstring should not be capitalized" % name
TestCase.__init__(self, doc=doc)
self.func = func
def get_function_name(self):
return self.func.func_name
def get_sandbox_name(self):
return None
def run(self, sandbox):
return self.func()
class Cvs2HgTestFunction(Cvs2SvnTestFunction):
"""Same as Cvs2SvnTestFunction, but for test cases that should be
skipped if Mercurial is not available.
"""
def run(self, sandbox):
if not have_hg:
raise svntest.Skip()
else:
return self.func()
class Cvs2SvnTestCase(TestCase):
def __init__(
self, name, doc=None, variant=None,
error_re=None, passbypass=None,
trunk=None, branches=None, tags=None,
args=None,
options_file=None, symbol_hints_file=None, dumpfile=None,
):
self.name = name
if doc is None:
# By default, use the first line of the class docstring as the
# doc:
doc = self.__doc__.splitlines()[0]
if variant is not None:
# Modify doc to show the variant. Trim doc first if necessary
# to stay within the 50-character limit.
suffix = '...variant %s' % (variant,)
doc = doc[:50 - len(suffix)] + suffix
TestCase.__init__(self, doc=doc)
self.error_re = error_re
self.passbypass = passbypass
self.trunk = trunk
self.branches = branches
self.tags = tags
self.args = args
self.options_file = options_file
self.symbol_hints_file = symbol_hints_file
self.dumpfile = dumpfile
def ensure_conversion(self):
return ensure_conversion(
self.name,
error_re=self.error_re, passbypass=self.passbypass,
trunk=self.trunk, branches=self.branches, tags=self.tags,
args=self.args,
options_file=self.options_file,
symbol_hints_file=self.symbol_hints_file,
dumpfile=self.dumpfile,
)
def get_sandbox_name(self):
return None
class Cvs2SvnPropertiesTestCase(Cvs2SvnTestCase):
"""Test properties resulting from a conversion."""
def __init__(self, name, props_to_test, expected_props, **kw):
"""Initialize an instance of Cvs2SvnPropertiesTestCase.
NAME is the name of the test, passed to Cvs2SvnTestCase.
PROPS_TO_TEST is a list of the names of svn properties that should
be tested. EXPECTED_PROPS is a list of tuples [(filename,
[value,...])], where the second item in each tuple is a list of
values expected for the properties listed in PROPS_TO_TEST for the
specified filename. If a property must *not* be set, then its
value should be listed as None."""
Cvs2SvnTestCase.__init__(self, name, **kw)
self.props_to_test = props_to_test
self.expected_props = expected_props
def run(self, sbox):
conv = self.ensure_conversion()
conv.check_props(self.props_to_test, self.expected_props)
#----------------------------------------------------------------------
# Tests.
#----------------------------------------------------------------------
@Cvs2SvnTestFunction
def show_usage():
"cvs2svn with no arguments shows usage"
out = run_script(cvs2svn, None)
if (len(out) > 2 and out[0].find('ERROR:') == 0
and out[1].find('DBM module')):
print 'cvs2svn cannot execute due to lack of proper DBM module.'
print 'Exiting without running any further tests.'
sys.exit(1)
if out[0].find('Usage:') < 0:
raise Failure('Basic cvs2svn invocation failed.')
@Cvs2SvnTestFunction
def cvs2svn_manpage():
"generate a manpage for cvs2svn"
out = run_script(cvs2svn, None, '--man')
@Cvs2SvnTestFunction
def cvs2git_manpage():
"generate a manpage for cvs2git"
out = run_script(cvs2git, None, '--man')
@XFail_deco()
@Cvs2HgTestFunction
def cvs2hg_manpage():
"generate a manpage for cvs2hg"
out = run_script(cvs2hg, None, '--man')
@Cvs2SvnTestFunction
def show_help_passes():
"cvs2svn --help-passes shows pass information"
out = run_script(cvs2svn, None, '--help-passes')
if out[0].find('PASSES') < 0:
raise Failure('cvs2svn --help-passes failed.')
@Cvs2SvnTestFunction
def attr_exec():
"detection of the executable flag"
if sys.platform == 'win32':
raise svntest.Skip()
st = os.stat(os.path.join('test-data', 'main-cvsrepos', 'single-files', 'attr-exec,v'))
if not st.st_mode & stat.S_IXUSR:
# This might be the case if the test is being run on a filesystem
# that is mounted "noexec".
raise svntest.Skip()
conv = ensure_conversion('main')
st = os.stat(conv.get_wc('trunk', 'single-files', 'attr-exec'))
if not st.st_mode & stat.S_IXUSR:
raise Failure()
@Cvs2SvnTestFunction
def space_fname():
"conversion of filename with a space"
conv = ensure_conversion('main')
if not conv.path_exists('trunk', 'single-files', 'space fname'):
raise Failure()
@Cvs2SvnTestFunction
def two_quick():