""" Uses conventional stubbing to grade RoseGraphics projects. See main() for examples of actual grading. Created on Aug 27, 2015. Written by: hays. """ # from unittest.mock import patch, MagicMock import imp import rosegraphics as rg from sys import stderr canvases = [] class RoseWindowStub(): def __init__(self, width=400, height=300, title='Rose Graphics', color='black', canvas_color=None, make_initial_canvas=True): canvas_color = "white" # FIXME self._is_closed = False self.width = width self.height = height self.initial_canvas = RoseCanvasStub(self, width, height, canvas_color) def render(self): pass def get_next_mouse_click(self): return rg.Point(0, 0) def close_on_mouse_click(self): return None def continue_on_mouse_click(self, message= 'To continue, click anywhere in this window', x_position=None, y_position=None, close_it=False, erase_it=True): return None class RoseCanvasStub(): def __init__(self, window, width, height, canvas_color): # super().__init__(window, width, height, canvas_color) canvases.append(self) self.recordedshapes = [] self.shapes = [] def _draw(self, shape): # super()._draw(shape) self.recordedshapes.append(shape) def render(self, seconds_to_pause=None): # super().render() # don't pause pass class RoseGraphicsGrader: def __init__(self, student_module_name, # "problem3" path_to_student_module, # "problem3.py" solution_module_name, # "problem3_solution" path_to_solution_module): # "problem3_solution.py" rg.RoseCanvas = RoseCanvasStub rg.RoseWindow = RoseWindowStub self.student_module = imp.load_source(student_module_name, path_to_student_module) self.solution_module = imp.load_source(solution_module_name, path_to_solution_module) for module in [self.student_module, self.solution_module]: module.rg.RoseCanvas = RoseCanvasStub module.rg.RoseWindow = RoseWindowStub def runTest(self, runner): """ Calls the given module runner on the student code and the solution code. Compares the generated shapes. For instance, if the students write a function that takes a window and an integer, your runner might look like: def run_problem3a(module): window=rg.RoseWindow(500,500) module.problem3a(window, 5) """ global canvases canvases = [] runner(self.student_module) student_canvases = canvases canvases = [] runner(self.solution_module) solution_canvases = canvases # print("#canvases: ", len(student_canvases), len(solution_canvases)) if not len(student_canvases) == len(solution_canvases): raise AssertionError("Wrong number of shapes: " + str(len(student_canvases)) + " vs " + str(len(solution_canvases))) # compare shapes for i in range(len(student_canvases)): # this canvas should have same shapes as solution student_canvas = student_canvases[i] solution_canvas = solution_canvases[i] # print(student_canvas.recordedshapes, solution_canvas.recordedshapes) for shape in student_canvas.recordedshapes: if not shape in solution_canvas.recordedshapes: raise AssertionError("Unexpected shape: " + str(shape)) def main(): """ This method is really a series of self-tests, but you can use them as a basis for your own grading. """ # sanity check: compare a student's work against itself. Should pass! grader = RoseGraphicsGrader('problem3_student', 'tests/problem3_solution.py', 'problem3_solution', 'tests/problem3_solution.py') def myrunner(module): """" This runner runs the module's test_problem3 function as-is. As you might imagine, this can be a BAD idea because students often hack the test code. """ module.test_problem3() grader.runTest(myrunner) print("Test 1 passed") try: # test 2: compare shape with wrong color against correct solution. Should fail! grader = RoseGraphicsGrader('problem3_student', 'tests/problem3_bugged.py', 'problem3_solution', 'tests/problem3_solution.py') def myrunner2(module): """ This runner is a copy/paste from the solution module. This is a BETTER way to write a runner. """ # rectangle tunnel window = rg.RoseWindow(400, 400) points = [rg.Point(10, 10), rg.Point(10, 350), rg.Point(390, 350), rg.Point(390, 10)] colors = ["red", "black", "white"] module.problem3(window, points, 3, colors) window.render() window.close_on_mouse_click() # triangle tunnel window = rg.RoseWindow(400, 400) points = [rg.Point(200, 10), rg.Point(100, 310), rg.Point(400, 110)] colors = ["red", "white", "black"] module.problem3(window, points, 5, colors) window.render() window.close_on_mouse_click() # pentagon tunnel window = rg.RoseWindow(400, 400) points = [rg.Point(200, 10), rg.Point(0, 110), rg.Point(100, 310), rg.Point(300, 310), rg.Point(400, 110)] colors = ["red", "orange", "yellow", "lightgreen", "blue", "purple"] module.problem3(window, points, 20, colors) window.render() window.close_on_mouse_click() grader.runTest(myrunner2) print("Test 2 FAILED to find the difference in color", file=stderr) except AssertionError: print("Test 2 passed") # test 3: compare a student's work that has the wrong number of shapes. Should fail! try: grader = RoseGraphicsGrader('problem3_student', 'tests/problem3_bugged.py', 'problem3_solution', 'tests/problem3_solution.py') def myrunner3(module): """ In theory, you can change the context of a function, but I don't know how in Python. """ # grader.solution_module.test_problem3.apply(module) module.test_problem3() grader.runTest(myrunner3) print("Test 3 FAILED to find the difference in # shapes", file=stderr) except AssertionError: print("Test 3 passed") #----------------------------------------------------------------------- # If this module is running at the top level (as opposed to being # imported by another module), then call the 'main' function. #----------------------------------------------------------------------- if __name__ == '__main__': main()