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

« back to all changes in this revision

Viewing changes to www/apps/tutorialservice/test/TestFramework.py

  • Committer: dcoles
  • Date: 2008-08-19 06:44:05 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1029
Tutorial Service: Ported the tutorial service to the console so that all 
student code should now be run inside the jail. This means that the code will 
also be contrained by trampoline's ulimits and will no longer need to run as 
the webserver (with all the badness that entails).

* TestFramework has been modifed to make eqvivalent calls to the console.
* Tutorial service now starts up a console for each attempt.
* Modifications to python-console script and console module to allow new calls 
needed.
* Added a FakeObject class to util to use to represent things that can not be 
pickled. (Ideally should be in the console module, but the console module can't 
be imported into the jail at the moment - relies on a full lib/conf/conf.py).  
Might be possible to make these fake objects able to call the console too?

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
import sys, StringIO, copy
27
27
import types
28
28
 
 
29
from common import util
 
30
 
29
31
# student error or author error
30
32
# errors in student code get handled internally
31
33
# errors in solution code get passed up
305
307
    """
306
308
    A set of tests with a common inputs
307
309
    """
308
 
    def __init__(self, name='', function=None, stdin='', filespace=None, global_space=None):
 
310
    def __init__(self, console, name='', function=None, stdin='', filespace=None, global_space=None):
309
311
        """Initialise with name and optionally, a function to test (instead of the entire script)
310
312
        The inputs stdin, the filespace and global variables can also be specified at
311
313
        initialisation, but may also be set later.
312
314
        """
 
315
        self._console = console
 
316
 
313
317
        if global_space == None:
314
318
            global_space = {}
315
319
        if filespace == None:
333
337
 
334
338
    def set_stdin(self, stdin):
335
339
        """ Set the given string as the stdin for this test case"""
 
340
        # TODO: Set stdin of console
336
341
        self._stdin = stdin
337
342
 
338
343
    def add_file(self, filename, data):
339
344
        """ Insert the given filename-data pair into the filespace for this test case"""
 
345
        # TODO: Add the file to the console
340
346
        self._filespace.add_file(filename, data)
341
347
        
342
348
    def add_variable(self, variable, value):
343
349
        """ Add the given varibale-value pair to the initial global environment
344
 
        for this test case.
345
 
        Throw and exception if the value cannot be paresed.
 
350
        for this test case. The value is the string repr() of an actual value.
 
351
        Throw an exception if the value cannot be paresed.
346
352
        """
347
353
        
348
354
        try:
394
400
            
395
401
            # if we are just testing a function
396
402
            if not self._function == None:
397
 
                if self._function not in global_space_copy:
 
403
                if self._function not in solution_data['globals']:
398
404
                    raise FunctionNotFoundError(self._function)
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)
 
405
                solution_data = self._run_function(self._function,
 
406
                    self._list_args, self._keyword_args, solution)
402
407
                
403
408
        except:
404
409
            raise ScriptExecutionError(sys.exc_info())
410
415
            
411
416
            # if we are just testing a function
412
417
            if not self._function == None:
413
 
                if self._function not in global_space_copy:
 
418
                if self._function not in attempt_data['globals']:
414
419
                    raise FunctionNotFoundError(self._function)
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)
 
420
                attempt_data = self._run_function(self._function,
 
421
                    self._list_args, self._keyword_args, attempt_code)
418
422
        except:
419
423
            case_dict['exception'] = ScriptExecutionError(sys.exc_info()).to_dict()
420
424
            case_dict['passed'] = False
457
461
        return data
458
462
 
459
463
    def _execstring(self, string, global_space):
460
 
        """ Execute the given string in global_space, and return the outputs. """
 
464
        """ Execute the given string in global_space, and return the outputs. 
 
465
        """
461
466
        self._initialise_global_space(global_space)
462
467
        
463
 
        def f():
464
 
            exec string in global_space
465
 
            
466
 
        data = self._run_function(f, code=string)
467
 
        return data
 
468
        inspection = self._console.inspect(string)
 
469
 
 
470
        exception_name = None
 
471
        if 'exception' in inspection:
 
472
            exception = inspection['exception']['except']
 
473
            exception_name = type(exception).__name__
 
474
            raise(exception)
 
475
 
 
476
        return {'code': string,
 
477
                'result': None,
 
478
                'globals': inspection['globals'],
 
479
                'exception': exception_name, # Hmmm... odd? Is this right?
 
480
                'stdout': inspection['stdout'],
 
481
                'stderr': inspection['stderr'],
 
482
                'modified_files': None}
468
483
 
469
484
    def _initialise_global_space(self, global_space):
470
485
        """ Modify the provided global_space so that file, open and raw_input are redefined
471
486
        to use our methods instead.
472
487
        """
 
488
        self._console.flush(global_space)
473
489
        self._current_filespace_copy = self._filespace.copy()
474
490
        global_space['file'] = lambda filename, mode='r', bufsize=-1: self._current_filespace_copy.openfile(filename, mode)
475
491
        global_space['open'] = global_space['file']
476
492
        global_space['raw_input'] = lambda x=None: raw_input()
477
493
        return global_space
478
494
 
479
 
    def _run_function(self, function, code):
 
495
    def _run_function(self, function, args, kwargs, code):
480
496
        """ Run the provided function with the provided stdin, capturing stdout and stderr
481
497
        and the return value.
482
498
        Return all the output data.
483
499
        code: The full text of the code, which needs to be stored as part of
484
500
        the returned dictionary.
485
501
        """
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
 
 
492
 
        result = None
 
502
        s_args = map(repr, args)
 
503
        s_kwargs = dict(zip(kwargs.keys(), map(repr, kwargs.values())))
 
504
        call = self._console.call(function, *s_args, **s_kwargs)
 
505
 
493
506
        exception_name = None
494
 
        
495
 
        try:
496
 
            result = function()
497
 
        except:
498
 
            sys.stdout, sys.stdin, sys.stderr = sys_stdout, sys_stdin, sys_stderr
499
 
            exception_name = sys.exc_info()[0].__name__
500
 
            if exception_name not in self._allowed_exceptions:
501
 
                raise
502
 
        
503
 
        sys.stdout, sys.stdin, sys.stderr = sys_stdout, sys_stdin, sys_stderr
 
507
        if 'exception' in call:
 
508
            exception = call['exception']['except']
 
509
            exception_name = type(exception).__name__
 
510
            raise(exception)
504
511
 
505
 
        self._current_filespace_copy.flush_all()
506
 
            
507
512
        return {'code': code,
508
 
                'result': result,
 
513
                'result': call['result'],
509
514
                'exception': exception_name,
510
 
                'stdout': output_stream.getvalue(),
511
 
                'stderr': output_stream.getvalue(),
512
 
                'modified_files': self._current_filespace_copy.get_modified_files()}
 
515
                'stdout': call['stdout'],
 
516
                'stderr': call['stderr'],
 
517
                'modified_files': None}
513
518
 
514
519
class TestSuite:
515
520
    """
516
521
    The complete collection of test cases for a given exercise
517
522
    """
518
 
    def __init__(self, name, solution=None):
 
523
    def __init__(self, name, console, solution=None):
519
524
        """Initialise with the name of the test suite (the exercise name) and the solution.
520
525
        The solution may be specified later.
521
526
        """
522
527
        self._solution = solution
523
528
        self._name = name
524
529
        self._tests = []
 
530
        self._console = console
525
531
        self.add_include_code("")
526
532
 
527
533
    def add_solution(self, solution):
558
564
        test_case.validate_functions(self._include_space)
559
565
 
560
566
    def run_tests(self, attempt_code, stop_on_fail=False):
561
 
        " Run all test cases and collate the results "
 
567
        " Run all test cases on the specified console and collate the results "
562
568
        
563
569
        exercise_dict = {}
564
570
        exercise_dict['name'] = self._name