1 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
3 # This program is free software; you can redistribute it and/or modify
4 # it under the terms of the GNU General Public License as published by
5 # the Free Software Foundation; either version 2 of the License, or
6 # (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License along
14 # with this program; if not, write to the Free Software Foundation, Inc.,
15 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 Abstract bug repository data storage to easily support multiple backends.
26 from libbe.error import NotSupported
28 from libbe.util.tree import Tree
29 from libbe.util import InvalidObject
31 from libbe import TESTING
39 from libbe.util.utility import Dir
41 class ConnectionError (Exception):
44 class InvalidStorageVersion(ConnectionError):
45 def __init__(self, active_version, expected_version=None):
46 if expected_version == None:
47 expected_version = libbe.storage.STORAGE_VERSION
48 msg = 'Storage in "%s" not the expected "%s"' \
49 % (active_version, expected_version)
50 Exception.__init__(self, msg)
51 self.active_version = active_version
52 self.expected_version = expected_version
54 class InvalidID (KeyError):
55 def __init__(self, id=None, revision=None, msg=None):
56 KeyError.__init__(self, id)
59 self.revision = revision
62 return '%s in revision %s' % (self.id, self.revision)
66 class InvalidRevision (KeyError):
69 class InvalidDirectory (Exception):
72 class DirectoryNotEmpty (InvalidDirectory):
75 class NotWriteable (NotSupported):
76 def __init__(self, msg):
77 NotSupported.__init__(self, 'write', msg)
79 class NotReadable (NotSupported):
80 def __init__(self, msg):
81 NotSupported.__init__(self, 'read', msg)
83 class EmptyCommit(Exception):
85 Exception.__init__(self, 'No changes to commit')
87 class _EMPTY (object):
88 """Entry has been added but has no user-set value."""
92 def __init__(self, id, value=_EMPTY, parent=None, directory=False,
97 Tree.__init__(self, children)
101 if self.parent != None:
102 if self.parent.directory == False:
103 raise InvalidDirectory(
104 'Non-directory %s cannot have children' % self.parent)
106 self.directory = directory
109 return '<Entry %s: %s>' % (self.id, self.value)
114 def __cmp__(self, other, local=False):
117 if cmp(self.id, other.id) != 0:
118 return cmp(self.id, other.id)
119 if cmp(self.value, other.value) != 0:
120 return cmp(self.value, other.value)
122 if self.parent == None:
123 if cmp(self.parent, other.parent) != 0:
124 return cmp(self.parent, other.parent)
125 elif self.parent.__cmp__(other.parent, local=True) != 0:
126 return self.parent.__cmp__(other.parent, local=True)
127 for sc,oc in zip(self, other):
128 if sc.__cmp__(oc, local=True) != 0:
129 return sc.__cmp__(oc, local=True)
132 def _objects_to_ids(self):
133 if self.parent != None:
134 self.parent = self.parent.id
135 for i,c in enumerate(self):
139 def _ids_to_objects(self, dict):
140 if self.parent != None:
141 self.parent = dict[self.parent]
142 for i,c in enumerate(self):
146 class Storage (object):
148 This class declares all the methods required by a Storage
149 interface. This implementation just keeps the data in a
150 dictionary and uses pickle for persistent storage.
154 def __init__(self, repo='/', encoding='utf-8', options=None):
156 self.encoding = encoding
157 self.options = options
158 self.readable = True # soft limit (user choice)
159 self._readable = True # hard limit (backend choice)
160 self.writeable = True # soft limit (user choice)
161 self._writeable = True # hard limit (backend choice)
162 self.versioned = False
164 self.connected = False
167 return '<%s %s %s>' % (self.__class__.__name__, id(self), self.repo)
173 """Return a version string for this backend."""
174 return libbe.version.version()
176 def storage_version(self, revision=None):
177 """Return the storage format for this backend."""
178 return libbe.storage.STORAGE_VERSION
180 def is_readable(self):
181 return self.readable and self._readable
183 def is_writeable(self):
184 return self.writeable and self._writeable
187 """Create a new storage repository."""
188 if self.can_init == False:
189 raise NotSupported('init',
190 'Cannot initialize this repository format.')
191 if self.is_writeable() == False:
192 raise NotWriteable('Cannot initialize unwriteable storage.')
196 f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
197 root = Entry(id='__ROOT__', directory=True)
199 pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1)
203 """Remove the storage repository."""
204 if self.is_writeable() == False:
205 raise NotWriteable('Cannot destroy unwriteable storage.')
206 return self._destroy()
209 os.remove(os.path.join(self.repo, 'repo.pkl'))
212 """Open a connection to the repository."""
213 if self.is_readable() == False:
214 raise NotReadable('Cannot connect to unreadable storage.')
216 self.connected = True
220 f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
222 raise ConnectionError(self)
224 self._data = dict((k,v._ids_to_objects(d)) for k,v in d.items())
227 def disconnect(self):
228 """Close the connection to the repository."""
229 if self.is_writeable() == False:
231 if self.connected == False:
234 self.connected = False
236 def _disconnect(self):
237 f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
238 pickle.dump(dict((k,v._objects_to_ids())
239 for k,v in self._data.items()), f, -1)
243 def add(self, id, *args, **kwargs):
245 if self.is_writeable() == False:
246 raise NotWriteable('Cannot add entry to unwriteable storage.')
247 if not self.exists(id):
248 self._add(id, *args, **kwargs)
250 def _add(self, id, parent=None, directory=False):
253 p = self._data[parent]
254 self._data[id] = Entry(id, parent=p, directory=directory)
256 def exists(self, *args, **kwargs):
257 """Check an entry's existence"""
258 if self.is_readable() == False:
259 raise NotReadable('Cannot check entry existence in unreadable storage.')
260 return self._exists(*args, **kwargs)
262 def _exists(self, id, revision=None):
263 return id in self._data
265 def remove(self, *args, **kwargs):
266 """Remove an entry."""
267 if self.is_writeable() == False:
268 raise NotSupported('write',
269 'Cannot remove entry from unwriteable storage.')
270 self._remove(*args, **kwargs)
272 def _remove(self, id):
273 if self._data[id].directory == True \
274 and len(self.children(id)) > 0:
275 raise DirectoryNotEmpty(id)
276 e = self._data.pop(id)
279 def recursive_remove(self, *args, **kwargs):
280 """Remove an entry and all its decendents."""
281 if self.is_writeable() == False:
282 raise NotSupported('write',
283 'Cannot remove entries from unwriteable storage.')
284 self._recursive_remove(*args, **kwargs)
286 def _recursive_remove(self, id):
287 for entry in reversed(list(self._data[id].traverse())):
288 self._remove(entry.id)
290 def ancestors(self, *args, **kwargs):
291 """Return a list of the specified entry's ancestors' ids."""
292 if self.is_readable() == False:
293 raise NotReadable('Cannot list parents with unreadable storage.')
294 return self._ancestors(*args, **kwargs)
296 def _ancestors(self, id=None, revision=None):
301 while len(stack) > 0:
303 parent = self._data[id].parent
304 if parent != None and not parent.id.startswith('__'):
306 ancestors.append(ancestor)
307 stack.append(ancestor)
310 def children(self, *args, **kwargs):
311 """Return a list of specified entry's children's ids."""
312 if self.is_readable() == False:
313 raise NotReadable('Cannot list children with unreadable storage.')
314 return self._children(*args, **kwargs)
316 def _children(self, id=None, revision=None):
319 return [c.id for c in self._data[id] if not c.id.startswith('__')]
321 def get(self, *args, **kwargs):
323 Get contents of and entry as they were in a given revision.
324 revision==None specifies the current revision.
326 If there is no id, return default, unless default is not
327 given, in which case raise InvalidID.
329 if self.is_readable() == False:
330 raise NotReadable('Cannot get entry with unreadable storage.')
331 if 'decode' in kwargs:
332 decode = kwargs.pop('decode')
335 value = self._get(*args, **kwargs)
337 if decode == True and type(value) != types.UnicodeType:
338 return unicode(value, self.encoding)
339 elif decode == False and type(value) != types.StringType:
340 return value.encode(self.encoding)
343 def _get(self, id, default=InvalidObject, revision=None):
344 if id in self._data and self._data[id].value != _EMPTY:
345 return self._data[id].value
346 elif default == InvalidObject:
350 def set(self, id, value, *args, **kwargs):
352 Set the entry contents.
354 if self.is_writeable() == False:
355 raise NotWriteable('Cannot set entry in unwriteable storage.')
356 if type(value) == types.UnicodeType:
357 value = value.encode(self.encoding)
358 self._set(id, value, *args, **kwargs)
360 def _set(self, id, value):
361 if id not in self._data:
363 if self._data[id].directory == True:
364 raise InvalidDirectory(
365 'Directory %s cannot have data' % self.parent)
366 self._data[id].value = value
368 class VersionedStorage (Storage):
370 This class declares all the methods required by a Storage
371 interface that supports versioning. This implementation just
372 keeps the data in a list and uses pickle for persistent
375 name = 'VersionedStorage'
377 def __init__(self, *args, **kwargs):
378 Storage.__init__(self, *args, **kwargs)
379 self.versioned = True
382 f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
383 root = Entry(id='__ROOT__', directory=True)
384 summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit')
385 body = Entry(id='__COMMIT__BODY__')
386 initial_commit = {root.id:root, summary.id:summary, body.id:body}
387 d = dict((k,v._objects_to_ids()) for k,v in initial_commit.items())
388 pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree]
393 f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
395 raise ConnectionError(self)
397 self._data = [dict((k,v._ids_to_objects(t)) for k,v in t.items())
401 def _disconnect(self):
402 f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
403 pickle.dump([dict((k,v._objects_to_ids())
404 for k,v in t.items()) for t in self._data], f, -1)
408 def _add(self, id, parent=None, directory=False):
411 p = self._data[-1][parent]
412 self._data[-1][id] = Entry(id, parent=p, directory=directory)
414 def _exists(self, id, revision=None):
418 revision = int(revision)
419 return id in self._data[revision]
421 def _remove(self, id):
422 if self._data[-1][id].directory == True \
423 and len(self.children(id)) > 0:
424 raise DirectoryNotEmpty(id)
425 e = self._data[-1].pop(id)
428 def _recursive_remove(self, id):
429 for entry in reversed(list(self._data[-1][id].traverse())):
430 self._remove(entry.id)
432 def _ancestors(self, id=None, revision=None):
438 revision = int(revision)
441 while len(stack) > 0:
443 parent = self._data[revision][id].parent
444 if parent != None and not parent.id.startswith('__'):
446 ancestors.append(ancestor)
447 stack.append(ancestor)
450 def _children(self, id=None, revision=None):
456 revision = int(revision)
457 return [c.id for c in self._data[revision][id]
458 if not c.id.startswith('__')]
460 def _get(self, id, default=InvalidObject, revision=None):
464 revision = int(revision)
465 if id in self._data[revision] \
466 and self._data[revision][id].value != _EMPTY:
467 return self._data[revision][id].value
468 elif default == InvalidObject:
472 def _set(self, id, value):
473 if id not in self._data[-1]:
475 self._data[-1][id].value = value
477 def commit(self, *args, **kwargs):
479 Commit the current repository, with a commit message string
480 summary and body. Return the name of the new revision.
482 If allow_empty == False (the default), raise EmptyCommit if
483 there are no changes to commit.
485 if self.is_writeable() == False:
486 raise NotWriteable('Cannot commit to unwriteable storage.')
487 return self._commit(*args, **kwargs)
489 def _commit(self, summary, body=None, allow_empty=False):
490 if self._data[-1] == self._data[-2] and allow_empty == False:
492 self._data[-1]["__COMMIT__SUMMARY__"].value = summary
493 self._data[-1]["__COMMIT__BODY__"].value = body
494 rev = str(len(self._data)-1)
495 self._data.append(copy.deepcopy(self._data[-1]))
498 def revision_id(self, index=None):
500 Return the name of the <index>th revision. The choice of
501 which branch to follow when crossing branches/merges is not
502 defined. Revision indices start at 1; ID 0 is the blank
505 Return None if index==None.
507 If the specified revision does not exist, raise InvalidRevision.
512 if int(index) != index:
513 raise InvalidRevision(index)
515 raise InvalidRevision(index)
516 L = len(self._data) - 1 # -1 b/c of initial commit
517 if index >= -L and index <= L:
518 return str(index % L)
519 raise InvalidRevision(i)
521 def changed(self, revision):
522 """Return a tuple of lists of ids `(new, modified, removed)` from the
523 specified revision to the current situation.
528 for id,value in self._data[int(revision)].items():
529 if id.startswith('__'):
531 if not id in self._data[-1]:
533 elif value.value != self._data[-1][id].value:
535 for id in self._data[-1]:
536 if not id in self._data[int(revision)]:
538 return (new, modified, removed)
542 class StorageTestCase (unittest.TestCase):
543 """Test cases for Storage class."""
547 def __init__(self, *args, **kwargs):
548 super(StorageTestCase, self).__init__(*args, **kwargs)
551 # this class will be the basis of tests for several classes,
552 # so make sure we print the name of the class we're dealing with.
553 def _classname(self):
556 if hasattr(self, 's'):
557 version = self.s.version()
560 return '%s:%s' % (self.Class.__name__, version)
562 def fail(self, msg=None):
563 """Fail immediately, with the given message."""
564 raise self.failureException, \
565 '(%s) %s' % (self._classname(), msg)
567 def failIf(self, expr, msg=None):
568 "Fail the test if the expression is true."
569 if expr: raise self.failureException, \
570 '(%s) %s' % (self._classname(), msg)
572 def failUnless(self, expr, msg=None):
573 """Fail the test unless the expression is true."""
574 if not expr: raise self.failureException, \
575 '(%s) %s' % (self._classname(), msg)
578 """Set up test fixtures for Storage test case."""
579 super(StorageTestCase, self).setUp()
581 self.dirname = self.dir.path
582 self.s = self.Class(repo=self.dirname)
583 self.assert_failed_connect()
588 super(StorageTestCase, self).tearDown()
591 self.assert_failed_connect()
594 def assert_failed_connect(self):
598 "Connected to %(name)s repository before initialising"
600 except ConnectionError:
603 class Storage_init_TestCase (StorageTestCase):
604 """Test cases for Storage.init method."""
606 def test_connect_should_succeed_after_init(self):
607 """Should connect after initialization."""
610 class Storage_connect_disconnect_TestCase (StorageTestCase):
611 """Test cases for Storage.connect and .disconnect methods."""
613 def test_multiple_disconnects(self):
614 """Should be able to call .disconnect multiple times."""
618 class Storage_add_remove_TestCase (StorageTestCase):
619 """Test cases for Storage.add, .remove, and .recursive_remove methods."""
621 def test_initially_empty(self):
622 """New repository should be empty."""
623 self.failUnless(len(self.s.children()) == 0, self.s.children())
625 def test_add_identical_rooted(self):
626 """Adding entries with the same ID should not increase the number of children.
629 self.s.add('some id', directory=False)
630 s = sorted(self.s.children())
631 self.failUnless(s == ['some id'], s)
633 def test_add_rooted(self):
634 """Adding entries should increase the number of children (rooted).
639 self.s.add(ids[-1], directory=(i % 2 == 0))
640 s = sorted(self.s.children())
641 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
643 def test_add_nonrooted(self):
644 """Adding entries should increase the number of children (nonrooted).
646 self.s.add('parent', directory=True)
650 self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
651 s = sorted(self.s.children('parent'))
652 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
653 s = self.s.children()
654 self.failUnless(s == ['parent'], s)
656 def test_ancestors(self):
657 """Check ancestors lists.
659 self.s.add('parent', directory=True)
662 self.s.add(i_id, 'parent', directory=True)
663 for j in range(10): # add some grandkids
664 j_id = str(20*(i+1)+j)
665 self.s.add(j_id, i_id, directory=(i%2 == 0))
666 ancestors = sorted(self.s.ancestors(j_id))
667 self.failUnless(ancestors == [i_id, 'parent'],
668 'Unexpected ancestors for %s/%s, "%s"'
669 % (i_id, j_id, ancestors))
671 def test_children(self):
672 """Non-UUID ids should be returned as such.
674 self.s.add('parent', directory=True)
677 ids.append('parent/%s' % str(i))
678 self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
679 s = sorted(self.s.children('parent'))
680 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
682 def test_add_invalid_directory(self):
683 """Should not be able to add children to non-directories.
685 self.s.add('parent', directory=False)
687 self.s.add('child', 'parent', directory=False)
689 '%s.add() succeeded instead of raising InvalidDirectory'
690 % (vars(self.Class)['name']))
691 except InvalidDirectory:
694 self.s.add('child', 'parent', directory=True)
696 '%s.add() succeeded instead of raising InvalidDirectory'
697 % (vars(self.Class)['name']))
698 except InvalidDirectory:
700 self.failUnless(len(self.s.children('parent')) == 0,
701 self.s.children('parent'))
703 def test_remove_rooted(self):
704 """Removing entries should decrease the number of children (rooted).
709 self.s.add(ids[-1], directory=(i % 2 == 0))
711 self.s.remove(ids.pop())
712 s = sorted(self.s.children())
713 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
715 def test_remove_nonrooted(self):
716 """Removing entries should decrease the number of children (nonrooted).
718 self.s.add('parent', directory=True)
722 self.s.add(ids[-1], 'parent', directory=False)#(i % 2 == 0))
724 self.s.remove(ids.pop())
725 s = sorted(self.s.children('parent'))
726 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
728 s = self.s.children()
729 self.failUnless(s == ['parent'], s)
731 def test_remove_directory_not_empty(self):
732 """Removing a non-empty directory entry should raise exception.
734 self.s.add('parent', directory=True)
738 self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
739 self.s.remove(ids.pop()) # empty directory removal succeeds
741 self.s.remove('parent') # empty directory removal succeeds
743 "%s.remove() didn't raise DirectoryNotEmpty"
744 % (vars(self.Class)['name']))
745 except DirectoryNotEmpty:
748 def test_recursive_remove(self):
749 """Recursive remove should empty the tree."""
750 self.s.add('parent', directory=True)
754 self.s.add(ids[-1], 'parent', directory=True)
755 for j in range(10): # add some grandkids
756 self.s.add(str(20*(i+1)+j), ids[-1], directory=(i%2 == 0))
757 self.s.recursive_remove('parent')
758 s = sorted(self.s.children())
759 self.failUnless(s == [], s)
761 class Storage_get_set_TestCase (StorageTestCase):
762 """Test cases for Storage.get and .set methods."""
765 val = 'unlikely value'
767 def test_get_default(self):
768 """Get should return specified default if id not in Storage.
770 ret = self.s.get(self.id, default=self.val)
771 self.failUnless(ret == self.val,
772 "%s.get() returned %s not %s"
773 % (vars(self.Class)['name'], ret, self.val))
775 def test_get_default_exception(self):
776 """Get should raise exception if id not in Storage and no default.
779 ret = self.s.get(self.id)
781 "%s.get() returned %s instead of raising InvalidID"
782 % (vars(self.Class)['name'], ret))
786 def test_get_initial_value(self):
787 """Data value should be default before any value has been set.
789 self.s.add(self.id, directory=False)
790 val = 'UNLIKELY DEFAULT'
791 ret = self.s.get(self.id, default=val)
792 self.failUnless(ret == val,
793 "%s.get() returned %s not %s"
794 % (vars(self.Class)['name'], ret, val))
796 def test_set_exception(self):
797 """Set should raise exception if id not in Storage.
800 self.s.set(self.id, self.val)
802 "%(name)s.set() did not raise InvalidID"
808 """Set should define the value returned by get.
810 self.s.add(self.id, directory=False)
811 self.s.set(self.id, self.val)
812 ret = self.s.get(self.id)
813 self.failUnless(ret == self.val,
814 "%s.get() returned %s not %s"
815 % (vars(self.Class)['name'], ret, self.val))
817 def test_unicode_set(self):
818 """Set should define the value returned by get.
821 self.s.add(self.id, directory=False)
822 self.s.set(self.id, val)
823 ret = self.s.get(self.id, decode=True)
824 self.failUnless(type(ret) == types.UnicodeType,
825 "%s.get() returned %s not UnicodeType"
826 % (vars(self.Class)['name'], type(ret)))
827 self.failUnless(ret == val,
828 "%s.get() returned %s not %s"
829 % (vars(self.Class)['name'], ret, self.val))
830 ret = self.s.get(self.id)
831 self.failUnless(type(ret) == types.StringType,
832 "%s.get() returned %s not StringType"
833 % (vars(self.Class)['name'], type(ret)))
834 s = unicode(ret, self.s.encoding)
835 self.failUnless(s == val,
836 "%s.get() returned %s not %s"
837 % (vars(self.Class)['name'], s, self.val))
840 class Storage_persistence_TestCase (StorageTestCase):
841 """Test cases for Storage.disconnect and .connect methods."""
844 val = 'unlikely value'
846 def test_get_set_persistence(self):
847 """Set should define the value returned by get after reconnect.
849 self.s.add(self.id, directory=False)
850 self.s.set(self.id, self.val)
853 ret = self.s.get(self.id)
854 self.failUnless(ret == self.val,
855 "%s.get() returned %s not %s"
856 % (vars(self.Class)['name'], ret, self.val))
858 def test_empty_get_set_persistence(self):
859 """After empty set, get may return either an empty string or default.
861 self.s.add(self.id, directory=False)
862 self.s.set(self.id, '')
865 default = 'UNLIKELY DEFAULT'
866 ret = self.s.get(self.id, default=default)
867 self.failUnless(ret in ['', default],
868 "%s.get() returned %s not in %s"
869 % (vars(self.Class)['name'], ret, ['', default]))
871 def test_add_nonrooted_persistence(self):
872 """Adding entries should increase the number of children after reconnect.
874 self.s.add('parent', directory=True)
878 self.s.add(ids[-1], 'parent', directory=(i % 2 == 0))
881 s = sorted(self.s.children('parent'))
882 self.failUnless(s == ids, '\n %s\n !=\n %s' % (s, ids))
883 s = self.s.children()
884 self.failUnless(s == ['parent'], s)
886 class VersionedStorageTestCase (StorageTestCase):
887 """Test cases for VersionedStorage methods."""
889 Class = VersionedStorage
891 class VersionedStorage_commit_TestCase (VersionedStorageTestCase):
892 """Test cases for VersionedStorage.commit and revision_ids methods."""
896 commit_msg = 'Committing something interesting'
897 commit_body = 'Some\nlonger\ndescription\n'
899 def _setup_for_empty_commit(self):
901 Initialization might add some files to version control, so
902 commit those first, before testing the empty commit
906 self.s.commit('Added initialization files')
910 def test_revision_id_exception(self):
911 """Invalid revision id should raise InvalidRevision.
914 rev = self.s.revision_id('highly unlikely revision id')
916 "%s.revision_id() didn't raise InvalidRevision, returned %s."
917 % (vars(self.Class)['name'], rev))
918 except InvalidRevision:
921 def test_empty_commit_raises_exception(self):
922 """Empty commit should raise exception.
924 self._setup_for_empty_commit()
926 self.s.commit(self.commit_msg, self.commit_body)
928 "Empty %(name)s.commit() didn't raise EmptyCommit."
933 def test_empty_commit_allowed(self):
934 """Empty commit should _not_ raise exception if allow_empty=True.
936 self._setup_for_empty_commit()
937 self.s.commit(self.commit_msg, self.commit_body,
940 def test_commit_revision_ids(self):
941 """Commit / revision_id should agree on revision ids.
944 return '%s:%d' % (self.val, i+1)
945 self.s.add(self.id, directory=False)
948 self.s.set(self.id, val(i))
949 revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
952 rev = self.s.revision_id(i+1)
953 self.failUnless(rev == revs[i],
954 "%s.revision_id(%d) returned %s not %s"
955 % (vars(self.Class)['name'], i+1, rev, revs[i]))
956 for i in range(-1, -9, -1):
957 rev = self.s.revision_id(i)
958 self.failUnless(rev == revs[i],
959 "%s.revision_id(%d) returned %s not %s"
960 % (vars(self.Class)['name'], i, rev, revs[i]))
962 def test_get_previous_version(self):
963 """Get should be able to return the previous version.
966 return '%s:%d' % (self.val, i+1)
967 self.s.add(self.id, directory=False)
970 self.s.set(self.id, val(i))
971 revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
974 ret = self.s.get(self.id, revision=revs[i])
975 self.failUnless(ret == val(i),
976 "%s.get() returned %s not %s for revision %s"
977 % (vars(self.Class)['name'], ret, val(i), revs[i]))
979 def test_get_previous_children(self):
980 """Children list should be revision dependent.
982 self.s.add('parent', directory=True)
988 self.s.add(new_child, 'parent')
989 self.s.set(new_child, self.val)
990 revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
992 cur_children.append(new_child)
993 children.append(list(cur_children))
995 ret = sorted(self.s.children('parent', revision=revs[i]))
996 self.failUnless(ret == children[i],
997 "%s.get() returned %s not %s for revision %s"
998 % (vars(self.Class)['name'], ret,
999 children[i], revs[i]))
1001 class VersionedStorage_changed_TestCase (VersionedStorageTestCase):
1002 """Test cases for VersionedStorage.changed() method."""
1004 def test_changed(self):
1005 """Changed lists should reflect past activity"""
1006 self.s.add('dir', directory=True)
1007 self.s.add('modified', parent='dir')
1008 self.s.set('modified', 'some value to be modified')
1009 self.s.add('moved', parent='dir')
1010 self.s.set('moved', 'this entry will be moved')
1011 self.s.add('removed', parent='dir')
1012 self.s.set('removed', 'this entry will be deleted')
1013 revA = self.s.commit('Initial state')
1014 self.s.add('new', parent='dir')
1015 self.s.set('new', 'this entry is new')
1016 self.s.set('modified', 'a new value')
1017 self.s.remove('moved')
1018 self.s.add('moved2', parent='dir')
1019 self.s.set('moved2', 'this entry will be moved')
1020 self.s.remove('removed')
1021 revB = self.s.commit('Final state')
1022 new,mod,rem = self.s.changed(revA)
1023 self.failUnless(sorted(new) == ['moved2', 'new'],
1024 'Unexpected new: %s' % new)
1025 self.failUnless(mod == ['modified'],
1026 'Unexpected modified: %s' % mod)
1027 self.failUnless(sorted(rem) == ['moved', 'removed'],
1028 'Unexpected removed: %s' % rem)
1030 def make_storage_testcase_subclasses(storage_class, namespace):
1031 """Make StorageTestCase subclasses for storage_class in namespace."""
1032 storage_testcase_classes = [
1034 ob for ob in globals().values() if isinstance(ob, type))
1035 if issubclass(c, StorageTestCase) \
1036 and c.Class == Storage]
1038 for base_class in storage_testcase_classes:
1039 testcase_class_name = storage_class.__name__ + base_class.__name__
1040 testcase_class_bases = (base_class,)
1041 testcase_class_dict = dict(base_class.__dict__)
1042 testcase_class_dict['Class'] = storage_class
1043 testcase_class = type(
1044 testcase_class_name, testcase_class_bases, testcase_class_dict)
1045 setattr(namespace, testcase_class_name, testcase_class)
1047 def make_versioned_storage_testcase_subclasses(storage_class, namespace):
1048 """Make VersionedStorageTestCase subclasses for storage_class in namespace."""
1049 storage_testcase_classes = [
1051 ob for ob in globals().values() if isinstance(ob, type))
1052 if ((issubclass(c, StorageTestCase) \
1053 and c.Class == Storage)
1055 (issubclass(c, VersionedStorageTestCase) \
1056 and c.Class == VersionedStorage))]
1058 for base_class in storage_testcase_classes:
1059 testcase_class_name = storage_class.__name__ + base_class.__name__
1060 testcase_class_bases = (base_class,)
1061 testcase_class_dict = dict(base_class.__dict__)
1062 testcase_class_dict['Class'] = storage_class
1063 testcase_class = type(
1064 testcase_class_name, testcase_class_bases, testcase_class_dict)
1065 setattr(namespace, testcase_class_name, testcase_class)
1067 make_storage_testcase_subclasses(VersionedStorage, sys.modules[__name__])
1069 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
1070 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])