[Checkins] SVN: zc.zodbdgc/branches/dev/ Initial version. Tests seem to be sufficient and pass.

Jim Fulton jim at zope.com
Mon May 11 17:38:04 EDT 2009


Log message for revision 99857:
  Initial version. Tests seem to be sufficient and pass.
  Requires non-released file-storage packing fixes.
  

Changed:
  _U  zc.zodbdgc/branches/dev/
  U   zc.zodbdgc/branches/dev/buildout.cfg
  U   zc.zodbdgc/branches/dev/setup.py
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.test
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.txt
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/__init__.py
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/oidset.test
  A   zc.zodbdgc/branches/dev/src/zc/zodbdgc/tests.py

-=-

Property changes on: zc.zodbdgc/branches/dev
___________________________________________________________________
Added: svn:externals
   + zodb svn+ssh://svn.zope.org/repos/main/ZODB/trunk


Modified: zc.zodbdgc/branches/dev/buildout.cfg
===================================================================
--- zc.zodbdgc/branches/dev/buildout.cfg	2009-05-11 21:08:05 UTC (rev 99856)
+++ zc.zodbdgc/branches/dev/buildout.cfg	2009-05-11 21:38:04 UTC (rev 99857)
@@ -1,10 +1,10 @@
 [buildout]
-develop = .
+develop = . zodb
 parts = test py
 
 [test]
 recipe = zc.recipe.testrunner
-eggs = 
+eggs = zc.zodbdgc
 
 [py]
 recipe = zc.recipe.egg

Modified: zc.zodbdgc/branches/dev/setup.py
===================================================================
--- zc.zodbdgc/branches/dev/setup.py	2009-05-11 21:08:05 UTC (rev 99856)
+++ zc.zodbdgc/branches/dev/setup.py	2009-05-11 21:38:04 UTC (rev 99857)
@@ -12,7 +12,7 @@
 #
 ##############################################################################
 
-name, version = '', '0'
+name, version = 'zc.zodbdgc', '0'
 
 import os
 from setuptools import setup, find_packages
@@ -36,14 +36,14 @@
     version = version,
     author = 'Jim Fulton',
     author_email = 'jim at zope.com',
-    description = '',
+    description = 'ZODB Distributed Garbage Collection',
     long_description=long_description,
     license = 'ZPL 2.1',
-    
+
     packages = find_packages('src'),
     namespace_packages = ['zc'],
     package_dir = {'': 'src'},
-    install_requires = ['setuptools'],
+    install_requires = ['setuptools', 'ZODB3'],
     zip_safe = False,
     entry_points=entry_points,
     include_package_data = True,

Added: zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.test
===================================================================
--- zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.test	                        (rev 0)
+++ zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.test	2009-05-11 21:38:04 UTC (rev 99857)
@@ -0,0 +1,203 @@
+ZODB Distributed GC
+===================
+
+This package provides a script for performing distributed garbage
+collection for a collection of ZODB storages, which will typically be
+ZEO clients.
+
+Here, we'll test the underlying script.
+
+We'll need to control time.
+
+    >>> now = 1241458549.614022
+    >>> def faux_time():
+    ...     global now
+    ...     now += 1
+    ...     return now
+    >>> import time
+    >>> time_time = time.time
+    >>> time.time = faux_time
+
+Let's define some storages::
+
+    >>> open('config', 'w').write("""
+    ... <zodb db1>
+    ...     <filestorage>
+    ...         pack-gc false
+    ...         path 1.fs
+    ...     </filestorage>
+    ... </zodb>
+    ... <zodb db2>
+    ...     <filestorage>
+    ...         pack-gc false
+    ...         path 2.fs
+    ...     </filestorage>
+    ... </zodb>
+    ... <zodb db3>
+    ...     <filestorage>
+    ...         pack-gc false
+    ...         path 3.fs
+    ...     </filestorage>
+    ... </zodb>
+    ... """)
+
+    >>> import ZODB.config, transaction
+    >>> db = ZODB.config.databaseFromFile(open('config'))
+
+And perform some updates:
+
+    >>> conn1 = db.open()
+    >>> conn2 = conn1.get_connection('db2')
+    >>> conn3 = conn1.get_connection('db3')
+
+    >>> import persistent.mapping
+    >>> C = persistent.mapping.PersistentMapping
+
+    >>> conn1.root.x = C()
+    >>> conn2.root.x = C()
+    >>> conn3.root.x = C()
+    >>> transaction.commit()
+    >>> conn2.root.y = C()
+    >>> conn3.root.x = C()
+    >>> transaction.commit()
+    >>> conn3.root.z = C()
+    >>> transaction.commit()
+
+    >>> conn1.root.x.y = conn2.root.y
+    >>> del conn2.root.y
+    >>> conn1.root.x.y.z = conn3.root.z
+    >>> del conn3.root.z
+
+In db loops:
+
+    >>> conn1.root.a = C()
+    >>> transaction.commit()
+    >>> conn1.root.b = C()
+    >>> transaction.commit()
+    >>> conn1.root.a.b = conn1.root.b
+    >>> conn1.root.b.a = conn1.root.a
+
+cross db loops
+
+    >>> conn2.root.x.x = conn3.root.x
+    >>> conn3.root.x.x = conn2.root.x
+
+    >>> transaction.commit()
+
+
+No garbage yet, because everything's reachable.
+
+    >>> from ZODB.utils import u64, p64
+    >>> print u64(conn1.root.a._p_oid), u64(conn1.root.b._p_oid)
+    2 3
+    >>> print u64(conn2.root.x._p_oid), u64(conn3.root.x._p_oid)
+    1 2
+    >>> del conn1.root.a
+    >>> del conn1.root.b
+    >>> del conn2.root.x
+    >>> del conn3.root.x
+
+    >>> transaction.commit()
+
+The objects we just deleted are now garbage.
+
+Time passes. :)
+
+    >>> now += 7 * 86400        # 7 days
+
+We'll create some more garbage:
+
+    >>> conn2.root.a = C()
+    >>> transaction.commit()
+    >>> conn2.root.b = C()
+    >>> transaction.commit()
+    >>> conn2.root.a.b = conn2.root.b
+    >>> conn2.root.b.a = conn2.root.a
+
+    >>> transaction.commit()
+
+    >>> print u64(conn2.root.a._p_oid), u64(conn2.root.b._p_oid)
+    3 4
+    >>> del conn2.root.a
+    >>> del conn2.root.b
+
+More time passes.
+
+    >>> now += 1
+
+The number of objecs in the databases now:
+
+    >>> len(conn1._storage), len(conn2._storage), len(conn3._storage)
+    (4, 5, 4)
+
+    >>> for d in db.databases.values():
+    ...     d.pack()
+
+Packing doesn't change it:
+
+    >>> len(conn1._storage), len(conn2._storage), len(conn3._storage)
+    (4, 5, 4)
+
+    >>> _ = conn1._storage.load(p64(2))
+    >>> _ = conn1._storage.load(p64(3))
+    >>> _ = conn2._storage.load(p64(1))
+    >>> _ = conn3._storage.load(p64(1))
+    >>> _ = conn3._storage.load(p64(2))
+
+    >>> _ = [d.close() for d in db.databases.values()]
+
+Now let's perform gc.
+
+    >>> import zc.zodbdgc
+    >>> bad = zc.zodbdgc.gc('config', days=2)
+
+    >>> for name, oid in sorted(bad.iterator()):
+    ...     print name, u64(oid)
+    db1 2
+    db1 3
+    db2 1
+    db3 1
+    db3 2
+
+    >>> db = ZODB.config.databaseFromFile(open('config'))
+    >>> conn1 = db.open()
+    >>> conn2 = conn1.get_connection('db2')
+    >>> conn3 = conn1.get_connection('db3')
+
+Note that we still have the same number of objects, because we
+haven't packed yet.
+
+    >>> len(conn1._storage), len(conn2._storage), len(conn3._storage)
+    (4, 5, 4)
+
+    >>> now += 1
+
+    >>> for d in db.databases.values():
+    ...     d.pack()
+
+    >>> _ = [d.close() for d in db.databases.values()]
+    >>> db = ZODB.config.databaseFromFile(open('config'))
+    >>> conn1 = db.open()
+    >>> conn2 = conn1.get_connection('db2')
+    >>> conn3 = conn1.get_connection('db3')
+
+    >>> len(conn1._storage), len(conn2._storage), len(conn3._storage)
+    (2, 4, 2)
+
+    >>> import ZODB.POSException
+    >>> for name, oid in bad.iterator():
+    ...     try:
+    ...         conn1.get_connection(name)._storage.load(oid)
+    ...     except ZODB.POSException.POSKeyError:
+    ...         pass
+    ...     else:
+    ...         print 'waaa', name, u64(oid)
+
+Make sure we have no broken refs:
+
+    >>> _ = [d.close() for d in db.databases.values()]
+    >>> zc.zodbdgc.check('config')
+
+.. cleanup
+
+    >>> time.time = time_time


Property changes on: zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.txt
===================================================================
--- zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.txt	                        (rev 0)
+++ zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.txt	2009-05-11 21:38:04 UTC (rev 99857)
@@ -0,0 +1,31 @@
+ZODB Distributed GC
+===================
+
+This package provides a script for performing distributed garbage
+collection for a collection of ZODB storages, which will typically be
+ZEO clients.
+
+Note that this script will likely be included in future ZODB
+releases. It's being developed independently now because it is new and
+we don't want to be limited by or to affect the ZODB release cycle.
+
+The script takes the fillowing options:
+
+-d n, --days n
+
+   Provide the number of days in the past to garbage collect to.  And
+   objects written after than number of days will be considered to be
+   non garbage.  This defaults to 3.
+
+-s config, --storage config
+
+   The name of a configuration file defining storages to be garbage
+   collected.
+
+-a config, --analyze config
+
+   The name of a configuration file defining storage servers to use
+   for analysis.  This is useful with replicated storages, as it
+   allows analysis to take place using stprage servers that are under
+   lighter load.  If not provided, then the storages specified using
+   the --storage option are used for analysis.


Property changes on: zc.zodbdgc/branches/dev/src/zc/zodbdgc/README.txt
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.zodbdgc/branches/dev/src/zc/zodbdgc/__init__.py
===================================================================
--- zc.zodbdgc/branches/dev/src/zc/zodbdgc/__init__.py	                        (rev 0)
+++ zc.zodbdgc/branches/dev/src/zc/zodbdgc/__init__.py	2009-05-11 21:38:04 UTC (rev 99857)
@@ -0,0 +1,227 @@
+##############################################################################
+#
+# Copyright (c) Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+from ZODB.utils import u64, z64, p64
+import BTrees.IIBTree
+import cPickle
+import cStringIO
+import logging
+import marshal
+import os
+import shutil
+import tempfile
+import time
+import transaction
+import ZODB.config
+import ZODB.TimeStamp
+
+logger = logging.getLogger(__name__)
+
+def gc(conf, days=1, conf2=None):
+    db = ZODB.config.databaseFromFile(open(conf))
+    if conf2 is None:
+        db2 = db
+    else:
+        db2 = ZODB.config.databaseFromFile(open(conf2))
+        if set(db.databases) != set(db2.databases):
+            raise ValueError("primary and secondary databases don't match.")
+
+    databases = db2.databases
+    storages = dict((name, d.storage) for (name, d) in databases.items())
+
+    ptid = repr(
+        ZODB.TimeStamp.TimeStamp(*time.gmtime(time.time() - 86400*days)[:6])
+        )
+
+    # Pre-populate good with roots and recently-written objects
+    good = oidset(databases)
+    bad = oidset(databases)
+    baddir = tempfile.mkdtemp()
+    for name in storages:
+        os.mkdir(os.path.join(baddir, name))
+
+    for name, storage in storages.iteritems():
+        # Make sure we can get the roots
+        _ = storage.load(z64, '')
+        good.insert(name, z64)
+
+        # All new records are good
+        for trans in storage.iterator(ptid):
+            for record in trans:
+                good.insert(name, record.oid)
+                # and anything they reference
+                for ref in getrefs(record.data, name):
+                    good.insert(*ref)
+
+        # Now iterate over older records
+        for trans in storage.iterator(None, ptid):
+            for record in trans:
+                oid = record.oid
+                data = record.data
+                if good.has(name, oid):
+                    if not data:
+                        continue
+                    for ref in getrefs(data, name):
+                        if good.insert(*ref) and bad.has(*ref):
+                            bad_to_good(baddir, bad, good, *ref)
+                else:
+                    bad.insert(name, oid)
+                    if not data:
+                        continue
+                    refs = tuple(ref for ref in getrefs(data, name)
+                                 if (not good.has(*ref)) and not bad.has(*ref))
+                    if not refs:
+                        continue    # leaves are common
+                    f = open(os.path.join(baddir, name,
+                                          oid.encode('base64').strip()),
+                             'ab')
+                    marshal.dump(refs, f)
+                    f.close()
+
+    # Now, we have the garbage in bad.  Remove it.
+    for name, db in db.databases.iteritems():
+        storage = db.storage
+        t = transaction.begin()
+        storage.tpc_begin(t)
+        nd = 0
+        for oid in bad.iterator(name):
+            p, s = storage.load(oid, '')
+            storage.deleteObject(oid, s, t)
+            nd += 1
+        logger.info("Removed %s objects from %s", nd, name)
+        if nd:
+            storage.tpc_vote(t)
+            storage.tpc_finish(t)
+            transaction.commit()
+        else:
+            storage.tpc_abort(t)
+            transaction.abort()
+        db.close()
+
+    shutil.rmtree(baddir)
+
+    return bad
+
+def bad_to_good(baddir, bad, good, name, oid):
+    bad.remove(name, oid)
+
+    path = os.path.join(baddir, name, oid.encode('base64').strip())
+    if not os.path.exists(path):
+        return
+
+    f = open(path , 'rb')
+    while 1:
+        try:
+            refs = marshal.load(f)
+        except EOFError:
+            break
+
+        for ref in refs:
+            if good.insert(*ref) and bad.has(*ref):
+                bad_to_good(baddir, bad, good, *ref)
+
+    f.close()
+    os.remove(path)
+
+def getrefs(p, rname):
+    refs = []
+    u = cPickle.Unpickler(cStringIO.StringIO(p))
+    u.persistent_load = refs
+    u.noload()
+    u.noload()
+    for ref in refs:
+        name = rname
+        if isinstance(ref, tuple):
+            yield rname, ref[0]
+        elif isinstance(ref, str):
+            yield rname, ref
+        else:
+            assert isinstance(ref, list)
+            yield ref[1][:2]
+
+class oidset(dict):
+
+    def __init__(self, names):
+        for name in names:
+            self[name] = {}
+
+    def insert(self, name, oid):
+        ioid1, ioid2 = divmod(u64(oid), 2147483648L)
+        ioid2 = int(ioid2)
+        data = self[name].get(ioid1)
+        if data is None:
+            data = self[name][ioid1] = BTrees.IIBTree.TreeSet()
+        elif ioid2 in data:
+            return False
+        data.insert(ioid2)
+        return True
+
+    def remove(self, name, oid):
+        ioid1, ioid2 = divmod(u64(oid), 2147483648L)
+        ioid2 = int(ioid2)
+        data = self[name].get(ioid1)
+        if data and ioid2 in data:
+            data.remove(ioid2)
+            if not data:
+                del self[name][ioid1]
+
+    def has(self, name, oid):
+        ioid1, ioid2 = divmod(u64(oid), 2147483648L)
+        data = self[name].get(ioid1)
+        return bool(data and (int(ioid2) in data))
+
+    def iterator(self, name=None):
+        if name is None:
+            for name in self:
+                for oid in self.iterator(name):
+                    yield name, oid
+        else:
+            for ioid1, data in self[name].iteritems():
+                ioid1 *= 2147483648L
+                for ioid2 in data:
+                    yield p64(ioid1+ioid2)
+
+def check(config):
+    db = ZODB.config.databaseFromFile(open(config))
+    databases = db.databases
+    storages = dict((name, db.storage) for (name, db) in databases.iteritems())
+    roots = set((name, z64) for name in databases)
+    referers = {}
+    seen = oidset(databases)
+    while roots:
+        name, oid = roots.pop()
+        if not seen.insert(name, oid):
+            continue
+
+        try:
+            p, tid = storages[name].load(oid, '')
+        except:
+            print '!!!', name, u64(oid),
+            referer = referers.pop((name, oid), None)
+            if referer:
+                rname, roid = referer
+                print rname, u64(roid)
+            else:
+                print '?'
+            continue
+
+        referers.pop((name, oid), None)
+
+        for ref in getrefs(p, name):
+            if seen.has(*ref):
+                continue
+            if ref in roots:
+                continue
+            roots.add(ref)
+            referers[ref] = name, oid


Property changes on: zc.zodbdgc/branches/dev/src/zc/zodbdgc/__init__.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: zc.zodbdgc/branches/dev/src/zc/zodbdgc/oidset.test
===================================================================
--- zc.zodbdgc/branches/dev/src/zc/zodbdgc/oidset.test	                        (rev 0)
+++ zc.zodbdgc/branches/dev/src/zc/zodbdgc/oidset.test	2009-05-11 21:38:04 UTC (rev 99857)
@@ -0,0 +1,208 @@
+The zc.zodbdgc module uses an oidset class to keep track of sets of
+name/oid pairs efficiently.
+
+    >>> import zc.zodbdgc
+    >>> oids = zc.zodbdgc.oidset(('foo', 'bar', 'baz'))
+
+    >>> from ZODB.utils import p64, u64
+
+    >>> oids.has('foo', p64(0))
+    False
+
+    >>> sorted(oids.iterator())
+    []
+
+    >>> oids.insert('foo', p64(0))
+    True
+    >>> oids.has('foo', p64(0))
+    True
+    >>> oids.has('bar', p64(0))
+    False
+    >>> oids.has('foo', p64(1))
+    False
+
+    >>> oids.has('foo', p64(1<<31))
+    False
+    >>> oids.has('foo', p64((1<<31)+1))
+    False
+    >>> oids.has('foo', p64((1<<31)-1))
+    False
+
+    >>> oids.insert('foo', p64(1<<31))
+    True
+    >>> oids.has('foo', p64(1<<31))
+    True
+    >>> oids.has('foo', p64((1<<31)+1))
+    False
+    >>> oids.has('foo', p64((1<<31)-1))
+    False
+
+    >>> oids.insert('foo', p64((1<<31)+1))
+    True
+    >>> oids.has('foo', p64(1<<31))
+    True
+    >>> oids.has('foo', p64((1<<31)+1))
+    True
+    >>> oids.has('foo', p64((1<<31)-1))
+    False
+
+    >>> oids.insert('foo', p64((1<<31)-1))
+    True
+    >>> oids.has('foo', p64(1<<31))
+    True
+    >>> oids.has('foo', p64((1<<31)+1))
+    True
+    >>> oids.has('foo', p64((1<<31)-1))
+    True
+
+    >>> oids.has('foo', p64((1<<32)))
+    False
+    >>> oids.has('foo', p64((1<<34)))
+    False
+    >>> oids.has('foo', p64((1<<35)))
+    False
+
+    >>> oids.insert('foo', p64((1<<32)))
+    True
+    >>> oids.insert('foo', p64((1<<34)))
+    True
+    >>> oids.insert('foo', p64((1<<35)))
+    True
+
+    >>> oids.has('foo', p64((1<<32)))
+    True
+    >>> oids.has('foo', p64((1<<34)))
+    True
+    >>> oids.has('foo', p64((1<<35)))
+    True
+
+    >>> oids.insert('foo', p64((1<<32)))
+    False
+    >>> oids.insert('foo', p64((1<<34)))
+    False
+    >>> oids.insert('foo', p64((1<<35)))
+    False
+    >>> oids.insert('foo', p64(1<<31))
+    False
+    >>> oids.insert('foo', p64((1<<31)+1))
+    False
+    >>> oids.insert('foo', p64((1<<31)-1))
+    False
+
+    >>> import pprint
+
+    >>> pprint.pprint(
+    ...     sorted((name, u64(oid)) for (name, oid) in oids.iterator()),
+    ...     width=1)
+    [('foo',
+      0L),
+     ('foo',
+      2147483647L),
+     ('foo',
+      2147483648L),
+     ('foo',
+      2147483649L),
+     ('foo',
+      4294967296L),
+     ('foo',
+      17179869184L),
+     ('foo',
+      34359738368L)]
+
+    >>> pprint.pprint(
+    ...     sorted(u64(oid) for oid in oids.iterator('foo')),
+    ...     width=1)
+    [0L,
+     2147483647L,
+     2147483648L,
+     2147483649L,
+     4294967296L,
+     17179869184L,
+     34359738368L]
+
+    >>> for oid in oids.iterator('foo'):
+    ...     if not oids.insert('bar', oid):
+    ...         print `oid`
+
+    >>> sorted(oids.iterator('foo')) == sorted(oids.iterator('bar'))
+    True
+
+    >>> pprint.pprint(
+    ...     sorted((name, u64(oid)) for (name, oid) in oids.iterator()),
+    ...     width=1)
+    [('bar',
+      0L),
+     ('bar',
+      2147483647L),
+     ('bar',
+      2147483648L),
+     ('bar',
+      2147483649L),
+     ('bar',
+      4294967296L),
+     ('bar',
+      17179869184L),
+     ('bar',
+      34359738368L),
+     ('foo',
+      0L),
+     ('foo',
+      2147483647L),
+     ('foo',
+      2147483648L),
+     ('foo',
+      2147483649L),
+     ('foo',
+      4294967296L),
+     ('foo',
+      17179869184L),
+     ('foo',
+      34359738368L)]
+
+    >>> oids.remove('foo', p64(1<<31))
+    >>> oids.remove('foo', p64((1<<31)+1))
+    >>> oids.remove('foo', p64((1<<31)-1))
+
+    >>> pprint.pprint(
+    ...     sorted(u64(oid) for oid in oids.iterator('foo')),
+    ...     width=1)
+    [0L,
+     4294967296L,
+     17179869184L,
+     34359738368L]
+
+    >>> import random
+    >>> r = random.Random()
+    >>> r.seed(0)
+
+    >>> generated_oids = list(oids.iterator())
+    >>> sorted(generated_oids) == sorted(oids.iterator())
+    True
+
+    >>> for i in range(1000):
+    ...     name = r.choice(('foo', 'bar'))
+    ...     oid = p64(r.randint(0, 1<<32))
+    ...     if (name, oid) in generated_oids:
+    ...         print 'dup', (name, oid)
+    ...         if oids.insert(name, oid):
+    ...            print 'wth dup', name, `oid`
+    ...     else:
+    ...         if not oids.insert(name, oid):
+    ...             print 'wth', name, `oid`
+    ...         generated_oids.append((name, oid))
+
+    >>> sorted(generated_oids) == sorted(oids.iterator())
+    True
+
+    >>> for i in range(1500):
+    ...     action = r.choice('ri')
+    ...     choice = r.choice(generated_oids)
+    ...     if action == 'i':
+    ...         if oids.insert(*choice):
+    ...             print 'wth', choice
+    ...     else:
+    ...         generated_oids.remove(choice)
+    ...         oids.remove(*choice)
+
+    >>> sorted(generated_oids) == sorted(oids.iterator())
+    True


Property changes on: zc.zodbdgc/branches/dev/src/zc/zodbdgc/oidset.test
___________________________________________________________________
Added: svn:eol-style
   + native

Added: zc.zodbdgc/branches/dev/src/zc/zodbdgc/tests.py
===================================================================
--- zc.zodbdgc/branches/dev/src/zc/zodbdgc/tests.py	                        (rev 0)
+++ zc.zodbdgc/branches/dev/src/zc/zodbdgc/tests.py	2009-05-11 21:38:04 UTC (rev 99857)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""XXX short summary goes here.
+
+$Id$
+"""
+import unittest
+from zope.testing import doctest, setupstack
+
+def test_suite():
+    return unittest.TestSuite((
+        doctest.DocFileSuite(
+            'README.test', 'oidset.test',
+            setUp=setupstack.setUpDirectory, tearDown = setupstack.tearDown,
+            ),
+        ))
+


Property changes on: zc.zodbdgc/branches/dev/src/zc/zodbdgc/tests.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native



More information about the Checkins mailing list