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