diff options
Diffstat (limited to 'libbe/storage/base.py')
-rw-r--r-- | libbe/storage/base.py | 224 |
1 files changed, 166 insertions, 58 deletions
diff --git a/libbe/storage/base.py b/libbe/storage/base.py index aa32ea9..202305b 100644 --- a/libbe/storage/base.py +++ b/libbe/storage/base.py @@ -53,11 +53,15 @@ class InvalidStorageVersion(ConnectionError): class InvalidID (KeyError): def __init__(self, id=None, revision=None, msg=None): - if msg == None and id != None: - msg = id - KeyError.__init__(self, msg) + KeyError.__init__(self, id) + self.msg = msg self.id = id self.revision = revision + def __str__(self): + if self.msg == None: + return '%s in revision %s' % (self.id, self.revision) + return self.msg + class InvalidRevision (KeyError): pass @@ -274,6 +278,26 @@ class Storage (object): for entry in reversed(list(self._data[id].traverse())): self._remove(entry.id) + def ancestors(self, *args, **kwargs): + """Return a list of the specified entry's ancestors' ids.""" + if self.is_readable() == False: + raise NotReadable('Cannot list parents with unreadable storage.') + return self._ancestors(*args, **kwargs) + + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def children(self, *args, **kwargs): """Return a list of specified entry's children's ids.""" if self.is_readable() == False: @@ -389,17 +413,39 @@ class VersionedStorage (Storage): for entry in reversed(list(self._data[-1][id].traverse())): self._remove(entry.id) + def _ancestors(self, id=None, revision=None): + if id == None: + return [] + if revision == None: + revision = -1 + else: + revision = int(revision) + ancestors = [] + stack = [id] + while len(stack) > 0: + id = stack.pop(0) + parent = self._data[revision][id].parent + if parent != None and not parent.id.startswith('__'): + ancestor = parent.id + ancestors.append(ancestor) + stack.append(ancestor) + return ancestors + def _children(self, id=None, revision=None): if id == None: id = '__ROOT__' if revision == None: revision = -1 + else: + revision = int(revision) return [c.id for c in self._data[revision][id] if not c.id.startswith('__')] def _get(self, id, default=InvalidObject, revision=None): if revision == None: revision = -1 + else: + revision = int(revision) if id in self._data[revision]: return self._data[revision][id].value elif default == InvalidObject: @@ -428,7 +474,7 @@ class VersionedStorage (Storage): raise EmptyCommit self._data[-1]["__COMMIT__SUMMARY__"].value = summary self._data[-1]["__COMMIT__BODY__"].value = body - rev = len(self._data)-1 + rev = str(len(self._data)-1) self._data.append(copy.deepcopy(self._data[-1])) return rev @@ -452,12 +498,34 @@ class VersionedStorage (Storage): raise InvalidRevision(index) L = len(self._data) - 1 # -1 b/c of initial commit if index >= -L and index <= L: - return index % L + return str(index % L) raise InvalidRevision(i) + def changed(self, revision): + """ + Return a tuple of lists of ids + (new, modified, removed) + from the specified revision to the current situation. + """ + new = [] + modified = [] + removed = [] + for id,value in self._data[int(revision)].items(): + if id.startswith('__'): + continue + if not id in self._data[-1]: + removed.append(id) + elif value.value != self._data[-1][id].value: + modified.append(id) + for id in self._data[-1]: + if not id in self._data[int(revision)]: + new.append(id) + return (new, modified, removed) + + if TESTING == True: class StorageTestCase (unittest.TestCase): - """Test cases for base Storage class.""" + """Test cases for Storage class.""" Class = Storage @@ -465,6 +533,23 @@ if TESTING == True: super(StorageTestCase, self).__init__(*args, **kwargs) self.dirname = None + # this class will be the basis of tests for several classes, + # so make sure we print the name of the class we're dealing with. + def fail(self, msg=None): + """Fail immediately, with the given message.""" + raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failIf(self, expr, msg=None): + "Fail the test if the expression is true." + if expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + + def failUnless(self, expr, msg=None): + """Fail the test unless the expression is true.""" + if not expr: raise self.failureException, \ + '(%s) %s' % (self.Class.__name__, msg) + def setUp(self): """Set up test fixtures for Storage test case.""" super(StorageTestCase, self).setUp() @@ -514,8 +599,7 @@ if TESTING == True: self.failUnless(len(self.s.children()) == 0, self.s.children()) def test_add_identical_rooted(self): - """ - Adding entries with the same ID should not increase the number of children. + """Adding entries with the same ID should not increase the number of children. """ for i in range(10): self.s.add('some id', directory=False) @@ -523,8 +607,7 @@ if TESTING == True: self.failUnless(s == ['some id'], s) def test_add_rooted(self): - """ - Adding entries should increase the number of children (rooted). + """Adding entries should increase the number of children (rooted). """ ids = [] for i in range(10): @@ -534,8 +617,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_add_nonrooted(self): - """ - Adding entries should increase the number of children (nonrooted). + """Adding entries should increase the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -547,9 +629,23 @@ if TESTING == True: s = self.s.children() self.failUnless(s == ['parent'], s) - def test_children(self): + def test_ancestors(self): + """Check ancestors lists. """ - Non-UUID ids should be returned as such. + self.s.add('parent', directory=True) + for i in range(10): + i_id = str(i) + self.s.add(i_id, 'parent', directory=True) + for j in range(10): # add some grandkids + j_id = str(20*(i+1)+j) + self.s.add(j_id, i_id, directory=(i%2 == 0)) + ancestors = sorted(self.s.ancestors(j_id)) + self.failUnless(ancestors == [i_id, 'parent'], + 'Unexpected ancestors for %s/%s, "%s"' + % (i_id, j_id, ancestors)) + + def test_children(self): + """Non-UUID ids should be returned as such. """ self.s.add('parent', directory=True) ids = [] @@ -560,8 +656,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_add_invalid_directory(self): - """ - Should not be able to add children to non-directories. + """Should not be able to add children to non-directories. """ self.s.add('parent', directory=False) try: @@ -582,8 +677,7 @@ if TESTING == True: self.s.children('parent')) def test_remove_rooted(self): - """ - Removing entries should decrease the number of children (rooted). + """Removing entries should decrease the number of children (rooted). """ ids = [] for i in range(10): @@ -595,8 +689,7 @@ if TESTING == True: self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids)) def test_remove_nonrooted(self): - """ - Removing entries should decrease the number of children (nonrooted). + """Removing entries should decrease the number of children (nonrooted). """ self.s.add('parent', directory=True) ids = [] @@ -612,8 +705,7 @@ if TESTING == True: self.failUnless(s == ['parent'], s) def test_remove_directory_not_empty(self): - """ - Removing a non-empty directory entry should raise exception. + """Removing a non-empty directory entry should raise exception. """ self.s.add('parent', directory=True) ids = [] @@ -630,9 +722,7 @@ if TESTING == True: pass def test_recursive_remove(self): - """ - Recursive remove should empty the tree. - """ + """Recursive remove should empty the tree.""" self.s.add('parent', directory=True) ids = [] for i in range(10): @@ -651,8 +741,7 @@ if TESTING == True: val = 'unlikely value' def test_get_default(self): - """ - Get should return specified default if id not in Storage. + """Get should return specified default if id not in Storage. """ ret = self.s.get(self.id, default=self.val) self.failUnless(ret == self.val, @@ -660,8 +749,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_get_default_exception(self): - """ - Get should raise exception if id not in Storage and no default. + """Get should raise exception if id not in Storage and no default. """ try: ret = self.s.get(self.id) @@ -672,8 +760,7 @@ if TESTING == True: pass def test_get_initial_value(self): - """ - Data value should be None before any value has been set. + """Data value should be None before any value has been set. """ self.s.add(self.id, directory=False) ret = self.s.get(self.id) @@ -682,8 +769,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret)) def test_set_exception(self): - """ - Set should raise exception if id not in Storage. + """Set should raise exception if id not in Storage. """ try: self.s.set(self.id, self.val) @@ -694,8 +780,7 @@ if TESTING == True: pass def test_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -705,8 +790,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_unicode_set(self): - """ - Set should define the value returned by get. + """Set should define the value returned by get. """ val = u'Fran\xe7ois' self.s.add(self.id, directory=False) @@ -735,8 +819,7 @@ if TESTING == True: val = 'unlikely value' def test_get_set_persistence(self): - """ - Set should define the value returned by get after reconnect. + """Set should define the value returned by get after reconnect. """ self.s.add(self.id, directory=False) self.s.set(self.id, self.val) @@ -748,8 +831,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, self.val)) def test_add_nonrooted_persistence(self): - """ - Adding entries should increase the number of children after reconnect. + """Adding entries should increase the number of children after reconnect. """ self.s.add('parent', directory=True) ids = [] @@ -764,12 +846,12 @@ if TESTING == True: self.failUnless(s == ['parent'], s) class VersionedStorageTestCase (StorageTestCase): - """Test cases for base VersionedStorage class.""" + """Test cases for VersionedStorage methods.""" Class = VersionedStorage class VersionedStorage_commit_TestCase (VersionedStorageTestCase): - """Test cases for VersionedStorage methods.""" + """Test cases for VersionedStorage.commit and revision_ids methods.""" id = 'unlikely id' val = 'Some value' @@ -788,8 +870,7 @@ if TESTING == True: pass def test_revision_id_exception(self): - """ - Invalid revision id should raise InvalidRevision. + """Invalid revision id should raise InvalidRevision. """ try: rev = self.s.revision_id('highly unlikely revision id') @@ -800,8 +881,7 @@ if TESTING == True: pass def test_empty_commit_raises_exception(self): - """ - Empty commit should raise exception. + """Empty commit should raise exception. """ self._setup_for_empty_commit() try: @@ -813,16 +893,14 @@ if TESTING == True: pass def test_empty_commit_allowed(self): - """ - Empty commit should _not_ raise exception if allow_empty=True. + """Empty commit should _not_ raise exception if allow_empty=True. """ self._setup_for_empty_commit() self.s.commit(self.commit_msg, self.commit_body, allow_empty=True) def test_commit_revision_ids(self): - """ - Commit / revision_id should agree on revision ids. + """Commit / revision_id should agree on revision ids. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -844,8 +922,7 @@ if TESTING == True: % (vars(self.Class)['name'], i, rev, revs[i])) def test_get_previous_version(self): - """ - Get should be able to return the previous version. + """Get should be able to return the previous version. """ def val(i): return '%s:%d' % (self.val, i+1) @@ -862,8 +939,7 @@ if TESTING == True: % (vars(self.Class)['name'], ret, val(i), revs[i])) def test_get_previous_children(self): - """ - Children list should be revision dependent. + """Children list should be revision dependent. """ self.s.add('parent', directory=True) revs = [] @@ -871,7 +947,7 @@ if TESTING == True: children = [] for i in range(10): new_child = str(i) - self.s.add(new_child, 'parent', directory=(i % 2 == 0)) + self.s.add(new_child, 'parent') self.s.set(new_child, self.val) revs.append(self.s.commit('%s: %d' % (self.commit_msg, i), self.commit_body)) @@ -884,6 +960,35 @@ if TESTING == True: % (vars(self.Class)['name'], ret, children[i], revs[i])) + class VersionedStorage_changed_TestCase (VersionedStorageTestCase): + """Test cases for VersionedStorage.changed() method.""" + + def test_changed(self): + """Changed lists should reflect past activity""" + self.s.add('dir', directory=True) + self.s.add('modified', parent='dir') + self.s.set('modified', 'some value to be modified') + self.s.add('moved', parent='dir') + self.s.set('moved', 'this entry will be moved') + self.s.add('removed', parent='dir') + self.s.set('removed', 'this entry will be deleted') + revA = self.s.commit('Initial state') + self.s.add('new', parent='dir') + self.s.set('new', 'this entry is new') + self.s.set('modified', 'a new value') + self.s.remove('moved') + self.s.add('moved2', parent='dir') + self.s.set('moved2', 'this entry will be moved') + self.s.remove('removed') + revB = self.s.commit('Final state') + new,mod,rem = self.s.changed(revA) + self.failUnless(sorted(new) == ['moved2', 'new'], + 'Unexpected new: %s' % new) + self.failUnless(mod == ['modified'], + 'Unexpected modified: %s' % mod) + self.failUnless(sorted(rem) == ['moved', 'removed'], + 'Unexpected removed: %s' % rem) + def make_storage_testcase_subclasses(storage_class, namespace): """Make StorageTestCase subclasses for storage_class in namespace.""" storage_testcase_classes = [ @@ -906,8 +1011,11 @@ if TESTING == True: storage_testcase_classes = [ c for c in ( ob for ob in globals().values() if isinstance(ob, type)) - if issubclass(c, StorageTestCase) \ - and c.Class == Storage] + if ((issubclass(c, StorageTestCase) \ + and c.Class == Storage) + or + (issubclass(c, VersionedStorageTestCase) \ + and c.Class == VersionedStorage))] for base_class in storage_testcase_classes: testcase_class_name = storage_class.__name__ + base_class.__name__ |