Merge lp:~jml/libdep-service/test-double into lp:libdep-service
- test-double
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | James Westby |
Approved revision: | 110 |
Merged at revision: | 52 |
Proposed branch: | lp:~jml/libdep-service/test-double |
Merge into: | lp:libdep-service |
Diff against target: |
486 lines (+408/-2) 8 files modified
buildout.cfg (+2/-0) djlibdep/test_double.py (+99/-0) djlibdep/tests/__init__.py (+3/-1) djlibdep/tests/_djangofixture.py (+86/-0) djlibdep/tests/test_interface.py (+77/-0) djlibdep/tests/test_test_double.py (+132/-0) setup.py (+6/-0) versions.cfg (+3/-1) |
To merge this branch: | bzr merge lp:~jml/libdep-service/test-double |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Westby (community) | Approve | ||
Review via email: mp+126976@code.launchpad.net |
Commit message
Add a network test double that can be used to test clients.
Description of the change
Adds a test double and interface tests for libdep-service.
ISD Branch Mangler (isd-branches-mangler) wrote : | # |
The attempt to merge lp:~jml/libdep-service/test-double into lp:libdep-service failed. Below is the output from the failed tests.
Tree is up to date at revision 23 of branch bzr+ssh:
Downloading file://
Extracting in /tmp/tmplEkf6m
Now working in /tmp/tmplEkf6m/
Building a Distribute egg in /tmp/tmpHJ74Us/eggs
/tmp/tmpHJ74Us/
zip_safe flag not set; analyzing archive contents...
devportalbinary
devportalbinary
devportalbinary
zip_safe flag not set; analyzing archive contents...
pyflakes.checker: module references __file__
pyflakes.checker: module references __path__
pyflakes.
pyflakes.
zip_safe flag not set; analyzing archive contents...
testresources.
Couldn't find index page for 'testscenarios' (maybe misspelled?)
While:
Installing scripts.
Getting distribution for 'testscenarios=
Error: Couldn't find a distribution for 'testscenarios=
make: *** [bin/py] Error 1
Preview Diff
1 | === modified file 'buildout.cfg' |
2 | --- buildout.cfg 2012-09-07 11:54:59 +0000 |
3 | +++ buildout.cfg 2012-09-28 15:15:27 +0000 |
4 | @@ -33,6 +33,8 @@ |
5 | pep8 |
6 | pkgme-devportal[testing] |
7 | pyflakes |
8 | + testresources |
9 | + testscenarios |
10 | testtools |
11 | include-site-packages = false |
12 | interpreter = py |
13 | |
14 | === added file 'djlibdep/test_double.py' |
15 | --- djlibdep/test_double.py 1970-01-01 00:00:00 +0000 |
16 | +++ djlibdep/test_double.py 2012-09-28 15:15:27 +0000 |
17 | @@ -0,0 +1,99 @@ |
18 | +import argparse |
19 | +import json |
20 | +from pprint import pformat |
21 | +import sys |
22 | + |
23 | +from twisted.internet import reactor as mod_reactor |
24 | +from twisted.internet.defer import maybeDeferred |
25 | +from twisted.internet.endpoints import TCP4ServerEndpoint |
26 | +from twisted.web.resource import ( |
27 | + NoResource, |
28 | + Resource, |
29 | + ) |
30 | +from twisted.web.server import Site |
31 | +from twisted.web.static import Data |
32 | + |
33 | + |
34 | +# XXX: Global mutable state :( |
35 | +_EXIT_CODE = 0 |
36 | + |
37 | + |
38 | +class StockResponse(Resource): |
39 | + |
40 | + def __init__(self, stock_data): |
41 | + Resource.__init__(self) |
42 | + self._stock_data = stock_data |
43 | + |
44 | + def getChild(self, name, request): |
45 | + if not name: |
46 | + return self |
47 | + full_name = '/'.join([name] + request.postpath) |
48 | + request.postpath = [] |
49 | + value = self._stock_data.get(full_name, []) |
50 | + for args, data in value: |
51 | + if args == request.args: |
52 | + return Data(data.encode('utf8'), 'text/html') |
53 | + return NoResource( |
54 | + '%r (%r) not in %r' % (full_name, request.args, self._stock_data)) |
55 | + |
56 | + def render_GET(self, request): |
57 | + if request.method == 'HEAD': |
58 | + return '' |
59 | + return ( |
60 | + '<html><head><title>Stock responses</title></head>' |
61 | + '<body><h1>Stock responses</h1><pre>%s</pre></body></html>' |
62 | + % pformat(self._stock_data)) |
63 | + |
64 | + |
65 | +def make_options(): |
66 | + parser = argparse.ArgumentParser("Test server for libdep-service") |
67 | + parser.add_argument('data', type=argparse.FileType('r'), nargs='?') |
68 | + return parser |
69 | + |
70 | + |
71 | +def run_web_server(reactor, root, port): |
72 | + endpoint = TCP4ServerEndpoint(reactor, port) |
73 | + site = Site(root) |
74 | + return endpoint.listen(site) |
75 | + |
76 | + |
77 | +def unexpected_error(failure): |
78 | + global _EXIT_CODE |
79 | + failure.printTraceback(sys.stderr) |
80 | + mod_reactor.stop() |
81 | + _EXIT_CODE = 2 |
82 | + |
83 | + |
84 | +def get_base_url(listening_port, hostname=None): |
85 | + address = listening_port.getHost() |
86 | + if not hostname: |
87 | + hostname = address.host |
88 | + return 'http://%s:%s' % (hostname, address.port) |
89 | + |
90 | + |
91 | +def run(reactor, data, hostname='localhost', port=0): |
92 | + root = StockResponse(data) |
93 | + d = maybeDeferred(run_web_server, reactor, root, port) |
94 | + |
95 | + def print_port(listening_port): |
96 | + print get_base_url(listening_port, hostname) |
97 | + # Must flush here, so that things watching our stdout know that we've |
98 | + # started. |
99 | + sys.stdout.flush() |
100 | + return listening_port |
101 | + |
102 | + d.addCallback(print_port) |
103 | + d.addErrback(unexpected_error) |
104 | + return d |
105 | + |
106 | + |
107 | +def main(): |
108 | + parser = make_options() |
109 | + args = parser.parse_args() |
110 | + if args.data: |
111 | + data = json.load(args.data) |
112 | + else: |
113 | + data = {} |
114 | + mod_reactor.callWhenRunning(run, mod_reactor, data) |
115 | + mod_reactor.run() |
116 | + sys.exit(_EXIT_CODE) |
117 | |
118 | === modified file 'djlibdep/tests/__init__.py' |
119 | --- djlibdep/tests/__init__.py 2012-09-10 14:34:38 +0000 |
120 | +++ djlibdep/tests/__init__.py 2012-09-28 15:15:27 +0000 |
121 | @@ -20,9 +20,11 @@ |
122 | |
123 | TEST_MODULES = [ |
124 | 'api', |
125 | + 'interface', |
126 | 'pep8', |
127 | 'preflight', |
128 | - 'views' |
129 | + 'test_double', |
130 | + 'views', |
131 | ] |
132 | |
133 | SUITE_FACTORY = OptimisingTestSuite |
134 | |
135 | === added file 'djlibdep/tests/_djangofixture.py' |
136 | --- djlibdep/tests/_djangofixture.py 1970-01-01 00:00:00 +0000 |
137 | +++ djlibdep/tests/_djangofixture.py 2012-09-28 15:15:27 +0000 |
138 | @@ -0,0 +1,86 @@ |
139 | +import errno |
140 | +import subprocess |
141 | +import sys |
142 | +import time |
143 | +from urllib2 import ( |
144 | + URLError, |
145 | + urlopen, |
146 | + ) |
147 | + |
148 | +from fixtures import Fixture |
149 | +from testtools.content import ( |
150 | + Content, |
151 | + UTF8_TEXT, |
152 | + ) |
153 | +from twisted.internet.error import TimeoutError |
154 | + |
155 | + |
156 | +def poll(poll_interval, max_tries, predicate, *args, **kwargs): |
157 | + for i in range(max_tries): |
158 | + if predicate(*args, **kwargs): |
159 | + return |
160 | + time.sleep(poll_interval) |
161 | + raise TimeoutError("Timed out waiting for %r" % (predicate,)) |
162 | + |
163 | + |
164 | +def _is_server_up(url): |
165 | + try: |
166 | + response = urlopen(url) |
167 | + except URLError, e: |
168 | + error_no = getattr(e.reason, 'errno', None) |
169 | + if error_no in (errno.ECONNREFUSED, errno.ECONNRESET): |
170 | + return False |
171 | + raise |
172 | + except IOError, e: |
173 | + if e.errno in (errno.ECONNREFUSED, errno.ECONNRESET): |
174 | + return False |
175 | + raise |
176 | + return response.code == 200 |
177 | + |
178 | + |
179 | +def poll_until_running(url, poll_interval=0.05, max_tries=100): |
180 | + try: |
181 | + poll(poll_interval, max_tries, _is_server_up, url) |
182 | + except TimeoutError: |
183 | + raise TimeoutError("Timed out waiting for %s to come up" % (url,)) |
184 | + |
185 | + |
186 | +def get_manage_location(): |
187 | + return 'django_project/manage.py' |
188 | + |
189 | + |
190 | +# XXX: Copied from lp:pkgme-service. Extract to separate library. |
191 | +class DjangoFixture(Fixture): |
192 | + """A simple Django service, with database. |
193 | + |
194 | + Essentially does 'runserver'. |
195 | + """ |
196 | + |
197 | + def __init__(self, all_clear_path, port=8001): |
198 | + super(DjangoFixture, self).__init__() |
199 | + self._all_clear_path = all_clear_path |
200 | + # XXX: parallelism: Hard-code the port to run on for now. Don't know |
201 | + # how to figure out what port it's actually listening on. |
202 | + self._port = port |
203 | + |
204 | + def setUp(self): |
205 | + super(DjangoFixture, self).setUp() |
206 | + process = subprocess.Popen( |
207 | + [sys.executable, get_manage_location(), |
208 | + 'runserver', '--noreload', str(self._port)], |
209 | + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
210 | + self.addCleanup(process.terminate) |
211 | + self.addCleanup( |
212 | + self.addDetail, |
213 | + 'runserver-log', |
214 | + Content( |
215 | + UTF8_TEXT, |
216 | + process.stdout.readlines)) |
217 | + self.base_url = 'http://localhost:%s' % (self._port,) |
218 | + all_clear_url = '%s/%s' % (self.base_url, self._all_clear_path) |
219 | + try: |
220 | + poll_until_running(all_clear_url) |
221 | + except: |
222 | + # fixtures don't get cleaned up automatically if setUp fails. |
223 | + self.cleanUp() |
224 | + raise |
225 | |
226 | === added file 'djlibdep/tests/test_interface.py' |
227 | --- djlibdep/tests/test_interface.py 1970-01-01 00:00:00 +0000 |
228 | +++ djlibdep/tests/test_interface.py 2012-09-28 15:15:27 +0000 |
229 | @@ -0,0 +1,77 @@ |
230 | +"""Test the HTTP interface of libdep-service.""" |
231 | + |
232 | +import json |
233 | +from urllib2 import urlopen |
234 | + |
235 | +from devportalbinary.database import PackageDatabase |
236 | +from devportalbinary.testing import ( |
237 | + DatabaseConfig, |
238 | + PostgresDatabaseFixture, |
239 | + ) |
240 | +from fixtures import Fixture |
241 | +from testresources import ( |
242 | + FixtureResource, |
243 | + ResourcedTestCase, |
244 | + ) |
245 | +from testscenarios import generate_scenarios |
246 | +from testtools import TestCase |
247 | + |
248 | +from .helpers import populate_sample_data |
249 | +from .test_test_double import ( |
250 | + test_double_fixture, |
251 | + ) |
252 | +from ._djangofixture import DjangoFixture |
253 | + |
254 | + |
255 | +class RealServerFixture(Fixture): |
256 | + |
257 | + def __init__(self, sample_data): |
258 | + super(RealServerFixture, self).__init__() |
259 | + self._sample_data = sample_data |
260 | + |
261 | + def setUp(self): |
262 | + super(RealServerFixture, self).setUp() |
263 | + db_fixture = self.useFixture(PostgresDatabaseFixture()) |
264 | + # This has to come first so the spawned Django server can inherit our |
265 | + # modified environment, and thus our modified configuration. |
266 | + self.useFixture(DatabaseConfig(db_fixture)) |
267 | + django = self.useFixture(DjangoFixture('v1/service_check')) |
268 | + db = PackageDatabase(db_fixture.conn) |
269 | + self.addCleanup(db.close) |
270 | + populate_sample_data(db, self._sample_data) |
271 | + self.base_url = django.base_url |
272 | + |
273 | +real_server_fixture = FixtureResource( |
274 | + RealServerFixture( |
275 | + [('libc', {'i386': {'libc.so.6': 'libc-bin'}}), |
276 | + ])) |
277 | + |
278 | + |
279 | +class InterfaceTests(TestCase, ResourcedTestCase): |
280 | + |
281 | + scenarios = [ |
282 | + ('real', {'resources': [('server', real_server_fixture)]}), |
283 | + ('double', {'resources': [('server', test_double_fixture)]}), |
284 | + ] |
285 | + |
286 | + def test_service_check(self): |
287 | + url = '%s/v1/service_check' % (self.server.base_url,) |
288 | + data = urlopen(url).read() |
289 | + self.assertEqual('Hello world!', data) |
290 | + |
291 | + def test_not_found(self): |
292 | + url = '%s/v1/get_binaries_for_libraries' % (self.server.base_url,) |
293 | + url += '?libs=doesnotexist' |
294 | + data = urlopen(url).read() |
295 | + self.assertEqual('{}', data) |
296 | + |
297 | + def test_found(self): |
298 | + url = '%s/v1/get_binaries_for_libraries' % (self.server.base_url,) |
299 | + url += '?libs=libc.so.6' |
300 | + data = urlopen(url).read() |
301 | + self.assertEqual(json.dumps({'libc.so.6': ['libc-bin']}), data) |
302 | + |
303 | + |
304 | +def load_tests(loader, tests, ignored): |
305 | + from unittest import TestSuite |
306 | + return TestSuite(generate_scenarios(tests)) |
307 | |
308 | === added file 'djlibdep/tests/test_test_double.py' |
309 | --- djlibdep/tests/test_test_double.py 1970-01-01 00:00:00 +0000 |
310 | +++ djlibdep/tests/test_test_double.py 2012-09-28 15:15:27 +0000 |
311 | @@ -0,0 +1,132 @@ |
312 | +import json |
313 | +import os |
314 | +from pprint import pformat |
315 | +import subprocess |
316 | +import sys |
317 | +from urllib2 import urlopen |
318 | + |
319 | +from fixtures import Fixture |
320 | +from testtools import TestCase |
321 | +from testresources import ( |
322 | + FixtureResource, |
323 | + ResourcedTestCase, |
324 | + ) |
325 | +from treeshape import ( |
326 | + CONTENT, |
327 | + FileTree, |
328 | + ) |
329 | +from twisted.web.resource import getChildForRequest |
330 | +from twisted.web.test.test_web import DummyRequest |
331 | + |
332 | +from ..test_double import ( |
333 | + StockResponse, |
334 | + ) |
335 | + |
336 | + |
337 | +def get_test_double_path(): |
338 | + """Get the path to the test double executable.""" |
339 | + # We are /blah/blah/djpkgme/tests/test_test_double.py. We want to get to |
340 | + # /blah/blah/bin/libdep-service-testd. |
341 | + up = os.path.dirname |
342 | + return os.path.join(up(up(up(__file__))), 'bin', 'libdep-service-testd') |
343 | + |
344 | + |
345 | +class LibdepServiceDouble(Fixture): |
346 | + |
347 | + def __init__(self, data): |
348 | + super(LibdepServiceDouble, self).__init__() |
349 | + self._data = data |
350 | + |
351 | + def setUp(self): |
352 | + super(LibdepServiceDouble, self).setUp() |
353 | + tree = self.useFixture( |
354 | + FileTree({'data.json': {CONTENT: json.dumps(self._data)}})) |
355 | + data_path = tree.join('data.json') |
356 | + p = subprocess.Popen( |
357 | + [sys.executable, get_test_double_path(), data_path], |
358 | + stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
359 | + self.addCleanup(p.terminate) |
360 | + # XXX: For better debug-ability. |
361 | + if p.poll() is not None: |
362 | + raise Exception(p.stderr.read()) |
363 | + # XXX: Assume that the "I'm ready" message is only a single line. |
364 | + # XXX: Maybe make it JSON? |
365 | + self.base_url = p.stdout.readline().strip() |
366 | + |
367 | + |
368 | +TEST_DATA = { |
369 | + 'v1/service_check': [({}, 'Hello world!')], |
370 | + 'v1/get_binaries_for_libraries': [ |
371 | + ({'libs': ['doesnotexist']}, '{}'), |
372 | + ({'libs': ['libc.so.6']}, '{"libc.so.6": ["libc-bin"]}'), |
373 | + ], |
374 | + } |
375 | +test_double_fixture = FixtureResource(LibdepServiceDouble(TEST_DATA)) |
376 | + |
377 | + |
378 | +class TestInterface(TestCase, ResourcedTestCase): |
379 | + """Tests for the public interface of the test double. |
380 | + |
381 | + We want any & all clients to be able to use the test double for writing |
382 | + their tests, regardless of the language that they're written in. As such, |
383 | + the interface for the test double is "run a process". These tests very |
384 | + that we can run the process, feed it data, and shut it down. |
385 | + """ |
386 | + |
387 | + resources = [('double', test_double_fixture)] |
388 | + |
389 | + def test_executable(self): |
390 | + # We can find and run the executable sanely. Using '--help' as an |
391 | + # option as that's unlikely to launch a web server and more likely to |
392 | + # terminate quickly! |
393 | + p = subprocess.Popen( |
394 | + [sys.executable, get_test_double_path(), '--help'], |
395 | + stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
396 | + out, err = p.communicate() |
397 | + self.assertEqual(0, p.returncode) |
398 | + |
399 | + def test_launches_web_server(self): |
400 | + fd = urlopen(self.double.base_url) |
401 | + self.assertEqual(200, fd.code) |
402 | + |
403 | + def test_serves_simple_data_from_file(self): |
404 | + service_check_url = '%s/v1/service_check' % (self.double.base_url,) |
405 | + fd = urlopen(service_check_url) |
406 | + self.assertEqual('Hello world!', fd.read()) |
407 | + |
408 | + |
409 | +def _get_path(resource, path, args=None): |
410 | + request = DummyRequest(path) |
411 | + if args: |
412 | + request.args = args |
413 | + child = getChildForRequest(resource, request) |
414 | + return child.render(request) |
415 | + |
416 | + |
417 | +class TestStockResponses(TestCase): |
418 | + |
419 | + def test_simple_resource(self): |
420 | + resource = StockResponse({'foo': [({}, 'bar')]}) |
421 | + result = _get_path(resource, ['foo']) |
422 | + self.assertEqual('bar', result) |
423 | + |
424 | + def test_recursive_resource(self): |
425 | + resource = StockResponse({'foo/bar': [({}, 'baz')]}) |
426 | + result = _get_path(resource, ['foo', 'bar']) |
427 | + self.assertEqual('baz', result) |
428 | + |
429 | + def test_query_params(self): |
430 | + resource = StockResponse({'foo': [({'baz': 'bar'}, 'qux')]}) |
431 | + result = _get_path(resource, ['foo'], {'baz': 'bar'}) |
432 | + self.assertEqual('qux', result) |
433 | + |
434 | + def test_not_found(self): |
435 | + resource = StockResponse({}) |
436 | + result = _get_path(resource, ['foo']) |
437 | + self.assertIn('404', result) |
438 | + |
439 | + def test_base(self): |
440 | + data = {'foo': [({}, 'bar')]} |
441 | + resource = StockResponse(data) |
442 | + result = _get_path(resource, []) |
443 | + self.assertIn(pformat(data), result) |
444 | |
445 | === modified file 'setup.py' |
446 | --- setup.py 2012-08-24 19:34:45 +0000 |
447 | +++ setup.py 2012-09-28 15:15:27 +0000 |
448 | @@ -37,8 +37,14 @@ |
449 | # The 0.4 package of django-openid-auth doesn't depend on |
450 | # python-openid, so we have to list it here. |
451 | 'python-openid>=2.2.5', |
452 | + 'Twisted', |
453 | 'txstatsd', |
454 | ], |
455 | + entry_points = { |
456 | + 'console_scripts': [ |
457 | + 'libdep-service-testd=djlibdep.test_double:main', |
458 | + ], |
459 | + }, |
460 | zip_safe=False, |
461 | packages=find_packages('.'), |
462 | ) |
463 | |
464 | === modified file 'versions.cfg' |
465 | --- versions.cfg 2012-09-13 22:20:17 +0000 |
466 | +++ versions.cfg 2012-09-28 15:15:27 +0000 |
467 | @@ -38,7 +38,7 @@ |
468 | pep8 = 1.3.3 |
469 | PIL = 1.1.7 |
470 | pkgme = 0.4.1 |
471 | -pkgme-devportal = 0.4.3 |
472 | +pkgme-devportal = 0.4.6 |
473 | postgresfixture = 0.1.2 |
474 | psycopg2 = 2.4.5 |
475 | pyflakes = 0.5.0 |
476 | @@ -50,8 +50,10 @@ |
477 | south = 0.7.3 |
478 | storm = 0.19 |
479 | testresources = 0.2.5 |
480 | +testscenarios = 0.3 |
481 | testtools = 0.9.16 |
482 | treeshape = 0.2.1 |
483 | +Twisted = 12.1.0 |
484 | # A pre-release of txstatsd as there hasn't been a real release yet. |
485 | # Can be replaced with the proper release when it happens. |
486 | # This dist was created from lp:~james-w/txstatsd/pre-release |
Hi,
We'll have to change this to not require the script to be at that relative
path before we can use the double elsewhere.
sys.executable -c "import sys; from pkg_resources import load_entry_point; sys.exit( load_entry_ point(' libdep- service= =$VERSION' , 'console_scripts', 'libdep- service- testd') ())"
seems like it should run the same thing.
Also, it would be to attach the output of the double as details.
All can be fixed later, so approving.
Thanks,
James