view upreckon/unix.py @ 193:a76cdc26ba9d

Added conf. var. match and match='regexp' for non-archives Specify match='regexp', and your tests and dummies will be treated as regular expressions describing test case identifiers. Every file that is in a suitable location and whose name matches {testcase,dummy}inname and the given regexp will be treated as a file with test case input data. You are free to use backreferences in the regexps, but group numbering starts at two rather than one. If you want test groups, you can get them magically created for you by putting a part of the test ID in a group in the regexp sense and specifying the tests variable as a pair consisting of the regexp itself and the number of this regexp group (remember group numbers start at two).
author Oleg Oshmyan <chortos@inbox.lv>
date Thu, 11 Aug 2011 23:20:52 +0300
parents d5b6708c1955
children 65b5c9390010
line wrap: on
line source

# Copyright (c) 2010-2011 Chortos-2 <chortos@inbox.lv>

from __future__ import division, with_statement

from .compat import *
from .exceptions import *

from subprocess import Popen
import os, sys, time

if sys.platform.startswith('java'):
	from time import clock
else:
	from time import time as clock

try:
	from signal import SIGTERM, SIGKILL
except ImportError:
	SIGTERM = 15
	SIGKILL = 9

__all__ = 'call', 'kill', 'pause', 'clock'


try:
	from signal import SIGCHLD, SIG_DFL, signal, set_wakeup_fd
	from select import select, error as SelectError
	from errno import EAGAIN, EINTR
	from fcntl import fcntl, F_SETFD, F_GETFD, F_SETFL, F_GETFL
	from os import O_NONBLOCK
	try:
		import cPickle as pickle
	except ImportError:
		import pickle
except ImportError:
	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 case.maxwalltime:
			while True:
				exitcode, now = case.process.poll(), clock()
				if exitcode is not None:
					case.time_stopped = now
					break
				else:
					time.sleep(.001)
		else:
			time_end = case.time_started + case.maxwalltime
			while True:
				exitcode, now = case.process.poll(), clock()
				if exitcode is not None:
					case.time_stopped = now
					break
				elif now >= time_end:
					raise WallTimeLimitExceeded
				else:
					time.sleep(.001)
else:
	try:
		from fcntl import FD_CLOEXEC
	except ImportError:
		FD_CLOEXEC = 1
	
	try:
		from signal import siginterrupt
	except ImportError:
		# Sucks.
		siginterrupt = lambda signalnum, flag: None
	
	try:
		from resource import getrusage, RUSAGE_SELF, RUSAGE_CHILDREN
	except ImportError:
		from time import clock as cpuclock
		getrusage = lambda who: None
	else:
		def cpuclock():
			rusage = getrusage(RUSAGE_SELF)
			return rusage.ru_utime + rusage.ru_stime
	
	try:
		from resource import setrlimit
		try:
			from resource import RLIMIT_AS
		except ImportError:
			from resource import RLIMIT_VMEM as RLIMIT_AS
	except ImportError:
		setrlimit = None
	
	# Make SIGCHLD interrupt sleep() and select()
	sigchld_pipe_read, sigchld_pipe_write = os.pipe()
	fcntl(sigchld_pipe_read, F_SETFL,
	      fcntl(sigchld_pipe_read, F_GETFL) | O_NONBLOCK)
	fcntl(sigchld_pipe_write, F_SETFL,
	      fcntl(sigchld_pipe_write, F_GETFL) | O_NONBLOCK)
	def bury_child(signum, frame):
		try:
			bury_child.case.time_stopped = clock()
		except Exception:
			pass
	signal(SIGCHLD, bury_child)
	set_wakeup_fd(sigchld_pipe_write)
	class SignalIgnorer(object):
		def __enter__(self):
			signal(SIGCHLD, SIG_DFL)
		def __exit__(self, exc_type, exc_value, traceback):
			signal(SIGCHLD, bury_child)
	signal_ignorer = SignalIgnorer()
	__all__ += 'signal_ignorer',
	
	# If you want this to work portably, don't set any stdio argument to PIPE
	def call(*args, **kwargs):
		global last_rusage
		bury_child.case = case = kwargs.pop('case')
		read, write = os.pipe()
		fcntl(write, F_SETFD, fcntl(write, F_GETFD) | FD_CLOEXEC)
		def preexec_fn():
			os.close(read)
			if setrlimit and case.maxmemory:
				maxmemory = ceil(case.maxmemory * 1048576)
				setrlimit(RLIMIT_AS, (maxmemory, maxmemory))
				# I would also set a CPU time limit but I do not want the time
				# passing between the calls to fork and exec to be counted in
			os.write(write, pickle.dumps((clock(), cpuclock()), 1))
		kwargs['preexec_fn'] = preexec_fn
		# So how the hell do I actually make use of pass_fds?
		# On 3.1-, calling Popen with pass_fds prints an exception
		# from Popen.__del__ to stderr. On 3.2, Popen without close_fds
		# or pass_fds creates a child and fails but that of course
		# generates a SIGCHLD, which causes problems, and I have
		# no process ID to wait upon to negate the changes made
		# by the SIGCHLD handler.
		kwargs['close_fds'] = False
		old_rusage = getrusage(RUSAGE_CHILDREN)
		last_rusage = None
		while True:
			try:
				os.read(sigchld_pipe_read, 512)
			except OSError:
				if sys.exc_info()[1].errno == EAGAIN:
					break
				else:
					raise
		siginterrupt(SIGCHLD, False)
		try:
			case.process = Popen(*args, **kwargs)
		except OSError:
			os.close(read)
			raise CannotStartTestee(sys.exc_info()[1])
		finally:
			siginterrupt(SIGCHLD, True)
			os.close(write)
		try:
			if not catch_escape:
				if case.maxwalltime:
					try:
						select((sigchld_pipe_read,), (), (), case.maxwalltime)
					except SelectError:
						if sys.exc_info()[1].args[0] != EINTR:
							raise
					# subprocess in Python 2.6- is not guarded against EINTR
					try:
						if case.process.poll() is None:
							raise WallTimeLimitExceeded
					except OSError:
						if sys.exc_info()[1].errno != EINTR:
							raise
						else:
							case.process.poll()
				else:
					wait(case.process)
			else:
				if not case.maxwalltime:
					try:
						while case.process.poll() is None:
							s = select((sys.stdin, sigchld_pipe_read), (), ())
							if (s[0] == [sys.stdin] and
							    sys.stdin.read(1) == '\33'):
								raise CanceledByUser
					except (SelectError, IOError, OSError):
						if sys.exc_info()[1].args[0] != EINTR:
							raise
						else:
							case.process.poll()
				else:
					time_end = clock() + case.maxwalltime
					try:
						while case.process.poll() is None:
							remaining = time_end - clock()
							if remaining > 0:
								s = select((sys.stdin, sigchld_pipe_read),
								           (), (), remaining)
								if (s[0] == [sys.stdin] and
								    sys.stdin.read(1) == '\33'):
									raise CanceledByUser
							else:
								raise WallTimeLimitExceeded
					except (SelectError, IOError, OSError):
						if sys.exc_info()[1].args[0] != EINTR:
							raise
						else:
							case.process.poll()
		finally:
			case.time_started, cpustart = pickle.loads(os.read(read, 512))
			os.close(read)
			del bury_child.case
		new_rusage = getrusage(RUSAGE_CHILDREN)
		if (case.maxwalltime and
		    case.time_stopped - case.time_started > case.maxwalltime):
			raise WallTimeLimitExceeded
		if new_rusage:
			time_started = old_rusage.ru_utime + old_rusage.ru_stime + cpustart
			time_stopped = new_rusage.ru_utime + new_rusage.ru_stime
			# Yes, this actually happens
			if time_started > time_stopped:
				time_started = time_stopped
			if case.maxcputime or not case.maxwalltime:
				case.time_started = time_started
				case.time_stopped = time_stopped
				case.time_limit_string = case.cpu_time_limit_string
				if (case.maxcputime and
				    time_stopped - time_started > case.maxcputime):
					raise CPUTimeLimitExceeded
		if case.maxmemory:
			if sys.platform != 'darwin':
				maxrss = case.maxmemory * 1024
			else:
				maxrss = case.maxmemory * 1048576
			if last_rusage and last_rusage.ru_maxrss > maxrss:
				raise MemoryLimitExceeded
			elif (new_rusage and
			      new_rusage.ru_maxrss > old_rusage.ru_maxrss and
			      new_rusage.ru_maxrss > maxrss):
				raise MemoryLimitExceeded

# 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:
		# Python 2.5 again
		Popen._internal_poll.im_func.func_defaults = defaults
except (AttributeError, ValueError):
	pass


def kill(process):
	try:
		process.kill()
	except AttributeError:
		os.kill(process.pid, SIGKILL)
	wait(process)


# subprocess in Python 2.6- is not guarded against EINTR
try:
	from errno import EINTR
except ImportError:
	wait = Popen.wait
else:
	def wait(process):
		while True:
			try:
				return process.wait()
			except OSError:
				if sys.exc_info()[1].errno != EINTR:
					raise


try:
	from ._unix import *
except ImportError:
	if not sys.stdin.isatty():
		pause = lambda: sys.stdin.read(1)
		catch_escape = False
	else:
		try:
			from select import select
			import termios, tty, atexit
		except ImportError:
			pause = lambda: sys.stdin.read(1)
			catch_escape = False
		else:
			catch_escape = True
			def cleanup(old=termios.tcgetattr(sys.stdin.fileno())):
				termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, old)
			atexit.register(cleanup)
			tty.setcbreak(sys.stdin.fileno())
			def pause():
				sys.stdin.read(1)