Merge lp:~amacleod/game/functional-test-convenience into lp:game

Proposed by Allister MacLeod on 2011-02-09
Status: Needs review
Proposed branch: lp:~amacleod/game/functional-test-convenience
Merge into: lp:game
Diff against target: 402 lines (+230/-38) 4 files modified
To merge this branch: bzr merge lp:~amacleod/game/functional-test-convenience
Reviewer Review Type Date Requested Status
Jean-Paul Calderone 2011-02-09 Pending
Review via email: mp+49018@code.launchpad.net

Description of the Change

This is a fix for bug 116710 that allows you to give quick feedback to the functional tests. You can now press Y or P to pass a test, N or F to fail it. Ending the test with Esc or Q reverts to the old raw_input style. Ending tests that play animations too early will result in test failure.

To post a comment you must log in.

Unmerged revisions

45. By Allister MacLeod on 2011-02-09

Implemented a convenient test reporting controller that allows
single keystrokes to indicate success or failure of functional
tests.

Preview Diff

1=== modified file 'game/functional/test_input.py'
2--- game/functional/test_input.py 2011-01-23 17:50:23 +0000
3+++ game/functional/test_input.py 2011-02-09 05:54:40 +0000
4@@ -10,28 +10,14 @@
5 from game.vector import Vector
6
7
8-class QuittableController(object):
9- # XXX Make an interface for the controller and verify these fakes.
10- def __init__(self, reactor, window):
11- self.player = Player(Vector(0, 0, 0), 0, reactor.seconds)
12- self.window = window
13-
14-
15+class StdoutReportingController(object):
16+ # XXX Make an interface for controllers.
17 def keyUp(self, key):
18- if key == K_q:
19- self.window.stop()
20-
21+ pass
22
23 def keyDown(self, key):
24 pass
25
26-
27- def mouseMotion(self, pos, rel, buttons):
28- pass
29-
30-
31-
32-class StdoutReportingController(QuittableController):
33 def mouseMotion(self, pos, rel, buttons):
34 """
35 Report to standard out the direction of the mouse movement.
36@@ -52,7 +38,8 @@
37 Tests for mouse input.
38 """
39 def _movementTest(self, grab):
40- self.window.submitTo(StdoutReportingController(reactor, self.window))
41+ controller = self.controller(self.window, StdoutReportingController())
42+ self.window.submitTo(controller)
43 if grab:
44 reactor.callLater(0, self.window._handleEvent, Event(MOUSEBUTTONUP))
45 return self.window.go()
46@@ -78,5 +65,5 @@
47 """
48 Clicking on the window grabs the mouse. Clicking again releases it.
49 """
50- self.window.submitTo(QuittableController(reactor, self.window))
51+ self.window.submitTo(self.controller(self.window))
52 return self.window.go()
53
54=== modified file 'game/functional/test_view3d.py'
55--- game/functional/test_view3d.py 2010-11-04 00:33:27 +0000
56+++ game/functional/test_view3d.py 2011-02-09 05:54:40 +0000
57@@ -41,6 +41,7 @@
58 """
59 self.window.scene.add(
60 Sphere(self.origin(0, 0, -3), 0.5, Color(1.0, 0.0, 0.0)))
61+ self.window.submitTo(self.controller(self.window))
62 return self.window.go()
63
64
65@@ -51,10 +52,16 @@
66 def move(amount):
67 setattr(
68 camera.position, axis, getattr(camera.position, axis) + amount)
69- for i in range(100):
70- reactor.callLater(1 + (i / 100.0), move, 0.01)
71- for i in range(100):
72- reactor.callLater(2 + (i / 100.0), move, -0.01)
73+ animations = []
74+ for i in range(100):
75+ c = reactor.callLater(1 + (i / 100.0), move, 0.01)
76+ animations.append(c)
77+ for i in range(100):
78+ c = reactor.callLater(2 + (i / 100.0), move, -0.01)
79+ animations.append(c)
80+ controller = self.controller(self.window)
81+ controller.setScheduleBacklog(animations)
82+ self.window.submitTo(controller)
83 return self.window.go()
84
85
86@@ -91,10 +98,16 @@
87 setattr(
88 camera.orientation, axis,
89 getattr(camera.orientation, axis) + amount)
90- for i in range(200):
91- reactor.callLater(1 + (i / 100.0), rotate, -0.2)
92- for i in range(200):
93- reactor.callLater(3 + (i / 100.0), rotate, 0.2)
94+ animations = []
95+ for i in range(200):
96+ c = reactor.callLater(1 + (i / 100.0), rotate, -0.2)
97+ animations.append(c)
98+ for i in range(200):
99+ c = reactor.callLater(3 + (i / 100.0), rotate, 0.2)
100+ animations.append(c)
101+ controller = self.controller(self.window)
102+ controller.setScheduleBacklog(animations)
103+ self.window.submitTo(controller)
104 return self.window.go()
105
106
107@@ -132,6 +145,7 @@
108 Sphere(self.origin(0, 0, -2), 0.5, Color(1.0, 0.0, 0.0)))
109 self.window.scene.add(
110 Sphere(self.origin(0, 0.5, -5), 2.0, Color(0.0, 1.0, 0.0)))
111+ self.window.submitTo(self.controller(self.window))
112 return self.window.go()
113
114
115@@ -152,6 +166,7 @@
116 A single piece of green terrain should appear.
117 """
118 self.terrain.set(0, 0, 0, loadTerrainFromString("G"))
119+ self.window.submitTo(self.controller(self.window))
120 return self.window.go()
121
122
123@@ -172,6 +187,7 @@
124 "GMD\n"
125 "MDG\n"
126 "DGM\n"))
127+ self.window.submitTo(self.controller(self.window))
128 return self.window.go()
129
130
131@@ -181,6 +197,7 @@
132 empty voxel.
133 """
134 self.terrain.set(0, 0, 0, loadTerrainFromString("G_M"))
135+ self.window.submitTo(self.controller(self.window))
136 return self.window.go()
137
138
139@@ -191,7 +208,7 @@
140 """
141 player = Player(self.origin(0, 1, 0), 2.0, reactor.seconds)
142 controller = PlayerController(player)
143- self.window.submitTo(controller)
144+ self.window.submitTo(self.controller(self.window, controller))
145 self.terrain.set(0, 0, 0, loadTerrainFromString(
146 "GMD\n"
147 "MDG\n"
148@@ -205,16 +222,23 @@
149 green cube should disappear.
150 """
151 self.window.scene.camera.position = Vector(2, 1.1, 5)
152+ animations = []
153
154- reactor.callLater(
155+ c = reactor.callLater(
156 1.0, self.terrain.set, 1, 0, 0, loadTerrainFromString("G"))
157+ animations.append(c)
158
159- reactor.callLater(
160+ c = reactor.callLater(
161 2.0, self.terrain.set, 0, 0, 0, loadTerrainFromString("M"))
162+ animations.append(c)
163
164- reactor.callLater(
165+ c = reactor.callLater(
166 3.0, self.terrain.set, 1, 0, 0, loadTerrainFromString("_"))
167+ animations.append(c)
168
169+ controller = self.controller(self.window)
170+ controller.setScheduleBacklog(animations)
171+ self.window.submitTo(controller)
172 return self.window.go()
173
174
175@@ -234,6 +258,7 @@
176 player.turn(0, 1)
177 c[0] = reactor.callLater(0.01, turn)
178 c = [reactor.callLater(0.01, turn)]
179+ self.window.submitTo(self.controller(self.window))
180 d = self.window.go()
181 def finish(passthrough):
182 c[0].cancel()
183
184=== modified file 'game/functional/util.py'
185--- game/functional/util.py 2010-10-28 12:50:19 +0000
186+++ game/functional/util.py 2011-02-09 05:54:40 +0000
187@@ -1,17 +1,193 @@
188-
189 """
190 Common code for functional tests.
191+
192 """
193
194+from pygame import (K_y, K_n, K_p, K_f, K_q, K_ESCAPE)
195+
196+
197 class FunctionalTestMixin:
198 def setUp(self):
199 # XXX PRIVATE VARIABLE USAGE ZOMG
200 print getattr(self, self._testMethodName).__doc__
201-
202+ self.testStatusReporter = TestReporter()
203+
204+ def controller(self, window, innerController=None):
205+ """
206+ Convenience method for constructing a TestReportingController
207+ that will gather user input as to the status of the current
208+ test.
209+
210+ """
211+ c = TestReportingController(window, innerController)
212+ c.subscribe(self.testStatusReporter)
213+ return c
214
215 def tearDown(self):
216- if not raw_input("Did it work?").lower().startswith('y'):
217- self.fail("User specified test failure")
218-
219-
220-
221+ if self.testStatusReporter.status is None:
222+ if raw_input("Did it work?").lower().startswith('y'):
223+ self.testStatusReporter.status = True
224+ if not self.testStatusReporter.status:
225+ self.fail(str(self.testStatusReporter))
226+
227+
228+class TestReporter(object):
229+ """
230+ Simple test reporter that can subscribe to a
231+ TestReportingController.
232+
233+ """
234+ def __init__(self):
235+ self.status = None
236+ self.message = None
237+
238+ def success(self, message=None):
239+ """
240+ Register success. If failure has already been published, this
241+ subscriber can never succeed.
242+
243+ """
244+ if self.status is not False:
245+ self.status = True
246+ self.message = message
247+
248+ def failure(self, message=None):
249+ """
250+ Register failure.
251+
252+ """
253+ self.status = False
254+ self.message = message
255+
256+ def __str__(self):
257+ if self.message is not None:
258+ return "Test failed due to %s." % self.message
259+ else:
260+ return "User specified test failure"
261+
262+
263+class TestReportingController(object):
264+ """
265+ Controller that can be used standalone or as a wrapper for another
266+ controller to gather user-supplied information about the success
267+ or failure of an individual functional test.
268+
269+ Keypresses indicate success or failure:
270+ . 'y' or 'p' indicates success (yes, pass)
271+ . 'n' or 'f' indicates failure (no, fail)
272+ . 'q' or Esc shuts the window without indicating either (quit)
273+ Any of these five keypresses will immediately close the window.
274+
275+ Tests that involve animation should use `setScheduleBacklog` to
276+ report their future events, so that the controller can clean up
277+ the reactor and report failure if user input prematurely ends the
278+ test.
279+
280+ """
281+ def __init__(self, window, inner=None):
282+ self.window = window
283+ self.inner = inner
284+ self.subscribers = []
285+ if hasattr(self.inner, 'player'):
286+ self.player = self.inner.player
287+ else:
288+ self.player = None
289+ self.backlog = []
290+
291+ def keyDown(self, key):
292+ """
293+ Handle a keypress, passing it on to the inner controller, if
294+ any.
295+
296+ """
297+ if self.inner is not None:
298+ self.inner.keyDown(key)
299+
300+ def keyUp(self, key):
301+ """
302+ Handle a key release, passing it on to the inner controller,
303+ if any.
304+
305+ """
306+ if key == K_y or key == K_p:
307+ self.publishSuccess()
308+ self.quit()
309+ elif key == K_n or key == K_f:
310+ self.publishFailure()
311+ self.quit()
312+ elif key == K_q or key == K_ESCAPE:
313+ self.quit()
314+ if self.inner is not None:
315+ self.inner.keyUp(key)
316+
317+ def mouseMotion(self, pos, rel, buttons):
318+ """
319+ Pass mouse motion through to the inner controller, if any.
320+
321+ """
322+ if self.inner is not None:
323+ self.inner.mouseMotion(pos, rel, buttons)
324+
325+ def quit(self):
326+ """
327+ Quit the test immediately, by stopping the window.
328+
329+ """
330+ backlogComplete = True
331+ for c in self.backlog:
332+ if c.active():
333+ backlogComplete = False
334+ c.cancel()
335+ if not backlogComplete:
336+ self.publishFailure("hasty termination")
337+ self.window.stop()
338+
339+ def publishSuccess(self, message=None):
340+ """
341+ Report test success, as reported by the user, to any
342+ subscribers.
343+
344+ """
345+ for sub in self.subscribers:
346+ sub.success(message)
347+
348+ def publishFailure(self, message=None):
349+ """
350+ Report test failure, as reported by the user, to any
351+ subscribers.
352+
353+ """
354+ for sub in self.subscribers:
355+ sub.failure(message)
356+
357+ def subscribe(self, subscriber):
358+ """
359+ Subscribe to success or failure notifications. The
360+ `subscriber` should be an object that has a `success` method
361+ and a `failure` method.
362+
363+ """
364+ self.subscribers.append(subscriber)
365+
366+ def unsubscribe(self, subscriber):
367+ """
368+ Remove `subscriber` from this controller's list of
369+ subscribers.
370+
371+ """
372+ self.subscribers.remove(subscriber)
373+
374+ def setScheduleBacklog(self, delayedCalls):
375+ """
376+ Report a backlog of scheduled calls. This serves two
377+ purposes. First, the controller can cancel all pending calls.
378+ Second, the controller can report failure because not all
379+ scheduled calls were completed.
380+
381+ This method should be used for tests where a full animation
382+ should run to completion before the user supplies input. It
383+ should not be used for tests that involve continuous
384+ animation.
385+
386+ """
387+ self.backlog = delayedCalls
388
389=== modified file 'game/view.py'
390--- game/view.py 2011-01-23 17:48:22 +0000
391+++ game/view.py 2011-02-09 05:54:40 +0000
392@@ -411,6 +411,10 @@
393 events.
394 """
395 self.controller = controller
396+ # Only set up player-related handlers if the controller
397+ # involves an actual player (real or simulated).
398+ if getattr(self.controller, 'player', None) is None:
399+ return
400 self._terrainCheck = LoopingCall(self._checkTerrain, controller.player)
401 self._terrainCheck.clock = self.clock
402 # XXX Needs an errback

Subscribers

People subscribed via source and target branches

to all changes:
to status/vote changes: