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[]
20 import shutil
# for rmtree()
22 import stat
# for ST_MODE
23 import copy
# for deepcopy()
24 import time
# for time()
25 import traceback
# for print_exc()
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 #####################################################################
63 class SVNProcessTerminatedBySignal(Failure
):
64 "Exception raised if a spawned process segfaulted, aborted, etc."
67 class SVNLineUnequal(Failure
):
68 "Exception raised if two lines are unequal"
71 class SVNUnmatchedError(Failure
):
72 "Exception raised if an expected error is not found"
75 class SVNCommitFailure(Failure
):
76 "Exception raised if a commit failed"
79 class SVNRepositoryCopyFailure(Failure
):
80 "Exception raised if unable to copy a repository"
83 class SVNRepositoryCreateFailure(Failure
):
84 "Exception raised if unable to create a repository"
88 if sys
.platform
== 'win32':
90 file_scheme_prefix
= 'file:///'
95 file_scheme_prefix
= 'file://'
100 from popen2
import Popen3
101 platform_with_popen3_class
= True
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')
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"
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' + \
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.
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.
146 # Global variable indicating if we want test data cleaned up after success
149 # Global variable indicating if svnserve should use Cyrus SASL
152 # Global variable indicating which DAV library, if any, is in use
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())
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.
173 # Global variable indicating the FS type for repository creations.
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.
209 greek_state
= wc
.State('', {
210 'iota' : _item("This is the file 'iota'.\n"),
212 'A/mu' : _item("This is the file 'mu'.\n"),
214 'A/B/lambda' : _item("This is the file 'lambda'.\n"),
216 'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
217 'A/B/E/beta' : _item("This is the file 'beta'.\n"),
221 'A/D/gamma' : _item("This is the file 'gamma'.\n"),
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"),
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
236 "Wrap a function, catch, print and ignore exceptions"
237 def w(*args
, **kwds
):
239 return func(*args
, **kwds
)
241 if ex
.__class
__ != Failure
or ex
.args
:
244 print 'EXCEPTION: %s: %s' % (ex
.__class
__.__name
__, ex_args
)
246 print 'EXCEPTION:', ex
.__class
__.__name
__
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']
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'):
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
311 If ERROR_EXPECTED is None, any stderr also will be printed."""
313 return run_command_stdin(command
, error_expected
, binary_mode
,
316 # A regular expression that matches arguments that are trivially safe
317 # to pass on a command line without quoting on any supported operating
319 _safe_arg_re
= re
.compile(r
'^[A-Za-z\d\.\_\/\-\:\@]+$')
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
335 if _safe_arg_re
.match(arg
):
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
)
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."""
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
))
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
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
))
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
,
395 infile
, outfile
, errfile
, kid
= open_pipe(command
+ ' ' + args
, mode
)
398 map(infile
.write
, stdin_lines
)
402 stdout_lines
= outfile
.readlines()
403 stderr_lines
= errfile
.readlines()
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."""
425 exit_code
, stdout_lines
, stderr_lines
= spawn_process(command
,
432 print '<TIME = %.6f>' % (stop
- start
)
434 if (not error_expected
) and (stderr_lines
):
435 map(sys
.stdout
.write
, stderr_lines
)
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"
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
):
451 # define default config file contents if none provided
452 if config_contents
is None:
453 config_contents
= """
456 interactive-conflicts = false
459 # define default server file contents if none provided
460 if server_contents
is None:
462 server_contents
= """
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
:
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
,
483 if '--username' in args
:
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
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
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
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
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
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
):
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"
535 chmod_tree(dirname
, 0666, 0666)
536 shutil
.rmtree(dirname
)
538 if not os
.path
.exists(dirname
):
542 for delay
in (0.5, 1, 2, 4):
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
)
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
)
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"""
586 fcontent
= fcontent
.replace(contents
, new_contents
)
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
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",
607 # Skip tests if we can't create the repository.
610 if line
.find('Unknown FS type') != -1:
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");
620 file_append(get_svnserve_conf_file_path(path
),
621 "realm = svntest\n[sasl]\nuse-sasl = true\n")
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
+ '"'
640 load_args
= load_args
+ " --ignore-uuid"
642 print 'CMD:', os
.path
.basename(svnadmin_binary
) + dump_args
, \
643 '|', os
.path
.basename(svnadmin_binary
) + load_args
,
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')
652 print '<TIME = %.6f>' % (stop
- start
)
655 data
= dump_out
.read(1024*1024) # Arbitrary buffer size
659 load_in
.close() # Tell load we are done
661 dump_lines
= dump_err
.readlines()
662 load_lines
= load_out
.readlines()
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?$')
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
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?$')
686 for load_line
in load_lines
:
687 match
= load_re
.match(load_line
)
689 if match
.group(1) != str(expect_revision
):
690 print 'ERROR: load failed:', load_line
,
691 raise SVNRepositoryCopyFailure
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)
704 return scheme
+ re
.sub(r
'//*', '/', input[len(scheme
):])
709 def create_python_hook_script (hook_path
, hook_script_code
):
710 """Create a Python hook script at HOOK_PATH with the specified
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
))
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
)
733 fp
.write("realm = svntest\n[sasl]\nuse-sasl = true\n");
735 fp
.write("password-db = passwd\n")
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
+ ":"
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
))
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,
781 """Return an expected output line that describes the beginning of a
782 merge operation on revisions REVSTART through REVEND. Omit both
783 REVSTART and REVEND for the case where the left and right sides of
784 the merge are from different URLs."""
785 from_foreign_phrase
= foreign
and "\(from foreign repository\) " or ""
787 return "--- Merging differences between %srepository URLs into '.+':\n" \
788 % (foreign
and "foreign " or "")
791 # The left and right sides of the merge are from different URLs.
792 return "--- Merging differences between %srepository URLs into '.+':\n" \
793 % (foreign
and "foreign " or "")
795 return "--- Reverse-merging %sr%ld into '.+':\n" \
796 % (from_foreign_phrase
, abs(revstart
))
798 return "--- Merging %sr%ld into '.+':\n" \
799 % (from_foreign_phrase
, revstart
)
801 if revstart
> revend
:
802 return "--- Reverse-merging %sr%ld through r%ld into '.+':\n" \
803 % (from_foreign_phrase
, revstart
, revend
)
805 return "--- Merging %sr%ld through r%ld into '.+':\n" \
806 % (from_foreign_phrase
, revstart
, revend
)
809 ######################################################################
810 # Functions which check the test configuration
811 # (useful for conditional XFails)
813 def _check_command_line_parsed():
814 """Raise an exception if the command line has not yet been parsed."""
815 if not command_line_parsed
:
816 raise Failure("Condition cannot be tested until command line is parsed")
818 def is_ra_type_dav():
819 _check_command_line_parsed()
820 return test_area_url
.startswith('http')
822 def is_ra_type_svn():
823 _check_command_line_parsed()
824 return test_area_url
.startswith('svn')
826 def is_ra_type_file():
827 _check_command_line_parsed()
828 return test_area_url
.startswith('file')
830 def is_fs_type_fsfs():
831 _check_command_line_parsed()
832 # This assumes that fsfs is the default fs implementation.
833 return fs_type
== 'fsfs' or fs_type
is None
836 return os
.name
== 'nt'
839 return os
.name
== 'posix'
842 return sys
.platform
== 'darwin'
844 def server_has_mergeinfo():
845 _check_command_line_parsed()
846 return server_minor_version
>= 5
848 def server_has_revprop_commit():
849 _check_command_line_parsed()
850 return server_minor_version
>= 5
852 def server_sends_copyfrom_on_update():
853 _check_command_line_parsed()
854 return server_minor_version
>= 5
856 def server_authz_has_aliases():
857 _check_command_line_parsed()
858 return server_minor_version
>= 5
860 def server_gets_client_capabilities():
861 _check_command_line_parsed()
862 return server_minor_version
>= 5
864 def server_has_partial_replay():
865 _check_command_line_parsed()
866 return server_minor_version
>= 5
869 ######################################################################
873 """Manages a sandbox (one or more repository/working copy pairs) for
874 a test to operate within."""
878 def __init__(self
, module
, idx
):
879 self
._set
_name
("%s-%d" % (module
, idx
))
881 def _set_name(self
, name
, read_only
= False):
882 """A convenience method for renaming a sandbox, useful when
883 working with multiple repositories in the same unit test."""
886 self
.read_only
= read_only
887 self
.wc_dir
= os
.path
.join(general_wc_dir
, self
.name
)
889 self
.repo_dir
= os
.path
.join(general_repo_dir
, self
.name
)
890 self
.repo_url
= test_area_url
+ '/' + self
.repo_dir
892 self
.repo_dir
= pristine_dir
893 self
.repo_url
= pristine_url
895 ### TODO: Move this into to the build() method
896 # For dav tests we need a single authz file which must be present,
897 # so we recreate it each time a sandbox is created with some default
899 if self
.repo_url
.startswith("http"):
900 # this dir doesn't exist out of the box, so we may have to make it
901 if not os
.path
.exists(work_dir
):
902 os
.makedirs(work_dir
)
903 self
.authz_file
= os
.path
.join(work_dir
, "authz")
904 file_write(self
.authz_file
, "[/]\n* = rw\n")
906 # For svnserve tests we have a per-repository authz file, and it
907 # doesn't need to be there in order for things to work, so we don't
908 # have any default contents.
909 elif self
.repo_url
.startswith("svn"):
910 self
.authz_file
= os
.path
.join(self
.repo_dir
, "conf", "authz")
913 self
.repo_url
= self
.repo_url
.replace('\\', '/')
914 self
.test_paths
= [self
.wc_dir
, self
.repo_dir
]
916 def clone_dependent(self
, copy_wc
=False):
917 """A convenience method for creating a near-duplicate of this
918 sandbox, useful when working with multiple repositories in the
919 same unit test. If COPY_WC is true, make an exact copy of this
920 sandbox's working copy at the new sandbox's working copy
921 directory. Any necessary cleanup operations are triggered by
922 cleanup of the original sandbox."""
924 if not self
.dependents
:
926 clone
= copy
.deepcopy(self
)
927 self
.dependents
.append(clone
)
928 clone
._set
_name
("%s-%d" % (self
.name
, len(self
.dependents
)))
930 self
.add_test_path(clone
.wc_dir
)
931 shutil
.copytree(self
.wc_dir
, clone
.wc_dir
, symlinks
=True)
934 def build(self
, name
= None, create_wc
= True, read_only
= False):
935 self
._set
_name
(name
, read_only
)
936 if actions
.make_repo_and_wc(self
, create_wc
, read_only
):
937 raise Failure("Could not build repository and sandbox '%s'" % self
.name
)
939 def add_test_path(self
, path
, remove
=True):
940 self
.test_paths
.append(path
)
944 def add_repo_path(self
, suffix
, remove
=1):
945 path
= os
.path
.join(general_repo_dir
, self
.name
) + '.' + suffix
946 url
= test_area_url
+ '/' + path
948 url
= url
.replace('\\', '/')
949 self
.add_test_path(path
, remove
)
952 def add_wc_path(self
, suffix
, remove
=1):
953 path
= self
.wc_dir
+ '.' + suffix
954 self
.add_test_path(path
, remove
)
957 def cleanup_test_paths(self
):
958 "Clean up detritus from this sandbox, and any dependents."
960 # Recursively cleanup any dependent sandboxes.
961 for sbox
in self
.dependents
:
962 sbox
.cleanup_test_paths()
963 # cleanup all test specific working copies and repositories
964 for path
in self
.test_paths
:
965 if not path
is pristine_dir
:
966 _cleanup_test_path(path
)
969 _deferred_test_paths
= []
970 def _cleanup_deferred_test_paths():
971 global _deferred_test_paths
972 test_paths
= _deferred_test_paths
[:]
973 _deferred_test_paths
= []
974 for path
in test_paths
:
975 _cleanup_test_path(path
, 1)
977 def _cleanup_test_path(path
, retrying
=None):
980 print "CLEANUP: RETRY:", path
982 print "CLEANUP:", path
987 print "WARNING: cleanup failed, will try again later"
988 _deferred_test_paths
.append(path
)
990 class TestSpawningThread(threading
.Thread
):
991 """A thread that runs test cases in their own processes.
992 Receives test numbers to run from the queue, and saves results into
993 the results field."""
994 def __init__(self
, queue
):
995 threading
.Thread
.__init
__(self
)
1002 next_index
= self
.queue
.get_nowait()
1006 self
.run_one(next_index
)
1008 def run_one(self
, index
):
1009 command
= sys
.argv
[0]
1012 args
.append(str(index
))
1014 # add some startup arguments from this process
1016 args
.append('--fs-type=' + fs_type
)
1018 args
.append('--url=' + test_area_url
)
1022 args
.append('--cleanup')
1024 args
.append('--enable-sasl')
1026 args
.append('--http-library=' + http_library
)
1027 if server_minor_version
:
1028 args
.append('--server-minor-version=' + str(server_minor_version
))
1030 result
, stdout_lines
, stderr_lines
= spawn_process(command
, 1, None, *args
)
1031 # "result" will be None on platforms without Popen3 (e.g. Windows)
1032 if filter(lambda x
: x
.startswith('FAIL: ') or x
.startswith('XPASS: '),
1035 self
.results
.append((index
, result
, stdout_lines
, stderr_lines
))
1036 sys
.stdout
.write('.')
1040 """Encapsulate a single test case (predicate), including logic for
1041 runing the test and test list output."""
1043 def __init__(self
, func
, index
):
1044 self
.pred
= testcase
.create_test_case(func
)
1048 print " %2d %-5s %s" % (self
.index
,
1049 self
.pred
.list_mode(),
1050 self
.pred
.get_description())
1051 self
.pred
.check_description()
1053 def _print_name(self
):
1054 print os
.path
.basename(sys
.argv
[0]), str(self
.index
) + ":", \
1055 self
.pred
.get_description()
1056 self
.pred
.check_description()
1059 """Run self.pred and return the result. The return value is
1060 - 0 if the test was successful
1061 - 1 if it errored in a way that indicates test failure
1062 - 2 if the test skipped
1064 if self
.pred
.need_sandbox():
1065 # ooh! this function takes a sandbox argument
1066 sandbox
= Sandbox(self
.pred
.get_sandbox_name(), self
.index
)
1067 kw
= { 'sandbox' : sandbox
}
1072 # Explicitly set this so that commands that commit but don't supply a
1073 # log message will fail rather than invoke an editor.
1074 # Tests that want to use an editor should invoke svntest.main.use_editor.
1075 os
.environ
['SVN_EDITOR'] = ''
1076 os
.environ
['SVNTEST_EDITOR_FUNC'] = ''
1077 actions
.no_sleep_for_timestamps()
1079 saved_dir
= os
.getcwd()
1081 rc
= apply(self
.pred
.run
, (), kw
)
1083 print 'STYLE ERROR in',
1085 print 'Test driver returned a status code.'
1092 # We captured Failure and its subclasses. We don't want to print
1093 # anything for plain old Failure since that just indicates test
1094 # failure, rather than relevant information. However, if there
1095 # *is* information in the exception's arguments, then print it.
1096 if ex
.__class
__ != Failure
or ex
.args
:
1099 print 'EXCEPTION: %s: %s' % (ex
.__class
__.__name
__, ex_args
)
1101 print 'EXCEPTION:', ex
.__class
__.__name
__
1102 traceback
.print_exc(file=sys
.stdout
)
1103 except KeyboardInterrupt:
1106 except SystemExit, ex
:
1107 print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex
.code
1108 print ex
.code
and 'FAIL: ' or 'PASS: ',
1113 print 'UNEXPECTED EXCEPTION:'
1114 traceback
.print_exc(file=sys
.stdout
)
1117 result
= self
.pred
.convert_result(result
)
1118 (result_text
, result_benignity
) = self
.pred
.run_text(result
)
1119 if not (quiet_mode
and result_benignity
):
1123 if sandbox
is not None and result
!= 1 and cleanup_mode
:
1124 sandbox
.cleanup_test_paths()
1127 ######################################################################
1128 # Main testing functions
1130 # These two functions each take a TEST_LIST as input. The TEST_LIST
1131 # should be a list of test functions; each test function should take
1132 # no arguments and return a 0 on success, non-zero on failure.
1133 # Ideally, each test should also have a short, one-line docstring (so
1134 # it can be displayed by the 'list' command.)
1136 # Func to run one test in the list.
1137 def run_one_test(n
, test_list
, parallel
= 0, finished_tests
= None):
1138 """Run the Nth client test in TEST_LIST, return the result.
1140 If we're running the tests in parallel spawn the test in a new process.
1143 if (n
< 1) or (n
> len(test_list
) - 1):
1144 print "There is no test", `n`
+ ".\n"
1149 st
= SpawnTest(n
, finished_tests
)
1153 exit_code
= TestRunner(test_list
[n
], n
).run()
1156 def _internal_run_tests(test_list
, testnums
, parallel
):
1157 """Run the tests from TEST_LIST whose indices are listed in TESTNUMS.
1159 If we're running the tests in parallel spawn as much parallel processes
1160 as requested and gather the results in a temp. buffer when a child
1161 process is finished.
1169 for testnum
in testnums
:
1170 if run_one_test(testnum
, test_list
) == 1:
1173 number_queue
= Queue
.Queue()
1174 for num
in testnums
:
1175 number_queue
.put(num
)
1177 threads
= [ TestSpawningThread(number_queue
) for i
in range(parallel
) ]
1184 # list of (index, result, stdout, stderr)
1187 results
+= t
.results
1190 # terminate the line of dots
1193 # all tests are finished, find out the result and print the logs.
1194 for (index
, result
, stdout_lines
, stderr_lines
) in results
:
1196 for line
in stdout_lines
:
1197 sys
.stdout
.write(line
)
1199 for line
in stderr_lines
:
1200 sys
.stdout
.write(line
)
1204 _cleanup_deferred_test_paths()
1209 prog_name
= os
.path
.basename(sys
.argv
[0])
1210 print "%s [--url] [--fs-type] [--verbose|--quiet] [--parallel] \\" % \
1212 print "%s [--enable-sasl] [--cleanup] [--bin] [<test> ...]" \
1213 % (" " * len(prog_name
))
1214 print "%s " % (" " * len(prog_name
))
1215 print "%s [--list] [<test> ...]\n" % prog_name
1217 print " test The number of the test to run (multiple okay), " \
1220 print " --list Print test doc strings instead of running them"
1221 print " --fs-type Subversion file system type (fsfs or bdb)"
1222 print " --http-library DAV library to use (neon or serf)"
1223 print " --url Base url to the repos (e.g. svn://localhost)"
1224 print " --verbose Print binary command-lines (not with --quiet)"
1225 print " --quiet Print only unexpected results (not with --verbose)"
1226 print " --cleanup Whether to clean up"
1227 print " --enable-sasl Whether to enable SASL authentication"
1228 print " --parallel Run the tests in parallel"
1229 print " --bin Use the svn binaries installed in this path"
1230 print " --use-jsvn Use the jsvn (SVNKit based) binaries. Can be\n" \
1231 " combined with --bin to point to a specific path"
1232 print " --development Test development mode: provides more detailed test\n"\
1233 " output and ignores all exceptions in the \n" \
1234 " run_and_verify* functions. This option is only \n" \
1235 " useful during test development!"
1236 print " --server-minor-version Set the minor version for the server.\n" \
1237 " Supports version 4 or 5."
1238 print " --help This information"
1241 # Main func. This is the "entry point" that all the test scripts call
1242 # to run their list of tests.
1244 # This routine parses sys.argv to decide what to do.
1245 def run_tests(test_list
, serial_only
= False):
1246 """Main routine to run all tests in TEST_LIST.
1248 NOTE: this function does not return. It does a sys.exit() with the
1249 appropriate exit code.
1252 global test_area_url
1259 global is_child_process
1261 global svnadmin_binary
1262 global svnlook_binary
1263 global svnsync_binary
1264 global svnversion_binary
1265 global command_line_parsed
1267 global server_minor_version
1270 # Should the tests be listed (as opposed to executed)?
1278 opts
, args
= my_getopt(sys
.argv
[1:], 'vqhpc',
1279 ['url=', 'fs-type=', 'verbose', 'quiet', 'cleanup',
1280 'list', 'enable-sasl', 'help', 'parallel',
1281 'bin=', 'http-library=', 'server-minor-version=',
1282 'use-jsvn', 'development'])
1283 except getopt
.GetoptError
, e
:
1284 print "ERROR: %s\n" % e
1290 # This is an old deprecated variant of the "--list" option:
1292 elif arg
.startswith('BASE_URL='):
1293 test_area_url
= arg
[9:]
1296 testnums
.append(int(arg
))
1298 print "ERROR: invalid test number '%s'\n" % arg
1302 for opt
, val
in opts
:
1306 elif opt
== "--fs-type":
1309 elif opt
== "-v" or opt
== "--verbose":
1312 elif opt
== "-q" or opt
== "--quiet":
1315 elif opt
== "--cleanup":
1318 elif opt
== "--list":
1321 elif opt
== "--enable-sasl":
1324 elif opt
== "-h" or opt
== "--help":
1328 elif opt
== '-p' or opt
== "--parallel":
1329 parallel
= 5 # use 5 parallel threads.
1332 is_child_process
= True
1334 elif opt
== '--bin':
1337 elif opt
== '--http-library':
1340 elif opt
== '--server-minor-version':
1341 server_minor_version
= int(val
)
1342 if server_minor_version
< 4 or server_minor_version
> 6:
1343 print "ERROR: test harness only supports server minor version 4 or 5"
1346 elif opt
== '--use-jsvn':
1349 elif opt
== '--development':
1350 setup_development_mode()
1352 if test_area_url
[-1:] == '/': # Normalize url to have no trailing slash
1353 test_area_url
= test_area_url
[:-1]
1355 if verbose_mode
and quiet_mode
:
1356 sys
.stderr
.write("ERROR: 'verbose' and 'quiet' are incompatible\n")
1359 # Calculate pristine_url from test_area_url.
1360 pristine_url
= test_area_url
+ '/' + pristine_dir
1362 pristine_url
= pristine_url
.replace('\\', '/')
1367 svn_binary
= os
.path
.join(svn_bin
, 'jsvn' + _bat
)
1368 svnadmin_binary
= os
.path
.join(svn_bin
, 'jsvnadmin' + _bat
)
1369 svnlook_binary
= os
.path
.join(svn_bin
, 'jsvnlook' + _bat
)
1370 svnsync_binary
= os
.path
.join(svn_bin
, 'jsvnsync' + _bat
)
1371 svnversion_binary
= os
.path
.join(svn_bin
, 'jsvnversion' + _bat
)
1375 svn_binary
= os
.path
.join(svn_bin
, 'svn' + _exe
)
1376 svnadmin_binary
= os
.path
.join(svn_bin
, 'svnadmin' + _exe
)
1377 svnlook_binary
= os
.path
.join(svn_bin
, 'svnlook' + _exe
)
1378 svnsync_binary
= os
.path
.join(svn_bin
, 'svnsync' + _exe
)
1379 svnversion_binary
= os
.path
.join(svn_bin
, 'svnversion' + _exe
)
1381 command_line_parsed
= True
1383 ######################################################################
1385 # Cleanup: if a previous run crashed or interrupted the python
1386 # interpreter, then `temp_dir' was never removed. This can cause wonkiness.
1387 if not is_child_process
:
1388 safe_rmtree(temp_dir
, 1)
1391 # If no test numbers were listed explicitly, include all of them:
1392 testnums
= range(1, len(test_list
))
1395 print "Test # Mode Test Description"
1396 print "------ ----- ----------------"
1397 for testnum
in testnums
:
1398 TestRunner(test_list
[testnum
], testnum
).list()
1400 # done. just exit with success.
1403 # don't run tests in parallel when the tests don't support it or there
1404 # are only a few tests to run.
1405 if serial_only
or len(testnums
) < 2:
1408 # Setup the pristine repository
1409 actions
.setup_pristine_repository()
1411 # Build out the default configuration directory
1412 create_config_dir(default_config_dir
)
1415 exit_code
= _internal_run_tests(test_list
, testnums
, parallel
)
1417 # Remove all scratchwork: the 'pristine' repository, greek tree, etc.
1418 # This ensures that an 'import' will happen the next time we run.
1419 if not is_child_process
:
1420 safe_rmtree(temp_dir
, 1)
1422 # Cleanup after ourselves.
1423 _cleanup_deferred_test_paths()
1425 # Return the appropriate exit code from the tests.
1428 # the modules import each other, so we do this import very late, to ensure
1429 # that the definitions in "main" have been completed.