Added storage.Storage.storage_version() and command.InvalidStorageVersion.
[be.git] / libbe / storage / base.py
1 # Copyright
2
3 """
4 Abstract bug repository data storage to easily support multiple backends.
5 """
6
7 import copy
8 import os
9 import pickle
10 import types
11
12 from libbe.error import NotSupported
13 import libbe.storage
14 from libbe.util.tree import Tree
15 from libbe.util import InvalidObject
16 from libbe import TESTING
17
18 if TESTING == True:
19     import doctest
20     import os.path
21     import sys
22     import unittest
23
24     from libbe.util.utility import Dir
25
26 class ConnectionError (Exception):
27     pass
28
29 class InvalidID (KeyError):
30     pass
31
32 class InvalidRevision (KeyError):
33     pass
34
35 class InvalidDirectory (Exception):
36     pass
37
38 class DirectoryNotEmpty (InvalidDirectory):
39     pass
40
41 class NotWriteable (NotSupported):
42     def __init__(self, msg):
43         NotSupported.__init__(self, 'write', msg)
44
45 class NotReadable (NotSupported):
46     def __init__(self, msg):
47         NotSupported.__init__(self, 'read', msg)
48
49 class EmptyCommit(Exception):
50     def __init__(self):
51         Exception.__init__(self, 'No changes to commit')
52
53 class Entry (Tree):
54     def __init__(self, id, value=None, parent=None, directory=False,
55                  children=None):
56         if children == None:
57             Tree.__init__(self)
58         else:
59             Tree.__init__(self, children)
60         self.id = id
61         self.value = value
62         self.parent = parent
63         if self.parent != None:
64             if self.parent.directory == False:
65                 raise InvalidDirectory(
66                     'Non-directory %s cannot have children' % self.parent)
67             parent.append(self)
68         self.directory = directory
69
70     def __str__(self):
71         return '<Entry %s: %s>' % (self.id, self.value)
72
73     def __repr__(self):
74         return str(self)
75
76     def __cmp__(self, other, local=False):
77         if other == None:
78             return cmp(1, None)
79         if cmp(self.id, other.id) != 0:
80             return cmp(self.id, other.id)
81         if cmp(self.value, other.value) != 0:
82             return cmp(self.value, other.value)
83         if local == False:
84             if self.parent == None:
85                 if cmp(self.parent, other.parent) != 0:
86                     return cmp(self.parent, other.parent)
87             elif self.parent.__cmp__(other.parent, local=True) != 0:
88                 return self.parent.__cmp__(other.parent, local=True)
89             for sc,oc in zip(self, other):
90                 if sc.__cmp__(oc, local=True) != 0:
91                     return sc.__cmp__(oc, local=True)
92         return 0
93
94     def _objects_to_ids(self):
95         if self.parent != None:
96             self.parent = self.parent.id
97         for i,c in enumerate(self):
98             self[i] = c.id
99         return self
100
101     def _ids_to_objects(self, dict):
102         if self.parent != None:
103             self.parent = dict[self.parent]
104         for i,c in enumerate(self):
105             self[i] = dict[c]
106         return self
107
108 class Storage (object):
109     """
110     This class declares all the methods required by a Storage
111     interface.  This implementation just keeps the data in a
112     dictionary and uses pickle for persistent storage.
113     """
114     name = 'Storage'
115
116     def __init__(self, repo='/', encoding='utf-8', options=None):
117         self.repo = repo
118         self.encoding = encoding
119         self.options = options
120         self.readable = True  # soft limit (user choice)
121         self._readable = True # hard limit (backend choice)
122         self.writeable = True  # soft limit (user choice)
123         self._writeable = True # hard limit (backend choice)
124         self.versioned = False
125         self.can_init = True
126
127     def __str__(self):
128         return '<%s %s %s>' % (self.__class__.__name__, id(self), self.repo)
129
130     def __repr__(self):
131         return str(self)
132
133     def version(self):
134         """Return a version string for this backend."""
135         return '0'
136
137     def storage_version(self):
138         """Return the storage format for this backend."""
139         return libbe.storage.STORAGE_VERSION
140
141     def is_readable(self):
142         return self.readable and self._readable
143
144     def is_writeable(self):
145         return self.writeable and self._writeable
146
147     def init(self):
148         """Create a new storage repository."""
149         if self.can_init == False:
150             raise NotSupported('init',
151                                'Cannot initialize this repository format.')
152         if self.is_writeable() == False:
153             raise NotWriteable('Cannot initialize unwriteable storage.')
154         return self._init()
155
156     def _init(self):
157         f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
158         root = Entry(id='__ROOT__', directory=True)
159         d = {root.id:root}
160         pickle.dump(dict((k,v._objects_to_ids()) for k,v in d.items()), f, -1)
161         f.close()
162
163     def destroy(self):
164         """Remove the storage repository."""
165         if self.is_writeable() == False:
166             raise NotWriteable('Cannot destroy unwriteable storage.')
167         return self._destroy()
168
169     def _destroy(self):
170         os.remove(os.path.join(self.repo, 'repo.pkl'))
171
172     def connect(self):
173         """Open a connection to the repository."""
174         if self.is_readable() == False:
175             raise NotReadable('Cannot connect to unreadable storage.')
176         self._connect()
177
178     def _connect(self):
179         try:
180             f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
181         except IOError:
182             raise ConnectionError(self)
183         d = pickle.load(f)
184         self._data = dict((k,v._ids_to_objects(d)) for k,v in d.items())
185         f.close()
186
187     def disconnect(self):
188         """Close the connection to the repository."""
189         if self.is_writeable() == False:
190             return
191         f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
192         pickle.dump(dict((k,v._objects_to_ids())
193                          for k,v in self._data.items()), f, -1)
194         f.close()
195         self._data = None
196
197     def add(self, id, *args, **kwargs):
198         """Add an entry"""
199         if self.is_writeable() == False:
200             raise NotWriteable('Cannot add entry to unwriteable storage.')
201         try:  # Maybe we've already added that id?
202             self.get(id)
203             pass # yup, no need to add another
204         except InvalidID:
205             self._add(id, *args, **kwargs)
206
207     def _add(self, id, parent=None, directory=False):
208         if parent == None:
209             parent = '__ROOT__'
210         p = self._data[parent]
211         self._data[id] = Entry(id, parent=p, directory=directory)
212
213     def remove(self, *args, **kwargs):
214         """Remove an entry."""
215         if self.is_writeable() == False:
216             raise NotSupported('write',
217                                'Cannot remove entry from unwriteable storage.')
218         self._remove(*args, **kwargs)
219
220     def _remove(self, id):
221         if self._data[id].directory == True \
222                 and len(self.children(id)) > 0:
223             raise DirectoryNotEmpty(id)
224         e = self._data.pop(id)
225         e.parent.remove(e)
226
227     def recursive_remove(self, *args, **kwargs):
228         """Remove an entry and all its decendents."""
229         if self.is_writeable() == False:
230             raise NotSupported('write',
231                                'Cannot remove entries from unwriteable storage.')
232         self._recursive_remove(*args, **kwargs)
233
234     def _recursive_remove(self, id):
235         for entry in reversed(list(self._data[id].traverse())):
236             self._remove(entry.id)
237
238     def children(self, *args, **kwargs):
239         """Return a list of specified entry's children's ids."""
240         if self.is_readable() == False:
241             raise NotReadable('Cannot list children with unreadable storage.')
242         return self._children(*args, **kwargs)
243
244     def _children(self, id=None, revision=None):
245         if id == None:
246             id = '__ROOT__'
247         return [c.id for c in self._data[id] if not c.id.startswith('__')]
248
249     def get(self, *args, **kwargs):
250         """
251         Get contents of and entry as they were in a given revision.
252         revision==None specifies the current revision.
253
254         If there is no id, return default, unless default is not
255         given, in which case raise InvalidID.
256         """
257         if self.is_readable() == False:
258             raise NotReadable('Cannot get entry with unreadable storage.')
259         if 'decode' in kwargs:
260             decode = kwargs.pop('decode')
261         else:
262             decode = False
263         value = self._get(*args, **kwargs)
264         if decode == True and type(value) != types.UnicodeType:
265             return unicode(value, self.encoding)
266         if decode == False and type(value) != types.StringType:
267             return value.encode(self.encoding)
268         return value
269
270     def _get(self, id, default=InvalidObject, revision=None):
271         if id in self._data:
272             return self._data[id].value
273         elif default == InvalidObject:
274             raise InvalidID(id)
275         return default
276
277     def set(self, id, value, *args, **kwargs):
278         """
279         Set the entry contents.
280         """
281         if self.is_writeable() == False:
282             raise NotWriteable('Cannot set entry in unwriteable storage.')
283         if type(value) == types.UnicodeType:
284             value = value.encode(self.encoding)
285         self._set(id, value, *args, **kwargs)
286
287     def _set(self, id, value):
288         if id not in self._data:
289             raise InvalidID(id)
290         if self._data[id].directory == True:
291             raise InvalidDirectory(
292                 'Directory %s cannot have data' % self.parent)
293         self._data[id].value = value
294
295 class VersionedStorage (Storage):
296     """
297     This class declares all the methods required by a Storage
298     interface that supports versioning.  This implementation just
299     keeps the data in a list and uses pickle for persistent
300     storage.
301     """
302     name = 'VersionedStorage'
303
304     def __init__(self, *args, **kwargs):
305         Storage.__init__(self, *args, **kwargs)
306         self.versioned = True
307
308     def _init(self):
309         f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
310         root = Entry(id='__ROOT__', directory=True)
311         summary = Entry(id='__COMMIT__SUMMARY__', value='Initial commit')
312         body = Entry(id='__COMMIT__BODY__')
313         initial_commit = {root.id:root, summary.id:summary, body.id:body}
314         d = dict((k,v._objects_to_ids()) for k,v in initial_commit.items())
315         pickle.dump([d, copy.deepcopy(d)], f, -1) # [inital tree, working tree]
316         f.close()
317
318     def _connect(self):
319         try:
320             f = open(os.path.join(self.repo, 'repo.pkl'), 'rb')
321         except IOError:
322             raise ConnectionError(self)
323         d = pickle.load(f)
324         self._data = [dict((k,v._ids_to_objects(t)) for k,v in t.items())
325                       for t in d]
326         f.close()
327
328     def disconnect(self):
329         """Close the connection to the repository."""
330         if self.is_writeable() == False:
331             return
332         f = open(os.path.join(self.repo, 'repo.pkl'), 'wb')
333         pickle.dump([dict((k,v._objects_to_ids())
334                           for k,v in t.items()) for t in self._data], f, -1)
335         f.close()
336         self._data = None
337
338     def _add(self, id, parent=None, directory=False):
339         if parent == None:
340             parent = '__ROOT__'
341         p = self._data[-1][parent]
342         self._data[-1][id] = Entry(id, parent=p, directory=directory)
343
344     def _remove(self, id):
345         if self._data[-1][id].directory == True \
346                 and len(self.children(id)) > 0:
347             raise DirectoryNotEmpty(id)
348         e = self._data[-1].pop(id)
349         e.parent.remove(e)
350
351     def _recursive_remove(self, id):
352         for entry in reversed(list(self._data[-1][id].traverse())):
353             self._remove(entry.id)
354
355     def _children(self, id=None, revision=None):
356         if id == None:
357             id = '__ROOT__'
358         if revision == None:
359             revision = -1
360         return [c.id for c in self._data[revision][id]
361                 if not c.id.startswith('__')]
362
363     def _get(self, id, default=InvalidObject, revision=None):
364         if revision == None:
365             revision = -1
366         if id in self._data[revision]:
367             return self._data[revision][id].value
368         elif default == InvalidObject:
369             raise InvalidID(id)
370         return default
371
372     def _set(self, id, value):
373         if id not in self._data[-1]:
374             raise InvalidID(id)
375         self._data[-1][id].value = value
376
377     def commit(self, *args, **kwargs):
378         """
379         Commit the current repository, with a commit message string
380         summary and body.  Return the name of the new revision.
381
382         If allow_empty == False (the default), raise EmptyCommit if
383         there are no changes to commit.
384         """
385         if self.is_writeable() == False:
386             raise NotWriteable('Cannot commit to unwriteable storage.')
387         return self._commit(*args, **kwargs)
388
389     def _commit(self, summary, body=None, allow_empty=False):
390         if self._data[-1] == self._data[-2] and allow_empty == False:
391             raise EmptyCommit
392         self._data[-1]["__COMMIT__SUMMARY__"].value = summary
393         self._data[-1]["__COMMIT__BODY__"].value = body
394         rev = len(self._data)-1
395         self._data.append(copy.deepcopy(self._data[-1]))
396         return rev
397
398     def revision_id(self, index=None):
399         """
400         Return the name of the <index>th revision.  The choice of
401         which branch to follow when crossing branches/merges is not
402         defined.  Revision indices start at 1; ID 0 is the blank
403         repository.
404
405         Return None if index==None.
406
407         If the specified revision does not exist, raise InvalidRevision.
408         """
409         if index == None:
410             return None
411         try:
412             if int(index) != index:
413                 raise InvalidRevision(index)
414         except ValueError:
415             raise InvalidRevision(index)
416         L = len(self._data) - 1  # -1 b/c of initial commit
417         if index >= -L and index <= L:
418             return index % L
419         raise InvalidRevision(i)
420
421 if TESTING == True:
422     class StorageTestCase (unittest.TestCase):
423         """Test cases for base Storage class."""
424
425         Class = Storage
426
427         def __init__(self, *args, **kwargs):
428             super(StorageTestCase, self).__init__(*args, **kwargs)
429             self.dirname = None
430
431         def setUp(self):
432             """Set up test fixtures for Storage test case."""
433             super(StorageTestCase, self).setUp()
434             self.dir = Dir()
435             self.dirname = self.dir.path
436             self.s = self.Class(repo=self.dirname)
437             self.assert_failed_connect()
438             self.s.init()
439             self.s.connect()
440
441         def tearDown(self):
442             super(StorageTestCase, self).tearDown()
443             self.s.disconnect()
444             self.s.destroy()
445             self.assert_failed_connect()
446             self.dir.cleanup()
447
448         def assert_failed_connect(self):
449             try:
450                 self.s.connect()
451                 self.fail(
452                     "Connected to %(name)s repository before initialising"
453                     % vars(self.Class))
454             except ConnectionError:
455                 pass
456
457     class Storage_init_TestCase (StorageTestCase):
458         """Test cases for Storage.init method."""
459
460         def test_connect_should_succeed_after_init(self):
461             """Should connect after initialization."""
462             self.s.connect()
463
464     class Storage_add_remove_TestCase (StorageTestCase):
465         """Test cases for Storage.add, .remove, and .recursive_remove methods."""
466
467         def test_initially_empty(self):
468             """New repository should be empty."""
469             self.failUnless(len(self.s.children()) == 0, self.s.children())
470
471         def test_add_identical_rooted(self):
472             """
473             Adding entries with the same ID should not increase the number of children.
474             """
475             for i in range(10):
476                 self.s.add('some id', directory=False)
477                 s = sorted(self.s.children())
478                 self.failUnless(s == ['some id'], s)
479
480         def test_add_rooted(self):
481             """
482             Adding entries should increase the number of children (rooted).
483             """
484             ids = []
485             for i in range(10):
486                 ids.append(str(i))
487                 self.s.add(ids[-1], directory=False)
488                 s = sorted(self.s.children())
489                 self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
490
491         def test_add_nonrooted(self):
492             """
493             Adding entries should increase the number of children (nonrooted).
494             """
495             self.s.add('parent', directory=True)
496             ids = []
497             for i in range(10):
498                 ids.append(str(i))
499                 self.s.add(ids[-1], 'parent', directory=True)
500                 s = sorted(self.s.children('parent'))
501                 self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
502                 s = self.s.children()
503                 self.failUnless(s == ['parent'], s)
504
505         def test_children(self):
506             """
507             Non-UUID ids should be returned as such.
508             """
509             self.s.add('parent', directory=True)
510             ids = []
511             for i in range(10):
512                 ids.append('parent/%s' % str(i))
513                 self.s.add(ids[-1], 'parent', directory=True)
514                 s = sorted(self.s.children('parent'))
515                 self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
516
517         def test_add_invalid_directory(self):
518             """
519             Should not be able to add children to non-directories.
520             """
521             self.s.add('parent', directory=False)
522             try:
523                 self.s.add('child', 'parent', directory=False)
524                 self.fail(
525                     '%s.add() succeeded instead of raising InvalidDirectory'
526                     % (vars(self.Class)['name']))
527             except InvalidDirectory:
528                 pass
529             try:
530                 self.s.add('child', 'parent', directory=True)
531                 self.fail(
532                     '%s.add() succeeded instead of raising InvalidDirectory'
533                     % (vars(self.Class)['name']))
534             except InvalidDirectory:
535                 pass
536             self.failUnless(len(self.s.children('parent')) == 0,
537                             self.s.children('parent'))
538
539         def test_remove_rooted(self):
540             """
541             Removing entries should decrease the number of children (rooted).
542             """
543             ids = []
544             for i in range(10):
545                 ids.append(str(i))
546                 self.s.add(ids[-1], directory=True)
547             for i in range(10):
548                 self.s.remove(ids.pop())
549                 s = sorted(self.s.children())
550                 self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
551
552         def test_remove_nonrooted(self):
553             """
554             Removing entries should decrease the number of children (nonrooted).
555             """
556             self.s.add('parent', directory=True)
557             ids = []
558             for i in range(10):
559                 ids.append(str(i))
560                 self.s.add(ids[-1], 'parent', directory=False)
561             for i in range(10):
562                 self.s.remove(ids.pop())
563                 s = sorted(self.s.children('parent'))
564                 self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
565                 s = self.s.children()
566                 self.failUnless(s == ['parent'], s)
567
568         def test_remove_directory_not_empty(self):
569             """
570             Removing a non-empty directory entry should raise exception.
571             """
572             self.s.add('parent', directory=True)
573             ids = []
574             for i in range(10):
575                 ids.append(str(i))
576                 self.s.add(ids[-1], 'parent', directory=True)
577             self.s.remove(ids.pop()) # empty directory removal succeeds
578             try:
579                 self.s.remove('parent') # empty directory removal succeeds
580                 self.fail(
581                     "%s.remove() didn't raise DirectoryNotEmpty"
582                     % (vars(self.Class)['name']))
583             except DirectoryNotEmpty:
584                 pass
585
586         def test_recursive_remove(self):
587             """
588             Recursive remove should empty the tree.
589             """
590             self.s.add('parent', directory=True)
591             ids = []
592             for i in range(10):
593                 ids.append(str(i))
594                 self.s.add(ids[-1], 'parent', directory=True)
595                 for j in range(10): # add some grandkids
596                     self.s.add(str(20*(i+1)+j), ids[-1], directory=False)
597             self.s.recursive_remove('parent')
598             s = sorted(self.s.children())
599             self.failUnless(s == [], s)
600
601     class Storage_get_set_TestCase (StorageTestCase):
602         """Test cases for Storage.get and .set methods."""
603
604         id = 'unlikely id'
605         val = 'unlikely value'
606
607         def test_get_default(self):
608             """
609             Get should return specified default if id not in Storage.
610             """
611             ret = self.s.get(self.id, default=self.val)
612             self.failUnless(ret == self.val,
613                     "%s.get() returned %s not %s"
614                     % (vars(self.Class)['name'], ret, self.val))
615
616         def test_get_default_exception(self):
617             """
618             Get should raise exception if id not in Storage and no default.
619             """
620             try:
621                 ret = self.s.get(self.id)
622                 self.fail(
623                     "%s.get() returned %s instead of raising InvalidID"
624                     % (vars(self.Class)['name'], ret))
625             except InvalidID:
626                 pass
627
628         def test_get_initial_value(self):
629             """
630             Data value should be None before any value has been set.
631             """
632             self.s.add(self.id, directory=False)
633             ret = self.s.get(self.id)
634             self.failUnless(ret == None,
635                     "%s.get() returned %s not None"
636                     % (vars(self.Class)['name'], ret))
637
638         def test_set_exception(self):
639             """
640             Set should raise exception if id not in Storage.
641             """
642             try:
643                 self.s.set(self.id, self.val)
644                 self.fail(
645                     "%(name)s.set() did not raise InvalidID"
646                     % vars(self.Class))
647             except InvalidID:
648                 pass
649
650         def test_set(self):
651             """
652             Set should define the value returned by get.
653             """
654             self.s.add(self.id, directory=False)
655             self.s.set(self.id, self.val)
656             ret = self.s.get(self.id)
657             self.failUnless(ret == self.val,
658                     "%s.get() returned %s not %s"
659                     % (vars(self.Class)['name'], ret, self.val))
660
661         def test_unicode_set(self):
662             """
663             Set should define the value returned by get.
664             """
665             val = u'Fran\xe7ois'
666             self.s.add(self.id, directory=False)
667             self.s.set(self.id, val)
668             ret = self.s.get(self.id, decode=True)
669             self.failUnless(type(ret) == types.UnicodeType,
670                     "%s.get() returned %s not UnicodeType"
671                     % (vars(self.Class)['name'], type(ret)))
672             self.failUnless(ret == val,
673                     "%s.get() returned %s not %s"
674                     % (vars(self.Class)['name'], ret, self.val))
675             ret = self.s.get(self.id)
676             self.failUnless(type(ret) == types.StringType,
677                     "%s.get() returned %s not StringType"
678                     % (vars(self.Class)['name'], type(ret)))
679             s = unicode(ret, self.s.encoding)
680             self.failUnless(s == val,
681                     "%s.get() returned %s not %s"
682                     % (vars(self.Class)['name'], s, self.val))
683
684
685     class Storage_persistence_TestCase (StorageTestCase):
686         """Test cases for Storage.disconnect and .connect methods."""
687
688         id = 'unlikely id'
689         val = 'unlikely value'
690
691         def test_get_set_persistence(self):
692             """
693             Set should define the value returned by get after reconnect.
694             """
695             self.s.add(self.id, directory=False)
696             self.s.set(self.id, self.val)
697             self.s.disconnect()
698             self.s.connect()
699             ret = self.s.get(self.id)
700             self.failUnless(ret == self.val,
701                     "%s.get() returned %s not %s"
702                     % (vars(self.Class)['name'], ret, self.val))
703
704         def test_add_nonrooted_persistence(self):
705             """
706             Adding entries should increase the number of children after reconnect.
707             """
708             self.s.add('parent', directory=True)
709             ids = []
710             for i in range(10):
711                 ids.append(str(i))
712                 self.s.add(ids[-1], 'parent', directory=False)
713             self.s.disconnect()
714             self.s.connect()
715             s = sorted(self.s.children('parent'))
716             self.failUnless(s == ids, '\n  %s\n  !=\n  %s' % (s, ids))
717             s = self.s.children()
718             self.failUnless(s == ['parent'], s)
719
720     class VersionedStorageTestCase (StorageTestCase):
721         """Test cases for base VersionedStorage class."""
722
723         Class = VersionedStorage
724
725     class VersionedStorage_commit_TestCase (VersionedStorageTestCase):
726         """Test cases for VersionedStorage methods."""
727
728         id = 'I' #unlikely id'
729         val = 'X'
730         commit_msg = 'C' #ommitting something interesting'
731         commit_body = 'B' #ome\nlonger\ndescription\n'
732
733         def test_revision_id_exception(self):
734             """
735             Invalid revision id should raise InvalidRevision.
736             """
737             try:
738                 rev = self.s.revision_id('highly unlikely revision id')
739                 self.fail(
740                     "%s.revision_id() didn't raise InvalidRevision, returned %s."
741                     % (vars(self.Class)['name'], rev))
742             except InvalidRevision:
743                 pass
744
745         def test_empty_commit_raises_exception(self):
746             """
747             Empty commit should raise exception.
748             """
749             try:
750                 self.s.commit(self.commit_msg, self.commit_body)
751                 self.fail(
752                     "Empty %(name)s.commit() didn't raise EmptyCommit."
753                     % vars(self.Class))
754             except EmptyCommit:
755                 pass
756
757         def test_empty_commit_allowed(self):
758             """
759             Empty commit should _not_ raise exception if allow_empty=True.
760             """
761             self.s.commit(self.commit_msg, self.commit_body,
762                           allow_empty=True)
763
764         def test_commit_revision_ids(self):
765             """
766             Commit / revision_id should agree on revision ids.
767             """
768             def val(i):
769                 return '%s:%d' % (self.val, i+1)
770             self.s.add(self.id, directory=False)
771             revs = []
772             for i in range(10):
773                 self.s.set(self.id, val(i))
774                 revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
775                                           self.commit_body))
776             for i in range(10):
777                 rev = self.s.revision_id(i+1)
778                 self.failUnless(rev == revs[i],
779                                 "%s.revision_id(%d) returned %s not %s"
780                                 % (vars(self.Class)['name'], i+1, rev, revs[i]))
781             for i in range(-1, -9, -1):
782                 rev = self.s.revision_id(i)
783                 self.failUnless(rev == revs[i],
784                                 "%s.revision_id(%d) returned %s not %s"
785                                 % (vars(self.Class)['name'], i, rev, revs[i]))
786
787         def test_get_previous_version(self):
788             """
789             Get should be able to return the previous version.
790             """
791             def val(i):
792                 return '%s:%d' % (self.val, i+1)
793             self.s.add(self.id, directory=False)
794             revs = []
795             for i in range(10):
796                 self.s.set(self.id, val(i))
797                 revs.append(self.s.commit('%s: %d' % (self.commit_msg, i),
798                                           self.commit_body))
799             for i in range(10):
800                 ret = self.s.get(self.id, revision=revs[i])
801                 self.failUnless(ret == val(i),
802                                 "%s.get() returned %s not %s for revision %s"
803                                 % (vars(self.Class)['name'], ret, val(i), revs[i]))
804
805     def make_storage_testcase_subclasses(storage_class, namespace):
806         """Make StorageTestCase subclasses for storage_class in namespace."""
807         storage_testcase_classes = [
808             c for c in (
809                 ob for ob in globals().values() if isinstance(ob, type))
810             if issubclass(c, StorageTestCase) \
811                 and not issubclass(c, VersionedStorageTestCase)]
812
813         for base_class in storage_testcase_classes:
814             testcase_class_name = storage_class.__name__ + base_class.__name__
815             testcase_class_bases = (base_class,)
816             testcase_class_dict = dict(base_class.__dict__)
817             testcase_class_dict['Class'] = storage_class
818             testcase_class = type(
819                 testcase_class_name, testcase_class_bases, testcase_class_dict)
820             setattr(namespace, testcase_class_name, testcase_class)
821
822     def make_versioned_storage_testcase_subclasses(storage_class, namespace):
823         """Make VersionedStorageTestCase subclasses for storage_class in namespace."""
824         storage_testcase_classes = [
825             c for c in (
826                 ob for ob in globals().values() if isinstance(ob, type))
827             if issubclass(c, StorageTestCase)]
828
829         for base_class in storage_testcase_classes:
830             testcase_class_name = storage_class.__name__ + base_class.__name__
831             testcase_class_bases = (base_class,)
832             testcase_class_dict = dict(base_class.__dict__)
833             testcase_class_dict['Class'] = storage_class
834             testcase_class = type(
835                 testcase_class_name, testcase_class_bases, testcase_class_dict)
836             setattr(namespace, testcase_class_name, testcase_class)
837
838     make_storage_testcase_subclasses(VersionedStorage, sys.modules[__name__])
839
840     unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
841     suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])