Skip a test when run against old servers.
[svn.git] / subversion / tests / cmdline / svntest / main.py
blobf14424336b97560c3a8be14a744b67baad07948d
2 # main.py: a shared, automated test suite for Subversion
4 # Subversion is a tool for revision control.
5 # See http://subversion.tigris.org for more information.
7 # ====================================================================
8 # Copyright (c) 2000-2007 CollabNet. All rights reserved.
10 # This software is licensed as described in the file COPYING, which
11 # you should have received as part of this distribution. The terms
12 # are also available at http://subversion.tigris.org/license-1.html.
13 # If newer versions of this license are posted there, you may use a
14 # newer version instead, at your option.
16 ######################################################################
18 import sys # for argv[]
19 import os
20 import shutil # for rmtree()
21 import re
22 import stat # for ST_MODE
23 import copy # for deepcopy()
24 import time # for time()
25 import traceback # for print_exc()
26 import threading
27 import Queue
29 import getopt
30 try:
31 my_getopt = getopt.gnu_getopt
32 except AttributeError:
33 my_getopt = getopt.getopt
35 from svntest import Failure
36 from svntest import Skip
37 from svntest import testcase
38 from svntest import wc
40 ######################################################################
42 # HOW TO USE THIS MODULE:
44 # Write a new python script that
46 # 1) imports this 'svntest' package
48 # 2) contains a number of related 'test' routines. (Each test
49 # routine should take no arguments, and return None on success
50 # or throw a Failure exception on failure. Each test should
51 # also contain a short docstring.)
53 # 3) places all the tests into a list that begins with None.
55 # 4) calls svntest.main.client_test() on the list.
57 # Also, your tests will probably want to use some of the common
58 # routines in the 'Utilities' section below.
60 #####################################################################
61 # Global stuff
63 class SVNProcessTerminatedBySignal(Failure):
64 "Exception raised if a spawned process segfaulted, aborted, etc."
65 pass
67 class SVNLineUnequal(Failure):
68 "Exception raised if two lines are unequal"
69 pass
71 class SVNUnmatchedError(Failure):
72 "Exception raised if an expected error is not found"
73 pass
75 class SVNCommitFailure(Failure):
76 "Exception raised if a commit failed"
77 pass
79 class SVNRepositoryCopyFailure(Failure):
80 "Exception raised if unable to copy a repository"
81 pass
83 class SVNRepositoryCreateFailure(Failure):
84 "Exception raised if unable to create a repository"
85 pass
87 # Windows specifics
88 if sys.platform == 'win32':
89 windows = True
90 file_scheme_prefix = 'file:///'
91 _exe = '.exe'
92 _bat = '.bat'
93 else:
94 windows = False
95 file_scheme_prefix = 'file://'
96 _exe = ''
97 _bat = ''
99 try:
100 from popen2 import Popen3
101 platform_with_popen3_class = True
102 except ImportError:
103 platform_with_popen3_class = False
105 # The location of our mock svneditor script.
106 if sys.platform == 'win32':
107 svneditor_script = os.path.join(sys.path[0], 'svneditor.bat')
108 else:
109 svneditor_script = os.path.join(sys.path[0], 'svneditor.py')
111 # Username and password used by the working copies
112 wc_author = 'jrandom'
113 wc_passwd = 'rayjandom'
115 # Username and password used by the working copies for "second user"
116 # scenarios
117 wc_author2 = 'jconstant' # use the same password as wc_author
119 ######################################################################
120 # Global variables set during option parsing. These should not be used
121 # until the variable command_line_parsed has been set to True, as is
122 # done in run_tests below.
123 command_line_parsed = False
125 # The locations of the svn, svnadmin and svnlook binaries, relative to
126 # the only scripts that import this file right now (they live in ../).
127 # Use --bin to override these defaults.
128 svn_binary = os.path.abspath('../../svn/svn' + _exe)
129 svnadmin_binary = os.path.abspath('../../svnadmin/svnadmin' + _exe)
130 svnlook_binary = os.path.abspath('../../svnlook/svnlook' + _exe)
131 svnsync_binary = os.path.abspath('../../svnsync/svnsync' + _exe)
132 svnversion_binary = os.path.abspath('../../svnversion/svnversion' + _exe)
133 svndumpfilter_binary = os.path.abspath('../../svndumpfilter/svndumpfilter' + \
134 _exe)
136 # Global variable indicating if we want verbose output, that is,
137 # details of what commands each test does as it does them. This is
138 # incompatible with quiet_mode.
139 verbose_mode = False
141 # Global variable indicating if we want quiet output, that is, don't
142 # show PASS, XFAIL, or SKIP notices, but do show FAIL and XPASS. This
143 # is incompatible with verbose_mode.
144 quiet_mode = False
146 # Global variable indicating if we want test data cleaned up after success
147 cleanup_mode = False
149 # Global variable indicating if svnserve should use Cyrus SASL
150 enable_sasl = False
152 # Global variable indicating which DAV library, if any, is in use
153 # ('neon', 'serf')
154 http_library = None
156 # Global variable indicating what the minor version of the server
157 # tested against is (4 for 1.4.x, for example).
158 server_minor_version = 5
160 # Global variable indicating if this is a child process and no cleanup
161 # of global directories is needed.
162 is_child_process = False
164 # Global URL to testing area. Default to ra_local, current working dir.
165 test_area_url = file_scheme_prefix + os.path.abspath(os.getcwd())
166 if windows:
167 test_area_url = test_area_url.replace('\\', '/')
169 # Location to the pristine repository, will be calculated from test_area_url
170 # when we know what the user specified for --url.
171 pristine_url = None
173 # Global variable indicating the FS type for repository creations.
174 fs_type = None
176 # End of command-line-set global variables.
177 ######################################################################
179 # All temporary repositories and working copies are created underneath
180 # this dir, so there's one point at which to mount, e.g., a ramdisk.
181 work_dir = "svn-test-work"
183 # Constant for the merge info property.
184 SVN_PROP_MERGEINFO = "svn:mergeinfo"
186 # Where we want all the repositories and working copies to live.
187 # Each test will have its own!
188 general_repo_dir = os.path.join(work_dir, "repositories")
189 general_wc_dir = os.path.join(work_dir, "working_copies")
191 # temp directory in which we will create our 'pristine' local
192 # repository and other scratch data. This should be removed when we
193 # quit and when we startup.
194 temp_dir = os.path.join(work_dir, 'local_tmp')
196 # (derivatives of the tmp dir.)
197 pristine_dir = os.path.join(temp_dir, "repos")
198 greek_dump_dir = os.path.join(temp_dir, "greekfiles")
199 default_config_dir = os.path.abspath(os.path.join(temp_dir, "config"))
202 # Our pristine greek-tree state.
204 # If a test wishes to create an "expected" working-copy tree, it should
205 # call main.greek_state.copy(). That method will return a copy of this
206 # State object which can then be edited.
208 _item = wc.StateItem
209 greek_state = wc.State('', {
210 'iota' : _item("This is the file 'iota'.\n"),
211 'A' : _item(),
212 'A/mu' : _item("This is the file 'mu'.\n"),
213 'A/B' : _item(),
214 'A/B/lambda' : _item("This is the file 'lambda'.\n"),
215 'A/B/E' : _item(),
216 'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
217 'A/B/E/beta' : _item("This is the file 'beta'.\n"),
218 'A/B/F' : _item(),
219 'A/C' : _item(),
220 'A/D' : _item(),
221 'A/D/gamma' : _item("This is the file 'gamma'.\n"),
222 'A/D/G' : _item(),
223 'A/D/G/pi' : _item("This is the file 'pi'.\n"),
224 'A/D/G/rho' : _item("This is the file 'rho'.\n"),
225 'A/D/G/tau' : _item("This is the file 'tau'.\n"),
226 'A/D/H' : _item(),
227 'A/D/H/chi' : _item("This is the file 'chi'.\n"),
228 'A/D/H/psi' : _item("This is the file 'psi'.\n"),
229 'A/D/H/omega' : _item("This is the file 'omega'.\n"),
233 ######################################################################
234 # Utilities shared by the tests
235 def wrap_ex(func):
236 "Wrap a function, catch, print and ignore exceptions"
237 def w(*args, **kwds):
238 try:
239 return func(*args, **kwds)
240 except Failure, ex:
241 if ex.__class__ != Failure or ex.args:
242 ex_args = str(ex)
243 if ex_args:
244 print 'EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args)
245 else:
246 print 'EXCEPTION:', ex.__class__.__name__
247 return w
249 def setup_development_mode():
250 "Wraps functions in module actions"
251 l = [ 'run_and_verify_svn',
252 'run_and_verify_svnversion',
253 'run_and_verify_load',
254 'run_and_verify_dump',
255 'run_and_verify_checkout',
256 'run_and_verify_export',
257 'run_and_verify_update',
258 'run_and_verify_merge',
259 'run_and_verify_merge2',
260 'run_and_verify_switch',
261 'run_and_verify_commit',
262 'run_and_verify_unquiet_status',
263 'run_and_verify_status',
264 'run_and_verify_diff_summarize',
265 'run_and_verify_diff_summarize_xml',
266 'run_and_validate_lock']
268 for func in l:
269 setattr(actions, func, wrap_ex(getattr(actions, func)))
271 def get_admin_name():
272 "Return name of SVN administrative subdirectory."
274 if (windows or sys.platform == 'cygwin') \
275 and os.environ.has_key('SVN_ASP_DOT_NET_HACK'):
276 return '_svn'
277 else:
278 return '.svn'
280 def get_start_commit_hook_path(repo_dir):
281 "Return the path of the start-commit-hook conf file in REPO_DIR."
283 return os.path.join(repo_dir, "hooks", "start-commit")
286 def get_pre_commit_hook_path(repo_dir):
287 "Return the path of the pre-commit-hook conf file in REPO_DIR."
289 return os.path.join(repo_dir, "hooks", "pre-commit")
292 def get_post_commit_hook_path(repo_dir):
293 "Return the path of the post-commit-hook conf file in REPO_DIR."
295 return os.path.join(repo_dir, "hooks", "post-commit")
297 def get_pre_revprop_change_hook_path(repo_dir):
298 "Return the path of the pre-revprop-change hook script in REPO_DIR."
300 return os.path.join(repo_dir, "hooks", "pre-revprop-change")
302 def get_svnserve_conf_file_path(repo_dir):
303 "Return the path of the svnserve.conf file in REPO_DIR."
305 return os.path.join(repo_dir, "conf", "svnserve.conf")
307 # Run any binary, logging the command line and return code
308 def run_command(command, error_expected, binary_mode=0, *varargs):
309 """Run COMMAND with VARARGS; return exit code as int; stdout, stderr
310 as lists of lines.
311 If ERROR_EXPECTED is None, any stderr also will be printed."""
313 return run_command_stdin(command, error_expected, binary_mode,
314 None, *varargs)
316 # A regular expression that matches arguments that are trivially safe
317 # to pass on a command line without quoting on any supported operating
318 # system:
319 _safe_arg_re = re.compile(r'^[A-Za-z\d\.\_\/\-\:\@]+$')
321 def _quote_arg(arg):
322 """Quote ARG for a command line.
324 Simply surround every argument in double-quotes unless it contains
325 only universally harmless characters.
327 WARNING: This function cannot handle arbitrary command-line
328 arguments. It can easily be confused by shell metacharacters. A
329 perfect job would be difficult and OS-dependent (see, for example,
330 http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp).
331 In other words, this function is just good enough for what we need
332 here."""
334 arg = str(arg)
335 if _safe_arg_re.match(arg):
336 return arg
337 else:
338 if os.name != 'nt':
339 arg = arg.replace('$', '\$')
340 return '"%s"' % (arg,)
342 def open_pipe(command, mode):
343 """Opens a popen3 pipe to COMMAND in MODE.
345 Returns (infile, outfile, errfile, waiter); waiter
346 should be passed to wait_on_pipe."""
347 if platform_with_popen3_class:
348 kid = Popen3(command, True)
349 return kid.tochild, kid.fromchild, kid.childerr, (kid, command)
350 else:
351 inf, outf, errf = os.popen3(command, mode)
352 return inf, outf, errf, None
354 def wait_on_pipe(waiter, stdout_lines, stderr_lines):
355 """Waits for KID (opened with open_pipe) to finish, dying
356 if it does. Uses STDOUT_LINES and STDERR_LINES for error message
357 if kid fails. Returns kid's exit code."""
358 if waiter is None:
359 return
361 kid, command = waiter
363 wait_code = kid.wait()
365 if os.WIFSIGNALED(wait_code):
366 exit_signal = os.WTERMSIG(wait_code)
367 if stdout_lines is not None:
368 sys.stdout.write("".join(stdout_lines))
369 if stderr_lines is not None:
370 sys.stderr.write("".join(stderr_lines))
371 if verbose_mode:
372 # show the whole path to make it easier to start a debugger
373 sys.stderr.write("CMD: %s terminated by signal %d\n"
374 % (command, exit_signal))
375 raise SVNProcessTerminatedBySignal
376 else:
377 exit_code = os.WEXITSTATUS(wait_code)
378 if exit_code and verbose_mode:
379 sys.stderr.write("CMD: %s exited with %d\n" % (command, exit_code))
380 return exit_code
382 # Run any binary, supplying input text, logging the command line
383 def spawn_process(command, binary_mode=0,stdin_lines=None, *varargs):
384 args = ' '.join(map(_quote_arg, varargs))
386 # Log the command line
387 if verbose_mode and not command.endswith('.py'):
388 print 'CMD:', os.path.basename(command) + ' ' + args,
390 if binary_mode:
391 mode = 'b'
392 else:
393 mode = 't'
395 infile, outfile, errfile, kid = open_pipe(command + ' ' + args, mode)
397 if stdin_lines:
398 map(infile.write, stdin_lines)
400 infile.close()
402 stdout_lines = outfile.readlines()
403 stderr_lines = errfile.readlines()
405 outfile.close()
406 errfile.close()
408 exit_code = wait_on_pipe(kid, stdout_lines, stderr_lines)
410 return exit_code, stdout_lines, stderr_lines
412 def run_command_stdin(command, error_expected, binary_mode=0,
413 stdin_lines=None, *varargs):
414 """Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
415 which should include newline characters) to program via stdin - this
416 should not be very large, as if the program outputs more than the OS
417 is willing to buffer, this will deadlock, with both Python and
418 COMMAND waiting to write to each other for ever.
419 Return exit code as int; stdout, stderr as lists of lines.
420 If ERROR_EXPECTED is None, any stderr also will be printed."""
422 if verbose_mode:
423 start = time.time()
425 exit_code, stdout_lines, stderr_lines = spawn_process(command,
426 binary_mode,
427 stdin_lines,
428 *varargs)
430 if verbose_mode:
431 stop = time.time()
432 print '<TIME = %.6f>' % (stop - start)
434 if (not error_expected) and (stderr_lines):
435 map(sys.stdout.write, stderr_lines)
436 raise Failure
438 return exit_code, stdout_lines, stderr_lines
440 def create_config_dir(cfgdir, config_contents=None, server_contents=None):
441 "Create config directories and files"
443 # config file names
444 cfgfile_cfg = os.path.join(cfgdir, 'config')
445 cfgfile_srv = os.path.join(cfgdir, 'servers')
447 # create the directory
448 if not os.path.isdir(cfgdir):
449 os.makedirs(cfgdir)
451 # define default config file contents if none provided
452 if config_contents is None:
453 config_contents = """
455 [miscellany]
456 interactive-conflicts = false
459 # define default server file contents if none provided
460 if server_contents is None:
461 if http_library:
462 server_contents = """
464 [global]
465 http-library=%s
466 """ % (http_library)
467 else:
468 server_contents = "#\n"
470 file_write(cfgfile_cfg, config_contents)
471 file_write(cfgfile_srv, server_contents)
473 def _with_config_dir(args):
474 if '--config-dir' in args:
475 return args
476 else:
477 return args + ('--config-dir', default_config_dir)
479 def _with_auth(args):
480 assert '--password' not in args
481 args = args + ('--password', wc_passwd,
482 '--no-auth-cache' )
483 if '--username' in args:
484 return args
485 else:
486 return args + ('--username', wc_author )
488 # For running subversion and returning the output
489 def run_svn(error_expected, *varargs):
490 """Run svn with VARARGS; return exit code as int; stdout, stderr as
491 lists of lines.
492 If ERROR_EXPECTED is None, any stderr also will be printed. If
493 you're just checking that something does/doesn't come out of
494 stdout/stderr, you might want to use actions.run_and_verify_svn()."""
495 return run_command(svn_binary, error_expected, 0,
496 *(_with_auth(_with_config_dir(varargs))))
498 # For running svnadmin. Ignores the output.
499 def run_svnadmin(*varargs):
500 """Run svnadmin with VARARGS, returns exit code as int; stdout, stderr as
501 list of lines."""
502 return run_command(svnadmin_binary, 1, 0, *varargs)
504 # For running svnlook. Ignores the output.
505 def run_svnlook(*varargs):
506 """Run svnlook with VARARGS, returns exit code as int; stdout, stderr as
507 list of lines."""
508 return run_command(svnlook_binary, 1, 0, *varargs)
510 def run_svnsync(*varargs):
511 """Run svnsync with VARARGS, returns exit code as int; stdout, stderr as
512 list of lines."""
513 return run_command(svnsync_binary, 1, 0, *(_with_config_dir(varargs)))
515 def run_svnversion(*varargs):
516 """Run svnversion with VARARGS, returns exit code as int; stdout, stderr
517 as list of lines."""
518 return run_command(svnversion_binary, 1, 0, *varargs)
520 # Chmod recursively on a whole subtree
521 def chmod_tree(path, mode, mask):
522 def visit(arg, dirname, names):
523 mode, mask = arg
524 for name in names:
525 fullname = os.path.join(dirname, name)
526 if not os.path.islink(fullname):
527 new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
528 os.chmod(fullname, new_mode)
529 os.path.walk(path, visit, (mode, mask))
531 # For clearing away working copies
532 def safe_rmtree(dirname, retry=0):
533 "Remove the tree at DIRNAME, making it writable first"
534 def rmtree(dirname):
535 chmod_tree(dirname, 0666, 0666)
536 shutil.rmtree(dirname)
538 if not os.path.exists(dirname):
539 return
541 if retry:
542 for delay in (0.5, 1, 2, 4):
543 try:
544 rmtree(dirname)
545 break
546 except:
547 time.sleep(delay)
548 else:
549 rmtree(dirname)
550 else:
551 rmtree(dirname)
553 # For making local mods to files
554 def file_append(path, new_text):
555 "Append NEW_TEXT to file at PATH"
556 file_write(path, new_text, 'a') # open in (a)ppend mode
558 # Append in binary mode
559 def file_append_binary(path, new_text):
560 "Append NEW_TEXT to file at PATH in binary mode"
561 file_write(path, new_text, 'ab') # open in (a)ppend mode
563 # For creating new files, and making local mods to existing files.
564 def file_write(path, contents, mode = 'w'):
565 """Write the CONTENTS to the file at PATH, opening file using MODE,
566 which is (w)rite by default."""
567 fp = open(path, mode)
568 fp.write(contents)
569 fp.close()
571 # For reading the contents of a file
572 def file_read(path, mode = 'r'):
573 """Return the contents of the file at PATH, opening file using MODE,
574 which is (r)ead by default."""
575 fp = open(path, mode)
576 contents = fp.read()
577 fp.close()
578 return contents
580 # For replacing parts of contents in an existing file, with new content.
581 def file_substitute(path, contents, new_contents):
582 """Replace the CONTENTS in the file at PATH using the NEW_CONTENTS"""
583 fp = open(path, 'r')
584 fcontent = fp.read()
585 fp.close()
586 fcontent = fcontent.replace(contents, new_contents)
587 fp = open(path, 'w')
588 fp.write(fcontent)
589 fp.close()
591 # For creating blank new repositories
592 def create_repos(path):
593 """Create a brand-new SVN repository at PATH. If PATH does not yet
594 exist, create it."""
596 if not os.path.exists(path):
597 os.makedirs(path) # this creates all the intermediate dirs, if neccessary
599 opts = ("--bdb-txn-nosync",)
600 if server_minor_version < 5:
601 opts += ("--pre-1.5-compatible",)
602 if fs_type is not None:
603 opts += ("--fs-type=" + fs_type,)
604 exit_code, stdout, stderr = run_command(svnadmin_binary, 1, 0, "create",
605 path, *opts)
607 # Skip tests if we can't create the repository.
608 if stderr:
609 for line in stderr:
610 if line.find('Unknown FS type') != -1:
611 raise Skip
612 # If the FS type is known, assume the repos couldn't be created
613 # (e.g. due to a missing 'svnadmin' binary).
614 raise SVNRepositoryCreateFailure("".join(stderr).rstrip())
616 # Allow unauthenticated users to write to the repos, for ra_svn testing.
617 file_write(get_svnserve_conf_file_path(path),
618 "[general]\nauth-access = write\n");
619 if enable_sasl:
620 file_append(get_svnserve_conf_file_path(path),
621 "realm = svntest\n[sasl]\nuse-sasl = true\n")
622 else:
623 file_append(get_svnserve_conf_file_path(path), "password-db = passwd\n")
624 file_append(os.path.join(path, "conf", "passwd"),
625 "[users]\njrandom = rayjandom\njconstant = rayjandom\n");
626 # make the repos world-writeable, for mod_dav_svn's sake.
627 chmod_tree(path, 0666, 0666)
629 # For copying a repository
630 def copy_repos(src_path, dst_path, head_revision, ignore_uuid = 1):
631 "Copy the repository SRC_PATH, with head revision HEAD_REVISION, to DST_PATH"
633 # Do an svnadmin dump|svnadmin load cycle. Print a fake pipe command so that
634 # the displayed CMDs can be run by hand
635 create_repos(dst_path)
636 dump_args = ' dump "' + src_path + '"'
637 load_args = ' load "' + dst_path + '"'
639 if ignore_uuid:
640 load_args = load_args + " --ignore-uuid"
641 if verbose_mode:
642 print 'CMD:', os.path.basename(svnadmin_binary) + dump_args, \
643 '|', os.path.basename(svnadmin_binary) + load_args,
644 start = time.time()
646 dump_in, dump_out, dump_err, dump_kid = \
647 open_pipe(svnadmin_binary + dump_args, 'b')
648 load_in, load_out, load_err, load_kid = \
649 open_pipe(svnadmin_binary + load_args, 'b')
650 stop = time.time()
651 if verbose_mode:
652 print '<TIME = %.6f>' % (stop - start)
654 while 1:
655 data = dump_out.read(1024*1024) # Arbitrary buffer size
656 if data == "":
657 break
658 load_in.write(data)
659 load_in.close() # Tell load we are done
661 dump_lines = dump_err.readlines()
662 load_lines = load_out.readlines()
663 dump_in.close()
664 dump_out.close()
665 dump_err.close()
666 load_out.close()
667 load_err.close()
668 # Wait on the pipes; ignore return code.
669 wait_on_pipe(dump_kid, None, dump_lines)
670 wait_on_pipe(load_kid, load_lines, None)
672 dump_re = re.compile(r'^\* Dumped revision (\d+)\.\r?$')
673 expect_revision = 0
674 for dump_line in dump_lines:
675 match = dump_re.match(dump_line)
676 if not match or match.group(1) != str(expect_revision):
677 print 'ERROR: dump failed:', dump_line,
678 raise SVNRepositoryCopyFailure
679 expect_revision += 1
680 if expect_revision != head_revision + 1:
681 print 'ERROR: dump failed; did not see revision', head_revision
682 raise SVNRepositoryCopyFailure
684 load_re = re.compile(r'^------- Committed revision (\d+) >>>\r?$')
685 expect_revision = 1
686 for load_line in load_lines:
687 match = load_re.match(load_line)
688 if match:
689 if match.group(1) != str(expect_revision):
690 print 'ERROR: load failed:', load_line,
691 raise SVNRepositoryCopyFailure
692 expect_revision += 1
693 if expect_revision != head_revision + 1:
694 print 'ERROR: load failed; did not see revision', head_revision
695 raise SVNRepositoryCopyFailure
698 def canonicalize_url(input):
699 "Canonicalize the url, if the scheme is unknown, returns intact input"
701 m = re.match(r"^((file://)|((svn|svn\+ssh|http|https)(://)))", input)
702 if m:
703 scheme = m.group(1)
704 return scheme + re.sub(r'//*', '/', input[len(scheme):])
705 else:
706 return input
709 def create_python_hook_script (hook_path, hook_script_code):
710 """Create a Python hook script at HOOK_PATH with the specified
711 HOOK_SCRIPT_CODE."""
713 if sys.platform == 'win32':
714 # Use an absolute path since the working directory is not guaranteed
715 hook_path = os.path.abspath(hook_path)
716 # Fill the python file.
717 file_write ("%s.py" % hook_path, hook_script_code)
718 # Fill the batch wrapper file.
719 file_append ("%s.bat" % hook_path,
720 "@\"%s\" %s.py %%*\n" % (sys.executable, hook_path))
721 else:
722 # For all other platforms
723 file_write (hook_path, "#!%s\n%s" % (sys.executable, hook_script_code))
724 os.chmod (hook_path, 0755)
726 def write_restrictive_svnserve_conf(repo_dir, anon_access="none"):
727 "Create a restrictive authz file ( no anynomous access )."
729 fp = open(get_svnserve_conf_file_path(repo_dir), 'w')
730 fp.write("[general]\nanon-access = %s\nauth-access = write\n"
731 "authz-db = authz\n" % anon_access)
732 if enable_sasl == 1:
733 fp.write("realm = svntest\n[sasl]\nuse-sasl = true\n");
734 else:
735 fp.write("password-db = passwd\n")
736 fp.close()
738 # Warning: because mod_dav_svn uses one shared authz file for all
739 # repositories, you *cannot* use write_authz_file in any test that
740 # might be run in parallel.
742 # write_authz_file can *only* be used in test suites which disable
743 # parallel execution at the bottom like so
744 # if __name__ == '__main__':
745 # svntest.main.run_tests(test_list, serial_only = True)
746 def write_authz_file(sbox, rules, sections=None):
747 """Write an authz file to SBOX, appropriate for the RA method used,
748 with authorizations rules RULES mapping paths to strings containing
749 the rules. You can add sections SECTIONS (ex. groups, aliases...) with
750 an appropriate list of mappings.
752 fp = open(sbox.authz_file, 'w')
754 # When the sandbox repository is read only it's name will be different from
755 # the repository name.
756 repo_name = sbox.repo_dir
757 while repo_name[-1] == '/':
758 repo_name = repo_name[:-1]
759 repo_name = os.path.basename(repo_name)
761 if sbox.repo_url.startswith("http"):
762 prefix = repo_name + ":"
763 else:
764 prefix = ""
765 if sections:
766 for p, r in sections.items():
767 fp.write("[%s]\n%s\n" % (p, r))
769 for p, r in rules.items():
770 fp.write("[%s%s]\n%s\n" % (prefix, p, r))
771 fp.close()
773 def use_editor(func):
774 os.environ['SVN_EDITOR'] = svneditor_script
775 os.environ['SVN_MERGE'] = svneditor_script
776 os.environ['SVNTEST_EDITOR_FUNC'] = func
779 def merge_notify_line(revstart=None, revend=None, same_URL=True):
780 """Return an expected output line that describes the beginning of a
781 merge operation on revisions REVSTART through REVEND. Omit both
782 REVSTART and REVEND for the case where the left and right sides of
783 the merge are from different URLs."""
784 if not same_URL:
785 return "--- Merging differences between repository URLs into '.+':\n"
786 if revend is None:
787 if revstart is None:
788 # The left and right sides of the merge are from different URLs.
789 return "--- Merging differences between repository URLs into '.+':\n"
790 elif revstart < 0:
791 return "--- Reverse-merging r%ld into '.+':\n" % abs(revstart)
792 else:
793 return "--- Merging r%ld into '.+':\n" % revstart
794 else:
795 if revstart > revend:
796 return "--- Reverse-merging r%ld through r%ld into '.+':\n" % (revstart,
797 revend)
798 else:
799 return "--- Merging r%ld through r%ld into '.+':\n" % (revstart, revend)
802 ######################################################################
803 # Functions which check the test configuration
804 # (useful for conditional XFails)
806 def _check_command_line_parsed():
807 """Raise an exception if the command line has not yet been parsed."""
808 if not command_line_parsed:
809 raise Failure("Condition cannot be tested until command line is parsed")
811 def is_ra_type_dav():
812 _check_command_line_parsed()
813 return test_area_url.startswith('http')
815 def is_ra_type_svn():
816 _check_command_line_parsed()
817 return test_area_url.startswith('svn')
819 def is_ra_type_file():
820 _check_command_line_parsed()
821 return test_area_url.startswith('file')
823 def is_fs_type_fsfs():
824 _check_command_line_parsed()
825 # This assumes that fsfs is the default fs implementation.
826 return fs_type == 'fsfs' or fs_type is None
828 def is_os_windows():
829 return os.name == 'nt'
831 def is_posix_os():
832 return os.name == 'posix'
834 def is_os_darwin():
835 return sys.platform == 'darwin'
837 def server_has_mergeinfo():
838 _check_command_line_parsed()
839 return server_minor_version >= 5
841 def server_has_revprop_commit():
842 _check_command_line_parsed()
843 return server_minor_version >= 5
845 def server_sends_copyfrom_on_update():
846 _check_command_line_parsed()
847 return server_minor_version >= 5
849 def server_authz_has_aliases():
850 _check_command_line_parsed()
851 return server_minor_version >= 5
853 def server_gets_client_capabilities():
854 _check_command_line_parsed()
855 return server_minor_version >= 5
858 ######################################################################
859 # Sandbox handling
861 class Sandbox:
862 """Manages a sandbox (one or more repository/working copy pairs) for
863 a test to operate within."""
865 dependents = None
867 def __init__(self, module, idx):
868 self._set_name("%s-%d" % (module, idx))
870 def _set_name(self, name, read_only = False):
871 """A convenience method for renaming a sandbox, useful when
872 working with multiple repositories in the same unit test."""
873 if not name is None:
874 self.name = name
875 self.read_only = read_only
876 self.wc_dir = os.path.join(general_wc_dir, self.name)
877 if not read_only:
878 self.repo_dir = os.path.join(general_repo_dir, self.name)
879 self.repo_url = test_area_url + '/' + self.repo_dir
880 else:
881 self.repo_dir = pristine_dir
882 self.repo_url = pristine_url
884 ### TODO: Move this into to the build() method
885 # For dav tests we need a single authz file which must be present,
886 # so we recreate it each time a sandbox is created with some default
887 # contents.
888 if self.repo_url.startswith("http"):
889 # this dir doesn't exist out of the box, so we may have to make it
890 if not os.path.exists(work_dir):
891 os.makedirs(work_dir)
892 self.authz_file = os.path.join(work_dir, "authz")
893 file_write(self.authz_file, "[/]\n* = rw\n")
895 # For svnserve tests we have a per-repository authz file, and it
896 # doesn't need to be there in order for things to work, so we don't
897 # have any default contents.
898 elif self.repo_url.startswith("svn"):
899 self.authz_file = os.path.join(self.repo_dir, "conf", "authz")
901 if windows:
902 self.repo_url = self.repo_url.replace('\\', '/')
903 self.test_paths = [self.wc_dir, self.repo_dir]
905 def clone_dependent(self, copy_wc=False):
906 """A convenience method for creating a near-duplicate of this
907 sandbox, useful when working with multiple repositories in the
908 same unit test. If COPY_WC is true, make an exact copy of this
909 sandbox's working copy at the new sandbox's working copy
910 directory. Any necessary cleanup operations are triggered by
911 cleanup of the original sandbox."""
913 if not self.dependents:
914 self.dependents = []
915 clone = copy.deepcopy(self)
916 self.dependents.append(clone)
917 clone._set_name("%s-%d" % (self.name, len(self.dependents)))
918 if copy_wc:
919 self.add_test_path(clone.wc_dir)
920 shutil.copytree(self.wc_dir, clone.wc_dir, symlinks=True)
921 return clone
923 def build(self, name = None, create_wc = True, read_only = False):
924 self._set_name(name, read_only)
925 if actions.make_repo_and_wc(self, create_wc, read_only):
926 raise Failure("Could not build repository and sandbox '%s'" % self.name)
928 def add_test_path(self, path, remove=True):
929 self.test_paths.append(path)
930 if remove:
931 safe_rmtree(path)
933 def add_repo_path(self, suffix, remove=1):
934 path = os.path.join(general_repo_dir, self.name) + '.' + suffix
935 url = test_area_url + '/' + path
936 if windows:
937 url = url.replace('\\', '/')
938 self.add_test_path(path, remove)
939 return path, url
941 def add_wc_path(self, suffix, remove=1):
942 path = self.wc_dir + '.' + suffix
943 self.add_test_path(path, remove)
944 return path
946 def cleanup_test_paths(self):
947 "Clean up detritus from this sandbox, and any dependents."
948 if self.dependents:
949 # Recursively cleanup any dependent sandboxes.
950 for sbox in self.dependents:
951 sbox.cleanup_test_paths()
952 # cleanup all test specific working copies and repositories
953 for path in self.test_paths:
954 if not path is pristine_dir:
955 _cleanup_test_path(path)
958 _deferred_test_paths = []
959 def _cleanup_deferred_test_paths():
960 global _deferred_test_paths
961 test_paths = _deferred_test_paths[:]
962 _deferred_test_paths = []
963 for path in test_paths:
964 _cleanup_test_path(path, 1)
966 def _cleanup_test_path(path, retrying=None):
967 if verbose_mode:
968 if retrying:
969 print "CLEANUP: RETRY:", path
970 else:
971 print "CLEANUP:", path
972 try:
973 safe_rmtree(path)
974 except:
975 if verbose_mode:
976 print "WARNING: cleanup failed, will try again later"
977 _deferred_test_paths.append(path)
979 class TestSpawningThread(threading.Thread):
980 """A thread that runs test cases in their own processes.
981 Receives test numbers to run from the queue, and saves results into
982 the results field."""
983 def __init__(self, queue):
984 threading.Thread.__init__(self)
985 self.queue = queue
986 self.results = []
988 def run(self):
989 while True:
990 try:
991 next_index = self.queue.get_nowait()
992 except Queue.Empty:
993 return
995 self.run_one(next_index)
997 def run_one(self, index):
998 command = sys.argv[0]
1000 args = []
1001 args.append(str(index))
1002 args.append('-c')
1003 # add some startup arguments from this process
1004 if fs_type:
1005 args.append('--fs-type=' + fs_type)
1006 if test_area_url:
1007 args.append('--url=' + test_area_url)
1008 if verbose_mode:
1009 args.append('-v')
1010 if cleanup_mode:
1011 args.append('--cleanup')
1012 if enable_sasl:
1013 args.append('--enable-sasl')
1014 if http_library:
1015 args.append('--http-library=' + http_library)
1016 if server_minor_version:
1017 args.append('--server-minor-version=' + str(server_minor_version))
1019 result, stdout_lines, stderr_lines = spawn_process(command, 1, None, *args)
1020 # "result" will be None on platforms without Popen3 (e.g. Windows)
1021 if filter(lambda x: x.startswith('FAIL: ') or x.startswith('XPASS: '),
1022 stdout_lines):
1023 result = 1
1024 self.results.append((index, result, stdout_lines, stderr_lines))
1025 sys.stdout.write('.')
1026 sys.stdout.flush()
1028 class TestRunner:
1029 """Encapsulate a single test case (predicate), including logic for
1030 runing the test and test list output."""
1032 def __init__(self, func, index):
1033 self.pred = testcase.create_test_case(func)
1034 self.index = index
1036 def list(self):
1037 print " %2d %-5s %s" % (self.index,
1038 self.pred.list_mode(),
1039 self.pred.get_description())
1040 self.pred.check_description()
1042 def _print_name(self):
1043 print os.path.basename(sys.argv[0]), str(self.index) + ":", \
1044 self.pred.get_description()
1045 self.pred.check_description()
1047 def run(self):
1048 """Run self.pred and return the result. The return value is
1049 - 0 if the test was successful
1050 - 1 if it errored in a way that indicates test failure
1051 - 2 if the test skipped
1053 if self.pred.need_sandbox():
1054 # ooh! this function takes a sandbox argument
1055 sandbox = Sandbox(self.pred.get_sandbox_name(), self.index)
1056 kw = { 'sandbox' : sandbox }
1057 else:
1058 sandbox = None
1059 kw = {}
1061 # Explicitly set this so that commands that commit but don't supply a
1062 # log message will fail rather than invoke an editor.
1063 # Tests that want to use an editor should invoke svntest.main.use_editor.
1064 os.environ['SVN_EDITOR'] = ''
1065 os.environ['SVNTEST_EDITOR_FUNC'] = ''
1066 actions.no_sleep_for_timestamps()
1068 saved_dir = os.getcwd()
1069 try:
1070 rc = apply(self.pred.run, (), kw)
1071 if rc is not None:
1072 print 'STYLE ERROR in',
1073 self._print_name()
1074 print 'Test driver returned a status code.'
1075 sys.exit(255)
1076 result = 0
1077 except Skip, ex:
1078 result = 2
1079 except Failure, ex:
1080 result = 1
1081 # We captured Failure and its subclasses. We don't want to print
1082 # anything for plain old Failure since that just indicates test
1083 # failure, rather than relevant information. However, if there
1084 # *is* information in the exception's arguments, then print it.
1085 if ex.__class__ != Failure or ex.args:
1086 ex_args = str(ex)
1087 if ex_args:
1088 print 'EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args)
1089 else:
1090 print 'EXCEPTION:', ex.__class__.__name__
1091 traceback.print_exc(file=sys.stdout)
1092 except KeyboardInterrupt:
1093 print 'Interrupted'
1094 sys.exit(0)
1095 except SystemExit, ex:
1096 print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex.code
1097 print ex.code and 'FAIL: ' or 'PASS: ',
1098 self._print_name()
1099 raise
1100 except:
1101 result = 1
1102 print 'UNEXPECTED EXCEPTION:'
1103 traceback.print_exc(file=sys.stdout)
1105 os.chdir(saved_dir)
1106 result = self.pred.convert_result(result)
1107 (result_text, result_benignity) = self.pred.run_text(result)
1108 if not (quiet_mode and result_benignity):
1109 print result_text,
1110 self._print_name()
1111 sys.stdout.flush()
1112 if sandbox is not None and result != 1 and cleanup_mode:
1113 sandbox.cleanup_test_paths()
1114 return result
1116 ######################################################################
1117 # Main testing functions
1119 # These two functions each take a TEST_LIST as input. The TEST_LIST
1120 # should be a list of test functions; each test function should take
1121 # no arguments and return a 0 on success, non-zero on failure.
1122 # Ideally, each test should also have a short, one-line docstring (so
1123 # it can be displayed by the 'list' command.)
1125 # Func to run one test in the list.
1126 def run_one_test(n, test_list, parallel = 0, finished_tests = None):
1127 """Run the Nth client test in TEST_LIST, return the result.
1129 If we're running the tests in parallel spawn the test in a new process.
1132 if (n < 1) or (n > len(test_list) - 1):
1133 print "There is no test", `n` + ".\n"
1134 return 1
1136 # Run the test.
1137 if parallel:
1138 st = SpawnTest(n, finished_tests)
1139 st.start()
1140 return 0
1141 else:
1142 exit_code = TestRunner(test_list[n], n).run()
1143 return exit_code
1145 def _internal_run_tests(test_list, testnums, parallel):
1146 """Run the tests from TEST_LIST whose indices are listed in TESTNUMS.
1148 If we're running the tests in parallel spawn as much parallel processes
1149 as requested and gather the results in a temp. buffer when a child
1150 process is finished.
1153 exit_code = 0
1154 finished_tests = []
1155 tests_started = 0
1157 if not parallel:
1158 for testnum in testnums:
1159 if run_one_test(testnum, test_list) == 1:
1160 exit_code = 1
1161 else:
1162 number_queue = Queue.Queue()
1163 for num in testnums:
1164 number_queue.put(num)
1166 threads = [ TestSpawningThread(number_queue) for i in range(parallel) ]
1167 for t in threads:
1168 t.start()
1170 for t in threads:
1171 t.join()
1173 # list of (index, result, stdout, stderr)
1174 results = []
1175 for t in threads:
1176 results += t.results
1177 results.sort()
1179 # terminate the line of dots
1180 print
1182 # all tests are finished, find out the result and print the logs.
1183 for (index, result, stdout_lines, stderr_lines) in results:
1184 if stdout_lines:
1185 for line in stdout_lines:
1186 sys.stdout.write(line)
1187 if stderr_lines:
1188 for line in stderr_lines:
1189 sys.stdout.write(line)
1190 if result == 1:
1191 exit_code = 1
1193 _cleanup_deferred_test_paths()
1194 return exit_code
1197 def usage():
1198 prog_name = os.path.basename(sys.argv[0])
1199 print "%s [--url] [--fs-type] [--verbose|--quiet] [--parallel] \\" % \
1200 prog_name
1201 print "%s [--enable-sasl] [--cleanup] [--bin] [<test> ...]" \
1202 % (" " * len(prog_name))
1203 print "%s " % (" " * len(prog_name))
1204 print "%s [--list] [<test> ...]\n" % prog_name
1205 print "Arguments:"
1206 print " test The number of the test to run (multiple okay), " \
1207 "or all tests\n"
1208 print "Options:"
1209 print " --list Print test doc strings instead of running them"
1210 print " --fs-type Subversion file system type (fsfs or bdb)"
1211 print " --http-library DAV library to use (neon or serf)"
1212 print " --url Base url to the repos (e.g. svn://localhost)"
1213 print " --verbose Print binary command-lines (not with --quiet)"
1214 print " --quiet Print only unexpected results (not with --verbose)"
1215 print " --cleanup Whether to clean up"
1216 print " --enable-sasl Whether to enable SASL authentication"
1217 print " --parallel Run the tests in parallel"
1218 print " --bin Use the svn binaries installed in this path"
1219 print " --use-jsvn Use the jsvn (SVNKit based) binaries. Can be\n" \
1220 " combined with --bin to point to a specific path"
1221 print " --development Test development mode: provides more detailed test\n"\
1222 " output and ignores all exceptions in the \n" \
1223 " run_and_verify* functions. This option is only \n" \
1224 " useful during test development!"
1225 print " --server-minor-version Set the minor version for the server.\n" \
1226 " Supports version 4 or 5."
1227 print " --help This information"
1230 # Main func. This is the "entry point" that all the test scripts call
1231 # to run their list of tests.
1233 # This routine parses sys.argv to decide what to do.
1234 def run_tests(test_list, serial_only = False):
1235 """Main routine to run all tests in TEST_LIST.
1237 NOTE: this function does not return. It does a sys.exit() with the
1238 appropriate exit code.
1241 global test_area_url
1242 global pristine_url
1243 global fs_type
1244 global verbose_mode
1245 global quiet_mode
1246 global cleanup_mode
1247 global enable_sasl
1248 global is_child_process
1249 global svn_binary
1250 global svnadmin_binary
1251 global svnlook_binary
1252 global svnsync_binary
1253 global svnversion_binary
1254 global command_line_parsed
1255 global http_library
1256 global server_minor_version
1258 testnums = []
1259 # Should the tests be listed (as opposed to executed)?
1260 list_tests = False
1262 parallel = 0
1263 svn_bin = None
1264 use_jsvn = False
1266 try:
1267 opts, args = my_getopt(sys.argv[1:], 'vqhpc',
1268 ['url=', 'fs-type=', 'verbose', 'quiet', 'cleanup',
1269 'list', 'enable-sasl', 'help', 'parallel',
1270 'bin=', 'http-library=', 'server-minor-version=',
1271 'use-jsvn', 'development'])
1272 except getopt.GetoptError, e:
1273 print "ERROR: %s\n" % e
1274 usage()
1275 sys.exit(1)
1277 for arg in args:
1278 if arg == "list":
1279 # This is an old deprecated variant of the "--list" option:
1280 list_tests = True
1281 elif arg.startswith('BASE_URL='):
1282 test_area_url = arg[9:]
1283 else:
1284 try:
1285 testnums.append(int(arg))
1286 except ValueError:
1287 print "ERROR: invalid test number '%s'\n" % arg
1288 usage()
1289 sys.exit(1)
1291 for opt, val in opts:
1292 if opt == "--url":
1293 test_area_url = val
1295 elif opt == "--fs-type":
1296 fs_type = val
1298 elif opt == "-v" or opt == "--verbose":
1299 verbose_mode = True
1301 elif opt == "-q" or opt == "--quiet":
1302 quiet_mode = True
1304 elif opt == "--cleanup":
1305 cleanup_mode = True
1307 elif opt == "--list":
1308 list_tests = True
1310 elif opt == "--enable-sasl":
1311 enable_sasl = True
1313 elif opt == "-h" or opt == "--help":
1314 usage()
1315 sys.exit(0)
1317 elif opt == '-p' or opt == "--parallel":
1318 parallel = 5 # use 5 parallel threads.
1320 elif opt == '-c':
1321 is_child_process = True
1323 elif opt == '--bin':
1324 svn_bin = val
1326 elif opt == '--http-library':
1327 http_library = val
1329 elif opt == '--server-minor-version':
1330 server_minor_version = int(val)
1331 if server_minor_version < 4 or server_minor_version > 6:
1332 print "ERROR: test harness only supports server minor version 4 or 5"
1333 sys.exit(1)
1335 elif opt == '--use-jsvn':
1336 use_jsvn = True
1338 elif opt == '--development':
1339 setup_development_mode()
1341 if test_area_url[-1:] == '/': # Normalize url to have no trailing slash
1342 test_area_url = test_area_url[:-1]
1344 if verbose_mode and quiet_mode:
1345 sys.stderr.write("ERROR: 'verbose' and 'quiet' are incompatible\n")
1346 sys.exit(1)
1348 # Calculate pristine_url from test_area_url.
1349 pristine_url = test_area_url + '/' + pristine_dir
1350 if windows:
1351 pristine_url = pristine_url.replace('\\', '/')
1353 if use_jsvn:
1354 if svn_bin is None:
1355 svn_bin = ''
1356 svn_binary = os.path.join(svn_bin, 'jsvn' + _bat)
1357 svnadmin_binary = os.path.join(svn_bin, 'jsvnadmin' + _bat)
1358 svnlook_binary = os.path.join(svn_bin, 'jsvnlook' + _bat)
1359 svnsync_binary = os.path.join(svn_bin, 'jsvnsync' + _bat)
1360 svnversion_binary = os.path.join(svn_bin, 'jsvnversion' + _bat)
1361 use_jsvn = False
1362 else:
1363 if svn_bin:
1364 svn_binary = os.path.join(svn_bin, 'svn' + _exe)
1365 svnadmin_binary = os.path.join(svn_bin, 'svnadmin' + _exe)
1366 svnlook_binary = os.path.join(svn_bin, 'svnlook' + _exe)
1367 svnsync_binary = os.path.join(svn_bin, 'svnsync' + _exe)
1368 svnversion_binary = os.path.join(svn_bin, 'svnversion' + _exe)
1370 command_line_parsed = True
1372 ######################################################################
1374 # Cleanup: if a previous run crashed or interrupted the python
1375 # interpreter, then `temp_dir' was never removed. This can cause wonkiness.
1376 if not is_child_process:
1377 safe_rmtree(temp_dir, 1)
1379 if not testnums:
1380 # If no test numbers were listed explicitly, include all of them:
1381 testnums = range(1, len(test_list))
1383 if list_tests:
1384 print "Test # Mode Test Description"
1385 print "------ ----- ----------------"
1386 for testnum in testnums:
1387 TestRunner(test_list[testnum], testnum).list()
1389 # done. just exit with success.
1390 sys.exit(0)
1392 # don't run tests in parallel when the tests don't support it or there
1393 # are only a few tests to run.
1394 if serial_only or len(testnums) < 2:
1395 parallel = 0
1397 # Setup the pristine repository
1398 actions.setup_pristine_repository()
1400 # Build out the default configuration directory
1401 create_config_dir(default_config_dir)
1403 # Run the tests.
1404 exit_code = _internal_run_tests(test_list, testnums, parallel)
1406 # Remove all scratchwork: the 'pristine' repository, greek tree, etc.
1407 # This ensures that an 'import' will happen the next time we run.
1408 if not is_child_process:
1409 safe_rmtree(temp_dir, 1)
1411 # Cleanup after ourselves.
1412 _cleanup_deferred_test_paths()
1414 # Return the appropriate exit code from the tests.
1415 sys.exit(exit_code)
1417 # the modules import each other, so we do this import very late, to ensure
1418 # that the definitions in "main" have been completed.
1419 import actions
1422 ### End of file.