Commit 1f4c78b7 authored by Jim Fulton's avatar Jim Fulton

Use doctest rather than zope.testing.doctest.

This required using manuel to get the footnote
feature. Unfortunately, manuel imports zope.testing.doctest. :)
Hpefully, this will be fixed soon.

Updated the testhistoricalconnections.py tearDown to clear conflict
resolution class cache. The cache was causing tests to fail when tests
were run multiple times.
parent 539a20f6
......@@ -185,8 +185,8 @@ setup(name="ZODB3",
read_file("README.txt") + "\n\n" +
read_file("src", "CHANGES.txt")),
test_suite="__main__.alltests", # to support "setup.py test"
tests_require = ['zope.testing'],
extras_require = dict(test=['zope.testing']),
tests_require = ['zope.testing', 'manuel'],
extras_require = dict(test=['zope.testing', 'manuel']),
install_requires = [
'transaction',
'zc.lockfile',
......
......@@ -29,55 +29,62 @@ use:
on the object. If the method succeeds, then the object change can be
committed, otherwise a ConflictError is raised as usual.
::
def _p_resolveConflict(oldState, savedState, newState):
Return the state of the object after resolving different changes.
Arguments:
oldState
The state of the object that the changes made by the current
transaction were based on.
The method is permitted to modify this value.
savedState
The state of the object that is currently stored in the
database. This state was written after oldState and reflects
changes made by a transaction that committed before the
current transaction.
The method is permitted to modify this value.
newState
The state after changes made by the current transaction.
The method is not permitted to modify this value.
This method should compute a new state by merging changes
reflected in savedState and newState, relative to oldState.
If the method cannot resolve the changes, then it should raise
ZODB.POSException.ConflictError.
Consider an extremely simple example, a counter::
from persistent import Persistent
class PCounter(Persistent):
'`value` is readonly; increment it with `inc`.'
_val = 0
def inc(self):
self._val += 1
@property
def value(self):
return self._val
def _p_resolveConflict(self, oldState, savedState, newState):
oldState['_val'] = (
savedState.get('_val', 0) +
newState.get('_val', 0) -
oldState.get('_val', 0))
return oldState
.. -> src
>>> import ConflictResolution_txt
>>> exec src in ConflictResolution_txt.__dict__
>>> PCounter = ConflictResolution_txt.PCounter
>>> PCounter.__module__ = 'ConflictResolution_txt'
def _p_resolveConflict(oldState, savedState, newState):
Return the state of the object after resolving different changes.
Arguments:
oldState
The state of the object that the changes made by the current
transaction were based on.
The method is permitted to modify this value.
savedState
The state of the object that is currently stored in the
database. This state was written after oldState and reflects
changes made by a transaction that committed before the
current transaction.
The method is permitted to modify this value.
newState
The state after changes made by the current transaction.
The method is not permitted to modify this value.
This method should compute a new state by merging changes
reflected in savedState and newState, relative to oldState.
If the method cannot resolve the changes, then it should raise
ZODB.POSException.ConflictError.
Consider an extremely simple example, a counter::
>>> from persistent import Persistent
>>> class PCounter(Persistent):
... '`value` is readonly; increment it with `inc`.'
... _val = 0
... def inc(self):
... self._val += 1
... @property
... def value(self):
... return self._val
... def _p_resolveConflict(self, oldState, savedState, newState):
... oldState['_val'] = (
... savedState.get('_val', 0) +
... newState.get('_val', 0) -
... oldState.get('_val', 0))
... return oldState
...
By "state", the excerpt above means the value used by __getstate__ and
__setstate__: a dictionary, in most cases. We'll look at more details below,
......@@ -149,7 +156,7 @@ But for now, the _p_resolveConflict method is what we have.
Caveats and Dangers
===================
Here are caveats for working with this conflict resolution approach.
Here are caveats for working with this conflict resolution approach.
Each sub-section has a "DANGERS" section that outlines what might happen
if you ignore the warning. We work from the least danger to the most.
......@@ -174,16 +181,21 @@ DANGERS: The changes you make to the instance will be discarded. The
instance is not initialized, so other methods that depend on instance
attributes will not work.
Here's an example of a broken _p_resolveConflict method.
Here's an example of a broken _p_resolveConflict method::
>>> class PCounter2(PCounter):
... def __init__(self):
... self.data = []
... def _p_resolveConflict(self, oldState, savedState, newState):
... self.data.append('bad idea')
... return super(PCounter2, self)._p_resolveConflict(
... oldState, savedState, newState)
...
class PCounter2(PCounter):
def __init__(self):
self.data = []
def _p_resolveConflict(self, oldState, savedState, newState):
self.data.append('bad idea')
return super(PCounter2, self)._p_resolveConflict(
oldState, savedState, newState)
.. -> src
>>> exec src in ConflictResolution_txt.__dict__
>>> PCounter2 = ConflictResolution_txt.PCounter2
>>> PCounter2.__module__ = 'ConflictResolution_txt'
Now we'll prepare for the conflict again.
......@@ -235,35 +247,35 @@ instances. These objects have the following interface::
class IPersistentReference(zope.interface.Interface):
'''public contract for references to persistent objects from an object
with conflicts.'''
oid = zope.interface.Attribute(
'The oid of the persistent object that this reference represents')
database_name = zope.interface.Attribute(
'''The name of the database of the reference, *if* different.
If not different, None.''')
klass = zope.interface.Attribute(
'''class meta data. Presence is not reliable.''')
weak = zope.interface.Attribute(
'''bool: whether this reference is weak''')
def __cmp__(other):
'''if other is equivalent reference, return 0; else raise ValueError.
Equivalent in this case means that oid and database_name are the same.
If either is a weak reference, we only support `is` equivalence, and
otherwise raise a ValueError even if the datbase_names and oids are
the same, rather than guess at the correct semantics.
It is impossible to sort reliably, since the actual persistent
class may have its own comparison, and we have no idea what it is.
We assert that it is reasonably safe to assume that an object is
equivalent to itself, but that's as much as we can say.
We don't compare on 'is other', despite the
PersistentReferenceFactory.data cache, because it is possible to
have two references to the same object that are spelled with different
......@@ -380,13 +392,19 @@ the situation above.
>>> tm_A.abort()
However, the story highlights the kinds of subtle problems that units
made up of multiple composite Persistent objects need to contemplate.
made up of multiple composite Persistent objects need to contemplate.
Any structure made up of objects that contain persistent objects with
conflict resolution code, as a catalog index is made up of multiple
BTree Buckets and Sets, each with conflict resolution, needs to think
through these kinds of problems or be faced with potential data
integrity issues.
.. cleanup
>>> db.close()
>>> db1.close()
>>> db2.close()
.. ......... ..
.. FOOTNOTES ..
.. ......... ..
......@@ -394,16 +412,24 @@ integrity issues.
.. [#get_persistent_reference] We'll catch persistent references with a class
mutable.
>>> class PCounter3(PCounter):
... data = []
... def _p_resolveConflict(self, oldState, savedState, newState):
... PCounter3.data.append(
... (oldState.get('other'),
... savedState.get('other'),
... newState.get('other')))
... return super(PCounter3, self)._p_resolveConflict(
... oldState, savedState, newState)
...
::
class PCounter3(PCounter):
data = []
def _p_resolveConflict(self, oldState, savedState, newState):
PCounter3.data.append(
(oldState.get('other'),
savedState.get('other'),
newState.get('other')))
return super(PCounter3, self)._p_resolveConflict(
oldState, savedState, newState)
.. -> src
>>> exec src in ConflictResolution_txt.__dict__
>>> PCounter3 = ConflictResolution_txt.PCounter3
>>> PCounter3.__module__ = 'ConflictResolution_txt'
>>> p3_A = conn_A.root()['p3'] = PCounter3()
>>> p3_A.other = conn_A.root()['p']
>>> tm_A.commit()
......@@ -441,7 +467,7 @@ integrity issues.
.. [#instantiation_test] We'll simply instantiate PersistentReferences
with examples of types described in ZODB/serialize.py.
>>> from ZODB.ConflictResolution import PersistentReference
>>> ref1 = PersistentReference('my_oid')
......@@ -522,13 +548,13 @@ integrity issues.
True
Non-weak references with the same oid and database_name are equal.
>>> ref1 == ref2 and ref4 == ref5
True
Everything else raises a ValueError: weak references with the same oid and
database, and references with a different database_name or oid.
>>> ref3 == ref6
Traceback (most recent call last):
...
......
......@@ -13,7 +13,7 @@ development continues on a "development" head.
A database can be opened historically ``at`` or ``before`` a given transaction
serial or datetime. Here's a simple example. It should work with any storage
that supports ``loadBefore``.
that supports ``loadBefore``.
We'll begin our example with a fairly standard set up. We
......@@ -29,25 +29,25 @@ We'll begin our example with a fairly standard set up. We
>>> conn = db.open()
>>> import persistent.mapping
>>> conn.root()['first'] = persistent.mapping.PersistentMapping(count=0)
>>> import transaction
>>> transaction.commit()
We wait for some time to pass, record he time, and then make some other changes.
>>> import time
>>> time.sleep(.01)
>>> import datetime
>>> now = datetime.datetime.utcnow()
>>> time.sleep(.01)
>>> root = conn.root()
>>> root['second'] = persistent.mapping.PersistentMapping()
>>> root['first']['count'] += 1
>>> transaction.commit()
Now we will show a historical connection. We'll open one using the ``now``
......@@ -56,14 +56,14 @@ connection, at the mutable head of the database, is different than the
historical state.
>>> transaction1 = transaction.TransactionManager()
>>> historical_conn = db.open(transaction_manager=transaction1, at=now)
>>> sorted(conn.root().keys())
['first', 'second']
>>> conn.root()['first']['count']
1
>>> historical_conn.root().keys()
['first']
>>> historical_conn.root()['first']['count']
......@@ -93,7 +93,7 @@ commit.
>>> historical_serial = historical_conn.root()._p_serial
>>> historical_conn.close()
>>> historical_conn = db.open(transaction_manager=transaction1,
... at=historical_serial)
>>> historical_conn.root().keys()
......@@ -155,7 +155,7 @@ historical connection should be kept.
>>> db.getHistoricalTimeout()
400
All three of these values can be specified in a ZConfig file.
All three of these values can be specified in a ZConfig file.
>>> import ZODB.config
>>> db2 = ZODB.config.databaseFromString('''
......@@ -287,6 +287,12 @@ possible. If historical connections are used for conflict resolution, these
connections will probably be temporary--not saved in a pool--so that the extra
memory usage would also be brief and unlikely to overlap.
.. cleanup
>>> db.close()
>>> db2.close()
.. ......... ..
.. Footnotes ..
.. ......... ..
......
......@@ -11,33 +11,30 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
$Id$
"""
import manuel.doctest
import manuel.footnote
import manuel.capture
import manuel.testing
import unittest
from zope.testing import doctest, module
import ZODB.ConflictResolution
import ZODB.tests.util
import zope.testing.module
def setUp(test):
ZODB.tests.util.setUp(test)
module.setUp(test, 'ConflictResolution_txt')
zope.testing.module.setUp(test, 'ConflictResolution_txt')
def tearDown(test):
test.globs['db'].close()
test.globs['db1'].close()
test.globs['db2'].close()
module.tearDown(test)
zope.testing.module.tearDown(test)
ZODB.tests.util.tearDown(test)
ZODB.ConflictResolution._class_cache.clear()
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('../ConflictResolution.txt',
setUp=setUp,
tearDown=tearDown,
optionflags=doctest.INTERPRET_FOOTNOTES,
),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
return manuel.testing.TestSuite(
manuel.doctest.Manuel()
+ manuel.footnote.Manuel()
+ manuel.capture.Manuel(),
'../ConflictResolution.txt',
setUp=setUp, tearDown=tearDown,
)
......@@ -11,33 +11,14 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
$Id$
"""
import unittest
from zope.testing import doctest, module
import manuel.doctest
import manuel.footnote
import manuel.testing
import ZODB.tests.util
def setUp(test):
ZODB.tests.util.setUp(test)
module.setUp(test, 'historical_connections_txt')
def tearDown(test):
test.globs['db'].close()
test.globs['db2'].close()
# the DB class masks the module because of __init__ shenanigans
module.tearDown(test)
ZODB.tests.util.tearDown(test)
def test_suite():
return unittest.TestSuite((
doctest.DocFileSuite('../historical_connections.txt',
setUp=setUp,
tearDown=tearDown,
optionflags=doctest.INTERPRET_FOOTNOTES,
),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
return manuel.testing.TestSuite(
manuel.doctest.Manuel() + manuel.footnote.Manuel(),
'../historical_connections.txt',
setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment