Mercurial > ~astiob > upreckon > hgweb
diff 2.00/testcases.py @ 22:f07b7a431ea6
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().
author | Oleg Oshmyan <chortos@inbox.lv> |
---|---|
date | Wed, 22 Sep 2010 22:01:56 +0000 |
parents | ec6f1a132109 |
children | c1f52b5d80d6 |
line wrap: on
line diff
--- 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