changeset 25:b500e117080e

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.
author Oleg Oshmyan <chortos@inbox.lv>
date Thu, 23 Sep 2010 23:05:58 +0000
parents c23d81f4a1a3
children 5bbb68833868
files 2.00/compat.py 2.00/config.py 2.00/test-svn.py 2.00/testcases.py
diffstat 4 files changed, 180 insertions(+), 131 deletions(-) [+]
line wrap: on
line diff
--- 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
--- 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
--- 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):
--- 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()