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: