From 8a57524850deb563111da68779fb571885e1be19 Mon Sep 17 00:00:00 2001 From: "W. Trevor King" Date: Tue, 5 Feb 2013 13:46:31 -0500 Subject: [PATCH] Add script invocation to ScriptQuestion We need Python >= 3.3 for the `timeout` argument to Popen.communicate(). --- pq.py | 2 +- quizzer/__init__.py | 3 ++- quizzer/error.py | 12 ++++++++++ quizzer/question.py | 51 +++++++++++++++++++++++++++++++++++++++--- quizzer/ui/__init__.py | 3 ++- quizzer/util.py | 35 +++++++++++++++++++++++++++++ 6 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 quizzer/error.py create mode 100644 quizzer/util.py diff --git a/pq.py b/pq.py index 5cdd0bb..d595691 100755 --- a/pq.py +++ b/pq.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3.3 import quizzer.cli diff --git a/quizzer/__init__.py b/quizzer/__init__.py index 3a4b731..c350f0f 100644 --- a/quizzer/__init__.py +++ b/quizzer/__init__.py @@ -7,4 +7,5 @@ import logging as _logging __version__ = '0.1' LOG = _logging.getLogger(__name__) -LOG.setLevel(_logging.ERROR) +LOG.setLevel(_logging.DEBUG) +LOG.addHandler(_logging.StreamHandler()) diff --git a/quizzer/error.py b/quizzer/error.py new file mode 100644 index 0000000..f16be24 --- /dev/null +++ b/quizzer/error.py @@ -0,0 +1,12 @@ +class CommandError (RuntimeError): + def __init__(self, arguments, stdin=None, stdout=None, stderr=None, + status=None, msg=None): + error_msg = 'error executing {}'.format(arguments) + if msg: + error_msg = '{}: {}'.format(error_msg, msg) + super(CommandError, self).__init__(error_msg) + self.arguments = arguments + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.status = status diff --git a/quizzer/question.py b/quizzer/question.py index 1783d9c..2dd57e9 100644 --- a/quizzer/question.py +++ b/quizzer/question.py @@ -1,3 +1,11 @@ +import logging as _logging +import tempfile as _tempfile + +from . import error as _error +from . import util as _util + + +LOG = _logging.getLogger(__name__) QUESTION_CLASS = {} @@ -55,20 +63,57 @@ class ScriptQuestion (Question): 'interpreter', 'setup', 'teardown', + 'timeout', ] def __setstate__(self, state): if 'interpreter' not in state: state['interpreter'] = 'sh' # POSIX-compatible shell + if 'timeout' not in state: + state['timeout'] = 3 for key in ['setup', 'teardown']: if key not in state: state[key] = [] super(ScriptQuestion, self).__setstate__(state) def check(self, answer): - script = '\n'.join(self.setup + [answer] + self.teardown) - raise ValueError(script) - + # figure out the expected values + e_status,e_stdout,e_stderr = self._invoke(self.answer) + # get values for the user-supplied answer + try: + a_status,a_stdout,a_stderr = self._invoke(answer) + except _error.CommandError as e: + LOG.warning(e) + return False + for (name, e, a) in [ + ('stderr', e_stderr, a_stderr), + ('status', e_status, a_status), + ('stdout', e_stdout, a_stdout), + ]: + if a != e: + if name == 'status': + LOG.info( + 'missmatched {}, expected {!r} but got {!r}'.format( + name, e, a)) + else: + LOG.info('missmatched {}, expected:'.format(name)) + LOG.info(e) + LOG.info('but got:') + LOG.info(a) + return False + return True + + def _invoke(self, answer): + with _tempfile.TemporaryDirectory( + prefix='{}-'.format(type(self).__name__), + ) as tempdir: + script = '\n'.join(self.setup + [answer] + self.teardown) + return _util.invoke( + args=[self.interpreter], + stdin=script, + cwd=tempdir, + universal_newlines=True, + timeout=self.timeout,) for name,obj in list(locals().items()): if name.startswith('_'): diff --git a/quizzer/ui/__init__.py b/quizzer/ui/__init__.py index 5bbb214..caf2a71 100644 --- a/quizzer/ui/__init__.py +++ b/quizzer/ui/__init__.py @@ -9,7 +9,8 @@ class UserInterface (object): answers = _answerdb.AnswerDatabase() self.answers = answers if stack is None: - stack = quiz.leaf_questions() + stack = self.answers.get_never_correctly_answered( + questions=quiz.leaf_questions()) self.stack = stack def run(self): diff --git a/quizzer/util.py b/quizzer/util.py new file mode 100644 index 0000000..a985631 --- /dev/null +++ b/quizzer/util.py @@ -0,0 +1,35 @@ +import subprocess as _subprocess + +from . import error as _error + + +def invoke(args, stdin=None, universal_newlines=False, timeout=None, + expect=None, **kwargs): + if stdin: + stdin_pipe = _subprocess.PIPE + else: + stdin_pipe = None + try: + p = _subprocess.Popen( + args, stdin=stdin_pipe, stdout=_subprocess.PIPE, + stderr=_subprocess.PIPE, universal_newlines=universal_newlines, + **kwargs) + except FileNotFoundError as e: + raise _error.CommandError(arguments=args, stdin=stdin) from e + try: + stdout,stderr = p.communicate(input=stdin, timeout=timeout) + except _subprocess.TimeoutExpired as e: + p.kill() + stdout,stderr = p.communicate() + status = p.wait() + raise _error.CommandError( + msg='timeout ({}s) expired'.format(timeout), + arguments=args, stdin=stdin, stdout=stdout, stderr=stderr, + status=status) from e + status = p.wait() + if expect and status not in expect: + raise _error.CommandError( + msg='unexpected exit status ({} not in {})'.format(status, expect), + args=args, stdin=stdin, stdout=stdout, stderr=stderr, + status=status) + return (status, stdout, stderr) -- 2.26.2