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
=== modified file 'src/lazr/testing/tests/test_js.py'
--- src/lazr/testing/tests/test_js.py 2010-09-03 15:03:15 +0000
+++ src/lazr/testing/tests/test_js.py 2010-12-23 16:11:33 +0000
@@ -1,10 +1,12 @@
1import doctest
2import operator
1import os3import os
2import re4import re
3import unittest
4import doctest
5import socket5import socket
6import operator
7import sys6import sys
7import unittest
8import warnings
9
8from cStringIO import StringIO10from cStringIO import StringIO
911
10from os.path import dirname12from os.path import dirname
@@ -331,6 +333,11 @@
331def test_suite():333def test_suite():
332 suite = unittest.TestSuite()334 suite = unittest.TestSuite()
333335
336 if not "JSTESTDRIVER" in os.environ:
337 warnings.warn("Environment variable 'JSTESTDRIVER' not set. "
338 "Skipping 'JSTESTDRIVER' tests.")
339 return suite
340
334 if os.environ.get("JSTESTDRIVER_SELFTEST"):341 if os.environ.get("JSTESTDRIVER_SELFTEST"):
335 suite.addTests(unittest.makeSuite(JsTestDriverSelfTest))342 suite.addTests(unittest.makeSuite(JsTestDriverSelfTest))
336 else:343 else:
337344
=== added file 'src/lazr/testing/tests/test_yeti.py'
--- src/lazr/testing/tests/test_yeti.py 1970-01-01 00:00:00 +0000
+++ src/lazr/testing/tests/test_yeti.py 2010-12-23 16:11:33 +0000
@@ -0,0 +1,272 @@
1import doctest
2import operator
3import os
4import re
5import socket
6import sys
7import unittest
8import warnings
9
10from cStringIO import StringIO
11
12from os.path import dirname
13
14from mocker import ANY, ARGS, KWARGS, MockerTestCase
15
16from zope.testing import testrunner
17
18from lazr.testing.yeti import YetiLayer
19
20
21class YetiLayerErrorTests(MockerTestCase):
22
23 def setUp(self):
24 super(YetiLayerErrorTests, self).setUp()
25 env_keys = [
26 "YETI",
27 "YETI_SERVER",
28 "YETI_PORT",
29 "YETI_CAPTURE_TIMEOUT",
30 "YETI_BROWSER"]
31
32 def cleanup_non_existing_key(some_key):
33 try:
34 del os.environ[some_key]
35 except KeyError:
36 pass
37
38 for key in env_keys:
39 if key in os.environ:
40 self.addCleanup(
41 operator.setitem, os.environ, key, os.environ[key])
42 else:
43 self.addCleanup(cleanup_non_existing_key, key)
44
45 def test_binding(self):
46 """
47 If a specific port is requested, and a server is already started in the
48 requested port, then the layer setup fails.
49 """
50 s = socket.socket()
51 try:
52 s.bind((socket.gethostbyname(""), 4225))
53 except socket.error:
54 s.close()
55 s = None
56 raise
57
58 if "YETI_SERVER" in os.environ:
59 del os.environ["YETI_SERVER"]
60 os.environ["YETI_PORT"] = "4225"
61 try:
62 try:
63 YetiLayer.setUp()
64 except ValueError, e:
65 msg = str(e)
66 self.assertIn(
67 "Failed to execute Yeti server on port 4225", msg)
68 self.assertIn(
69 "Address already in use", msg)
70 else:
71 self.fail("ValueError not raised")
72 finally:
73 YetiLayer.tearDown()
74 s.close()
75
76 def mock_popen(self):
77 """Replace subprocess.Popen and make it return a mock process.
78
79 The mock process is returned.
80 """
81 mock_Popen = self.mocker.replace("subprocess.Popen")
82 self.mock_proc = self.mocker.mock()
83 mock_Popen(ARGS, KWARGS)
84 self.mocker.result(self.mock_proc)
85 return self.mock_proc
86
87 def mock_builtin_open(self):
88 """Replace built-in open and make it return a mock file.
89
90 The mock file is returned.
91 """
92 mock_open = self.mocker.replace("__builtin__.open")
93 mock_open(ANY)
94 self.mock_file = self.mocker.mock()
95 self.mocker.result(self.mock_file)
96 return self.mock_file
97
98 def test_wait_for_server_startup(self):
99 """
100 Even if we don't wait for the browser to be captured, we wait
101 for the server to start up.
102 """
103 mock_proc = self.mock_popen()
104 mock_file = self.mock_builtin_open()
105
106 with self.mocker.order():
107 mock_time = self.mocker.replace("time.time")
108 # The first time is to initialize the start time.
109 mock_time()
110 start_time = 0
111 self.mocker.result(start_time)
112
113 # The second time is to check if the timeout is exceeded in
114 # the while loop.
115 mock_time()
116 self.mocker.result(start_time)
117 # Go one iteration of the while loop, reporting the server
118 # has started.
119 mock_proc.poll()
120 self.mocker.result(None)
121 mock_file.readline()
122 self.mocker.result("to run and report the results")
123
124 # The opened file is closed.
125 mock_file.close()
126 self.mocker.result(None)
127
128 # Last check to make sure the server is running ok.
129 mock_proc.poll()
130 self.mocker.result(None)
131
132 self.mocker.replay()
133
134 os.environ["YETI_BROWSER"] = ""
135 if "YETI_SERVER" in os.environ:
136 del os.environ["YETI_SERVER"]
137 os.environ["YETI_PORT"] = "4225"
138
139 YetiLayer.setUp()
140 self.assertEqual(
141 "http://localhost:4225", os.environ["YETI_SERVER"])
142
143 def test_server_fail(self):
144 """
145 If we a poll of the process returns a non-None value while we
146 are waiting, we report that server couldn't be started.
147 """
148 mock_proc = self.mock_popen()
149 mock_file = self.mock_builtin_open()
150
151 with self.mocker.order():
152 mock_time = self.mocker.replace("time.time")
153 # The first time is to initialize the start time.
154 mock_time()
155 start_time = 0
156 self.mocker.result(start_time)
157
158 # The second time is to check if the timeout is exceeded in
159 # the while loop.
160 mock_time()
161 self.mocker.result(start_time)
162 # Go one iteration of the while loop, reporting the server
163 # is starting up.
164 mock_proc.poll()
165 self.mocker.result(None)
166 mock_file.readline()
167 self.mocker.result("not yeti?")
168
169 # Go another iteration of the while loop, reporting the
170 # server failed to start up.
171 mock_time()
172 self.mocker.result(start_time)
173 mock_proc.poll()
174 self.mocker.result(1)
175
176 # The opened file is closed.
177 mock_file.close()
178 self.mocker.result(None)
179
180 self.mocker.replay()
181
182 if "YETI_SERVER" in os.environ:
183 del os.environ["YETI_SERVER"]
184 os.environ["YETI_PORT"] = "4225"
185
186 try:
187 YetiLayer.setUp()
188 except ValueError, e:
189 msg = str(e)
190 self.assertIn(
191 "Failed to execute Yeti server on port 4225", msg)
192 else:
193 self.fail("ValueError not raised")
194
195 def test_server_timeout(self):
196 """
197 If we don't see that the server is started before the timeout, a
198 ValueError is raised, even if the process is still running.
199 """
200 timeout = 1
201 os.environ["YETI_CAPTURE_TIMEOUT"] = "%s" % timeout
202 os.environ["YETI_BROWSER"] = ""
203 if "YETI_SERVER" in os.environ:
204 del os.environ["YETI_SERVER"]
205 os.environ["YETI_PORT"] = "4225"
206
207 mock_proc = self.mock_popen()
208 mock_file = self.mock_builtin_open()
209
210 with self.mocker.order():
211 mock_time = self.mocker.replace("time.time")
212 # The first time is to initialize the start time.
213 mock_time()
214 start_time = 0
215 self.mocker.result(start_time)
216
217 # The second time is to check if the timeout is exceeded in
218 # the while loop.
219 mock_time()
220 self.mocker.result(start_time)
221 # Go one iteration of the while loop, reporting the server
222 # is starting up.
223 mock_proc.poll()
224 self.mocker.result(None)
225 mock_file.readline()
226 self.mocker.result("not yeti?")
227
228 # Trigger a timeout.
229 mock_time()
230 self.mocker.result(start_time + timeout + 1)
231
232 # The opened file is closed.
233 mock_file.close()
234 self.mocker.result(None)
235
236 # Last check whether the server is still running.
237 mock_proc.poll()
238 self.mocker.result(None)
239
240 # Since the server is running, it gets terminated.
241 mock_proc.terminate()
242 self.mocker.result(None)
243 mock_proc.wait()
244 self.mocker.result(None)
245
246 self.mocker.replay()
247
248 try:
249 YetiLayer.setUp()
250 except ValueError, e:
251 msg = str(e)
252 self.assertIn(
253 "Failed to execute Yeti server in 1 seconds"
254 " on port 4225", msg)
255 else:
256 self.fail("ValueError not raised")
257
258 def tearDown(self):
259 super(YetiLayerErrorTests, self).tearDown()
260 self.mocker.restore()
261
262
263def test_suite():
264 suite = unittest.TestSuite()
265
266 if not "YETI" in os.environ:
267 warnings.warn("Environment variable 'YETI' not set. "
268 "Skipping 'YETI' tests.")
269 return suite
270
271 suite.addTests(unittest.makeSuite(YetiLayerErrorTests))
272 return suite
0273
=== added file 'src/lazr/testing/yeti.py'
--- src/lazr/testing/yeti.py 1970-01-01 00:00:00 +0000
+++ src/lazr/testing/yeti.py 2010-12-23 16:11:33 +0000
@@ -0,0 +1,149 @@
1import os
2import time
3import fnmatch
4import signal
5import tempfile
6import subprocess
7
8from unittest import TestCase
9from subunit import ProtocolTestCase
10
11
12def startYeti():
13 yeti = os.environ["YETI"]
14 port = os.environ.get("YETI_PORT", "4422")
15
16 capture_timeout = int(os.environ.get(
17 "YETI_CAPTURE_TIMEOUT", "30"))
18
19 cmd = yeti.split() + ["--port", port, "--server"]
20
21 browser = os.environ.get("YETI_BROWSER", "default")
22
23 if browser:
24 cmd.extend(["--browsers", browser])
25
26 # Redirect stderr through a temporary file, so that it doesn't
27 # block and we don't get an IOError on readline(), apparently
28 # caused by an unhandled SIGINT (Google for it. :)
29 fd, name = tempfile.mkstemp()
30 stderr = open(name)
31 server_started = False
32 rc = None
33
34 try:
35 proc = subprocess.Popen(cmd,
36 shell=False,
37 stdin=subprocess.PIPE,
38 stdout=subprocess.PIPE,
39 stderr=fd,
40 close_fds=True)
41
42 # Give the server process a few seconds to start, and
43 # capture the browser if needed.
44 output = []
45 start = time.time()
46 while time.time() - start < capture_timeout:
47 rc = proc.poll()
48 if rc is not None:
49 break
50 line = stderr.readline()
51 if not line:
52 continue
53 output.append(line)
54 if line.startswith("Running tests locally with:"):
55 server_started = True
56 break
57 if line.startswith("to run and report the results"):
58 server_started = True
59 break
60 finally:
61 stderr.close()
62
63 if rc is None:
64 rc = proc.poll()
65 if rc is not None:
66 raise ValueError(
67 "Failed to execute Yeti server on port %s:"
68 "\nError: (%s) %s" %
69 (port, rc, "\n".join(output)))
70 if not server_started:
71 terminateProcess(proc)
72 raise ValueError(
73 "Failed to execute Yeti server in %d seconds on port %s:"
74 "\nError: (%s) %s" %
75 (capture_timeout, port, rc, "\n".join(output)))
76 else:
77 os.environ["YETI_SERVER"] = (
78 "http://localhost:%s" % port)
79 return proc
80
81
82def terminateProcess(proc):
83 try:
84 proc.terminate()
85 except AttributeError:
86 os.kill(proc.pid, signal.SIGTERM)
87 proc.wait()
88
89
90class YetiLayer(object):
91 """Manages startup/shutdown of a I{Yeti} server.
92 """
93
94 @classmethod
95 def setUp(cls):
96 cls.proc = None
97 if os.environ.get("YETI_SERVER") is None:
98 cls.proc = startYeti()
99
100 @classmethod
101 def tearDown(cls):
102 if cls.proc is not None:
103 # If the process was created by us, then that means the
104 # environment variable has been set by ourselves too, so
105 # we must unset it.
106 del os.environ["YETI_SERVER"]
107 terminateProcess(cls.proc)
108
109
110class YetiTestCase(TestCase):
111 """Controls a I{Yeti} client for a specific configuration.
112
113 Test output from I{Yeti} is captured and then parsed and
114 reported to unittest through clever subunit usage.
115
116 We require a L{tests_directory} class variable to be set by
117 subclasses, and that's the only configuration needed.
118 """
119 layer = YetiLayer
120
121 def _runTest(self, result):
122 yeti = os.environ["YETI"]
123 port = os.environ.get("YETI_PORT", "4422")
124 cmd = yeti.split() + ["--formatter=subunit",
125 "--quiet",
126 "--solo=1",
127 "--port=%s" % port]
128 for base, dirs, files in os.walk(self.tests_directory):
129 for filename in fnmatch.filter(files, "test_*.html"):
130 cmd.append(os.path.join(base, filename))
131 proc = subprocess.Popen(cmd,
132 stdin=subprocess.PIPE,
133 stdout=subprocess.PIPE,
134 stderr=subprocess.STDOUT)
135 suite = ProtocolTestCase(proc.stdout)
136 suite.run(result)
137 proc.wait()
138
139 def run(self, result=None):
140 if result is None:
141 result = self.defaultTestResult()
142 self.setUp()
143 try:
144 self._runTest(result)
145 finally:
146 self.tearDown()
147
148 def runTest(self):
149 self.run()

Subscribers

People subscribed via source and target branches

to all changes: