Merge lp:~pedronis/u1db/http-app-sync-testing into lp:u1db
- http-app-sync-testing
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
John A Meinel (community) | Approve | ||
Review via email: mp+82292@code.launchpad.net |
Commit message
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
- 119. By Samuele Pedroni
-
merge trunk
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.
> 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
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:')) |
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: HTTPConnection( self._url. hostname,
43 + self._client = httplib.
44 + self._url.port)