~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
26
import sys, StringIO, 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
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
29
# student error or author error
30
# errors in student code get handled internally
31
# errors in solution code get passed up
32
class ScriptExecutionError(Exception):
33
    """Runtime error in the student code or solution code"""
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
34
    def __init__(self, exc_info):
35
        cla, exc, trbk = exc_info
36
        self._name = cla.__name__
37
        self._detail = str(exc)
310 by dilshan_a
Add lineno to exception info.
38
        self._trbk = trbk
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
39
40
    def is_critical(self):
41
        if (    self._name == 'FunctionNotFoundError'
42
            or  self._name == 'SyntaxError'
43
            or  self._name == 'IndentationError'):
44
            return True
45
        else:
46
            return False
47
48
    def to_dict(self):
310 by dilshan_a
Add lineno to exception info.
49
        import traceback
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
50
        return {'name': self._name,
51
                'detail': self._detail,
311 by dilshan_a
Last commit was buggy.
52
                'critical': self.is_critical(),
310 by dilshan_a
Add lineno to exception info.
53
                'lineno': traceback.tb_lineno(self._trbk)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
54
                }
55
56
    def __str__(self):
57
        return self._name + " - " + str(self._detail)
58
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
59
# author error
60
class TestCreationError(Exception):
61
    """An error occured while creating the test suite or one of its components"""
62
    def __init__(self, reason):
63
        self._reason = reason
64
        
65
    def __str__(self):
66
        return self._reason
67
68
# author error
69
class TestError(Exception):
70
    """Runtime error in the testing framework outside of the provided or student code"""
71
    def __init__(self, exc_info):
72
        cla, exc, trbk = exc_info
73
        self._name = cla.__name__
74
        self._detail = str(exc)
75
        self._exc_info = exc_info
76
77
    def exc_info(self):
78
        return self._exc_info
79
80
    def __str__(self):
81
        return "Error testing solution against attempt: %s - %s" %(self._name, self._detail)
82
83
# author error
84
# raised when expected file not found in solution output
85
# Always gets caught and passed up as a TestError
86
class FileNotFoundError(Exception):
87
    def __init__(self, filename):
88
        self._filename = filename
89
90
    def __str__(self):
91
        return "File %s not found in output" %(self._filename)
92
    
93
94
# Error encountered when executing solution or attempt code
95
# Always gets caught and passed up in a ScriptExecutionError
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
96
class FunctionNotFoundError(Exception):
97
    """This error is returned when a function was expected in a
98
    test case but was not found"""
99
    def __init__(self, function_name):
100
        self.function_name = function_name
101
102
    def __str__(self):
103
        return "Function " + self.function_name + " not found"
104
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
105
class TestCasePart:
106
    """
107
    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
108
    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
109
    applying normalisations.
110
    """
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
111
112
    ident = classmethod(lambda x: x)
113
    ignore = classmethod(lambda x: None)
114
    match = classmethod(lambda x,y: x==y)
115
    always_match = classmethod(lambda x,y: True)
116
    true = classmethod(lambda *x: True)
117
    false = classmethod(lambda *x: False)
118
502 by stevenbird
www/apps/tutorialservice/__init__.py
119
    def __init__(self, pass_msg, fail_msg, default='match'):
120
        """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
121
        If default is match, unspecified files are matched exactly
122
        If default is ignore, unspecified files are ignored
123
        The default default is match.
124
        """
502 by stevenbird
www/apps/tutorialservice/__init__.py
125
        self._pass_msg = pass_msg
126
        self._fail_msg = fail_msg
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
127
        self._default = default
128
        if default == 'ignore':
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
129
            self._default_func = self.true
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
130
        else:
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
131
            self._default_func = self.match
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
132
133
        self._file_tests = {}
134
        self._stdout_test = ('check', self._default_func)
135
        self._stderr_test = ('check', self._default_func)
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
136
        self._exception_test = ('check', self._default_func)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
137
        self._result_test = ('check', self._default_func)
502 by stevenbird
www/apps/tutorialservice/__init__.py
138
        self._code_test = ('check', self._default_func)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
139
140
    def _set_default_function(self, function, test_type):
141
        """"Ensure test type is valid and set function to a default
142
        if not specified"""
143
        
144
        if test_type not in ['norm', 'check']:
145
            raise TestCreationError("Invalid test type in %s" %self._desc)
146
        
147
        if function == '':
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
148
            if test_type == 'norm': function = self.ident
149
            else: function = self.match
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
150
151
        return function
152
153
    def _validate_function(self, function, included_code):
154
        """Create a function object from the given string.
502 by stevenbird
www/apps/tutorialservice/__init__.py
155
        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
156
        """
157
        if not callable(function):
158
            try:
159
                exec "__f__ = %s" %function in included_code
160
            except:
507 by stevenbird
Extended code test framework to support tests on the code string, rather
161
                raise TestCreationError("Invalid function %s" % function)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
162
163
            f = included_code['__f__']
164
165
            if not callable(f):
507 by stevenbird
Extended code test framework to support tests on the code string, rather
166
                raise TestCreationError("Invalid function %s" % function)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
167
        else:
168
            f = function
169
170
        return f
171
172
    def validate_functions(self, included_code):
173
        """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
174
        Also convert their string representations to function objects.
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
175
        This can only be done once all the include code has been specified.
176
        """
177
        (test_type, function) = self._stdout_test
178
        self._stdout_test = (test_type, self._validate_function(function, included_code))
179
        
180
        (test_type, function) = self._stderr_test
181
        self._stderr_test = (test_type, self._validate_function(function, included_code))
716 by mattgiuca
Test Framework: Numerous bug fixes.
182
        
183
        (test_type, function) = self._result_test
184
        self._result_test = (test_type, self._validate_function(function, included_code))
185
        
186
        (test_type, function) = self._exception_test
187
        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
188
189
        for filename, (test_type, function) in self._file_tests.items():
190
            self._file_tests[filename] = (test_type, self._validate_function(function, included_code))
191
            
192
    def add_result_test(self, function, test_type='norm'):
193
        "Test part that compares function return values"
194
        function = self._set_default_function(function, test_type)
195
        self._result_test = (test_type, function)
196
            
197
    def add_stdout_test(self, function, test_type='norm'):
198
        "Test part that compares stdout"
199
        function = self._set_default_function(function, test_type)
200
        self._stdout_test = (test_type, function)
201
202
    def add_stderr_test(self, function, test_type='norm'):
203
        "Test part that compares stderr"
204
        function = self._set_default_function(function, test_type)
205
        self._stderr_test = (test_type, function)
206
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
207
    def add_exception_test(self, function, test_type='norm'):
208
        "Test part that compares stderr"
209
        function = self._set_default_function(function, test_type)
210
        self._exception_test = (test_type, function)
211
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
212
    def add_file_test(self, filename, function, test_type='norm'):
213
        "Test part that compares the contents of a specified file"
214
        function = self._set_default_function(function, test_type)
215
        self._file_tests[filename] = (test_type, function)
216
502 by stevenbird
www/apps/tutorialservice/__init__.py
217
    def add_code_test(self, function, test_type='norm'):
218
        "Test part that examines the supplied code"
219
        function = self._set_default_function(function, test_type)
220
        self._code_test = (test_type, function)
221
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
222
    def _check_output(self, solution_output, attempt_output, test_type, f):
223
        """Compare solution output and attempt output using the
502 by stevenbird
www/apps/tutorialservice/__init__.py
224
        specified comparison function.
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
225
        """
507 by stevenbird
Extended code test framework to support tests on the code string, rather
226
        solution_output = str(solution_output)
227
        attempt_output = str(attempt_output)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
228
            
229
        if test_type == 'norm':
230
            return f(solution_output) == f(attempt_output)
231
        else:
232
            return f(solution_output, attempt_output)
233
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
234
    def _check_code(self, solution, attempt, test_type, f, include_space):
502 by stevenbird
www/apps/tutorialservice/__init__.py
235
        """Compare solution code and attempt code using the
236
        specified comparison function.
237
        """
507 by stevenbird
Extended code test framework to support tests on the code string, rather
238
        if type(f) in types.StringTypes:  # kludge
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
239
            f = eval(str(f), include_space)
502 by stevenbird
www/apps/tutorialservice/__init__.py
240
        if test_type == 'norm':
241
            return f(solution) == f(attempt)
242
        else:
243
            return f(solution, attempt)
244
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
245
    def run(self, solution_data, attempt_data, include_space):
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
246
        """Run the tests to compare the solution and attempt data
328 by mattgiuca
console: Renamed HTML element IDs to prefix "console_".
247
        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
248
        """
249
502 by stevenbird
www/apps/tutorialservice/__init__.py
250
        # check source code itself
251
        (test_type, f) = self._code_test
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
252
        if not self._check_code(solution_data['code'], attempt_data['code'], test_type, f, include_space):       
502 by stevenbird
www/apps/tutorialservice/__init__.py
253
            return 'Unexpected code'
254
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
255
        # check function return value (None for scripts)
256
        (test_type, f) = self._result_test
507 by stevenbird
Extended code test framework to support tests on the code string, rather
257
        if not self._check_output(solution_data['result'], attempt_data['result'], test_type, f):
502 by stevenbird
www/apps/tutorialservice/__init__.py
258
            return 'Unexpected function return value'
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
259
260
        # check stdout
261
        (test_type, f) = self._stdout_test
507 by stevenbird
Extended code test framework to support tests on the code string, rather
262
        if not self._check_output(solution_data['stdout'], attempt_data['stdout'], test_type, f):
502 by stevenbird
www/apps/tutorialservice/__init__.py
263
            return 'Unexpected output'
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
264
265
        #check stderr
266
        (test_type, f) = self._stderr_test
507 by stevenbird
Extended code test framework to support tests on the code string, rather
267
        if not self._check_output(solution_data['stderr'], attempt_data['stderr'], test_type, f):
502 by stevenbird
www/apps/tutorialservice/__init__.py
268
            return 'Unexpected error output'
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
269
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
270
        #check exception
271
        (test_type, f) = self._exception_test
507 by stevenbird
Extended code test framework to support tests on the code string, rather
272
        if not self._check_output(solution_data['exception'], attempt_data['exception'], test_type, f):
502 by stevenbird
www/apps/tutorialservice/__init__.py
273
            return 'Unexpected exception'
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
274
275
        solution_files = solution_data['modified_files']
276
        attempt_files = attempt_data['modified_files']
277
278
        # check files indicated by test
279
        for (filename, (test_type, f)) in self._file_tests.items():
280
            if filename not in solution_files:
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
281
                raise FileNotFoundError(filename)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
282
            elif filename not in attempt_files:
283
                return filename + ' not found'
284
            elif not self._check_output(solution_files[filename], attempt_files[filename], test_type, f):
285
                return filename + ' does not match'
286
287
        if self._default == 'ignore':
288
            return ''
289
290
        # check files found in solution, but not indicated by test
291
        for filename in [f for f in solution_files if f not in self._file_tests]:
292
            if filename not in attempt_files:
293
                return filename + ' not found'
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
294
            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
295
                return filename + ' does not match'
296
297
        # check if attempt has any extra files
298
        for filename in [f for f in attempt_files if f not in solution_files]:
299
            return "Unexpected file found: " + filename
300
301
        # Everything passed with no problems
302
        return ''
303
        
304
class TestCase:
305
    """
306
    A set of tests with a common inputs
307
    """
308
    def __init__(self, name='', function=None, stdin='', filespace=None, global_space=None):
309
        """Initialise with name and optionally, a function to test (instead of the entire script)
310
        The inputs stdin, the filespace and global variables can also be specified at
311
        initialisation, but may also be set later.
312
        """
313
        if global_space == None:
314
            global_space = {}
315
        if filespace == None:
316
            filespace = {}
317
        
318
        self._name = name
319
        
320
        if function == '': function = None
321
        self._function = function
322
        self._list_args = []
323
        self._keyword_args = {}
324
        
325
        # stdin must have a newline at the end for raw_input to work properly
326
        if stdin[-1:] != '\n': stdin += '\n'
327
        
328
        self._stdin = stdin
329
        self._filespace = TestFilespace(filespace)
330
        self._global_space = global_space
331
        self._parts = []
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
332
        self._allowed_exceptions = set()
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
333
334
    def set_stdin(self, stdin):
335
        """ Set the given string as the stdin for this test case"""
336
        self._stdin = stdin
337
338
    def add_file(self, filename, data):
339
        """ Insert the given filename-data pair into the filespace for this test case"""
340
        self._filespace.add_file(filename, data)
341
        
342
    def add_variable(self, variable, value):
343
        """ Add the given varibale-value pair to the initial global environment
344
        for this test case.
507 by stevenbird
Extended code test framework to support tests on the code string, rather
345
        Throw and exception if the value cannot be paresed.
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
346
        """
347
        
348
        try:
349
            self._global_space[variable] = eval(value)
350
        except:
351
            raise TestCreationError("Invalid value for variable %s: %s" %(variable, value))
352
353
    def add_arg(self, value, name=None):
354
        """ Add a value to the argument list. This only applies when testing functions.
355
        By default arguments are not named, but if they are, they become keyword arguments.
356
        """
357
        try:
358
            if name == None or name == '':
359
                self._list_args.append(eval(value))
360
            else:
361
                self._keyword_args[name] = value
362
        except:
363
            raise TestCreationError("Invalid value for function argument: %s" %value)
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
364
365
    def add_exception(self, exception_name):
366
        self._allowed_exceptions.add(exception_name)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
367
        
368
    def add_part(self, test_part):
369
        """ Add a TestPart to this test case"""
370
        self._parts.append(test_part)
371
372
    def validate_functions(self, included_code):
373
        """ Validate all the functions in each part in this test case
374
        This can only be done once all the include code has been specified.
375
        """
376
        for part in self._parts:
377
            part.validate_functions(included_code)
378
379
    def get_name(self):
380
        """ Get the name of the test case """
381
        return self._name
382
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
383
    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
384
        """ Run the solution and the attempt with the inputs specified for this test case.
385
        Then pass the outputs to each test part and collate the results.
386
        """
387
        case_dict = {}
388
        case_dict['name'] = self._name
389
        
390
        # Run solution
391
        try:
392
            global_space_copy = copy.deepcopy(self._global_space)
393
            solution_data = self._execstring(solution, global_space_copy)
394
            
395
            # if we are just testing a function
396
            if not self._function == None:
397
                if self._function not in global_space_copy:
398
                    raise FunctionNotFoundError(self._function)
716 by mattgiuca
Test Framework: Numerous bug fixes.
399
                func_to_exec = lambda: global_space_copy[self._function](
400
                                    *self._list_args, **self._keyword_args)
401
                solution_data = self._run_function(func_to_exec, solution)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
402
                
403
        except:
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
404
            raise ScriptExecutionError(sys.exc_info())
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
405
406
        # Run student attempt
407
        try:
408
            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.
409
            attempt_data = self._execstring(attempt_code, global_space_copy)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
410
            
411
            # if we are just testing a function
412
            if not self._function == None:
413
                if self._function not in global_space_copy:
414
                    raise FunctionNotFoundError(self._function)
716 by mattgiuca
Test Framework: Numerous bug fixes.
415
                func_to_exec = lambda: global_space_copy[self._function](
416
                    *self._list_args, **self._keyword_args)
417
                attempt_data = self._run_function(func_to_exec, attempt_code)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
418
        except:
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
419
            case_dict['exception'] = ScriptExecutionError(sys.exc_info()).to_dict()
304 by dilshan_a
Added documentation of output of TestSuite.
420
            case_dict['passed'] = False
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
421
            return case_dict
422
        
423
        results = []
304 by dilshan_a
Added documentation of output of TestSuite.
424
        passed = True
425
        
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
426
        # generate results
427
        for test_part in self._parts:
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
428
            try:
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
429
                result = test_part.run(solution_data, attempt_data, include_space)
306 by dilshan_a
Consolidated SolutionError and AttemptError into ScriptExecutionError.
430
            except:
431
                raise TestError(sys.exc_info())
432
            
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
433
            result_dict = {}
502 by stevenbird
www/apps/tutorialservice/__init__.py
434
            result_dict['description'] = test_part._pass_msg
495 by stevenbird
Fixes to permit content authors to produce nicer diagnostic responses as a result
435
            result_dict['passed'] = (result == '')
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
436
            if result_dict['passed'] == False:
437
                result_dict['error_message'] = result
502 by stevenbird
www/apps/tutorialservice/__init__.py
438
                result_dict['description'] = test_part._fail_msg
304 by dilshan_a
Added documentation of output of TestSuite.
439
                passed = False
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
440
                
441
            results.append(result_dict)
442
514 by stevenbird
More flexible control of test_case_parts via optional flag
443
            # Do we continue the test_parts after one of them has failed?
444
            if not passed and stop_on_fail:
445
                break;
446
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
447
        case_dict['parts'] = results
304 by dilshan_a
Added documentation of output of TestSuite.
448
        case_dict['passed'] = passed
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
449
450
        return case_dict
451
                
452
    def _execfile(self, filename, global_space):
453
        """ Execute the file given by 'filename' in global_space, and return the outputs. """
454
        self._initialise_global_space(global_space)
716 by mattgiuca
Test Framework: Numerous bug fixes.
455
        data = self._run_function(lambda: execfile(filename, global_space),
456
            code = open(filename).read())
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
457
        return data
458
459
    def _execstring(self, string, global_space):
460
        """ Execute the given string in global_space, and return the outputs. """
461
        self._initialise_global_space(global_space)
305 by dilshan_a
Neated up execution of strings in TestCase.
462
        
463
        def f():
464
            exec string in global_space
465
            
716 by mattgiuca
Test Framework: Numerous bug fixes.
466
        data = self._run_function(f, code=string)
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
467
        return data
468
469
    def _initialise_global_space(self, global_space):
470
        """ Modify the provided global_space so that file, open and raw_input are redefined
471
        to use our methods instead.
472
        """
473
        self._current_filespace_copy = self._filespace.copy()
474
        global_space['file'] = lambda filename, mode='r', bufsize=-1: self._current_filespace_copy.openfile(filename, mode)
475
        global_space['open'] = global_space['file']
476
        global_space['raw_input'] = lambda x=None: raw_input()
477
        return global_space
478
716 by mattgiuca
Test Framework: Numerous bug fixes.
479
    def _run_function(self, function, code):
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
480
        """ Run the provided function with the provided stdin, capturing stdout and stderr
481
        and the return value.
482
        Return all the output data.
716 by mattgiuca
Test Framework: Numerous bug fixes.
483
        code: The full text of the code, which needs to be stored as part of
484
        the returned dictionary.
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
485
        """
486
        import sys, StringIO
487
        sys_stdout, sys_stdin, sys_stderr = sys.stdout, sys.stdin, sys.stderr
488
489
        output_stream, input_stream, error_stream = StringIO.StringIO(), StringIO.StringIO(self._stdin), StringIO.StringIO()
490
        sys.stdout, sys.stdin, sys.stderr = output_stream, input_stream, error_stream
491
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
492
        result = None
493
        exception_name = None
494
        
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
495
        try:
305 by dilshan_a
Neated up execution of strings in TestCase.
496
            result = function()
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
497
        except:
498
            sys.stdout, sys.stdin, sys.stderr = sys_stdout, sys_stdin, sys_stderr
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
499
            exception_name = sys.exc_info()[0].__name__
500
            if exception_name not in self._allowed_exceptions:
501
                raise
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
502
        
503
        sys.stdout, sys.stdin, sys.stderr = sys_stdout, sys_stdin, sys_stderr
504
505
        self._current_filespace_copy.flush_all()
506
            
716 by mattgiuca
Test Framework: Numerous bug fixes.
507
        return {'code': code,
508
                'result': result,
299 by dilshan_a
Test framework now handles exceptions as valid outputs for scripts.
509
                'exception': exception_name,
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
510
                'stdout': output_stream.getvalue(),
511
                'stderr': output_stream.getvalue(),
512
                'modified_files': self._current_filespace_copy.get_modified_files()}
513
514
class TestSuite:
515
    """
513 by stevenbird
test/test_framework/*, exercises/sample/*
516
    The complete collection of test cases for a given exercise
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
517
    """
518
    def __init__(self, name, solution=None):
513 by stevenbird
test/test_framework/*, exercises/sample/*
519
        """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
520
        The solution may be specified later.
521
        """
522
        self._solution = solution
523
        self._name = name
524
        self._tests = []
525
        self.add_include_code("")
526
527
    def add_solution(self, solution):
513 by stevenbird
test/test_framework/*, exercises/sample/*
528
        " Specify the solution script for this exercise "
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
529
        self._solution = solution
530
531
    def has_solution(self):
523 by stevenbird
Adding ReStructured Text preprocessing of exercise descriptions,
532
        " Returns true if a solution has been provided "
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
533
        return self._solution != None
534
535
    def add_include_code(self, include_code = ''):
536
        """ Add include code that may be used by the test cases during
537
        comparison of outputs.
538
        """
539
        
540
        # if empty, make sure it can still be executed
541
        if include_code == "":
542
            include_code = "pass"
543
        self._include_code = str(include_code)
544
        
545
        include_space = {}
546
        try:
547
            exec self._include_code in include_space
548
        except:
549
            raise TestCreationError("Bad include code")
550
551
        self._include_space = include_space
552
    
553
    def add_case(self, test_case):
554
        """ Add a TestCase, then validate all functions inside test case
555
        now that the include code is known
556
        """
557
        self._tests.append(test_case)
558
        test_case.validate_functions(self._include_space)
559
502 by stevenbird
www/apps/tutorialservice/__init__.py
560
    def run_tests(self, attempt_code, stop_on_fail=False):
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
561
        " Run all test cases and collate the results "
562
        
513 by stevenbird
test/test_framework/*, exercises/sample/*
563
        exercise_dict = {}
564
        exercise_dict['name'] = self._name
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
565
        
566
        test_case_results = []
309 by dilshan_a
Added a passed key to return value of problem suite.
567
        passed = True
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
568
        for test in self._tests:
519 by stevenbird
Fixed bug in test framework. Code given in the <include> section was
569
            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
570
            if 'exception' in result_dict and result_dict['exception']['critical']:
571
                # critical error occured, running more cases is useless
572
                # FunctionNotFound, Syntax, Indentation
513 by stevenbird
test/test_framework/*, exercises/sample/*
573
                exercise_dict['critical_error'] = result_dict['exception']
574
                exercise_dict['passed'] = False
575
                return exercise_dict
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
576
            
577
            test_case_results.append(result_dict)
304 by dilshan_a
Added documentation of output of TestSuite.
578
            
309 by dilshan_a
Added a passed key to return value of problem suite.
579
            if not result_dict['passed']:
580
                passed = False
581
                if stop_on_fail:
582
                    break
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
583
513 by stevenbird
test/test_framework/*, exercises/sample/*
584
        exercise_dict['cases'] = test_case_results
585
        exercise_dict['passed'] = passed
586
        return exercise_dict
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
587
588
    def get_name(self):
589
        return self._name
590
591
class TestFilespace:
592
    """
593
    Our dummy file system which is accessed by code being tested.
594
    Implemented as a dictionary which maps filenames to strings
595
    """
596
    def __init__(self, files=None):
597
        "Initialise, optionally with filename-filedata pairs"
598
599
        if files == None:
600
            files = {}
601
602
        # dict mapping files to strings
603
        self._files = {}
604
        self._files.update(files)
605
        # set of file names
606
        self._modified_files = set([])
607
        # dict mapping files to stringIO objects
608
        self._open_files = {}
609
610
    def add_file(self, filename, data):
611
        " Add a file to the filespace "
612
        self._files[filename] = data
613
614
    def openfile(self, filename, mode='r'):
615
        """ Open a file from the filespace with the given mode.
616
        Return a StringIO subclass object with the file contents.
617
        """
309 by dilshan_a
Added a passed key to return value of problem suite.
618
        # currently very messy, needs to be cleaned up
619
        # Probably most of this should be in the initialiser to the TestStringIO
620
        
294 by mattgiuca
Added application: tutorialservice. Will be used as the Ajax backend for
621
        import re
622
623
        if filename in self._open_files:
624
            raise IOError("File already open: %s" %filename)
625
626
        if not re.compile("[rwa][+b]{0,2}").match(mode):
627
            raise IOError("invalid mode %s" %mode)
628
        
629
        ## TODO: validate filename?
630
        
631
        mode.replace("b",'')
632
        
633
        # initialise the file properly (truncate/create if required)
634
        if mode[0] == 'w':
635
            self._files[filename] = ''
636
            self._modified_files.add(filename)
637
        elif filename not in self._files:
638
            if mode[0] == 'a':
639
                self._files[filename] = ''
640
                self._modified_files.add(filename)
641
            else:
642
                raise IOError(2, "Access to file denied: %s" %filename)
643
644
        # for append mode, remember the existing data
645
        if mode[0] == 'a':
646
            existing_data = self._files[filename]
647
        else:
648
            existing_data = ""
649
650
        # determine what operations are allowed
651
        reading_ok = (len(mode) == 2 or mode[0] == 'r')
652
        writing_ok = (len(mode) == 2 or mode[0] in 'wa')
653
654
        # for all writing modes, start off with blank file
655
        if mode[0] == 'w':
656
            initial_data = ''
657
        else:
658
            initial_data = self._files[filename]
659
660
        file_object = TestStringIO(initial_data, filename, self, reading_ok, writing_ok, existing_data)
661
        self._open_files[filename] = file_object
662
        
663
        return file_object
664
665
    def flush_all(self):
666
        """ Flush all open files
667
        """
668
        for file_object in self._open_files.values():
669
            file_object.flush()
670
671
    def updatefile(self,filename, data):
672
        """ Callback function used by an open file to inform when it has been updated.
673
        """
674
        if filename in self._open_files:
675
            self._files[filename] = data
676
            if self._open_files[filename].is_modified():
677
                self._modified_files.add(filename)
678
        else:
679
            raise IOError(2, "Access to file denied: %s" %filename)
680
681
    def closefile(self, filename):
682
        """ Callback function used by an open file to inform when it has been closed.
683
        """
684
        if filename in self._open_files:
685
            del self._open_files[filename]
686
687
    def get_modified_files(self):
688
        """" A subset of the filespace containing only those files which have been
689
        modified
690
        """
691
        modified_files = {}
692
        for filename in self._modified_files:
693
            modified_files[filename] = self._files[filename]
694
695
        return modified_files
696
697
    def get_open_files(self):
698
        " Return the names of all open files "
699
        return self._open_files.keys()
700
            
701
    def copy(self):
702
        """ Return a copy of the current filespace.
703
        Only the files are copied, not the modified or open file lists.
704
        """
705
        self.flush_all()
706
        return TestFilespace(self._files)
707
708
class TestStringIO(StringIO.StringIO):
709
    """
710
    A subclass of StringIO which acts as a file in our dummy file system
711
    """
712
    def __init__(self, string, filename, filespace, reading_ok, writing_ok, existing_data):
713
        """ Initialise with the filedata, file name and infomation on what ops are
714
        acceptable """
715
        StringIO.StringIO.__init__(self, string)
716
        self._filename = filename
717
        self._filespace = filespace
718
        self._reading_ok = reading_ok
719
        self._writing_ok = writing_ok
720
        self._existing_data = existing_data
721
        self._modified = False
722
        self._open = True
723
724
    # Override all standard file ops. Make sure that they are valid with the given
725
    # permissions and if so then call the corresponding method in StringIO
726
    
727
    def read(self, *args):
728
        if not self._reading_ok:
729
            raise IOError(9, "Bad file descriptor")
730
        else:
731
            return StringIO.StringIO.read(self, *args)
732
733
    def readline(self, *args):
734
        if not self._reading_ok:
735
            raise IOError(9, "Bad file descriptor")
736
        else:
737
            return StringIO.StringIO.readline(self, *args)
738
739
    def readlines(self, *args):
740
        if not self._reading_ok:
741
            raise IOError(9, "Bad file descriptor")
742
        else:
743
            return StringIO.StringIO.readlines(self, *args)
744
745
    def seek(self, *args):
746
        if not self._reading_ok:
747
            raise IOError(9, "Bad file descriptor")
748
        else:
749
            return StringIO.StringIO.seek(self, *args)
750
751
    def truncate(self, *args):
752
        self._modified = True
753
        if not self._writing_ok:
754
            raise IOError(9, "Bad file descriptor")
755
        else:
756
            return StringIO.StringIO.truncate(self, *args)
757
        
758
    def write(self, *args):
759
        self._modified = True
760
        if not self._writing_ok:
761
            raise IOError(9, "Bad file descriptor")
762
        else:
763
            return StringIO.StringIO.write(self, *args)
764
765
    def writelines(self, *args):
766
        self._modified = True
767
        if not self._writing_ok:
768
            raise IOError(9, "Bad file descriptor")
769
        else:
770
            return StringIO.StringIO.writelines(self, *args)
771
772
    def is_modified(self):
773
        " Return true if the file has been written to, or truncated"
774
        return self._modified
775
        
776
    def flush(self):
777
        " Update the contents of the filespace with the new data "
778
        self._filespace.updatefile(self._filename, self._existing_data+self.getvalue())
779
        return StringIO.StringIO.flush(self)
780
781
    def close(self):
782
        " Flush the file and close it "
783
        self.flush()
784
        self._filespace.closefile(self._filename)
785
        return StringIO.StringIO.close(self)
786
787
##def get_function(filename, function_name):
788
##	import compiler
789
##	mod = compiler.parseFile(filename)
790
##	for node in mod.node.nodes:
791
##		if isinstance(node, compiler.ast.Function) and node.name == function_name:
792
##			return node
793
##