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 |
||
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
121 |
def __init__(self, test_case): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
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 |
"""
|
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
127 |
self._pass_msg = test_case.passmsg |
128 |
self._fail_msg = test_case.failmsg |
|
129 |
self._default = test_case.test_default |
|
130 |
if self._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) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
141 |
|
142 |
for part in test_case.parts: |
|
143 |
if part.part_type =="file": |
|
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
144 |
self.add_file_test(part) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
145 |
elif part.part_type =="stdout": |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
146 |
self.add_stdout_test(part) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
147 |
elif part.part_type =="stderr": |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
148 |
self.add_stderr_test(part) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
149 |
elif part.part_type =="result": |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
150 |
self.add_result_test(part) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
151 |
elif part.part_type =="exception": |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
152 |
self.add_exception_test(part) |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
153 |
elif part.part_type =="code": |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
154 |
self.add_code_test(part) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
155 |
|
156 |
def _set_default_function(self, function, test_type): |
|
157 |
""""Ensure test type is valid and set function to a default
|
|
158 |
if not specified"""
|
|
159 |
||
160 |
if test_type not in ['norm', 'check']: |
|
161 |
raise TestCreationError("Invalid test type in %s" %self._desc) |
|
162 |
||
163 |
if function == '': |
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
164 |
if test_type == 'norm': function = self.ident |
165 |
else: function = self.match |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
166 |
|
167 |
return function |
|
168 |
||
169 |
def _validate_function(self, function, included_code): |
|
170 |
"""Create a function object from the given string.
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
171 |
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 |
172 |
"""
|
173 |
if not callable(function): |
|
174 |
try: |
|
175 |
exec "__f__ = %s" %function in included_code |
|
176 |
except: |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
177 |
raise TestCreationError("Invalid function %s" % function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
178 |
|
179 |
f = included_code['__f__'] |
|
180 |
||
181 |
if not callable(f): |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
182 |
raise TestCreationError("Invalid function %s" % function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
183 |
else: |
184 |
f = function |
|
185 |
||
186 |
return f |
|
187 |
||
188 |
def validate_functions(self, included_code): |
|
189 |
"""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 |
190 |
Also convert their string representations to function objects.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
191 |
This can only be done once all the include code has been specified.
|
192 |
"""
|
|
193 |
(test_type, function) = self._stdout_test |
|
194 |
self._stdout_test = (test_type, self._validate_function(function, included_code)) |
|
195 |
||
196 |
(test_type, function) = self._stderr_test |
|
197 |
self._stderr_test = (test_type, self._validate_function(function, included_code)) |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
198 |
|
199 |
(test_type, function) = self._result_test |
|
200 |
self._result_test = (test_type, self._validate_function(function, included_code)) |
|
201 |
||
202 |
(test_type, function) = self._exception_test |
|
203 |
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 |
204 |
|
205 |
for filename, (test_type, function) in self._file_tests.items(): |
|
206 |
self._file_tests[filename] = (test_type, self._validate_function(function, included_code)) |
|
207 |
||
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
208 |
def add_result_test(self, part): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
209 |
"Test part that compares function return values"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
210 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
211 |
self._result_test = (part.test_type, function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
212 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
213 |
def add_stdout_test(self, part): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
214 |
"Test part that compares stdout"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
215 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
216 |
self._stdout_test = (part.test_type, function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
217 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
218 |
def add_stderr_test(self, part): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
219 |
"Test part that compares stderr"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
220 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
221 |
self._stderr_test = (part.test_type, function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
222 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
223 |
def add_exception_test(self, part): |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
224 |
"Test part that compares stderr"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
225 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
226 |
self._exception_test = (part.test_type, function) |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
227 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
228 |
def add_file_test(self, part): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
229 |
"Test part that compares the contents of a specified file"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
230 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
231 |
self._file_tests[part.filename] = (part.test_type, function) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
232 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
233 |
def add_code_test(self, part): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
234 |
"Test part that examines the supplied code"
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
235 |
function = self._set_default_function(part.data, part.test_type) |
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
236 |
self._code_test = (part.test_type, function) |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
237 |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
238 |
def _check_output(self, solution_output, attempt_output, test_type, f): |
239 |
"""Compare solution output and attempt output using the
|
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
240 |
specified comparison function.
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
241 |
"""
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
242 |
solution_output = str(solution_output) |
243 |
attempt_output = str(attempt_output) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
244 |
|
245 |
if test_type == 'norm': |
|
246 |
return f(solution_output) == f(attempt_output) |
|
247 |
else: |
|
248 |
return f(solution_output, attempt_output) |
|
249 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
250 |
def _check_code(self, solution, attempt, test_type, f, include_space): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
251 |
"""Compare solution code and attempt code using the
|
252 |
specified comparison function.
|
|
253 |
"""
|
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
254 |
if type(f) in types.StringTypes: # kludge |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
255 |
f = eval(str(f), include_space) |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
256 |
if test_type == 'norm': |
257 |
return f(solution) == f(attempt) |
|
258 |
else: |
|
259 |
return f(solution, attempt) |
|
260 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
261 |
def run(self, solution_data, attempt_data, include_space): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
262 |
"""Run the tests to compare the solution and attempt data
|
328
by mattgiuca
console: Renamed HTML element IDs to prefix "console_". |
263 |
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 |
264 |
"""
|
265 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
266 |
# check source code itself
|
267 |
(test_type, f) = self._code_test |
|
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
268 |
if not self._check_code(solution_data['code'], attempt_data['code'], test_type, f, include_space): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
269 |
return 'Unexpected code' |
270 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
271 |
# check function return value (None for scripts)
|
272 |
(test_type, f) = self._result_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
273 |
if not self._check_output(solution_data['result'], attempt_data['result'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
274 |
return 'Unexpected function return value' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
275 |
|
276 |
# check stdout
|
|
277 |
(test_type, f) = self._stdout_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
278 |
if not self._check_output(solution_data['stdout'], attempt_data['stdout'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
279 |
return 'Unexpected output' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
280 |
|
281 |
#check stderr
|
|
282 |
(test_type, f) = self._stderr_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
283 |
if not self._check_output(solution_data['stderr'], attempt_data['stderr'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
284 |
return 'Unexpected error output' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
285 |
|
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
286 |
#check exception
|
287 |
(test_type, f) = self._exception_test |
|
507
by stevenbird
Extended code test framework to support tests on the code string, rather |
288 |
if not self._check_output(solution_data['exception'], attempt_data['exception'], test_type, f): |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
289 |
return 'Unexpected exception' |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
290 |
|
291 |
solution_files = solution_data['modified_files'] |
|
292 |
attempt_files = attempt_data['modified_files'] |
|
293 |
||
294 |
# check files indicated by test
|
|
295 |
for (filename, (test_type, f)) in self._file_tests.items(): |
|
296 |
if filename not in solution_files: |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
297 |
raise FileNotFoundError(filename) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
298 |
elif filename not in attempt_files: |
299 |
return filename + ' not found' |
|
300 |
elif not self._check_output(solution_files[filename], attempt_files[filename], test_type, f): |
|
301 |
return filename + ' does not match' |
|
302 |
||
303 |
if self._default == 'ignore': |
|
304 |
return '' |
|
305 |
||
306 |
# check files found in solution, but not indicated by test
|
|
307 |
for filename in [f for f in solution_files if f not in self._file_tests]: |
|
308 |
if filename not in attempt_files: |
|
309 |
return filename + ' not found' |
|
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
310 |
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 |
311 |
return filename + ' does not match' |
312 |
||
313 |
# check if attempt has any extra files
|
|
314 |
for filename in [f for f in attempt_files if f not in solution_files]: |
|
315 |
return "Unexpected file found: " + filename |
|
316 |
||
317 |
# Everything passed with no problems
|
|
318 |
return '' |
|
319 |
||
320 |
class TestCase: |
|
321 |
"""
|
|
322 |
A set of tests with a common inputs
|
|
323 |
"""
|
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
324 |
def __init__(self, console, suite): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
325 |
"""Initialise with name and optionally, a function to test (instead of the entire script)
|
326 |
The inputs stdin, the filespace and global variables can also be specified at
|
|
327 |
initialisation, but may also be set later.
|
|
328 |
"""
|
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
329 |
self._console = console |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
330 |
self._name = suite.description |
331 |
||
332 |
function = suite.function |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
333 |
if function == '': function = None |
334 |
self._function = function |
|
335 |
self._list_args = [] |
|
336 |
self._keyword_args = {} |
|
337 |
||
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
338 |
self.set_stdin(suite.stdin) |
339 |
self._filespace = testfilespace.TestFilespace(None) |
|
340 |
self._global_space = {} |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
341 |
self._parts = [] |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
342 |
self._allowed_exceptions = set() |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
343 |
|
1099.4.1
by Nick Chadwick
Working on putting worksheets into the database. |
344 |
args = {} |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
345 |
for var in suite.variables: |
346 |
if var.var_type == "file": |
|
347 |
self.add_file(var) |
|
348 |
elif var.var_type == "var": |
|
349 |
self.add_variable(var) |
|
350 |
elif var.var_type == "arg": |
|
1099.4.1
by Nick Chadwick
Working on putting worksheets into the database. |
351 |
args[var.arg_no] = var |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
352 |
elif var.var_type == "exception": |
353 |
self.add_exception(var) |
|
354 |
||
1099.4.1
by Nick Chadwick
Working on putting worksheets into the database. |
355 |
for i in xrange(len(args)): |
356 |
self.add_arg(args[i]) |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
357 |
for test_case in suite.test_cases: |
358 |
self.add_part(TestCasePart(test_case)) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
359 |
|
360 |
def set_stdin(self, stdin): |
|
361 |
""" 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) |
362 |
# stdin must have a newline at the end for raw_input to work properly
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
363 |
if stdin is not None: |
364 |
if stdin[-1:] != '\n': |
|
365 |
stdin += '\n' |
|
366 |
else: |
|
367 |
stdin = "" |
|
1036
by dcoles
TutorialService: Fix up bug in setting of stdin (would crash scripts) |
368 |
self._stdin = stdin |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
369 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
370 |
def add_file(self, filevar): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
371 |
""" 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 |
372 |
# TODO: Add the file to the console
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
373 |
self._filespace.add_file(filevar.var_name, "") |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
374 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
375 |
def add_variable(self, var): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
376 |
""" 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 |
377 |
for this test case. The value is the string repr() of an actual value.
|
378 |
Throw an exception if the value cannot be paresed.
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
379 |
"""
|
380 |
||
381 |
try: |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
382 |
self._global_space[var.var_name] = eval(var.var_value) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
383 |
except: |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
384 |
raise TestCreationError("Invalid value for variable %s: %s" |
385 |
%(var.var_name, var.var_value)) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
386 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
387 |
def add_arg(self, var): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
388 |
""" Add a value to the argument list. This only applies when testing functions.
|
389 |
By default arguments are not named, but if they are, they become keyword arguments.
|
|
390 |
"""
|
|
391 |
try: |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
392 |
if var.var_name == None or var.var_name == '': |
393 |
self._list_args.append(eval(var.var_value)) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
394 |
else: |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
395 |
self._keyword_args[var.var_name] = var.var_value |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
396 |
except: |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
397 |
raise TestCreationError("Invalid value for function argument: %s" %var.var_value) |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
398 |
|
399 |
def add_exception(self, exception_name): |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
400 |
self._allowed_exceptions.add(var.var_name) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
401 |
|
402 |
def add_part(self, test_part): |
|
403 |
""" Add a TestPart to this test case"""
|
|
404 |
self._parts.append(test_part) |
|
405 |
||
406 |
def validate_functions(self, included_code): |
|
407 |
""" Validate all the functions in each part in this test case
|
|
408 |
This can only be done once all the include code has been specified.
|
|
409 |
"""
|
|
410 |
for part in self._parts: |
|
411 |
part.validate_functions(included_code) |
|
412 |
||
413 |
def get_name(self): |
|
414 |
""" Get the name of the test case """
|
|
415 |
return self._name |
|
416 |
||
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
417 |
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 |
418 |
""" Run the solution and the attempt with the inputs specified for this test case.
|
419 |
Then pass the outputs to each test part and collate the results.
|
|
420 |
"""
|
|
421 |
case_dict = {} |
|
422 |
case_dict['name'] = self._name |
|
423 |
||
424 |
# Run solution
|
|
425 |
try: |
|
426 |
global_space_copy = copy.deepcopy(self._global_space) |
|
427 |
solution_data = self._execstring(solution, global_space_copy) |
|
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
428 |
self._console.stdin.truncate(0) |
429 |
self._console.stdin.write(self._stdin) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
430 |
|
431 |
# if we are just testing a function
|
|
432 |
if not self._function == None: |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
433 |
if self._function not in solution_data['globals']: |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
434 |
raise FunctionNotFoundError(self._function) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
435 |
solution_data = self._run_function(self._function, |
436 |
self._list_args, self._keyword_args, solution) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
437 |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
438 |
except Exception, e: |
439 |
raise e #ScriptExecutionError(sys.exc_info()) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
440 |
|
441 |
# Run student attempt
|
|
442 |
try: |
|
443 |
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. |
444 |
attempt_data = self._execstring(attempt_code, global_space_copy) |
1035
by dcoles
Tutorial: Added in stdin support for exercises (sets them up in console) |
445 |
self._console.stdin.truncate(0) |
446 |
self._console.stdin.write(self._stdin) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
447 |
|
448 |
# if we are just testing a function
|
|
449 |
if not self._function == None: |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
450 |
if self._function not in attempt_data['globals']: |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
451 |
raise FunctionNotFoundError(self._function) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
452 |
attempt_data = self._run_function(self._function, |
453 |
self._list_args, self._keyword_args, attempt_code) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
454 |
except: |
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
455 |
case_dict['exception'] = ScriptExecutionError(sys.exc_info()).to_dict() |
304
by dilshan_a
Added documentation of output of TestSuite. |
456 |
case_dict['passed'] = False |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
457 |
return case_dict |
458 |
||
459 |
results = [] |
|
304
by dilshan_a
Added documentation of output of TestSuite. |
460 |
passed = True |
461 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
462 |
# generate results
|
463 |
for test_part in self._parts: |
|
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
464 |
try: |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
465 |
result = test_part.run(solution_data, attempt_data, include_space) |
306
by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError. |
466 |
except: |
467 |
raise TestError(sys.exc_info()) |
|
468 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
469 |
result_dict = {} |
502
by stevenbird
www/apps/tutorialservice/__init__.py |
470 |
result_dict['description'] = test_part._pass_msg |
495
by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result |
471 |
result_dict['passed'] = (result == '') |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
472 |
if result_dict['passed'] == False: |
473 |
result_dict['error_message'] = result |
|
502
by stevenbird
www/apps/tutorialservice/__init__.py |
474 |
result_dict['description'] = test_part._fail_msg |
304
by dilshan_a
Added documentation of output of TestSuite. |
475 |
passed = False |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
476 |
|
477 |
results.append(result_dict) |
|
478 |
||
514
by stevenbird
More flexible control of test_case_parts via optional flag |
479 |
# Do we continue the test_parts after one of them has failed?
|
480 |
if not passed and stop_on_fail: |
|
481 |
break; |
|
482 |
||
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
483 |
case_dict['parts'] = results |
304
by dilshan_a
Added documentation of output of TestSuite. |
484 |
case_dict['passed'] = passed |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
485 |
|
486 |
return case_dict |
|
487 |
||
488 |
def _execfile(self, filename, global_space): |
|
489 |
""" Execute the file given by 'filename' in global_space, and return the outputs. """
|
|
490 |
self._initialise_global_space(global_space) |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
491 |
data = self._run_function(lambda: execfile(filename, global_space), |
492 |
code = open(filename).read()) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
493 |
return data |
494 |
||
495 |
def _execstring(self, string, global_space): |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
496 |
""" Execute the given string in global_space, and return the outputs.
|
497 |
"""
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
498 |
self._initialise_global_space(global_space) |
305
by dilshan_a
Neated up execution of strings in TestCase. |
499 |
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
500 |
inspection = self._console.execute(string) |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
501 |
|
502 |
exception_name = None |
|
503 |
if 'exception' in inspection: |
|
504 |
exception = inspection['exception']['except'] |
|
505 |
exception_name = type(exception).__name__ |
|
506 |
raise(exception) |
|
507 |
||
508 |
return {'code': string, |
|
509 |
'result': None, |
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
510 |
'globals': self._console.globals(), |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
511 |
'exception': exception_name, # Hmmm... odd? Is this right? |
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
512 |
'stdout': self._console.stdout.read(), |
513 |
'stderr': self._console.stderr.read(), |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
514 |
'modified_files': None} |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
515 |
|
516 |
def _initialise_global_space(self, global_space): |
|
517 |
""" Modify the provided global_space so that file, open and raw_input are redefined
|
|
518 |
to use our methods instead.
|
|
519 |
"""
|
|
1034
by dcoles
Console: Refactored the code to support supplying of stdin to the console. |
520 |
self._console.globals(global_space) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
521 |
self._current_filespace_copy = self._filespace.copy() |
522 |
global_space['file'] = lambda filename, mode='r', bufsize=-1: self._current_filespace_copy.openfile(filename, mode) |
|
523 |
global_space['open'] = global_space['file'] |
|
524 |
global_space['raw_input'] = lambda x=None: raw_input() |
|
525 |
return global_space |
|
526 |
||
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
527 |
def _run_function(self, function, args, kwargs, code): |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
528 |
""" Run the provided function with the provided stdin, capturing stdout and stderr
|
529 |
and the return value.
|
|
530 |
Return all the output data.
|
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
531 |
code: The full text of the code, which needs to be stored as part of
|
532 |
the returned dictionary.
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
533 |
"""
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
534 |
s_args = map(repr, args) |
535 |
s_kwargs = dict(zip(kwargs.keys(), map(repr, kwargs.values()))) |
|
536 |
call = self._console.call(function, *s_args, **s_kwargs) |
|
537 |
||
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
538 |
exception_name = None |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
539 |
if 'exception' in call: |
540 |
exception = call['exception']['except'] |
|
541 |
exception_name = type(exception).__name__ |
|
542 |
raise(exception) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
543 |
|
716
by mattgiuca
Test Framework: Numerous bug fixes. |
544 |
return {'code': code, |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
545 |
'result': call['result'], |
299
by dilshan_a
Test framework now handles exceptions as valid outputs for scripts. |
546 |
'exception': exception_name, |
1036
by dcoles
TutorialService: Fix up bug in setting of stdin (would crash scripts) |
547 |
'stdout': self._console.stdout.read(), |
548 |
'stderr': self._console.stderr.read(), |
|
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
549 |
'modified_files': None} |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
550 |
|
551 |
class TestSuite: |
|
552 |
"""
|
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
553 |
The complete collection of test cases for a given exercise
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
554 |
"""
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
555 |
def __init__(self, exercise, console): |
513
by stevenbird
test/test_framework/*, exercises/sample/* |
556 |
"""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 |
557 |
The solution may be specified later.
|
558 |
"""
|
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
559 |
self._solution = exercise.solution |
560 |
self._name = exercise.id |
|
561 |
self._exercise = exercise |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
562 |
self._tests = [] |
1029
by dcoles
Tutorial Service: Ported the tutorial service to the console so that all |
563 |
self._console = console |
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
564 |
self.add_include_code(exercise.include) |
565 |
||
566 |
for test_case in exercise.test_suites: |
|
567 |
new_case = TestCase(console, test_case) |
|
568 |
self.add_case(new_case) |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
569 |
|
570 |
def has_solution(self): |
|
523
by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions, |
571 |
" Returns true if a solution has been provided "
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
572 |
return self._solution != None |
573 |
||
574 |
def add_include_code(self, include_code = ''): |
|
575 |
""" Add include code that may be used by the test cases during
|
|
576 |
comparison of outputs.
|
|
577 |
"""
|
|
578 |
||
579 |
# if empty, make sure it can still be executed
|
|
1175
by William Grant
Treat a NULL exercise.include the same as an empty one. Don't crash. |
580 |
if include_code == "" or include_code is None: |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
581 |
include_code = "pass" |
1099.1.221
by Nick Chadwick
added in extra parts to the exercise edit view. Now almost all |
582 |
self._include_code = include_code |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
583 |
|
584 |
include_space = {} |
|
585 |
try: |
|
586 |
exec self._include_code in include_space |
|
587 |
except: |
|
1099.1.221
by Nick Chadwick
added in extra parts to the exercise edit view. Now almost all |
588 |
raise TestCreationError("-= Bad include code =-\n" + include_code) |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
589 |
|
590 |
self._include_space = include_space |
|
1099.1.150
by Nick Chadwick
Modified worksheets to properly link attempts to worksheets and |
591 |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
592 |
def add_case(self, test_case): |
593 |
""" Add a TestCase, then validate all functions inside test case
|
|
594 |
now that the include code is known
|
|
595 |
"""
|
|
596 |
self._tests.append(test_case) |
|
597 |
test_case.validate_functions(self._include_space) |
|
598 |
||
502
by stevenbird
www/apps/tutorialservice/__init__.py |
599 |
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 |
600 |
" 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 |
601 |
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
602 |
exercise_dict = {} |
603 |
exercise_dict['name'] = self._name |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
604 |
|
605 |
test_case_results = [] |
|
309
by dilshan_a
Added a passed key to return value of problem suite. |
606 |
passed = True |
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
607 |
for test in self._tests: |
519
by stevenbird
Fixed bug in test framework. Code given in the <include> section was |
608 |
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 |
609 |
if 'exception' in result_dict and result_dict['exception']['critical']: |
610 |
# critical error occured, running more cases is useless
|
|
611 |
# FunctionNotFound, Syntax, Indentation
|
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
612 |
exercise_dict['critical_error'] = result_dict['exception'] |
613 |
exercise_dict['passed'] = False |
|
614 |
return exercise_dict |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
615 |
|
616 |
test_case_results.append(result_dict) |
|
304
by dilshan_a
Added documentation of output of TestSuite. |
617 |
|
309
by dilshan_a
Added a passed key to return value of problem suite. |
618 |
if not result_dict['passed']: |
619 |
passed = False |
|
620 |
if stop_on_fail: |
|
621 |
break
|
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
622 |
|
513
by stevenbird
test/test_framework/*, exercises/sample/* |
623 |
exercise_dict['cases'] = test_case_results |
624 |
exercise_dict['passed'] = passed |
|
625 |
return exercise_dict |
|
294
by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for |
626 |
|
627 |
def get_name(self): |
|
1099.1.141
by Nick Chadwick
Updated the exercises to be loaded from the database, not a local file. |
628 |
return self._names |