~azzar1/unity/add-show-desktop-key

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