Commit 18601a1a 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 3ea253ad
...@@ -185,8 +185,8 @@ setup(name="ZODB3", ...@@ -185,8 +185,8 @@ setup(name="ZODB3",
read_file("README.txt") + "\n\n" + read_file("README.txt") + "\n\n" +
read_file("src", "CHANGES.txt")), read_file("src", "CHANGES.txt")),
test_suite="__main__.alltests", # to support "setup.py test" test_suite="__main__.alltests", # to support "setup.py test"
tests_require = ['zope.testing'], tests_require = ['zope.testing', 'manuel'],
extras_require = dict(test=['zope.testing']), extras_require = dict(test=['zope.testing', 'manuel']),
install_requires = [ install_requires = [
'transaction', 'transaction',
'zc.lockfile', 'zc.lockfile',
......
...@@ -29,55 +29,62 @@ use: ...@@ -29,55 +29,62 @@ use:
on the object. If the method succeeds, then the object change can be on the object. If the method succeeds, then the object change can be
committed, otherwise a ConflictError is raised as usual. 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 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, __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. ...@@ -149,7 +156,7 @@ But for now, the _p_resolveConflict method is what we have.
Caveats and Dangers 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 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. 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 ...@@ -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 instance is not initialized, so other methods that depend on instance
attributes will not work. 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): class PCounter2(PCounter):
... def __init__(self): def __init__(self):
... self.data = [] self.data = []
... def _p_resolveConflict(self, oldState, savedState, newState): def _p_resolveConflict(self, oldState, savedState, newState):
... self.data.append('bad idea') self.data.append('bad idea')
... return super(PCounter2, self)._p_resolveConflict( return super(PCounter2, self)._p_resolveConflict(
... oldState, savedState, newState) 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. Now we'll prepare for the conflict again.
...@@ -235,35 +247,35 @@ instances. These objects have the following interface:: ...@@ -235,35 +247,35 @@ instances. These objects have the following interface::
class IPersistentReference(zope.interface.Interface): class IPersistentReference(zope.interface.Interface):
'''public contract for references to persistent objects from an object '''public contract for references to persistent objects from an object
with conflicts.''' with conflicts.'''
oid = zope.interface.Attribute( oid = zope.interface.Attribute(
'The oid of the persistent object that this reference represents') 'The oid of the persistent object that this reference represents')
database_name = zope.interface.Attribute( database_name = zope.interface.Attribute(
'''The name of the database of the reference, *if* different. '''The name of the database of the reference, *if* different.
If not different, None.''') If not different, None.''')
klass = zope.interface.Attribute( klass = zope.interface.Attribute(
'''class meta data. Presence is not reliable.''') '''class meta data. Presence is not reliable.''')
weak = zope.interface.Attribute( weak = zope.interface.Attribute(
'''bool: whether this reference is weak''') '''bool: whether this reference is weak''')
def __cmp__(other): def __cmp__(other):
'''if other is equivalent reference, return 0; else raise ValueError. '''if other is equivalent reference, return 0; else raise ValueError.
Equivalent in this case means that oid and database_name are the same. 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 If either is a weak reference, we only support `is` equivalence, and
otherwise raise a ValueError even if the datbase_names and oids are otherwise raise a ValueError even if the datbase_names and oids are
the same, rather than guess at the correct semantics. the same, rather than guess at the correct semantics.
It is impossible to sort reliably, since the actual persistent It is impossible to sort reliably, since the actual persistent
class may have its own comparison, and we have no idea what it is. 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 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. equivalent to itself, but that's as much as we can say.
We don't compare on 'is other', despite the We don't compare on 'is other', despite the
PersistentReferenceFactory.data cache, because it is possible to PersistentReferenceFactory.data cache, because it is possible to
have two references to the same object that are spelled with different have two references to the same object that are spelled with different
...@@ -380,13 +392,19 @@ the situation above. ...@@ -380,13 +392,19 @@ the situation above.
>>> tm_A.abort() >>> tm_A.abort()
However, the story highlights the kinds of subtle problems that units 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 Any structure made up of objects that contain persistent objects with
conflict resolution code, as a catalog index is made up of multiple conflict resolution code, as a catalog index is made up of multiple
BTree Buckets and Sets, each with conflict resolution, needs to think BTree Buckets and Sets, each with conflict resolution, needs to think
through these kinds of problems or be faced with potential data through these kinds of problems or be faced with potential data
integrity issues. integrity issues.
.. cleanup
>>> db.close()
>>> db1.close()
>>> db2.close()
.. ......... .. .. ......... ..
.. FOOTNOTES .. .. FOOTNOTES ..
.. ......... .. .. ......... ..
...@@ -394,16 +412,24 @@ integrity issues. ...@@ -394,16 +412,24 @@ integrity issues.
.. [#get_persistent_reference] We'll catch persistent references with a class .. [#get_persistent_reference] We'll catch persistent references with a class
mutable. mutable.
>>> class PCounter3(PCounter): ::
... data = []
... def _p_resolveConflict(self, oldState, savedState, newState): class PCounter3(PCounter):
... PCounter3.data.append( data = []
... (oldState.get('other'), def _p_resolveConflict(self, oldState, savedState, newState):
... savedState.get('other'), PCounter3.data.append(
... newState.get('other'))) (oldState.get('other'),
... return super(PCounter3, self)._p_resolveConflict( savedState.get('other'),
... oldState, savedState, newState) 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 = conn_A.root()['p3'] = PCounter3()
>>> p3_A.other = conn_A.root()['p'] >>> p3_A.other = conn_A.root()['p']
>>> tm_A.commit() >>> tm_A.commit()
...@@ -441,7 +467,7 @@ integrity issues. ...@@ -441,7 +467,7 @@ integrity issues.
.. [#instantiation_test] We'll simply instantiate PersistentReferences .. [#instantiation_test] We'll simply instantiate PersistentReferences
with examples of types described in ZODB/serialize.py. with examples of types described in ZODB/serialize.py.
>>> from ZODB.ConflictResolution import PersistentReference >>> from ZODB.ConflictResolution import PersistentReference
>>> ref1 = PersistentReference('my_oid') >>> ref1 = PersistentReference('my_oid')
...@@ -522,13 +548,13 @@ integrity issues. ...@@ -522,13 +548,13 @@ integrity issues.
True True
Non-weak references with the same oid and database_name are equal. Non-weak references with the same oid and database_name are equal.
>>> ref1 == ref2 and ref4 == ref5 >>> ref1 == ref2 and ref4 == ref5
True True
Everything else raises a ValueError: weak references with the same oid and Everything else raises a ValueError: weak references with the same oid and
database, and references with a different database_name or oid. database, and references with a different database_name or oid.
>>> ref3 == ref6 >>> ref3 == ref6
Traceback (most recent call last): Traceback (most recent call last):
... ...
......
...@@ -13,7 +13,7 @@ development continues on a "development" head. ...@@ -13,7 +13,7 @@ development continues on a "development" head.
A database can be opened historically ``at`` or ``before`` a given transaction 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 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 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 ...@@ -29,25 +29,25 @@ We'll begin our example with a fairly standard set up. We
>>> conn = db.open() >>> conn = db.open()
>>> import persistent.mapping >>> import persistent.mapping
>>> conn.root()['first'] = persistent.mapping.PersistentMapping(count=0) >>> conn.root()['first'] = persistent.mapping.PersistentMapping(count=0)
>>> import transaction >>> import transaction
>>> transaction.commit() >>> transaction.commit()
We wait for some time to pass, record he time, and then make some other changes. We wait for some time to pass, record he time, and then make some other changes.
>>> import time >>> import time
>>> time.sleep(.01) >>> time.sleep(.01)
>>> import datetime >>> import datetime
>>> now = datetime.datetime.utcnow() >>> now = datetime.datetime.utcnow()
>>> time.sleep(.01) >>> time.sleep(.01)
>>> root = conn.root() >>> root = conn.root()
>>> root['second'] = persistent.mapping.PersistentMapping() >>> root['second'] = persistent.mapping.PersistentMapping()
>>> root['first']['count'] += 1 >>> root['first']['count'] += 1
>>> transaction.commit() >>> transaction.commit()
Now we will show a historical connection. We'll open one using the ``now`` 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 ...@@ -56,14 +56,14 @@ connection, at the mutable head of the database, is different than the
historical state. historical state.
>>> transaction1 = transaction.TransactionManager() >>> transaction1 = transaction.TransactionManager()
>>> historical_conn = db.open(transaction_manager=transaction1, at=now) >>> historical_conn = db.open(transaction_manager=transaction1, at=now)
>>> sorted(conn.root().keys()) >>> sorted(conn.root().keys())
['first', 'second'] ['first', 'second']
>>> conn.root()['first']['count'] >>> conn.root()['first']['count']
1 1
>>> historical_conn.root().keys() >>> historical_conn.root().keys()
['first'] ['first']
>>> historical_conn.root()['first']['count'] >>> historical_conn.root()['first']['count']
...@@ -93,7 +93,7 @@ commit. ...@@ -93,7 +93,7 @@ commit.
>>> historical_serial = historical_conn.root()._p_serial >>> historical_serial = historical_conn.root()._p_serial
>>> historical_conn.close() >>> historical_conn.close()
>>> historical_conn = db.open(transaction_manager=transaction1, >>> historical_conn = db.open(transaction_manager=transaction1,
... at=historical_serial) ... at=historical_serial)
>>> historical_conn.root().keys() >>> historical_conn.root().keys()
...@@ -155,7 +155,7 @@ historical connection should be kept. ...@@ -155,7 +155,7 @@ historical connection should be kept.
>>> db.getHistoricalTimeout() >>> db.getHistoricalTimeout()
400 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 >>> import ZODB.config
>>> db2 = ZODB.config.databaseFromString(''' >>> db2 = ZODB.config.databaseFromString('''
...@@ -287,6 +287,12 @@ possible. If historical connections are used for conflict resolution, these ...@@ -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 connections will probably be temporary--not saved in a pool--so that the extra
memory usage would also be brief and unlikely to overlap. memory usage would also be brief and unlikely to overlap.
.. cleanup
>>> db.close()
>>> db2.close()
.. ......... .. .. ......... ..
.. Footnotes .. .. Footnotes ..
.. ......... .. .. ......... ..
......
...@@ -11,33 +11,30 @@ ...@@ -11,33 +11,30 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
""" import manuel.doctest
$Id$ import manuel.footnote
""" import manuel.capture
import manuel.testing
import unittest import unittest
from zope.testing import doctest, module import ZODB.ConflictResolution
import ZODB.tests.util import ZODB.tests.util
import zope.testing.module
def setUp(test): def setUp(test):
ZODB.tests.util.setUp(test) ZODB.tests.util.setUp(test)
module.setUp(test, 'ConflictResolution_txt') zope.testing.module.setUp(test, 'ConflictResolution_txt')
def tearDown(test): def tearDown(test):
test.globs['db'].close() zope.testing.module.tearDown(test)
test.globs['db1'].close()
test.globs['db2'].close()
module.tearDown(test)
ZODB.tests.util.tearDown(test) ZODB.tests.util.tearDown(test)
ZODB.ConflictResolution._class_cache.clear()
def test_suite(): def test_suite():
return unittest.TestSuite(( return manuel.testing.TestSuite(
doctest.DocFileSuite('../ConflictResolution.txt', manuel.doctest.Manuel()
setUp=setUp, + manuel.footnote.Manuel()
tearDown=tearDown, + manuel.capture.Manuel(),
optionflags=doctest.INTERPRET_FOOTNOTES, '../ConflictResolution.txt',
), setUp=setUp, tearDown=tearDown,
)) )
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
...@@ -11,33 +11,14 @@ ...@@ -11,33 +11,14 @@
# FOR A PARTICULAR PURPOSE. # FOR A PARTICULAR PURPOSE.
# #
############################################################################## ##############################################################################
""" import manuel.doctest
$Id$ import manuel.footnote
""" import manuel.testing
import unittest
from zope.testing import doctest, module
import ZODB.tests.util 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(): def test_suite():
return unittest.TestSuite(( return manuel.testing.TestSuite(
doctest.DocFileSuite('../historical_connections.txt', manuel.doctest.Manuel() + manuel.footnote.Manuel(),
setUp=setUp, '../historical_connections.txt',
tearDown=tearDown, setUp=ZODB.tests.util.setUp, tearDown=ZODB.tests.util.tearDown,
optionflags=doctest.INTERPRET_FOOTNOTES, )
),
))
if __name__ == '__main__':
unittest.main(defaultTest='test_suite')
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