[Zope-CVS] SVN: zversioning/trunk/src/versioning/tests/ modified test suite

Uwe Oestermeier uwe_oestermeier at iwm-kmrc.de
Sat Oct 9 14:21:06 EDT 2004


Log message for revision 27868:
  modified test suite


Changed:
  A   zversioning/trunk/src/versioning/tests/README.txt
  U   zversioning/trunk/src/versioning/tests/test_versioncontrol.py


-=-
Added: zversioning/trunk/src/versioning/tests/README.txt
===================================================================
--- zversioning/trunk/src/versioning/tests/README.txt	2004-10-09 17:48:14 UTC (rev 27867)
+++ zversioning/trunk/src/versioning/tests/README.txt	2004-10-09 18:21:06 UTC (rev 27868)
@@ -0,0 +1,732 @@
+Versioning
+==========
+
+
+We start by testing some of the existing infrastructure from zope.app.versioncontrol
+and try to apply the existing versioning to sample data. We take the sample
+folder tree:
+
+  >>> import zope.app.versioncontrol.interfaces
+  >>> from zope.app.versioncontrol.repository import declare_versioned
+  >>> from zope.app.tests.setup import buildSampleFolderTree
+  >>> folder = buildSampleFolderTree()
+  >>> len(folder.keys())
+  2
+  >>> declare_versioned(folder)
+  >>> zope.app.versioncontrol.interfaces.IVersioned.providedBy(folder)
+  True
+
+This package provides a framework for managing multiple versions of objects
+within a ZODB database.  The framework defines several interfaces that objects
+may provide to participate with the framework.  For an object to particpate in
+version control, it must provide `IVersionable`.  `IVersionable` is an
+interface that promises that there will be adapters to:
+
+- `INonVersionedData`, and
+
+- `IPhysicallyLocatable`.
+
+It also requires that instances support `IPersistent` and `IAnnotatable`.
+   
+Normally, these interfaces will be provided by adapters.  To simplify the
+example, we'll just create a class that already implements the required
+interfaces directly.  We need to be careful to avoid including the __name__
+and __parent__ attributes in state copies, so even a fairly simple
+implementation of INonVersionedData has to deal with these for objects that
+contain their own location information.
+
+  >>> import persistent
+  >>> import zope.interface
+  >>> import zope.app.annotation.attribute
+  >>> import zope.app.annotation.interfaces
+  >>> import zope.app.traversing.interfaces
+  >>> from zope.app.versioncontrol import interfaces
+  
+  >>> marker = object()
+
+  >>> class Sample(persistent.Persistent):
+  ...     zope.interface.implements(
+  ...         interfaces.IVersionable,
+  ...         interfaces.INonVersionedData,
+  ...         zope.app.annotation.interfaces.IAttributeAnnotatable,
+  ...         zope.app.traversing.interfaces.IPhysicallyLocatable,
+  ...         )
+  ...   
+  ...     # Methods defined by INonVersionedData
+  ...     # This is a trivial implementation; using INonVersionedData
+  ...     # is discussed later.
+  ...
+  ...     def listNonVersionedObjects(self):
+  ...         return ()
+  ...
+  ...     def removeNonVersionedData(self):
+  ...         if "__name__" in self.__dict__:
+  ...             del self.__name__
+  ...         if "__parent__" in self.__dict__:
+  ...             del self.__parent__
+  ...
+  ...     def getNonVersionedData(self):
+  ...         return (getattr(self, "__name__", marker),
+  ...                 getattr(self, "__parent__", marker))
+  ...
+  ...     def restoreNonVersionedData(self, data):
+  ...         name, parent = data
+  ...         if name is not marker:
+  ...             self.__name__ = name
+  ...         if parent is not marker:
+  ...             self.__parent__ = parent
+  ...
+  ...     # Method from IPhysicallyLocatable that is actually used:
+  ...     def getPath(self):
+  ...         return '/' + self.__name__
+
+  >>> from zope.app.tests import ztapi
+  >>> ztapi.provideAdapter(zope.app.annotation.interfaces.IAttributeAnnotatable,
+  ...                      zope.app.annotation.interfaces.IAnnotations,
+  ...                      zope.app.annotation.attribute.AttributeAnnotations)
+
+Now we need to create a database with an instance of our sample object to work
+with:
+
+  >>> from ZODB.tests import util
+  >>> db = util.DB()
+  >>> connection = db.open()
+  >>> root = connection.root()
+
+  >>> samp = Sample()
+  >>> samp.__name__ = "samp"
+  >>> root["samp"] = samp
+  >>> util.commit()
+
+Some basic queries may be asked of objects without using an instance of
+`IVersionControl`.  In particular, we can determine whether an object can be
+managed by version control by checking for the `IVersionable` interface:
+
+  >>> interfaces.IVersionable.providedBy(samp)
+  True
+  >>> interfaces.IVersionable.providedBy(42)
+  False
+
+We can also determine whether an object is actually under version
+control using the `IVersioned` interface:
+
+  >>> interfaces.IVersioned.providedBy(samp)
+  False
+  >>> interfaces.IVersioned.providedBy(42)
+  False
+
+Placing an object under version control requires an instance of an
+`IVersionControl` object.  This package provides an implementation of this
+interface on the `Repository` class (from
+`zope.app.versioncontrol.repository`).  Only the `IVersionControl` instance is
+responsible for providing version control operations; an instance should never
+be asked to perform operations directly.
+
+  >>> import zope.app.versioncontrol.repository
+  >>> import zope.interface.verify
+
+  >>> repository = zope.app.versioncontrol.repository.Repository()
+  >>> zope.interface.verify.verifyObject(
+  ...     interfaces.IVersionControl,
+  ...	  repository)
+  True
+
+In order to actually use version control, there must be an
+interaction.  This is needed to allow the framework to determine the
+user making changes.  Let's set up an interaction now. First we need a
+principal. For our purposes, a principal just needs to have an id:
+
+  >>> class FauxPrincipal:
+  ...    def __init__(self, id):
+  ...        self.id = id
+  >>> principal = FauxPrincipal('bob')
+
+Then we need to define an participation for the principal in the
+interaction:
+
+  >>> class FauxParticipation:
+  ...     interaction=None
+  ...     def __init__(self, principal):
+  ...         self.principal = principal
+  >>> participation = FauxParticipation(principal)
+
+Finally, we can create the interaction:
+
+  >>> import zope.security.management
+  >>> zope.security.management.newInteraction(participation)
+
+Now, let's put an object under version control and verify that we can
+determine that fact by checking against the interface:
+
+  >>> repository.applyVersionControl(samp)
+  >>> interfaces.IVersioned.providedBy(samp)
+  True
+  >>> util.commit()
+
+Once an object is under version control, it's possible to get an
+information object that provides some interesting bits of data:
+
+  >>> info = repository.getVersionInfo(samp)
+  >>> type(info.history_id)
+  <type 'str'>
+
+It's an error to ask for the version info for an object which isn't
+under revision control:
+
+  >>> samp2 = Sample()
+  >>> repository.getVersionInfo(samp2)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+  >>> repository.getVersionInfo(42)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+You can retrieve a version of an object using the `.history_id` and a
+version selector.  A version selector is a string that specifies which
+available version to return.  The value `mainline` tells the
+`IVersionControl` to return the most recent version on the main branch.
+
+  >>> ob = repository.getVersionOfResource(info.history_id, 'mainline')
+  >>> type(ob)
+  <class 'zope.app.versioncontrol.README.Sample'>
+  >>> ob is samp
+  False
+  >>> root["ob"] = ob
+  >>> ob.__name__ = "ob"
+  >>> ob_info = repository.getVersionInfo(ob)
+  >>> ob_info.history_id == info.history_id
+  True
+  >>> ob_info is info
+  False
+
+Once version control has been applied, the object can be "checked
+out", modified and "checked in" to create new versions.  For many
+applications, this parallels form-based changes to objects, but this
+is a matter of policy.
+
+Let's save some information about the current version of the object so
+we can see that it changes:
+
+  >>> orig_history_id = info.history_id
+  >>> orig_version_id = info.version_id
+
+Now, let's check out the object and add an attribute:
+
+  >>> repository.checkoutResource(ob)
+  >>> ob.value = 42
+  >>> repository.checkinResource(ob)
+  >>> util.commit()
+
+We can now compare information about the updated version with the
+original information:
+
+  >>> newinfo = repository.getVersionInfo(ob)
+  >>> newinfo.history_id == orig_history_id
+  True
+  >>> newinfo.version_id != orig_version_id
+  True
+
+Retrieving both versions of the object allows use to see the
+differences between the two:
+
+  >>> o1 = repository.getVersionOfResource(orig_history_id,
+  ...                                      orig_version_id)
+  >>> o2 = repository.getVersionOfResource(orig_history_id,
+  ...                                      newinfo.version_id)
+  >>> o1.value
+  Traceback (most recent call last):
+    ...
+  AttributeError: 'Sample' object has no attribute 'value'
+  >>> o2.value
+  42
+
+We can determine whether an object that's been checked out is
+up-to-date with the most recent version from the repository:
+
+  >>> repository.isResourceUpToDate(o1)
+  False
+  >>> repository.isResourceUpToDate(o2)
+  True
+
+Asking whether a non-versioned object is up-to-date produces an error:
+
+  >>> repository.isResourceUpToDate(42)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+  >>> repository.isResourceUpToDate(samp2)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+It's also possible to check whether an object has been changed since
+it was checked out.  Since we're only looking at changes that have
+been committed to the database, we'll start by making a change and
+committing it without checking a new version into the version control
+repository.
+
+  >>> repository.updateResource(samp)
+  >>> repository.checkoutResource(samp)
+  >>> util.commit()
+
+  >>> repository.isResourceChanged(samp)
+  False
+  >>> samp.value += 1
+  >>> util.commit()
+
+We can now see that the object has been changed since it was last
+checked in::
+
+  >>> repository.isResourceChanged(samp)
+  True
+
+Checking in the object and commiting shows that we can now veryify
+that the object is considered up-to-date after a subsequent checkout.
+We'll also demonstrate that `checkinResource()` can take an optional
+message argument; we'll see later how this can be used.
+
+  >>> repository.checkinResource(samp, 'sample checkin')
+  >>> util.commit()
+
+  >>> repository.checkoutResource(samp)
+  >>> util.commit()
+
+  >>> repository.isResourceUpToDate(samp)
+  True
+  >>> repository.isResourceChanged(samp)
+  False
+  >>> repository.getVersionInfo(samp).version_id
+  '3'
+
+It's also possible to use version control to discard changes that
+haven't been checked in yet, even though they've been committed to the
+database for the "working copy".  This is done using the
+`uncheckoutResource()` method of the `IVersionControl` object:
+
+  >>> samp.value
+  43
+  >>> samp.value += 2
+  >>> samp.value
+  45
+  >>> util.commit()
+  >>> repository.isResourceChanged(samp)
+  True
+  >>> repository.uncheckoutResource(samp)
+  >>> util.commit()
+
+  >>> samp.value
+  43
+  >>> repository.isResourceChanged(samp)
+  False
+  >>> version_id = repository.getVersionInfo(samp).version_id
+  >>> version_id
+  '3'
+
+An old copy of an object can be "updated" to the most recent version
+of an object:
+
+  >>> ob = repository.getVersionOfResource(orig_history_id, orig_version_id)
+  >>> ob.__name__ = "foo"
+  >>> repository.isResourceUpToDate(ob)
+  False
+  >>> repository.getVersionInfo(ob).version_id
+  '1'
+  >>> repository.updateResource(ob, version_id)
+  >>> repository.getVersionInfo(ob).version_id == version_id
+  True
+  >>> ob.value
+  43
+
+It's possible to get a list of all the versions of a particular object
+from the repository as well.  We can use any copy of the object to
+make the request:
+
+  >>> list(repository.getVersionIds(samp))
+  ['1', '2', '3']
+  >>> list(repository.getVersionIds(ob))
+  ['1', '2', '3']
+
+No version information is available for objects that have not had
+version control applied::
+
+  >>> repository.getVersionIds(samp2)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+  >>> repository.getVersionIds(42)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: Object is not under version control.
+
+
+Naming specific revisions
+-------------------------
+
+Similar to other version control systems, specific versions may be
+given symbolic names, and these names may be used to retrieve versions
+from the repository.  This package calls these names *labels*; they
+are similar to *tags* in CVS.
+
+Labels can be assigned to objects that are checked into the
+repository:
+
+  >>> repository.labelResource(samp, 'my-first-label')
+  >>> repository.labelResource(samp, 'my-second-label')
+
+The list of labels assigned to some version of an object can be
+retrieved using the repository's `getLabelsForResource()` method::
+
+  >>> list(repository.getLabelsForResource(samp))
+  ['my-first-label', 'my-second-label']
+
+The labels can be retrieved using any object that refers to the same
+line of history in the repository:
+
+  >>> list(repository.getLabelsForResource(ob))
+  ['my-first-label', 'my-second-label']
+
+Labels can be used to retrieve specific versions of an object from the
+repository:
+
+  >>> repository.getVersionInfo(samp).version_id
+  '3'
+  >>> ob = repository.getVersionOfResource(orig_history_id, 'my-first-label')
+  >>> repository.getVersionInfo(ob).version_id
+  '3'
+
+It's also possible to move a label from one version to another, but
+only when this is specifically indicated as allowed:
+
+  >>> ob = repository.getVersionOfResource(orig_history_id, orig_version_id)
+  >>> ob.__name__ = "bar"
+  >>> repository.labelResource(ob, 'my-second-label')
+  Traceback (most recent call last):
+    ...
+  VersionControlError: The label my-second-label is already associated with a version.
+  >>> repository.labelResource(ob, 'my-second-label', force=True)
+
+Labels can also be used to update an object to a specific version:
+
+  >>> repository.getVersionInfo(ob).version_id
+  '1'
+  >>> repository.updateResource(ob, 'my-first-label')
+  >>> repository.getVersionInfo(ob).version_id
+  '3'
+  >>> ob.value
+  43
+
+
+Sticky settings
+---------------
+
+Similar to CVS, this package supports a sort of "sticky" updating: if
+an object is updated to a specific date, determination of whether
+it is up-to-date or changed is based on the version it was updated to.
+
+  >>> repository.updateResource(samp, orig_version_id)
+  >>> util.commit()
+
+  >>> samp.value
+  Traceback (most recent call last):
+    ...
+  AttributeError: 'Sample' object has no attribute 'value'
+
+  >>> repository.getVersionInfo(samp).version_id == orig_version_id
+  True
+  >>> repository.isResourceChanged(samp)
+  False
+  >>> repository.isResourceUpToDate(samp)
+  False
+
+The `isResourceUpToDate()` method indicates whether
+`checkoutResource()` will succeed or raise an exception::
+
+  >>> repository.checkoutResource(samp)
+  Traceback (most recent call last):
+    ...
+  VersionControlError: The selected resource has been updated to a particular version, label or date. The resource must be updated to the mainline or a branch before it may be checked out.
+
+
+TODO: Figure out how to write date-based tests.  Perhaps the
+repository should implement a hook used to get the current date so
+tests can hook that.
+
+
+Examining the change history
+----------------------------
+
+  >>> actions = {
+  ...	  interfaces.ACTION_CHECKIN: "Check in",
+  ...	  interfaces.ACTION_CHECKOUT: "Check out",
+  ...	  interfaces.ACTION_UNCHECKOUT: "Uncheckout",
+  ...	  interfaces.ACTION_UPDATE: "Update",
+  ... }
+
+  >>> entries = repository.getLogEntries(samp)
+  >>> for entry in entries:
+  ...	  print "Action:", actions[entry.action]
+  ...	  print "Version:", entry.version_id
+  ...	  print "Path:", entry.path
+  ...	  if entry.message:
+  ...	      print "Message:", entry.message
+  ...	  print "--"
+  Action: Update
+  Version: 1
+  Path: /samp
+  --
+  Action: Update
+  Version: 3
+  Path: /bar
+  --
+  Action: Update
+  Version: 3
+  Path: /foo
+  --
+  Action: Uncheckout
+  Version: 3
+  Path: /samp
+  --
+  Action: Check out
+  Version: 3
+  Path: /samp
+  --
+  Action: Check in
+  Version: 3
+  Path: /samp
+  Message: sample checkin
+  --
+  Action: Check out
+  Version: 2
+  Path: /samp
+  --
+  Action: Update
+  Version: 2
+  Path: /samp
+  --
+  Action: Check in
+  Version: 2
+  Path: /ob
+  --
+  Action: Check out
+  Version: 1
+  Path: /ob
+  --
+  Action: Check in
+  Version: 1
+  Path: /samp
+  Message: Initial checkin.
+  --
+
+Note that the entry with the checkin entry for version 3 includes the
+comment passed to `checkinResource()`.
+
+The version history also contains the principal id related to each
+entry::
+
+  >>> entries[0].user_id
+  'bob'
+
+
+Branches
+--------
+
+The implementation contains some support for branching, but it's not
+fully exposed in the interface at this time.  It's too early to
+document at this time.  Branches will interact heavily with
+"stickiness".
+
+
+Supporting separately versioned subobjects
+------------------------------------------
+
+`INonVersionedData` is responsible for dealing with parts of the object
+state that should *not* be versioned as part of this object.  This can
+include both subobjects that are versioned independently as well as
+object-specific data that isn't part of the abstract resource the
+version control framework is supporting.
+
+For the sake of examples, let's create a simple class that actually
+implements these to interfaces.  In this example, we'll create a
+simple object that excluses any versionable subobjects and any
+subobjects with names that start with "bob".  Note that as for the
+`Sample` class above, we're still careful to consider the values for
+`__name__` and `__parent__` to be non-versioned:
+
+  >>> def ignored_item(name, ob):
+  ...     """Return True for non-versioned items."""
+  ...     return (interfaces.IVersionable.providedBy(ob)
+  ...             or name.startswith("bob")
+  ...             or (name in ["__name__", "__parent__"]))
+
+  >>> class SampleContainer(Sample):
+  ...   
+  ...     # Methods defined by INonVersionedData
+  ...     def listNonVersionedObjects(self):
+  ...         return [ob for (name, ob) in self.__dict__.items()
+  ...                 if ignored_item(name, ob)
+  ...                 ]
+  ...
+  ...     def removeNonVersionedData(self):
+  ...         for name, value in self.__dict__.items():
+  ...             if ignored_item(name, value):
+  ...                 del self.__dict__[name]
+  ...
+  ...     def getNonVersionedData(self):
+  ...         return [(name, ob) for (name, ob) in self.__dict__.items()
+  ...                 if ignored_item(name, ob)
+  ...                 ]
+  ...
+  ...     def restoreNonVersionedData(self, data):
+  ...         for name, value in data:
+  ...             if name not in self.__dict__:
+  ...                 self.__dict__[name] = value
+
+Let's take a look at how the `INonVersionedData` interface is used.
+We'll start by creating an instance of our sample container and
+storing it in the database:
+
+  >>> box = SampleContainer()
+  >>> box.__name__ = "box"
+  >>> root[box.__name__] = box
+
+We'll also add some contained objects:
+
+  >>> box.aList = [1, 2, 3]
+
+  >>> samp1 = Sample()
+  >>> samp1.__name__ = "box/samp1"
+  >>> samp1.__parent__ = box
+  >>> box.samp1 = samp1
+
+  >>> box.bob_list = [3, 2, 1]
+
+  >>> bob_samp = Sample()
+  >>> bob_samp.__name__ = "box/bob_samp"
+  >>> bob_samp.__parent__ = box
+  >>> box.bob_samp = bob_samp
+
+  >>> util.commit()
+
+Let's apply version control to the container:
+
+  >>> repository.applyVersionControl(box)
+
+We'll start by showing some basics of how the INonVersionedData
+interface is used.  
+
+The `getNonVersionedData()`, `removeNonVersionedData()`, and
+`restoreNonVersionedData()` methods work together, allowing the
+version control framework to ensure that data that is not versioned as
+part of the object is not lost or inappropriately stored in the
+repository as part of version control operations.
+
+The basic pattern for this trio of operations is simple:
+
+1. Use `getNonVersionedData()` to get a value that can be used to
+   restore the current non-versioned data of the object.
+
+2. Use `removeNonVersionedData()` to remove any non-versioned data
+   from the object so it doesn't enter the repository as object state
+   is copied around.
+
+3. Make object state changes based on the version control operation
+   being performed.
+
+4. Use `restoreNonVersionedData()` to restore the data retrieved using
+   `getNonVersionedData()`.
+
+This is fairly simple to see in an example.  Step 1 is to save the
+non-versioned data:
+
+  >>> saved = box.getNonVersionedData()
+
+While the version control framework treats this as an opaque value, we
+can take a closer look to make sure we got what we expected (since we
+know our implementation):
+
+  >>> names = [name for (name, ob) in saved]
+  >>> names.sort()
+  >>> names
+  ['__name__', 'bob_list', 'bob_samp', 'samp1']
+
+Step 2 is to remove the data from the object:
+
+  >>> box.removeNonVersionedData()
+
+The non-versioned data should no longer be part of the object:
+
+  >>> box.bob_samp
+  Traceback (most recent call last):
+    ...
+  AttributeError: 'SampleContainer' object has no attribute 'bob_samp'
+
+While versioned data should remain present:
+
+  >>> box.aList
+  [1, 2, 3]
+
+At this point, the version control framework will perform any
+appropriate state copies are needed.
+
+Once that's done, `restoreNonVersionedData()` will be called with the
+saved data to perform the restore operation:
+
+  >>> box.restoreNonVersionedData(saved)
+
+We can verify that the restoraion has been performed by checking the
+non-versioned data:
+
+  >>> box.bob_list
+  [3, 2, 1]
+  >>> type(box.samp1)
+  <class 'zope.app.versioncontrol.README.Sample'>
+
+We can see how this is affects object state by making some changes to
+the container object's versioned and non-versioned data and watching
+how those attributes are affected by updating to specific versions
+using `updateResource()` and retrieving specific versions using
+`getVersionOfResource()`.  Let's start by generating some new
+revisions in the repository:
+
+  >>> repository.checkoutResource(box)
+  >>> util.commit()
+  >>> version_id = repository.getVersionInfo(box).version_id
+
+  >>> box.aList.append(4)
+  >>> box.bob_list.append(0)
+  >>> repository.checkinResource(box)
+  >>> util.commit()
+
+  >>> box.aList
+  [1, 2, 3, 4]
+  >>> box.bob_list
+  [3, 2, 1, 0]
+
+  >>> repository.updateResource(box, version_id)
+  >>> box.aList
+  [1, 2, 3]
+  >>> box.bob_list
+  [3, 2, 1, 0]
+
+The list-remaining method of the `INonVersionedData` interface is a
+little different, but remains very tightly tied to the details of the
+object's state.  The `listNonVersionedObjects()` method should return
+a sequence of all the objects that should not be copied as part of the
+object's state.  The difference between this method and
+`getNonVersionedData()` may seem simple, but is significant in
+practice.
+
+The `listNonVersionedObjects()` method allows the version control
+framework to identify data that should not be included in state
+copies, without saying anything else about the data.  The
+`getNonVersionedData()` method allows the INonVersionedData
+implementation to communicate with itself (by providing data to be
+restored by the `restoreNonVersionedData()` method) without exposing
+any information about how it communicates with itself (it could store
+all the relevant data into an external file and use the value returned
+to locate the state file again, if that was needed for some reason).

Modified: zversioning/trunk/src/versioning/tests/test_versioncontrol.py
===================================================================
--- zversioning/trunk/src/versioning/tests/test_versioncontrol.py	2004-10-09 17:48:14 UTC (rev 27867)
+++ zversioning/trunk/src/versioning/tests/test_versioncontrol.py	2004-10-09 18:21:06 UTC (rev 27868)
@@ -25,9 +25,12 @@
 from zope.app.tests.placelesssetup import setUp, tearDown
 from zope.app.tests import ztapi
 
-from zope.app.tests.setup import buildSampleFolderTree
+# import basic test infrastructure from existing version control implementation
+from zope.app.versioncontrol.tests import setUp, tearDown, name
+
 from zope.testing import doctest
 
+from zope.app.tests.setup import buildSampleFolderTree
 
 def buildSite(items=None) :
     """ Returns s small test site of original content objects:
@@ -35,11 +38,13 @@
         >>> folders = buildSampleFolderTree()
         >>> folders is not None
         True
+      
     """
 
 def test_suite():
     return unittest.TestSuite((
         doctest.DocTestSuite(),
+        doctest.DocFileSuite("README.txt", setUp=setUp, tearDown=tearDown),
         ))
 if __name__=='__main__':
     unittest.main(defaultTest='test_suite')



More information about the Zope-CVS mailing list