[Checkins] SVN: relstorage/trunk/ Implemented the storage iterator protocol, making it possible to copy

Shane Hathaway shane at hathawaymix.org
Sun Feb 17 01:49:07 EST 2008


Log message for revision 83973:
  Implemented the storage iterator protocol, making it possible to copy
  transactions to and from FileStorage and other RelStorage instances.
  Added tests of this capability.
  

Changed:
  U   relstorage/trunk/CHANGELOG.txt
  U   relstorage/trunk/relstorage/adapters/common.py
  U   relstorage/trunk/relstorage/adapters/mysql.py
  U   relstorage/trunk/relstorage/adapters/oracle.py
  U   relstorage/trunk/relstorage/adapters/postgresql.py
  U   relstorage/trunk/relstorage/relstorage.py
  U   relstorage/trunk/relstorage/tests/reltestbase.py
  U   relstorage/trunk/relstorage/tests/testmysql.py
  U   relstorage/trunk/relstorage/tests/testoracle.py
  U   relstorage/trunk/relstorage/tests/testpostgresql.py

-=-
Modified: relstorage/trunk/CHANGELOG.txt
===================================================================
--- relstorage/trunk/CHANGELOG.txt	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/CHANGELOG.txt	2008-02-17 06:49:05 UTC (rev 83973)
@@ -24,7 +24,7 @@
 - Stored objects are now buffered in a database table rather than a file.
 
 - Stopped using the LISTEN and NOTIFY statements in PostgreSQL since
-  they are not reliable enough.
+  they are not strictly transactional in the sense we require.
 
 - Started using a prepared statement in PostgreSQL for getting the
   newest transaction ID quickly.
@@ -38,7 +38,10 @@
   polls, but it also increases the potential for conflict errors on
   servers with high write volume.
 
+- Implemented the storage iterator protocol, making it possible to copy
+  transactions to and from FileStorage and other RelStorage instances.
 
+
 PGStorage 0.4
 
 - Began using the PostgreSQL LISTEN and NOTIFY statements as a shortcut

Modified: relstorage/trunk/relstorage/adapters/common.py
===================================================================
--- relstorage/trunk/relstorage/adapters/common.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/adapters/common.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -38,6 +38,8 @@
         'pack_tid':     '%(pack_tid)s',
         'undo_tid':     '%(undo_tid)s',
         'self_tid':     '%(self_tid)s',
+        'min_tid':      '%(min_tid)s',
+        'max_tid':      '%(max_tid)s',
     }
 
     _scripts = {
@@ -121,8 +123,9 @@
 
 
     def iter_transactions(self, cursor):
-        """Iterate over the transaction log.
+        """Iterate over the transaction log, newest first.
 
+        Skips packed transactions.
         Yields (tid, username, description, extension) for each transaction.
         """
         stmt = """
@@ -136,6 +139,30 @@
         return iter(cursor)
 
 
+    def iter_transactions_range(self, cursor, start=None, stop=None):
+        """Iterate over the transactions in the given range, oldest first.
+
+        Includes packed transactions.
+        Yields (tid, packed, username, description, extension)
+            for each transaction.
+        """
+        stmt = """
+        SELECT tid,
+            CASE WHEN packed = %(TRUE)s THEN 1 ELSE 0 END,
+            username, description, extension
+        FROM transaction
+        WHERE tid >= 0
+        """
+        if start is not None:
+            stmt += " AND tid >= %(min_tid)s"
+        if stop is not None:
+            stmt += " AND tid <= %(max_tid)s"
+        stmt += " ORDER BY tid"
+        self._run_script_stmt(cursor, stmt,
+            {'min_tid': start, 'max_tid': stop})
+        return iter(cursor)
+
+
     def iter_object_history(self, cursor, oid):
         """Iterate over an object's history.
 
@@ -162,6 +189,25 @@
         return iter(cursor)
 
 
+    def iter_objects(self, cursor, tid):
+        """Iterate over object states in a transaction.
+
+        Yields (oid, prev_tid, state) for each object state.
+        """
+        stmt = """
+        SELECT zoid, state
+        FROM object_state
+        WHERE tid = %(tid)s
+        ORDER BY zoid
+        """
+        self._run_script_stmt(cursor, stmt, {'tid': tid})
+        for oid, state in cursor:
+            if hasattr(state, 'read'):
+                # Oracle
+                state = state.read()
+            yield oid, state
+
+
     def verify_undoable(self, cursor, undo_tid):
         """Raise UndoError if it is not safe to undo the specified txn."""
         stmt = """
@@ -255,11 +301,11 @@
                     AND prev.tid = temp_undo.prev_tid);
 
         -- List the changed OIDs.
-        SELECT zoid FROM object_state WHERE tid = %(undo_tid)s
+        SELECT zoid FROM temp_undo
         """
         self._run_script(cursor, stmt,
             {'undo_tid': undo_tid, 'self_tid': self_tid})
-        res = [oid_int for (oid_int,) in cursor]
+        res = [oid for (oid,) in cursor]
 
         stmt = self._scripts['reset_temp_undo']
         if stmt:
@@ -495,7 +541,7 @@
         add_rows = []  # [(from_oid, tid, to_oid)]
         for from_oid, state in cursor:
             if hasattr(state, 'read'):
-                # cx_Oracle detail
+                # Oracle
                 state = state.read()
             if state:
                 to_oids = get_references(str(state))

Modified: relstorage/trunk/relstorage/adapters/mysql.py
===================================================================
--- relstorage/trunk/relstorage/adapters/mysql.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/adapters/mysql.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -379,6 +379,21 @@
         """
         cursor.execute(stmt, (prev_tid, md5sum, MySQLdb.Binary(data), oid))
 
+    def restore(self, cursor, oid, tid, md5sum, data):
+        """Store an object directly, without conflict detection.
+
+        Used for copying transactions into this database.
+        """
+        stmt = """
+        INSERT INTO object_state (zoid, tid, prev_tid, md5, state)
+        VALUES (%s, %s,
+            COALESCE((SELECT tid FROM current_object WHERE zoid = %s), 0),
+            %s, %s)
+        """
+        if data is not None:
+            data = MySQLdb.Binary(data)
+        cursor.execute(stmt, (oid, tid, oid, md5sum, data))
+
     def start_commit(self, cursor):
         """Prepare to commit."""
         # Hold commit_lock to prevent concurrent commits.
@@ -411,15 +426,16 @@
             timestamp = now
         return tid, timestamp
 
-    def add_transaction(self, cursor, tid, username, description, extension):
+    def add_transaction(self, cursor, tid, username, description, extension,
+            packed=False):
         """Add a transaction."""
         stmt = """
         INSERT INTO transaction
-            (tid, username, description, extension)
-        VALUES (%s, %s, %s, %s)
+            (tid, packed, username, description, extension)
+        VALUES (%s, %s, %s, %s, %s)
         """
         cursor.execute(stmt, (
-            tid, username, description, MySQLdb.Binary(extension)))
+            tid, packed, username, description, MySQLdb.Binary(extension)))
 
     def detect_conflict(self, cursor):
         """Find one conflict in the data about to be committed.

Modified: relstorage/trunk/relstorage/adapters/oracle.py
===================================================================
--- relstorage/trunk/relstorage/adapters/oracle.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/adapters/oracle.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -37,6 +37,8 @@
         'pack_tid':     ':pack_tid',
         'undo_tid':     ':undo_tid',
         'self_tid':     ':self_tid',
+        'min_tid':      ':min_tid',
+        'max_tid':      ':max_tid',
     }
 
     _scripts = {
@@ -103,8 +105,8 @@
         CREATE TABLE transaction (
             tid         NUMBER(20) NOT NULL PRIMARY KEY,
             packed      CHAR DEFAULT 'N' CHECK (packed IN ('N', 'Y')),
-            username    VARCHAR2(255) NOT NULL,
-            description VARCHAR2(4000) NOT NULL,
+            username    VARCHAR2(255),
+            description VARCHAR2(4000),
             extension   RAW(2000)
         );
 
@@ -458,6 +460,22 @@
         cursor.execute(stmt, oid=oid, prev_tid=prev_tid,
             md5sum=md5sum, data=cx_Oracle.Binary(data))
 
+    def restore(self, cursor, oid, tid, md5sum, data):
+        """Store an object directly, without conflict detection.
+
+        Used for copying transactions into this database.
+        """
+        cursor.setinputsizes(data=cx_Oracle.BLOB)
+        stmt = """
+        INSERT INTO object_state (zoid, tid, prev_tid, md5, state)
+        VALUES (:oid, :tid,
+            COALESCE((SELECT tid FROM current_object WHERE zoid = :oid), 0),
+            :md5sum, :data)
+        """
+        if data is not None:
+            data = cx_Oracle.Binary(data)
+        cursor.execute(stmt, oid=oid, tid=tid, md5sum=md5sum, data=data)
+
     def start_commit(self, cursor):
         """Prepare to commit."""
         # Hold commit_lock to prevent concurrent commits
@@ -489,15 +507,16 @@
         tid, now = cursor.fetchone()
         return tid, self._parse_dsinterval(now)
 
-    def add_transaction(self, cursor, tid, username, description, extension):
+    def add_transaction(self, cursor, tid, username, description, extension,
+            packed=False):
         """Add a transaction."""
         stmt = """
         INSERT INTO transaction
-            (tid, username, description, extension)
-        VALUES (:1, :2, :3, :4)
+            (tid, packed, username, description, extension)
+        VALUES (:1, :2, :3, :4, :5)
         """
         cursor.execute(stmt, (
-            tid, username or '-', description or '-',
+            tid, packed and 'Y' or 'N', username, description,
             cx_Oracle.Binary(extension)))
 
     def detect_conflict(self, cursor):

Modified: relstorage/trunk/relstorage/adapters/postgresql.py
===================================================================
--- relstorage/trunk/relstorage/adapters/postgresql.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/adapters/postgresql.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -397,6 +397,21 @@
         """
         cursor.execute(stmt, (prev_tid, md5sum, encodestring(data), oid))
 
+    def restore(self, cursor, oid, tid, md5sum, data):
+        """Store an object directly, without conflict detection.
+
+        Used for copying transactions into this database.
+        """
+        stmt = """
+        INSERT INTO object_state (zoid, tid, prev_tid, md5, state)
+        VALUES (%s, %s,
+            COALESCE((SELECT tid FROM current_object WHERE zoid = %s), 0),
+            %s, decode(%s, 'base64'))
+        """
+        if data is not None:
+            data = encodestring(data)
+        cursor.execute(stmt, (oid, tid, oid, md5sum, data))
+
     def start_commit(self, cursor):
         """Prepare to commit."""
         # Hold commit_lock to prevent concurrent commits
@@ -423,15 +438,16 @@
         assert cursor.rowcount == 1
         return cursor.fetchone()
 
-    def add_transaction(self, cursor, tid, username, description, extension):
+    def add_transaction(self, cursor, tid, username, description, extension,
+            packed=False):
         """Add a transaction."""
         stmt = """
         INSERT INTO transaction
-            (tid, username, description, extension)
-        VALUES (%s, %s, %s, decode(%s, 'base64'))
+            (tid, packed, username, description, extension)
+        VALUES (%s, %s, %s, %s, decode(%s, 'base64'))
         """
         cursor.execute(stmt, (
-            tid, username, description, encodestring(extension)))
+            tid, packed, username, description, encodestring(extension)))
 
     def detect_conflict(self, cursor):
         """Find one conflict in the data about to be committed.

Modified: relstorage/trunk/relstorage/relstorage.py
===================================================================
--- relstorage/trunk/relstorage/relstorage.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/relstorage.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -270,6 +270,39 @@
             self._lock_release()
 
 
+    def restore(self, oid, serial, data, version, prev_txn, transaction):
+        # Like store(), but used for importing transactions.  See the
+        # comments in FileStorage.restore().  The prev_txn optimization
+        # is not used.
+        if self._is_read_only:
+            raise POSException.ReadOnlyError()
+        if transaction is not self._transaction:
+            raise POSException.StorageTransactionError(self, transaction)
+        if version:
+            raise POSException.Unsupported("Versions aren't supported")
+
+        assert self._tid is not None
+        assert self._prepared_txn is None
+        if data is not None:
+            md5sum = md5.new(data).hexdigest()
+        else:
+            # George Bailey object
+            md5sum = None
+
+        adapter = self._adapter
+        cursor = self._store_cursor
+        assert cursor is not None
+        oid_int = u64(oid)
+        tid_int = u64(serial)
+
+        self._lock_acquire()
+        try:
+            # save the data.  Note that md5sum and data can be None.
+            adapter.restore(cursor, oid_int, tid_int, md5sum, data)
+        finally:
+            self._lock_release()
+
+
     def tpc_begin(self, transaction, tid=None, status=' '):
         if self._is_read_only:
             raise POSException.ReadOnlyError()
@@ -309,10 +342,12 @@
 
             if tid is not None:
                 # get the commit lock and add the transaction now
+                packed = (status == 'p')
                 adapter.start_commit(cursor)
                 tid_int = u64(tid)
                 try:
-                    adapter.add_transaction(cursor, tid_int, user, desc, ext)
+                    adapter.add_transaction(
+                        cursor, tid_int, user, desc, ext, packed)
                 except:
                     self._drop_store_connection()
                     raise
@@ -518,8 +553,8 @@
                 tid = p64(tid_int)
                 d = {'id': base64.encodestring(tid)[:-1],
                      'time': TimeStamp(tid).timeTime(),
-                     'user_name': user,
-                     'description': desc}
+                     'user_name': user or '',
+                     'description': desc or ''}
                 if ext:
                     ext = str(ext)
                 if ext:
@@ -553,8 +588,8 @@
                 else:
                     d = {}
                 d.update({"time": TimeStamp(tid).timeTime(),
-                          "user_name": username,
-                          "description": description,
+                          "user_name": username or '',
+                          "description": description or '',
                           "tid": tid,
                           "version": '',
                           "size": length,
@@ -666,7 +701,10 @@
         # The tests depend on this.
         self._rollback_load_connection()
 
+    def iterator(self, start=None, stop=None):
+        return TransactionIterator(self._adapter, start, stop)
 
+
 class BoundRelStorage(RelStorage):
     """Storage to a database, bound to a particular ZODB.Connection."""
 
@@ -769,3 +807,93 @@
         # wants to see the new state, it should call sync().
         pass
 
+
+class TransactionIterator(object):
+    """Iterate over the transactions in a RelStorage instance."""
+
+    def __init__(self, adapter, start, stop):
+        self._adapter = adapter
+        self._conn, self._cursor = self._adapter.open_for_load()
+        self._closed = False
+
+        if start is not None:
+            start_int = u64(start)
+        else:
+            start_int = 1
+        if stop is not None:
+            stop_int = u64(stop)
+        else:
+            stop_int = None
+
+        # _transactions: [(tid, packed, username, description, extension)]
+        self._transactions = list(adapter.iter_transactions_range(
+            self._cursor, start_int, stop_int))
+        self._index = 0
+
+    def close(self):
+        self._adapter.close(self._conn, self._cursor)
+        self._closed = True
+
+    def iterator(self):
+        return self
+
+    def __len__(self):
+        return len(self._transactions)
+
+    def __getitem__(self, n):
+        self._index = n
+        return self.next()
+
+    def next(self):
+        if self._closed:
+            raise IOError("TransactionIterator already closed")
+        params = self._transactions[self._index]
+        res = RecordIterator(self, *params)
+        self._index += 1
+        return res
+
+
+class RecordIterator(object):
+    """Iterate over the objects in a transaction."""
+    def __init__(self, trans_iter, tid_int, packed, user, desc, ext):
+        self.tid = p64(tid_int)
+        self.status = packed and 'p' or ' '
+        self.user = user or ''
+        self.description = desc or ''
+        if ext:
+            self._extension = cPickle.loads(ext)
+        else:
+            self._extension = {}
+
+        cursor = trans_iter._cursor
+        adapter = trans_iter._adapter
+        self._records = list(adapter.iter_objects(cursor, tid_int))
+        self._index = 0
+
+    def __len__(self):
+        return len(self._records)
+
+    def __getitem__(self, n):
+        self._index = n
+        return self.next()
+
+    def next(self):
+        params = self._records[self._index]
+        res = Record(self.tid, *params)
+        self._index += 1
+        return res
+
+
+class Record(object):
+    """An object state in a transaction"""
+    version = ''
+    data_txn = None
+
+    def __init__(self, tid, oid_int, data):
+        self.tid = tid
+        self.oid = p64(oid_int)
+        if data is not None:
+            self.data = str(data)
+        else:
+            self.data = None
+

Modified: relstorage/trunk/relstorage/tests/reltestbase.py
===================================================================
--- relstorage/trunk/relstorage/tests/reltestbase.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/tests/reltestbase.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -17,15 +17,18 @@
 from relstorage.relstorage import RelStorage
 
 from ZODB.DB import DB
+from ZODB.utils import p64
+from ZODB.FileStorage import FileStorage
 from persistent.mapping import PersistentMapping
 import transaction
 
 from ZODB.tests import StorageTestBase, BasicStorage, \
      TransactionalUndoStorage, PackableStorage, \
      Synchronization, ConflictResolution, HistoryStorage, \
-     RevisionStorage, PersistentStorage, \
-     MTStorage, ReadOnlyStorage
+     IteratorStorage, RevisionStorage, PersistentStorage, \
+     MTStorage, ReadOnlyStorage, RecoveryStorage
 
+from ZODB.tests.MinPO import MinPO
 from ZODB.tests.StorageTestBase import zodb_unpickle, zodb_pickle
 
 
@@ -58,6 +61,8 @@
     Synchronization.SynchronizedStorage,
     ConflictResolution.ConflictResolvingStorage,
     HistoryStorage.HistoryStorage,
+    IteratorStorage.IteratorStorage,
+    IteratorStorage.ExtendedIteratorStorage,
     PersistentStorage.PersistentStorage,
     MTStorage.MTStorage,
     ReadOnlyStorage.ReadOnlyStorage
@@ -260,3 +265,178 @@
 
         finally:
             db.close()
+
+
+    def checkTransactionalUndoIterator(self):
+        # this test overrides the broken version in TransactionalUndoStorage.
+
+        s = self._storage
+
+        BATCHES = 4
+        OBJECTS = 4
+
+        orig = []
+        for i in range(BATCHES):
+            t = transaction.Transaction()
+            tid = p64(i + 1)
+            s.tpc_begin(t, tid)
+            txn_orig = []
+            for j in range(OBJECTS):
+                oid = s.new_oid()
+                obj = MinPO(i * OBJECTS + j)
+                revid = s.store(oid, None, zodb_pickle(obj), '', t)
+                txn_orig.append((tid, oid, revid))
+            serials = s.tpc_vote(t)
+            if not serials:
+                orig.extend(txn_orig)
+            else:
+                # The storage provided revision IDs after the vote
+                serials = dict(serials)
+                for tid, oid, revid in txn_orig:
+                    self.assertEqual(revid, None)
+                    orig.append((tid, oid, serials[oid]))
+            s.tpc_finish(t)
+
+        i = 0
+        for tid, oid, revid in orig:
+            self._dostore(oid, revid=revid, data=MinPO(revid),
+                          description="update %s" % i)
+
+        # Undo the OBJECTS transactions that modified objects created
+        # in the ith original transaction.
+
+        def undo(i):
+            info = s.undoInfo()
+            t = transaction.Transaction()
+            s.tpc_begin(t)
+            base = i * OBJECTS + i
+            for j in range(OBJECTS):
+                tid = info[base + j]['id']
+                s.undo(tid, t)
+            s.tpc_vote(t)
+            s.tpc_finish(t)
+
+        for i in range(BATCHES):
+            undo(i)
+
+        # There are now (2 + OBJECTS) * BATCHES transactions:
+        #     BATCHES original transactions, followed by
+        #     OBJECTS * BATCHES modifications, followed by
+        #     BATCHES undos
+
+        iter = s.iterator()
+        offset = 0
+
+        eq = self.assertEqual
+
+        for i in range(BATCHES):
+            txn = iter[offset]
+            offset += 1
+
+            tid = p64(i + 1)
+            eq(txn.tid, tid)
+
+            L1 = [(rec.oid, rec.tid, rec.data_txn) for rec in txn]
+            L2 = [(oid, revid, None) for _tid, oid, revid in orig
+                  if _tid == tid]
+
+            eq(L1, L2)
+
+        for i in range(BATCHES * OBJECTS):
+            txn = iter[offset]
+            offset += 1
+            eq(len([rec for rec in txn if rec.data_txn is None]), 1)
+
+        for i in range(BATCHES):
+            txn = iter[offset]
+            offset += 1
+
+            # The undos are performed in reverse order.
+            otid = p64(BATCHES - i)
+            L1 = [rec.oid for rec in txn]
+            L2 = [oid for _tid, oid, revid in orig if _tid == otid]
+            L1.sort()
+            L2.sort()
+            eq(L1, L2)
+
+        self.assertRaises(IndexError, iter.__getitem__, offset)
+
+
+class IteratorDeepCompareUnordered:
+    # Like IteratorDeepCompare, but compensates for OID order
+    # differences in transactions.
+    def compare(self, storage1, storage2):
+        eq = self.assertEqual
+        iter1 = storage1.iterator()
+        iter2 = storage2.iterator()
+        for txn1, txn2 in zip(iter1, iter2):
+            eq(txn1.tid,         txn2.tid)
+            eq(txn1.status,      txn2.status)
+            eq(txn1.user,        txn2.user)
+            eq(txn1.description, txn2.description)
+            eq(txn1._extension,  txn2._extension)
+            recs1 = [(r.oid, r) for r in txn1]
+            recs1.sort()
+            recs2 = [(r.oid, r) for r in txn2]
+            recs2.sort()
+            for (oid1, rec1), (oid2, rec2) in zip(recs1, recs2):
+                eq(rec1.oid,     rec2.oid)
+                eq(rec1.tid,  rec2.tid)
+                eq(rec1.version, rec2.version)
+                eq(rec1.data,    rec2.data)
+            # Make sure there are no more records left in rec1 and rec2,
+            # meaning they were the same length.
+            self.assertRaises(IndexError, txn1.next)
+            self.assertRaises(IndexError, txn2.next)
+        # Make sure ther are no more records left in txn1 and txn2, meaning
+        # they were the same length
+        self.assertRaises(IndexError, iter1.next)
+        self.assertRaises(IndexError, iter2.next)
+        iter1.close()
+        iter2.close()
+
+
+
+class RecoveryStorageSubset(IteratorDeepCompareUnordered):
+    # The subset of RecoveryStorage tests that do not rely on version
+    # support.
+    pass
+
+for name, attr in RecoveryStorage.RecoveryStorage.__dict__.items():
+    if 'check' in name and 'Version' not in name:
+        setattr(RecoveryStorageSubset, name, attr)
+
+
+class ToFileStorage(BaseRelStorageTests, RecoveryStorageSubset):
+    def setUp(self):
+        self.open(create=1)
+        self._storage._zap()
+        self._dst = FileStorage("Dest.fs", create=True)
+
+    def tearDown(self):
+        self._storage.close()
+        self._dst.close()
+        self._storage.cleanup()
+        self._dst.cleanup()
+
+    def new_dest(self):
+        return FileStorage('Dest.fs')
+
+
+class FromFileStorage(BaseRelStorageTests, RecoveryStorageSubset):
+    def setUp(self):
+        self.open(create=1)
+        self._storage._zap()
+        self._dst = self._storage
+        self._storage = FileStorage("Source.fs", create=True)
+
+    def tearDown(self):
+        self._storage.close()
+        self._dst.close()
+        self._storage.cleanup()
+        self._dst.cleanup()
+
+    def new_dest(self):
+        return self._dst
+
+

Modified: relstorage/trunk/relstorage/tests/testmysql.py
===================================================================
--- relstorage/trunk/relstorage/tests/testmysql.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/tests/testmysql.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -16,17 +16,28 @@
 import logging
 import unittest
 
-from reltestbase import RelStorageTests
+import reltestbase
 from relstorage.adapters.mysql import MySQLAdapter
 
 
-class MySQLTests(RelStorageTests):
+class UseMySQLAdapter:
     def make_adapter(self):
         return MySQLAdapter(db='relstoragetest')
 
+class MySQLTests(UseMySQLAdapter, reltestbase.RelStorageTests):
+    pass
+
+class MySQLToFile(UseMySQLAdapter, reltestbase.ToFileStorage):
+    pass
+
+class FileToMySQL(UseMySQLAdapter, reltestbase.FromFileStorage):
+    pass
+
+
 def test_suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(MySQLTests, "check"))
+    for klass in [MySQLTests, MySQLToFile, FileToMySQL]:
+        suite.addTest(unittest.makeSuite(klass, "check"))
     return suite
 
 if __name__=='__main__':

Modified: relstorage/trunk/relstorage/tests/testoracle.py
===================================================================
--- relstorage/trunk/relstorage/tests/testoracle.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/tests/testoracle.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -18,7 +18,7 @@
 import re
 import unittest
 
-from reltestbase import RelStorageTests
+import reltestbase
 from relstorage.adapters.oracle import OracleAdapter
 
 
@@ -38,17 +38,27 @@
     return user, password, dsn
 
 
-class OracleTests(RelStorageTests):
+class UseOracleAdapter:
     def make_adapter(self):
         user, password, dsn = getOracleParams()
         return OracleAdapter(user, password, dsn)
 
+class OracleTests(UseOracleAdapter, reltestbase.RelStorageTests):
+    pass
+
+class OracleToFile(UseOracleAdapter, reltestbase.ToFileStorage):
+    pass
+
+class FileToOracle(UseOracleAdapter, reltestbase.FromFileStorage):
+    pass
+
+
 def test_suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(OracleTests, "check"))
+    for klass in [OracleTests, OracleToFile, FileToOracle]:
+        suite.addTest(unittest.makeSuite(klass, "check"))
     return suite
 
 if __name__=='__main__':
     logging.basicConfig()
     unittest.main(defaultTest="test_suite")
-

Modified: relstorage/trunk/relstorage/tests/testpostgresql.py
===================================================================
--- relstorage/trunk/relstorage/tests/testpostgresql.py	2008-02-17 02:05:07 UTC (rev 83972)
+++ relstorage/trunk/relstorage/tests/testpostgresql.py	2008-02-17 06:49:05 UTC (rev 83973)
@@ -16,17 +16,28 @@
 import logging
 import unittest
 
-from reltestbase import RelStorageTests
+import reltestbase
 from relstorage.adapters.postgresql import PostgreSQLAdapter
 
 
-class PostgreSQLTests(RelStorageTests):
+class UsePostgreSQLAdapter:
     def make_adapter(self):
         return PostgreSQLAdapter('dbname=relstoragetest')
 
+class PostgreSQLTests(UsePostgreSQLAdapter, reltestbase.RelStorageTests):
+    pass
+
+class PGToFile(UsePostgreSQLAdapter, reltestbase.ToFileStorage):
+    pass
+
+class FileToPG(UsePostgreSQLAdapter, reltestbase.FromFileStorage):
+    pass
+
+
 def test_suite():
     suite = unittest.TestSuite()
-    suite.addTest(unittest.makeSuite(PostgreSQLTests, "check"))
+    for klass in [PostgreSQLTests, PGToFile, FileToPG]:
+        suite.addTest(unittest.makeSuite(klass, "check"))
     return suite
 
 if __name__=='__main__':



More information about the Checkins mailing list