Test isolation errors with collective.z3cform.datagridfield

After adding collective.cookiecuttr to my package's GS dependencies i got a strange error when running unit tests:

File ".buildout/eggs/wm.sampledata-0.5.2-py2.7.egg/wm/sampledata/utils.py", line 244, in doWorkflowTransitions
    include_children=includeChildren
File "<string>", line 3, in transitionObjectsByPaths
File ".buildout/eggs/plone.protect-3.1.4-py2.7.egg/plone/protect/utils.py", line 58, in _curried
    return callable(*args, **kw)
File "<string>", line 3, in transitionObjectsByPaths
File ".buildout/eggs/AccessControl-3.0.11-py2.7-linux-x86_64.egg/AccessControl/requestmethod.py", line 70, in _curried
    return callable(*args, **kw)
File ".buildout/eggs/Products.CMFPlone-4.3.18-py2.7.egg/Products/CMFPlone/PloneTool.py", line 1265, in transitionObjectsByPaths
    sp = transaction.savepoint(optimistic=True)
File ".buildout/eggs/transaction-1.1.1-py2.7.egg/transaction/_manager.py", line 101, in savepoint
    return self.get().savepoint(optimistic)
File ".buildout/eggs/transaction-1.1.1-py2.7.egg/transaction/_transaction.py", line 260, in savepoint
    self._saveAndRaiseCommitishError() # reraises!
File ".buildout/eggs/transaction-1.1.1-py2.7.egg/transaction/_transaction.py", line 257, in savepoint
    savepoint = Savepoint(self, optimistic, *self._resources)
File ".buildout/eggs/transaction-1.1.1-py2.7.egg/transaction/_transaction.py", line 690, in __init__
    savepoint = savepoint()
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/Connection.py", line 1125, in savepoint
    self._commit(None)
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/Connection.py", line 623, in _commit
    self._store_objects(ObjectWriter(obj), transaction)
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/Connection.py", line 658, in _store_objects
    p = writer.serialize(obj)  # This calls __getstate__ of obj
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/serialize.py", line 422, in serialize
    return self._dump(meta, obj.__getstate__())
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/serialize.py", line 431, in _dump
    self._p.dump(state)
File ".buildout/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/serialize.py", line 352, in persistent_id
    "database connection", self._jar, obj,
InvalidObjectReference: ('Attempt to store an object from a foreign database connection', <Connection at 7f0e20836210>, <collective.z3cform.datagridfield.registry.DictRow object at 0x7f0e212d07d0>)   

Searching the internet did not reveal usefu informationl. @thet hat a similar issue https://github.com/bluedynamics/bda.plone.ticketshop/issues/4 but worked around it.

This is what i found out so far:

The error happens, if two or more tests within the same layer install the package collective.cookiecuttr (or a package depending on it) in the TestCase's setUp method (by using quickinstaller.installProduct or plone.app.testing.helpers.applyProfile).

Proof: If we comment out the installation of the registry records (done in https://github.com/fourdigits/collective.cookiecuttr/blob/0.7.6/src/collective/cookiecuttr/profiles/default/registry.xml) the error does not happen.

I worked around this issue by moving the installation out of TestCase.setUp into a separate PloneSandboxLayer that calls applyProfile in setUpPloneSite.

Somehow an imported registry record of type DictRow "survives" the tearDown (of the Demostorage) between two TestCases of the same layer (or we get the old object when installing it again).
Fact is, it referes a stale database connection in ._jar leading to the error above when calling transaction.commit()::

#breakpoint in ZODB.serialize:
(Pdb) repr(obj)
'<collective.z3cform.datagridfield.registry.DictRow object at 0x7fe4b92796e0>'
(Pdb) self._jar
<Connection at 7fe4b8340d90>
(Pdb) obj.__jar
*** ConnectionStateError: Shouldn't load state for 0x19f744a58b895605 when the connection is closed

To me this smells like collective.z3cform.datagridfield (combined with plone.registry is leading to test isolation issues here.

This is why I pin @jensens, @gbastien and @tomgross as the latest contributors to this package and kindly ask for your help/comments.

If other users get a similar error this thread hopefully helps them to solve the issue more quickly now.

However, it should be possible to quickinstall a package that installs collective.z3cform.datagridfield values to the registry in a TestCase's setUp w/o running into test isolation problems.

Hi @frisi

yes we had similar issues when re-applying collective.contact.plonegroup profile in tests (the configuration uses a datagridfield stored in the registry), we get following error :

File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/Products.GenericSetup-1.8.8-py2.7.egg/Products/GenericSetup/tool.py", line 388, in runAllImportStepsFromProfile
    dependency_strategy=dependency_strategy)
   - __traceback_info__: profile-Products.PloneMeeting:default
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/Products.GenericSetup-1.8.8-py2.7.egg/Products/GenericSetup/tool.py", line 1433, in _runImportStepsFromContext
    message = self._doRunImportStep(step, context)
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/Products.GenericSetup-1.8.8-py2.7.egg/Products/GenericSetup/tool.py", line 1245, in _doRunImportStep
    return handler(context)
   - __traceback_info__: contact-plonegroup-postInstall
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/collective.contact.plonegroup-1.10-py2.7.egg/collective/contact/plonegroup/setuphandlers.py", line 17, in postInstall
    registry[FUNCTIONS_REGISTRY] = []
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/plone.registry-1.0.5-py2.7.egg/plone/registry/registry.py", line 47, in __setitem__
    self.records[name].value = value
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/plone.registry-1.0.5-py2.7.egg/plone/registry/record.py", line 80, in _set_value
    field = field.bind(self)
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/zope.schema-4.2.2-py2.7.egg/zope/schema/_field.py", line 471, in bind
    clone.value_type = clone.value_type.bind(object)
  File "/home/jenkins/jobs/buildout-pm41-communes/workspace/eggs/ZODB3-3.10.7-py2.7-linux-x86_64.egg/ZODB/Connection.py", line 857, in setstate
    raise ConnectionStateError(msg)
ConnectionStateError: Shouldn't load state for 0x128253654af0327f when the connection is closed

Finally we workarounded it but this is a strange behavior, like if the object used to persist data in the registry was not doing the work correctly.

This is still using Plone 4.3.x so we hope that it will be solved while using Plone5, we are slowly moving to it but that is huge work...

Gauthier

thanks for sharing your thoughts @gbastien.

for debugging it could be helpful to know if the problem still persists when running plone5.

@frisi Would you have a snippet of how exactly this was done?

i tried to look this up @idgserpro but unfortunately can't find the related code/project anymore. sorry!

in an active plone4.3 project (currently working on 5.2 upgrade) testsetup only includes cookiecuttr zcml but the install profile does not use it.
installation has been done via a generic setup upgrade step to overcome these test-setup problems.

hope this helps

other than that i'd understand the above notes this way (untested code):

from plone.app.testing.bbb import PTC_FIXTURE
from plone.app.testing.helpers import PloneSandboxLayer, applyProfile

class CustomLayer(PloneSandboxLayer):

    # use ptc_fixture as base to make sure everything is setup properly to mimik
    # products.plonetestcase infrastructure
    defaultBases = (PTC_FIXTURE,)

    def setUpZope(self, app, configurationContext):
        # Configure ZCML
        xmlconfig.file('testing.zcml', my.types.tests,
                       context=configurationContext)
        # testing.zcml includes cookiecuttr

        # load overrides https://blog.niteo.co/tag/testing/
        xmlconfig.includeOverrides(configurationContext,
                                   file='overrides.zcml',
                                   package=my.types)

        z2.installProduct(app, 'my.types')

    def setUpPloneSite(self, portal):
        # Activate product
        applyProfile(portal, 'my.types:default')
        applyProfile(portal, 'collective.cookiecuttr:default')
1 Like