Add script invocation to ScriptQuestion
[quizzer.git] / quizzer / question.py
1 import logging as _logging
2 import tempfile as _tempfile
3
4 from . import error as _error
5 from . import util as _util
6
7
8 LOG = _logging.getLogger(__name__)
9 QUESTION_CLASS = {}
10
11
12 def register_question(question_class):
13     QUESTION_CLASS[question_class.__name__] = question_class
14
15
16 class Question (object):
17     _state_attributes = [
18         'id',
19         'prompt',
20         'answer',
21         'help',
22         'dependencies',
23         ]
24
25     def __init__(self, **kwargs):
26         self.__setstate__(kwargs)
27
28     def __str__(self):
29         return '<{} id:{!r}>'.format(type(self).__name__, self.id)
30
31     def __repr__(self):
32         return '<{} id:{!r} at {:#x}>'.format(
33             type(self).__name__, self.id, id(self))
34
35     def __getstate__(self):
36         return {attr: getattr(self, attr)
37                 for attr in self._state_attributes} 
38
39     def __setstate__(self, state):
40         if 'id' not in state:
41             state['id'] = state.get('prompt', None)
42         if 'dependencies' not in state:
43             state['dependencies'] = []
44         for attr in self._state_attributes:
45             if attr not in state:
46                 state[attr] = None
47         self.__dict__.update(state)
48
49     def check(self, answer):
50         return answer == self.answer
51
52
53 class NormalizedStringQuestion (Question):
54     def normalize(self, string):
55         return string.strip().lower()
56
57     def check(self, answer):
58         return self.normalize(answer) == self.normalize(self.answer)
59
60
61 class ScriptQuestion (Question):
62     _state_attributes = Question._state_attributes + [
63         'interpreter',
64         'setup',
65         'teardown',
66         'timeout',
67         ]
68
69     def __setstate__(self, state):
70         if 'interpreter' not in state:
71             state['interpreter'] = 'sh'  # POSIX-compatible shell
72         if 'timeout' not in state:
73             state['timeout'] = 3
74         for key in ['setup', 'teardown']:
75             if key not in state:
76                 state[key] = []
77         super(ScriptQuestion, self).__setstate__(state)
78
79     def check(self, answer):
80         # figure out the expected values
81         e_status,e_stdout,e_stderr = self._invoke(self.answer)
82         # get values for the user-supplied answer
83         try:
84             a_status,a_stdout,a_stderr = self._invoke(answer)
85         except _error.CommandError as e:
86             LOG.warning(e)
87             return False
88         for (name, e, a) in [
89                 ('stderr', e_stderr, a_stderr),
90                 ('status', e_status, a_status),
91                 ('stdout', e_stdout, a_stdout),
92                 ]:
93             if a != e:
94                 if name == 'status':
95                     LOG.info(
96                         'missmatched {}, expected {!r} but got {!r}'.format(
97                             name, e, a))
98                 else:
99                     LOG.info('missmatched {}, expected:'.format(name))
100                     LOG.info(e)
101                     LOG.info('but got:')
102                     LOG.info(a)
103                 return False
104         return True
105
106     def _invoke(self, answer):
107         with _tempfile.TemporaryDirectory(
108                 prefix='{}-'.format(type(self).__name__),
109                 ) as tempdir:
110             script = '\n'.join(self.setup + [answer] + self.teardown)
111             return _util.invoke(
112                 args=[self.interpreter],
113                 stdin=script,
114                 cwd=tempdir,
115                 universal_newlines=True,
116                 timeout=self.timeout,)
117
118 for name,obj in list(locals().items()):
119     if name.startswith('_'):
120         continue
121     try:
122         subclass = issubclass(obj, Question)
123     except TypeError:  # obj is not a class
124         continue
125     if subclass:
126         register_question(obj)
127 del name, obj