Merge lp:~mandel/ubuntuone-dev-tools/mocked-webserver into lp:ubuntuone-dev-tools

Proposed by Manuel de la Peña
Status: Merged
Approved by: Manuel de la Peña
Approved revision: 99
Merged at revision: 62
Proposed branch: lp:~mandel/ubuntuone-dev-tools/mocked-webserver
Merge into: lp:ubuntuone-dev-tools
Prerequisite: lp:~mandel/ubuntuone-dev-tools/tcp-testcases
Diff against target: 349 lines (+322/-1)
4 files modified
ubuntuone/devtools/testcases/txtcpserver.py (+0/-1)
ubuntuone/devtools/testing/tests/__init__.py (+29/-0)
ubuntuone/devtools/testing/tests/test_txwebserver.py (+172/-0)
ubuntuone/devtools/testing/txwebserver.py (+121/-0)
To merge this branch: bzr merge lp:~mandel/ubuntuone-dev-tools/mocked-webserver
Reviewer Review Type Date Requested Status
dobey (community) Approve
Alejandro J. Cura (community) Approve
Review via email: mp+101224@code.launchpad.net

Commit message

- Added a mock Web server which ensures its resources are correctly finalized, for use in tests (LP: #972366).

Description of the change

- Added a mock Webserver than can be used in tests and that ensures that its resources are correctly closed (LP: #972366).

To post a comment you must log in.
Revision history for this message
dobey (dobey) wrote :

107 + def test_get_iri(self):
108 + """Test getting the iri from the server."""
109 + expected = u"http://127.0.0.1:%d/"
110 + self.assert_url(expected)
111 +
112 + def test_get_uri(self):
113 + """Test getting the uri from the server."""
114 + expected = "http://127.0.0.1:%d/"
115 + self.assert_url(expected)

128 + def test_get_ssl_iri(self):
129 + """Test getting the ssl iri."""
130 + self.assertEqual(None, self.server.get_ssl_uri())
131 +
132 + def test_get_ssl_uri(self):
133 + """Test getting the ssl uri."""
134 + self.assertEqual(None, self.server.get_ssl_uri())

274 + def get_iri(self):
275 + """Build the iri for this mock server."""
276 + url = u"http://127.0.0.1:%d/"
277 + return url % self.get_port()
278 +
279 + def get_uri(self):
280 + """Build the uri for this mock server."""
281 + url = "http://127.0.0.1:%d/"
282 + return url % self.get_port()
283 +
284 + def get_ssl_iri(self):
285 + """Build the ssl iri for this mock server."""
286 + if self.ssl_settings:
287 + url = u"https://127.0.0.1:%d/"
288 + return url % self.get_ssl_port()
289 +
290 + def get_ssl_uri(self):
291 + """Build the ssl iri for this mock server."""
292 + if self.ssl_settings:
293 + url = "https://127.0.0.1:%d/"
294 + return url % self.get_ssl_port()

Why all this duplicated code?

review: Needs Fixing
Revision history for this message
Alejandro J. Cura (alecu) wrote :

Nice branch. This is a very good second step in order to reduce the amount of boilerplate we have everywhere in our tests.

The only bit I don't like is the many "if self.ssl_settings" scattered around.
What about splitting the WebServer class into HTTPS and HTTP classes?

review: Needs Fixing
Revision history for this message
Manuel de la Peña (mandel) wrote :

> 107 + def test_get_iri(self):
> 108 + """Test getting the iri from the server."""
> 109 + expected = u"http://127.0.0.1:%d/"
> 110 + self.assert_url(expected)
> 111 +
> 112 + def test_get_uri(self):
> 113 + """Test getting the uri from the server."""
> 114 + expected = "http://127.0.0.1:%d/"
> 115 + self.assert_url(expected)
>
> 128 + def test_get_ssl_iri(self):
> 129 + """Test getting the ssl iri."""
> 130 + self.assertEqual(None, self.server.get_ssl_uri())
> 131 +
> 132 + def test_get_ssl_uri(self):
> 133 + """Test getting the ssl uri."""
> 134 + self.assertEqual(None, self.server.get_ssl_uri())
>
> 274 + def get_iri(self):
> 275 + """Build the iri for this mock server."""
> 276 + url = u"http://127.0.0.1:%d/"
> 277 + return url % self.get_port()
> 278 +
> 279 + def get_uri(self):
> 280 + """Build the uri for this mock server."""
> 281 + url = "http://127.0.0.1:%d/"
> 282 + return url % self.get_port()
> 283 +
> 284 + def get_ssl_iri(self):
> 285 + """Build the ssl iri for this mock server."""
> 286 + if self.ssl_settings:
> 287 + url = u"https://127.0.0.1:%d/"
> 288 + return url % self.get_ssl_port()
> 289 +
> 290 + def get_ssl_uri(self):
> 291 + """Build the ssl iri for this mock server."""
> 292 + if self.ssl_settings:
> 293 + url = "https://127.0.0.1:%d/"
> 294 + return url % self.get_ssl_port()
>
> Why all this duplicated code?

Looks like a merge fail from me, let me double check.

Revision history for this message
Manuel de la Peña (mandel) wrote :

> Nice branch. This is a very good second step in order to reduce the amount of
> boilerplate we have everywhere in our tests.
>
> The only bit I don't like is the many "if self.ssl_settings" scattered around.
> What about splitting the WebServer class into HTTPS and HTTP classes?

Sure!

Revision history for this message
dobey (dobey) wrote :

+SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)

There are a lot of strings being combined with %-notation in here. Please use the new .format() notation for string formatting instead, as %-notation is deprecated. As the new notation uses {0} for example as insertion markers, you probably should use unicode({u'{0}'.format(SAMPLE_KEY): u'{0}'.format(SAMPLE_VALUE)}) for this perhaps.

review: Needs Fixing
Revision history for this message
Alejandro J. Cura (alecu) wrote :

> +SAMPLE_RESOURCE = '{"%s": "%s"}' % (SAMPLE_KEY, SAMPLE_VALUE)
>
> There are a lot of strings being combined with %-notation in here. Please use
> the new .format() notation for string formatting instead, as %-notation is
> deprecated. As the new notation uses {0} for example as insertion markers, you
> probably should use unicode({u'{0}'.format(SAMPLE_KEY):
> u'{0}'.format(SAMPLE_VALUE)}) for this perhaps.

The SAMPLE_RESOURCE is just a json-like sequence of bytes used for testing, so the fix requested is wrong.
It should be something like:
+SAMPLE_RESOURCE = '{{"{0}": "{1}"}}'.format(SAMPLE_KEY, SAMPLE_VALUE)

Also, and for the record, the string interpolation operator % is not deprecated; not even in python 3.3:
"there are no current plans to deprecate printf-style formatting."
http://docs.python.org/dev/library/stdtypes.html#old-string-formatting-operations

Revision history for this message
Alejandro J. Cura (alecu) wrote :

+1

review: Approve
Revision history for this message
dobey (dobey) wrote :

Can we have the scheme, host, and port all passed around separately? Passing around "http://foo:{0}" is ugly, and exposes the string structure in the API. Would be better to just pass in scheme and host in these functions, and have the internal code properly construct the URL.

Revision history for this message
dobey (dobey) wrote :

Also, as this doesn't provide any testcases, I think it's better to be under testing/ instead of testcases/.

Revision history for this message
dobey (dobey) :
review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

The attempt to merge lp:~mandel/ubuntuone-dev-tools/mocked-webserver into lp:ubuntuone-dev-tools failed. Below is the output from the failed tests.

Traceback (most recent call last):
  File "bin/u1trial", line 337, in <module>
    main()
  File "bin/u1trial", line 317, in main
    suite = trial_runner.get_suite(config)
  File "bin/u1trial", line 196, in get_suite
    config['ignore-paths']))
  File "bin/u1trial", line 180, in _collect_tests
    module_suite = self._load_unittest(filepath)
  File "bin/u1trial", line 120, in _load_unittest
    module = __import__(modpath, None, None, [""])
ImportError: No module named tests.test_txwebserver

99. By Manuel de la Peña

Added missing __init__.py

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'ubuntuone/devtools/testcases/txtcpserver.py'
2--- ubuntuone/devtools/testcases/txtcpserver.py 2012-04-13 16:39:56 +0000
3+++ ubuntuone/devtools/testcases/txtcpserver.py 2012-04-17 19:35:27 +0000
4@@ -114,7 +114,6 @@
5
6 def listen_server(self, server_class, *args, **kwargs):
7 """Start a server in a random port."""
8-
9 from twisted.internet import reactor
10 self.server_factory = server_class(*args, **kwargs)
11 self.server_factory._disconnecting = False
12
13=== added directory 'ubuntuone/devtools/testing/tests'
14=== added file 'ubuntuone/devtools/testing/tests/__init__.py'
15--- ubuntuone/devtools/testing/tests/__init__.py 1970-01-01 00:00:00 +0000
16+++ ubuntuone/devtools/testing/tests/__init__.py 2012-04-17 19:35:27 +0000
17@@ -0,0 +1,29 @@
18+# -*- coding: utf-8 -*-
19+#
20+# Copyright 2012 Canonical Ltd.
21+#
22+# This program is free software: you can redistribute it and/or modify it
23+# under the terms of the GNU General Public License version 3, as published
24+# by the Free Software Foundation.
25+#
26+# This program is distributed in the hope that it will be useful, but
27+# WITHOUT ANY WARRANTY; without even the implied warranties of
28+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
29+# PURPOSE. See the GNU General Public License for more details.
30+#
31+# You should have received a copy of the GNU General Public License along
32+# with this program. If not, see <http://www.gnu.org/licenses/>.
33+#
34+# In addition, as a special exception, the copyright holders give
35+# permission to link the code of portions of this program with the
36+# OpenSSL library under certain conditions as described in each
37+# individual source file, and distribute linked combinations
38+# including the two.
39+# You must obey the GNU General Public License in all respects
40+# for all of the code used other than OpenSSL. If you modify
41+# file(s) with this exception, you may extend this exception to your
42+# version of the file(s), but you are not obligated to do so. If you
43+# do not wish to do so, delete this exception statement from your
44+# version. If you delete this exception statement from all source
45+# files in the program, then also delete it here.
46+"""Tests for the testing module."""
47
48=== added file 'ubuntuone/devtools/testing/tests/test_txwebserver.py'
49--- ubuntuone/devtools/testing/tests/test_txwebserver.py 1970-01-01 00:00:00 +0000
50+++ ubuntuone/devtools/testing/tests/test_txwebserver.py 2012-04-17 19:35:27 +0000
51@@ -0,0 +1,172 @@
52+# -*- coding: utf-8 -*-
53+# Copyright 2012 Canonical Ltd.
54+#
55+# This program is free software: you can redistribute it and/or modify it
56+# under the terms of the GNU General Public License version 3, as published
57+# by the Free Software Foundation.
58+#
59+# This program is distributed in the hope that it will be useful, but
60+# WITHOUT ANY WARRANTY; without even the implied warranties of
61+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
62+# PURPOSE. See the GNU General Public License for more details.
63+#
64+# You should have received a copy of the GNU General Public License along
65+# with this program. If not, see <http://www.gnu.org/licenses/>.
66+#
67+# In addition, as a special exception, the copyright holders give
68+# permission to link the code of portions of this program with the
69+# OpenSSL library under certain conditions as described in each
70+# individual source file, and distribute linked combinations
71+# including the two.
72+# You must obey the GNU General Public License in all respects
73+# for all of the code used other than OpenSSL. If you modify
74+# file(s) with this exception, you may extend this exception to your
75+# version of the file(s), but you are not obligated to do so. If you
76+# do not wish to do so, delete this exception statement from your
77+# version. If you delete this exception statement from all source
78+# files in the program, then also delete it here.
79+
80+"""Test the twisted webserver."""
81+
82+
83+from twisted.internet import defer
84+from twisted.trial.unittest import TestCase
85+from twisted.web import client, resource
86+
87+from ubuntuone.devtools.testing.txwebserver import HTTPWebServer
88+
89+
90+SAMPLE_KEY = "result"
91+SAMPLE_VALUE = "sample result"
92+SAMPLE_RESOURCE = '{{"{0}": "{1}"}}'.format(SAMPLE_KEY, SAMPLE_VALUE)
93+SIMPLERESOURCE = "simpleresource"
94+OTHER_SIMPLERESOURCE = "othersimpleresource"
95+THROWERROR = "throwerror"
96+UNAUTHORIZED = "unauthorized"
97+
98+# no init
99+# pylint: disable=W0232
100+
101+
102+class SimpleResource(resource.Resource):
103+ """A simple web resource."""
104+
105+ # pylint: disable=C0103
106+ def render_GET(self, request):
107+ """Make a bit of html out of these resource's content."""
108+ return SAMPLE_RESOURCE
109+ # pylint: enable=C0103
110+
111+
112+class WebServerTestCase(TestCase):
113+ """Test the web server that will allow to have connections."""
114+
115+ @defer.inlineCallbacks
116+ def setUp(self):
117+ """Set the different tests."""
118+ yield super(WebServerTestCase, self).setUp()
119+ # create the root to be used by the webserver
120+ root = resource.Resource()
121+ root.putChild(SIMPLERESOURCE, SimpleResource())
122+ root.putChild(OTHER_SIMPLERESOURCE, SimpleResource())
123+ root.putChild(THROWERROR, resource.NoResource())
124+
125+ unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED,
126+ "Unauthorized", "Unauthorized")
127+ root.putChild(UNAUTHORIZED, unauthorized_resource)
128+ self.server = HTTPWebServer(root)
129+ self.server.start()
130+ self.uri = "http://127.0.0.1:{port}/".format(
131+ port=self.server.get_port())
132+ self.addCleanup(self.server.stop)
133+
134+ @defer.inlineCallbacks
135+ def test_single_request(self):
136+ """Test performing a single request to get the data."""
137+ url = self.uri + SIMPLERESOURCE
138+ result = yield client.getPage(url)
139+ self.assertEqual(SAMPLE_RESOURCE, result)
140+
141+ @defer.inlineCallbacks
142+ def test_multiple_requests(self):
143+ """Test performing multiple requests."""
144+ simple_url = self.uri + SIMPLERESOURCE
145+ other_simple_url = self.uri + OTHER_SIMPLERESOURCE
146+ simple_result = yield client.getPage(simple_url)
147+ other_result = yield client.getPage(other_simple_url)
148+ self.assertEqual(SAMPLE_RESOURCE, simple_result)
149+ self.assertEqual(SAMPLE_RESOURCE, other_result)
150+
151+ def assert_url(self, expected):
152+ """Assert the url."""
153+ port = self.server.get_port()
154+ self.assertEqual(expected.format(port=port), self.server.get_iri())
155+
156+ def test_get_iri(self):
157+ """Test getting the iri from the server."""
158+ expected = u"http://127.0.0.1:{port}/"
159+ self.assert_url(expected)
160+
161+ def test_get_port(self):
162+ """Test getting the port."""
163+ # pylint: disable=W0212
164+ port = self.server.port.getHost().port
165+ # pylint: enable=W0212
166+ self.assertEqual(port, self.server.get_port())
167+
168+
169+class MultipleWebServersTestCase(TestCase):
170+ """Test with multiple webservers running."""
171+
172+ @defer.inlineCallbacks
173+ def setUp(self):
174+ """Set the diff tests."""
175+ yield super(MultipleWebServersTestCase, self).setUp()
176+ self.root = resource.Resource()
177+ self.root.putChild(SIMPLERESOURCE, SimpleResource())
178+ self.root.putChild(OTHER_SIMPLERESOURCE, SimpleResource())
179+ self.root.putChild(THROWERROR, resource.NoResource())
180+
181+ unauthorized_resource = resource.ErrorPage(resource.http.UNAUTHORIZED,
182+ "Unauthorized", "Unauthorized")
183+ self.root.putChild(UNAUTHORIZED, unauthorized_resource)
184+
185+ def get_uri(self, server):
186+ """Return the uri for the server."""
187+ url = "http://127.0.0.1:{port}/"
188+ return url.format(port=server.get_port())
189+
190+ @defer.inlineCallbacks
191+ def test_single_request(self):
192+ """Test performing a single request to get the data."""
193+ first_server = HTTPWebServer(self.root)
194+ first_server.start()
195+ self.addCleanup(first_server.stop)
196+
197+ second_server = HTTPWebServer(self.root)
198+ second_server.start()
199+ self.addCleanup(second_server.stop)
200+
201+ for server in [first_server, second_server]:
202+ url = self.get_uri(server) + SIMPLERESOURCE
203+ result = yield client.getPage(url)
204+ self.assertEqual(SAMPLE_RESOURCE, result)
205+
206+ @defer.inlineCallbacks
207+ def test_multiple_requests(self):
208+ """Test performing multiple requests."""
209+ first_server = HTTPWebServer(self.root)
210+ first_server.start()
211+ self.addCleanup(first_server.stop)
212+
213+ second_server = HTTPWebServer(self.root)
214+ second_server.start()
215+ self.addCleanup(second_server.stop)
216+
217+ for server in [first_server, second_server]:
218+ simple_url = self.get_uri(server) + SIMPLERESOURCE
219+ other_simple_url = self.get_uri(server) + OTHER_SIMPLERESOURCE
220+ simple_result = yield client.getPage(simple_url)
221+ other_result = yield client.getPage(other_simple_url)
222+ self.assertEqual(SAMPLE_RESOURCE, simple_result)
223+ self.assertEqual(SAMPLE_RESOURCE, other_result)
224
225=== added file 'ubuntuone/devtools/testing/txwebserver.py'
226--- ubuntuone/devtools/testing/txwebserver.py 1970-01-01 00:00:00 +0000
227+++ ubuntuone/devtools/testing/txwebserver.py 2012-04-17 19:35:27 +0000
228@@ -0,0 +1,121 @@
229+# -*- coding: utf-8 -*-
230+# Copyright 2012 Canonical Ltd.
231+#
232+# This program is free software: you can redistribute it and/or modify it
233+# under the terms of the GNU General Public License version 3, as published
234+# by the Free Software Foundation.
235+#
236+# This program is distributed in the hope that it will be useful, but
237+# WITHOUT ANY WARRANTY; without even the implied warranties of
238+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
239+# PURPOSE. See the GNU General Public License for more details.
240+#
241+# You should have received a copy of the GNU General Public License along
242+# with this program. If not, see <http://www.gnu.org/licenses/>.
243+#
244+# In addition, as a special exception, the copyright holders give
245+# permission to link the code of portions of this program with the
246+# OpenSSL library under certain conditions as described in each
247+# individual source file, and distribute linked combinations
248+# including the two.
249+# You must obey the GNU General Public License in all respects
250+# for all of the code used other than OpenSSL. If you modify
251+# file(s) with this exception, you may extend this exception to your
252+# version of the file(s), but you are not obligated to do so. If you
253+# do not wish to do so, delete this exception statement from your
254+# version. If you delete this exception statement from all source
255+# files in the program, then also delete it here.
256+
257+"""A tx based web server."""
258+
259+from twisted.internet import defer, reactor, ssl
260+from twisted.protocols.policies import WrappingFactory
261+from twisted.web import server
262+
263+from ubuntuone.devtools.testcases.txtcpserver import server_protocol_factory
264+
265+# no init method + twisted common warnings
266+# pylint: disable=W0232, C0103, E1101
267+
268+
269+class BaseWebServer(object):
270+ """Webserver used to perform requests in tests."""
271+
272+ def __init__(self, root_resource, scheme):
273+ """Create and start the instance.
274+
275+ The ssl_settings parameter contains a dictionary with the key and cert
276+ that will be used to perform ssl connections. The root_resource
277+ contains the resource with all its childre.
278+ """
279+ self.root = root_resource
280+ self.scheme = scheme
281+ self.port = None
282+ # use an http.HTTPFactory that was modified to ensure that we have
283+ # clean close connections
284+ self.site = server.Site(self.root, timeout=None)
285+ self.wrapper = WrappingFactory(self.site)
286+ self.wrapper.testserver_on_connection_lost = defer.Deferred()
287+ self.wrapper.protocol = server_protocol_factory(self.wrapper.protocol)
288+ self.wrapper._disconnecting = False
289+
290+ def listen(self, site):
291+ """Listen a port to allow the tests."""
292+ raise NotImplementedError('Base abstract class.')
293+
294+ def get_iri(self):
295+ """Build the iri for this mock server."""
296+ return u"{scheme}://127.0.0.1:{port}/".format(scheme=self.scheme,
297+ port=self.get_port())
298+
299+ def get_port(self):
300+ """Return the port where we are listening."""
301+ return self.port.getHost().port
302+
303+ def start(self):
304+ """Start the service."""
305+ self.port = self.listen(self.wrapper)
306+
307+ def stop(self):
308+ """Shut it down."""
309+ if self.port:
310+ self.wrapper._disconnecting = True
311+ connected = self.wrapper.protocols.keys()
312+ if connected:
313+ for con in connected:
314+ con.transport.loseConnection()
315+ else:
316+ self.wrapper.testserver_on_connection_lost = \
317+ defer.succeed(None)
318+ d = defer.maybeDeferred(self.port.stopListening)
319+ return defer.gatherResults([d,
320+ self.wrapper.testserver_on_connection_lost])
321+ return defer.succeed(None)
322+
323+
324+class HTTPWebServer(BaseWebServer):
325+ """A Webserver that listens to http connections."""
326+
327+ def __init__(self, root_resource):
328+ """Create a new instance."""
329+ super(HTTPWebServer, self).__init__(root_resource, 'http')
330+
331+ def listen(self, site):
332+ """Listen a port to allow the tests."""
333+ return reactor.listenTCP(0, site)
334+
335+
336+class HTTPSWebServer(BaseWebServer):
337+ """A WebServer that listens to https connections."""
338+
339+ def __init__(self, root_resource, ssl_settings=None):
340+ """Create a new instance."""
341+ super(HTTPSWebServer, self).__init__(root_resource, 'https')
342+ self.ssl_settings = ssl_settings
343+
344+ def listen(self, site):
345+ """Listen a port to allow the tests."""
346+ ssl_context = ssl.DefaultOpenSSLContextFactory(
347+ self.ssl_settings['key'], self.ssl_settings['cert'])
348+
349+ return reactor.listenSSL(0, site, ssl_context)

Subscribers

People subscribed via source and target branches

to all changes: