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-2008 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 # Set C locale for command line programs
120 os
.environ
['LC_ALL'] = 'C'
122 ######################################################################
123 # Global variables set during option parsing. These should not be used
124 # until the variable command_line_parsed has been set to True, as is
125 # done in run_tests below.
126 command_line_parsed
= False
128 # The locations of the svn, svnadmin and svnlook binaries, relative to
129 # the only scripts that import this file right now (they live in ../).
130 # Use --bin to override these defaults.
131 svn_binary
= os
.path
.abspath('../../svn/svn' + _exe
)
132 svnadmin_binary
= os
.path
.abspath('../../svnadmin/svnadmin' + _exe
)
133 svnlook_binary
= os
.path
.abspath('../../svnlook/svnlook' + _exe
)
134 svnsync_binary
= os
.path
.abspath('../../svnsync/svnsync' + _exe
)
135 svnversion_binary
= os
.path
.abspath('../../svnversion/svnversion' + _exe
)
136 svndumpfilter_binary
= os
.path
.abspath('../../svndumpfilter/svndumpfilter' + \
139 # Global variable indicating if we want verbose output, that is,
140 # details of what commands each test does as it does them. This is
141 # incompatible with quiet_mode.
144 # Global variable indicating if we want quiet output, that is, don't
145 # show PASS, XFAIL, or SKIP notices, but do show FAIL and XPASS. This
146 # is incompatible with verbose_mode.
149 # Global variable indicating if we want test data cleaned up after success
152 # Global variable indicating if svnserve should use Cyrus SASL
155 # Global variable indicating that SVNKit binaries should be used
158 # Global variable indicating which DAV library, if any, is in use
162 # Configuration file (copied into FSFS fsfs.conf).
165 # Global variable indicating what the minor version of the server
166 # tested against is (4 for 1.4.x, for example).
167 server_minor_version
= 5
169 # Global variable indicating if this is a child process and no cleanup
170 # of global directories is needed.
171 is_child_process
= False
173 # Global URL to testing area. Default to ra_local, current working dir.
174 test_area_url
= file_scheme_prefix
+ os
.path
.abspath(os
.getcwd())
176 test_area_url
= test_area_url
.replace('\\', '/')
178 # Location to the pristine repository, will be calculated from test_area_url
179 # when we know what the user specified for --url.
182 # Global variable indicating the FS type for repository creations.
185 # End of command-line-set global variables.
186 ######################################################################
188 # All temporary repositories and working copies are created underneath
189 # this dir, so there's one point at which to mount, e.g., a ramdisk.
190 work_dir
= "svn-test-work"
192 # Constant for the merge info property.
193 SVN_PROP_MERGEINFO
= "svn:mergeinfo"
195 # Where we want all the repositories and working copies to live.
196 # Each test will have its own!
197 general_repo_dir
= os
.path
.join(work_dir
, "repositories")
198 general_wc_dir
= os
.path
.join(work_dir
, "working_copies")
200 # temp directory in which we will create our 'pristine' local
201 # repository and other scratch data. This should be removed when we
202 # quit and when we startup.
203 temp_dir
= os
.path
.join(work_dir
, 'local_tmp')
205 # (derivatives of the tmp dir.)
206 pristine_dir
= os
.path
.join(temp_dir
, "repos")
207 greek_dump_dir
= os
.path
.join(temp_dir
, "greekfiles")
208 default_config_dir
= os
.path
.abspath(os
.path
.join(temp_dir
, "config"))
211 # Our pristine greek-tree state.
213 # If a test wishes to create an "expected" working-copy tree, it should
214 # call main.greek_state.copy(). That method will return a copy of this
215 # State object which can then be edited.
218 greek_state
= wc
.State('', {
219 'iota' : _item("This is the file 'iota'.\n"),
221 'A/mu' : _item("This is the file 'mu'.\n"),
223 'A/B/lambda' : _item("This is the file 'lambda'.\n"),
225 'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
226 'A/B/E/beta' : _item("This is the file 'beta'.\n"),
230 'A/D/gamma' : _item("This is the file 'gamma'.\n"),
232 'A/D/G/pi' : _item("This is the file 'pi'.\n"),
233 'A/D/G/rho' : _item("This is the file 'rho'.\n"),
234 'A/D/G/tau' : _item("This is the file 'tau'.\n"),
236 'A/D/H/chi' : _item("This is the file 'chi'.\n"),
237 'A/D/H/psi' : _item("This is the file 'psi'.\n"),
238 'A/D/H/omega' : _item("This is the file 'omega'.\n"),
242 ######################################################################
243 # Utilities shared by the tests
245 "Wrap a function, catch, print and ignore exceptions"
246 def w(*args
, **kwds
):
248 return func(*args
, **kwds
)
250 if ex
.__class
__ != Failure
or ex
.args
:
253 print 'EXCEPTION: %s: %s' % (ex
.__class
__.__name
__, ex_args
)
255 print 'EXCEPTION:', ex
.__class
__.__name
__
258 def setup_development_mode():
259 "Wraps functions in module actions"
260 l
= [ 'run_and_verify_svn',
261 'run_and_verify_svnversion',
262 'run_and_verify_load',
263 'run_and_verify_dump',
264 'run_and_verify_checkout',
265 'run_and_verify_export',
266 'run_and_verify_update',
267 'run_and_verify_merge',
268 'run_and_verify_merge2',
269 'run_and_verify_switch',
270 'run_and_verify_commit',
271 'run_and_verify_unquiet_status',
272 'run_and_verify_status',
273 'run_and_verify_diff_summarize',
274 'run_and_verify_diff_summarize_xml',
275 'run_and_validate_lock']
278 setattr(actions
, func
, wrap_ex(getattr(actions
, func
)))
280 def get_admin_name():
281 "Return name of SVN administrative subdirectory."
283 if (windows
or sys
.platform
== 'cygwin') \
284 and os
.environ
.has_key('SVN_ASP_DOT_NET_HACK'):
289 def get_start_commit_hook_path(repo_dir
):
290 "Return the path of the start-commit-hook conf file in REPO_DIR."
292 return os
.path
.join(repo_dir
, "hooks", "start-commit")
295 def get_pre_commit_hook_path(repo_dir
):
296 "Return the path of the pre-commit-hook conf file in REPO_DIR."
298 return os
.path
.join(repo_dir
, "hooks", "pre-commit")
301 def get_post_commit_hook_path(repo_dir
):
302 "Return the path of the post-commit-hook conf file in REPO_DIR."
304 return os
.path
.join(repo_dir
, "hooks", "post-commit")
306 def get_pre_revprop_change_hook_path(repo_dir
):
307 "Return the path of the pre-revprop-change hook script in REPO_DIR."
309 return os
.path
.join(repo_dir
, "hooks", "pre-revprop-change")
311 def get_svnserve_conf_file_path(repo_dir
):
312 "Return the path of the svnserve.conf file in REPO_DIR."
314 return os
.path
.join(repo_dir
, "conf", "svnserve.conf")
316 def get_fsfs_conf_file_path(repo_dir
):
317 "Return the path of the fsfs.conf file in REPO_DIR."
319 return os
.path
.join(repo_dir
, "db", "fsfs.conf")
321 # Run any binary, logging the command line and return code
322 def run_command(command
, error_expected
, binary_mode
=0, *varargs
):
323 """Run COMMAND with VARARGS; return exit code as int; stdout, stderr
325 If ERROR_EXPECTED is None, any stderr also will be printed."""
327 return run_command_stdin(command
, error_expected
, binary_mode
,
330 # A regular expression that matches arguments that are trivially safe
331 # to pass on a command line without quoting on any supported operating
333 _safe_arg_re
= re
.compile(r
'^[A-Za-z\d\.\_\/\-\:\@]+$')
336 """Quote ARG for a command line.
338 Simply surround every argument in double-quotes unless it contains
339 only universally harmless characters.
341 WARNING: This function cannot handle arbitrary command-line
342 arguments. It can easily be confused by shell metacharacters. A
343 perfect job would be difficult and OS-dependent (see, for example,
344 http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp).
345 In other words, this function is just good enough for what we need
349 if _safe_arg_re
.match(arg
):
353 arg
= arg
.replace('$', '\$')
354 return '"%s"' % (arg
,)
356 def open_pipe(command
, mode
):
357 """Opens a popen3 pipe to COMMAND in MODE.
359 Returns (infile, outfile, errfile, waiter); waiter
360 should be passed to wait_on_pipe."""
361 if platform_with_popen3_class
:
362 kid
= Popen3(command
, True)
363 return kid
.tochild
, kid
.fromchild
, kid
.childerr
, (kid
, command
)
365 inf
, outf
, errf
= os
.popen3(command
, mode
)
366 return inf
, outf
, errf
, None
368 def wait_on_pipe(waiter
, stdout_lines
, stderr_lines
):
369 """Waits for KID (opened with open_pipe) to finish, dying
370 if it does. Uses STDOUT_LINES and STDERR_LINES for error message
371 if kid fails. Returns kid's exit code."""
375 kid
, command
= waiter
377 wait_code
= kid
.wait()
379 if os
.WIFSIGNALED(wait_code
):
380 exit_signal
= os
.WTERMSIG(wait_code
)
381 if stdout_lines
is not None:
382 sys
.stdout
.write("".join(stdout_lines
))
383 if stderr_lines
is not None:
384 sys
.stderr
.write("".join(stderr_lines
))
386 # show the whole path to make it easier to start a debugger
387 sys
.stderr
.write("CMD: %s terminated by signal %d\n"
388 % (command
, exit_signal
))
389 raise SVNProcessTerminatedBySignal
391 exit_code
= os
.WEXITSTATUS(wait_code
)
392 if exit_code
and verbose_mode
:
393 sys
.stderr
.write("CMD: %s exited with %d\n" % (command
, exit_code
))
396 # Run any binary, supplying input text, logging the command line
397 def spawn_process(command
, binary_mode
=0,stdin_lines
=None, *varargs
):
398 args
= ' '.join(map(_quote_arg
, varargs
))
400 # Log the command line
401 if verbose_mode
and not command
.endswith('.py'):
402 print 'CMD:', os
.path
.basename(command
) + ' ' + args
,
409 infile
, outfile
, errfile
, kid
= open_pipe(command
+ ' ' + args
, mode
)
412 map(infile
.write
, stdin_lines
)
416 stdout_lines
= outfile
.readlines()
417 stderr_lines
= errfile
.readlines()
422 exit_code
= wait_on_pipe(kid
, stdout_lines
, stderr_lines
)
424 return exit_code
, stdout_lines
, stderr_lines
426 def run_command_stdin(command
, error_expected
, binary_mode
=0,
427 stdin_lines
=None, *varargs
):
428 """Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
429 which should include newline characters) to program via stdin - this
430 should not be very large, as if the program outputs more than the OS
431 is willing to buffer, this will deadlock, with both Python and
432 COMMAND waiting to write to each other for ever.
433 Return exit code as int; stdout, stderr as lists of lines.
434 If ERROR_EXPECTED is None, any stderr also will be printed."""
439 exit_code
, stdout_lines
, stderr_lines
= spawn_process(command
,
446 print '<TIME = %.6f>' % (stop
- start
)
447 map(sys
.stdout
.write
, stdout_lines
)
448 map(sys
.stdout
.write
, stderr_lines
)
450 if (not error_expected
) and (stderr_lines
):
452 map(sys
.stdout
.write
, stderr_lines
)
455 return exit_code
, stdout_lines
, stderr_lines
457 def create_config_dir(cfgdir
, config_contents
=None, server_contents
=None):
458 "Create config directories and files"
461 cfgfile_cfg
= os
.path
.join(cfgdir
, 'config')
462 cfgfile_srv
= os
.path
.join(cfgdir
, 'servers')
464 # create the directory
465 if not os
.path
.isdir(cfgdir
):
468 # define default config file contents if none provided
469 if config_contents
is None:
470 config_contents
= """
473 interactive-conflicts = false
476 # define default server file contents if none provided
477 if server_contents
is None:
478 http_library_str
= ""
480 http_library_str
= "http-library=%s" % (http_library
)
481 server_contents
= """
485 store-plaintext-passwords=yes
487 """ % (http_library_str
)
489 file_write(cfgfile_cfg
, config_contents
)
490 file_write(cfgfile_srv
, server_contents
)
492 def _with_config_dir(args
):
493 if '--config-dir' in args
:
496 return args
+ ('--config-dir', default_config_dir
)
498 def _with_auth(args
):
499 assert '--password' not in args
500 args
= args
+ ('--password', wc_passwd
,
502 if '--username' in args
:
505 return args
+ ('--username', wc_author
)
507 # For running subversion and returning the output
508 def run_svn(error_expected
, *varargs
):
509 """Run svn with VARARGS; return exit code as int; stdout, stderr as
511 If ERROR_EXPECTED is None, any stderr also will be printed. If
512 you're just checking that something does/doesn't come out of
513 stdout/stderr, you might want to use actions.run_and_verify_svn()."""
514 return run_command(svn_binary
, error_expected
, 0,
515 *(_with_auth(_with_config_dir(varargs
))))
517 # For running svnadmin. Ignores the output.
518 def run_svnadmin(*varargs
):
519 """Run svnadmin with VARARGS, returns exit code as int; stdout, stderr as
521 return run_command(svnadmin_binary
, 1, 0, *varargs
)
523 # For running svnlook. Ignores the output.
524 def run_svnlook(*varargs
):
525 """Run svnlook with VARARGS, returns exit code as int; stdout, stderr as
527 return run_command(svnlook_binary
, 1, 0, *varargs
)
529 def run_svnsync(*varargs
):
530 """Run svnsync with VARARGS, returns exit code as int; stdout, stderr as
532 return run_command(svnsync_binary
, 1, 0, *(_with_config_dir(varargs
)))
534 def run_svnversion(*varargs
):
535 """Run svnversion with VARARGS, returns exit code as int; stdout, stderr
537 return run_command(svnversion_binary
, 1, 0, *varargs
)
539 # Chmod recursively on a whole subtree
540 def chmod_tree(path
, mode
, mask
):
541 def visit(arg
, dirname
, names
):
544 fullname
= os
.path
.join(dirname
, name
)
545 if not os
.path
.islink(fullname
):
546 new_mode
= (os
.stat(fullname
)[stat
.ST_MODE
] & ~mask
) | mode
547 os
.chmod(fullname
, new_mode
)
548 os
.path
.walk(path
, visit
, (mode
, mask
))
550 # For clearing away working copies
551 def safe_rmtree(dirname
, retry
=0):
552 "Remove the tree at DIRNAME, making it writable first"
554 chmod_tree(dirname
, 0666, 0666)
555 shutil
.rmtree(dirname
)
557 if not os
.path
.exists(dirname
):
561 for delay
in (0.5, 1, 2, 4):
572 # For making local mods to files
573 def file_append(path
, new_text
):
574 "Append NEW_TEXT to file at PATH"
575 file_write(path
, new_text
, 'a') # open in (a)ppend mode
577 # Append in binary mode
578 def file_append_binary(path
, new_text
):
579 "Append NEW_TEXT to file at PATH in binary mode"
580 file_write(path
, new_text
, 'ab') # open in (a)ppend mode
582 # For creating new files, and making local mods to existing files.
583 def file_write(path
, contents
, mode
= 'w'):
584 """Write the CONTENTS to the file at PATH, opening file using MODE,
585 which is (w)rite by default."""
586 fp
= open(path
, mode
)
590 # For reading the contents of a file
591 def file_read(path
, mode
= 'r'):
592 """Return the contents of the file at PATH, opening file using MODE,
593 which is (r)ead by default."""
594 fp
= open(path
, mode
)
599 # For replacing parts of contents in an existing file, with new content.
600 def file_substitute(path
, contents
, new_contents
):
601 """Replace the CONTENTS in the file at PATH using the NEW_CONTENTS"""
605 fcontent
= fcontent
.replace(contents
, new_contents
)
610 # For creating blank new repositories
611 def create_repos(path
):
612 """Create a brand-new SVN repository at PATH. If PATH does not yet
615 if not os
.path
.exists(path
):
616 os
.makedirs(path
) # this creates all the intermediate dirs, if neccessary
618 opts
= ("--bdb-txn-nosync",)
619 if server_minor_version
< 5:
620 opts
+= ("--pre-1.5-compatible",)
621 if fs_type
is not None:
622 opts
+= ("--fs-type=" + fs_type
,)
623 exit_code
, stdout
, stderr
= run_command(svnadmin_binary
, 1, 0, "create",
626 # Skip tests if we can't create the repository.
629 if line
.find('Unknown FS type') != -1:
631 # If the FS type is known, assume the repos couldn't be created
632 # (e.g. due to a missing 'svnadmin' binary).
633 raise SVNRepositoryCreateFailure("".join(stderr
).rstrip())
635 # Allow unauthenticated users to write to the repos, for ra_svn testing.
636 file_write(get_svnserve_conf_file_path(path
),
637 "[general]\nauth-access = write\n");
639 file_append(get_svnserve_conf_file_path(path
),
640 "realm = svntest\n[sasl]\nuse-sasl = true\n")
642 file_append(get_svnserve_conf_file_path(path
), "password-db = passwd\n")
643 file_append(os
.path
.join(path
, "conf", "passwd"),
644 "[users]\njrandom = rayjandom\njconstant = rayjandom\n");
646 if config_file
is not None and (fs_type
is None or fs_type
== 'fsfs'):
647 shutil
.copy(config_file
, get_fsfs_conf_file_path(path
))
649 # make the repos world-writeable, for mod_dav_svn's sake.
650 chmod_tree(path
, 0666, 0666)
652 # For copying a repository
653 def copy_repos(src_path
, dst_path
, head_revision
, ignore_uuid
= 1):
654 "Copy the repository SRC_PATH, with head revision HEAD_REVISION, to DST_PATH"
656 # Do an svnadmin dump|svnadmin load cycle. Print a fake pipe command so that
657 # the displayed CMDs can be run by hand
658 create_repos(dst_path
)
659 dump_args
= ' dump "' + src_path
+ '"'
660 load_args
= ' load "' + dst_path
+ '"'
663 load_args
= load_args
+ " --ignore-uuid"
665 print 'CMD:', os
.path
.basename(svnadmin_binary
) + dump_args
, \
666 '|', os
.path
.basename(svnadmin_binary
) + load_args
,
669 dump_in
, dump_out
, dump_err
, dump_kid
= \
670 open_pipe(svnadmin_binary
+ dump_args
, 'b')
672 load_in
, load_out
, load_err
, load_kid
= \
673 open_pipe(svnadmin_binary
+ load_args
, 'b')
676 print '<TIME = %.6f>' % (stop
- start
)
679 data
= dump_out
.read(1024*1024) # Arbitrary buffer size
683 load_in
.close() # Tell load we are done
685 dump_lines
= dump_err
.readlines()
686 load_lines
= load_out
.readlines()
691 # Wait on the pipes; ignore return code.
692 wait_on_pipe(dump_kid
, None, dump_lines
)
693 wait_on_pipe(load_kid
, load_lines
, None)
695 dump_re
= re
.compile(r
'^\* Dumped revision (\d+)\.\r?$')
697 for dump_line
in dump_lines
:
698 match
= dump_re
.match(dump_line
)
699 if not match
or match
.group(1) != str(expect_revision
):
700 print 'ERROR: dump failed:', dump_line
,
701 raise SVNRepositoryCopyFailure
703 if expect_revision
!= head_revision
+ 1:
704 print 'ERROR: dump failed; did not see revision', head_revision
705 raise SVNRepositoryCopyFailure
707 load_re
= re
.compile(r
'^------- Committed revision (\d+) >>>\r?$')
709 for load_line
in load_lines
:
710 match
= load_re
.match(load_line
)
712 if match
.group(1) != str(expect_revision
):
713 print 'ERROR: load failed:', load_line
,
714 raise SVNRepositoryCopyFailure
716 if expect_revision
!= head_revision
+ 1:
717 print 'ERROR: load failed; did not see revision', head_revision
718 raise SVNRepositoryCopyFailure
721 def canonicalize_url(input):
722 "Canonicalize the url, if the scheme is unknown, returns intact input"
724 m
= re
.match(r
"^((file://)|((svn|svn\+ssh|http|https)(://)))", input)
727 return scheme
+ re
.sub(r
'//*', '/', input[len(scheme
):])
732 def create_python_hook_script (hook_path
, hook_script_code
):
733 """Create a Python hook script at HOOK_PATH with the specified
736 if sys
.platform
== 'win32':
737 # Use an absolute path since the working directory is not guaranteed
738 hook_path
= os
.path
.abspath(hook_path
)
739 # Fill the python file.
740 file_write ("%s.py" % hook_path
, hook_script_code
)
741 # Fill the batch wrapper file.
742 file_append ("%s.bat" % hook_path
,
743 "@\"%s\" %s.py %%*\n" % (sys
.executable
, hook_path
))
745 # For all other platforms
746 file_write (hook_path
, "#!%s\n%s" % (sys
.executable
, hook_script_code
))
747 os
.chmod (hook_path
, 0755)
749 def write_restrictive_svnserve_conf(repo_dir
, anon_access
="none"):
750 "Create a restrictive authz file ( no anynomous access )."
752 fp
= open(get_svnserve_conf_file_path(repo_dir
), 'w')
753 fp
.write("[general]\nanon-access = %s\nauth-access = write\n"
754 "authz-db = authz\n" % anon_access
)
756 fp
.write("realm = svntest\n[sasl]\nuse-sasl = true\n");
758 fp
.write("password-db = passwd\n")
761 # Warning: because mod_dav_svn uses one shared authz file for all
762 # repositories, you *cannot* use write_authz_file in any test that
763 # might be run in parallel.
765 # write_authz_file can *only* be used in test suites which disable
766 # parallel execution at the bottom like so
767 # if __name__ == '__main__':
768 # svntest.main.run_tests(test_list, serial_only = True)
769 def write_authz_file(sbox
, rules
, sections
=None):
770 """Write an authz file to SBOX, appropriate for the RA method used,
771 with authorizations rules RULES mapping paths to strings containing
772 the rules. You can add sections SECTIONS (ex. groups, aliases...) with
773 an appropriate list of mappings.
775 fp
= open(sbox
.authz_file
, 'w')
777 # When the sandbox repository is read only it's name will be different from
778 # the repository name.
779 repo_name
= sbox
.repo_dir
780 while repo_name
[-1] == '/':
781 repo_name
= repo_name
[:-1]
782 repo_name
= os
.path
.basename(repo_name
)
784 if sbox
.repo_url
.startswith("http"):
785 prefix
= repo_name
+ ":"
789 for p
, r
in sections
.items():
790 fp
.write("[%s]\n%s\n" % (p
, r
))
792 for p
, r
in rules
.items():
793 fp
.write("[%s%s]\n%s\n" % (prefix
, p
, r
))
796 def use_editor(func
):
797 os
.environ
['SVN_EDITOR'] = svneditor_script
798 os
.environ
['SVN_MERGE'] = svneditor_script
799 os
.environ
['SVNTEST_EDITOR_FUNC'] = func
802 def merge_notify_line(revstart
=None, revend
=None, same_URL
=True,
804 """Return an expected output line that describes the beginning of a
805 merge operation on revisions REVSTART through REVEND. Omit both
806 REVSTART and REVEND for the case where the left and right sides of
807 the merge are from different URLs."""
808 from_foreign_phrase
= foreign
and "\(from foreign repository\) " or ""
810 return "--- Merging differences between %srepository URLs into '.+':\n" \
811 % (foreign
and "foreign " or "")
814 # The left and right sides of the merge are from different URLs.
815 return "--- Merging differences between %srepository URLs into '.+':\n" \
816 % (foreign
and "foreign " or "")
818 return "--- Reverse-merging %sr%ld into '.+':\n" \
819 % (from_foreign_phrase
, abs(revstart
))
821 return "--- Merging %sr%ld into '.+':\n" \
822 % (from_foreign_phrase
, revstart
)
824 if revstart
> revend
:
825 return "--- Reverse-merging %sr%ld through r%ld into '.+':\n" \
826 % (from_foreign_phrase
, revstart
, revend
)
828 return "--- Merging %sr%ld through r%ld into '.+':\n" \
829 % (from_foreign_phrase
, revstart
, revend
)
832 ######################################################################
833 # Functions which check the test configuration
834 # (useful for conditional XFails)
836 def _check_command_line_parsed():
837 """Raise an exception if the command line has not yet been parsed."""
838 if not command_line_parsed
:
839 raise Failure("Condition cannot be tested until command line is parsed")
841 def is_ra_type_dav():
842 _check_command_line_parsed()
843 return test_area_url
.startswith('http')
845 def is_ra_type_svn():
846 _check_command_line_parsed()
847 return test_area_url
.startswith('svn')
849 def is_ra_type_file():
850 _check_command_line_parsed()
851 return test_area_url
.startswith('file')
853 def is_fs_type_fsfs():
854 _check_command_line_parsed()
855 # This assumes that fsfs is the default fs implementation.
856 return fs_type
== 'fsfs' or fs_type
is None
859 return os
.name
== 'nt'
862 return os
.name
== 'posix'
865 return sys
.platform
== 'darwin'
867 def server_has_mergeinfo():
868 _check_command_line_parsed()
869 return server_minor_version
>= 5
871 def server_has_revprop_commit():
872 _check_command_line_parsed()
873 return server_minor_version
>= 5
875 def server_sends_copyfrom_on_update():
876 _check_command_line_parsed()
877 return server_minor_version
>= 5
879 def server_authz_has_aliases():
880 _check_command_line_parsed()
881 return server_minor_version
>= 5
883 def server_gets_client_capabilities():
884 _check_command_line_parsed()
885 return server_minor_version
>= 5
887 def server_has_partial_replay():
888 _check_command_line_parsed()
889 return server_minor_version
>= 5
891 def server_enforces_date_syntax():
892 _check_command_line_parsed()
893 return server_minor_version
>= 5
896 ######################################################################
900 """Manages a sandbox (one or more repository/working copy pairs) for
901 a test to operate within."""
905 def __init__(self
, module
, idx
):
906 self
._set
_name
("%s-%d" % (module
, idx
))
908 def _set_name(self
, name
, read_only
= False):
909 """A convenience method for renaming a sandbox, useful when
910 working with multiple repositories in the same unit test."""
913 self
.read_only
= read_only
914 self
.wc_dir
= os
.path
.join(general_wc_dir
, self
.name
)
916 self
.repo_dir
= os
.path
.join(general_repo_dir
, self
.name
)
917 self
.repo_url
= test_area_url
+ '/' + self
.repo_dir
919 self
.repo_dir
= pristine_dir
920 self
.repo_url
= pristine_url
922 ### TODO: Move this into to the build() method
923 # For dav tests we need a single authz file which must be present,
924 # so we recreate it each time a sandbox is created with some default
926 if self
.repo_url
.startswith("http"):
927 # this dir doesn't exist out of the box, so we may have to make it
928 if not os
.path
.exists(work_dir
):
929 os
.makedirs(work_dir
)
930 self
.authz_file
= os
.path
.join(work_dir
, "authz")
931 file_write(self
.authz_file
, "[/]\n* = rw\n")
933 # For svnserve tests we have a per-repository authz file, and it
934 # doesn't need to be there in order for things to work, so we don't
935 # have any default contents.
936 elif self
.repo_url
.startswith("svn"):
937 self
.authz_file
= os
.path
.join(self
.repo_dir
, "conf", "authz")
940 self
.repo_url
= self
.repo_url
.replace('\\', '/')
941 self
.test_paths
= [self
.wc_dir
, self
.repo_dir
]
943 def clone_dependent(self
, copy_wc
=False):
944 """A convenience method for creating a near-duplicate of this
945 sandbox, useful when working with multiple repositories in the
946 same unit test. If COPY_WC is true, make an exact copy of this
947 sandbox's working copy at the new sandbox's working copy
948 directory. Any necessary cleanup operations are triggered by
949 cleanup of the original sandbox."""
951 if not self
.dependents
:
953 clone
= copy
.deepcopy(self
)
954 self
.dependents
.append(clone
)
955 clone
._set
_name
("%s-%d" % (self
.name
, len(self
.dependents
)))
957 self
.add_test_path(clone
.wc_dir
)
958 shutil
.copytree(self
.wc_dir
, clone
.wc_dir
, symlinks
=True)
961 def build(self
, name
= None, create_wc
= True, read_only
= False):
962 self
._set
_name
(name
, read_only
)
963 if actions
.make_repo_and_wc(self
, create_wc
, read_only
):
964 raise Failure("Could not build repository and sandbox '%s'" % self
.name
)
966 def add_test_path(self
, path
, remove
=True):
967 self
.test_paths
.append(path
)
971 def add_repo_path(self
, suffix
, remove
=1):
972 path
= os
.path
.join(general_repo_dir
, self
.name
) + '.' + suffix
973 url
= test_area_url
+ '/' + path
975 url
= url
.replace('\\', '/')
976 self
.add_test_path(path
, remove
)
979 def add_wc_path(self
, suffix
, remove
=1):
980 path
= self
.wc_dir
+ '.' + suffix
981 self
.add_test_path(path
, remove
)
984 def cleanup_test_paths(self
):
985 "Clean up detritus from this sandbox, and any dependents."
987 # Recursively cleanup any dependent sandboxes.
988 for sbox
in self
.dependents
:
989 sbox
.cleanup_test_paths()
990 # cleanup all test specific working copies and repositories
991 for path
in self
.test_paths
:
992 if not path
is pristine_dir
:
993 _cleanup_test_path(path
)
996 _deferred_test_paths
= []
997 def _cleanup_deferred_test_paths():
998 global _deferred_test_paths
999 test_paths
= _deferred_test_paths
[:]
1000 _deferred_test_paths
= []
1001 for path
in test_paths
:
1002 _cleanup_test_path(path
, 1)
1004 def _cleanup_test_path(path
, retrying
=None):
1007 print "CLEANUP: RETRY:", path
1009 print "CLEANUP:", path
1014 print "WARNING: cleanup failed, will try again later"
1015 _deferred_test_paths
.append(path
)
1017 class TestSpawningThread(threading
.Thread
):
1018 """A thread that runs test cases in their own processes.
1019 Receives test numbers to run from the queue, and saves results into
1020 the results field."""
1021 def __init__(self
, queue
):
1022 threading
.Thread
.__init
__(self
)
1029 next_index
= self
.queue
.get_nowait()
1033 self
.run_one(next_index
)
1035 def run_one(self
, index
):
1036 command
= sys
.argv
[0]
1039 args
.append(str(index
))
1041 # add some startup arguments from this process
1043 args
.append('--fs-type=' + fs_type
)
1045 args
.append('--url=' + test_area_url
)
1049 args
.append('--cleanup')
1051 args
.append('--enable-sasl')
1053 args
.append('--http-library=' + http_library
)
1054 if server_minor_version
:
1055 args
.append('--server-minor-version=' + str(server_minor_version
))
1057 result
, stdout_lines
, stderr_lines
= spawn_process(command
, 1, None, *args
)
1058 # "result" will be None on platforms without Popen3 (e.g. Windows)
1059 if filter(lambda x
: x
.startswith('FAIL: ') or x
.startswith('XPASS: '),
1062 self
.results
.append((index
, result
, stdout_lines
, stderr_lines
))
1063 sys
.stdout
.write('.')
1067 """Encapsulate a single test case (predicate), including logic for
1068 runing the test and test list output."""
1070 def __init__(self
, func
, index
):
1071 self
.pred
= testcase
.create_test_case(func
)
1075 print " %2d %-5s %s" % (self
.index
,
1076 self
.pred
.list_mode(),
1077 self
.pred
.get_description())
1078 self
.pred
.check_description()
1080 def _print_name(self
):
1081 print os
.path
.basename(sys
.argv
[0]), str(self
.index
) + ":", \
1082 self
.pred
.get_description()
1083 self
.pred
.check_description()
1086 """Run self.pred and return the result. The return value is
1087 - 0 if the test was successful
1088 - 1 if it errored in a way that indicates test failure
1089 - 2 if the test skipped
1091 if self
.pred
.need_sandbox():
1092 # ooh! this function takes a sandbox argument
1093 sandbox
= Sandbox(self
.pred
.get_sandbox_name(), self
.index
)
1094 kw
= { 'sandbox' : sandbox
}
1099 # Explicitly set this so that commands that commit but don't supply a
1100 # log message will fail rather than invoke an editor.
1101 # Tests that want to use an editor should invoke svntest.main.use_editor.
1102 os
.environ
['SVN_EDITOR'] = ''
1103 os
.environ
['SVNTEST_EDITOR_FUNC'] = ''
1106 # Set this SVNKit specific variable to the current test (test name plus
1107 # its index) being run so that SVNKit daemon could use this test name
1108 # for its separate log file
1109 os
.environ
['SVN_CURRENT_TEST'] = os
.path
.basename(sys
.argv
[0]) + "_" + \
1112 actions
.no_sleep_for_timestamps()
1114 saved_dir
= os
.getcwd()
1116 rc
= apply(self
.pred
.run
, (), kw
)
1118 print 'STYLE ERROR in',
1120 print 'Test driver returned a status code.'
1127 # We captured Failure and its subclasses. We don't want to print
1128 # anything for plain old Failure since that just indicates test
1129 # failure, rather than relevant information. However, if there
1130 # *is* information in the exception's arguments, then print it.
1131 if ex
.__class
__ != Failure
or ex
.args
:
1134 print 'EXCEPTION: %s: %s' % (ex
.__class
__.__name
__, ex_args
)
1136 print 'EXCEPTION:', ex
.__class
__.__name
__
1137 traceback
.print_exc(file=sys
.stdout
)
1138 except KeyboardInterrupt:
1141 except SystemExit, ex
:
1142 print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex
.code
1143 print ex
.code
and 'FAIL: ' or 'PASS: ',
1148 print 'UNEXPECTED EXCEPTION:'
1149 traceback
.print_exc(file=sys
.stdout
)
1152 result
= self
.pred
.convert_result(result
)
1153 (result_text
, result_benignity
) = self
.pred
.run_text(result
)
1154 if not (quiet_mode
and result_benignity
):
1158 if sandbox
is not None and result
!= 1 and cleanup_mode
:
1159 sandbox
.cleanup_test_paths()
1162 ######################################################################
1163 # Main testing functions
1165 # These two functions each take a TEST_LIST as input. The TEST_LIST
1166 # should be a list of test functions; each test function should take
1167 # no arguments and return a 0 on success, non-zero on failure.
1168 # Ideally, each test should also have a short, one-line docstring (so
1169 # it can be displayed by the 'list' command.)
1171 # Func to run one test in the list.
1172 def run_one_test(n
, test_list
, parallel
= 0, finished_tests
= None):
1173 """Run the Nth client test in TEST_LIST, return the result.
1175 If we're running the tests in parallel spawn the test in a new process.
1178 if (n
< 1) or (n
> len(test_list
) - 1):
1179 print "There is no test", `n`
+ ".\n"
1184 st
= SpawnTest(n
, finished_tests
)
1188 exit_code
= TestRunner(test_list
[n
], n
).run()
1191 def _internal_run_tests(test_list
, testnums
, parallel
):
1192 """Run the tests from TEST_LIST whose indices are listed in TESTNUMS.
1194 If we're running the tests in parallel spawn as much parallel processes
1195 as requested and gather the results in a temp. buffer when a child
1196 process is finished.
1204 for testnum
in testnums
:
1205 if run_one_test(testnum
, test_list
) == 1:
1208 number_queue
= Queue
.Queue()
1209 for num
in testnums
:
1210 number_queue
.put(num
)
1212 threads
= [ TestSpawningThread(number_queue
) for i
in range(parallel
) ]
1219 # list of (index, result, stdout, stderr)
1222 results
+= t
.results
1225 # terminate the line of dots
1228 # all tests are finished, find out the result and print the logs.
1229 for (index
, result
, stdout_lines
, stderr_lines
) in results
:
1231 for line
in stdout_lines
:
1232 sys
.stdout
.write(line
)
1234 for line
in stderr_lines
:
1235 sys
.stdout
.write(line
)
1239 _cleanup_deferred_test_paths()
1244 prog_name
= os
.path
.basename(sys
.argv
[0])
1245 print "%s [--url] [--fs-type] [--verbose|--quiet] [--parallel] \\" % \
1247 print "%s [--enable-sasl] [--cleanup] [--bin] [<test> ...]" \
1248 % (" " * len(prog_name
))
1249 print "%s " % (" " * len(prog_name
))
1250 print "%s [--list] [<test> ...]\n" % prog_name
1252 print " test The number of the test to run (multiple okay), " \
1255 print " --list Print test doc strings instead of running them"
1256 print " --fs-type Subversion file system type (fsfs or bdb)"
1257 print " --http-library DAV library to use (neon or serf)"
1258 print " --url Base url to the repos (e.g. svn://localhost)"
1259 print " --verbose Print binary command-lines (not with --quiet)"
1260 print " --quiet Print only unexpected results (not with --verbose)"
1261 print " --cleanup Whether to clean up"
1262 print " --enable-sasl Whether to enable SASL authentication"
1263 print " --parallel Run the tests in parallel"
1264 print " --bin Use the svn binaries installed in this path"
1265 print " --use-jsvn Use the jsvn (SVNKit based) binaries. Can be\n" \
1266 " combined with --bin to point to a specific path"
1267 print " --development Test development mode: provides more detailed test\n"\
1268 " output and ignores all exceptions in the \n" \
1269 " run_and_verify* functions. This option is only \n" \
1270 " useful during test development!"
1271 print " --server-minor-version Set the minor version for the server.\n" \
1272 " Supports version 4 or 5."
1273 print " --config-file Configuration file for tests."
1274 print " --help This information"
1277 # Main func. This is the "entry point" that all the test scripts call
1278 # to run their list of tests.
1280 # This routine parses sys.argv to decide what to do.
1281 def run_tests(test_list
, serial_only
= False):
1282 """Main routine to run all tests in TEST_LIST.
1284 NOTE: this function does not return. It does a sys.exit() with the
1285 appropriate exit code.
1288 global test_area_url
1295 global is_child_process
1297 global svnadmin_binary
1298 global svnlook_binary
1299 global svnsync_binary
1300 global svndumpfilter_binary
1301 global svnversion_binary
1302 global command_line_parsed
1305 global server_minor_version
1309 # Should the tests be listed (as opposed to executed)?
1318 opts
, args
= my_getopt(sys
.argv
[1:], 'vqhpc',
1319 ['url=', 'fs-type=', 'verbose', 'quiet', 'cleanup',
1320 'list', 'enable-sasl', 'help', 'parallel',
1321 'bin=', 'http-library=', 'server-minor-version=',
1322 'use-jsvn', 'development', 'config-file='])
1323 except getopt
.GetoptError
, e
:
1324 print "ERROR: %s\n" % e
1330 # This is an old deprecated variant of the "--list" option:
1332 elif arg
.startswith('BASE_URL='):
1333 test_area_url
= arg
[9:]
1336 testnums
.append(int(arg
))
1338 print "ERROR: invalid test number '%s'\n" % arg
1342 for opt
, val
in opts
:
1346 elif opt
== "--fs-type":
1349 elif opt
== "-v" or opt
== "--verbose":
1352 elif opt
== "-q" or opt
== "--quiet":
1355 elif opt
== "--cleanup":
1358 elif opt
== "--list":
1361 elif opt
== "--enable-sasl":
1364 elif opt
== "-h" or opt
== "--help":
1368 elif opt
== '-p' or opt
== "--parallel":
1369 parallel
= 5 # use 5 parallel threads.
1372 is_child_process
= True
1374 elif opt
== '--bin':
1377 elif opt
== '--http-library':
1380 elif opt
== '--server-minor-version':
1381 server_minor_version
= int(val
)
1382 if server_minor_version
< 4 or server_minor_version
> 6:
1383 print "ERROR: test harness only supports server minor version 4 or 5"
1386 elif opt
== '--use-jsvn':
1389 elif opt
== '--development':
1390 setup_development_mode()
1392 elif opt
== '--config-file':
1395 if test_area_url
[-1:] == '/': # Normalize url to have no trailing slash
1396 test_area_url
= test_area_url
[:-1]
1398 if verbose_mode
and quiet_mode
:
1399 sys
.stderr
.write("ERROR: 'verbose' and 'quiet' are incompatible\n")
1402 # Calculate pristine_url from test_area_url.
1403 pristine_url
= test_area_url
+ '/' + pristine_dir
1405 pristine_url
= pristine_url
.replace('\\', '/')
1410 svn_binary
= os
.path
.join(svn_bin
, 'jsvn' + _bat
)
1411 svnadmin_binary
= os
.path
.join(svn_bin
, 'jsvnadmin' + _bat
)
1412 svnlook_binary
= os
.path
.join(svn_bin
, 'jsvnlook' + _bat
)
1413 svnsync_binary
= os
.path
.join(svn_bin
, 'jsvnsync' + _bat
)
1414 svndumpfilter_binary
= os
.path
.join(svn_bin
, 'jsvndumpfilter' + _bat
)
1415 svnversion_binary
= os
.path
.join(svn_bin
, 'jsvnversion' + _bat
)
1418 svn_binary
= os
.path
.join(svn_bin
, 'svn' + _exe
)
1419 svnadmin_binary
= os
.path
.join(svn_bin
, 'svnadmin' + _exe
)
1420 svnlook_binary
= os
.path
.join(svn_bin
, 'svnlook' + _exe
)
1421 svnsync_binary
= os
.path
.join(svn_bin
, 'svnsync' + _exe
)
1422 svndumpfilter_binary
= os
.path
.join(svn_bin
, 'svndumpfilter' + _exe
)
1423 svnversion_binary
= os
.path
.join(svn_bin
, 'svnversion' + _exe
)
1425 command_line_parsed
= True
1427 ######################################################################
1429 # Cleanup: if a previous run crashed or interrupted the python
1430 # interpreter, then `temp_dir' was never removed. This can cause wonkiness.
1431 if not is_child_process
:
1432 safe_rmtree(temp_dir
, 1)
1435 # If no test numbers were listed explicitly, include all of them:
1436 testnums
= range(1, len(test_list
))
1439 print "Test # Mode Test Description"
1440 print "------ ----- ----------------"
1441 for testnum
in testnums
:
1442 TestRunner(test_list
[testnum
], testnum
).list()
1444 # done. just exit with success.
1447 # don't run tests in parallel when the tests don't support it or there
1448 # are only a few tests to run.
1449 if serial_only
or len(testnums
) < 2:
1452 # Setup the pristine repository
1453 actions
.setup_pristine_repository()
1455 # Build out the default configuration directory
1456 create_config_dir(default_config_dir
)
1459 exit_code
= _internal_run_tests(test_list
, testnums
, parallel
)
1461 # Remove all scratchwork: the 'pristine' repository, greek tree, etc.
1462 # This ensures that an 'import' will happen the next time we run.
1463 if not is_child_process
:
1464 safe_rmtree(temp_dir
, 1)
1466 # Cleanup after ourselves.
1467 _cleanup_deferred_test_paths()
1469 # Return the appropriate exit code from the tests.
1472 # the modules import each other, so we do this import very late, to ensure
1473 # that the definitions in "main" have been completed.