294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
1 |
# IVLE - Informatics Virtual Learning Environment
|
2 |
# Copyright (C) 2007-2008 The University of Melbourne
|
|
3 |
#
|
|
4 |
# This program is free software; you can redistribute it and/or modify
|
|
5 |
# it under the terms of the GNU General Public License as published by
|
|
6 |
# the Free Software Foundation; either version 2 of the License, or
|
|
7 |
# (at your option) any later version.
|
|
8 |
#
|
|
9 |
# This program is distributed in the hope that it will be useful,
|
|
10 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12 |
# GNU General Public License for more details.
|
|
13 |
#
|
|
14 |
# You should have received a copy of the GNU General Public License
|
|
15 |
# along with this program; if not, write to the Free Software
|
|
16 |
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
17 |
||
18 |
# Module: TestFramework
|
|
19 |
# Author: Dilshan Angampitiya
|
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
20 |
# Steven Bird (revisions)
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
21 |
# Date: 24/1/2008
|
22 |
||
23 |
# Brief description of the Module# define custom exceptions
|
|
24 |
# use exceptions for all errors found in testing
|
|
25 |
||
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
26 |
import sys, copy |
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
27 |
import types |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
28 |
|
1079
by William Grant
Merge setup-refactor branch. This completely breaks existing installations; |
29 |
from ivle import testfilespace |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
30 |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
31 |
# student error or author error
|
32 |
# errors in student code get handled internally
|
|
33 |
# errors in solution code get passed up
|
|
34 |
class ScriptExecutionError(Exception): |
|
35 |
"""Runtime error in the student code or solution code"""
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
36 |
def __init__(self, exc_info): |
37 |
cla, exc, trbk = exc_info |
|
38 |
self._name = cla.__name__ |
|
39 |
self._detail = str(exc) |
|
310
by dilshan_a
Add lineno to exception info. |
40 |
self._trbk = trbk |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
41 |
|
42 |
def is_critical(self): |
|
43 |
if ( self._name == 'FunctionNotFoundError' |
|
44 |
or self._name == 'SyntaxError' |
|
45 |
or self._name == 'IndentationError'): |
|
46 |
return True |
|
47 |
else: |
|
48 |
return False |
|
49 |
||
50 |
def to_dict(self): |
|
310
by dilshan_a
Add lineno to exception info. |
51 |
import traceback |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
52 |
return {'name': self._name, |
53 |
'detail': self._detail, |
|
311
by dilshan_a
Last commit was buggy. |
54 |
'critical': self.is_critical(), |
310
by dilshan_a
Add lineno to exception info. |
55 |
'lineno': traceback.tb_lineno(self._trbk) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
56 |
}
|
57 |
||
58 |
def __str__(self): |
|
59 |
return self._name + " - " + str(self._detail) |
|
60 |
||
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
61 |
# author error
|
62 |
class TestCreationError(Exception): |
|
63 |
"""An error occured while creating the test suite or one of its components"""
|
|
64 |
def __init__(self, reason): |
|
65 |
self._reason = reason |
|
66 |
||
67 |
def __str__(self): |
|
68 |
return self._reason |
|
69 |
||
70 |
# author error
|
|
71 |
class TestError(Exception): |
|
72 |
"""Runtime error in the testing framework outside of the provided or student code"""
|
|
73 |
def __init__(self, exc_info): |
|
74 |
cla, exc, trbk = exc_info |
|
75 |
self._name = cla.__name__ |
|
76 |
self._detail = str(exc) |
|
77 |
self._exc_info = exc_info |
|
78 |
||
79 |
def exc_info(self): |
|
80 |
return self._exc_info |
|
81 |
||
82 |
def __str__(self): |
|
83 |
return "Error testing solution against attempt: %s - %s" %(self._name, self._detail) |
|
84 |
||
85 |
# author error
|
|
86 |
# raised when expected file not found in solution output
|
|
87 |
# Always gets caught and passed up as a TestError
|
|
88 |
class FileNotFoundError(Exception): |
|
89 |
def __init__(self, filename): |
|
90 |
self._filename = filename |
|
91 |
||
92 |
def __str__(self): |
|
93 |
return "File %s not found in output" %(self._filename) |
|
94 |
||
95 |
||
96 |
# Error encountered when executing solution or attempt code
|
|
97 |
# Always gets caught and passed up in a ScriptExecutionError
|
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
98 |
class FunctionNotFoundError(Exception): |
99 |
"""This error is returned when a function was expected in a
|
|
100 |
test case but was not found"""
|
|
101 |
def __init__(self, function_name): |
|
102 |
self.function_name = function_name |
|
103 |
||
104 |
def __str__(self): |
|
105 |
return "Function " + self.function_name + " not found" |
|
106 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
107 |
class TestCasePart: |
108 |
"""
|
|
109 |
A part of a test case which compares a subset of the input files or file streams.
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
110 |
This can be done either with a comparison function, or by comparing directly, after
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
111 |
applying normalisations.
|
112 |
"""
|
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
113 |
|
114 |
ident = classmethod(lambda x: x) |
|
115 |
ignore = classmethod(lambda x: None) |
|
116 |
match = classmethod(lambda x,y: x==y) |
|
117 |
always_match = classmethod(lambda x,y: True) |
|
118 |
true = classmethod(lambda *x: True) |
|
119 |
false = classmethod(lambda *x: False) |
|
120 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
121 |
def __init__(self, pass_msg, fail_msg, default='match'): |
122 |
"""Initialise with descriptions (pass,fail) and a default behavior for output
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
123 |
If default is match, unspecified files are matched exactly
|
124 |
If default is ignore, unspecified files are ignored
|
|
125 |
The default default is match.
|
|
126 |
"""
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
127 |
self._pass_msg = pass_msg |
128 |
self._fail_msg = fail_msg |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
129 |
self._default = default |
130 |
if default == 'ignore': |
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
131 |
self._default_func = self.true |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
132 |
else: |
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
133 |
self._default_func = self.match |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
134 |
|
135 |
self._file_tests = {} |
|
136 |
self._stdout_test = ('check', self._default_func) |
|
137 |
self._stderr_test = ('check', self._default_func) |
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
138 |
self._exception_test = ('check', self._default_func) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
139 |
self._result_test = ('check', self._default_func) |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
140 |
self._code_test = ('check', self._default_func) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
141 |
|
142 |
def _set_default_function(self, function, test_type): |
|
143 |
""""Ensure test type is valid and set function to a default
|
|
144 |
if not specified"""
|
|
145 |
||
146 |
if test_type not in ['norm', 'check']: |
|
147 |
raise TestCreationError("Invalid test type in %s" %self._desc) |
|
148 |
||
149 |
if function == '': |
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
150 |
if test_type == 'norm': function = self.ident |
151 |
else: function = self.match |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
152 |
|
153 |
return function |
|
154 |
||
155 |
def _validate_function(self, function, included_code): |
|
156 |
"""Create a function object from the given string.
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
157 |
If a valid function object cannot be created, raise an error.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
158 |
"""
|
159 |
if not callable(function): |
|
160 |
try: |
|
161 |
exec "__f__ = %s" %function in included_code |
|
162 |
except: |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
163 |
raise TestCreationError("Invalid function %s" % function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
164 |
|
165 |
f = included_code['__f__'] |
|
166 |
||
167 |
if not callable(f): |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
168 |
raise TestCreationError("Invalid function %s" % function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
169 |
else: |
170 |
f = function |
|
171 |
||
172 |
return f |
|
173 |
||
174 |
def validate_functions(self, included_code): |
|
175 |
"""Ensure all functions used by the test cases exist and are callable.
|
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
176 |
Also convert their string representations to function objects.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
177 |
This can only be done once all the include code has been specified.
|
178 |
"""
|
|
179 |
(test_type, function) = self._stdout_test |
|
180 |
self._stdout_test = (test_type, self._validate_function(function, included_code)) |
|
181 |
||
182 |
(test_type, function) = self._stderr_test |
|
183 |
self._stderr_test = (test_type, self._validate_function(function, included_code)) |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
184 |
|
185 |
(test_type, function) = self._result_test |
|
186 |
self._result_test = (test_type, self._validate_function(function, included_code)) |
|
187 |
||
188 |
(test_type, function) = self._exception_test |
|
189 |
self._exception_test = (test_type, self._validate_function(function, included_code)) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
190 |
|
191 |
for filename, (test_type, function) in self._file_tests.items(): |
|
192 |
self._file_tests[filename] = (test_type, self._validate_function(function, included_code)) |
|
193 |
||
194 |
def add_result_test(self, function, test_type='norm'): |
|
195 |
"Test part that compares function return values"
|
|
196 |
function = self._set_default_function(function, test_type) |
|
197 |
self._result_test = (test_type, function) |
|
198 |
||
199 |
def add_stdout_test(self, function, test_type='norm'): |
|
200 |
"Test part that compares stdout"
|
|
201 |
function = self._set_default_function(function, test_type) |
|
202 |
self._stdout_test = (test_type, function) |
|
203 |
||
204 |
def add_stderr_test(self, function, test_type='norm'): |
|
205 |
"Test part that compares stderr"
|
|
206 |
function = self._set_default_function(function, test_type) |
|
207 |
self._stderr_test = (test_type, function) |
|
208 |
||
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
209 |
def add_exception_test(self, function, test_type='norm'): |
210 |
"Test part that compares stderr"
|
|
211 |
function = self._set_default_function(function, test_type) |
|
212 |
self._exception_test = (test_type, function) |
|
213 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
214 |
def add_file_test(self, filename, function, test_type='norm'): |
215 |
"Test part that compares the contents of a specified file"
|
|
216 |
function = self._set_default_function(function, test_type) |
|
217 |
self._file_tests[filename] = (test_type, function) |
|
218 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
219 |
def add_code_test(self, function, test_type='norm'): |
220 |
"Test part that examines the supplied code"
|
|
221 |
function = self._set_default_function(function, test_type) |
|
222 |
self._code_test = (test_type, function) |
|
223 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
224 |
def _check_output(self, solution_output, attempt_output, test_type, f): |
225 |
"""Compare solution output and attempt output using the
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
226 |
specified comparison function.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
227 |
"""
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
228 |
solution_output = str(solution_output) |
229 |
attempt_output = str(attempt_output) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
230 |
|
231 |
if test_type == 'norm': |
|
232 |
return f(solution_output) == f(attempt_output) |
|
233 |
else: |
|
234 |
return f(solution_output, attempt_output) |
|
235 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
236 |
def _check_code(self, solution, attempt, test_type, f, include_space): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
237 |
"""Compare solution code and attempt code using the
|
238 |
specified comparison function.
|
|
239 |
"""
|
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
240 |
if type(f) in types.StringTypes: # kludge |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
241 |
f = eval(str(f), include_space) |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
242 |
if test_type == 'norm': |
243 |
return f(solution) == f(attempt) |
|
244 |
else: |
|
245 |
return f(solution, attempt) |
|
246 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
247 |
def run(self, solution_data, attempt_data, include_space): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
248 |
"""Run the tests to compare the solution and attempt data
|
328
by mattgiuca
console: Renamed HTML element IDs to prefix "console_". |
249 |
Returns the empty string if the test passes, or else an error message.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
250 |
"""
|
251 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
252 |
# check source code itself
|
253 |
(test_type, f) = self._code_test |
|
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
254 |
if not self._check_code(solution_data['code'], attempt_data['code'], test_type, f, include_space): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
255 |
return 'Unexpected code' |
256 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
257 |
# check function return value (None for scripts)
|
258 |
(test_type, f) = self._result_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
259 |
if not self._check_output(solution_data['result'], attempt_data['result'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
260 |
return 'Unexpected function return value' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
261 |
|
262 |
# check stdout
|
|
263 |
(test_type, f) = self._stdout_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
264 |
if not self._check_output(solution_data['stdout'], attempt_data['stdout'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
265 |
return 'Unexpected output' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
266 |
|
267 |
#check stderr
|
|
268 |
(test_type, f) = self._stderr_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
269 |
if not self._check_output(solution_data['stderr'], attempt_data['stderr'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
270 |
return 'Unexpected error output' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
271 |
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
272 |
#check exception
|
273 |
(test_type, f) = self._exception_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
274 |
if not self._check_output(solution_data['exception'], attempt_data['exception'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
275 |
return 'Unexpected exception' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
276 |
|
277 |
solution_files = solution_data['modified_files'] |
|
278 |
attempt_files = attempt_data['modified_files'] |
|
279 |
||
280 |
# check files indicated by test
|
|
281 |
for (filename, (test_type, f)) in self._file_tests.items(): |
|
282 |
if filename not in solution_files: |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
283 |
raise FileNotFoundError(filename) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
284 |
elif filename not in attempt_files: |
285 |
return filename + ' not found' |
|
286 |
elif not self._check_output(solution_files[filename], attempt_files[filename], test_type, f): |
|
287 |
return filename + ' does not match' |
|
288 |
||
289 |
if self._default == 'ignore': |
|
290 |
return '' |
|
291 |
||
292 |
# check files found in solution, but not indicated by test
|
|
293 |
for filename in [f for f in solution_files if f not in self._file_tests]: |
|
294 |
if filename not in attempt_files: |
|
295 |
return filename + ' not found' |
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
296 |
elif not self._check_output(solution_files[filename], attempt_files[filename], 'match', self.match): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
297 |
return filename + ' does not match' |
298 |
||
299 |
# check if attempt has any extra files
|
|
300 |
for filename in [f for f in attempt_files if f not in solution_files]: |
|
301 |
return "Unexpected file found: " + filename |
|
302 |
||
303 |
# Everything passed with no problems
|
|
304 |
return '' |
|
305 |
||
306 |
class TestCase: |
|
307 |
"""
|
|
308 |
A set of tests with a common inputs
|
|
309 |
"""
|
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
310 |
def __init__(self, console, name='', function=None, stdin='', filespace=None, global_space=None): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
311 |
"""Initialise with name and optionally, a function to test (instead of the entire script)
|
312 |
The inputs stdin, the filespace and global variables can also be specified at
|
|
313 |
initialisation, but may also be set later.
|
|
314 |
"""
|
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
315 |
self._console = console |
316 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
317 |
if global_space == None: |
318 |
global_space = {} |
|
319 |
if filespace == None: |
|
320 |
filespace = {} |
|
321 |
||
322 |
self._name = name |
|
323 |
||
324 |
if function == '': function = None |
|
325 |
self._function = function |
|
326 |
self._list_args = [] |
|
327 |
self._keyword_args = {} |
|
328 |
||
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
329 |
self.set_stdin(stdin) |
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
330 |
self._filespace = testfilespace.TestFilespace(filespace) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
331 |
self._global_space = global_space |
332 |
self._parts = [] |
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
333 |
self._allowed_exceptions = set() |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
334 |
|
335 |
def set_stdin(self, stdin): |
|
336 |
""" Set the given string as the stdin for this test case"""
|
|
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
337 |
# stdin must have a newline at the end for raw_input to work properly
|
338 |
if stdin[-1:] != '\n': |
|
339 |
stdin += '\n' |
|
1036
by dcoles
TutorialService: Fix up bug in setting of stdin (would crash scripts) |
340 |
self._stdin = stdin |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
341 |
|
342 |
def add_file(self, filename, data): |
|
343 |
""" Insert the given filename-data pair into the filespace for this test case"""
|
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
344 |
# TODO: Add the file to the console
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
345 |
self._filespace.add_file(filename, data) |
346 |
||
347 |
def add_variable(self, variable, value): |
|
348 |
""" Add the given varibale-value pair to the initial global environment
|
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
349 |
for this test case. The value is the string repr() of an actual value.
|
350 |
Throw an exception if the value cannot be paresed.
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
351 |
"""
|
352 |
||
353 |
try: |
|
354 |
self._global_space[variable] = eval(value) |
|
355 |
except: |
|
356 |
raise TestCreationError("Invalid value for variable %s: %s" %(variable, value)) |
|
357 |
||
358 |
def add_arg(self, value, name=None): |
|
359 |
""" Add a value to the argument list. This only applies when testing functions.
|
|
360 |
By default arguments are not named, but if they are, they become keyword arguments.
|
|
361 |
"""
|
|
362 |
try: |
|
363 |
if name == None or name == '': |
|
364 |
self._list_args.append(eval(value)) |
|
365 |
else: |
|
366 |
self._keyword_args[name] = value |
|
367 |
except: |
|
368 |
raise TestCreationError("Invalid value for function argument: %s" %value) |
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
369 |
|
370 |
def add_exception(self, exception_name): |
|
371 |
self._allowed_exceptions.add(exception_name) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
372 |
|
373 |
def add_part(self, test_part): |
|
374 |
""" Add a TestPart to this test case"""
|
|
375 |
self._parts.append(test_part) |
|
376 |
||
377 |
def validate_functions(self, included_code): |
|
378 |
""" Validate all the functions in each part in this test case
|
|
379 |
This can only be done once all the include code has been specified.
|
|
380 |
"""
|
|
381 |
for part in self._parts: |
|
382 |
part.validate_functions(included_code) |
|
383 |
||
384 |
def get_name(self): |
|
385 |
""" Get the name of the test case """
|
|
386 |
return self._name |
|
387 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
388 |
def run(self, solution, attempt_code, include_space, stop_on_fail=True): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
389 |
""" Run the solution and the attempt with the inputs specified for this test case.
|
390 |
Then pass the outputs to each test part and collate the results.
|
|
391 |
"""
|
|
392 |
case_dict = {} |
|
393 |
case_dict['name'] = self._name |
|
394 |
||
395 |
# Run solution
|
|
396 |
try: |
|
397 |
global_space_copy = copy.deepcopy(self._global_space) |
|
398 |
solution_data = self._execstring(solution, global_space_copy) |
|
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
399 |
self._console.stdin.truncate(0) |
400 |
self._console.stdin.write(self._stdin) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
401 |
|
402 |
# if we are just testing a function
|
|
403 |
if not self._function == None: |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
404 |
if self._function not in solution_data['globals']: |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
405 |
raise FunctionNotFoundError(self._function) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
406 |
solution_data = self._run_function(self._function, |
407 |
self._list_args, self._keyword_args, solution) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
408 |
|
409 |
except: |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
410 |
raise ScriptExecutionError(sys.exc_info()) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
411 |
|
412 |
# Run student attempt
|
|
413 |
try: |
|
414 |
global_space_copy = copy.deepcopy(self._global_space) |
|
302
by dilshan_a
Updated test framework so that student's code is passed in as a string. |
415 |
attempt_data = self._execstring(attempt_code, global_space_copy) |
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
416 |
self._console.stdin.truncate(0) |
417 |
self._console.stdin.write(self._stdin) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
418 |
|
419 |
# if we are just testing a function
|
|
420 |
if not self._function == None: |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
421 |
if self._function not in attempt_data['globals']: |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
422 |
raise FunctionNotFoundError(self._function) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
423 |
attempt_data = self._run_function(self._function, |
424 |
self._list_args, self._keyword_args, attempt_code) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
425 |
except: |
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
426 |
case_dict['exception'] = ScriptExecutionError(sys.exc_info()).to_dict() |
304
by dilshan_a
Added documentation of output of TestSuite. |
427 |
case_dict['passed'] = False |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
428 |
return case_dict |
429 |
||
430 |
results = [] |
|
304
by dilshan_a
Added documentation of output of TestSuite. |
431 |
passed = True |
432 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
433 |
# generate results
|
434 |
for test_part in self._parts: |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
435 |
try: |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
436 |
result = test_part.run(solution_data, attempt_data, include_space) |
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
437 |
except: |
438 |
raise TestError(sys.exc_info()) |
|
439 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
440 |
result_dict = {} |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
441 |
result_dict['description'] = test_part._pass_msg |
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
442 |
result_dict['passed'] = (result == '') |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
443 |
if result_dict['passed'] == False: |
444 |
result_dict['error_message'] = result |
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
445 |
result_dict['description'] = test_part._fail_msg |
304
by dilshan_a
Added documentation of output of TestSuite. |
446 |
passed = False |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
447 |
|
448 |
results.append(result_dict) |
|
449 |
||
514
by stevenbird
More flexible control of test_case_parts via optional flag |
450 |
# Do we continue the test_parts after one of them has failed?
|
451 |
if not passed and stop_on_fail: |
|
452 |
break; |
|
453 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
454 |
case_dict['parts'] = results |
304
by dilshan_a
Added documentation of output of TestSuite. |
455 |
case_dict['passed'] = passed |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
456 |
|
457 |
return case_dict |
|
458 |
||
459 |
def _execfile(self, filename, global_space): |
|
460 |
""" Execute the file given by 'filename' in global_space, and return the outputs. """
|
|
461 |
self._initialise_global_space(global_space) |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
462 |
data = self._run_function(lambda: execfile(filename, global_space), |
463 |
code = open(filename).read()) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
464 |
return data |
465 |
||
466 |
def _execstring(self, string, global_space): |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
467 |
""" Execute the given string in global_space, and return the outputs.
|
468 |
"""
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
469 |
self._initialise_global_space(global_space) |
305
by dilshan_a
Neated up execution of strings in TestCase. |
470 |
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
471 |
inspection = self._console.execute(string) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
472 |
|
473 |
exception_name = None |
|
474 |
if 'exception' in inspection: |
|
475 |
exception = inspection['exception']['except'] |
|
476 |
exception_name = type(exception).__name__ |
|
477 |
raise(exception) |
|
478 |
||
479 |
return {'code': string, |
|
480 |
'result': None, |
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
481 |
'globals': self._console.globals(), |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
482 |
'exception': exception_name, # Hmmm... odd? Is this right? |
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
483 |
'stdout': self._console.stdout.read(), |
484 |
'stderr': self._console.stderr.read(), |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
485 |
'modified_files': None} |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
486 |
|
487 |
def _initialise_global_space(self, global_space): |
|
488 |
""" Modify the provided global_space so that file, open and raw_input are redefined
|
|
489 |
to use our methods instead.
|
|
490 |
"""
|
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
491 |
self._console.globals(global_space) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
492 |
self._current_filespace_copy = self._filespace.copy() |
493 |
global_space['file'] = lambda filename, mode='r', bufsize=-1: self._current_filespace_copy.openfile(filename, mode) |
|
494 |
global_space['open'] = global_space['file'] |
|
495 |
global_space['raw_input'] = lambda x=None: raw_input() |
|
496 |
return global_space |
|
497 |
||
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
498 |
def _run_function(self, function, args, kwargs, code): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
499 |
""" Run the provided function with the provided stdin, capturing stdout and stderr
|
500 |
and the return value.
|
|
501 |
Return all the output data.
|
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
502 |
code: The full text of the code, which needs to be stored as part of
|
503 |
the returned dictionary.
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
504 |
"""
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
505 |
s_args = map(repr, args) |
506 |
s_kwargs = dict(zip(kwargs.keys(), map(repr, kwargs.values()))) |
|
507 |
call = self._console.call(function, *s_args, **s_kwargs) |
|
508 |
||
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
509 |
exception_name = None |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
510 |
if 'exception' in call: |
511 |
exception = call['exception']['except'] |
|
512 |
exception_name = type(exception).__name__ |
|
513 |
raise(exception) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
514 |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
515 |
return {'code': code, |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
516 |
'result': call['result'], |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
517 |
'exception': exception_name, |
1036
by dcoles
TutorialService: Fix up bug in setting of stdin (would crash scripts) |
518 |
'stdout': self._console.stdout.read(), |
519 |
'stderr': self._console.stderr.read(), |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
520 |
'modified_files': None} |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
521 |
|
522 |
class TestSuite: |
|
523 |
"""
|
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
524 |
The complete collection of test cases for a given exercise
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
525 |
"""
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
526 |
def __init__(self, name, console, solution=None): |
513
by stevenbird
test/test_framework/*, exercises/sample/* |
527 |
"""Initialise with the name of the test suite (the exercise name) and the solution.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
528 |
The solution may be specified later.
|
529 |
"""
|
|
530 |
self._solution = solution |
|
531 |
self._name = name |
|
532 |
self._tests = [] |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
533 |
self._console = console |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
534 |
self.add_include_code("") |
535 |
||
536 |
def add_solution(self, solution): |
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
537 |
" Specify the solution script for this exercise "
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
538 |
self._solution = solution |
539 |
||
540 |
def has_solution(self): |
|
523
by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions, |
541 |
" Returns true if a solution has been provided "
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
542 |
return self._solution != None |
543 |
||
544 |
def add_include_code(self, include_code = ''): |
|
545 |
""" Add include code that may be used by the test cases during
|
|
546 |
comparison of outputs.
|
|
547 |
"""
|
|
548 |
||
549 |
# if empty, make sure it can still be executed
|
|
550 |
if include_code == "": |
|
551 |
include_code = "pass" |
|
552 |
self._include_code = str(include_code) |
|
553 |
||
554 |
include_space = {} |
|
555 |
try: |
|
556 |
exec self._include_code in include_space |
|
557 |
except: |
|
558 |
raise TestCreationError("Bad include code") |
|
559 |
||
560 |
self._include_space = include_space |
|
561 |
||
562 |
def add_case(self, test_case): |
|
563 |
""" Add a TestCase, then validate all functions inside test case
|
|
564 |
now that the include code is known
|
|
565 |
"""
|
|
566 |
self._tests.append(test_case) |
|
567 |
test_case.validate_functions(self._include_space) |
|
568 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
569 |
def run_tests(self, attempt_code, stop_on_fail=False): |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
570 |
" Run all test cases on the specified console and collate the results "
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
571 |
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
572 |
exercise_dict = {} |
573 |
exercise_dict['name'] = self._name |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
574 |
|
575 |
test_case_results = [] |
|
309
by dilshan_a
Added a passed key to return value of problem suite. |
576 |
passed = True |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
577 |
for test in self._tests: |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
578 |
result_dict = test.run(self._solution, attempt_code, self._include_space) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
579 |
if 'exception' in result_dict and result_dict['exception']['critical']: |
580 |
# critical error occured, running more cases is useless
|
|
581 |
# FunctionNotFound, Syntax, Indentation
|
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
582 |
exercise_dict['critical_error'] = result_dict['exception'] |
583 |
exercise_dict['passed'] = False |
|
584 |
return exercise_dict |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
585 |
|
586 |
test_case_results.append(result_dict) |
|
304
by dilshan_a
Added documentation of output of TestSuite. |
587 |
|
309
by dilshan_a
Added a passed key to return value of problem suite. |
588 |
if not result_dict['passed']: |
589 |
passed = False |
|
590 |
if stop_on_fail: |
|
591 |
break
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
592 |
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
593 |
exercise_dict['cases'] = test_case_results |
594 |
exercise_dict['passed'] = passed |
|
595 |
return exercise_dict |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
596 |
|
597 |
def get_name(self): |
|
598 |
return self._name |
|
599 |
||
600 |
##def get_function(filename, function_name):
|
|
601 |
## import compiler
|
|
602 |
## mod = compiler.parseFile(filename)
|
|
603 |
## for node in mod.node.nodes:
|
|
604 |
## if isinstance(node, compiler.ast.Function) and node.name == function_name:
|
|
605 |
## return node
|
|
606 |
##
|