def find_unmatched(self, test): """Does this manager's filters match the given test properties? Returns '' if it does, the filter that failed if not. """ fe = self.filterenv(test) fe['SELF'] = test for f in self.filters: #print 'SAD DEBUG Filter is %s.'% repr(f) #print fe.copy() #print 'SAD END' try: if eval(f, {}, fe.copy()): #print 'SAD DEBUG Filter %s DID pass.'% repr(f) pass else: #print 'SAD DEBUG Filter %s did not pass.'% repr(f) if debug(): log('Filter %s did not pass.' % repr(f)) return f except KeyboardInterrupt: raise except Exception as e: if debug(): log('In filter %s:' % repr(f), e) return f return ''
def logStart(self, test, result): "Make appropriate log entries about the test that was started" if result: m1 = "Start" elif configuration.options.skip: m1 = "SKIP " else: m1 = "" if self.verbose or debug(): m1 = "Failed attempting to start" n = len(test.group) my_nn = 0 my_nt = 0 my_ngpu = 0 msgHosts = "" if hasattr(test, 'rs_nodesToUse'): if len(test.rs_nodesToUse) > 0: msgHosts = "Hosts = [ " for host in test.rs_nodesToUse: msgHosts += str(host) + " " msgHosts += "]" if hasattr(test, 'num_nodes'): my_nn = test.num_nodes if hasattr(test, 'nt'): my_nt = test.nt if hasattr(test, 'ngpu'): my_ngpu = test.ngpu if n == 1: if (test.srunRelativeNode >= 0): msg = '%s #%4d r=%d, N=%d-%d, np=%s, %s, %s' % \ (m1, test.serialNumber, test.srunRelativeNode, test.numberOfNodesNeeded, test.numNodesToUse, test.np, time.asctime(), test.name) else: msg = '%s #%4d %s, %s nn=%i, np=%i, nt=%i, ngpu=%i %s' % \ (m1, test.serialNumber, test.name, msgHosts, my_nn, test.np, my_nt, my_ngpu, time.asctime()) else: if (test.srunRelativeNode >= 0): msg = '%s #%4d r=%d, N=%d-%d, np=%s, %s, (Group %d #%d) %s' % \ (m1, test.serialNumber, test.srunRelativeNode, test.numberOfNodesNeeded, test.numNodesToUse, test.np, test.groupNumber, time.asctime(), test.groupSerialNumber, test.name) else: msg = '%s #%4d (Group %d #%d) %s, %s nn=%i, np=%i, nt=%i, ngpu=%i %s' % \ (m1, test.serialNumber, test.groupNumber, test.groupSerialNumber, test.name, msgHosts, my_nn, test.np, my_nt, my_ngpu, time.asctime()) if configuration.options.showGroupStartOnly: echo = (not result) or self.verbose or (test.groupSerialNumber == 1) or test.options.get( 'record', False) else: echo = (result) or self.verbose or test.options.get( 'record', False) log(msg, echo=echo) self.schedule(msg) if self.verbose or debug(): log.indent() log("Executing", test.commandLine) log("in directory", test.directory) #log("with timelimit", test.timelimit) log.dedent()
def step(self): """Do one step of the loop, checking for tests that have finished and starting new ones. Return True until all tests done. """ machine.checkRunning() if machine.remainingCapacity() == 0: return True # It is possible that a job can be started. Try to do so. # Note that this is not certain; for example, all waiting jobs have np = 2 but only one # processor available, so nothing is eligible. nextTest = self.findNextTest() while nextTest is not None: if debug(): self.schedule("Chose #%d to start." % nextTest.serialNumber) self.addBlock(nextTest) result = machine.startRun(nextTest) self.logStart(nextTest, result) if not result: self.removeBlock(nextTest) break # failure to launch, let it come back if tests left. nextTest = self.findNextTest() # find out if we need to be called again, as cheaply as possible. if machine.numberTestsRunning > 0: return True for t in chain(*self.groups): if t.status is CREATED: return True else: return False
def firstBanner(self): "Write the opening banner." log.echo = True log('ATS START', atsStartTimeLong) log('ATS VERSION', version.version) log('ATS HOST NAME:', socket.gethostname()) log('ATS LOG DIRECTORY:', log.directory) log('SYS_TYPE:', configuration.SYS_TYPE) log('MACHINE_TYPE', configuration.MACHINE_TYPE) log('BATCH_TYPE', configuration.BATCH_TYPE) log("Machine description: ", self.machine.label(), echo=True) if self.batchmachine: log("Batch facility name:", self.batchmachine.label(), echo=True) else: log("No batch facility found.", echo=True) if not configuration.options.logUsage: log('NOT logging usage.') if configuration.options.info or debug(): configuration.documentConfiguration() log.echo = self.verbose if configuration.options.oneFailure: log('Will stop after first failure.') if configuration.options.allInteractive: log('Will run all tests (including any batch tests) as interactive.' ) log('Default time limit for each test=', Duration(configuration.timelimit))
def init(self, clas='', adder=None, examiner=None): """This initialization is separate so that unit tests can be done on this module. For this reason we delay any logging until main is called. adder and examiner are called in configuration if given to allow user a chance to add options and see results of option parsing. """ tempfile.tempdir = os.getcwd() configuration.init(clas, adder, examiner) self.options = configuration.options self.inputFiles = configuration.inputFiles self.machine = configuration.machine self.batchmachine = configuration.batchmachine if configuration.options.nobatch: self.batchmachine = None self.verbose = configuration.options.verbose or debug() log.echo = self.verbose self.started = datestamp(long_format=True) self.continuationFileName = '' self.atsRunPath = os.getcwd() for a in configuration.options.filter: self.filter(a) pat1 = re.compile(r'^([^\'].*)\'$') pat2 = re.compile(r'^([^\"].*)\"$') for a in configuration.options.glue: if pat1.search(a) or pat2.search(a): a1 = a else: a1 = a.strip('"').strip("'") exec('AtsTest.glue(%s)' % a1) if configuration.options.level: self.filter("level<= %s" % configuration.options.level)
def filterdefs(text=None): """Add the given text into the environment used for filtering. With no arguments, clear defined list. """ global _filterwith if text is None: log('filterdefs: erasing definitions') _filterwith = [] else: try: d = {} for f in _filterwith: exec(f, d) exec(text, d) except SyntaxError as e: raise AtsError(e) except KeyboardInterrupt: raise except Exception as e: pass if debug(): log('filterdefs:') log.indent() log(text) log.dedent() _filterwith.append(text)
def report(self): "Log a report, showing each test." doAll = debug() or \ configuration.options.skip or \ configuration.options.verbose outputCaptured = False for test in self.testlist: if test.output: outputCaptured = True if outputCaptured and not configuration.options.hideOutput: log("NOTICE:", "Captured output, see log.", echo=True, logging=False) for test in self.testlist: if doAll or test.notes or test.groupSerialNumber ==1 or \ test.group.echoStatus() or test.options.get('record', False): echo = True else: echo = False log("#%d %s %s %s (Group %d #%d)" % \ (test.serialNumber, test.status, test.name, test.message, test.group.number, test.groupSerialNumber), echo=echo) for line in test.notes: log("NOTE:", line, echo=echo) log.indent() if debug() or configuration.options.skip: log([t.serialNumber for t in test.waitUntil], echo=False) log.dedent()
def source(self, *paths, **vocabulary): """Input one or more source files, with optional additional vocabulary. If introspection=f given in the vocabulary, or using define, it should be a function taking one argument and returning any introspective portion of it. """ if debug(): log("source:", ' '.join(paths), echo=True) introspector = vocabulary.get( 'introspection', testEnvironment.get('introspection', standardIntrospection)) for path in paths: self._source(path, introspector, vocabulary)
def prioritize(self, interactiveTests): """Give each test a final totalPriority. onCollected routines may change.""" # have to delay looking at the machine / configuration to avoid import race global machine, configuration from ats import configuration machine = configuration.machine self.verbose = configuration.options.verbose or debug() or \ configuration.options.skip self.schedule = AtsLog(directory=log.directory, name='atss.log', logging=configuration.options.logUsage, echo=False) for t in interactiveTests: waitOnMe = [x for x in interactiveTests if t in x.waitUntil] t.totalPriority += sum([w.priority for w in waitOnMe])
def reportObstacles(self, echo=False): "Report on status of tests that can't run now." s = self.schedule if machine.remainingCapacity() == 0: return if (not machine.numberTestsRunning) or debug(): tc = [t for t in chain(*self.groups) if t.status is CREATED and \ ((not machine.canRunNow(t)) or self.isBlocked(t) or self.isWaiting(t))] if not tc: return s("------------------------------------------------", echo=echo) s("Jobs ready to run but not able to due to wait, block, or cpu", echo=echo) s("Serial", "tPriority", "Priority", "Group", "W", "B", "C", "Name", echo=echo) for t in tc: s("%6d %9d %8d %5d %1d %1d %1d %s" % \ (t.serialNumber, t.totalPriority, t.priority, t.groupNumber, self.isWaiting(t), self.isBlocked(t), not machine.canRunNow(t), t.name), echo=echo) s(" ", echo=echo) elif self.verbose: tc = [t for t in chain(*self.groups) if t.status is CREATED and \ self.isBlocked(t) and (not self.isWaiting(t))] if not tc: return s("------------------------------------------------", echo=echo) s("Jobs ready to run but not able due to block or cpu", echo=echo) s("Serial", "tPriority", "Priority", "Group", "B", "C", "Name", echo=echo) for t in tc: s("%6d %9d %8d %5d %1d %1d %s" % \ (t.serialNumber, t.totalPriority, t.priority, t.groupNumber, self.isBlocked(t), not machine.canRunNow(t), t.name), echo=echo)
def addBlock(self, test): "Block directories, if any, needed for test and its group." g = test.group if g.isBlocking: return for t in g: if t.independent: continue d = t.block if d: # If any test in the group blocks, add the block (directory) # to the blocking list and mark this group as blocking. g.isBlocking = True self.blocks[d] = g.number if g.isBlocking: if debug(): self.schedule("Add blocks", g.number)
def __init__(self, *fixedargs, **options): "Must not throw an exception -- object must always get created." super(AtsTest, self).__init__() AtsTest.serialNumber += 1 AtsTest.waitUntilAccumulator.append(self) # populate attributes self.serialNumber = AtsTest.serialNumber if AtsTest.group is None: AtsTest.groupCounter += 1 self.group = AtsTestGroup(AtsTest.groupCounter) else: self.group = AtsTest.group self.group.append(self) self.groupNumber = self.group.number self.groupSerialNumber = len(self.group) self.waitUntil = AtsTest.waitUntil #never modify this, it may be shared. self.runOrder = 0 # to aid in diagnosis of wait, priority self.depends_on = None self.dependents = [] self.expectedResult = PASSED self.setName("uninitialized") self.set(INVALID, "New test, unitialized") self.srunRelativeNode = -1 self.numNodesToUse = -1 self.priority = -1 self.totalPriority = -1 self.startDateTime = curDateTime() self.endDateTime = curDateTime() self.output = [] #magic output, newlines and magic removed. self.notes = [] #note from the run self.block = '' # these will all get changed below but want them set to something for getResults self.level = 0 self.independent = False self.np = 1 self.priority = 1 self.totalPriority = 1 self.directory = '' self.batch = False self.clas = '' self.combineOutput = False self.outname = '' self.shortoutname = '' self.errname = '' self.outhandle = None self.errhandle = None self.commandList = ['not run'] # this is just used for documentation self.commandLine = 'not run' rootdict = dict(ATSROOT=configuration.ATSROOT) # Combine the options: first the defaults, then the glued, then the tacked, # then the stuck, then the test options. self.options = AttributeDict( script='', clas=[], executable='', directory='', ) try: self.options.update(configuration.options.testDefaults) self.options.update(AtsTest.glued) self.options.update(AtsTest.tacked) self.options.update(AtsTest.stuck) self.options.update(AtsTest.grouped) self.options.update(options) except Exception as e: self.set(INVALID, 'Bad options: ' + e) return self.level = self.options['level'] self.np = self.options['np'] self.priority = self.options.get('priority', max(1, self.np)) self.totalPriority = self.priority self.testStdout = self.options['testStdout'] outOpts = ['file', 'terminal', 'both'] if not self.testStdout in outOpts: msg = 'Invalid setting for option testStdout: ' + self.testStdout raise AtsError(msg) if configuration.options.allInteractive: self.batch = False else: self.batch = self.options['batch'] if configuration.options.combineOutErr: self.combineOutput = True else: self.combineOutput = False # process the arguments # Note: old interface was script, clas='', **options # Now allow for possibility of no script, or clas as unnamed second # positional lc = len(fixedargs) if lc > 2: self.set(INVALID, 'Too many positional arguments to test command.') return elif lc == 2: self.options['script'] = fixedargs[0] self.options['clas'] = fixedargs[1] elif lc == 1: self.options['script'] = fixedargs[0] script = self.options['script'] clas = self.options['clas'] if isinstance(clas, str): clas = configuration.machine.split(clas) self.clas = [c % self.options for c in clas] executable = str(self.options.get('executable')) self.directory = self.options['directory'] if executable == '1': if not script: self.set(INVALID, "executable = 1 requires a first argument.") return script = script.replace('$ATSROOT', configuration.ATSROOT) if len(configuration.ATSROOT) == 0: script = script[1:] # remove leading "/" or "\" script = script % rootdict self.executable = Executable(script) if self.directory == '': self.directory = os.getcwd() path = self.executable.path junk, filename = os.path.split(path) else: if executable: executable = executable.replace('$ATSROOT', configuration.ATSROOT) self.executable = Executable(executable % rootdict) else: self.executable = configuration.defaultExecutable if script: script = abspath(script) % self.options self.clas.insert(0, script) if self.directory == '': self.directory, filename = os.path.split(script) else: if self.directory == '': self.directory = os.getcwd() junk, filename = os.path.split(self.executable.path) name, junk = os.path.splitext(filename) self.setName(self.options.get('name', name)) label = self.options.get('label', '') if label: label = str(label).strip() self.setName(self.name + '(' + label + ')') if debug(): log("Results of parsing test arguments", echo=False) log.indent() log("Name:", self.name, echo=False) log("Options:", echo=False) log.indent() for k in self.options: log(k, ": ", self.options[k], echo=False) log.dedent() log("Executable path:", self.executable.path, echo=False) log("Directory:", self.directory, echo=False) log.dedent() self.independent = self.options.get('independent', False) if not self.independent: # the lower() is due to peculiarities on at least the Mac # where os.chdir() seems to change case partially. self.block = self.directory.lower() if not self.executable.is_valid(): self.set(INVALID, 'Executable "%s" not valid.' % self.executable) return if not os.path.isdir(self.directory): self.set(INVALID, 'Directory not valid: %s' % self.directory) if script and not is_valid_file(script): self.set(INVALID, "Script %s does not exist." % script) return self.fileOutNamesSet() #set the timelimit try: tl = options.get('timelimit', None) if tl is None: self.timelimit = configuration.timelimit else: self.timelimit = Duration(tl) except AtsError as msg: self.set(INVALID, msg) return if self.priority <= 0: self.set(SKIPPED, 'Test has priority <= zero.') return # if the test ends up BATCHED, such jobs are legal. if self.batch and configuration.options.nobatch: self.set(SKIPPED, "Batch not available") elif self.batch: problem = configuration.batchmachine.canRun(self) if not problem: if configuration.options.skip: self.set(SKIPPED, "BACH skipped due to skip flag") else: self.set(BATCHED, "Ready to run in batch.") else: self.set(SKIPPED, problem) else: problem = configuration.machine.canRun(self) if not problem: self.set(CREATED, "Ready to run interactively.") elif configuration.options.allInteractive or \ configuration.options.nobatch or \ self.groupNumber: self.set(SKIPPED, problem) else: self.set(BATCHED, problem) self.notes.append(\ "Changed to batch since unable to run interactively on this machine.")
def init(clas = '', adder = None, examiner=None): """Called by manager.init(class, adder, examiner) Initialize configuration and process command-line options; create log, options, inputFiles, timelimit, machine, and batchmatchine. Call backs to machine and to adder/examiner for options. """ global log, options, inputFiles, timelimit, machine, batchmachine,\ defaultExecutable, ATSROOT, cuttime init_debugClass = False if init_debugClass: print("DEBUG init entered clas=%s " % (clas)) # get the machine and possible batch facility machineDirs = MACHINE_DIR # delete, not needed, handled above #if MACHINE_OVERRIDE_DIR: # machineDirs.append(MACHINE_OVERRIDE_DIR) machineList = [] for machineDir in machineDirs: log('machineDir', machineDir) machineList.extend([os.path.join(machineDir,x) for x in os.listdir(machineDir) if x.endswith('.py') and not x.endswith('__init__.py')]) sys.path.insert(0, machineDir) #machineList = [] #for machineDir in machineDirs: # print("DEBUG machineDir=%s " % (machineDir)) # machineList.extend( # [os.path.join(machineDir,x) for x in os.listdir(machineDir) # if x.endswith('.py') and not x.endswith('__init__.py')]) machine = None batchmachine = None specFoundIn = '' bspecFoundIn = '' if init_debugClass: print("DEBUG init 100") print(machineDirs) print(machineList) print(MACHINE_TYPE) print("DEBUG init 200") for full_path in machineList: moduleName = '' fname = os.path.basename(full_path) # print "DEBUG 000 fname = %s" % fname f = open(full_path, 'r') for line in f: if line.startswith('#ATS:') and not machine: items = line[5:-1].split() machineName, moduleName, machineClass, npMaxH = items if init_debugClass: print("DEBUG init machineName=%s moduleName=%s machineClass=%s npMaxH=%s" % (machineName, moduleName, machineClass, npMaxH)) # print "DEBUG init MACHINE_TYPE=%s machineName=%s moduleName=%s machineClass=%s npMaxH=%s" % (MACHINE_TYPE, machineName, moduleName, machineClass, npMaxH) if machineName == MACHINE_TYPE: if moduleName == "SELF": moduleName, junk = os.path.splitext(fname) specFoundIn = full_path print(f"from ats.atsMachines.{moduleName} " f"import {machineClass} as Machine") try: machine_factory = get_machine_factory(moduleName, machineClass) except ModuleNotFoundError: machine_factory = get_machine_factory(moduleName, machineClass, machine_package='atsMachines') machine = machine_factory(machineName, int(npMaxH)) elif line.startswith('#BATS:') and not batchmachine: items = line[6:-1].split() machineName, moduleName, machineClass, npMaxH = items if machineName == BATCH_TYPE: if moduleName == "SELF": moduleName, junk = os.path.splitext(fname) bspecFoundIn = full_path try: machine_factory = get_machine_factory(moduleName, machineClass) except ModuleNotFoundError: machine_factory = get_machine_factory(moduleName, machineClass, machine_package='atsMachines') batchmachine = machine_factory(moduleName, int(npMaxH)) f.close() if machine and batchmachine: break if machine is None: terminal("No machine specifications for", SYS_TYPE, "found, using generic.") machine = machines.Machine('generic', -1) # create the option set usage = "usage: %prog [options] [input files]" parser = OptionParser(usage=usage, version="%prog " + version.version) addOptions(parser) machine.addOptions(parser) # add the --nobatch option but force it true if no batch facility here. parser.add_option('--nobatch', action='store_true', dest='nobatch', default=(batchmachine is None), help = 'Do not run batch jobs.') if batchmachine: batchmachine.addOptions(parser) # user callback? if adder is not None: adder(parser) # parse the command line if clas: import shlex argv = shlex.split(clas) else: argv = sys.argv[1:] (toptions, inputFiles) = parser.parse_args(argv) # immediately make the options a real dictionary -- the way optparse leaves it # is misleading. options = AttributeDict() for k in vars(toptions).keys(): options[k] = getattr(toptions, k) # set up the test default options so the machine(s) can add to it options['testDefaults'] = AttributeDict(np=1, batch=0, level=1, keep = options.keep, hideOutput = options.hideOutput, verbose = options.verbose, testStdout = options.testStdout, globalPrerunScript = options.globalPrerunScript, globalPostrunScript = options.globalPostrunScript, sequential = options.sequential, nosrun = options.nosrun, salloc = options.salloc ) # let the machine(s) modify the results or act upon them in other ways. machine.examineOptions(options) if batchmachine: batchmachine.examineOptions(options) # unpack basic options debug(options.debug) if options.logdir: log.set(directory = options.logdir) else: dirname = SYS_TYPE + "." + atsStartTime + ".logs" log.set(directory = dirname) log.mode="w" log.logging = 1 # user callback? if examiner is not None: examiner(options) if specFoundIn: log("Found specification for", MACHINE_TYPE, "in", specFoundIn) else: log("No specification found for", MACHINE_TYPE, ', using generic') if bspecFoundIn: log("Batch specification for ", BATCH_TYPE, "in", bspecFoundIn) # unpack other options cuttime = options.cuttime if cuttime is not None: cuttime = Duration(cuttime) timelimit = Duration(options.timelimit) defaultExecutable = executables.Executable(abspath(options.executable)) # ATSROOT is used in tests.py to allow paths pointed at the executable's directory if 'ATSROOT' in os.environ: ATSROOT = os.environ['ATSROOT'] else: ATSROOT = os.path.dirname(defaultExecutable.path)
def collectTests(self): """Process the input and collect the tests to be executed. We immediately make sure each input file exists and is readable. (If we don't we might not find out until many tests have run.) """ # It is worth settling this now. if debug(): log("Checking that input files exist.") files = [] for input_file in self.inputFiles: t = abspath(input_file) dir, filename = os.path.split(t) name, e = os.path.splitext(filename) if e: namelist = [t] else: namelist = [t, t + '.ats', t + '.py'] for t1 in namelist: try: f = open(t1, 'r') break except IOError: pass else: log.fatal_error('Cannot open %s.' % t) f.close() files.append(t1) log("Input ok. Now collect the tests.") # Now collect the tests. for t in files: self.source(t) # Stop the execution of ats when the first INVALID test is found unless option --okInvalid. log.indent() found = False for item in self.badlist: found = True log('Bad file:', item, echo=True) for test in self.testlist: if test.status is INVALID: found = True log(test.status, "#%d" % test.serialNumber, test.name, echo=True) log.dedent() if found: log('************************************************', echo=True) log('NOTE: Invalid tests or files', echo=True) if not configuration.options.okInvalid: log.fatal_error("Fix invalid tests or rerun with --okInvalid.") # Make sure that every test has distinct name testnames = [t.name.lower() for t in self.testlist] for i in range(len(testnames)): name = testnames[i] while testnames.count(name) > 1: count = 1 for j in range(i + 1, len(testnames)): if testnames[j] == name: count += 1 t = self.testlist[j] t.name += ("#%d" % count) testnames[j] = t.name.lower() # Add parents to each test's waitlist. for t in self.testlist: if t.status is CREATED: for d in t.dependents: if t not in d.waitUntil: d.waitUntil = d.waitUntil + [t] log.leading = '' log("------------------ Input complete --------", echo=True) echo = configuration.options.verbose or \ debug() or \ configuration.options.skip for t in self.testlist: log(repr(t), echo=echo)
def _source(self, path, introspector, vocabulary): "Process source file. Returns true if successful" here = os.getcwd() t = abspath(path) directory, filename = os.path.split(t) name, e = os.path.splitext(filename) if e: namelist = [t] else: namelist = [t, t + '.ats', t + '.py'] for t1 in namelist: if t1 in AtsManager.alreadysourced: log("Already sourced:", t1) return try: f = open(t1) break except IOError as e: pass else: log("Error opening input file:", t1, echo=True) self.badlist.append(t1) raise AtsError("Could not open input file %s" % path) t = abspath(t1) directory, filename = os.path.split(t1) name, e = os.path.splitext(filename) AtsManager.alreadysourced.append(t1) # save to restore after this file is read savestuck = dict(AtsTest.stuck) savetacked = dict(AtsTest.tacked) unstick() #clear sticky list at the start of a file. AtsTest.waitNewSource() testenv = dict(testEnvironment) testenv.update(vocabulary) testenv['SELF'] = t1 atstext = [] for line1 in f: if not line1: continue if line1.startswith('#!'): continue magic = introspector(line1[:-1]) if magic is not None: atstext.append(magic) f.close() if atstext: log('-> Executing statements in', t1, echo=False) log.indent() code = '\n'.join(atstext) if debug(): for line in atstext: log(line, echo=False) os.chdir(directory) try: exec(code, testenv) if debug(): log('Finished ', t1, datestamp()) except KeyboardInterrupt: raise except Exception as details: self.badlist.append(t1) log('Error while processing statements in', t1, ':', echo=True) log(details, echo=True) log.dedent() else: log('-> Sourcing', t1, echo=False) log.indent() os.chdir(directory) try: exec(compile(open(t1, "rb").read(), t1, 'exec'), testenv) if debug(): log('Finished ', t1, datestamp()) result = 1 except KeyboardInterrupt: raise except Exception as details: self.badlist.append(t1) log('Error in input file', t1, ':', echo=True) log(details, echo=True) log('------------------------------------------', echo=True) log.dedent() AtsTest.endGroup() unstick() stick(**savestuck) untack() tack(**savetacked) AtsTest.waitEndSource() os.chdir(here)