Mercurial > ~astiob > upreckon > hgweb
diff testcases.py @ 82:06356af50bf9
Finished testcases reorganization and CPU time limit implementation
We now have:
* Win32-specific code in the win32 module (including bug fixes),
* UNIX-specific and generic code in the unix module,
* a much cleaner testcases module,
* wait4-based resource limits working on Python 3 (this is a bug fix),
* no warning/error reported on non-Win32 when -x is not passed
but standard input does not come from a terminal,
* the maxtime configuration variable replaced with two new variables
named maxcputime and maxwalltime,
* CPU time reported if it can be determined unless an error occurs sooner
than it is determined (e. g. if the wall-clock time limit is exceeded),
* memory limits enforced even if Upreckon's forking already breaks them,
* CPU time limits and private virtual memory limits honoured on Win32,
* CPU time limits honoured on UNIX(-like) platforms supporting wait4
or getrusage,
* address space limits honoured on UNIX(-like) platforms supporting
setrlimit with RLIMIT_AS/RLIMIT_VMEM,
* resident set size limits honoured on UNIX(-like) platforms supporting
wait4.
author | Oleg Oshmyan <chortos@inbox.lv> |
---|---|
date | Wed, 23 Feb 2011 23:35:27 +0000 |
parents | 24752db487c5 |
children | 37c4ad87583c |
line wrap: on
line diff
--- a/testcases.py Wed Feb 16 15:30:57 2011 +0000 +++ b/testcases.py Wed Feb 23 23:35:27 2011 +0000 @@ -21,290 +21,17 @@ devnull = open(os.path.devnull, 'w+') try: - from signal import SIGTERM, SIGKILL -except ImportError: - SIGTERM = 15 - SIGKILL = 9 - -try: - from _subprocess import TerminateProcess -except ImportError: - # CPython 2.5 does define _subprocess.TerminateProcess even though it is - # not used in the subprocess module, but maybe something else does not - try: - import ctypes - TerminateProcess = ctypes.windll.kernel32.TerminateProcess - except (ImportError, AttributeError): - TerminateProcess = None - - -# Do not show error messages due to errors in the program being tested -try: - import ctypes - try: - errmode = ctypes.windll.kernel32.GetErrorMode() - except AttributeError: - errmode = ctypes.windll.kernel32.SetErrorMode(0) - errmode |= 0x8003 - ctypes.windll.kernel32.SetErrorMode(errmode) + from win32 import * except Exception: - pass - - -# 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: - from select import select - import 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(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(): - while select((sys.stdin,), (), (), 0)[0]: - sys.stdin.read(1) - def pause(): - sys.stdin.read(1) - else: - 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 - getch() - return False - def init_canceled(): - while msvcrt.kbhit(): - msvcrt.getch() - def pause(): - msvcrt.getch() - -try: - from signal import SIGCHLD, signal, SIG_DFL - from select import select, error as select_error - from errno import EINTR - import fcntl - try: - import cPickle as pickle - except ImportError: - import pickle -except ImportError: - try: - from _subprocess import WAIT_OBJECT_0, STD_INPUT_HANDLE, INFINITE - except ImportError: - WAIT_OBJECT_0 = 0 - STD_INPUT_HANDLE = -10 - INFINITE = -1 - try: - import ctypes - SetConsoleMode = ctypes.windll.kernel32.SetConsoleMode - FlushConsoleInputBuffer = ctypes.windll.kernel32.FlushConsoleInputBuffer - WaitForMultipleObjects = ctypes.windll.kernel32.WaitForMultipleObjects - ReadConsoleInputA = ctypes.windll.kernel32.ReadConsoleInputA - try: - from _subprocess import GetStdHandle - except ImportError: - GetStdHandle = ctypes.windll.kernel32.GetStdHandle - except (ImportError, AttributeError): - console_input = False - else: - hStdin = GetStdHandle(STD_INPUT_HANDLE) - console_input = bool(SetConsoleMode(hStdin, 1)) - if console_input: - FlushConsoleInputBuffer(hStdin) - class KEY_EVENT_RECORD(ctypes.Structure): - _fields_ = (("bKeyDown", ctypes.c_int), - ("wRepeatCount", ctypes.c_ushort), - ("wVirtualKeyCode", ctypes.c_ushort), - ("wVirtualScanCode", ctypes.c_ushort), - ("UnicodeChar", ctypes.c_wchar), - ("dwControlKeyState", ctypes.c_uint)) - class INPUT_RECORD(ctypes.Structure): - _fields_ = (("EventType", ctypes.c_int), - ("KeyEvent", KEY_EVENT_RECORD)) - # Memory limits (currently) are not supported - def call(*args, **kwargs): - case = kwargs.pop('case') - try: - case.process = Popen(*args, **kwargs) - except OSError: - raise CannotStartTestee(sys.exc_info()[1]) - case.time_started = clock() - if not console_input: - if case.maxtime: - if WaitForSingleObject(case.process._handle, int(case.maxtime * 1000)) != WAIT_OBJECT_0: - raise TimeLimitExceeded - else: - case.process.wait() - else: - ir = INPUT_RECORD() - n = ctypes.c_int() - lpHandles = (ctypes.c_int * 2)(hStdin, case.process._handle) - if case.maxtime: - time_end = clock() + case.maxtime - while case.process.poll() is None: - remaining = time_end - clock() - if remaining > 0: - if WaitForMultipleObjects(2, lpHandles, False, int(remaining * 1000)) == WAIT_OBJECT_0: - ReadConsoleInputA(hStdin, ctypes.byref(ir), 1, ctypes.byref(n)) - if ir.EventType == 1 and ir.KeyEvent.bKeyDown and ir.KeyEvent.wVirtualKeyCode == 27: - raise CanceledByUser - else: - raise TimeLimitExceeded - else: - while case.process.poll() is None: - if WaitForMultipleObjects(2, lpHandles, False, INFINITE) == WAIT_OBJECT_0: - ReadConsoleInputA(hStdin, ctypes.byref(ir), 1, ctypes.byref(n)) - if ir.EventType == 1 and ir.KeyEvent.bKeyDown and ir.KeyEvent.wVirtualKeyCode == 27: - raise CanceledByUser - case.time_stopped = clock() - if not console_input: - try: - try: - from _subprocess import WaitForSingleObject - except ImportError: - import ctypes - WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject - except (ImportError, AttributeError): - # TODO: move the default implementation here - call = None -else: - # Make SIGCHLD interrupt sleep() and select() - def bury_child(signum, frame): - try: - bury_child.case.time_stopped = clock() - except Exception: - pass - signal(SIGCHLD, bury_child) - - # If you want this to work, don't set any stdio argument to PIPE - def call_real(*args, **kwargs): - bury_child.case = case = kwargs.pop('case') - preexec_fn_ = kwargs.get('preexec_fn', None) - read, write = os.pipe() - def preexec_fn(): - os.close(read) - if preexec_fn_: - preexec_fn_() - fcntl.fcntl(write, fcntl.F_SETFD, fcntl.fcntl(write, fcntl.F_GETFD) | getattr(fcntl, 'FD_CLOEXEC', 1)) - fwrite = os.fdopen(write, 'wb') - pickle.dump(clock(), fwrite, 1) - kwargs['preexec_fn'] = preexec_fn - try: - case.process = Popen(*args, **kwargs) - except OSError: - os.close(read) - raise CannotStartTestee(sys.exc_info()[1]) - finally: - os.close(write) - try: - if pause is None: - if case.maxtime: - time.sleep(case.maxtime) - if case.process.poll() is None: - raise TimeLimitExceeded - else: - case.process.wait() - else: - if not case.maxtime: - try: - while case.process.poll() is None: - if select((sys.stdin,), (), ())[0]: - if sys.stdin.read(1) == '\33': - raise CanceledByUser - except select_error: - if sys.exc_info()[1].args[0] != EINTR: - raise - else: - case.process.poll() - else: - time_end = clock() + case.maxtime - try: - while case.process.poll() is None: - remaining = time_end - clock() - if remaining > 0: - if select((sys.stdin,), (), (), remaining)[0]: - if sys.stdin.read(1) == '\33': - raise CanceledByUser - else: - raise TimeLimitExceeded - except select_error: - if sys.exc_info()[1].args[0] != EINTR: - raise - else: - case.process.poll() - finally: - case.time_started = pickle.loads(os.read(read, 512)) - os.close(read) - del bury_child.case - def call(*args, **kwargs): - if 'preexec_fn' in kwargs: - try: - return call_real(*args, **kwargs) - except MemoryError: - # If there is not enough memory for the forked test.py, - # opt for silent dropping of the limit - # TODO: show a warning somewhere - del kwargs['preexec_fn'] - return call_real(*args, **kwargs) - else: - return call_real(*args, **kwargs) - -# Emulate memory limits on platforms compatible with 4.3BSD but not XSI -# I say 'emulate' because the OS will allow excessive memory usage -# anyway; Upreckon will just treat the test case as not passed. -# To do this, we not only require os.wait4 to be present but also -# assume things about the implementation of subprocess.Popen. -try: - def waitpid_emu(pid, options, _wait4=os.wait4): - global last_rusage - pid, status, last_rusage = _wait4(pid, options) - return pid, status - _waitpid = os.waitpid - os.waitpid = waitpid_emu - try: - defaults = Popen._internal_poll.__func__.__defaults__ - except AttributeError: - # Python 2.5 - defaults = Popen._internal_poll.im_func.func_defaults - i = defaults.index(_waitpid) - defaults = defaults[:i] + (waitpid_emu,) + defaults[i+1:] - try: - Popen._internal_poll.__func__.__defaults__ = defaults - except AttributeError: - pass - Popen._internal_poll.im_func.func_defaults = defaults -except (AttributeError, ValueError): - pass + from unix import * __all__ = ('TestCase', 'load_problem', 'TestCaseNotPassed', 'TimeLimitExceeded', 'CanceledByUser', 'WrongAnswer', 'NonZeroExitCode', 'CannotStartTestee', 'CannotStartValidator', 'CannotReadOutputFile', 'CannotReadInputFile', 'CannotReadAnswerFile', - 'MemoryLimitExceeded') + 'MemoryLimitExceeded', 'CPUTimeLimitExceeded', + 'WallTimeLimitExceeded') @@ -312,6 +39,8 @@ class TestCaseNotPassed(Exception): __slots__ = () class TimeLimitExceeded(TestCaseNotPassed): __slots__ = () +class CPUTimeLimitExceeded(TimeLimitExceeded): __slots__ = () +class WallTimeLimitExceeded(TimeLimitExceeded): __slots__ = () class MemoryLimitExceeded(TestCaseNotPassed): __slots__ = () class CanceledByUser(TestCaseNotPassed): __slots__ = () @@ -384,9 +113,11 @@ class TestCase(object): __slots__ = ('problem', 'id', 'isdummy', 'infile', 'outfile', 'points', - 'process', 'time_started', 'time_stopped', 'time_limit_string', - 'realinname', 'realoutname', 'maxtime', 'maxmemory', - 'has_called_back', 'files_to_delete') + 'process', 'time_started', 'time_stopped', + 'realinname', 'realoutname', 'maxcputime', 'maxwalltime', + 'maxmemory', 'has_called_back', 'files_to_delete', + 'cpu_time_limit_string', 'wall_time_limit_string', + 'time_limit_string') if ABCMeta: __metaclass__ = ABCMeta @@ -396,12 +127,17 @@ case.id = id case.isdummy = isdummy case.points = points - case.maxtime = case.problem.config.maxtime + case.maxcputime = case.problem.config.maxcputime + case.maxwalltime = case.problem.config.maxwalltime case.maxmemory = case.problem.config.maxmemory - if case.maxtime: - case.time_limit_string = '/%.3f' % case.maxtime + if case.maxcputime: + case.cpu_time_limit_string = '/%.3f' % case.maxcputime else: - case.time_limit_string = '' + case.cpu_time_limit_string = '' + if case.maxwalltime: + case.wall_time_limit_string = '/%.3f' % case.maxwalltime + else: + case.wall_time_limit_string = '' if not isdummy: case.realinname = case.problem.config.testcaseinname case.realoutname = case.problem.config.testcaseoutname @@ -415,13 +151,14 @@ def __call__(case, callback): case.has_called_back = False case.files_to_delete = [] + case.time_limit_string = case.wall_time_limit_string try: return case.test(callback) finally: now = clock() - if not getattr(case, 'time_started', None): + if getattr(case, 'time_started', None) is None: case.time_started = case.time_stopped = now - elif not getattr(case, 'time_stopped', None): + elif getattr(case, 'time_stopped', None) is None: case.time_stopped = now if not case.has_called_back: callback() @@ -432,21 +169,11 @@ # case.infile.close() #if getattr(case, 'outfile', None): # case.outfile.close() - if getattr(case, 'process', None): - # Try killing after three unsuccessful TERM attempts in a row - # (except on Windows, where TERMing is killing) + if getattr(case, 'process', None) and case.process.returncode is None: + # Try KILLing after three unsuccessful TERM attempts in a row for i in range(3): try: - try: - case.process.terminate() - except AttributeError: - # Python 2.5 - if TerminateProcess and hasattr(proc, '_handle'): - # Windows API - TerminateProcess(proc._handle, 1) - else: - # POSIX - os.kill(proc.pid, SIGTERM) + terminate(case.process) except Exception: time.sleep(0) case.process.poll() @@ -458,16 +185,7 @@ # just silently stop trying for i in range(3): try: - try: - case.process.kill() - except AttributeError: - # Python 2.5 - if TerminateProcess and hasattr(proc, '_handle'): - # Windows API - TerminateProcess(proc._handle, 1) - else: - # POSIX - os.kill(proc.pid, SIGKILL) + kill(case.process) except Exception: time.sleep(0) case.process.poll() @@ -547,28 +265,8 @@ __slots__ = () def test(case, callback): - init_canceled() - if sys.platform == 'win32' or not case.maxmemory: - preexec_fn = None - else: - def preexec_fn(): - try: - import resource - maxmemory = int(case.maxmemory * 1048576) - resource.setrlimit(resource.RLIMIT_AS, (maxmemory, maxmemory)) - # I would also set a CPU time limit but I do not want the time - # that passes between the calls to fork and exec to be counted in - except MemoryError: - # We do not have enough memory for ourselves; - # let the parent know about this - raise - except Exception: - # Well, at least we tried - pass case.open_infile() case.time_started = None - global last_rusage - last_rusage = None if case.problem.config.stdio: if options.erase and not case.validator or not case.problem.config.inname: # TODO: re-use the same file name if possible @@ -582,110 +280,18 @@ with contextmgr: with open(inputdatafname) as infile: with tempfile.TemporaryFile('w+') if options.erase and not case.validator else open(case.problem.config.outname, 'w+') as outfile: - if call is not None: - call(case.problem.config.path, case=case, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1, preexec_fn=preexec_fn) - else: - try: - 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 - # TODO: show a warning somewhere - 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() - 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 - # 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(.001) - 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 - else: - if now >= time_next_check: - if canceled(): - raise CanceledByUser - else: - time_next_check = now + .15 - time.sleep(.001) + call(case.problem.config.path, case=case, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1) if config.globalconf.force_zero_exitcode and case.process.returncode or case.process.returncode < 0: raise NonZeroExitCode(case.process.returncode) - if case.maxmemory and last_rusage and last_rusage.ru_maxrss > case.maxmemory * (1024 if sys.platform != 'darwin' else 1048576): - raise MemoryLimitExceeded callback() case.has_called_back = True outfile.seek(0) return case.validate(outfile) else: case.infile.copy(case.problem.config.inname) - if call is not None: - call(case.problem.config.path, case=case, stdin=devnull, stdout=devnull, stderr=STDOUT, preexec_fn=preexec_fn) - else: - try: - try: - case.process = Popen(case.problem.config.path, stdin=devnull, stdout=devnull, stderr=STDOUT, preexec_fn=preexec_fn) - except MemoryError: - # If there is not enough memory for the forked test.py, - # opt for silent dropping of the limit - # TODO: show a warning somewhere - case.process = Popen(case.problem.config.path, stdin=devnull, stdout=devnull, stderr=STDOUT) - 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 - else: - if now >= time_next_check: - if canceled(): - raise CanceledByUser - else: - time_next_check = now + .15 - time.sleep(.001) - 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 - else: - if now >= time_next_check: - if canceled(): - raise CanceledByUser - else: - time_next_check = now + .15 - time.sleep(.001) + call(case.problem.config.path, case=case, stdin=devnull, stdout=devnull, stderr=STDOUT) if config.globalconf.force_zero_exitcode and case.process.returncode or case.process.returncode < 0: raise NonZeroExitCode(case.process.returncode) - if case.maxmemory and last_rusage and last_rusage.ru_maxrss > case.maxmemory * (1024 if sys.platform != 'darwin' else 1048576): - raise MemoryLimitExceeded callback() case.has_called_back = True with open(case.problem.config.outname, 'rU') as output: