[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