Merge lp:~sidnei/lazr.testing/yeti into lp:lazr.testing
- yeti
- Merge into trunk
Proposed by
Sidnei da Silva
Status: | Merged |
---|---|
Merged at revision: | 35 |
Proposed branch: | lp:~sidnei/lazr.testing/yeti |
Merge into: | lp:lazr.testing |
Diff against target: |
462 lines (+431/-3) 3 files modified
src/lazr/testing/tests/test_js.py (+10/-3) src/lazr/testing/tests/test_yeti.py (+272/-0) src/lazr/testing/yeti.py (+149/-0) |
To merge this branch: | bzr merge lp:~sidnei/lazr.testing/yeti |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Paul Hummer (community) | Approve | ||
Review via email: mp+44591@code.launchpad.net |
Commit message
Description of the change
- Adds a YetiLayer and YetiTestCase, which use a custom branch of yeti
with subunit output format to better integrate into unittest.
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | === modified file 'src/lazr/testing/tests/test_js.py' |
2 | --- src/lazr/testing/tests/test_js.py 2010-09-03 15:03:15 +0000 |
3 | +++ src/lazr/testing/tests/test_js.py 2010-12-23 16:11:33 +0000 |
4 | @@ -1,10 +1,12 @@ |
5 | +import doctest |
6 | +import operator |
7 | import os |
8 | import re |
9 | -import unittest |
10 | -import doctest |
11 | import socket |
12 | -import operator |
13 | import sys |
14 | +import unittest |
15 | +import warnings |
16 | + |
17 | from cStringIO import StringIO |
18 | |
19 | from os.path import dirname |
20 | @@ -331,6 +333,11 @@ |
21 | def test_suite(): |
22 | suite = unittest.TestSuite() |
23 | |
24 | + if not "JSTESTDRIVER" in os.environ: |
25 | + warnings.warn("Environment variable 'JSTESTDRIVER' not set. " |
26 | + "Skipping 'JSTESTDRIVER' tests.") |
27 | + return suite |
28 | + |
29 | if os.environ.get("JSTESTDRIVER_SELFTEST"): |
30 | suite.addTests(unittest.makeSuite(JsTestDriverSelfTest)) |
31 | else: |
32 | |
33 | === added file 'src/lazr/testing/tests/test_yeti.py' |
34 | --- src/lazr/testing/tests/test_yeti.py 1970-01-01 00:00:00 +0000 |
35 | +++ src/lazr/testing/tests/test_yeti.py 2010-12-23 16:11:33 +0000 |
36 | @@ -0,0 +1,272 @@ |
37 | +import doctest |
38 | +import operator |
39 | +import os |
40 | +import re |
41 | +import socket |
42 | +import sys |
43 | +import unittest |
44 | +import warnings |
45 | + |
46 | +from cStringIO import StringIO |
47 | + |
48 | +from os.path import dirname |
49 | + |
50 | +from mocker import ANY, ARGS, KWARGS, MockerTestCase |
51 | + |
52 | +from zope.testing import testrunner |
53 | + |
54 | +from lazr.testing.yeti import YetiLayer |
55 | + |
56 | + |
57 | +class YetiLayerErrorTests(MockerTestCase): |
58 | + |
59 | + def setUp(self): |
60 | + super(YetiLayerErrorTests, self).setUp() |
61 | + env_keys = [ |
62 | + "YETI", |
63 | + "YETI_SERVER", |
64 | + "YETI_PORT", |
65 | + "YETI_CAPTURE_TIMEOUT", |
66 | + "YETI_BROWSER"] |
67 | + |
68 | + def cleanup_non_existing_key(some_key): |
69 | + try: |
70 | + del os.environ[some_key] |
71 | + except KeyError: |
72 | + pass |
73 | + |
74 | + for key in env_keys: |
75 | + if key in os.environ: |
76 | + self.addCleanup( |
77 | + operator.setitem, os.environ, key, os.environ[key]) |
78 | + else: |
79 | + self.addCleanup(cleanup_non_existing_key, key) |
80 | + |
81 | + def test_binding(self): |
82 | + """ |
83 | + If a specific port is requested, and a server is already started in the |
84 | + requested port, then the layer setup fails. |
85 | + """ |
86 | + s = socket.socket() |
87 | + try: |
88 | + s.bind((socket.gethostbyname(""), 4225)) |
89 | + except socket.error: |
90 | + s.close() |
91 | + s = None |
92 | + raise |
93 | + |
94 | + if "YETI_SERVER" in os.environ: |
95 | + del os.environ["YETI_SERVER"] |
96 | + os.environ["YETI_PORT"] = "4225" |
97 | + try: |
98 | + try: |
99 | + YetiLayer.setUp() |
100 | + except ValueError, e: |
101 | + msg = str(e) |
102 | + self.assertIn( |
103 | + "Failed to execute Yeti server on port 4225", msg) |
104 | + self.assertIn( |
105 | + "Address already in use", msg) |
106 | + else: |
107 | + self.fail("ValueError not raised") |
108 | + finally: |
109 | + YetiLayer.tearDown() |
110 | + s.close() |
111 | + |
112 | + def mock_popen(self): |
113 | + """Replace subprocess.Popen and make it return a mock process. |
114 | + |
115 | + The mock process is returned. |
116 | + """ |
117 | + mock_Popen = self.mocker.replace("subprocess.Popen") |
118 | + self.mock_proc = self.mocker.mock() |
119 | + mock_Popen(ARGS, KWARGS) |
120 | + self.mocker.result(self.mock_proc) |
121 | + return self.mock_proc |
122 | + |
123 | + def mock_builtin_open(self): |
124 | + """Replace built-in open and make it return a mock file. |
125 | + |
126 | + The mock file is returned. |
127 | + """ |
128 | + mock_open = self.mocker.replace("__builtin__.open") |
129 | + mock_open(ANY) |
130 | + self.mock_file = self.mocker.mock() |
131 | + self.mocker.result(self.mock_file) |
132 | + return self.mock_file |
133 | + |
134 | + def test_wait_for_server_startup(self): |
135 | + """ |
136 | + Even if we don't wait for the browser to be captured, we wait |
137 | + for the server to start up. |
138 | + """ |
139 | + mock_proc = self.mock_popen() |
140 | + mock_file = self.mock_builtin_open() |
141 | + |
142 | + with self.mocker.order(): |
143 | + mock_time = self.mocker.replace("time.time") |
144 | + # The first time is to initialize the start time. |
145 | + mock_time() |
146 | + start_time = 0 |
147 | + self.mocker.result(start_time) |
148 | + |
149 | + # The second time is to check if the timeout is exceeded in |
150 | + # the while loop. |
151 | + mock_time() |
152 | + self.mocker.result(start_time) |
153 | + # Go one iteration of the while loop, reporting the server |
154 | + # has started. |
155 | + mock_proc.poll() |
156 | + self.mocker.result(None) |
157 | + mock_file.readline() |
158 | + self.mocker.result("to run and report the results") |
159 | + |
160 | + # The opened file is closed. |
161 | + mock_file.close() |
162 | + self.mocker.result(None) |
163 | + |
164 | + # Last check to make sure the server is running ok. |
165 | + mock_proc.poll() |
166 | + self.mocker.result(None) |
167 | + |
168 | + self.mocker.replay() |
169 | + |
170 | + os.environ["YETI_BROWSER"] = "" |
171 | + if "YETI_SERVER" in os.environ: |
172 | + del os.environ["YETI_SERVER"] |
173 | + os.environ["YETI_PORT"] = "4225" |
174 | + |
175 | + YetiLayer.setUp() |
176 | + self.assertEqual( |
177 | + "http://localhost:4225", os.environ["YETI_SERVER"]) |
178 | + |
179 | + def test_server_fail(self): |
180 | + """ |
181 | + If we a poll of the process returns a non-None value while we |
182 | + are waiting, we report that server couldn't be started. |
183 | + """ |
184 | + mock_proc = self.mock_popen() |
185 | + mock_file = self.mock_builtin_open() |
186 | + |
187 | + with self.mocker.order(): |
188 | + mock_time = self.mocker.replace("time.time") |
189 | + # The first time is to initialize the start time. |
190 | + mock_time() |
191 | + start_time = 0 |
192 | + self.mocker.result(start_time) |
193 | + |
194 | + # The second time is to check if the timeout is exceeded in |
195 | + # the while loop. |
196 | + mock_time() |
197 | + self.mocker.result(start_time) |
198 | + # Go one iteration of the while loop, reporting the server |
199 | + # is starting up. |
200 | + mock_proc.poll() |
201 | + self.mocker.result(None) |
202 | + mock_file.readline() |
203 | + self.mocker.result("not yeti?") |
204 | + |
205 | + # Go another iteration of the while loop, reporting the |
206 | + # server failed to start up. |
207 | + mock_time() |
208 | + self.mocker.result(start_time) |
209 | + mock_proc.poll() |
210 | + self.mocker.result(1) |
211 | + |
212 | + # The opened file is closed. |
213 | + mock_file.close() |
214 | + self.mocker.result(None) |
215 | + |
216 | + self.mocker.replay() |
217 | + |
218 | + if "YETI_SERVER" in os.environ: |
219 | + del os.environ["YETI_SERVER"] |
220 | + os.environ["YETI_PORT"] = "4225" |
221 | + |
222 | + try: |
223 | + YetiLayer.setUp() |
224 | + except ValueError, e: |
225 | + msg = str(e) |
226 | + self.assertIn( |
227 | + "Failed to execute Yeti server on port 4225", msg) |
228 | + else: |
229 | + self.fail("ValueError not raised") |
230 | + |
231 | + def test_server_timeout(self): |
232 | + """ |
233 | + If we don't see that the server is started before the timeout, a |
234 | + ValueError is raised, even if the process is still running. |
235 | + """ |
236 | + timeout = 1 |
237 | + os.environ["YETI_CAPTURE_TIMEOUT"] = "%s" % timeout |
238 | + os.environ["YETI_BROWSER"] = "" |
239 | + if "YETI_SERVER" in os.environ: |
240 | + del os.environ["YETI_SERVER"] |
241 | + os.environ["YETI_PORT"] = "4225" |
242 | + |
243 | + mock_proc = self.mock_popen() |
244 | + mock_file = self.mock_builtin_open() |
245 | + |
246 | + with self.mocker.order(): |
247 | + mock_time = self.mocker.replace("time.time") |
248 | + # The first time is to initialize the start time. |
249 | + mock_time() |
250 | + start_time = 0 |
251 | + self.mocker.result(start_time) |
252 | + |
253 | + # The second time is to check if the timeout is exceeded in |
254 | + # the while loop. |
255 | + mock_time() |
256 | + self.mocker.result(start_time) |
257 | + # Go one iteration of the while loop, reporting the server |
258 | + # is starting up. |
259 | + mock_proc.poll() |
260 | + self.mocker.result(None) |
261 | + mock_file.readline() |
262 | + self.mocker.result("not yeti?") |
263 | + |
264 | + # Trigger a timeout. |
265 | + mock_time() |
266 | + self.mocker.result(start_time + timeout + 1) |
267 | + |
268 | + # The opened file is closed. |
269 | + mock_file.close() |
270 | + self.mocker.result(None) |
271 | + |
272 | + # Last check whether the server is still running. |
273 | + mock_proc.poll() |
274 | + self.mocker.result(None) |
275 | + |
276 | + # Since the server is running, it gets terminated. |
277 | + mock_proc.terminate() |
278 | + self.mocker.result(None) |
279 | + mock_proc.wait() |
280 | + self.mocker.result(None) |
281 | + |
282 | + self.mocker.replay() |
283 | + |
284 | + try: |
285 | + YetiLayer.setUp() |
286 | + except ValueError, e: |
287 | + msg = str(e) |
288 | + self.assertIn( |
289 | + "Failed to execute Yeti server in 1 seconds" |
290 | + " on port 4225", msg) |
291 | + else: |
292 | + self.fail("ValueError not raised") |
293 | + |
294 | + def tearDown(self): |
295 | + super(YetiLayerErrorTests, self).tearDown() |
296 | + self.mocker.restore() |
297 | + |
298 | + |
299 | +def test_suite(): |
300 | + suite = unittest.TestSuite() |
301 | + |
302 | + if not "YETI" in os.environ: |
303 | + warnings.warn("Environment variable 'YETI' not set. " |
304 | + "Skipping 'YETI' tests.") |
305 | + return suite |
306 | + |
307 | + suite.addTests(unittest.makeSuite(YetiLayerErrorTests)) |
308 | + return suite |
309 | |
310 | === added file 'src/lazr/testing/yeti.py' |
311 | --- src/lazr/testing/yeti.py 1970-01-01 00:00:00 +0000 |
312 | +++ src/lazr/testing/yeti.py 2010-12-23 16:11:33 +0000 |
313 | @@ -0,0 +1,149 @@ |
314 | +import os |
315 | +import time |
316 | +import fnmatch |
317 | +import signal |
318 | +import tempfile |
319 | +import subprocess |
320 | + |
321 | +from unittest import TestCase |
322 | +from subunit import ProtocolTestCase |
323 | + |
324 | + |
325 | +def startYeti(): |
326 | + yeti = os.environ["YETI"] |
327 | + port = os.environ.get("YETI_PORT", "4422") |
328 | + |
329 | + capture_timeout = int(os.environ.get( |
330 | + "YETI_CAPTURE_TIMEOUT", "30")) |
331 | + |
332 | + cmd = yeti.split() + ["--port", port, "--server"] |
333 | + |
334 | + browser = os.environ.get("YETI_BROWSER", "default") |
335 | + |
336 | + if browser: |
337 | + cmd.extend(["--browsers", browser]) |
338 | + |
339 | + # Redirect stderr through a temporary file, so that it doesn't |
340 | + # block and we don't get an IOError on readline(), apparently |
341 | + # caused by an unhandled SIGINT (Google for it. :) |
342 | + fd, name = tempfile.mkstemp() |
343 | + stderr = open(name) |
344 | + server_started = False |
345 | + rc = None |
346 | + |
347 | + try: |
348 | + proc = subprocess.Popen(cmd, |
349 | + shell=False, |
350 | + stdin=subprocess.PIPE, |
351 | + stdout=subprocess.PIPE, |
352 | + stderr=fd, |
353 | + close_fds=True) |
354 | + |
355 | + # Give the server process a few seconds to start, and |
356 | + # capture the browser if needed. |
357 | + output = [] |
358 | + start = time.time() |
359 | + while time.time() - start < capture_timeout: |
360 | + rc = proc.poll() |
361 | + if rc is not None: |
362 | + break |
363 | + line = stderr.readline() |
364 | + if not line: |
365 | + continue |
366 | + output.append(line) |
367 | + if line.startswith("Running tests locally with:"): |
368 | + server_started = True |
369 | + break |
370 | + if line.startswith("to run and report the results"): |
371 | + server_started = True |
372 | + break |
373 | + finally: |
374 | + stderr.close() |
375 | + |
376 | + if rc is None: |
377 | + rc = proc.poll() |
378 | + if rc is not None: |
379 | + raise ValueError( |
380 | + "Failed to execute Yeti server on port %s:" |
381 | + "\nError: (%s) %s" % |
382 | + (port, rc, "\n".join(output))) |
383 | + if not server_started: |
384 | + terminateProcess(proc) |
385 | + raise ValueError( |
386 | + "Failed to execute Yeti server in %d seconds on port %s:" |
387 | + "\nError: (%s) %s" % |
388 | + (capture_timeout, port, rc, "\n".join(output))) |
389 | + else: |
390 | + os.environ["YETI_SERVER"] = ( |
391 | + "http://localhost:%s" % port) |
392 | + return proc |
393 | + |
394 | + |
395 | +def terminateProcess(proc): |
396 | + try: |
397 | + proc.terminate() |
398 | + except AttributeError: |
399 | + os.kill(proc.pid, signal.SIGTERM) |
400 | + proc.wait() |
401 | + |
402 | + |
403 | +class YetiLayer(object): |
404 | + """Manages startup/shutdown of a I{Yeti} server. |
405 | + """ |
406 | + |
407 | + @classmethod |
408 | + def setUp(cls): |
409 | + cls.proc = None |
410 | + if os.environ.get("YETI_SERVER") is None: |
411 | + cls.proc = startYeti() |
412 | + |
413 | + @classmethod |
414 | + def tearDown(cls): |
415 | + if cls.proc is not None: |
416 | + # If the process was created by us, then that means the |
417 | + # environment variable has been set by ourselves too, so |
418 | + # we must unset it. |
419 | + del os.environ["YETI_SERVER"] |
420 | + terminateProcess(cls.proc) |
421 | + |
422 | + |
423 | +class YetiTestCase(TestCase): |
424 | + """Controls a I{Yeti} client for a specific configuration. |
425 | + |
426 | + Test output from I{Yeti} is captured and then parsed and |
427 | + reported to unittest through clever subunit usage. |
428 | + |
429 | + We require a L{tests_directory} class variable to be set by |
430 | + subclasses, and that's the only configuration needed. |
431 | + """ |
432 | + layer = YetiLayer |
433 | + |
434 | + def _runTest(self, result): |
435 | + yeti = os.environ["YETI"] |
436 | + port = os.environ.get("YETI_PORT", "4422") |
437 | + cmd = yeti.split() + ["--formatter=subunit", |
438 | + "--quiet", |
439 | + "--solo=1", |
440 | + "--port=%s" % port] |
441 | + for base, dirs, files in os.walk(self.tests_directory): |
442 | + for filename in fnmatch.filter(files, "test_*.html"): |
443 | + cmd.append(os.path.join(base, filename)) |
444 | + proc = subprocess.Popen(cmd, |
445 | + stdin=subprocess.PIPE, |
446 | + stdout=subprocess.PIPE, |
447 | + stderr=subprocess.STDOUT) |
448 | + suite = ProtocolTestCase(proc.stdout) |
449 | + suite.run(result) |
450 | + proc.wait() |
451 | + |
452 | + def run(self, result=None): |
453 | + if result is None: |
454 | + result = self.defaultTestResult() |
455 | + self.setUp() |
456 | + try: |
457 | + self._runTest(result) |
458 | + finally: |
459 | + self.tearDown() |
460 | + |
461 | + def runTest(self): |
462 | + self.run() |
sidnei, once again, I bow to your prowess.
Land this kthxbai! :)