From 8e32fdb0be5d34c6554a8c0f75bdf8bf0debcd4a Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 9 Jan 2013 11:46:58 -0200 Subject: CouchDatabase passes u1db LocalDatabaseTests. --- backends/couch.py | 8 +++-- backends/objectstore.py | 69 ++++++++++++++++++++++++++++++++++------ tests/test_couch.py | 83 ++++++++++++++++++++++++++++++++++++++++++------- tests/test_logs.py | 28 ++++++++++++----- util.py | 17 +++++++++- 5 files changed, 172 insertions(+), 33 deletions(-) diff --git a/backends/couch.py b/backends/couch.py index 8603a36b..14021737 100644 --- a/backends/couch.py +++ b/backends/couch.py @@ -58,6 +58,8 @@ class CouchDatabase(ObjectStore): generation = self._get_generation() results = [] for doc_id in self._database: + if doc_id == self.U1DB_DATA_DOC_ID: + continue doc = self._get_doc(doc_id) if doc.content is None and not include_deleted: continue @@ -106,13 +108,15 @@ class CouchDatabase(ObjectStore): content = json.loads(cdoc['u1db_json']) self._sync_log.log = content['sync_log'] self._transaction_log.log = content['transaction_log'] + self._conflict_log.log = content['conflict_log'] self._replica_uid = content['replica_uid'] self._couch_rev = cdoc['_rev'] def _set_u1db_data(self): doc = self._factory(doc_id=self.U1DB_DATA_DOC_ID) - doc.content = { 'transaction_log' : self._transaction_log.log, - 'sync_log' : self._sync_log.log, + doc.content = { 'sync_log' : self._sync_log.log, + 'transaction_log' : self._transaction_log.log, + 'conflict_log' : self._conflict_log.log, 'replica_uid' : self._replica_uid, '_rev' : self._couch_rev} self._put_doc(doc) diff --git a/backends/objectstore.py b/backends/objectstore.py index 54ffa9dd..cd051588 100644 --- a/backends/objectstore.py +++ b/backends/objectstore.py @@ -16,6 +16,7 @@ class ObjectStore(CommonBackend): self.set_document_factory(Document) self._sync_log = soledadutil.SyncLog() self._transaction_log = soledadutil.TransactionLog() + self._conflict_log = soledadutil.ConflictLog() self._replica_uid = replica_uid self._ensure_u1db_data() @@ -44,6 +45,12 @@ class ObjectStore(CommonBackend): def _put_doc(self, doc): raise NotImplementedError(self._put_doc) + def _update_gen_and_transaction_log(self, doc_id): + new_gen = self._get_generation() + 1 + trans_id = self._allocate_transaction_id() + self._transaction_log.append((new_gen, doc_id, trans_id)) + self._set_u1db_data() + def put_doc(self, doc): # consistency check if doc.doc_id is None: @@ -66,11 +73,7 @@ class ObjectStore(CommonBackend): new_rev = self._allocate_doc_rev(doc.rev) doc.rev = new_rev self._put_doc(doc) - # update u1db generation and logs - new_gen = self._get_generation() + 1 - trans_id = self._allocate_transaction_id() - self._transaction_log.append((new_gen, doc.doc_id, trans_id)) - self._set_u1db_data() + self._update_gen_and_transaction_log(doc.doc_id) return doc.rev def delete_doc(self, doc): @@ -87,6 +90,7 @@ class ObjectStore(CommonBackend): doc.rev = new_rev doc.make_tombstone() self._put_doc(doc) + self._update_gen_and_transaction_log(doc.doc_id) return new_rev # start of index-related methods: these are not supported by this backend. @@ -130,6 +134,16 @@ class ObjectStore(CommonBackend): other_transaction_id) self._set_u1db_data() + def _do_set_replica_gen_and_trans_id(self, other_replica_uid, + other_generation, other_transaction_id): + return self._set_replica_gen_and_trans_id( + other_replica_uid, + other_generation, + other_transaction_id) + + def _get_transaction_log(self): + return self._transaction_log.get_transaction_log() + #------------------------------------------------------------------------- # implemented methods from CommonBackend #------------------------------------------------------------------------- @@ -146,9 +160,10 @@ class ObjectStore(CommonBackend): # Documents never have conflicts on server. return False - def _put_and_update_indexes(self, doc_id, old_doc, new_rev, content): - raise NotImplementedError(self._put_and_update_indexes) - + def _put_and_update_indexes(self, old_doc, doc): + # TODO: implement index update + self._put_doc(doc) + self._update_gen_and_transaction_log(doc.doc_id) def _get_trans_id_for_gen(self, generation): self._get_u1db_data() @@ -187,8 +202,9 @@ class ObjectStore(CommonBackend): if self._replica_uid is None: self._replica_uid = uuid.uuid4().hex doc = self._factory(doc_id=self.U1DB_DATA_DOC_ID) - doc.content = { 'transaction_log' : [], - 'sync_log' : [], + doc.content = { 'sync_log' : [], + 'transaction_log' : [], + 'conflict_log' : [], 'replica_uid' : self._replica_uid } self._put_doc(doc) @@ -213,3 +229,36 @@ class ObjectStore(CommonBackend): replica_uid = property( _get_replica_uid, _set_replica_uid, doc="Replica UID of the database") + + + #------------------------------------------------------------------------- + # The methods below were cloned from u1db sqlite backend. They should at + # least exist and raise a NotImplementedError exception in CommonBackend + # (should we maybe fill a bug in u1db bts?). + #------------------------------------------------------------------------- + + def _add_conflict(self, doc_id, my_doc_rev, my_content): + self._conflict_log.append((doc_id, my_doc_rev, my_content)) + + def _delete_conflicts(self, doc, conflict_revs): + deleting = [(doc.doc_id, c_rev) for c_rev in conflict_revs] + self._conflict_log.delete_conflicts(deleting) + doc.has_conflicts = self._has_conflicts(doc.doc_id) + + def _prune_conflicts(self, doc, doc_vcr): + if self._has_conflicts(doc.doc_id): + autoresolved = False + c_revs_to_prune = [] + for c_doc in self._get_conflicts(doc.doc_id): + c_vcr = vectorclock.VectorClockRev(c_doc.rev) + if doc_vcr.is_newer(c_vcr): + c_revs_to_prune.append(c_doc.rev) + elif doc.same_content_as(c_doc): + c_revs_to_prune.append(c_doc.rev) + doc_vcr.maximize(c_vcr) + autoresolved = True + if autoresolved: + doc_vcr.increment(self._replica_uid) + doc.rev = doc_vcr.as_str() + c = self._db_handle.cursor() + self._delete_conflicts(c, doc, c_revs_to_prune) diff --git a/tests/test_couch.py b/tests/test_couch.py index 6a2c7dab..3f6c45f6 100644 --- a/tests/test_couch.py +++ b/tests/test_couch.py @@ -1,4 +1,8 @@ -"""Test ObjectStore backend bits.""" +"""Test ObjectStore backend bits. + +For these tests to run, a couch server has to be running on (default) port +5984. +""" import sys import copy @@ -6,8 +10,20 @@ import testtools import testscenarios from leap.soledad.backends import couch from leap.soledad.tests import u1db_tests as tests -from leap.soledad.tests.u1db_tests.test_backends import AllDatabaseTests +from leap.soledad.tests.u1db_tests.test_backends import ( + TestAlternativeDocument, + AllDatabaseTests, + LocalDatabaseTests, + LocalDatabaseValidateGenNTransIdTests, + LocalDatabaseValidateSourceGenTests, + LocalDatabaseWithConflictsTests, + DatabaseIndexTests, +) + +#----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_common_backends`. +#----------------------------------------------------------------------------- class TestCouchBackendImpl(tests.TestCase): @@ -19,6 +35,11 @@ class TestCouchBackendImpl(tests.TestCase): int(doc_id1[len('D-'):], 16) self.assertNotEqual(doc_id1, db._allocate_doc_id()) + +#----------------------------------------------------------------------------- +# The following tests come from `u1db.tests.test_backends`. +#----------------------------------------------------------------------------- + def make_couch_database_for_test(test, replica_uid, path='test'): return couch.CouchDatabase('http://localhost:5984', 'u1db_tests', replica_uid=replica_uid) @@ -34,28 +55,66 @@ def copy_couch_database_for_test(test, db): new_db._ensure_u1db_data() return new_db -def make_couch_app(test): - pass - -class CouchTests(AllDatabaseTests): - - scenarios = [ +COUCH_SCENARIOS = [ ('couch', {'make_database_for_test': make_couch_database_for_test, 'copy_database_for_test': copy_couch_database_for_test, 'make_document_for_test': tests.make_document_for_test,}), ] + +class CouchTests(AllDatabaseTests): + + scenarios = COUCH_SCENARIOS + def tearDown(self): self.db.delete_database() super(CouchTests, self).tearDown() - #make_database_for_test = make_couch_database_for_test - #copy_database_for_test = copy_couch_database_for_test +class CouchDatabaseTests(LocalDatabaseTests): + + scenarios = COUCH_SCENARIOS + + def tearDown(self): + self.db.delete_database() + super(CouchDatabaseTests, self).tearDown() + -# def runTest(self): -# pass +#class CouchValidateGenNTransIdTests(LocalDatabaseValidateGenNTransIdTests): +# +# scenarios = COUCH_SCENARIOS +# +# def tearDown(self): +# self.db.delete_database() +# super(CouchTests, self).tearDown() +# +# +#class CouchValidateSourceGenTests(LocalDatabaseValidateSourceGenTests): +# +# scenarios = COUCH_SCENARIOS +# +# def tearDown(self): +# self.db.delete_database() +# super(CouchTests, self).tearDown() +# +# +#class CouchWithConflictsTests(LocalDatabaseWithConflictsTests): +# +# scenarios = COUCH_SCENARIOS +# +# def tearDown(self): +# self.db.delete_database() +# super(CouchTests, self).tearDown() +# +# +#class CouchIndexTests(DatabaseIndexTests): +# +# scenarios = COUCH_SCENARIOS +# +# def tearDown(self): +# self.db.delete_database() +# super(CouchTests, self).tearDown() # load_tests = tests.load_with_scenarios diff --git a/tests/test_logs.py b/tests/test_logs.py index 072ac1a5..7fbb1cb7 100644 --- a/tests/test_logs.py +++ b/tests/test_logs.py @@ -1,5 +1,5 @@ import unittest2 as unittest -from leap.soledad.util import TransactionLog, SyncLog +from leap.soledad.util import TransactionLog, SyncLog, ConflictLog class LogTestCase(unittest.TestCase): @@ -49,25 +49,37 @@ class LogTestCase(unittest.TestCase): def test_whats_changed(self): data = [ - (2, "doc_3", "tran_3"), - (3, "doc_2", "tran_2"), - (1, "doc_1", "tran_1") - ] + (1, "doc_1", "tran_1"), + (2, "doc_2", "tran_2"), + (3, "doc_3", "tran_3") + ] log = TransactionLog() log.log = data self.assertEqual( log.whats_changed(3), - (3, "tran_2", []), + (3, "tran_3", []), 'error getting whats changed.') self.assertEqual( log.whats_changed(2), - (3, "tran_2", [("doc_2",3,"tran_2")]), + (3, "tran_3", [("doc_3",3,"tran_3")]), 'error getting whats changed.') self.assertEqual( log.whats_changed(1), - (3, "tran_2", [("doc_3",2,"tran_3"),("doc_2",3,"tran_2")]), + (3, "tran_3", [("doc_2",2,"tran_2"),("doc_3",3,"tran_3")]), 'error getting whats changed.') + def test_conflict_log(self): + data = [('1', 'my:1', 'irrelevant'), + ('2', 'my:1', 'irrelevant'), + ('3', 'my:1', 'irrelevant')] + log = ConflictLog() + log.log = data + log.delete_conflicts([('1','my:1'),('2','my:1')]) + self.assertEqual( + log.log, + [('3', 'my:1', 'irrelevant')], + 'error deleting conflicts.') + if __name__ == '__main__': unittest.main() diff --git a/util.py b/util.py index af38cd76..8683fbb9 100644 --- a/util.py +++ b/util.py @@ -149,7 +149,13 @@ class TransactionLog(SimpleLog): cur_gen, _, newest_trans_id = results[0] return cur_gen, newest_trans_id, changes - + + + def get_transaction_log(self): + """ + Return only a list of (doc_id, transaction_id) + """ + return map(lambda x: (x[1], x[2]), sorted(self._log)) class SyncLog(SimpleLog): @@ -182,3 +188,12 @@ class SyncLog(SimpleLog): self.append((other_replica_uid, other_generation, other_transaction_id)) +class ConflictLog(SimpleLog): + """ + A list of (doc_id, my_doc_rev, my_content) tuples. + """ + + def delete_conflicts(self, conflicts): + for conflict in conflicts: + self.log = self.filter(lambda x: + x[0] != conflict[0] or x[1] != conflict[1]) -- cgit v1.2.3