# HG changeset patch # User Oleg Oshmyan # Date 1285192916 0 # Node ID f07b7a431ea684d35958f008e87bc57fa1ff7074 # Parent ec6f1a1321092e2b4f359f2e2bd1a701d294e9f6 Further 2.00 work Testconfs in all supported kinds of archives should now work. Test runs are now cancelled by pressing Escape rather than Ctrl+C. Improved time control. Greatly improved temporary and helper file cleanup. The pause configuration variable can now be a callable and is now processed using subprocess rather than system(). diff -r ec6f1a132109 -r f07b7a431ea6 2.00/compat.py --- a/2.00/compat.py Fri Aug 06 15:39:29 2010 +0000 +++ b/2.00/compat.py Wed Sep 22 22:01:56 2010 +0000 @@ -109,4 +109,5 @@ for name in __all__: __builtins__[name] = globals()[name] +# Support simple testconf.py's written for test.py 1.x __builtins__['xrange'] = range \ No newline at end of file diff -r ec6f1a132109 -r f07b7a431ea6 2.00/config.py --- a/2.00/config.py Fri Aug 06 15:39:29 2010 +0000 +++ b/2.00/config.py Wed Sep 22 22:01:56 2010 +0000 @@ -19,7 +19,7 @@ else: zipimport = None -import imp, os, sys +import imp, os, sys, tempfile __all__ = 'load_problem', 'load_global', 'globalconf' @@ -39,10 +39,10 @@ 'maxexitcode': 0, 'inname': '', 'ansname': ''} +defaults_global = {'tasknames': None, + 'force_zero_exitcode': True} patterns = ('inname', 'outname', 'ansname', 'testcaseinname', 'testcaseoutname', 'dummyinname', 'dummyoutname') -defaults_global = {'tasknames': None, - 'force_zero_exitcode': True} class Config(object): __slots__ = 'modules', '__dict__' @@ -59,6 +59,26 @@ # TODO: provide a message raise AttributeError(name) +# A helper context manager +class ReadDeleting(object): + __slots__ = 'name' + + def __init__(self, name): + self.name = name + + def __enter__(self): + try: + return open(self.name, 'rU') + except: + try: + self.__exit__(None, None, None) + except: + pass + raise + + def __exit__(self, exc_type, exc_val, exc_tb): + os.remove(self.name) + def load_problem(problem_name): dwb = sys.dont_write_bytecode sys.dont_write_bytecode = True @@ -72,9 +92,18 @@ else: del sys.modules['testconf'] if not module: - with metafile.open() as f: - module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) - del sys.modules['testconf'] + try: + with metafile.open() as f: + module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) + # Handle the case when f is not a true file object but imp requires one + except ValueError: + # FIXME: 2.5 lacks the delete parameter + with tempfile.NamedTemporaryFile(delete=False) as f: + inputdatafname = f.name + metafile.extract(inputdatafname) + with ReadDeleting(inputdatafname) as f: + module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) + del sys.modules['testconf'] if hasattr(module, 'padwithzeroestolength'): if not hasattr(module, 'padtests'): try: @@ -117,9 +146,18 @@ else: del sys.modules['testconf'] if not module: - with metafile.open() as f: - module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) - del sys.modules['testconf'] + try: + with metafile.open() as f: + module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) + # Handle the case when f is not a true file object but imp requires one + except ValueError: + # FIXME: 2.5 lacks the delete parameter + with tempfile.NamedTemporaryFile(delete=False) as f: + inputdatafname = f.name + metafile.extract(inputdatafname) + with ReadDeleting(inputdatafname) as f: + module = imp.load_module('testconf', f, metafile.full_real_path, ('.py', 'r', imp.PY_SOURCE)) + del sys.modules['testconf'] for name in defaults_global: setattr(module, name, getattr(module, name, defaults_global[name])) global globalconf diff -r ec6f1a132109 -r f07b7a431ea6 2.00/problem.py --- a/2.00/problem.py Fri Aug 06 15:39:29 2010 +0000 +++ b/2.00/problem.py Wed Sep 22 22:01:56 2010 +0000 @@ -10,9 +10,9 @@ import __main__ __main__.import_error(sys.exc_info()[1]) else: - from __main__ import clock + from __main__ import clock, options -import sys, re +import os, re, sys try: import signal @@ -26,7 +26,7 @@ for name in dir(signal): if re.match('SIG[A-Z]+$', name): value = signal.__dict__[name] - if isinstance(value, int) and (value not in signalnames or signalnames[value][3:] not in unixnames): + if isinstance(value, int) and (value not in signalnames or name[3:] in unixnames): signalnames[value] = name del unixnames @@ -58,99 +58,112 @@ raise NotImplementedError def test(prob): - real = max = ntotal = nvalued = ncorrect = ncorrectvalued = 0 - for case in prob.testcases: - ntotal += 1 - max += case.points - if case.points: nvalued += 1 - granted = 0 - id = str(case.id) - if case.isdummy: - id = 'sample ' + id - say('%*s: ' % (prob.cache.padoutput, id), end='') - sys.stdout.flush() - try: - granted = case() - except KeyboardInterrupt: - if not hasattr(case, 'time_stopped'): - # Too quick! The testing has not even started! - raise - verdict = 'canceled by the user' - except testcases.TimeLimitExceeded: - verdict = 'time limit exceeded' - except testcases.WrongAnswer: - e = sys.exc_info()[1] - if e.comment: - verdict = 'wrong answer (%s)' % e.comment - else: - verdict = 'wrong answer' - except testcases.NonZeroExitCode: - e = sys.exc_info()[1] - if e.exitcode < 0: - if sys.platform == 'win32': - verdict = 'terminated with error 0x%X' % (e.exitcode + 0x100000000) - elif -e.exitcode in signalnames: - verdict = 'terminated by signal %d (%s)' % (-e.exitcode, signalnames[-e.exitcode]) + try: + real = max = ntotal = nvalued = ncorrect = ncorrectvalued = 0 + for case in prob.testcases: + ntotal += 1 + max += case.points + if case.points: nvalued += 1 + granted = 0 + id = str(case.id) + if case.isdummy: + id = 'sample ' + id + say('%*s: ' % (prob.cache.padoutput, id), end='') + sys.stdout.flush() + try: + granted = case(lambda: (say('%7.3f%s s, ' % (case.time_stopped - case.time_started, case.time_limit_string), end=''), sys.stdout.flush())) + except testcases.CanceledByUser: + verdict = 'canceled by the user' + except testcases.TimeLimitExceeded: + verdict = 'time limit exceeded' + except testcases.WrongAnswer: + e = sys.exc_info()[1] + if e.comment: + verdict = 'wrong answer (%s)' % e.comment + else: + verdict = 'wrong answer' + except testcases.NonZeroExitCode: + e = sys.exc_info()[1] + if e.exitcode < 0: + if sys.platform == 'win32': + verdict = 'terminated with error 0x%X' % (e.exitcode + 0x100000000) + elif -e.exitcode in signalnames: + verdict = 'terminated by signal %d (%s)' % (-e.exitcode, signalnames[-e.exitcode]) + else: + verdict = 'terminated by signal %d' % -e.exitcode else: - verdict = 'terminated by signal %d' % -e.exitcode - else: - verdict = 'non-zero return code %d' % e.exitcode - except testcases.CannotStartTestee: - e = sys.exc_info()[1] - if e.upstream.strerror: - verdict = 'cannot launch the program to test (%s)' % e.upstream.strerror.lower() - else: - verdict = 'cannot launch the program to test' - except testcases.CannotStartValidator: - e = sys.exc_info()[1] - if e.upstream.strerror: - verdict = 'cannot launch the validator (%s)' % e.upstream.strerror.lower() + verdict = 'non-zero return code %d' % e.exitcode + except testcases.CannotStartTestee: + e = sys.exc_info()[1] + if e.upstream.strerror: + verdict = 'cannot launch the program to test (%s)' % e.upstream.strerror.lower() + else: + verdict = 'cannot launch the program to test' + except testcases.CannotStartValidator: + e = sys.exc_info()[1] + if e.upstream.strerror: + verdict = 'cannot launch the validator (%s)' % e.upstream.strerror.lower() + else: + verdict = 'cannot launch the validator' + except testcases.CannotReadOutputFile: + e = sys.exc_info()[1] + if e.upstream.strerror: + verdict = 'cannot read the output file (%s)' % e.upstream.strerror.lower() + else: + verdict = 'cannot read the output file' + except testcases.CannotReadInputFile: + e = sys.exc_info()[1] + if e.upstream.strerror: + verdict = 'cannot read the input file (%s)' % e.upstream.strerror.lower() + else: + verdict = 'cannot read the input file' + except testcases.CannotReadAnswerFile: + e = sys.exc_info()[1] + if e.upstream.strerror: + verdict = 'cannot read the reference output file (%s)' % e.upstream.strerror.lower() + else: + verdict = 'cannot read the reference output file' + except testcases.TestCaseNotPassed: + e = sys.exc_info()[1] + verdict = 'unspecified reason [this may be a bug in test.py] (%s)' % e + #except Exception: + # e = sys.exc_info()[1] + # verdict = 'unknown error [this may be a bug in test.py] (%s)' % e else: - verdict = 'cannot launch the validator' - except testcases.CannotReadOutputFile: - e = sys.exc_info()[1] - if e.upstream.strerror: - verdict = 'cannot read the output file (%s)' % e.upstream.strerror.lower() - else: - verdict = 'cannot read the output file' - except testcases.CannotReadInputFile: - e = sys.exc_info()[1] - if e.upstream.strerror: - verdict = 'cannot read the input file (%s)' % e.upstream.strerror.lower() - else: - verdict = 'cannot read the input file' - except testcases.CannotReadAnswerFile: - e = sys.exc_info()[1] - if e.upstream.strerror: - verdict = 'cannot read the reference output file (%s)' % e.upstream.strerror.lower() - else: - verdict = 'cannot read the reference output file' - except testcases.TestCaseNotPassed: - e = sys.exc_info()[1] - verdict = 'unspecified reason [this may be a bug in test.py] (%s)' % e - #except Exception: - # e = sys.exc_info()[1] - # verdict = 'unknown error [this may be a bug in test.py] (%s)' % e + if hasattr(granted, '__iter__'): + granted, comment = granted + if comment: + comment = ' (%s)' % comment + else: + comment = '' + if granted == case.points: + ncorrect += 1 + if granted: ncorrectvalued += 1 + verdict = 'OK' + comment + elif not granted: + verdict = 'wrong answer' + comment + else: + verdict = 'partly correct' + comment + say('%g/%g, %s' % (granted, case.points, verdict)) + real += granted + weighted = real * prob.config.taskweight / max if max else 0 + if nvalued != ntotal: + say('Problem total: %d/%d tests (%d/%d valued); %g/%g points; weighted score: %g/%g' % (ncorrect, ntotal, ncorrectvalued, nvalued, real, max, weighted, prob.config.taskweight)) else: - if hasattr(granted, '__iter__'): - granted, comment = granted - if comment: - comment = ' (%s)' % comment - else: - comment = '' - if granted == case.points: - ncorrect += 1 - if granted: ncorrectvalued += 1 - verdict = 'OK' + comment - elif not granted: - verdict = 'wrong answer' + comment - else: - verdict = 'partly correct' + comment - say('%.3f%s s, %g/%g, %s' % (case.time_stopped - case.time_started, case.time_limit_string, granted, case.points, verdict)) - real += granted - weighted = real * prob.config.taskweight / max if max else 0 - if nvalued != ntotal: - say('Grand total: %d/%d tests (%d/%d valued); %g/%g points; weighted score: %g/%g' % (ncorrect, ntotal, ncorrectvalued, nvalued, real, max, weighted, prob.config.taskweight)) - else: - say('Grand total: %d/%d tests; %g/%g points; weighted score: %g/%g' % (ncorrect, ntotal, real, max, weighted, prob.config.taskweight)) - return weighted, prob.config.taskweight + say('Problem total: %d/%d tests; %g/%g points; weighted score: %g/%g' % (ncorrect, ntotal, real, max, weighted, prob.config.taskweight)) + return weighted, prob.config.taskweight + finally: + if options.erase and (not prob.config.stdio or case.validator): + for var in 'in', 'out': + name = getattr(prob.config, var + 'name') + if name: + try: + os.remove(name) + except Exception: + pass + if case.validator and not callable(case.validator): + if prob.config.ansname: + try: + os.remove(prob.config.ansname) + except Exception: + pass \ No newline at end of file diff -r ec6f1a132109 -r f07b7a431ea6 2.00/test-svn.py --- a/2.00/test-svn.py Fri Aug 06 15:39:29 2010 +0000 +++ b/2.00/test-svn.py Wed Sep 22 22:01:56 2010 +0000 @@ -57,9 +57,15 @@ say('Downloaded and installed. Now you are using test.py ' + latesttext + '.') sys.exit() -import config, itertools, os, sys, time +import config, itertools, os, subprocess, sys, time if options.autotime: + # This is really a dirty hack that assumes that sleep() does not spend + # the CPU time of the current process and that if clock() measures + # wall-clock time, then it is more precise than time() is. Both these + # assumptions are true on all platforms I have tested this on so far, + # but I am not aware of any guarantee that they will both be true + # on every other platform. c = time.clock() time.sleep(1) c = time.clock() - c @@ -73,19 +79,25 @@ clock = time.time try: + from testcases import pause +except ImportError: + pause = None + +try: globalconf = config.load_global() # Do this check here so that if we have to warn them, we do it as early as possible - if options.pause and not hasattr(globalconf, 'pause'): - try: - # If we have getch, we don't need config.pause - import msvcrt - msvcrt.getch.__call__ - except Exception: + if options.pause and not pause and not hasattr(globalconf, 'pause'): + # testcases.pause will be sure to import msvcrt if it can + #try: + # # If we have getch, we don't need globalconf.pause + # import msvcrt + # msvcrt.getch.__call__ + #except Exception: if os.name == 'posix': globalconf.pause = 'read -s -n 1' - say('Warning: configuration variable pause is not defined; it was devised automatically but the choice might be incorrect, so test.py might exit immediately after the testing is completed.') - sys.stdout.flush() + say('Warning: configuration variable pause is not defined; it was devised automatically but the choice might be incorrect, so test.py might exit immediately after the testing is completed.', file=sys.stderr) + sys.stderr.flush() elif os.name == 'nt': globalconf.pause = 'pause' else: @@ -138,7 +150,7 @@ for taskname in globalconf.tasknames: problem = Problem(taskname) - if ntasks: say() + if ntasks and not options.copyonly: say() if shouldprintnames: say(taskname) if options.copyonly: @@ -147,7 +159,7 @@ real, max = problem.test() ntasks += 1 - nfulltasks += (real == max) + nfulltasks += real == max realscore += real maxscore += max @@ -164,8 +176,14 @@ say('Press any key to exit...') sys.stdout.flush() - try: - import msvcrt - msvcrt.getch() - except Exception: - os.system(globalconf.pause + ' >' + os.devnull) \ No newline at end of file + #try: + # import msvcrt + # msvcrt.getch() + #except Exception: + if pause: + pause() + elif callable(globalconf.pause): + globalconf.pause() + else: + with open(os.devnull, 'w') as devnull: + subprocess.call(globalconf.pause, stdout=devnull, stderr=subprocess.STDOUT) \ No newline at end of file diff -r ec6f1a132109 -r f07b7a431ea6 2.00/test.py.sublime-project --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/2.00/test.py.sublime-project Wed Sep 22 22:01:56 2010 +0000 @@ -0,0 +1,10 @@ + + + + + + + + diff -r ec6f1a132109 -r f07b7a431ea6 2.00/testcases.py --- a/2.00/testcases.py Fri Aug 06 15:39:29 2010 +0000 +++ b/2.00/testcases.py Wed Sep 22 22:01:56 2010 +0000 @@ -35,10 +35,62 @@ except (ImportError, AttributeError): TerminateProcess = None + +# Do the hacky-wacky dark magic needed to catch presses of the Escape button. +# If only Python supported forcible termination of threads... +if not sys.stdin.isatty(): + canceled = init_canceled = lambda: False + pause = None +else: + try: + # Windows has select() too, but it is not the select() we want + import msvcrt + except ImportError: + try: + import select, termios, tty, atexit + except ImportError: + # It cannot be helped! + # Silently disable support for killing the program being tested + canceled = init_canceled = lambda: False + pause = None + else: + def cleanup(old=termios.tcgetattr(sys.stdin.fileno())): + termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, old) + atexit.register(cleanup) + del cleanup + tty.setcbreak(sys.stdin.fileno()) + def canceled(): + while select.select((sys.stdin,), (), (), 0)[0]: + if sys.stdin.read(1) == '\33': + return True + return False + def init_canceled(): + while select.select((sys.stdin,), (), (), 0)[0]: + sys.stdin.read(1) + def pause(): + sys.stdin.read(1) + else: + def canceled(): + while msvcrt.kbhit(): + c = msvcrt.getch() + if c == '\33': + return True + elif c == '\0': + # Let's hope no-one is fiddling with this + msvcrt.getch() + return False + def init_canceled(): + while msvcrt.kbhit(): + msvcrt.getch() + def pause(): + msvcrt.getch() + + __all__ = ('TestCase', 'load_problem', 'TestCaseNotPassed', - 'TimeLimitExceeded', 'WrongAnswer', 'NonZeroExitCode', - 'CannotStartTestee', 'CannotStartValidator', - 'CannotReadOutputFile') + 'TimeLimitExceeded', 'CanceledByUser', 'WrongAnswer', + 'NonZeroExitCode', 'CannotStartTestee', + 'CannotStartValidator', 'CannotReadOutputFile', + 'CannotReadInputFile', 'CannotReadAnswerFile') @@ -46,6 +98,7 @@ class TestCaseNotPassed(Exception): __slots__ = () class TimeLimitExceeded(TestCaseNotPassed): __slots__ = () +class CanceledByUser(TestCaseNotPassed): __slots__ = () class WrongAnswer(TestCaseNotPassed): __slots__ = 'comment' @@ -70,12 +123,55 @@ +# Helper context managers + +class CopyDeleting(object): + __slots__ = 'case', 'file', 'name' + + def __init__(self, case, file, name): + self.case = case + self.file = file + self.name = name + + def __enter__(self): + if self.name: + try: + self.file.copy(self.name) + except: + try: + self.__exit__(None, None, None) + except: + pass + raise + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.name: + self.case.files_to_delete.append(self.name) + + +class Copying(object): + __slots__ = 'file', 'name' + + def __init__(self, file, name): + self.file = file + self.name = name + + def __enter__(self): + if self.name: + self.file.copy(self.name) + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + + + # Test case types class TestCase(object): __slots__ = ('problem', 'id', 'isdummy', 'infile', 'outfile', 'points', 'process', 'time_started', 'time_stopped', 'time_limit_string', - 'realinname', 'realoutname', 'maxtime', 'maxmemory') + 'realinname', 'realoutname', 'maxtime', 'maxmemory', + 'has_called_back', 'files_to_delete') if ABCMeta: __metaclass__ = ABCMeta @@ -101,17 +197,22 @@ @abstractmethod def test(case): raise NotImplementedError - def __call__(case): + def __call__(case, callback): + case.has_called_back = False + case.files_to_delete = [] try: - return case.test() + return case.test(callback) finally: + now = clock() + if not getattr(case, 'time_started', None): + case.time_started = case.time_stopped = now + elif not getattr(case, 'time_stopped', None): + case.time_stopped = now + if not case.has_called_back: + callback() case.cleanup() def cleanup(case): - if not getattr(case, 'time_started', None): - case.time_started = case.time_stopped = clock() - elif not getattr(case, 'time_stopped', None): - case.time_stopped = clock() #if getattr(case, 'infile', None): # case.infile.close() #if getattr(case, 'outfile', None): @@ -135,6 +236,7 @@ time.sleep(0) case.process.poll() else: + case.process.wait() break else: # If killing the process is unsuccessful three times in a row, @@ -155,7 +257,15 @@ time.sleep(0) case.process.poll() else: + case.process.wait() break + if case.files_to_delete: + for name in case.files_to_delete: + try: + os.remove(name) + except Exception: + # It can't be helped + pass def open_infile(case): try: @@ -192,7 +302,7 @@ if not isinstance(refline, basestring): line = bytes(line, sys.getdefaultencoding()) if line != refline: - raise WrongAnswer() + raise WrongAnswer try: try: next(output) @@ -201,7 +311,7 @@ except StopIteration: pass else: - raise WrongAnswer() + raise WrongAnswer try: try: next(refoutput) @@ -210,7 +320,7 @@ except StopIteration: pass else: - raise WrongAnswer() + raise WrongAnswer return case.points elif callable(case.validator): return case.validator(output) @@ -218,8 +328,7 @@ # Call the validator program output.close() case.open_outfile() - if case.problem.config.ansname: - case.outfile.copy(case.problem.config.ansname) + case.outfile.copy(case.problem.config.ansname) case.process = Popen(case.validator, stdin=devnull, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=-1) comment = case.process.communicate()[0].strip() lower = comment.lower() @@ -238,7 +347,8 @@ class BatchTestCase(ValidatedTestCase): __slots__ = () - def test(case): + def test(case, callback): + init_canceled() if sys.platform == 'win32' or not case.maxmemory: preexec_fn = None else: @@ -260,48 +370,54 @@ case.time_started = None if case.problem.config.stdio: if options.erase and not case.validator: + # TODO: re-use the same file name if possible # FIXME: 2.5 lacks the delete parameter with tempfile.NamedTemporaryFile(delete=False) as f: - inputdatafname = f.name + inputdatafname = f.name + context = CopyDeleting(case, case.infile, inputdatafname) else: inputdatafname = case.problem.config.inname - case.infile.copy(inputdatafname) - # FIXME: inputdatafname should be deleted on __exit__ - with open(inputdatafname, 'rU') as infile: - with tempfile.TemporaryFile('w+') if options.erase and not case.validator else open(case.problem.config.outname, 'w+') as outfile: - try: + context = Copying(case.infile, inputdatafname) + with context: + with open(inputdatafname, 'rU') as infile: + with tempfile.TemporaryFile('w+') if options.erase and not case.validator else open(case.problem.config.outname, 'w+') as outfile: try: - case.process = Popen(case.problem.config.path, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1, preexec_fn=preexec_fn) - except MemoryError: - # If there is not enough memory for the forked test.py, - # opt for silent dropping of the limit - case.process = Popen(case.problem.config.path, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1) - except OSError: - raise CannotStartTestee(sys.exc_info()[1]) - case.time_started = clock() - # If we use a temporary file, it may not be a true file object, - # and if so, Popen will relay the standard output through pipes - if not case.maxtime: - case.process.communicate() - case.time_stopped = clock() - else: - time_end = case.time_started + case.maxtime - # FIXME: emulate communicate() - while True: - exitcode = case.process.poll() - now = clock() - if exitcode is not None: - case.time_stopped = now - break - elif now >= time_end: - raise TimeLimitExceeded() - if config.globalconf.force_zero_exitcode and case.process.returncode: - raise NonZeroExitCode(case.process.returncode) - outfile.seek(0) - return case.validate(outfile) + try: + case.process = Popen(case.problem.config.path, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1, preexec_fn=preexec_fn) + except MemoryError: + # If there is not enough memory for the forked test.py, + # opt for silent dropping of the limit + case.process = Popen(case.problem.config.path, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1) + except OSError: + raise CannotStartTestee(sys.exc_info()[1]) + case.time_started = clock() + if not case.maxtime: + while True: + exitcode, now = case.process.poll(), clock() + if exitcode is not None: + case.time_stopped = now + break + elif canceled(): + raise CanceledByUser + else: + time_end = case.time_started + case.maxtime + while True: + exitcode, now = case.process.poll(), clock() + if exitcode is not None: + case.time_stopped = now + break + elif now >= time_end: + raise TimeLimitExceeded + elif canceled(): + raise CanceledByUser + if config.globalconf.force_zero_exitcode and case.process.returncode: + raise NonZeroExitCode(case.process.returncode) + callback() + case.has_called_back = True + outfile.seek(0) + return case.validate(outfile) else: - if case.problem.config.inname: - case.infile.copy(case.problem.config.inname) + case.infile.copy(case.problem.config.inname) try: try: case.process = Popen(case.problem.config.path, stdin=devnull, stdout=devnull, stderr=STDOUT, preexec_fn=preexec_fn) @@ -313,20 +429,28 @@ raise CannotStartTestee(sys.exc_info()[1]) case.time_started = clock() if not case.maxtime: - case.process.wait() - case.time_stopped = clock() + while True: + exitcode, now = case.process.poll(), clock() + if exitcode is not None: + case.time_stopped = now + break + elif canceled(): + raise CanceledByUser else: time_end = case.time_started + case.maxtime while True: - exitcode = case.process.poll() - now = clock() + exitcode, now = case.process.poll(), clock() if exitcode is not None: case.time_stopped = now break elif now >= time_end: - raise TimeLimitExceeded() + raise TimeLimitExceeded + elif canceled(): + raise CanceledByUser if config.globalconf.force_zero_exitcode and case.process.returncode: raise NonZeroExitCode(case.process.returncode) + callback() + case.has_called_back = True with open(case.problem.config.outname, 'rU') as output: return case.validate(output) @@ -353,6 +477,7 @@ 'bestout' : BestOutputTestCase, 'reactive': ReactiveTestCase}): if prob.config.usegroups: + # FIXME: test groups should again be supported! pass else: # We will need to iterate over these configuration variables twice diff -r ec6f1a132109 -r f07b7a431ea6 test.py.sublime-project --- a/test.py.sublime-project Fri Aug 06 15:39:29 2010 +0000 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ - - - - - - -