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