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