Merge lp:~sidnei/lazr.testing/yeti into lp:lazr.testing

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
Reviewer Review Type Date Requested Status
Paul Hummer (community) Approve
Review via email: mp+44591@code.launchpad.net

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.
Revision history for this message
Paul Hummer (rockstar) wrote :

sidnei, once again, I bow to your prowess.

Land this kthxbai! :)

review: Approve

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()

Subscribers

People subscribed via source and target branches

to all changes: