3
# main.py: a shared, automated test suite for Subversion
5
# Subversion is a tool for revision control.
6
# See http://subversion.tigris.org for more information.
8
# ====================================================================
9
# Copyright (c) 2000-2004 CollabNet. All rights reserved.
11
# This software is licensed as described in the file COPYING, which
12
# you should have received as part of this distribution. The terms
13
# are also available at http://subversion.tigris.org/license-1.html.
14
# If newer versions of this license are posted there, you may use a
15
# newer version instead, at your option.
17
######################################################################
19
import sys # for argv[]
20
import os # for popen2()
21
import shutil # for rmtree()
23
import stat # for ST_MODE
24
import string # for atof()
25
import copy # for deepcopy()
26
import time # for time()
27
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
### Grandfather in SVNTreeUnequal, which used to live here. If you're
64
# ever feeling saucy, you could go through the testsuite and change
65
# main.SVNTreeUnequal to test.SVNTreeUnequal.
67
SVNTreeUnequal = tree.SVNTreeUnequal
69
class SVNProcessTerminatedBySignal(Failure):
70
"Exception raised if a spawned process segfaulted, aborted, etc."
73
class SVNLineUnequal(Failure):
74
"Exception raised if two lines are unequal"
77
class SVNUnmatchedError(Failure):
78
"Exception raised if an expected error is not found"
81
class SVNCommitFailure(Failure):
82
"Exception raised if a commit failed"
85
class SVNRepositoryCopyFailure(Failure):
86
"Exception raised if unable to copy a repository"
89
class SVNRepositoryCreateFailure(Failure):
90
"Exception raised if unable to create a repository"
94
if sys.platform == 'win32':
96
file_scheme_prefix = 'file:///'
100
file_scheme_prefix = 'file://'
103
# os.wait() specifics
106
platform_with_os_wait = 1
108
platform_with_os_wait = 0
110
# The locations of the svn, svnadmin and svnlook binaries, relative to
111
# the only scripts that import this file right now (they live in ../).
112
svn_binary = os.path.abspath('../../svn/svn' + _exe)
113
svnadmin_binary = os.path.abspath('../../svnadmin/svnadmin' + _exe)
114
svnlook_binary = os.path.abspath('../../svnlook/svnlook' + _exe)
115
svnsync_binary = os.path.abspath('../../svnsync/svnsync' + _exe)
116
svnversion_binary = os.path.abspath('../../svnversion/svnversion' + _exe)
118
# Username and password used by the working copies
119
wc_author = 'jrandom'
120
wc_passwd = 'rayjandom'
122
# Username and password used by the working copies for "second user"
124
wc_author2 = 'jconstant' # use the same password as wc_author
126
# Global variable indicating if we want verbose output.
129
# Global variable indicating if we want test data cleaned up after success
132
# Global URL to testing area. Default to ra_local, current working dir.
133
test_area_url = file_scheme_prefix + os.path.abspath(os.getcwd())
135
test_area_url = string.replace(test_area_url, '\\', '/')
137
# Global variable indicating the FS type for repository creations.
140
# All temporary repositories and working copies are created underneath
141
# this dir, so there's one point at which to mount, e.g., a ramdisk.
142
work_dir = "svn-test-work"
144
# Where we want all the repositories and working copies to live.
145
# Each test will have its own!
146
general_repo_dir = os.path.join(work_dir, "repositories")
147
general_wc_dir = os.path.join(work_dir, "working_copies")
149
# A relative path that will always point to latest repository
150
current_repo_dir = None
151
current_repo_url = None
153
# temp directory in which we will create our 'pristine' local
154
# repository and other scratch data. This should be removed when we
155
# quit and when we startup.
156
temp_dir = os.path.join(work_dir, 'local_tmp')
158
# (derivatives of the tmp dir.)
159
pristine_dir = os.path.join(temp_dir, "repos")
160
greek_dump_dir = os.path.join(temp_dir, "greekfiles")
161
config_dir = os.path.abspath(os.path.join(temp_dir, "config"))
162
default_config_dir = config_dir
166
# Our pristine greek-tree state.
168
# If a test wishes to create an "expected" working-copy tree, it should
169
# call main.greek_state.copy(). That method will return a copy of this
170
# State object which can then be edited.
173
greek_state = wc.State('', {
174
'iota' : _item("This is the file 'iota'.\n"),
176
'A/mu' : _item("This is the file 'mu'.\n"),
178
'A/B/lambda' : _item("This is the file 'lambda'.\n"),
180
'A/B/E/alpha' : _item("This is the file 'alpha'.\n"),
181
'A/B/E/beta' : _item("This is the file 'beta'.\n"),
185
'A/D/gamma' : _item("This is the file 'gamma'.\n"),
187
'A/D/G/pi' : _item("This is the file 'pi'.\n"),
188
'A/D/G/rho' : _item("This is the file 'rho'.\n"),
189
'A/D/G/tau' : _item("This is the file 'tau'.\n"),
191
'A/D/H/chi' : _item("This is the file 'chi'.\n"),
192
'A/D/H/psi' : _item("This is the file 'psi'.\n"),
193
'A/D/H/omega' : _item("This is the file 'omega'.\n"),
197
######################################################################
198
# Utilities shared by the tests
200
def get_admin_name():
201
"Return name of SVN administrative subdirectory."
203
if (windows or sys.platform == 'cygwin') \
204
and os.environ.has_key('SVN_ASP_DOT_NET_HACK'):
209
def get_start_commit_hook_path(repo_dir):
210
"Return the path of the start-commit-hook conf file in REPO_DIR."
212
return os.path.join(repo_dir, "hooks", "start-commit")
215
def get_pre_commit_hook_path(repo_dir):
216
"Return the path of the pre-commit-hook conf file in REPO_DIR."
218
return os.path.join(repo_dir, "hooks", "pre-commit")
221
def get_post_commit_hook_path(repo_dir):
222
"Return the path of the post-commit-hook conf file in REPO_DIR."
224
return os.path.join(repo_dir, "hooks", "post-commit")
226
def get_pre_revprop_change_hook_path(repo_dir):
227
"Return the path of the pre-revprop-change hook script in REPO_DIR."
229
return os.path.join(repo_dir, "hooks", "pre-revprop-change")
231
def get_svnserve_conf_file_path(repo_dir):
232
"Return the path of the svnserve.conf file in REPO_DIR."
234
return os.path.join(repo_dir, "conf", "svnserve.conf")
236
# Run any binary, logging the command line (TODO: and return code)
237
def run_command(command, error_expected, binary_mode=0, *varargs):
238
"""Run COMMAND with VARARGS; return stdout, stderr as lists of lines.
239
If ERROR_EXPECTED is None, any stderr also will be printed."""
241
return run_command_stdin(command, error_expected, binary_mode,
244
# Run any binary, supplying input text, logging the command line
245
def run_command_stdin(command, error_expected, binary_mode=0,
246
stdin_lines=None, *varargs):
247
"""Run COMMAND with VARARGS; input STDIN_LINES (a list of strings
248
which should include newline characters) to program via stdin - this
249
should not be very large, as if the program outputs more than the OS
250
is willing to buffer, this will deadlock, with both Python and
251
COMMAND waiting to write to each other for ever.
252
Return stdout, stderr as lists of lines.
253
If ERROR_EXPECTED is None, any stderr also will be printed."""
256
for arg in varargs: # build the command string
259
arg = arg.replace('$', '\$')
260
args = args + ' "' + arg + '"'
262
# Log the command line
264
print 'CMD:', os.path.basename(command) + args,
272
infile, outfile, errfile = os.popen3(command + args, mode)
275
map(infile.write, stdin_lines)
279
stdout_lines = outfile.readlines()
280
stderr_lines = errfile.readlines()
285
if platform_with_os_wait:
286
pid, wait_code = os.wait()
288
exit_code = int(wait_code / 256)
289
exit_signal = wait_code % 256
292
raise SVNProcessTerminatedBySignal
296
print '<TIME = %.6f>' % (stop - start)
298
if (not error_expected) and (stderr_lines):
299
map(sys.stdout.write, stderr_lines)
302
return stdout_lines, stderr_lines
304
def set_config_dir(cfgdir):
305
"Set the config directory."
310
def reset_config_dir():
311
"Reset the config directory to the default value."
314
global default_config_dir
316
config_dir = default_config_dir
318
def create_config_dir(cfgdir,
319
config_contents = '#\n',
320
server_contents = '#\n'):
321
"Create config directories and files"
324
cfgfile_cfg = os.path.join(cfgdir, 'config')
325
cfgfile_srv = os.path.join(cfgdir, 'server')
327
# create the directory
328
if not os.path.isdir(cfgdir):
331
fd = open(cfgfile_cfg, 'w')
332
fd.write(config_contents)
335
fd = open(cfgfile_srv, 'w')
336
fd.write(server_contents)
340
# For running subversion and returning the output
341
def run_svn(error_expected, *varargs):
342
"""Run svn with VARARGS; return stdout, stderr as lists of lines.
343
If ERROR_EXPECTED is None, any stderr also will be printed. If
344
you're just checking that something does/doesn't come out of
345
stdout/stderr, you might want to use actions.run_and_verify_svn()."""
347
return run_command(svn_binary, error_expected, 0,
348
*varargs + ('--config-dir', config_dir))
350
# For running svnadmin. Ignores the output.
351
def run_svnadmin(*varargs):
352
"Run svnadmin with VARARGS, returns stdout, stderr as list of lines."
353
return run_command(svnadmin_binary, 1, 0, *varargs)
355
# For running svnlook. Ignores the output.
356
def run_svnlook(*varargs):
357
"Run svnlook with VARARGS, returns stdout, stderr as list of lines."
358
return run_command(svnlook_binary, 1, 0, *varargs)
360
def run_svnsync(*varargs):
361
"Run svnsync with VARARGS, returns stdout, stderr as list of lines."
362
return run_command(svnsync_binary, 1, 0, *varargs)
364
def run_svnversion(*varargs):
365
"Run svnversion with VARARGS, returns stdout, stderr as list of lines."
366
return run_command(svnversion_binary, 1, 0, *varargs)
368
# Chmod recursively on a whole subtree
369
def chmod_tree(path, mode, mask):
370
def visit(arg, dirname, names):
373
fullname = os.path.join(dirname, name)
374
if not os.path.islink(fullname):
375
new_mode = (os.stat(fullname)[stat.ST_MODE] & ~mask) | mode
376
os.chmod(fullname, new_mode)
377
os.path.walk(path, visit, (mode, mask))
379
# For clearing away working copies
380
def safe_rmtree(dirname, retry=0):
381
"Remove the tree at DIRNAME, making it writable first"
383
chmod_tree(dirname, 0666, 0666)
384
shutil.rmtree(dirname)
386
if not os.path.exists(dirname):
390
for delay in (0.5, 1, 2, 4):
401
# For making local mods to files
402
def file_append(path, new_text):
403
"Append NEW_TEXT to file at PATH"
405
fp = open(path, 'a') # open in (a)ppend mode
409
# For making local mods to files
410
def file_write(path, new_text):
411
"Replace contents of file at PATH with NEW_TEXT"
413
fp = open(path, 'w') # open in (w)rite mode
417
# For creating blank new repositories
418
def create_repos(path):
419
"""Create a brand-new SVN repository at PATH. If PATH does not yet
422
if not(os.path.exists(path)):
423
os.makedirs(path) # this creates all the intermediate dirs, if neccessary
425
opts = ("--bdb-txn-nosync",)
426
if fs_type is not None:
427
opts += ("--fs-type=" + fs_type,)
428
stdout, stderr = run_command(svnadmin_binary, 1, 0, "create", path, *opts)
430
# Skip tests if we can't create the repository.
433
if line.find('Unknown FS type') != -1:
435
# If the FS type is known, assume the repos couldn't be created
436
# (e.g. due to a missing 'svnadmin' binary).
437
raise SVNRepositoryCreateFailure("".join(stderr).rstrip())
439
# Allow unauthenticated users to write to the repos, for ra_svn testing.
440
file_append(os.path.join(path, "conf", "svnserve.conf"),
441
"[general]\nauth-access = write\npassword-db = passwd\n");
442
file_append(os.path.join(path, "conf", "passwd"),
443
"[users]\njrandom = rayjandom\njconstant = rayjandom\n");
444
# make the repos world-writeable, for mod_dav_svn's sake.
445
chmod_tree(path, 0666, 0666)
447
# For copying a repository
448
def copy_repos(src_path, dst_path, head_revision, ignore_uuid = 0):
449
"Copy the repository SRC_PATH, with head revision HEAD_REVISION, to DST_PATH"
451
# A BDB hot-backup procedure would be more efficient, but that would
452
# require access to the BDB tools, and this doesn't. Print a fake
453
# pipe command so that the displayed CMDs can be run by hand
454
create_repos(dst_path)
455
dump_args = ' dump "' + src_path + '"'
456
load_args = ' load "' + dst_path + '"'
459
load_args = load_args + " --ignore-uuid"
461
print 'CMD:', os.path.basename(svnadmin_binary) + dump_args, \
462
'|', os.path.basename(svnadmin_binary) + load_args,
464
dump_in, dump_out, dump_err = os.popen3(svnadmin_binary + dump_args, 'b')
465
load_in, load_out, load_err = os.popen3(svnadmin_binary + load_args, 'b')
468
print '<TIME = %.6f>' % (stop - start)
471
data = dump_out.read(1024*1024) # Arbitrary buffer size
475
load_in.close() # Tell load we are done
477
dump_lines = dump_err.readlines()
478
load_lines = load_out.readlines()
485
dump_re = re.compile(r'^\* Dumped revision (\d+)\.\r?$')
487
for dump_line in dump_lines:
488
match = dump_re.match(dump_line)
489
if not match or match.group(1) != str(expect_revision):
490
print 'ERROR: dump failed:', dump_line,
491
raise SVNRepositoryCopyFailure
493
if expect_revision != head_revision + 1:
494
print 'ERROR: dump failed; did not see revision', head_revision
495
raise SVNRepositoryCopyFailure
497
load_re = re.compile(r'^------- Committed revision (\d+) >>>\r?$')
499
for load_line in load_lines:
500
match = load_re.match(load_line)
502
if match.group(1) != str(expect_revision):
503
print 'ERROR: load failed:', load_line,
504
raise SVNRepositoryCopyFailure
506
if expect_revision != head_revision + 1:
507
print 'ERROR: load failed; did not see revision', head_revision
508
raise SVNRepositoryCopyFailure
511
def set_repos_paths(repo_dir):
512
"Set current_repo_dir and current_repo_url from a relative path to the repo."
513
global current_repo_dir, current_repo_url
514
current_repo_dir = repo_dir
515
current_repo_url = test_area_url + '/' + repo_dir
517
current_repo_url = string.replace(current_repo_url, '\\', '/')
520
def canonicalize_url(input):
521
"Canonicalize the url, if the scheme is unknown, returns intact input"
523
m = re.match(r"^((file://)|((svn|svn\+ssh|http|https)(://)))", input)
526
return scheme + re.sub(r'//*', '/', input[len(scheme):])
531
def create_python_hook_script (hook_path, hook_script_code):
532
"""Create a Python hook script at HOOK_PATH with the specified
535
if sys.platform == 'win32':
536
# Use an absolute path since the working directory is not guaranteed
537
hook_path = os.path.abspath(hook_path)
538
# Fill the python file.
539
file_append ("%s.py" % hook_path, hook_script_code)
540
# Fill the batch wrapper file.
541
file_append ("%s.bat" % hook_path,
542
"@\"%s\" %s.py\n" % (sys.executable, hook_path))
544
# For all other platforms
545
file_append (hook_path, "#!%s\n%s" % (sys.executable, hook_script_code))
546
os.chmod (hook_path, 0755)
549
def compare_unordered_output(expected, actual):
550
"""Compare lists of output lines for equality disregarding the
551
order of the lines"""
552
if len(actual) != len(expected):
553
raise Failure("Length of expected output not equal to actual length")
556
i = expected.index(aline)
559
raise Failure("Expected output does not match actual output")
562
######################################################################
566
"""Manages a sandbox (one or more repository/working copy pairs) for
567
a test to operate within."""
571
def __init__(self, module, idx):
572
self._set_name("%s-%d" % (module, idx))
574
def _set_name(self, name):
575
"""A convenience method for renaming a sandbox, useful when
576
working with multiple repositories in the same unit test."""
578
self.wc_dir = os.path.join(general_wc_dir, self.name)
579
self.repo_dir = os.path.join(general_repo_dir, self.name)
580
self.repo_url = test_area_url + '/' + self.repo_dir
582
# For dav tests we need a single authz file which must be present,
583
# so we recreate it each time a sandbox is created with some default
585
if self.repo_url.startswith("http"):
586
# this dir doesn't exist out of the box, so we may have to make it
587
if not(os.path.exists(work_dir)):
588
os.makedirs(work_dir)
589
self.authz_file = os.path.join(work_dir, "authz")
590
fp = open(self.authz_file, "w")
591
fp.write("[/]\n* = rw\n")
594
# For svnserve tests we have a per-repository authz file, and it
595
# doesn't need to be there in order for things to work, so we don't
596
# have any default contents.
597
elif self.repo_url.startswith("svn"):
598
self.authz_file = os.path.join(self.repo_dir, "conf", "authz")
601
self.repo_url = string.replace(self.repo_url, '\\', '/')
602
self.test_paths = [self.wc_dir, self.repo_dir]
604
def clone_dependent(self):
605
"""A convenience method for creating a near-duplicate of this
606
sandbox, useful when working with multiple repositories in the
607
same unit test. Any necessary cleanup operations are triggered
608
by cleanup of the original sandbox."""
609
if not self.dependents:
611
self.dependents.append(copy.deepcopy(self))
612
self.dependents[-1]._set_name("%s-%d" % (self.name, len(self.dependents)))
613
return self.dependents[-1]
615
def build(self, name = None, create_wc = True):
618
if actions.make_repo_and_wc(self, create_wc):
619
raise Failure("Could not build repository and sandbox '%s'" % self.name)
621
def add_test_path(self, path, remove=1):
622
self.test_paths.append(path)
626
def add_repo_path(self, suffix, remove=1):
627
path = self.repo_dir + '.' + suffix
628
url = self.repo_url + '.' + suffix
629
self.add_test_path(path, remove)
632
def add_wc_path(self, suffix, remove=1):
633
path = self.wc_dir + '.' + suffix
634
self.add_test_path(path, remove)
637
def cleanup_test_paths(self):
638
"Clean up detritus from this sandbox, and any dependents."
640
# Recursively cleanup any dependent sandboxes.
641
for sbox in self.dependents:
642
sbox.cleanup_test_paths()
643
for path in self.test_paths:
644
_cleanup_test_path(path)
647
_deferred_test_paths = []
648
def _cleanup_deferred_test_paths():
649
global _deferred_test_paths
650
test_paths = _deferred_test_paths[:]
651
_deferred_test_paths = []
652
for path in test_paths:
653
_cleanup_test_path(path, 1)
655
def _cleanup_test_path(path, retrying=None):
658
print "CLEANUP: RETRY:", path
660
print "CLEANUP:", path
665
print "WARNING: cleanup failed, will try again later"
666
_deferred_test_paths.append(path)
670
"""Encapsulate a single test case (predicate), including logic for
671
runing the test and test list output."""
673
def __init__(self, func, index):
674
self.pred = testcase.create_test_case(func)
678
print " %2d %-5s %s" % (self.index,
679
self.pred.list_mode(),
680
self.pred.get_description())
681
self.pred.check_description()
683
def _print_name(self):
684
print os.path.basename(sys.argv[0]), str(self.index) + ":", \
685
self.pred.get_description()
686
self.pred.check_description()
689
"""Run self.pred and return the result. The return value is
690
- 0 if the test was successful
691
- 1 if it errored in a way that indicates test failure
692
- 2 if the test skipped
694
if self.pred.need_sandbox():
695
# ooh! this function takes a sandbox argument
696
sandbox = Sandbox(self.pred.get_sandbox_name(), self.index)
703
rc = self.pred.run(args)
705
print 'STYLE ERROR in',
707
print 'Test driver returned a status code.'
714
# We captured Failure and its subclasses. We don't want to print
715
# anything for plain old Failure since that just indicates test
716
# failure, rather than relevant information. However, if there
717
# *is* information in the exception's arguments, then print it.
718
if ex.__class__ != Failure or ex.args:
721
print 'EXCEPTION: %s: %s' % (ex.__class__.__name__, ex_args)
723
print 'EXCEPTION:', ex.__class__.__name__
724
except KeyboardInterrupt:
727
except SystemExit, ex:
728
print 'EXCEPTION: SystemExit(%d), skipping cleanup' % ex.code
729
print ex.code and 'FAIL: ' or 'PASS: ',
734
print 'UNEXPECTED EXCEPTION:'
735
traceback.print_exc(file=sys.stdout)
736
result = self.pred.convert_result(result)
737
print self.pred.run_text(result),
740
if sandbox is not None and result != 1 and cleanup_mode:
741
sandbox.cleanup_test_paths()
745
######################################################################
746
# Main testing functions
748
# These two functions each take a TEST_LIST as input. The TEST_LIST
749
# should be a list of test functions; each test function should take
750
# no arguments and return a 0 on success, non-zero on failure.
751
# Ideally, each test should also have a short, one-line docstring (so
752
# it can be displayed by the 'list' command.)
754
# Func to run one test in the list.
755
def run_one_test(n, test_list):
756
"Run the Nth client test in TEST_LIST, return the result."
758
if (n < 1) or (n > len(test_list) - 1):
759
print "There is no test", `n` + ".\n"
762
# Clear the repos paths for this test
763
global current_repo_dir, current_repo_url
764
current_repo_dir = None
765
current_repo_url = None
768
exit_code = TestRunner(test_list[n], n).run()
772
def _internal_run_tests(test_list, testnums):
773
"""Run the tests from TEST_LIST whose indices are listed in TESTNUMS."""
777
for testnum in testnums:
778
# 1 is the only return code that indicates actual test failure.
779
if run_one_test(testnum, test_list) == 1:
782
_cleanup_deferred_test_paths()
786
# Main func. This is the "entry point" that all the test scripts call
787
# to run their list of tests.
789
# This routine parses sys.argv to decide what to do. Basic usage:
791
# test-script.py [--list] [<testnum>]...
793
# --list : Option to print the docstrings for the chosen tests
794
# instead of running them.
796
# [<testnum>]... : the numbers of the tests that should be run. If no
797
# testnums are specified, then all tests in TEST_LIST are run.
798
def run_tests(test_list):
799
"""Main routine to run all tests in TEST_LIST.
801
NOTE: this function does not return. It does a sys.exit() with the
802
appropriate exit code.
810
# Should the tests be listed (as opposed to executed)?
813
# Explicitly set this so that commands that commit but don't supply a
814
# log message will fail rather than invoke an editor.
815
os.environ['SVN_EDITOR'] = ''
817
opts, args = my_getopt(sys.argv[1:], 'v',
818
['url=', 'fs-type=', 'verbose', 'cleanup', 'list'])
822
# This is an old deprecated variant of the "--list" option:
824
elif arg.startswith('BASE_URL='):
825
test_area_url = arg[9:]
827
testnums.append(int(arg))
829
for opt, val in opts:
833
elif opt == "--fs-type":
836
elif opt == "-v" or opt == "--verbose":
839
elif opt == "--cleanup":
842
elif opt == "--list":
845
if test_area_url[-1:] == '/': # Normalize url to have no trailing slash
846
test_area_url = test_area_url[:-1]
849
# If no test numbers were listed explicitly, include all of them:
850
testnums = range(1, len(test_list))
853
print "Test # Mode Test Description"
854
print "------ ----- ----------------"
855
for testnum in testnums:
856
TestRunner(test_list[testnum], testnum).list()
858
# done. just exit with success.
862
exit_code = _internal_run_tests(test_list, testnums)
864
# remove all scratchwork: the 'pristine' repository, greek tree, etc.
865
# This ensures that an 'import' will happen the next time we run.
866
safe_rmtree(temp_dir)
868
_cleanup_deferred_test_paths()
870
# return the appropriate exit code from the tests.
874
######################################################################
877
# Cleanup: if a previous run crashed or interrupted the python
878
# interpreter, then `temp_dir' was never removed. This can cause wonkiness.
880
safe_rmtree(temp_dir)
882
# the modules import each other, so we do this import very late, to ensure
883
# that the definitions in "main" have been completed.