Merge lp:~jml/libdep-service/test-double into lp:libdep-service

Proposed by Jonathan Lange
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
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.

To post a comment you must log in.
Revision history for this message
James Westby (james-w) wrote :

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

review: Approve
Revision history for this message
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://bazaar.launchpad.net/+branch/ca-download-cache
Downloading file:///tmp/tmpHJ74Us/download-cache/dist/distribute-0.6.29DEV.tar.gz
Extracting in /tmp/tmplEkf6m
Now working in /tmp/tmplEkf6m/distribute-0.6.29DEV
Building a Distribute egg in /tmp/tmpHJ74Us/eggs
/tmp/tmpHJ74Us/eggs/distribute-0.6.29DEV-py2.7.egg
zip_safe flag not set; analyzing archive contents...
devportalbinary.testing: module references __file__
devportalbinary.acceptance.tests.__init__: module references __file__
devportalbinary.tests.test_aptfile: module references __file__
zip_safe flag not set; analyzing archive contents...
pyflakes.checker: module references __file__
pyflakes.checker: module references __path__
pyflakes.test.test_undefined_names: module references __file__
pyflakes.test.test_undefined_names: module references __path__
zip_safe flag not set; analyzing archive contents...
testresources.__init__: module MAY be using inspect.stack
Couldn't find index page for 'testscenarios' (maybe misspelled?)
While:
  Installing scripts.
  Getting distribution for 'testscenarios==0.3'.
Error: Couldn't find a distribution for 'testscenarios==0.3'.
make: *** [bin/py] Error 1

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches