# HG changeset patch # User Oleg Oshmyan # Date 1285283158 0 # Node ID b500e117080e6f8baec38c7af8b04ed43cc11880 # Parent c23d81f4a1a36aecb1f4a2ad44dab36acca04034 Bug fixes and overhead reduction Added the --problem/-p option. (WARNING: not the same as the -p option of test.py 1.x.) The problem names supplied are not validated. Added zip_longest to compat.py. Experimental: problem names are now _always_ printed for multi-problem sets. Overhead: Escape presses are now checked only once every .15 seconds (at least kbhit() on Windows is very slow). Overhead: sleep(0) is now called in the time-control-and-Escape-watching loop (at least on Windows, it immediately transfers control to some waiting thread). Bug fix: compat.py now overwrites built-ins only while including testconfs (--help was broken in Python 2). Bug fix: ReadDeleting in config.py now closes the file it opens (especially important on Windows, where open files cannot be deleted). Bug fix: added callable to compat.py (it is absent from Python 3). Bug fix: the default (built-in) output validator now properly handles unwanted trailing data. Bug fix: testconfs in custom archives no more raise NameError. Bug fix: if a validator program cannot be launched, CannotStartValidator is now raised instead of the fatal OSError. diff -r c23d81f4a1a3 -r b500e117080e 2.00/compat.py --- a/2.00/compat.py Thu Sep 23 00:11:24 2010 +0000 +++ b/2.00/compat.py Thu Sep 23 23:05:58 2010 +0000 @@ -25,8 +25,11 @@ # with Python 2 and not usable conditionally via exec() and such # because it is a detail of the syntax of the class statement itself. -__all__ = ('say', 'basestring', 'range', 'map', 'zip', 'filter', - 'items', 'keys', 'values', 'ABCMeta', 'abstractmethod') +import __builtin__ + +__all__ = ('say', 'basestring', 'range', 'map', 'zip', 'filter', 'items', + 'keys', 'values', 'zip_longest', 'callable', + 'ABCMeta', 'abstractmethod', 'CompatBuiltins') try: # Python 3 @@ -88,6 +91,11 @@ range = range try: + callable = callable +except NameError: + callable = lambda obj: hasattr(obj, '__call__') + +try: from itertools import imap as map except ImportError: map = map @@ -106,8 +114,42 @@ keys = dict.iterkeys if hasattr(dict, 'iterkeys') else dict.keys values = dict.itervalues if hasattr(dict, 'itervalues') else dict.values -for name in __all__: - __builtins__[name] = globals()[name] +try: + # Python 3 + from itertools import zip_longest +except ImportError: + # Python 2.6/2.7 + from itertools import izip_longest as zip_longest +except ImportError: + # Python 2.5 + from itertools import chain, repeat + # Adapted from the documentation of itertools.izip_longest + def zip_longest(*args, **kwargs): + fillvalue = kwargs.get('fillvalue') + def sentinel(counter=([fillvalue]*(len(args)-1)).pop): + yield counter() + fillers = repeat(fillvalue) + iters = [chain(it, sentinel(), fillers) for it in args] + try: + for tup in zip(*iters): + yield tup + except IndexError: + pass + +# Automatically import * from this module into testconf.py's +class CompatBuiltins(object): + __slots__ = 'originals' + def __init__(self): + self.originals = {} + def __enter__(self): + g = globals() + for name in __all__: + if hasattr(__builtin__, name): + self.originals[name] = getattr(__builtin__, name) + setattr(__builtin__, name, g[name]) + def __exit__(self, exc_type, exc_val, exc_tb): + for name in self.originals: + setattr(__builtin__, name, self.originals[name]) # Support simple testconf.py's written for test.py 1.x -__builtins__['xrange'] = range \ No newline at end of file +__builtin__.xrange = range \ No newline at end of file diff -r c23d81f4a1a3 -r b500e117080e 2.00/config.py --- a/2.00/config.py Thu Sep 23 00:11:24 2010 +0000 +++ b/2.00/config.py Thu Sep 23 23:05:58 2010 +0000 @@ -4,6 +4,7 @@ from __future__ import division, with_statement try: + from compat import * import files except ImportError: import __main__ @@ -61,14 +62,15 @@ # A helper context manager class ReadDeleting(object): - __slots__ = 'name' + __slots__ = 'name', 'file' def __init__(self, name): self.name = name def __enter__(self): try: - return open(self.name, 'rU') + self.file = open(self.name, 'rU') + return self.file except: try: self.__exit__(None, None, None) @@ -77,6 +79,7 @@ raise def __exit__(self, exc_type, exc_val, exc_tb): + self.file.close() os.remove(self.name) def load_problem(problem_name): @@ -84,26 +87,27 @@ sys.dont_write_bytecode = True metafile = files.File('/'.join((problem_name, 'testconf.py')), True, 'configuration') module = None - if zipimport and isinstance(metafile.archive, files.ZipArchive): - try: - module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf') - except zipimport.ZipImportError: - pass - else: + with CompatBuiltins(): + if zipimport and isinstance(metafile.archive, files.ZipArchive): + try: + module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf') + except zipimport.ZipImportError: + pass + else: + del sys.modules['testconf'] + if not module: + 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.copy(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 not module: - 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: @@ -139,26 +143,27 @@ sys.dont_write_bytecode = True metafile = files.File('testconf.py', True, 'configuration') module = None - if zipimport and isinstance(metafile.archive, files.ZipArchive): - try: - module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf') - except zipimport.ZipImportError: - pass - else: + with CompatBuiltins(): + if zipimport and isinstance(metafile.archive, files.ZipArchive): + try: + module = zipimport.zipimporter(os.path.dirname(metafile.full_real_path)).load_module('testconf') + except zipimport.ZipImportError: + pass + else: + del sys.modules['testconf'] + if not module: + 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.copy(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 not module: - 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 c23d81f4a1a3 -r b500e117080e 2.00/test-svn.py --- a/2.00/test-svn.py Thu Sep 23 00:11:24 2010 +0000 +++ b/2.00/test-svn.py Thu Sep 23 23:05:58 2010 +0000 @@ -15,6 +15,7 @@ parser = optparse.OptionParser(version='test.py '+version, epilog='Python 2.5 or newer is required, unless you have a custom build of Python.') parser.add_option('-1', dest='legacy', action='store_true', default=False, help='handle configuration files in a way more compatible with test.py 1.x') parser.add_option('-u', '--update', dest='update', action='store_true', default=False, help='check for an updated version of test.py') +parser.add_option('-p', '--problem', dest='problems', metavar='PROBLEM', action='append', help='test only the PROBLEM (this option can be specified more than once with different problem names, all of which will be tested)') parser.add_option('-m', '--copy-io', dest='copyonly', action='store_true', default=False, help='create a copy of the input/output files of the last test case for manual testing and exit') parser.add_option('-x', '--auto-exit', dest='pause', action='store_false', default=True, help='do not wait for a key to be pressed after finishing testing') parser.add_option('-s', '--save-io', dest='erase', action='store_false', default=True, help='do not delete the copies of input/output files after the last test case; create copies of input files and store output in files even if the solution uses standard I/O; delete the stored input/output files if the solution uses standard I/O and the -c/--cleanup option is specified') @@ -89,20 +90,14 @@ # Do this check here so that if we have to warn them, we do it as early as possible 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.', file=sys.stderr) - sys.stderr.flush() - elif os.name == 'nt': - globalconf.pause = 'pause' - else: - sys.exit('Error: configuration variable pause is not defined and cannot be devised automatically.') + 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.', file=sys.stderr) + sys.stderr.flush() + elif os.name == 'nt': + globalconf.pause = 'pause' + else: + sys.exit('Error: configuration variable pause is not defined and cannot be devised automatically.') try: from problem import * @@ -116,39 +111,41 @@ globalconf.tasknames = os.path.curdir, else: globalconf.multiproblem = True - try: - shouldprintnames = len(globalconf.tasknames) > 1 - except Exception: - # Try to retrieve the first two problem names and cache them on success - globalconf.tasknames = iter(globalconf.tasknames) - try: - try: - first = next(globalconf.tasknames) - except NameError: - # Python 2.5 lacks the next() built-in - first = globalconf.tasknames.next() - except StopIteration: - globalconf.tasknames = () - shouldprintnames = False - else: - try: - try: - second = next(globalconf.tasknames) - except NameError: - second = globalconf.tasknames.next() - except StopIteration: - globalconf.tasknames = first, - shouldprintnames = False - else: - globalconf.tasknames = itertools.chain((first, second), globalconf.tasknames) - shouldprintnames = True + # TODO: erase the commented part? if it has a tasknames variable, it is by definition multi-problem + shouldprintnames = True + # try: + # shouldprintnames = len(globalconf.tasknames) > 1 + # except Exception: + # # Try to retrieve the first two problem names and cache them on success + # globalconf.tasknames = iter(globalconf.tasknames) + # try: + # try: + # first = next(globalconf.tasknames) + # except NameError: + # # Python 2.5 lacks the next() built-in + # first = globalconf.tasknames.next() + # except StopIteration: + # globalconf.tasknames = () + # shouldprintnames = False + # else: + # try: + # try: + # second = next(globalconf.tasknames) + # except NameError: + # second = globalconf.tasknames.next() + # except StopIteration: + # globalconf.tasknames = first, + # shouldprintnames = False + # else: + # globalconf.tasknames = itertools.chain((first, second), globalconf.tasknames) + # shouldprintnames = True ntasks = 0 nfulltasks = 0 maxscore = 0 realscore = 0 - for taskname in globalconf.tasknames: + for taskname in (globalconf.tasknames if not options.problems else options.problems): problem = Problem(taskname) if ntasks and not options.copyonly: say() @@ -177,10 +174,6 @@ say('Press any key to exit...') sys.stdout.flush() - #try: - # import msvcrt - # msvcrt.getch() - #except Exception: if pause: pause() elif callable(globalconf.pause): diff -r c23d81f4a1a3 -r b500e117080e 2.00/testcases.py --- a/2.00/testcases.py Thu Sep 23 00:11:24 2010 +0000 +++ b/2.00/testcases.py Thu Sep 23 23:05:58 2010 +0000 @@ -59,9 +59,9 @@ 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': + def canceled(select=select.select, stdin=sys.stdin, read=sys.stdin.read): + while select((stdin,), (), (), 0)[0]: + if read(1) == '\33': return True return False def init_canceled(): @@ -70,14 +70,14 @@ def pause(): sys.stdin.read(1) else: - def canceled(): - while msvcrt.kbhit(): - c = msvcrt.getch() + def canceled(kbhit=msvcrt.kbhit, getch=msvcrt.getch): + while kbhit(): + c = getch() if c == '\33': return True elif c == '\0': # Let's hope no-one is fiddling with this - msvcrt.getch() + getch() return False def init_canceled(): while msvcrt.kbhit(): @@ -298,29 +298,11 @@ # Compare the output with the reference output case.open_outfile() with case.outfile.open() as refoutput: - for line, refline in zip(output, refoutput): - if not isinstance(refline, basestring): + for line, refline in zip_longest(output, refoutput): + if refline is not None and not isinstance(refline, basestring): line = bytes(line, sys.getdefaultencoding()) if line != refline: raise WrongAnswer - try: - try: - next(output) - except NameError: - output.next() - except StopIteration: - pass - else: - raise WrongAnswer - try: - try: - next(refoutput) - except NameError: - refoutput.next() - except StopIteration: - pass - else: - raise WrongAnswer return 1 elif callable(case.validator): return case.validator(output) @@ -330,10 +312,12 @@ if case.problem.config.ansname: case.open_outfile() case.outfile.copy(case.problem.config.ansname) - case.process = Popen(case.validator, stdin=devnull, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=-1) + try: + case.process = Popen(case.validator, stdin=devnull, stdout=PIPE, stderr=STDOUT, universal_newlines=True, bufsize=-1) + except OSError: + raise CannotStartValidator(sys.exc_info()[1]) comment = case.process.communicate()[0].strip() - lower = comment.lower() - match = re.match(r'(ok|correct|wrong(?:(?:\s|_)*answer)?)(?:$|\s+|[.,!:]+\s*)', lower) + match = re.match(r'(?i)(ok|correct|wrong(?:(?:\s|_)*answer)?)(?:$|\s+|[.,!:]+\s*)', comment) if match: comment = comment[match.end():] if not case.problem.config.maxexitcode: @@ -375,11 +359,12 @@ # FIXME: 2.5 lacks the delete parameter with tempfile.NamedTemporaryFile(delete=False) as f: inputdatafname = f.name - context = CopyDeleting(case, case.infile, inputdatafname) + contextmgr = CopyDeleting(case, case.infile, inputdatafname) else: inputdatafname = case.problem.config.inname - context = Copying(case.infile, inputdatafname) - with context: + contextmgr = Copying(case.infile, inputdatafname) + with contextmgr: + # FIXME: this U doesn't do anything good for the child process, does it? 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: # TODO: make sure outfile.file is passed to Popen if needed @@ -393,14 +378,22 @@ except OSError: raise CannotStartTestee(sys.exc_info()[1]) case.time_started = clock() + time_next_check = case.time_started + .15 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 + # For some reason (probably Microsoft's fault), + # msvcrt.kbhit() is slow as hell + else: + if now >= time_next_check: + if canceled(): + raise CanceledByUser + else: + time_next_check = now + .15 + time.sleep(0) else: time_end = case.time_started + case.maxtime while True: @@ -410,8 +403,13 @@ break elif now >= time_end: raise TimeLimitExceeded - elif canceled(): - raise CanceledByUser + else: + if now >= time_next_check: + if canceled(): + raise CanceledByUser + else: + time_next_check = now + .15 + time.sleep(0) if config.globalconf.force_zero_exitcode and case.process.returncode: raise NonZeroExitCode(case.process.returncode) callback() @@ -430,14 +428,20 @@ except OSError: raise CannotStartTestee(sys.exc_info()[1]) case.time_started = clock() + time_next_check = case.time_started + .15 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: + if now >= time_next_check: + if canceled(): + raise CanceledByUser + else: + time_next_check = now + .15 + time.sleep(0) else: time_end = case.time_started + case.maxtime while True: @@ -447,8 +451,13 @@ break elif now >= time_end: raise TimeLimitExceeded - elif canceled(): - raise CanceledByUser + else: + if now >= time_next_check: + if canceled(): + raise CanceledByUser + else: + time_next_check = now + .15 + time.sleep(0) if config.globalconf.force_zero_exitcode and case.process.returncode: raise NonZeroExitCode(case.process.returncode) callback()