Merge lp:~pedronis/u1db/improve-reusability-of-sync-tests into lp:u1db

Proposed by Samuele Pedroni
Status: Merged
Approved by: Samuele Pedroni
Approved revision: 409
Merged at revision: 407
Proposed branch: lp:~pedronis/u1db/improve-reusability-of-sync-tests
Merge into: lp:u1db
Diff against target: 447 lines (+150/-19)
4 files modified
u1db/__init__.py (+10/-0)
u1db/remote/http_target.py (+10/-0)
u1db/tests/c_backend_wrapper.pyx (+2/-0)
u1db/tests/test_sync.py (+128/-19)
To merge this branch: bzr merge lp:~pedronis/u1db/improve-reusability-of-sync-tests
Reviewer Review Type Date Requested Status
Eric Casteleijn (community) Approve
Review via email: mp+125575@code.launchpad.net

Commit message

improve reusuability of the sync tests

Description of the change

improve reusuability of the sync tests:

- distinguish tests that need shallow hooking into the target vs. tests that need to hook to follow the inner
  working of sync_exchange

- in DatabaseSyncTests annotate database creations with their sync role (source, target, both)

To post a comment you must log in.
Revision history for this message
Eric Casteleijn (thisfred) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'u1db/__init__.py'
2--- u1db/__init__.py 2012-09-17 16:35:19 +0000
3+++ u1db/__init__.py 2012-09-20 20:46:21 +0000
4@@ -671,3 +671,13 @@
5 :param cb: A callable that takes cb(state)
6 """
7 raise NotImplementedError(self._set_trace_hook)
8+
9+ def _set_trace_hook_shallow(self, cb):
10+ """Set a callback that will be invoked to trace database actions.
11+
12+ Similar to _set_trace_hook, for implementations that don't offer
13+ state changes from the inner working of sync_exchange().
14+
15+ :param cb: A callable that takes cb(state)
16+ """
17+ self._set_trace_hook(cb)
18
19=== modified file 'u1db/remote/http_target.py'
20--- u1db/remote/http_target.py 2012-09-20 17:26:47 +0000
21+++ u1db/remote/http_target.py 2012-09-20 20:46:21 +0000
22@@ -51,6 +51,8 @@
23 def record_sync_info(self, source_replica_uid, source_replica_generation,
24 source_transaction_id):
25 self._ensure_connection()
26+ if self._trace_hook: # for tests
27+ self._trace_hook('record_sync_info')
28 self._request_json('PUT', ['sync-from', source_replica_uid], {},
29 {'generation': source_replica_generation,
30 'transaction_id': source_transaction_id})
31@@ -88,6 +90,8 @@
32 last_known_generation, last_known_trans_id,
33 return_doc_cb):
34 self._ensure_connection()
35+ if self._trace_hook: # for tests
36+ self._trace_hook('sync_exchange')
37 url = '%s/sync-from/%s' % (self._url.path, source_replica_uid)
38 self._conn.putrequest('POST', url)
39 self._conn.putheader('content-type', 'application/x-u1db-sync-stream')
40@@ -120,3 +124,9 @@
41 res = self._parse_sync_stream(data, return_doc_cb)
42 data = None
43 return res['new_generation'], res['new_transaction_id']
44+
45+ # for tests
46+ _trace_hook = None
47+
48+ def _set_trace_hook_shallow(self, cb):
49+ self._trace_hook = cb
50
51=== modified file 'u1db/tests/c_backend_wrapper.pyx'
52--- u1db/tests/c_backend_wrapper.pyx 2012-08-15 15:36:30 +0000
53+++ u1db/tests/c_backend_wrapper.pyx 2012-09-20 20:46:21 +0000
54@@ -873,6 +873,8 @@
55 handle_status("_set_trace_hook",
56 self._st._set_trace_hook(self._st, <void*>cb, _trace_hook))
57
58+ _set_trace_hook_shallow = _set_trace_hook
59+
60
61 cdef class CDatabase(object):
62 """A thin wrapper/shim to interact with the C implementation.
63
64=== modified file 'u1db/tests/test_sync.py'
65--- u1db/tests/test_sync.py 2012-08-15 14:51:29 +0000
66+++ u1db/tests/test_sync.py 2012-09-20 20:46:21 +0000
67@@ -24,6 +24,7 @@
68 sync,
69 tests,
70 vectorclock,
71+ SyncTarget,
72 )
73 from u1db.backends import (
74 inmemory,
75@@ -135,9 +136,11 @@
76 self.other_changes.append(
77 (doc.doc_id, doc.rev, doc.get_json(), gen, trans_id))
78
79- def set_trace_hook(self, callback):
80+ def set_trace_hook(self, callback, shallow=False):
81+ setter = (self.st._set_trace_hook if not shallow else
82+ self.st._set_trace_hook_shallow)
83 try:
84- self.st._set_trace_hook(callback)
85+ setter(callback)
86 except NotImplementedError:
87 self.skipTest("%s does not implement _set_trace_hook"
88 % (self.st.__class__.__name__,))
89@@ -400,9 +403,34 @@
90 ],
91 called)
92
93-
94-def sync_via_synchronizer(test, db_source, db_target, trace_hook=None):
95+ def test__set_trace_hook_shallow(self):
96+ if (self.st._set_trace_hook_shallow == self.st._set_trace_hook
97+ or self.st._set_trace_hook_shallow.im_func ==
98+ SyncTarget._set_trace_hook_shallow.im_func):
99+ # shallow same as full
100+ expected = ['before whats_changed',
101+ 'after whats_changed',
102+ 'before get_docs',
103+ 'record_sync_info',
104+ ]
105+ else:
106+ expected = ['sync_exchange', 'record_sync_info']
107+
108+ called = []
109+
110+ def cb(state):
111+ called.append(state)
112+
113+ self.set_trace_hook(cb, shallow=True)
114+ self.st.sync_exchange([], 'replica', 0, None, self.receive_doc)
115+ self.st.record_sync_info('replica', 0, 'T-sid')
116+ self.assertEqual(expected, called)
117+
118+
119+def sync_via_synchronizer(test, db_source, db_target, trace_hook=None,
120+ trace_hook_shallow=None):
121 target = db_target.get_sync_target()
122+ trace_hook = trace_hook or trace_hook_shallow
123 if trace_hook:
124 target._set_trace_hook(trace_hook)
125 return sync.Synchronizer(db_source, target).sync()
126@@ -448,11 +476,13 @@
127
128
129 def sync_via_synchronizer_and_http(test, db_source, db_target,
130- trace_hook=None):
131+ trace_hook=None, trace_hook_shallow=None):
132 if trace_hook:
133- test.skipTest("trace_hook unsupported over http")
134+ test.skipTest("full trace hook unsupported over http")
135 path = test._http_at[db_target]
136 target = http_target.HTTPSyncTarget.connect(test.getURL(path))
137+ if trace_hook_shallow:
138+ target._set_trace_hook_shallow(trace_hook_shallow)
139 return sync.Synchronizer(db_source, target).sync()
140
141
142@@ -467,8 +497,10 @@
143
144 if tests.c_backend_wrapper is not None:
145 # TODO: We should hook up sync tests with an HTTP target
146- def sync_via_c_sync(test, db_source, db_target, trace_hook=None):
147+ def sync_via_c_sync(test, db_source, db_target, trace_hook=None,
148+ trace_hook_shallow=None):
149 target = db_target.get_sync_target()
150+ trace_hook = trace_hook or trace_hook_shallow
151 if trace_hook:
152 target._set_trace_hook(trace_hook)
153 return tests.c_backend_wrapper.sync_db_to_target(db_source, target)
154@@ -488,13 +520,42 @@
155 scenarios = sync_scenarios
156 do_sync = None # set by scenarios
157
158- def sync(self, db_source, db_target, trace_hook=None):
159- return self.do_sync(self, db_source, db_target, trace_hook)
160+ def create_database(self, replica_uid, sync_role=None):
161+ if replica_uid != 'test':
162+ assert sync_role
163+ db = self.make_database_for_test(self, replica_uid)
164+ if sync_role:
165+ self._use_tracking[db] = (replica_uid, sync_role)
166+ return db
167+
168+ def copy_database(self, db, sync_role=None):
169+ # DO NOT COPY OR REUSE THIS CODE OUTSIDE TESTS: COPYING U1DB DATABASES
170+ # IS THE WRONG THING TO DO, THE ONLY REASON WE DO SO HERE IS TO TEST
171+ # THAT WE CORRECTLY DETECT IT HAPPENING SO THAT WE CAN RAISE ERRORS
172+ # RATHER THAN CORRUPT USER DATA. USE SYNC INSTEAD, OR WE WILL SEND
173+ # NINJA TO YOUR HOUSE.
174+ db_copy = self.copy_database_for_test(self, db)
175+ name, orig_sync_role = self._use_tracking[db]
176+ self._use_tracking[db_copy] = (name + '(copy)', sync_role
177+ or orig_sync_role)
178+ return db_copy
179+
180+ def sync(self, db_from, db_to, trace_hook=None,
181+ trace_hook_shallow=None):
182+ from_name, from_sync_role = self._use_tracking[db_from]
183+ to_name, to_sync_role = self._use_tracking[db_to]
184+ if from_sync_role not in ('source', 'both'):
185+ raise Exception("%s marked for %s use but used as source" %
186+ (from_name, from_sync_role))
187+ if to_sync_role not in ('target', 'both'):
188+ raise Exception("%s marked for %s use but used as target" %
189+ (to_name, to_sync_role))
190+ return self.do_sync(self, db_from, db_to, trace_hook,
191+ trace_hook_shallow)
192
193 def setUp(self):
194+ self._use_tracking = {}
195 super(DatabaseSyncTests, self).setUp()
196- self.db1 = self.create_database('test1')
197- self.db2 = self.create_database('test2')
198
199 def assertLastExchangeLog(self, db, expected):
200 log = getattr(db, '_last_exchange_log', None)
201@@ -503,6 +564,8 @@
202 self.assertEqual(expected, log)
203
204 def test_sync_tracks_db_generation_of_other(self):
205+ self.db1 = self.create_database('test1', 'source')
206+ self.db2 = self.create_database('test2', 'target')
207 self.assertEqual(0, self.sync(self.db1, self.db2))
208 self.assertEqual(
209 (0, ''), self.db1._get_replica_gen_and_trans_id('test2'))
210@@ -513,6 +576,8 @@
211 'return': {'docs': [], 'last_gen': 0}})
212
213 def test_sync_autoresolves(self):
214+ self.db1 = self.create_database('test1', 'source')
215+ self.db2 = self.create_database('test2', 'target')
216 doc1 = self.db1.create_doc_from_json(simple_doc, doc_id='doc')
217 rev1 = doc1.rev
218 doc2 = self.db2.create_doc_from_json(simple_doc, doc_id='doc')
219@@ -548,6 +613,8 @@
220 # a3b2 a1b2 (autoresolved)
221 # `------->
222 # a3b2 a3b2
223+ self.db1 = self.create_database('test1', 'source')
224+ self.db2 = self.create_database('test2', 'target')
225 self.db1.create_doc_from_json(simple_doc, doc_id='doc')
226 self.sync(self.db1, self.db2)
227 for db, content in [(self.db1, '{}'), (self.db2, '{"hi": 42}')]:
228@@ -600,6 +667,8 @@
229 # a1b1+a2 a1b2 (a1b2 has same content as a2)
230 # <-------'
231 # a3b2 a3b2 (autoresolved and propagated)
232+ self.db1 = self.create_database('test1', 'both')
233+ self.db2 = self.create_database('test2', 'both')
234 self.db1.create_doc_from_json(simple_doc, doc_id='doc')
235 self.sync(self.db1, self.db2)
236 for db, content in [(self.db1, '{}'), (self.db2, '{"hi": 42}')]:
237@@ -655,7 +724,9 @@
238 # a2c1 a2+a1b1 a2c1
239 # `------->
240 # a2b2c1 a2b2c1 a2c1
241- self.db3 = self.create_database('test3')
242+ self.db1 = self.create_database('test1', 'source')
243+ self.db2 = self.create_database('test2', 'both')
244+ self.db3 = self.create_database('test3', 'target')
245 self.db1.create_doc_from_json(simple_doc, doc_id='doc')
246 self.sync(self.db1, self.db2)
247 self.sync(self.db2, self.db3)
248@@ -703,6 +774,8 @@
249 self.assertEqual(self.db1.get_doc('doc'), self.db2.get_doc('doc'))
250
251 def test_sync_puts_changes(self):
252+ self.db1 = self.create_database('test1', 'source')
253+ self.db2 = self.create_database('test2', 'target')
254 doc = self.db1.create_doc_from_json(simple_doc)
255 self.assertEqual(1, self.sync(self.db1, self.db2))
256 self.assertGetDoc(self.db2, doc.doc_id, doc.rev, simple_doc, False)
257@@ -715,6 +788,8 @@
258 'return': {'docs': [], 'last_gen': 1}})
259
260 def test_sync_pulls_changes(self):
261+ self.db1 = self.create_database('test1', 'source')
262+ self.db2 = self.create_database('test2', 'target')
263 doc = self.db2.create_doc_from_json(simple_doc)
264 self.db1.create_index('test-idx', 'key')
265 self.assertEqual(0, self.sync(self.db1, self.db2))
266@@ -728,6 +803,8 @@
267 self.assertEqual([doc], self.db1.get_from_index('test-idx', 'value'))
268
269 def test_sync_pulling_doesnt_update_other_if_changed(self):
270+ self.db1 = self.create_database('test1', 'source')
271+ self.db2 = self.create_database('test2', 'target')
272 doc = self.db2.create_doc_from_json(simple_doc)
273 # After the local side has sent its list of docs, before we start
274 # receiving the "targets" response, we update the local database with a
275@@ -754,6 +831,8 @@
276 (0, ''), self.db2._get_replica_gen_and_trans_id('test1'))
277
278 def test_sync_doesnt_update_other_if_nothing_pulled(self):
279+ self.db1 = self.create_database('test1', 'source')
280+ self.db2 = self.create_database('test2', 'target')
281 self.db1.create_doc_from_json(simple_doc)
282
283 def no_record_sync_info(state):
284@@ -761,14 +840,16 @@
285 return
286 self.fail('SyncTarget.record_sync_info was called')
287 self.assertEqual(1, self.sync(self.db1, self.db2,
288- trace_hook=no_record_sync_info))
289+ trace_hook_shallow=no_record_sync_info))
290 self.assertEqual(
291 1,
292 self.db2._get_replica_gen_and_trans_id(self.db1._replica_uid)[0])
293
294 def test_sync_ignores_convergence(self):
295+ self.db1 = self.create_database('test1', 'source')
296+ self.db2 = self.create_database('test2', 'both')
297 doc = self.db1.create_doc_from_json(simple_doc)
298- self.db3 = self.create_database('test3')
299+ self.db3 = self.create_database('test3', 'target')
300 self.assertEqual(1, self.sync(self.db1, self.db3))
301 self.assertEqual(0, self.sync(self.db2, self.db3))
302 self.assertEqual(1, self.sync(self.db1, self.db2))
303@@ -779,9 +860,11 @@
304 'return': {'docs': [], 'last_gen': 1}})
305
306 def test_sync_ignores_superseded(self):
307+ self.db1 = self.create_database('test1', 'both')
308+ self.db2 = self.create_database('test2', 'both')
309 doc = self.db1.create_doc_from_json(simple_doc)
310 doc_rev1 = doc.rev
311- self.db3 = self.create_database('test3')
312+ self.db3 = self.create_database('test3', 'target')
313 self.sync(self.db1, self.db3)
314 self.sync(self.db2, self.db3)
315 new_content = '{"key": "altval"}'
316@@ -798,6 +881,8 @@
317 self.assertGetDoc(self.db1, doc.doc_id, doc_rev2, new_content, False)
318
319 def test_sync_sees_remote_conflicted(self):
320+ self.db1 = self.create_database('test1', 'source')
321+ self.db2 = self.create_database('test2', 'target')
322 doc1 = self.db1.create_doc_from_json(simple_doc)
323 doc_id = doc1.doc_id
324 doc1_rev = doc1.rev
325@@ -823,6 +908,8 @@
326 self.assertEqual([], self.db1.get_from_index('test-idx', 'value'))
327
328 def test_sync_sees_remote_delete_conflicted(self):
329+ self.db1 = self.create_database('test1', 'source')
330+ self.db2 = self.create_database('test2', 'target')
331 doc1 = self.db1.create_doc_from_json(simple_doc)
332 doc_id = doc1.doc_id
333 self.db1.create_index('test-idx', 'key')
334@@ -847,6 +934,8 @@
335 self.assertEqual([], self.db1.get_from_index('test-idx', 'value'))
336
337 def test_sync_local_race_conflicted(self):
338+ self.db1 = self.create_database('test1', 'source')
339+ self.db2 = self.create_database('test2', 'target')
340 doc = self.db1.create_doc_from_json(simple_doc)
341 doc_id = doc.doc_id
342 doc1_rev = doc.rev
343@@ -877,12 +966,14 @@
344 self.assertEqual([], self.db1.get_from_index('test-idx', 'localval'))
345
346 def test_sync_propagates_deletes(self):
347+ self.db1 = self.create_database('test1', 'source')
348+ self.db2 = self.create_database('test2', 'both')
349 doc1 = self.db1.create_doc_from_json(simple_doc)
350 doc_id = doc1.doc_id
351 self.db1.create_index('test-idx', 'key')
352 self.sync(self.db1, self.db2)
353 self.db2.create_index('test-idx', 'key')
354- self.db3 = self.create_database('test3')
355+ self.db3 = self.create_database('test3', 'target')
356 self.sync(self.db1, self.db3)
357 self.db1.delete_doc(doc1)
358 deleted_rev = doc1.rev
359@@ -908,8 +999,10 @@
360 self.db3, doc_id, deleted_rev, None, False)
361
362 def test_sync_propagates_resolution(self):
363+ self.db1 = self.create_database('test1', 'both')
364+ self.db2 = self.create_database('test2', 'both')
365 doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc')
366- db3 = self.create_database('test3')
367+ db3 = self.create_database('test3', 'both')
368 self.sync(self.db2, self.db1)
369 self.assertEqual(
370 self.db1._get_generation_info(),
371@@ -945,7 +1038,9 @@
372 self.assertFalse(doc3.has_conflicts)
373
374 def test_sync_supersedes_conflicts(self):
375- db3 = self.create_database('test3')
376+ self.db1 = self.create_database('test1', 'both')
377+ self.db2 = self.create_database('test2', 'target')
378+ db3 = self.create_database('test3', 'both')
379 doc1 = self.db1.create_doc_from_json('{"a": 1}', doc_id='the-doc')
380 self.db2.create_doc_from_json('{"b": 1}', doc_id='the-doc')
381 db3.create_doc_from_json('{"c": 1}', doc_id='the-doc')
382@@ -971,15 +1066,19 @@
383 self.assertEqual(3, len(db3.get_doc_conflicts('the-doc')))
384
385 def test_sync_stops_after_get_sync_info(self):
386+ self.db1 = self.create_database('test1', 'source')
387+ self.db2 = self.create_database('test2', 'target')
388 self.db1.create_doc_from_json(tests.simple_doc)
389 self.sync(self.db1, self.db2)
390
391 def put_hook(state):
392 self.fail("Tracehook triggered for %s" % (state,))
393
394- self.sync(self.db1, self.db2, trace_hook=put_hook)
395+ self.sync(self.db1, self.db2, trace_hook_shallow=put_hook)
396
397 def test_sync_detects_rollback_in_source(self):
398+ self.db1 = self.create_database('test1', 'source')
399+ self.db2 = self.create_database('test2', 'target')
400 self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1')
401 self.sync(self.db1, self.db2)
402 db1_copy = self.copy_database(self.db1)
403@@ -989,6 +1088,8 @@
404 errors.InvalidGeneration, self.sync, db1_copy, self.db2)
405
406 def test_sync_detects_rollback_in_target(self):
407+ self.db1 = self.create_database('test1', 'source')
408+ self.db2 = self.create_database('test2', 'target')
409 self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent")
410 self.sync(self.db1, self.db2)
411 db2_copy = self.copy_database(self.db2)
412@@ -998,6 +1099,8 @@
413 errors.InvalidGeneration, self.sync, self.db1, db2_copy)
414
415 def test_sync_detects_diverged_source(self):
416+ self.db1 = self.create_database('test1', 'source')
417+ self.db2 = self.create_database('test2', 'target')
418 db3 = self.copy_database(self.db1)
419 self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent")
420 db3.create_doc_from_json(tests.simple_doc, doc_id="divergent")
421@@ -1006,6 +1109,8 @@
422 errors.InvalidTransactionId, self.sync, db3, self.db2)
423
424 def test_sync_detects_diverged_target(self):
425+ self.db1 = self.create_database('test1', 'source')
426+ self.db2 = self.create_database('test2', 'target')
427 db3 = self.copy_database(self.db2)
428 db3.create_doc_from_json(tests.nested_doc, doc_id="divergent")
429 self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent")
430@@ -1014,6 +1119,8 @@
431 errors.InvalidTransactionId, self.sync, self.db1, db3)
432
433 def test_sync_detects_rollback_and_divergence_in_source(self):
434+ self.db1 = self.create_database('test1', 'source')
435+ self.db2 = self.create_database('test2', 'target')
436 self.db1.create_doc_from_json(tests.simple_doc, doc_id='doc1')
437 self.sync(self.db1, self.db2)
438 db1_copy = self.copy_database(self.db1)
439@@ -1026,6 +1133,8 @@
440 errors.InvalidTransactionId, self.sync, db1_copy, self.db2)
441
442 def test_sync_detects_rollback_and_divergence_in_target(self):
443+ self.db1 = self.create_database('test1', 'source')
444+ self.db2 = self.create_database('test2', 'target')
445 self.db1.create_doc_from_json(tests.simple_doc, doc_id="divergent")
446 self.sync(self.db1, self.db2)
447 db2_copy = self.copy_database(self.db2)

Subscribers

People subscribed via source and target branches