Fix compiler warning due to missing function prototype.
[svn.git] / subversion / tests / cmdline / svntest / main.py
blob953354319024a52471c9beb8cd7ff9cc51876389
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 foreign=False):
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 ""
786 if not same_URL:
787 return "--- Merging differences between %srepository URLs into '.+':\n" \
788 % (foreign and "foreign " or "")
789 if revend is None:
790 if revstart is None:
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 "")
794 elif revstart < 0:
795 return "--- Reverse-merging %sr%ld into '.+':\n" \
796 % (from_foreign_phrase, abs(revstart))
797 else:
798 return "--- Merging %sr%ld into '.+':\n" \
799 % (from_foreign_phrase, revstart)
800 else:
801 if revstart > revend:
802 return "--- Reverse-merging %sr%ld through r%ld into '.+':\n" \
803 % (from_foreign_phrase, revstart, revend)
804 else:
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
835 def is_os_windows():
836 return os.name == 'nt'
838 def is_posix_os():
839 return os.name == 'posix'
841 def is_os_darwin():
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 ######################################################################
870 # Sandbox handling
872 class Sandbox:
873 """Manages a sandbox (one or more repository/working copy pairs) for
874 a test to operate within."""
876 dependents = None
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."""
884 if not name is None:
885 self.name = name
886 self.read_only = read_only
887 self.wc_dir = os.path.join(general_wc_dir, self.name)
888 if not read_only:
889 self.repo_dir = os.path.join(general_repo_dir, self.name)
890 self.repo_url = test_area_url + '/' + self.repo_dir
891 else:
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
898 # contents.
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")
912 if windows:
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:
925 self.dependents = []
926 clone = copy.deepcopy(self)
927 self.dependents.append(clone)
928 clone._set_name("%s-%d" % (self.name, len(self.dependents)))
929 if copy_wc:
930 self.add_test_path(clone.wc_dir)
931 shutil.copytree(self.wc_dir, clone.wc_dir, symlinks=True)
932 return clone
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)
941 if remove:
942 safe_rmtree(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
947 if windows:
948 url = url.replace('\\', '/')
949 self.add_test_path(path, remove)
950 return path, url
952 def add_wc_path(self, suffix, remove=1):
953 path = self.wc_dir + '.' + suffix
954 self.add_test_path(path, remove)
955 return path
957 def cleanup_test_paths(self):
958 "Clean up detritus from this sandbox, and any dependents."
959 if self.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):
978 if verbose_mode:
979 if retrying:
980 print "CLEANUP: RETRY:", path
981 else:
982 print "CLEANUP:", path
983 try:
984 safe_rmtree(path)
985 except:
986 if verbose_mode:
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)
996 self.queue = queue
997 self.results = []
999 def run(self):
1000 while True:
1001 try:
1002 next_index = self.queue.get_nowait()
1003 except Queue.Empty:
1004 return
1006 self.run_one(next_index)
1008 def run_one(self, index):
1009 command = sys.argv[0]
1011 args = []
1012 args.append(str(index))
1013 args.append('-c')
1014 # add some startup arguments from this process
1015 if fs_type:
1016 args.append('--fs-type=' + fs_type)
1017 if test_area_url:
1018 args.append('--url=' + test_area_url)
1019 if verbose_mode:
1020 args.append('-v')
1021 if cleanup_mode:
1022 args.append('--cleanup')
1023 if enable_sasl:
1024 args.append('--enable-sasl')
1025 if http_library:
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: '),
1033 stdout_lines):
1034 result = 1
1035 self.results.append((index, result, stdout_lines, stderr_lines))
1036 sys.stdout.write('.')
1037 sys.stdout.flush()
1039 class TestRunner:
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)
1045 self.index = index
1047 def list(self):
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()
1058 def run(self):
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 }
1068 else:
1069 sandbox = None
1070 kw = {}
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()
1080 try:
1081 rc = apply(self.pred.run, (), kw)
1082 if rc is not None:
1083 print 'STYLE ERROR in',
1084 self._print_name()
1085 print 'Test driver returned a status code.'
1086 sys.exit(255)
1087 result = 0
1088 except Skip, ex:
1089 result = 2
1090 except Failure, ex:
1091 result = 1
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:
1097 ex_args = str(ex)
1098 if ex_args:
1099 print 'EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args)
1100 else:
1101 print 'EXCEPTION:', ex.__class__.__name__
1102 traceback.print_exc(file=sys.stdout)
1103 except KeyboardInterrupt:
1104 print 'Interrupted'
1105 sys.exit(0)
1106 except SystemExit, ex:
1107 print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex.code
1108 print ex.code and 'FAIL: ' or 'PASS: ',
1109 self._print_name()
1110 raise
1111 except:
1112 result = 1
1113 print 'UNEXPECTED EXCEPTION:'
1114 traceback.print_exc(file=sys.stdout)
1116 os.chdir(saved_dir)
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):
1120 print result_text,
1121 self._print_name()
1122 sys.stdout.flush()
1123 if sandbox is not None and result != 1 and cleanup_mode:
1124 sandbox.cleanup_test_paths()
1125 return result
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"
1145 return 1
1147 # Run the test.
1148 if parallel:
1149 st = SpawnTest(n, finished_tests)
1150 st.start()
1151 return 0
1152 else:
1153 exit_code = TestRunner(test_list[n], n).run()
1154 return exit_code
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.
1164 exit_code = 0
1165 finished_tests = []
1166 tests_started = 0
1168 if not parallel:
1169 for testnum in testnums:
1170 if run_one_test(testnum, test_list) == 1:
1171 exit_code = 1
1172 else:
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) ]
1178 for t in threads:
1179 t.start()
1181 for t in threads:
1182 t.join()
1184 # list of (index, result, stdout, stderr)
1185 results = []
1186 for t in threads:
1187 results += t.results
1188 results.sort()
1190 # terminate the line of dots
1191 print
1193 # all tests are finished, find out the result and print the logs.
1194 for (index, result, stdout_lines, stderr_lines) in results:
1195 if stdout_lines:
1196 for line in stdout_lines:
1197 sys.stdout.write(line)
1198 if stderr_lines:
1199 for line in stderr_lines:
1200 sys.stdout.write(line)
1201 if result == 1:
1202 exit_code = 1
1204 _cleanup_deferred_test_paths()
1205 return exit_code
1208 def usage():
1209 prog_name = os.path.basename(sys.argv[0])
1210 print "%s [--url] [--fs-type] [--verbose|--quiet] [--parallel] \\" % \
1211 prog_name
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
1216 print "Arguments:"
1217 print " test The number of the test to run (multiple okay), " \
1218 "or all tests\n"
1219 print "Options:"
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
1253 global pristine_url
1254 global fs_type
1255 global verbose_mode
1256 global quiet_mode
1257 global cleanup_mode
1258 global enable_sasl
1259 global is_child_process
1260 global svn_binary
1261 global svnadmin_binary
1262 global svnlook_binary
1263 global svnsync_binary
1264 global svnversion_binary
1265 global command_line_parsed
1266 global http_library
1267 global server_minor_version
1269 testnums = []
1270 # Should the tests be listed (as opposed to executed)?
1271 list_tests = False
1273 parallel = 0
1274 svn_bin = None
1275 use_jsvn = False
1277 try:
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
1285 usage()
1286 sys.exit(1)
1288 for arg in args:
1289 if arg == "list":
1290 # This is an old deprecated variant of the "--list" option:
1291 list_tests = True
1292 elif arg.startswith('BASE_URL='):
1293 test_area_url = arg[9:]
1294 else:
1295 try:
1296 testnums.append(int(arg))
1297 except ValueError:
1298 print "ERROR: invalid test number '%s'\n" % arg
1299 usage()
1300 sys.exit(1)
1302 for opt, val in opts:
1303 if opt == "--url":
1304 test_area_url = val
1306 elif opt == "--fs-type":
1307 fs_type = val
1309 elif opt == "-v" or opt == "--verbose":
1310 verbose_mode = True
1312 elif opt == "-q" or opt == "--quiet":
1313 quiet_mode = True
1315 elif opt == "--cleanup":
1316 cleanup_mode = True
1318 elif opt == "--list":
1319 list_tests = True
1321 elif opt == "--enable-sasl":
1322 enable_sasl = True
1324 elif opt == "-h" or opt == "--help":
1325 usage()
1326 sys.exit(0)
1328 elif opt == '-p' or opt == "--parallel":
1329 parallel = 5 # use 5 parallel threads.
1331 elif opt == '-c':
1332 is_child_process = True
1334 elif opt == '--bin':
1335 svn_bin = val
1337 elif opt == '--http-library':
1338 http_library = val
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"
1344 sys.exit(1)
1346 elif opt == '--use-jsvn':
1347 use_jsvn = True
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")
1357 sys.exit(1)
1359 # Calculate pristine_url from test_area_url.
1360 pristine_url = test_area_url + '/' + pristine_dir
1361 if windows:
1362 pristine_url = pristine_url.replace('\\', '/')
1364 if use_jsvn:
1365 if svn_bin is None:
1366 svn_bin = ''
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)
1372 use_jsvn = False
1373 else:
1374 if svn_bin:
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)
1390 if not testnums:
1391 # If no test numbers were listed explicitly, include all of them:
1392 testnums = range(1, len(test_list))
1394 if list_tests:
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.
1401 sys.exit(0)
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:
1406 parallel = 0
1408 # Setup the pristine repository
1409 actions.setup_pristine_repository()
1411 # Build out the default configuration directory
1412 create_config_dir(default_config_dir)
1414 # Run the tests.
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.
1426 sys.exit(exit_code)
1428 # the modules import each other, so we do this import very late, to ensure
1429 # that the definitions in "main" have been completed.
1430 import actions
1433 ### End of file.