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