[Checkins] SVN: z3c.vcsync/trunk/ Add a test for the type-change bug and fix it.
Martijn Faassen
faassen at startifact.com
Fri Jun 5 11:44:59 EDT 2009
Log message for revision 100654:
Add a test for the type-change bug and fix it.
Changed:
U z3c.vcsync/trunk/CHANGES.txt
U z3c.vcsync/trunk/src/z3c/vcsync/tests.py
A z3c.vcsync/trunk/src/z3c/vcsync/type_change.txt
U z3c.vcsync/trunk/src/z3c/vcsync/vc.py
-=-
Modified: z3c.vcsync/trunk/CHANGES.txt
===================================================================
--- z3c.vcsync/trunk/CHANGES.txt 2009-06-05 15:13:20 UTC (rev 100653)
+++ z3c.vcsync/trunk/CHANGES.txt 2009-06-05 15:44:58 UTC (rev 100654)
@@ -11,6 +11,13 @@
* Use a somewhat less verbose way to set up the tests.
+* An issue existed when a common filesystem representation was used
+ that could be translated (by a single factory) into a number of
+ different classes. If an object was added, synchronized, then
+ removed and an object of the name but different class was added, an
+ error would occur when synchronizing this. This bug has been fixed
+ at the (small) cost of a few-reparses here and there.
+
0.16 (2009-06-02)
-----------------
Modified: z3c.vcsync/trunk/src/z3c/vcsync/tests.py
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/tests.py 2009-06-05 15:13:20 UTC (rev 100653)
+++ z3c.vcsync/trunk/src/z3c/vcsync/tests.py 2009-06-05 15:44:58 UTC (rev 100654)
@@ -67,6 +67,40 @@
self.revision_nr = self.get_revision_nr()
payload = property(_get_payload, _set_payload)
+class BaseItem(object):
+ def __init__(self):
+ pass
+
+class SubItem1(BaseItem):
+ def __init__(self, payload):
+ self.payload = payload
+ def _get_payload(self):
+ return self._payload
+ def _set_payload(self, value):
+ self._payload = value
+ self.revision_nr = self.get_revision_nr()
+ payload = property(_get_payload, _set_payload)
+
+ # force error if payload2 is written to
+ def _readonly(self):
+ pass
+ payload2 = property(_readonly)
+
+class SubItem2(BaseItem):
+ def __init__(self, payload2):
+ self.payload2 = payload2
+ def _get_payload2(self):
+ return self._payload2
+ def _set_payload2(self, value):
+ self._payload2 = value
+ self.revision_nr = self.get_revision_nr()
+ payload2 = property(_get_payload2, _set_payload2)
+
+ # force error if payload is written to
+ def _readonly(self):
+ pass
+ payload = property(_readonly)
+
class TestState(object):
implements(IState)
def __init__(self, root):
@@ -107,12 +141,37 @@
def name(self):
return self.context.__name__ + '.test'
+class BaseItemSerializer(grok.Adapter):
+ grok.provides(ISerializer)
+ grok.context(BaseItem)
+ def serialize(self, f):
+ if isinstance(self.context, SubItem1):
+ f.write('subitem1\n')
+ f.write(str(self.context.payload))
+ f.write('\n')
+ else:
+ f.write('subitem2\n')
+ f.write(str(self.context.payload2))
+ f.write('\n')
+ def name(self):
+ return self.context.__name__ + '.test2'
+
class ItemParser(grok.GlobalUtility):
grok.provides(IParser)
grok.name('.test')
def __call__(self, object, path):
object.payload = int(path.read())
+class BaseItemParser(grok.GlobalUtility):
+ grok.provides(IParser)
+ grok.name('.test2')
+ def __call__(self, object, path):
+ lines = path.readlines()
+ if lines[0].strip() == 'subitem1':
+ object.payload = int(lines[1])
+ else:
+ object.payload2 = int(lines[1])
+
class ItemFactory(grok.GlobalUtility):
grok.provides(IFactory)
grok.name('.test')
@@ -122,6 +181,16 @@
parser(item, path)
return item
+class BaseItemFactory(grok.GlobalUtility):
+ grok.provides(IFactory)
+ grok.name('.test2')
+ def __call__(self, path):
+ lines = path.readlines()
+ if lines[0].strip() == 'subitem1':
+ return SubItem1(int(lines[1]))
+ else:
+ return SubItem2(int(lines[1]))
+
class Container(object):
implements(IContainer)
@@ -149,6 +218,12 @@
def __getitem__(self, name):
return self._data[name]
+ def get(self, key, default=None):
+ try:
+ return self[key]
+ except KeyError:
+ return default
+
def __delitem__(self, name):
del self._data[name]
@@ -165,13 +240,13 @@
def __call__(self, path):
return Container()
-def svn_repo_wc():
+def svn_repo_wc(postfix=''):
"""Create an empty SVN repository.
Based on an internal testing function of the py library.
"""
- repo = py.test.ensuretemp('testrepo')
- wcdir = py.test.ensuretemp('wc')
+ repo = py.test.ensuretemp('testrepo' + postfix)
+ wcdir = py.test.ensuretemp('wc' + postfix)
if not repo.listdir():
repo.ensure(dir=1)
py.process.cmdexec('svnadmin create "%s"' %
@@ -206,7 +281,7 @@
suite = unittest.TestSuite([
doctest.DocFileSuite(
'internal.txt', 'importexport.txt',
- 'README.txt', 'conflict.txt',
+ 'README.txt', 'conflict.txt', 'type_change.txt',
setUp=setUpZope,
tearDown=cleanUpZope,
globs=globs,
Added: z3c.vcsync/trunk/src/z3c/vcsync/type_change.txt
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/type_change.txt (rev 0)
+++ z3c.vcsync/trunk/src/z3c/vcsync/type_change.txt 2009-06-05 15:44:58 UTC (rev 100654)
@@ -0,0 +1,127 @@
+Changing the class of an object
+-------------------------------
+
+In some uses of z3c.vcsync, the extension of the object remains the
+same (say, ``.xml``), and the same parser and serializer are in use
+for all objects, but the class of the object that is represented by
+this XML can actually change.
+
+This can lead to some problems during synchronization that will be
+explored here.
+
+We set up a repository and a checkout::
+
+ >>> from z3c.vcsync.tests import svn_repo_wc
+ >>> repo, wc1 = svn_repo_wc('typechange')
+ >>> from z3c.vcsync.svn import SvnCheckout
+ >>> checkout1 = SvnCheckout(wc1)
+
+For testing purposes we need to make sure that each ``BaseItem`` subclass
+has a ``get_revision_nr`` method. We can't really set up a correct one
+here, but we'll need a temporary one::
+
+ >>> from z3c.vcsync.tests import BaseItem
+ >>> def get_revision_nr(self):
+ ... return 0
+ >>> BaseItem.get_revision_nr = get_revision_nr
+
+Now we set up a container with some content for the state. Note that
+the state is of type ``SubItem1``::
+
+ >>> from z3c.vcsync.tests import Container
+ >>> root1 = Container()
+ >>> root1.__name__ = 'root'
+ >>> root1['data'] = data1 = Container()
+ >>> root1['found'] = Container()
+ >>> from z3c.vcsync.tests import SubItem1
+ >>> data1['foo'] = SubItem1(payload=1)
+ >>> from z3c.vcsync.tests import TestState
+ >>> state1 = TestState(root1)
+
+We can now create the first synchronizer::
+
+ >>> from z3c.vcsync import Synchronizer
+ >>> s1 = Synchronizer(checkout1, state1)
+
+Now we can set up a proper ``get_revision_nr``::
+
+ >>> current_synchronizer = s1
+ >>> def get_revision_nr(self):
+ ... return current_synchronizer.state.get_revision_nr()
+ >>> BaseItem.get_revision_nr = get_revision_nr
+
+Now we'll grok the serializers and parser. Note that these are generic
+ones and can deal with both ``SubItem1`` and ``SubItem2`` subclasses::
+
+ >>> import grokcore.component as grok
+ >>> from z3c.vcsync.tests import (BaseItemSerializer, BaseItemParser, BaseItemFactory,
+ ... ContainerParser, ContainerFactory)
+ >>> grok.testing.grok('z3c.vcsync')
+ >>> grok.testing.grok_component('BaseItemSerializer', BaseItemSerializer)
+ True
+ >>> grok.testing.grok_component('BaseItemParser', BaseItemParser)
+ True
+ >>> grok.testing.grok_component('BaseItemFactory', BaseItemFactory)
+ True
+ >>> grok.testing.grok_component('ContainerParser', ContainerParser)
+ True
+ >>> grok.testing.grok_component('ContainerFactory', ContainerFactory)
+ True
+
+We can now synchronize the data::
+
+ >>> info = s1.sync("synchronize")
+
+We'll now set up a second synchronizer that can load up the data::
+
+ >>> import py
+ >>> wc2 = py.test.ensuretemp('wc2-2')
+ >>> wc2 = py.path.svnwc(wc2)
+ >>> wc2.checkout(repo)
+ >>> checkout2 = SvnCheckout(wc2)
+ >>> root2 = Container()
+ >>> root2.__name__ = 'root'
+ >>> state2 = TestState(root2)
+ >>> s2 = Synchronizer(checkout2, state2)
+ >>> current_synchronizer = s2
+
+We can now synchronize the data into s2::
+
+ >>> info = s2.sync("synchronize")
+
+We will now also have a ``SubItem1`` object in the second tree::
+
+ >>> root2['data']['foo'].payload
+ 1
+ >>> isinstance(root2['data']['foo'], SubItem1)
+ True
+
+We go back to the first synchronizer::
+
+ >>> current_synchronizer = s1
+
+We'll remove ``foo``::
+
+ >>> del data1['foo']
+
+And replace it with a ``foo`` (same name!) but of the class
+``SubItem2``::
+
+ >>> from z3c.vcsync.tests import SubItem2
+ >>> data1['foo'] = SubItem2(payload2=2)
+
+We synchronize again::
+
+ >>> info = s1.sync("synchronize")
+
+Going back to the second synchronizer and synchronizing we should get a proper
+``SubItem2`` object without errors::
+
+ >>> current_synchronizer = s2
+ >>> info = s2.sync("synchronize")
+
+ >>> root2['data']['foo'].payload2
+ 2
+ >>> isinstance(root2['data']['foo'], SubItem2)
+ True
+
Modified: z3c.vcsync/trunk/src/z3c/vcsync/vc.py
===================================================================
--- z3c.vcsync/trunk/src/z3c/vcsync/vc.py 2009-06-05 15:13:20 UTC (rev 100653)
+++ z3c.vcsync/trunk/src/z3c/vcsync/vc.py 2009-06-05 15:44:58 UTC (rev 100654)
@@ -244,14 +244,26 @@
name = file_path.purebasename
ext = file_path.ext
+ # we always use the factory to create an item
+ factory = getUtility(IFactory, name=ext)
+ created = factory(file_path)
+
+ # we observe the stored item in the container
+ stored = container.get(name, None)
+
+ # if the object stored is not the object just created, we need
+ # to remove the stored object first
+ if stored is not None and type(stored) is not type(created):
+ del container[name]
+ stored = None
+
# if we already have the object, overwrite it, otherwise
# create a new one
- if name in container:
+ if stored is not None:
parser = getUtility(IParser, name=ext)
- parser(container[name], file_path)
+ parser(stored, file_path)
else:
- factory = getUtility(IFactory, name=ext)
- container[name] = factory(file_path)
+ container[name] = created
modified_objects.append(container[name])
return modified_objects
More information about the Checkins
mailing list