From 01840c37f04ca20fdc3859a177438dc717afc81c Mon Sep 17 00:00:00 2001
From: Sebastien Robin <seb@nexedi.com>
Date: Tue, 3 Feb 2004 09:53:38 +0000
Subject: [PATCH] added new tests (change an object on two different places at
 the same time,                  working on subojbects) added the simulation
 of conflicts in the synchronization corrected some bugs

git-svn-id: https://svn.erp5.org/repos/public/erp5/trunk@387 20353a03-c40f-0410-a6d1-a30d3c3de9de
---
 product/ERP5SyncML/Conduit/ERP5Conduit.py  |  62 +++++----
 product/ERP5SyncML/Subscription.py         |  26 ++++
 product/ERP5SyncML/XMLSyncUtils.py         |  64 ++++++---
 product/ERP5SyncML/tests/testERP5SyncML.py | 143 ++++++++++++++-------
 4 files changed, 211 insertions(+), 84 deletions(-)

diff --git a/product/ERP5SyncML/Conduit/ERP5Conduit.py b/product/ERP5SyncML/Conduit/ERP5Conduit.py
index 0ba44d1ec9..6e2c811974 100755
--- a/product/ERP5SyncML/Conduit/ERP5Conduit.py
+++ b/product/ERP5SyncML/Conduit/ERP5Conduit.py
@@ -103,7 +103,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
 
   security.declareProtected(Permissions.ModifyPortalContent, 'addNode')
   def addNode(self, xml=None, object=None, previous_xml=None,
-              object_id=None, force=0, **kw):
+              object_id=None, force=0, simulate=0, **kw):
     """
     A node is added
 
@@ -132,7 +132,8 @@ class ERP5Conduit(XMLSyncUtilsMixin):
         for element in self.getXupdateElementList(xml):
           xml = self.getElementFromXupdate(element)
           conflict_list += self.addNode(xml=xml,object=object,
-                          previous_xml=previous_xml, force=force, **kw)
+                          previous_xml=previous_xml, force=force,
+                          simulate=simulate, **kw)
     elif xml.nodeName == 'object':
       if object_id is None:
         object_id = self.getAttribute(xml,'id')
@@ -170,7 +171,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
             px_tool._modifyProxy(proxy,rpath)
 
           subobject = object._getOb(object_id)
-        self.newObject(object=subobject,xml=xml)
+        self.newObject(object=subobject,xml=xml,simulate=simulate)
     elif xml.nodeName in self.XUPDATE_INSERT_OR_ADD \
          and self.getSubObjectDepth(xml)>=1:
       sub_object_id = self.getSubObjectId(xml)
@@ -194,7 +195,8 @@ class ERP5Conduit(XMLSyncUtilsMixin):
             LOG('addNode',0,'sub_xml: %s' % str(sub_xml))
             # Then do the udpate
             conflict_list += self.addNode(xml=sub_xml,object=sub_object,
-                            previous_xml=sub_previous_xml, force=force)
+                            previous_xml=sub_previous_xml, force=force,
+                            simulate=simulate, **kw)
     elif xml.nodeName == self.history_tag or self.isHistoryAdd(xml)>0:
       # We want to add a workflow action
       wf_tool = getToolByName(object,'portal_workflow')
@@ -211,12 +213,12 @@ class ERP5Conduit(XMLSyncUtilsMixin):
                                              status=status,wf_tool=wf_tool,
                                              xml=xml)
       LOG('addNode, workflow_history wf_conflict_list:',0,wf_conflict_list)
-      if wf_conflict_list==[] or force:
+      if wf_conflict_list==[] or force and not simulate:
         LOG('addNode, setting status:',0,'ok')
         wf_tool.setStatusOf(wf_id,object,status)
       else:
         conflict_list += wf_conflict_list
-    elif xml.nodeName in self.local_role_list:
+    elif xml.nodeName in self.local_role_list and not simulate:
       # We want to add a local role
       roles = self.convertXmlValue(xml.childNodes[0].data,data_type='tokens')
       roles = list(roles) # Needed for CPS, or we have a CPS error
@@ -224,11 +226,13 @@ class ERP5Conduit(XMLSyncUtilsMixin):
       roles = roles[1:]
       object.manage_setLocalRoles(user,roles)
     else:
-      conflict_list += self.updateNode(xml=xml,object=object, force=force, **kw)
+      conflict_list += self.updateNode(xml=xml,object=object, force=force,
+                                       simulate=simulate,  **kw)
     return conflict_list
 
   security.declareProtected(Permissions.ModifyPortalContent, 'deleteNode')
-  def deleteNode(self, xml=None, object=None, object_id=None, force=None, **kw):
+  def deleteNode(self, xml=None, object=None, object_id=None, force=None,
+                 simulate=0, **kw):
     """
     A node is deleted
     """
@@ -251,7 +255,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
           sub_object = object._getOb(sub_object_id)
           sub_xml = self.getSubObjectXupdate(xml)
           conflict_list += self.deleteNode(xml=sub_xml,object=sub_object,
-                                           force=force)
+                                           force=force, simulate=simulate, **kw)
         except KeyError:
           pass
     else: # We do have an object_id
@@ -263,7 +267,8 @@ class ERP5Conduit(XMLSyncUtilsMixin):
     return conflict_list
 
   security.declareProtected(Permissions.ModifyPortalContent, 'updateNode')
-  def updateNode(self, xml=None, object=None, previous_xml=None, force=0, **kw):
+  def updateNode(self, xml=None, object=None, previous_xml=None, force=0,
+                 simulate=0,  **kw):
     """
     A node is updated with some xupdate
       - xml : the xml corresponding to the update, it should be xupdate
@@ -280,7 +285,8 @@ class ERP5Conduit(XMLSyncUtilsMixin):
       #xupdate_utils = XupdateUtils()
       xupdate_utils = self
       conflict_list += xupdate_utils.applyXupdate(object=object,xupdate=xml,conduit=self,
-                                 previous_xml=previous_xml, force=force)
+                                 previous_xml=previous_xml, force=force, simulate=simulate,
+                                 **kw)
     # we may have only the part of an xupdate
     else:
       args = {}
@@ -358,7 +364,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
                                            #publisher_value=current_data, # not needed any more
                                            #subscriber_value=data)] # not needed any more
           # We will now apply the argument with the method edit
-          if args != {} and (isConflict==0 or force):
+          if args != {} and (isConflict==0 or force) and (not simulate):
             LOG('updateNode',0,'object._edit, args: %s' % str(args))
             object._edit(**args)
             # It is sometimes required to do something after an edit
@@ -368,12 +374,14 @@ class ERP5Conduit(XMLSyncUtilsMixin):
         if keyword == 'object':
           # This is the case where we have to call addNode
           LOG('updateNode',0,'we will add sub-object')
-          conflict_list += self.addNode(xml=subnode,object=object,force=force)
-        elif keyword == self.history_tag:
+          conflict_list += self.addNode(xml=subnode,object=object,force=force,
+                                        simulate=simulate, **kw)
+        elif keyword == self.history_tag and not simulate:
           # This is the case where we have to call addNode
           LOG('updateNode',0,'we will add history')
-          conflict_list += self.addNode(xml=subnode,object=object,force=force)
-        elif keyword == self.local_role_tag:
+          conflict_list += self.addNode(xml=subnode,object=object,force=force,
+                                        simulate=simulate,**kw)
+        elif keyword == self.local_role_tag and not simulate:
           # This is the case where we have to call addNode
           LOG('updateNode',0,'we will add a local role')
           roles = self.convertXmlValue(data,data_type='tokens')
@@ -403,7 +411,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
               LOG('updateNode',0,'sub_xml: %s' % str(sub_xml))
               # Then do the udpate
               conflict_list += self.updateNode(xml=sub_xml,object=sub_object, force=force,
-                              previous_xml=sub_previous_xml)
+                              previous_xml=sub_previous_xml,simulate=simulate, **kw)
     return conflict_list
 
   security.declareProtected(Permissions.AccessContentsInformation,'getFormatedArgs')
@@ -635,7 +643,6 @@ class ERP5Conduit(XMLSyncUtilsMixin):
         except IndexError: # There is no data
           data = None
         data =  self.convertXmlValue(data, data_type=data_type)
-        LOG('getObjectProperty',0,'prop; %s, data_type:%s, data: %s' % (property,data_type,data))
         return data
     return None
 
@@ -699,12 +706,14 @@ class ERP5Conduit(XMLSyncUtilsMixin):
 
 
   security.declareProtected(Permissions.ModifyPortalContent, 'newObject')
-  def newObject(self, object=None, xml=None):
+  def newObject(self, object=None, xml=None, simulate=0):
     """
       modify the object with datas from
       the xml (action section)
     """
     args = {}
+    if simulate:
+      return
     # Retrieve the list of users with a role and delete default roles
     user_role_list = map(lambda x:x[0],object.get_local_roles())
     object.manage_delLocalRoles(user_role_list)
@@ -766,7 +775,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
     for subnode in self.getElementNodeList(xml):
       if subnode.nodeName in self.XUPDATE_EL:
         e_list += [subnode]
-    LOG('getXupdateElementList, e_list:%',0,e_list)
+    LOG('getXupdateElementList, e_list:',0,e_list)
     return e_list
 
   security.declareProtected(Permissions.AccessContentsInformation,'getElementFromXupdate')
@@ -775,7 +784,7 @@ class ERP5Conduit(XMLSyncUtilsMixin):
     from a xupdate:element returns the element as xml
     """
     if xml.nodeName in self.XUPDATE_EL:
-      result = '<'
+      result = u'<'
       result += xml.attributes[0].nodeValue
       for subnode in self.getElementNodeList(xml):  #getElementNodeList
         if subnode.nodeName == 'xupdate:attribute':
@@ -786,11 +795,11 @@ class ERP5Conduit(XMLSyncUtilsMixin):
       xml_string = StringIO()
       PrettyPrint(xml,xml_string)
       xml_string = xml_string.getvalue()
+      xml_string = unicode(xml_string,encoding='utf-8')
       maxi = max(xml_string.find('>')+1,\
                  xml_string.rfind('</xupdate:attribute>')+len('</xupdate:attribute>'))
       result += xml_string[maxi:xml_string.find('</xupdate:element>')]
       result += '</' + xml.attributes[0].nodeValue + '>'
-      LOG('getElementFromXupdate, result:',0,result)
       return self.convertToXml(result)
     return xml
 
@@ -856,7 +865,8 @@ class ERP5Conduit(XMLSyncUtilsMixin):
   # XXX is it the right place ? It should be in XupdateUtils, but here we
   # have some specific things to do
   security.declareProtected(Permissions.ModifyPortalContent, 'applyXupdate')
-  def applyXupdate(self, object=None, xupdate=None, conduit=None, force=0, **kw):
+  def applyXupdate(self, object=None, xupdate=None, conduit=None, force=0,
+                   simulate=0, **kw):
     """
     Parse the xupdate and then it will call the conduit
     """
@@ -869,13 +879,13 @@ class ERP5Conduit(XMLSyncUtilsMixin):
       selection_name = ''
       if subnode.nodeName in self.XUPDATE_INSERT_OR_ADD:
         conflict_list += conduit.addNode(xml=sub_xupdate,object=object, \
-                                         force=force, **kw)
+                                         force=force, simulate=simulate, **kw)
       elif subnode.nodeName in self.XUPDATE_DEL:
         conflict_list += conduit.deleteNode(xml=sub_xupdate, object=object, \
-                                         force=force, **kw)
+                                         force=force, simulate=simulate, **kw)
       elif subnode.nodeName in self.XUPDATE_UPDATE:
         conflict_list += conduit.updateNode(xml=sub_xupdate, object=object, \
-                                         force=force, **kw)
+                                         force=force, simulate=simulate, **kw)
       #elif subnode.nodeName in self.XUPDATE_INSERT:
       #  conflict_list += conduit.addNode(xml=subnode, object=object, force=force, **kw)
 
diff --git a/product/ERP5SyncML/Subscription.py b/product/ERP5SyncML/Subscription.py
index c5e04c9d35..04b2358eb8 100755
--- a/product/ERP5SyncML/Subscription.py
+++ b/product/ERP5SyncML/Subscription.py
@@ -198,6 +198,8 @@ class Signature(SyncCode):
     self.resetConflictList()
     self.md5_string = None
     self.force = 0
+    self.setSubscriberXupdate(None)
+    self.setPublisherXupdate(None)
 
   #def __init__(self,object=None, status=None, xml_string=None):
   #  self.uid = object.uid
@@ -271,6 +273,30 @@ class Signature(SyncCode):
     """
     return self.temp_xml
 
+  def setSubscriberXupdate(self, xupdate):
+    """
+    set the full temp xupdate
+    """
+    self.subscriber_xupdate = xupdate
+
+  def getSubscriberXupdate(self):
+    """
+    get the full temp xupdate
+    """
+    return self.subscriber_xupdate
+
+  def setPublisherXupdate(self, xupdate):
+    """
+    set the full temp xupdate
+    """
+    self.publisher_xupdate = xupdate
+
+  def getPublisherXupdate(self):
+    """
+    get the full temp xupdate
+    """
+    return self.publisher_xupdate
+
   def setMD5(self, xml):
     """
       set the MD5 object of this signature
diff --git a/product/ERP5SyncML/XMLSyncUtils.py b/product/ERP5SyncML/XMLSyncUtils.py
index 301bb1ec25..f21202ce8c 100755
--- a/product/ERP5SyncML/XMLSyncUtils.py
+++ b/product/ERP5SyncML/XMLSyncUtils.py
@@ -552,13 +552,23 @@ class XMLSyncUtilsMixin(SyncCode):
 
   def getSyncMLData(self, domain=None,remote_xml=None,cmd_id=0,
                           subscriber=None,destination_path=None,
-                          xml_confirmation=None):
+                          xml_confirmation=None,conduit=None):
     """
     This generate the syncml data message. This returns a string
     with all modification made locally (ie replace, add ,delete...)
+
+    if object is not None, this usually means we want to set the
+    actual xupdate on the signature.
     """
     local_gid_list = []
     syncml_data = ''
+#     store_xupdate = 0
+#     if object is None:
+#       object_list = domain.getObjectList()
+#     else:
+#       store_xupdate = 1
+#       object_list = [object]
+
     for object in domain.getObjectList():
       status = self.SENT
       gid_generator = getattr(object,domain.getGidGenerator(),None)
@@ -575,6 +585,7 @@ class XMLSyncUtilsMixin(SyncCode):
         LOG('getSyncMLData',0,'hasSignature: %s' % str(subscriber.hasSignature(object_gid)))
         LOG('getSyncMLData',0,'alert_code == slowsync: %s' % str(self.getAlertCode(remote_xml)==self.SLOW_SYNC))
         signature = subscriber.getSignature(object_gid)
+        LOG('getSyncMLData',0,'current object: %s' % str(object.getId()))
         if signature is not None:
           LOG('getSyncMLData',0,'signature.status: %s' % str(signature.getStatus()))
           LOG('getSyncMLData',0,'signature.action: %s' % str(signature.getAction()))
@@ -615,7 +626,9 @@ class XMLSyncUtilsMixin(SyncCode):
           if signature.getStatus()==self.PUB_CONFLICT_MERGE:
             xml_confirmation += self.SyncMLConfirmation(cmd_id,object.id,
                                   self.CONFLICT_MERGE,'Replace')
+          set_synchronized = 1
           if not signature.checkMD5(xml_object):
+            set_synchronized = 0
             # This object has changed on this side, we have to generate some xmldiff
             xml_string = self.getXupdateObject(object=object,
                                               xml_mapping=domain.xml_mapping,
@@ -638,7 +651,18 @@ class XMLSyncUtilsMixin(SyncCode):
                                                 xml_string=xml_string, more_data=more_data)
             cmd_id += 1
             signature.setTempXML(xml_object)
-          else: # We should not have this case when we are in CONFLICT_MERGE
+          # Now we can apply the xupdate from the subscriber
+          subscriber_xupdate = signature.getSubscriberXupdate()
+          LOG('getSyncMLData subscriber_xupdate',0,subscriber_xupdate)
+          if subscriber_xupdate is not None:
+            old_xml = signature.getXML()
+            conduit.updateNode(xml=subscriber_xupdate, object=object,
+                              previous_xml=old_xml,force=(domain.getDomainType==self.SUB),
+                              simulate=0)
+            xml_object = self.getXMLObject(object=object,xml_mapping=domain.xml_mapping)
+            signature.setTempXML(xml_object)
+          if set_synchronized: # We have to do that after this previous update
+            # We should not have this case when we are in CONFLICT_MERGE
             signature.setStatus(self.SYNCHRONIZED)
         elif signature.getStatus()==self.PUB_CONFLICT_CLIENT_WIN:
           # We have decided to apply the update
@@ -691,7 +715,7 @@ class XMLSyncUtilsMixin(SyncCode):
     return (syncml_data,xml_confirmation,cmd_id)
 
   def applyActionList(self, domain=None, subscriber=None,destination_path=None,
-                      cmd_id=0,remote_xml=None,conduit=None):
+                      cmd_id=0,remote_xml=None,conduit=None,simulate=0):
     """
     This just look to a list of action to do, then id applies
     each action one by one, thanks to a conduit
@@ -706,8 +730,6 @@ class XMLSyncUtilsMixin(SyncCode):
       status_code = self.SUCCESS
       # Thirst we have to check the kind of action it is
       partial_data = self.getPartialData(next_action)
-      #LOG('XMLSyncUtils',0,'partial_data: %s' % str(partial_data))
-      #LOG('XMLSyncUtils',0,'checkActionMoreData: %s' % str(self.checkActionMoreData(next_action)))
       object_gid = self.getActionId(next_action)
       signature = subscriber.getSignature(object_gid)
       if signature == None:
@@ -749,12 +771,6 @@ class XMLSyncUtilsMixin(SyncCode):
                  self.SyncMLConfirmation(cmd_id,object_gid,self.SUCCESS,'Add')
             cmd_id +=1
         elif next_action.nodeName == 'Replace':
-          #object = domain.getObjectFromGid(object=destination_path,gid=object_gid)
-#           object =None
-#           try:
-#             object = destination_path._getOb(self.getActionId(next_action))
-#           except (AttributeError, KeyError):
-#             pass
           LOG('SyncModif',0,'object: %s will be updated...' % str(object))
           if object is not None:
             LOG('SyncModif',0,'object: %s will be updated...' % object.id)
@@ -763,7 +779,8 @@ class XMLSyncUtilsMixin(SyncCode):
             previous_xml = signature.getXML()
             LOG('SyncModif',0,'previous signature: %i' % len(previous_xml))
             conflict_list += conduit.updateNode(xml=data_subnode, object=object,
-                              previous_xml=signature.getXML(),force=force)
+                              previous_xml=signature.getXML(),force=force,
+                              simulate=simulate)
             mapping = getattr(object,domain.getXMLMapping(),None)
             xml_object = ''
             if mapping is not None:
@@ -778,11 +795,25 @@ class XMLSyncUtilsMixin(SyncCode):
               PrettyPrint(data_subnode,stream=string_io)
               data_subnode_string = string_io.getvalue()
               signature.setPartialXML(data_subnode_string)
-            else:
+            elif not simulate:
               signature.setStatus(self.SYNCHRONIZED)
             xml_confirmation += self.SyncMLConfirmation(cmd_id,
                                         object_gid,status_code,'Replace')
             cmd_id +=1
+            if simulate:
+              # This means we are on the publiher side and we want to store
+              # the xupdate from the subscriber and we also want to generate
+              # the current xupdate from the last synchronization
+              string_io = StringIO()
+              PrettyPrint(data_subnode,stream=string_io)
+              data_subnode_string = string_io.getvalue()
+              LOG('applyActionList, subscriber_xupdate:',0,data_subnode_string)
+              signature.setSubscriberXupdate(data_subnode_string)
+#               xml_string = self.getXupdateObject(object=object,
+#                                                 xml_mapping=domain.xml_mapping,
+#                                                 old_xml=signature.getXML())
+              # signature.setPublisherXupdate(xml_string) XXX is it needed ??
+
         elif next_action.nodeName == 'Delete':
           object_id = object.id
           conduit.deleteNode(xml=self.getDataSubNode(next_action), object=destination_path,
@@ -881,7 +912,9 @@ class XMLSyncUtils(XMLSyncUtilsMixin):
       return
 
     subscriber = domain # If we are the client, this is fine
+    simulate = 0 # used by applyActionList, should be 0 for client
     if domain.domain_type == self.PUB:
+      simulate = 1
       for subnode in xml_header.childNodes:
         if subnode.nodeType == subnode.ELEMENT_NODE and subnode.nodeName == "Source":
           subscription_url = str(subnode.childNodes[0].data)
@@ -901,7 +934,7 @@ class XMLSyncUtils(XMLSyncUtilsMixin):
                                          destination_path=destination_path,
                                          subscriber=subscriber,
                                          remote_xml=remote_xml,
-                                         conduit=conduit)
+                                         conduit=conduit, simulate=simulate)
     LOG('SyncModif, has_next_action:',0,has_next_action)
 
     xml = ""
@@ -925,7 +958,8 @@ class XMLSyncUtils(XMLSyncUtilsMixin):
                                        remote_xml=remote_xml,
                                        subscriber=subscriber,
                                        destination_path=destination_path,
-                                       cmd_id=cmd_id,xml_confirmation=xml_confirmation)
+                                       cmd_id=cmd_id,xml_confirmation=xml_confirmation,
+                                       conduit=conduit)
 
     # syncml body
     xml += ' <SyncBody>\n'
diff --git a/product/ERP5SyncML/tests/testERP5SyncML.py b/product/ERP5SyncML/tests/testERP5SyncML.py
index 6efebde022..9d0cd6b0da 100755
--- a/product/ERP5SyncML/tests/testERP5SyncML.py
+++ b/product/ERP5SyncML/tests/testERP5SyncML.py
@@ -58,10 +58,10 @@ class TestERP5SyncML(ERP5TypeTestCase):
   description1 = 'description1 $sdfrç_sdfsçdf_oisfsopf'
   first_name2 = 'Jean-Paul'
   last_name2 = 'Smets'
-  description2 = 'descrip  ti on2éà@  $*<<<<>>>></title>&oekd'
+  description2 = 'description2éà@  $*< <<<  >>>></title>&oekd'
   first_name3 = 'Yoshinori'
   last_name3 = 'Okuji'
-  description3 = 'description çsdf__sdfççç_df___&&é]]]°°°°°°'
+  description3 = 'description3 çsdf__sdfççç_df___&&é]]]°°°°°°'
   xml_mapping = 'asXML'
   id1 = '170'
   id2 = '171'
@@ -157,11 +157,11 @@ class TestERP5SyncML(ERP5TypeTestCase):
     user = uf.getUserById('ERP5TypeTestCase').__of__(uf)
     newSecurityManager(None, user)
 
-  def testPopulatePersonServer(self, quiet=0, run=run_all_test):
+  def populatePersonServer(self, quiet=0, run=run_all_test):
     if not run: return
     if not quiet:
       ZopeTestCase._print('\nTest Populate Person Server ')
-      LOG('Testing... ',0,'testPopulatePersonServer')
+      LOG('Testing... ',0,'populatePersonServer')
     self.login()
     person_server = self.getPersonServer()
     person_id = ''
@@ -205,7 +205,7 @@ class TestERP5SyncML(ERP5TypeTestCase):
       LOG('Testing... ',0,'testGetObjectList')
     self.login()
     self.setupPublicationAndSubscription(quiet=1,run=1)
-    nb_person = self.testPopulatePersonServer(quiet=1,run=1)
+    nb_person = self.populatePersonServer(quiet=1,run=1)
     portal_sync = self.getSynchronizationTool()
     publication_list = portal_sync.getPublicationList()
     publication = publication_list[0]
@@ -231,7 +231,7 @@ class TestERP5SyncML(ERP5TypeTestCase):
       ZopeTestCase._print('\nTest Export and Import ')
       LOG('Testing... ',0,'testExportImport')
     self.login()
-    self.testPopulatePersonServer(quiet=1,run=1)
+    self.populatePersonServer(quiet=1,run=1)
     person_server = self.getPersonServer()
     person_client1 = self.getPersonClient1()
     person = person_server._getOb(self.id1)
@@ -278,7 +278,7 @@ class TestERP5SyncML(ERP5TypeTestCase):
       LOG('Testing... ',0,'testFirstSynchronization')
     self.login()
     self.setupPublicationAndSubscription(quiet=1,run=1)
-    nb_person = self.testPopulatePersonServer(quiet=1,run=1)
+    nb_person = self.populatePersonServer(quiet=1,run=1)
     # Synchronize the first client
     nb_message1 = self.synchronize(self.sub_id1)
     self.failUnless(nb_message1==self.nb_message_first_synchronization)
@@ -316,7 +316,7 @@ class TestERP5SyncML(ERP5TypeTestCase):
       LOG('Testing... ',0,'testGetObjectFromGid')
     self.login()
     self.setupPublicationAndSubscription(quiet=1,run=1)
-    self.testPopulatePersonServer(quiet=1,run=1)
+    self.populatePersonServer(quiet=1,run=1)
     # By default we can just give the id
     portal_sync = self.getSynchronizationTool()
     publication = portal_sync.getPublication(self.pub_id)
@@ -400,13 +400,12 @@ class TestERP5SyncML(ERP5TypeTestCase):
     person1_s.edit(**kw)
     kw = {'description':self.description3}
     person1_c.edit(**kw)
-    # XXX Warning XXX This does not works actually, need to be CORRECTED !!!!
-#     self.synchronize(self.sub_id1)
-#     self.checkSynchronizationStateIsSynchronized()
-#     self.failUnless(person1_s.getFirstName()==self.first_name3)
-#     self.failUnless(person1_s.getDescription()==self.description3)
-#     self.failUnless(person1_c.getFirstName()==self.first_name3)
-#     self.failUnless(person1_c.getDescription()==self.description3)
+    self.synchronize(self.sub_id1)
+    self.checkSynchronizationStateIsSynchronized()
+    self.failUnless(person1_s.getFirstName()==self.first_name3)
+    self.failUnless(person1_s.getDescription()==self.description3)
+    self.failUnless(person1_c.getFirstName()==self.first_name3)
+    self.failUnless(person1_c.getDescription()==self.description3)
 
   def testGetConflictList(self, quiet=0, run=run_all_test):
     # We will try to generate a conflict and then to get it
@@ -428,6 +427,8 @@ class TestERP5SyncML(ERP5TypeTestCase):
     conflict_list = portal_sync.getConflictList()
     self.failUnless(len(conflict_list)==1)
     conflict = conflict_list[0]
+    self.failUnless(person1_c.getDescription()==self.description3)
+    self.failUnless(person1_s.getDescription()==self.description2)
     self.failUnless(conflict.getPropertyId()=='description')
     self.failUnless(conflict.getPublisherValue()==self.description2)
     self.failUnless(conflict.getSubscriberValue()==self.description3)
@@ -438,10 +439,10 @@ class TestERP5SyncML(ERP5TypeTestCase):
     # We will try to generate a conflict and then to get it
     # We will also make sure it contains what we want
     if not run: return
+    self.testGetConflictList(quiet=1,run=1)
     if not quiet:
       ZopeTestCase._print('\nTest Apply Publisher Value ')
       LOG('Testing... ',0,'testApplyPublisherValue')
-    self.testGetConflictList(quiet=1,run=1)
     portal_sync = self.getSynchronizationTool()
     conflict_list = portal_sync.getConflictList()
     conflict = conflict_list[0]
@@ -452,8 +453,8 @@ class TestERP5SyncML(ERP5TypeTestCase):
     conflict.applyPublisherValue()
     self.synchronize(self.sub_id1)
     self.checkSynchronizationStateIsSynchronized()
-    self.failUnless(person1_s.getDescription()==self.description2)
     self.failUnless(person1_c.getDescription()==self.description2)
+    self.failUnless(person1_s.getDescription()==self.description2)
     conflict_list = portal_sync.getConflictList()
     self.failUnless(len(conflict_list)==0)
 
@@ -461,12 +462,12 @@ class TestERP5SyncML(ERP5TypeTestCase):
     # We will try to generate a conflict and then to get it
     # We will also make sure it contains what we want
     if not run: return
-    if not quiet:
-      ZopeTestCase._print('\nTest Apply Subscriber Value ')
-      LOG('Testing... ',0,'testApplySubscriberValue')
     self.testGetConflictList(quiet=1,run=1)
     portal_sync = self.getSynchronizationTool()
     conflict_list = portal_sync.getConflictList()
+    if not quiet:
+      ZopeTestCase._print('\nTest Apply Subscriber Value ')
+      LOG('Testing... ',0,'testApplySubscriberValue')
     conflict = conflict_list[0]
     person_server = self.getPersonServer()
     person1_s = person_server._getOb(self.id1)
@@ -480,30 +481,86 @@ class TestERP5SyncML(ERP5TypeTestCase):
     conflict_list = portal_sync.getConflictList()
     self.failUnless(len(conflict_list)==0)
 
-#   def testPopulatePersonServerWithSubObject(self, quiet=0, run=1):
-#     if not run: return
-#     if not quiet:
-#       ZopeTestCase._print('\nTest Populate Person Server With Sub Object ')
-#       LOG('Testing... ',0,'testPopulatePersonServerWithSubObject')
-#     self.testPopulatePersonServer(quiet=1,run=1)
-#     person_server = self.getPersonServer()
-#     person1 = person_server._getOb(self.id1)
-#     sub_person1 = person1.newContent(id=self.id1,portal_type='Person')
-#     person2 = person_server.newContent(id=self.id2,portal_type='Person')
-#     kw = {'first_name':self.first_name1,'last_name':self.last_name1,
-#           'description':self.description1}
-#     person1.edit(**kw)
-#     kw = {'first_name':self.first_name2,'last_name':self.last_name2,
-#           'description':self.description2}
-#     person2.edit(**kw)
-#     nb_person = len(person_server.objectValues())
-#     self.failUnless(nb_person==2)
-#     return nb_person
-
-# XXX TODO : need to add a test with all kind of strange sign, like &, à è...><
-
-
+  def populatePersonServerWithSubObject(self, quiet=0, run=run_all_test):
+    """
+    Before this method, we need to call populatePersonServer
+    Then it will give the following tree :
+    - person_server :
+      - id1
+        - id1
+          - id2
+      - id2
+    """
+    if not run: return
+    if not quiet:
+      ZopeTestCase._print('\nTest Populate Person Server With Sub Object ')
+      LOG('Testing... ',0,'populatePersonServerWithSubObject')
+    person_server = self.getPersonServer()
+    person1 = person_server._getOb(self.id1)
+    sub_person1 = person1.newContent(id=self.id1,portal_type='Person')
+    kw = {'first_name':self.first_name1,'last_name':self.last_name1,
+          'description':self.description1}
+    sub_person1.edit(**kw)
+    sub_sub_person = sub_person1.newContent(id=self.id2,portal_type='Person')
+    kw = {'first_name':self.first_name2,'last_name':self.last_name2,
+          'description':self.description2}
+    sub_sub_person.edit(**kw)
+    # remove ('','portal...','person_server')
+    len_path = len(sub_sub_person.getPhysicalPath()) - 3 
+    self.failUnless(len_path==3)
 
+  def testAddSubObject(self, quiet=0, run=run_all_test):
+    """
+    In this test, we synchronize, then add sub object on the
+    server and then see if the next synchronization will also
+    create sub-objects on the client
+    """
+    if not run: return
+    self.testFirstSynchronization(quiet=1,run=1)
+    if not quiet:
+      ZopeTestCase._print('\nTest Add Sub Object ')
+      LOG('Testing... ',0,'testAddSubObject')
+    self.populatePersonServerWithSubObject(quiet=1,run=1)
+    self.synchronize(self.sub_id1)
+    self.checkSynchronizationStateIsSynchronized()
+    person_client1 = self.getPersonClient1()
+    person1_c = person_client1._getOb(self.id1)
+    sub_person1_c = person1_c._getOb(self.id1)
+    sub_sub_person = sub_person1_c._getOb(self.id2)
+    # remove ('','portal...','person_server')
+    len_path = len(sub_sub_person.getPhysicalPath()) - 3 
+    self.failUnless(len_path==3)
+    self.failUnless(sub_sub_person.getDescription()==self.description2)
+    self.failUnless(sub_sub_person.getFirstName()==self.first_name2)
+    self.failUnless(sub_sub_person.getLastName()==self.last_name2)
+
+  def testUpdateSubObject(self, quiet=0, run=1):
+    """
+    In this test, we start with a tree of object already
+    synchronized, then we update a subobject, and we will see
+    if it is updated correctly
+    """
+    if not run: return
+    self.testAddSubObject(quiet=1,run=1)
+    if not quiet:
+      ZopeTestCase._print('\nTest Update Sub Object ')
+      LOG('Testing... ',0,'testUpdateSubObject')
+    person_client1 = self.getPersonClient1()
+    person1_c = person_client1._getOb(self.id1)
+    sub_person1_c = person1_c._getOb(self.id1)
+    sub_sub_person_c = sub_person1_c._getOb(self.id2)
+    person_server = self.getPersonServer()
+    sub_sub_person_s = person_server._getOb(self.id1)._getOb(self.id1)._getOb(self.id2)
+    kw = {'first_name':self.first_name3}
+    sub_sub_person_c.edit(**kw)
+    kw = {'description':self.description3}
+    sub_sub_person_s.edit(**kw)
+    self.synchronize(self.sub_id1)
+    self.checkSynchronizationStateIsSynchronized()
+    self.failUnless(sub_sub_person_c.getDescription()==self.description3)
+    self.failUnless(sub_sub_person_c.getFirstName()==self.first_name3)
+    self.failUnless(sub_sub_person_s.getDescription()==self.description3)
+    self.failUnless(sub_sub_person_s.getFirstName()==self.first_name3)
 
 if __name__ == '__main__':
     framework()
-- 
2.30.9