[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