X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=runner%2Fadt-run;h=7787e5761971fb566993e8e87bd40058ed5d01b4;hb=aa43f2a81adb120c1da99919c5b3523ea952d170;hp=9822d16de5733d4c399441caddde85c2c10de9ff;hpb=18dca52feac6a59c110c6abb25f091e8aa801bd6;p=autopkgtest.git diff --git a/runner/adt-run b/runner/adt-run index 9822d16..7787e57 100755 --- a/runner/adt-run +++ b/runner/adt-run @@ -45,7 +45,9 @@ signal.signal(signal.SIGINT, signal.SIG_DFL) # undo stupid Python SIGINT thing tmpdir = None # pathstring on host testbed = None # Testbed errorcode = 0 # exit status that we are going to use +timeouts = { 'short':100, 'install':3000, 'test':10000, 'build':100000 } binaries = None # Binaries (.debs we have registered) +build_essential = ["build-essential"] #---------- output handling # @@ -73,7 +75,12 @@ binaries = None # Binaries (.debs we have registered) # is done by forking off a copy of ourselves to do plumbing, # which copy we wait for at the appropriate point. +class DummyOpts: + def __init__(do): do.debuglevel = 0 + +opts = DummyOpts() trace_stream = None +summary_stream = None def pstderr(m): print >>sys.stderr, m @@ -82,7 +89,7 @@ def pstderr(m): def debug(m, minlevel=0): if opts.debuglevel < minlevel: return if opts.quiet and trace_stream is None: return - p = 'adt-run: debug' + p = 'adt-run: trace' if minlevel: p += `minlevel` p += ': ' for l in m.rstrip('\n').split('\n'): @@ -107,16 +114,19 @@ class Errplumb: ep.stream = open('/dev/null','w') ep._sp = None elif count == 1: - if to_stderr: ep.stream = sys.stderr + if to_stderr: ep.stream = os.dup(2) else: ep.stream = trace_stream ep._sp = None else: - ep._sp = subprocess.Popen(['tee','/dev/stderr'], + ep._sp = subprocess.Popen(['tee','-a','/dev/stderr'], stdin=subprocess.PIPE, stdout=trace_stream, close_fds=True) ep.stream = ep._sp.stdin def wait(ep): if ep._sp is None: return + if type(ep.stream) == type(2): + os.close(ep.stream) + ep.stream = () ep._sp.stdin.close() rc = ep._sp.wait() if rc: bomb('stderr plumbing tee(1) failed, exit code %d' % rc) @@ -134,9 +144,14 @@ def subprocess_cooked(cmdl, critical=False, dbg=None, **kwargs): ep.wait() return (rc, output) +def psummary(m): + if summary_stream is not None: print >>summary_stream, m + def preport(m): print m + sys.stdout.flush() if trace_stream is not None: print >>trace_stream, m + psummary(m) def report(tname, result): preport('%-20s %s' % (tname, result)) @@ -168,12 +183,14 @@ class Unsupported: def mkdir_okexist(pathname, mode=02755): try: os.mkdir(pathname, mode) - except OSError, oe: + except (IOError,OSError), oe: if oe.errno != errno.EEXIST: raise def rmtree(what, pathname): debug('/ %s rmtree %s' % (what, pathname), 2) - shutil.rmtree(pathname) + try: shutil.rmtree(pathname) + except (IOError,OSError), oe: + if oe.errno != errno.EEXIST: raise def debug_subprocess(what, cmdl=None, script=None): o = '$ '+what+':' @@ -220,7 +237,8 @@ class AutoFile: else: return p.path[tbp]+p.dir+'!' out = p.what if p.spec is not None: - if p.spec_tbp: out += '#' + if not hasattr(p,'spec_tb'): out += '~' + elif p.spec_tbp: out += '#' else: out += '=' out += p.spec out += ':' @@ -231,7 +249,8 @@ class AutoFile: def _wrong(p, how): xtra = '' - if p.spec is not None: xtra = ' spec[%s]=%s' % (p.spec, p.spec_tb) + if p.spec is not None: + xtra = ' spec[%s]=%s' % (p.spec, getattr(p,'spec_tb',None)) raise ("internal error: %s (%s)" % (how, str(p))) def _ensure_path(p, tbp): @@ -389,7 +408,8 @@ class Action: return "" % (a.kind, a.what, `a.af`) def parse_args(): - global opts + global opts, timeouts + global n_non_actions # argh, stupid python scoping rules usage = "%prog --- ..." parser = OptionParser(usage=usage) pa = parser.add_option @@ -401,7 +421,8 @@ def parse_args(): 'deb_forbuilds': 'auto', 'deb_fortests': 'auto', 'tb': False, - 'override_control': None + 'override_control': None, + 'set_lang': 'C' } initial_arghandling = arghandling.copy() n_non_actions = 0 @@ -410,6 +431,7 @@ def parse_args(): # actions (ie, test sets to run, sources to build, binaries to use): def cb_action(op,optstr,value,parser, long,kindpath,is_act): + global n_non_actions parser.largs.append((value,kindpath)) n_non_actions += not(is_act) @@ -432,6 +454,15 @@ def parse_args(): help='use binary package DEB according' ' to most recent --binaries-* settings') + def cb_actnoarg(op,optstr,value,parser, largsentry): + parser.largs.append(largsentry) + pa('','--instantiate', action='callback', callback=cb_actnoarg, + callback_args=((None, ('instantiate',)),), + help='instantiate testbed now (during testing phase)' + ' and install packages' + ' selected for automatic installation, even' + ' if this might apparently not be required otherwise') + pa_action('override-control', 'CONTROL', ('control',), is_act=0, help='run tests from control file CONTROL instead,' ' (applies to next test suite only)') @@ -479,7 +510,6 @@ def parse_args(): ' according to most recent --binaries-* settings') pa_setah('--no-built-binaries', ['dsc_filter'], '_', help='from subsequent sources, do not use any binaries') - #---- binary package processing settings: def pa_setahbins(long,toset,how): @@ -501,7 +531,7 @@ def parse_args(): def cb_path(op,optstr,value,parser, constructor,long,dir): name = long.replace('-','_') - af = constructor(arghandling['tb'], value, long, dir) + af = constructor(long, value,arghandling['tb']) setattr(parser.values, name, af) def pa_path(long, constructor, help, dir=False): @@ -513,12 +543,27 @@ def parse_args(): pa_path('output-dir', OutputDir, dir=True, help='write stderr/out files in PATH') + pa('--leave-lang', dest='set_lang', action='store_false', + help="leave LANG on testbed set to testbed's default") + pa('--set-lang', dest='set_lang', action='store', metavar='LANGVAL', + help='set LANG on testbed to LANGVAL', default='C') + pa('','--tmp-dir', type='string', dest='tmpdir', help='write temporary files to TMPDIR, emptying it' ' beforehand and leaving it behind at the end') pa('','--log-file', type='string', dest='logfile', help='write the log LOGFILE, emptying it beforehand,' ' instead of using OUTPUT-DIR/log or TMPDIR/log') + pa('','--summary-file', type='string', dest='summary', + help='write a summary report to SUMMARY,' + ' emptying it beforehand') + + for k in timeouts.keys(): + pa('','--timeout-'+k, type='int', dest='timeout_'+k, + metavar='T', help='set %s timeout to T') + pa('','--timeout-factor', type='float', dest='timeout_factor', + metavar='FACTOR', default=1.0, + help='multiply all default timeouts by FACTOR') pa('','--user', type='string', dest='user', help='run tests as USER (needs root on testbed)') @@ -556,6 +601,11 @@ def parse_args(): if n_non_actions >= len(parser.largs): parser.error('nothing to do specified') + for k in timeouts.keys(): + t = getattr(opts,'timeout_'+k) + if t is None: t = timeouts[k] * opts.timeout_factor + timeouts[k] = int(t) + arghandling = initial_arghandling opts.actions = [] ix = 0 @@ -582,19 +632,21 @@ def parse_args(): elif kindpath.endswith('/'): kind = 'tree' constructor = InputDir - else: parser.error("do not know how to handle filename \`%s';" - " specify --source --binary or --build-tree") + else: parser.error("do not know how to handle filename `%s';" + " specify --source --binary or --build-tree" % + kindpath) what = '%s%s' % (kind,ix); ix += 1 if kind == 'dsc': fwhatx = '/' + os.path.basename(pathstr) else: fwhatx = '-'+kind - af = constructor(what+fwhatx, pathstr, arghandling['tb']) + if pathstr is None: af = None + else: af = constructor(what+fwhatx, pathstr, arghandling['tb']) opts.actions.append(Action(kind, af, arghandling, what)) def setup_trace(): - global trace_stream, tmpdir + global trace_stream, tmpdir, summary_stream if opts.tmpdir is not None: rmtree('tmpdir(specified)',opts.tmpdir) @@ -611,11 +663,13 @@ def setup_trace(): opts.logfile = opts.tmpdir + '/log' if opts.logfile is not None: trace_stream = open(opts.logfile, 'w', 0) + if opts.summary is not None: + summary_stream = open(opts.summary, 'w', 0) - debug('options: '+`opts`, 1) + debug('options: '+`opts`+'; timeouts: '+`timeouts`, 1) def finalise_options(): - global opts, tb + global opts, tb, build_essential if opts.user is None and 'root-on-testbed' not in testbed.caps: opts.user = '' @@ -646,6 +700,7 @@ def finalise_options(): if (opts.user or 'root-on-testbed' not in testbed.caps): opts.gainroot = 'fakeroot' + build_essential += ['fakeroot'] if opts.gnupghome.startswith('~/'): try: home = os.environ['HOME'] @@ -705,8 +760,8 @@ class Testbed: def reset_apt(tb): if not tb._need_reset_apt: return what = 'aptget-update-reset' - cmdl = ['apt-get','-qy','update'] - rc = tb.execute(what, cmdl) + cmdl = ['sh','-c','apt-get -qy update 2>&1'] + rc = tb.execute(what, cmdl, kind='install') if rc: pstderr("\n" "warning: failed to restore" " testbed apt cache, exit code %d" % rc) @@ -722,6 +777,7 @@ class Testbed: (tb.modified, tb.deps_processed, deps_new), 1) if 'revert' in tb.caps and (tb.modified or [d for d in tb.deps_processed if d not in deps_new]): + for af in tb._ephemeral: af.read(False) tb._debug('reset **') tb.command('revert') tb.blamed = [] @@ -735,7 +791,7 @@ class Testbed: tb.prepare1(deps_new) tb.prepare2(deps_new) def register_ephemeral(tb, af): - if not getattr(af,'spec_tbp',False): tb._ephemeral.append(af) + tb._ephemeral.append(af) def _install_deps(tb, deps_new): tb._debug(' installing dependencies '+`deps_new`, 1) tb.deps_processed = deps_new @@ -749,7 +805,8 @@ class Testbed: ]]) cmdl = ['python','-c',script] what = 'install-deps' - rc = testbed.execute(what+'-debinstall', cmdl, script=script) + rc = testbed.execute(what+'-debinstall', cmdl, script=script, + kind='install') if rc: badpkg('dependency install failed, exit code %d' % rc) def needs_reset(tb): tb._debug('needs_reset, previously=%s' % tb.modified, 1) @@ -816,7 +873,7 @@ class Testbed: return rl[0] def execute(tb, what, cmdl, si='/dev/null', so='/dev/null', se=None, cwd=None, - script=False, tmpdir=None): + script=False, tmpdir=None, kind='short'): # Options for script: # False - do not call debug_subprocess, no synch. reporting required # None or string - call debug_subprocess with that value, @@ -825,6 +882,8 @@ class Testbed: # None - usual Errplumb (output is of kind 2c) # string - path on testbed (output is of kind 2d) + timeout = timeouts[kind] + if script is not False: debug_subprocess(what, cmdl, script=script) if cwd is None: cwd = tb.scratch.write(True) @@ -845,8 +904,14 @@ class Testbed: ','.join(map(urllib.quote, cmdl)), si, so, se_use, cwd] + if timeout is not None and timeout > 0: + cmdl.append('timeout=%d' % timeout) + if xdump is not None and 'execute-debug' in tb.caps: cmdl += [xdump] if tmpdir is not None: cmdl.append('env=TMPDIR=%s' % tmpdir) + if kind=='install': cmdl.append('env=DEBIAN_FRONTEND=noninteractive') + if opts.set_lang is not False: + cmdl.append('env=LANG=%s' % opts.set_lang) rc = tb.commandr1('execute', cmdl) try: rc = int(rc) @@ -950,6 +1015,7 @@ def run_tests(stanzas, tree): global errorcode, testbed if stanzas == (): report('*', 'SKIP no tests in this package') + errorcode |= 8 for stanza in stanzas: tests = stanza[' tests'] if not tests: @@ -958,7 +1024,7 @@ def run_tests(stanzas, tree): for t in tests: t.prepare() t.run(tree) - if 'breaks-testbed' in t.restrictions: + if 'breaks-testbed' in t.restriction_names: testbed.needs_reset() testbed.needs_reset() @@ -1000,13 +1066,14 @@ class Test: t._debug('[----------------------------------------') def stdouterr(oe): idstr = t.what + '-' + oe - if opts.output_dir is not None and opts.output_dir.tb: + if opts.output_dir is not None and opts.output_dir.spec_tbp: use_dir = opts.output_dir else: use_dir = testbed.scratch return RelativeOutputFile(idstr, use_dir, idstr) - t.act.work.write(True) + if hasattr(t.act,'work'): t.act.work.write(True) + tree.read(True) af = RelativeInputFile(t.what, tree, t.path) so = stdouterr('stdout') @@ -1015,19 +1082,26 @@ class Test: tf = af.read(True) tmpdir = None - if 'needs-root' not in t.restrictions: - tf = opts.user_wrap(tf) - tmpdir = '%s/%s-tmpdir' % (testbed.scratch.read(True), t.what) + rc = testbed.execute('testchmod-'+t.what, ['chmod','+x','--',tf]) + if rc: bomb('failed to chmod +x %s' % tf) + + if 'needs-root' not in t.restriction_names and opts.user is not None: + tfl = ['su',opts.user,'-c',tf] + tmpdir = '%s%s-tmpdir' % (testbed.scratch.read(True), t.what) script = 'rm -rf -- "$1"; mkdir -- "$1"' if opts.user: script += '; chown %s "$1"' % opts.user + if 'rw-build-tree' in t.restriction_names: + script += '; chown -R %s "$2"' % opts.user rc = testbed.execute('mktmpdir-'+t.what, - ['sh','-xec',script,'x',tmpdir]) + ['sh','-xec',script,'x',tmpdir,tree.read(True)]) if rc: bomb("could not create test tmpdir `%s', exit code %d" % (tmpdir, rc)) + else: + tfl = [tf] - rc = testbed.execute('test-'+t.what, [tf], + rc = testbed.execute('test-'+t.what, tfl, so=so.write(True), se=se.write(True), cwd=tree.read(True), - tmpdir=tmpdir) + tmpdir=tmpdir, kind='test') so_read = so.read() se_read = se.read() @@ -1037,7 +1111,7 @@ class Test: if stab.st_size != 0: l = open(se_read).readline() l = l.rstrip('\n \t\r') - if len(l) > 40: l = l[:40] + '...' + if len(l) > 35: l = l[:35] + '...' t.reportfail('status: %d, stderr: %s' % (rc, l)) t._debug(' - - - - - - - - - - stderr - - - - - - - - - -') debug_file(se_read) @@ -1066,7 +1140,7 @@ def read_control(act, tree, control_override): tree, 'debian/tests/control') try: control = open(control_af.read(), 'r') - except OSError, oe: + except (IOError,OSError), oe: if oe[0] != errno.ENOENT: raise return [] @@ -1150,9 +1224,11 @@ def print_exception(ei, msgprefix=''): (et, q, tb) = ei if et is Quit: pstderr('adt-run: ' + q.m) + psummary('quitting: '+q.m) return q.ec else: pstderr("adt-run: unexpected, exceptional, error:") + psummary('quitting: unexpected error, consult transcript') traceback.print_exc(None, sys.stderr) if trace_stream is not None: traceback.print_exc(None, trace_stream) @@ -1161,13 +1237,11 @@ def print_exception(ei, msgprefix=''): def cleanup(): global trace_stream try: - rm_ec = 0 - if opts.tmpdir is None and tmpdir is not None: - rmtree('tmpdir', tmpdir) if testbed is not None: testbed.reset_apt() testbed.stop() - if rm_ec: bomb('rm -rf -- %s failed, code %d' % (tmpdir, ec)) + if opts.tmpdir is None and tmpdir is not None: + rmtree('tmpdir', tmpdir) if trace_stream is not None: trace_stream.close() trace_stream = None @@ -1205,7 +1279,7 @@ class Binaries: for x in ['pubring','secring']: os.stat(opts.gnupghome + '/' + x + '.gpg') ok = True - except OSError, oe: + except (IOError,OSError), oe: if oe.errno != errno.ENOENT: raise if ok: b._debug('no key generation needed') @@ -1235,18 +1309,13 @@ END gpg --homedir="$1" --batch --gen-key key-gen-params ''' cmdl = ['sh','-ec',script,'x',opts.gnupghome] - rc = subprocess_cooked(cmdl, dbg=(genkey,script))[0] - if rc: - try: - f = open(opts.gnupghome+'/key-gen-log') - tp = file.read() - except OSError, e: tp = e - pstderr(tp) - bomb('key generation failed, code %d' % rc) + rc = subprocess_cooked(cmdl, dbg=('genkey',script))[0] + if rc: bomb('key generation failed, code %d' % rc) def apt_configs(b): return { "Dir::Etc::sourcelist": b.dir.read(True)+'sources.list', + "Debug::pkgProblemResolver": "true", } def apt_pkg_gdebi_script(b, arg, middle): @@ -1268,7 +1337,9 @@ END 'print res', 'print d.missingDeps', 'print d.requiredChanges', - 'assert(res)', + 'if not res: raise "gdebi failed (%s, %s, %s): %s" % '+ + ' (`res`, `d.missingDeps`, `d.requiredChanges`, '+ + 'd._failureString)', 'cache.commit()', '' ] @@ -1277,7 +1348,7 @@ END ag = ['apt-get','-qy'] for kv in b.apt_configs().iteritems(): ag += ['-o', '%s=%s' % kv] - return ag + return ' '.join(ag) def reset(b): b._debug('reset') @@ -1300,11 +1371,11 @@ END dest = RelativeOutputFile('binaries--'+leafname, b.dir, leafname) try: os.remove(dest.write()) - except OSError, oe: + except (IOError,OSError), oe: if oe.errno != errno.ENOENT: raise e try: os.link(af.read(), dest.write()) - except OSError, oe: + except (IOError,OSError), oe: if oe.errno != errno.EXDEV: raise e shutil.copy(af.read(), dest) @@ -1342,13 +1413,13 @@ END if [ "x`ls /var/lib/dpkg/updates`" != x ]; then echo >&2 "/var/lib/dpkg/updates contains some files, aargh"; exit 1 fi - '''+ ' '.join(b.apt_get()) +''' update >&2 + '''+ b.apt_get() +''' update >&2 cat /var/lib/dpkg/status >&3 ''' testbed.mungeing_apt() rc = testbed.execute('apt-key', ['sh','-ec',script], so=so.write(True), cwd=b.dir.write(True), - script=script) + script=script, kind='install') if rc: bomb('apt setup failed with exit code %d' % rc) testbed.blamed += b.blamed @@ -1367,9 +1438,10 @@ END if pkgs_reinstall: for pkg in pkgs_reinstall: testbed.blame(pkg) what = 'apt-get-reinstall' - cmdl = (b.apt_get() + ['--reinstall','install'] + - [pkg for pkg in pkgs_reinstall]) - rc = testbed.execute(what, cmdl, script=None) + cmdl = (b.apt_get() + ' --reinstall install '+ + ' '.join([pkg for pkg in pkgs_reinstall])+' >&2') + cmdl = ['sh','-c',cmdl] + rc = testbed.execute(what, cmdl, script=None, kind='install') if rc: badpkg("installation of basic binarries failed," " exit code %d" % rc) @@ -1377,8 +1449,9 @@ END for pkg in b.install: what = 'apt-get-install-%s' % pkg testbed.blame(pkg) - cmdl = b.apt_get() + ['install',pkg] - rc = testbed.execute(what, cmdl, script=None) + cmdl = b.apt_get() + ' install ' + pkg + ' >&2' + cmdl = ['sh','-c',cmdl] + rc = testbed.execute(what, cmdl, script=None, kind='install') if rc: badpkg("installation of %s failed, exit code %d" % (pkg, rc)) @@ -1394,11 +1467,11 @@ def source_rules_command(act,script,what,which,work,cwd, so = TemporaryFile('%s-%s-results' % (what,which)) rc = testbed.execute('%s-%s' % (what,which), ['sh','-ec',script]+xargs, script=script, - so=so.write(True), cwd=cwd) + so=so.write(True), cwd=cwd, kind='build') results = open(so.read()).read().rstrip('\n') if len(results): results = results.split("\n") else: results = [] - if rc: badpkg("%s failed with exit code %d" % (which,rc)) + if rc: badpkg("rules %s failed with exit code %d" % (which,rc)) if results_lines is not None and len(results) != results_lines: badpkg("got %d lines of results from %s where %d expected" % (len(results), which, results_lines)) @@ -1460,11 +1533,11 @@ def build_source(act, control_override): script = binaries.apt_pkg_gdebi_script('', [[ 'from GDebi.DebPackage import DebPackage', 'd = DebPackage(cache)', - 'res = d.satisfyDependsStr("dpkg-source")', + 'res = d.satisfyDependsStr("dpkg-dev")', ]]) cmdl = ['python','-c',script] whatp = what+'-dpkgsource' - rc = testbed.execute(what, cmdl, script=script) + rc = testbed.execute(what, cmdl, script=script, kind='install') if rc: badpkg('dpkg-source install failed, exit code %d' % rc) work = TemporaryDir(what+'-build') @@ -1510,7 +1583,7 @@ def build_source(act, control_override): [ create_command ]) script += [ - 'cd */.', + 'cd [a-z0-9]*-*/.', 'pwd >&3', 'set +e; test -f debian/tests/control; echo $? >&3' ] @@ -1546,11 +1619,11 @@ def build_source(act, control_override): for stanza in stanzas: for t in stanza[' tests']: if 'no-build-needed' not in t.feature_names: - build_needed('test %s' % t.name) + build_needed('test %s' % t.tname) for d in t.depends: if '@' in d: build_needed('test %s ' - 'dependency %s' % (t.name,d)) + 'dependency %s' % (t.tname,d)) debug_b('build not needed') built = False @@ -1568,12 +1641,14 @@ def build_source(act, control_override): ],[ 'from GDebi.DebPackage import DebPackage', 'd = DebPackage(cache)', - 'res = d.satisfyDependsStr("build-essential")', + 'res = d.satisfyDependsStr("'+ + ','.join(build_essential)+ + '")', ]]) cmdl = ['python','-c',script] whatp = what+'-builddeps' - rc = testbed.execute(what, cmdl, script=script) + rc = testbed.execute(what, cmdl, script=script, kind='install') if rc: badpkg('build-depends install failed,' ' exit code %d' % rc) @@ -1595,18 +1670,19 @@ def build_source(act, control_override): work.read(True)+os.path.basename(result_pwd), True) if act.ah['dsc_tests']: - act.tests_tree.read() testbed.register_ephemeral(act.work) testbed.register_ephemeral(act.tests_tree) - if not built: return + if not built: + act.blamed = [] + return act.blamed = copy.copy(testbed.blamed) debug_b('filter=%s' % filter) if filter != '_': script = tmpdir_script + [ - 'cd '+work.write(True)+'/*/.', + 'cd '+work.write(True)+'/[a-z0-9]*-*/.', opts.user_wrap(opts.gainroot+' debian/rules binary'), 'cd ..', 'echo *.deb >&3', @@ -1614,7 +1690,7 @@ def build_source(act, control_override): result_debs = source_rules_command(act,script,what, 'binary',work,work.write(True), results_lines=1, xargs=['x',tmpdir]) - if result_debs == '*': debs = [] + if result_debs == '*.deb': debs = [] else: debs = result_debs.split(' ') debug_b('debs='+`debs`) re = regexp.compile('^([-+.0-9a-z]+)_[^_/]+(?:_[^_/]+)\.deb$') @@ -1652,7 +1728,8 @@ def process_actions(): binaries = Binaries() for act in opts.actions: - testbed.register_ephemeral(act.af) + if act.af is not None and not act.af.spec_tbp: + testbed.register_ephemeral(act.af) binaries.reset() control_override = None @@ -1676,6 +1753,8 @@ def process_actions(): act.binaries = [] if act.kind.endswith('tree') or act.kind == 'dsc': control_override = None + if act.kind == 'instantiate': + pass debug_a1('builds done.') @@ -1711,6 +1790,8 @@ def process_actions(): debug_a3('run_tests ...') run_tests(stanzas, act.af) control_override = None + if act.kind == 'instantiate': + testbed.prepare([]) debug_a1('tests done.') def main():