changeset 56:693a938bdeee

Accurate run-time reporting on POSIX
author Oleg Oshmyan <chortos@inbox.lv>
date Tue, 21 Dec 2010 02:24:18 +0200
parents c726235e49ee
children 855bdfeb32a6
files testcases.py
diffstat 1 files changed, 166 insertions(+), 83 deletions(-) [+]
line wrap: on
line diff
--- a/testcases.py	Mon Dec 20 23:03:28 2010 +0200
+++ b/testcases.py	Tue Dec 21 02:24:18 2010 +0200
@@ -49,7 +49,8 @@
 		import msvcrt
 	except ImportError:
 		try:
-			import select, termios, tty, atexit
+			from select import select
+			import termios, tty, atexit
 		except ImportError:
 			# It cannot be helped!
 			# Silently disable support for killing the program being tested
@@ -61,13 +62,13 @@
 			atexit.register(cleanup)
 			del cleanup
 			tty.setcbreak(sys.stdin.fileno())
-			def canceled(select=select.select, stdin=sys.stdin, read=sys.stdin.read):
+			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.select((sys.stdin,), (), (), 0)[0]:
+				while select((sys.stdin,), (), (), 0)[0]:
 					sys.stdin.read(1)
 			def pause():
 				sys.stdin.read(1)
@@ -87,6 +88,82 @@
 		def pause():
 			msvcrt.getch()
 
+try:
+	from signal import SIGCHLD, signal, SIG_DFL
+	from select import select, error as select_error
+	from errno import EINTR
+except ImportError:
+	try:
+		import ctypes
+		WaitForMultipleObjects = ctypes.windll.kernel32.WaitForMultipleObjects
+	except (ImportError, AttributeError):
+		pass
+	else:
+		# TODO: implement Win32 call()
+		pass
+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')
+		# FIXME: kill the process no matter what
+		try:
+			case.process = Popen(*args, **kwargs)
+		except OSError:
+			raise CannotStartTestee(sys.exc_info()[1])
+		case.time_started = clock()
+		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:
+				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
+		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)
+
 
 __all__ = ('TestCase', 'load_problem', 'TestCaseNotPassed',
            'TimeLimitExceeded', 'CanceledByUser', 'WrongAnswer',
@@ -369,48 +446,51 @@
 				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:
+							call(case.problem.config.path, case=case, stdin=infile, stdout=outfile, stderr=devnull, universal_newlines=True, bufsize=-1, preexec_fn=preexec_fn)
+						except NameError:
 							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)
+								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)
 						if config.globalconf.force_zero_exitcode and case.process.returncode:
 							raise NonZeroExitCode(case.process.returncode)
 						callback()
@@ -420,46 +500,49 @@
 		else:
 			case.infile.copy(case.problem.config.inname)
 			try:
+				call(case.problem.config.path, case=case, stdin=devnull, stdout=devnull, stderr=STDOUT, preexec_fn=preexec_fn)
+			except NameError:
 				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)
+					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)
 			if config.globalconf.force_zero_exitcode and case.process.returncode:
 				raise NonZeroExitCode(case.process.returncode)
 			callback()