Merge lp:~pedronis/u1db/http-app-sync-testing into lp:u1db

Proposed by Samuele Pedroni
Status: Merged
Merged at revision: 110
Proposed branch: lp:~pedronis/u1db/http-app-sync-testing
Merge into: lp:u1db
Prerequisite: lp:~pedronis/u1db/http-app
Diff against target: 405 lines (+228/-33)
7 files modified
u1db/remote/http_target.py (+93/-0)
u1db/tests/__init__.py (+20/-8)
u1db/tests/commandline/test_client.py (+6/-1)
u1db/tests/test_remote_sync_server.py (+6/-9)
u1db/tests/test_remote_sync_target.py (+48/-13)
u1db/tests/test_sync.py (+19/-2)
u1db/tests/test_test_infrastructure.py (+36/-0)
To merge this branch: bzr merge lp:~pedronis/u1db/http-app-sync-testing
Reviewer Review Type Date Requested Status
John A Meinel (community) Approve
Review via email: mp+82292@code.launchpad.net

Description of the change

Integration testing for the sync parts of http-app and a start of a HTTP client side, for now it just introduces a HTTPSyncTarget (but subsequently we will need a full HTTPDatabase).

todo for later branches: error handling checking response status

To post a comment you must log in.
119. By Samuele Pedroni

merge trunk

Revision history for this message
John A Meinel (jameinel) wrote :

def prepare(dic):

looks like it would be easier to use as:

def prepare(**dic):

Since the two callers use prepare(dict(...)) they could just call prepare(...)
(minor thing, though)

I like that it runs against both servers, though that may not last particularly long. What I really like, though, is that as a transition step, we know that both work the same way.

I'm curious why you call the HTTPConnection a _client:
43 + self._client = httplib.HTTPConnection(self._url.hostname,
44 + self._url.port)

review: Approve
Revision history for this message
Samuele Pedroni (pedronis) wrote :

> def prepare(dic):
>
> looks like it would be easier to use as:
>
> def prepare(**dic):
>
> Since the two callers use prepare(dict(...)) they could just call prepare(...)
> (minor thing, though)

true

>
> I like that it runs against both servers, though that may not last
> particularly long. What I really like, though, is that as a transition step,
> we know that both work the same way.
>

yup

>
>
> I'm curious why you call the HTTPConnection a _client:
> 43 + self._client = httplib.HTTPConnection(self._url.hostname,
> 44 + self._url.port)

was just easier to share tests with that name, can be renamed later when the tests are organized differently

120. By Samuele Pedroni

small simplification

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'u1db/remote/http_target.py'
2--- u1db/remote/http_target.py 1970-01-01 00:00:00 +0000
3+++ u1db/remote/http_target.py 2011-11-17 14:38:24 +0000
4@@ -0,0 +1,93 @@
5+# Copyright 2011 Canonical Ltd.
6+#
7+# This program is free software: you can redistribute it and/or modify it
8+# under the terms of the GNU General Public License version 3, as published
9+# by the Free Software Foundation.
10+#
11+# This program is distributed in the hope that it will be useful, but
12+# WITHOUT ANY WARRANTY; without even the implied warranties of
13+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14+# PURPOSE. See the GNU General Public License for more details.
15+#
16+# You should have received a copy of the GNU General Public License along
17+# with this program. If not, see <http://www.gnu.org/licenses/>.
18+
19+""""""
20+
21+import httplib
22+import json
23+import urlparse
24+
25+from u1db import (
26+ SyncTarget,
27+ )
28+
29+class HTTPSyncTarget(SyncTarget):
30+ """Implement the SyncTarget api to a remote HTTP server."""
31+
32+ @staticmethod
33+ def connect(url):
34+ return HTTPSyncTarget(url)
35+
36+ def __init__(self, url):
37+ self._url = urlparse.urlsplit(url)
38+ self._client = None
39+
40+ def _ensure_connection(self):
41+ if self._client is not None:
42+ return
43+ self._client = httplib.HTTPConnection(self._url.hostname,
44+ self._url.port)
45+
46+ def get_sync_info(self, other_replica_uid):
47+ self._ensure_connection()
48+ self._client.request('GET', '%s/sync-from/%s' % (self._url.path,
49+ other_replica_uid))
50+ # xxx check for errors with status
51+ res = json.loads(self._client.getresponse().read())
52+ return (res['this_replica_uid'], res['this_replica_generation'],
53+ res['other_replica_generation'])
54+
55+ def record_sync_info(self, other_replica_uid, other_replica_generation):
56+ self._ensure_connection()
57+ self._client.request('PUT',
58+ '%s/sync-from/%s' % (self._url.path,
59+ other_replica_uid),
60+ json.dumps({'generation': other_replica_generation}),
61+ {'content-type': 'application/json'})
62+ self._client.getresponse().read() # xxx check for errors with status
63+
64+ def sync_exchange(self, docs_info, from_replica_uid,
65+ from_replica_generation,
66+ last_known_generation, return_doc_cb):
67+ self._ensure_connection()
68+ self._client.putrequest('POST',
69+ '%s/sync-from/%s' % (self._url.path,
70+ from_replica_uid))
71+ self._client.putheader('content-type', 'application/x-u1db-multi-json')
72+ entries = []
73+ size = 0
74+ def prepare(**dic):
75+ entry = json.dumps(dic)+"\r\n"
76+ entries.append(entry)
77+ return len(entry)
78+ size += prepare(last_known_generation=last_known_generation,
79+ from_replica_generation=from_replica_generation)
80+ for doc_id, doc_rev, doc in docs_info:
81+ size += prepare(id=doc_id, rev=doc_rev, doc=doc)
82+ self._client.putheader('content-length', str(size))
83+ self._client.endheaders()
84+ for entry in entries:
85+ self._client.send(entry)
86+ entries = None
87+ resp = self._client.getresponse() # xxx check for errors with status
88+ data = resp.read().splitlines() # one at a time
89+ res = json.loads(data[0])
90+ for entry in data[1:]:
91+ entry = json.loads(entry)
92+ return_doc_cb(entry['id'], entry['rev'], entry['doc'])
93+ data = None
94+ return res['new_generation']
95+
96+ def get_sync_exchange(self):
97+ return None # not a local target
98
99=== modified file 'u1db/tests/__init__.py'
100--- u1db/tests/__init__.py 2011-11-04 14:46:39 +0000
101+++ u1db/tests/__init__.py 2011-11-17 14:38:24 +0000
102@@ -124,28 +124,40 @@
103 self.sent_response = True
104
105
106-class TestCaseWithSyncServer(TestCase):
107+class TestCaseWithServer(TestCase):
108+
109+ @staticmethod
110+ def server_def():
111+ # should return (ServerClass, RequestHandlerClass,
112+ # "shutdown method name", "url_scheme")
113+ raise NotImplementedError(TestCaseWithServer.server_def)
114
115 def setUp(self):
116- super(TestCaseWithSyncServer, self).setUp()
117+ super(TestCaseWithServer, self).setUp()
118 self.server = self.server_thread = None
119
120- def startServer(self, request_handler=sync_server.TCPSyncRequestHandler):
121+ @property
122+ def url_scheme(self):
123+ return self.server_def()[-1]
124+
125+ def startServer(self, other_request_handler=None):
126+ server_def = self.server_def()
127+ server_class, request_handler, shutdown_meth, _ = server_def
128+ request_handler = other_request_handler or request_handler
129 self.request_state = ServerStateForTests()
130- self.server = sync_server.TCPSyncServer(
131- ('127.0.0.1', 0), request_handler,
132- self.request_state)
133+ self.server = server_class(('127.0.0.1', 0), request_handler,
134+ self.request_state)
135 self.server_thread = threading.Thread(target=self.server.serve_forever,
136 kwargs=dict(poll_interval=0.01))
137 self.server_thread.start()
138 self.addCleanup(self.server_thread.join)
139- self.addCleanup(self.server.force_shutdown)
140+ self.addCleanup(getattr(self.server, shutdown_meth))
141
142 def getURL(self, path=None):
143 host, port = self.server.server_address
144 if path is None:
145 path = ''
146- return 'u1db://%s:%s/%s' % (host, port, path)
147+ return '%s://%s:%s/%s' % (self.url_scheme, host, port, path)
148
149
150 def socket_pair():
151
152=== modified file 'u1db/tests/commandline/test_client.py'
153--- u1db/tests/commandline/test_client.py 2011-11-14 13:52:12 +0000
154+++ u1db/tests/commandline/test_client.py 2011-11-17 14:38:24 +0000
155@@ -27,6 +27,7 @@
156 )
157 from u1db.commandline import client
158 from u1db.tests.commandline import safe_close
159+from u1db.tests import test_remote_sync_server
160
161
162 class TestArgs(tests.TestCase):
163@@ -192,7 +193,11 @@
164 self.db.get_doc('my-test-id'))
165
166
167-class TestCmdSyncRemote(tests.TestCaseWithSyncServer, TestCaseWithDB):
168+class TestCmdSyncRemote(tests.TestCaseWithServer, TestCaseWithDB):
169+
170+ @staticmethod
171+ def server_def():
172+ return test_remote_sync_server.TestTCPSyncServer.server_def()
173
174 def setUp(self):
175 super(TestCmdSyncRemote, self).setUp()
176
177=== modified file 'u1db/tests/test_remote_sync_server.py'
178--- u1db/tests/test_remote_sync_server.py 2011-11-01 19:07:07 +0000
179+++ u1db/tests/test_remote_sync_server.py 2011-11-17 14:38:24 +0000
180@@ -51,15 +51,12 @@
181 self.request.sendall('goodbye\n')
182
183
184-class TestTestCaseWithSyncServer(tests.TestCaseWithSyncServer):
185-
186- def test_getURL(self):
187- self.startServer()
188- url = self.getURL()
189- self.assertTrue(url.startswith('u1db://127.0.0.1:'))
190-
191-
192-class TestTCPSyncServer(tests.TestCaseWithSyncServer):
193+class TestTCPSyncServer(tests.TestCaseWithServer):
194+
195+ @staticmethod
196+ def server_def():
197+ return (sync_server.TCPSyncServer, sync_server.TCPSyncRequestHandler,
198+ "force_shutdown", "u1db")
199
200 def connectToServer(self):
201 client_sock = socket.socket()
202
203=== modified file 'u1db/tests/test_remote_sync_target.py'
204--- u1db/tests/test_remote_sync_target.py 2011-11-02 22:42:05 +0000
205+++ u1db/tests/test_remote_sync_target.py 2011-11-17 14:38:24 +0000
206@@ -12,38 +12,72 @@
207 # You should have received a copy of the GNU General Public License along
208 # with this program. If not, see <http://www.gnu.org/licenses/>.
209
210-"""Tests for the RemoteSyncTarget"""
211+"""Tests for the remote sync targets"""
212
213 import os
214+from wsgiref import simple_server
215+#from paste import httpserver
216
217 from u1db import (
218 tests,
219 )
220 from u1db.remote import (
221+ sync_server,
222 sync_target,
223+ http_app,
224+ http_target
225 )
226 from u1db.backends import (
227 sqlite_backend,
228 )
229
230
231-class TestRemoteSyncTarget(tests.TestCaseWithSyncServer):
232+def remote_server_def():
233+ return (sync_server.TCPSyncServer, sync_server.TCPSyncRequestHandler,
234+ "force_shutdown", "u1db")
235+
236+def http_server_def():
237+ def make_server(host_port, handler, state):
238+ application = http_app.HTTPApp(state)
239+ srv = simple_server.WSGIServer(host_port, handler)
240+ srv.set_app(application)
241+ #srv = httpserver.WSGIServerBase(application,
242+ # host_port,
243+ # handler
244+ # )
245+ return srv
246+ class req_handler(simple_server.WSGIRequestHandler):
247+ def log_request(*args):
248+ pass # suppress
249+ #rh = httpserver.WSGIHandler
250+ return make_server, req_handler, "shutdown", "http"
251+
252+
253+class TestRemoteSyncTargets(tests.TestCaseWithServer):
254+
255+ scenarios = [
256+ ('http', {'server_def': http_server_def,
257+ 'sync_target_class': http_target.HTTPSyncTarget}),
258+ ('remote', {'server_def': remote_server_def,
259+ 'sync_target_class': sync_target.RemoteSyncTarget}),
260+ ]
261
262 def getSyncTarget(self, path=None):
263 if self.server is None:
264 self.startServer()
265- return sync_target.RemoteSyncTarget.connect(self.getURL(path))
266+ return self.sync_target_class(self.getURL(path))
267
268 def test_connect(self):
269 self.startServer()
270 url = self.getURL()
271- remote_target = sync_target.RemoteSyncTarget.connect(url)
272+ remote_target = self.sync_target_class.connect(url)
273 self.assertEqual(url, remote_target._url.geturl())
274- self.assertIs(None, remote_target._conn)
275+ self.assertIs(None, remote_target._client)
276
277 def test_parse_url(self):
278- remote_target = sync_target.RemoteSyncTarget('u1db://127.0.0.1:12345/')
279- self.assertEqual('u1db', remote_target._url.scheme)
280+ remote_target = self.sync_target_class(
281+ '%s://127.0.0.1:12345/' % self.url_scheme)
282+ self.assertEqual(self.url_scheme, remote_target._url.scheme)
283 self.assertEqual('127.0.0.1', remote_target._url.hostname)
284 self.assertEqual(12345, remote_target._url.port)
285 self.assertEqual('/', remote_target._url.path)
286@@ -54,13 +88,12 @@
287
288 def test__ensure_connection(self):
289 remote_target = self.getSyncTarget()
290- self.assertIs(None, remote_target._conn)
291- remote_target._ensure_connection()
292- self.assertIsNot(None, remote_target._conn)
293- c = remote_target._conn
294- remote_target._ensure_connection()
295- self.assertIs(c, remote_target._conn)
296+ self.assertIs(None, remote_target._client)
297+ remote_target._ensure_connection()
298 self.assertIsNot(None, remote_target._client)
299+ cli = remote_target._client
300+ remote_target._ensure_connection()
301+ self.assertIs(cli, remote_target._client)
302
303 def test_get_sync_info(self):
304 self.startServer()
305@@ -106,3 +139,5 @@
306 last_known_generation=0, return_doc_cb=receive_doc)
307 self.assertEqual(1, new_gen)
308 self.assertEqual([(doc_id, doc_rev, {'value': 'there'})], other_docs)
309+
310+load_tests = tests.load_with_scenarios
311
312=== modified file 'u1db/tests/test_sync.py'
313--- u1db/tests/test_sync.py 2011-11-02 22:42:05 +0000
314+++ u1db/tests/test_sync.py 2011-11-17 14:38:24 +0000
315@@ -14,6 +14,7 @@
316
317 """The Synchronization class for U1DB."""
318
319+from wsgiref import simple_server
320
321 from u1db import (
322 errors,
323@@ -23,8 +24,14 @@
324 )
325 from u1db.remote import (
326 sync_target,
327+ http_app,
328+ http_target,
329 )
330
331+from u1db.tests.test_remote_sync_target import (
332+ http_server_def,
333+ remote_server_def,
334+ )
335
336 simple_doc = tests.simple_doc
337 nested_doc = tests.nested_doc
338@@ -43,14 +50,24 @@
339 return db, st
340
341
342+def _make_local_db_and_http_target(test):
343+ test.startServer()
344+ db = test.request_state._create_database('test')
345+ st = http_target.HTTPSyncTarget.connect(test.getURL('test'))
346+ return db, st
347+
348+
349 target_scenarios = [
350 ('local', {'create_db_and_target': _make_local_db_and_target}),
351- ('remote', {'create_db_and_target': _make_local_db_and_remote_target}),
352+ ('remote', {'create_db_and_target': _make_local_db_and_remote_target,
353+ 'server_def': remote_server_def}),
354+ ('http', {'create_db_and_target': _make_local_db_and_http_target,
355+ 'server_def': http_server_def}),
356 ]
357
358
359 class DatabaseSyncTargetTests(tests.DatabaseBaseTests,
360- tests.TestCaseWithSyncServer):
361+ tests.TestCaseWithServer):
362
363 scenarios = tests.multiply_scenarios(tests.DatabaseBaseTests.scenarios,
364 target_scenarios)
365
366=== added file 'u1db/tests/test_test_infrastructure.py'
367--- u1db/tests/test_test_infrastructure.py 1970-01-01 00:00:00 +0000
368+++ u1db/tests/test_test_infrastructure.py 2011-11-17 14:38:24 +0000
369@@ -0,0 +1,36 @@
370+# Copyright 2011 Canonical Ltd.
371+#
372+# This program is free software: you can redistribute it and/or modify it
373+# under the terms of the GNU General Public License version 3, as published
374+# by the Free Software Foundation.
375+#
376+# This program is distributed in the hope that it will be useful, but
377+# WITHOUT ANY WARRANTY; without even the implied warranties of
378+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
379+# PURPOSE. See the GNU General Public License for more details.
380+#
381+# You should have received a copy of the GNU General Public License along
382+# with this program. If not, see <http://www.gnu.org/licenses/>.
383+
384+"""Tests for test infrastructure bits"""
385+
386+from wsgiref import simple_server
387+
388+from u1db import (
389+ tests,
390+ )
391+
392+
393+class TestTestCaseWithServer(tests.TestCaseWithServer):
394+
395+ @staticmethod
396+ def server_def():
397+ def make_server(host_port, handler, state):
398+ return simple_server.WSGIServer(host_port, handler)
399+ return (make_server, simple_server.WSGIRequestHandler,
400+ "shutdown", "http")
401+
402+ def test_getURL(self):
403+ self.startServer()
404+ url = self.getURL()
405+ self.assertTrue(url.startswith('http://127.0.0.1:'))

Subscribers

People subscribed via source and target branches