[Checkins] SVN: transaction/trunk/ Merge 'sphinx' branch.
Tres Seaver
cvs-admin at zope.org
Tue Dec 18 05:26:55 UTC 2012
Log message for revision 128757:
Merge 'sphinx' branch.
Changed:
_U transaction/trunk/
U transaction/trunk/CHANGES.txt
A transaction/trunk/docs/convenience.rst
A transaction/trunk/docs/datamanager.rst
A transaction/trunk/docs/doom.rst
A transaction/trunk/docs/hooks.rst
U transaction/trunk/docs/index.rst
A transaction/trunk/docs/resourcemanager.rst
A transaction/trunk/docs/savepoint.rst
U transaction/trunk/setup.py
A transaction/trunk/transaction/_compat.py
U transaction/trunk/transaction/_manager.py
U transaction/trunk/transaction/_transaction.py
D transaction/trunk/transaction/compat.py
A transaction/trunk/transaction/tests/common.py
D transaction/trunk/transaction/tests/convenience.txt
D transaction/trunk/transaction/tests/doom.txt
A transaction/trunk/transaction/tests/examples.py
D transaction/trunk/transaction/tests/sampledm.py
D transaction/trunk/transaction/tests/savepoint.txt
U transaction/trunk/transaction/tests/savepointsample.py
D transaction/trunk/transaction/tests/test_SampleDataManager.py
D transaction/trunk/transaction/tests/test_SampleResourceManager.py
A transaction/trunk/transaction/tests/test__manager.py
A transaction/trunk/transaction/tests/test__transaction.py
D transaction/trunk/transaction/tests/test_attempt.py
U transaction/trunk/transaction/tests/test_register_compat.py
U transaction/trunk/transaction/tests/test_savepoint.py
D transaction/trunk/transaction/tests/test_transaction.py
U transaction/trunk/transaction/tests/test_weakset.py
-=-
Property changes on: transaction/trunk
___________________________________________________________________
Added: svn:mergeinfo
+ /transaction/branches/sphinx:128695-128726,128733-128739,128741-128756
Added: svk:merge
+ 62d5b8a3-27da-0310-9561-8e5933582275:/transaction/branches/sphinx:128756
Modified: transaction/trunk/CHANGES.txt
===================================================================
--- transaction/trunk/CHANGES.txt 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/CHANGES.txt 2012-12-18 05:26:55 UTC (rev 128757)
@@ -4,6 +4,20 @@
1.3.1 (unreleased)
------------------
+- Refactored existing doctests as Sphinx documentation (snippets are exercised
+ via 'tox').
+
+- 100% unit test coverage.
+
+- Raise ValueError from ``Transaction.doom`` if the transaction is in a
+ non-doomable state (rather than using ``assert``).
+
+- Raise ValueError from ``TransactionManager.attempts`` if passed a
+ non-positive value (rather than using ``assert``).
+
+- Raise ValueError from ``TransactionManager.free`` if passed a foreign
+ transaction (rather tna using ``assert``).
+
- Declared support for Python 3.3 in ``setup.py``, and added ``tox`` testing.
- When a non-retryable exception was raised as the result of a call to
Copied: transaction/trunk/docs/convenience.rst (from rev 128495, transaction/trunk/transaction/tests/convenience.txt)
===================================================================
--- transaction/trunk/docs/convenience.rst (rev 0)
+++ transaction/trunk/docs/convenience.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,180 @@
+Transaction convenience support
+===============================
+
+(We *really* need to write proper documentation for the transaction
+ package, but I don't want to block the conveniences documented here
+ for that.)
+
+with support
+------------
+
+We can now use the with statement to define transaction boundaries.
+
+.. doctest::
+
+ >>> import transaction.tests.savepointsample
+ >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
+ >>> list(dm.keys())
+ []
+
+We can use it with a manager:
+
+.. doctest::
+
+ >>> with transaction.manager as t:
+ ... dm['z'] = 3
+ ... t.note('test 3')
+
+ >>> dm['z']
+ 3
+
+ >>> dm.last_note
+ 'test 3'
+
+ >>> with transaction.manager: #doctest ELLIPSIS
+ ... dm['z'] = 4
+ ... xxx
+ Traceback (most recent call last):
+ ...
+ NameError: ... name 'xxx' is not defined
+
+ >>> dm['z']
+ 3
+
+On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with
+transaction:``. This does not work on Python 3 (see see
+http://bugs.python.org/issue12022).
+
+Retries
+-------
+
+Commits can fail for transient reasons, especially conflicts.
+Applications will often retry transactions some number of times to
+overcome transient failures. This typically looks something like:
+
+.. doctest::
+
+ for i in range(3):
+ try:
+ with transaction.manager:
+ ... some something ...
+ except SomeTransientException:
+ contine
+ else:
+ break
+
+This is rather ugly.
+
+Transaction managers provide a helper for this case. To show this,
+we'll use a contrived example:
+
+.. doctest::
+
+ >>> ntry = 0
+ >>> with transaction.manager:
+ ... dm['ntry'] = 0
+
+ >>> import transaction.interfaces
+ >>> class Retry(transaction.interfaces.TransientError):
+ ... pass
+
+ >>> for attempt in transaction.manager.attempts():
+ ... with attempt as t:
+ ... t.note('test')
+ ... print("%s %s" % (dm['ntry'], ntry))
+ ... ntry += 1
+ ... dm['ntry'] = ntry
+ ... if ntry % 3:
+ ... raise Retry(ntry)
+ 0 0
+ 0 1
+ 0 2
+
+The raising of a subclass of TransientError is critical here. It's
+what signals that the transaction should be retried. It is generally
+up to the data manager to signal that a transaction should try again
+by raising a subclass of TransientError (or TransientError itself, of
+course).
+
+You shouldn't make any assumptions about the object returned by the
+iterator. (It isn't a transaction or transaction manager, as far as
+you know. :) If you use the ``as`` keyword in the ``with`` statement,
+a transaction object will be assigned to the variable named.
+
+By default, it tries 3 times. We can tell it how many times to try:
+
+.. doctest::
+
+ >>> for attempt in transaction.manager.attempts(2):
+ ... with attempt:
+ ... ntry += 1
+ ... if ntry % 3:
+ ... raise Retry(ntry)
+ Traceback (most recent call last):
+ ...
+ Retry: 5
+
+It it doesn't succeed in that many times, the exception will be
+propagated.
+
+Of course, other errors are propagated directly:
+
+.. doctest::
+
+ >>> ntry = 0
+ >>> for attempt in transaction.manager.attempts():
+ ... with attempt:
+ ... ntry += 1
+ ... if ntry == 3:
+ ... raise ValueError(ntry)
+ Traceback (most recent call last):
+ ...
+ ValueError: 3
+
+We can use the default transaction manager:
+
+.. doctest::
+
+ >>> for attempt in transaction.attempts():
+ ... with attempt as t:
+ ... t.note('test')
+ ... print("%s %s" % (dm['ntry'], ntry))
+ ... ntry += 1
+ ... dm['ntry'] = ntry
+ ... if ntry % 3:
+ ... raise Retry(ntry)
+ 3 3
+ 3 4
+ 3 5
+
+Sometimes, a data manager doesn't raise exceptions directly, but
+wraps other other systems that raise exceptions outside of it's
+control. Data managers can provide a should_retry method that takes
+an exception instance and returns True if the transaction should be
+attempted again.
+
+.. doctest::
+
+ >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
+ ... def should_retry(self, e):
+ ... if 'should retry' in str(e):
+ ... return True
+
+ >>> ntry = 0
+ >>> dm2 = DM()
+ >>> with transaction.manager:
+ ... dm2['ntry'] = 0
+ >>> for attempt in transaction.manager.attempts():
+ ... with attempt:
+ ... print("%s %s" % (dm['ntry'], ntry))
+ ... ntry += 1
+ ... dm['ntry'] = ntry
+ ... dm2['ntry'] = ntry
+ ... if ntry % 3:
+ ... raise ValueError('we really should retry this')
+ 6 0
+ 6 1
+ 6 2
+
+ >>> dm2['ntry']
+ 3
Copied: transaction/trunk/docs/datamanager.rst (from rev 128756, transaction/branches/sphinx/docs/datamanager.rst)
===================================================================
--- transaction/trunk/docs/datamanager.rst (rev 0)
+++ transaction/trunk/docs/datamanager.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,391 @@
+Writing a Data Manager
+======================
+
+Simple Data Manager
+------------------
+
+.. doctest::
+
+ >>> from transaction.tests.examples import DataManager
+
+This :class:`transaction.tests.examples.DataManager` class
+provides a trivial data-manager implementation and docstrings to illustrate
+the the protocol and to provide a tool for writing tests.
+
+Our sample data manager has state that is updated through an inc
+method and through transaction operations.
+
+
+When we create a sample data manager:
+
+.. doctest::
+
+ >>> dm = DataManager()
+
+It has two bits of state, state:
+
+.. doctest::
+
+ >>> dm.state
+ 0
+
+and delta:
+
+.. doctest::
+
+ >>> dm.delta
+ 0
+
+Both of which are initialized to 0. state is meant to model
+committed state, while delta represents tentative changes within a
+transaction. We change the state by calling inc:
+
+.. doctest::
+
+ >>> dm.inc()
+
+which updates delta:
+
+.. doctest::
+
+ >>> dm.delta
+ 1
+
+but state isn't changed until we commit the transaction:
+
+.. doctest::
+
+ >>> dm.state
+ 0
+
+To commit the changes, we use 2-phase commit. We execute the first
+stage by calling prepare. We need to pass a transation. Our
+sample data managers don't really use the transactions for much,
+so we'll be lazy and use strings for transactions:
+
+.. doctest::
+
+ >>> t1 = '1'
+ >>> dm.prepare(t1)
+
+The sample data manager updates the state when we call prepare:
+
+.. doctest::
+
+ >>> dm.state
+ 1
+ >>> dm.delta
+ 1
+
+This is mainly so we can detect some affect of calling the methods.
+
+Now if we call commit:
+
+.. doctest::
+
+ >>> dm.commit(t1)
+
+Our changes are"permanent". The state reflects the changes and the
+delta has been reset to 0.
+
+.. doctest::
+
+ >>> dm.state
+ 1
+ >>> dm.delta
+ 0
+
+The :meth:`prepare` Method
+----------------------------
+
+Prepare to commit data
+
+.. doctest::
+
+ >>> dm = DataManager()
+ >>> dm.inc()
+ >>> t1 = '1'
+ >>> dm.prepare(t1)
+ >>> dm.commit(t1)
+ >>> dm.state
+ 1
+ >>> dm.inc()
+ >>> t2 = '2'
+ >>> dm.prepare(t2)
+ >>> dm.abort(t2)
+ >>> dm.state
+ 1
+
+It is en error to call prepare more than once without an intervening
+commit or abort:
+
+.. doctest::
+
+ >>> dm.prepare(t1)
+
+ >>> dm.prepare(t1)
+ Traceback (most recent call last):
+ ...
+ TypeError: Already prepared
+
+ >>> dm.prepare(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: Already prepared
+
+ >>> dm.abort(t1)
+
+If there was a preceeding savepoint, the transaction must match:
+
+.. doctest::
+
+ >>> rollback = dm.savepoint(t1)
+ >>> dm.prepare(t2)
+ Traceback (most recent call last):
+ ,,,
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> dm.prepare(t1)
+
+The :meth:`abort` method
+--------------------------
+
+The abort method can be called before two-phase commit to
+throw away work done in the transaction:
+
+.. doctest::
+
+ >>> dm = DataManager()
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (0, 1)
+ >>> t1 = '1'
+ >>> dm.abort(t1)
+ >>> dm.state, dm.delta
+ (0, 0)
+
+The abort method also throws away work done in savepoints:
+
+.. doctest::
+
+ >>> dm.inc()
+ >>> r = dm.savepoint(t1)
+ >>> dm.inc()
+ >>> r = dm.savepoint(t1)
+ >>> dm.state, dm.delta
+ (0, 2)
+ >>> dm.abort(t1)
+ >>> dm.state, dm.delta
+ (0, 0)
+
+If savepoints are used, abort must be passed the same
+transaction:
+
+.. doctest::
+
+ >>> dm.inc()
+ >>> r = dm.savepoint(t1)
+ >>> t2 = '2'
+ >>> dm.abort(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> dm.abort(t1)
+
+The abort method is also used to abort a two-phase commit:
+
+.. doctest::
+
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (0, 1)
+ >>> dm.prepare(t1)
+ >>> dm.state, dm.delta
+ (1, 1)
+ >>> dm.abort(t1)
+ >>> dm.state, dm.delta
+ (0, 0)
+
+Of course, the transactions passed to prepare and abort must
+match:
+
+.. doctest::
+
+ >>> dm.prepare(t1)
+ >>> dm.abort(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> dm.abort(t1)
+
+
+
+The :meth:`commit` method
+---------------------------
+
+Called to omplete two-phase commit
+
+.. doctest::
+
+ >>> dm = DataManager()
+ >>> dm.state
+ 0
+ >>> dm.inc()
+
+We start two-phase commit by calling prepare:
+
+.. doctest::
+
+ >>> t1 = '1'
+ >>> dm.prepare(t1)
+
+ We complete it by calling commit:
+
+.. doctest::
+
+ >>> dm.commit(t1)
+ >>> dm.state
+ 1
+
+It is an error ro call commit without calling prepare first:
+
+.. doctest::
+
+ >>> dm.inc()
+ >>> t2 = '2'
+ >>> dm.commit(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: Not prepared to commit
+
+ >>> dm.prepare(t2)
+ >>> dm.commit(t2)
+
+If course, the transactions given to prepare and commit must
+be the same:
+
+.. doctest::
+
+ >>> dm.inc()
+ >>> t3 = '3'
+ >>> dm.prepare(t3)
+ >>> dm.commit(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '3')
+
+
+The :meth:`savepoint` method
+------------------------------
+
+Provide the ability to rollback transaction state
+
+Savepoints provide a way to:
+
+ - Save partial transaction work. For some data managers, this
+ could allow resources to be used more efficiently.
+
+ - Provide the ability to revert state to a point in a
+ transaction without aborting the entire transaction. In
+ other words, savepoints support partial aborts.
+
+Savepoints don't use two-phase commit. If there are errors in
+setting or rolling back to savepoints, the application should
+abort the containing transaction. This is *not* the
+responsibility of the data manager.
+
+Savepoints are always associated with a transaction. Any work
+done in a savepoint's transaction is tentative until the
+transaction is committed using two-phase commit.
+
+.. doctest::
+
+ >>> dm = DataManager()
+ >>> dm.inc()
+ >>> t1 = '1'
+ >>> r = dm.savepoint(t1)
+ >>> dm.state, dm.delta
+ (0, 1)
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (0, 2)
+ >>> r.rollback()
+ >>> dm.state, dm.delta
+ (0, 1)
+ >>> dm.prepare(t1)
+ >>> dm.commit(t1)
+ >>> dm.state, dm.delta
+ (1, 0)
+
+Savepoints must have the same transaction:
+
+.. doctest::
+
+ >>> r1 = dm.savepoint(t1)
+ >>> dm.state, dm.delta
+ (1, 0)
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (1, 1)
+ >>> t2 = '2'
+ >>> r2 = dm.savepoint(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> r2 = dm.savepoint(t1)
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (1, 2)
+
+If we rollback to an earlier savepoint, we discard all work
+done later:
+
+.. doctest::
+
+ >>> r1.rollback()
+ >>> dm.state, dm.delta
+ (1, 0)
+
+and we can no longer rollback to the later savepoint:
+
+.. doctest::
+
+ >>> r2.rollback()
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+We can roll back to a savepoint as often as we like:
+
+.. doctest::
+
+ >>> r1.rollback()
+ >>> r1.rollback()
+ >>> r1.rollback()
+ >>> dm.state, dm.delta
+ (1, 0)
+
+ >>> dm.inc()
+ >>> dm.inc()
+ >>> dm.inc()
+ >>> dm.state, dm.delta
+ (1, 3)
+ >>> r1.rollback()
+ >>> dm.state, dm.delta
+ (1, 0)
+
+But we can't rollback to a savepoint after it has been
+committed:
+
+.. doctest::
+
+ >>> dm.prepare(t1)
+ >>> dm.commit(t1)
+
+ >>> r1.rollback()
+ Traceback (most recent call last):
+ ...
+ TypeError: Attempt to rollback stale rollback
Copied: transaction/trunk/docs/doom.rst (from rev 128495, transaction/trunk/transaction/tests/doom.txt)
===================================================================
--- transaction/trunk/docs/doom.rst (rev 0)
+++ transaction/trunk/docs/doom.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,164 @@
+Dooming Transactions
+====================
+
+A doomed transaction behaves exactly the same way as an active transaction but
+raises an error on any attempt to commit it, thus forcing an abort.
+
+Doom is useful in places where abort is unsafe and an exception cannot be
+raised. This occurs when the programmer wants the code following the doom to
+run but not commit. It is unsafe to abort in these circumstances as a following
+get() may implicitly open a new transaction.
+
+Any attempt to commit a doomed transaction will raise a DoomedTransaction
+exception.
+
+An example of such a use case can be found in
+zope/app/form/browser/editview.py. Here a form validation failure must doom
+the transaction as committing the transaction may have side-effects. However,
+the form code must continue to calculate a form containing the error messages
+to return.
+
+For Zope in general, code running within a request should always doom
+transactions rather than aborting them. It is the responsibilty of the
+publication to either abort() or commit() the transaction. Application code can
+use savepoints and doom() safely.
+
+To see how it works we first need to create a stub data manager:
+
+.. doctest::
+
+ >>> from transaction.interfaces import IDataManager
+ >>> from zope.interface import implementer
+ >>> @implementer(IDataManager)
+ ... class DataManager:
+ ... def __init__(self):
+ ... self.attr_counter = {}
+ ... def __getattr__(self, name):
+ ... def f(transaction):
+ ... self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
+ ... return f
+ ... def total(self):
+ ... count = 0
+ ... for access_count in self.attr_counter.values():
+ ... count += access_count
+ ... return count
+ ... def sortKey(self):
+ ... return 1
+
+Start a new transaction:
+
+.. doctest::
+
+ >>> import transaction
+ >>> txn = transaction.begin()
+ >>> dm = DataManager()
+ >>> txn.join(dm)
+
+We can ask a transaction if it is doomed to avoid expensive operations. An
+example of a use case is an object-relational mapper where a pre-commit hook
+sends all outstanding SQL to a relational database for objects changed during
+the transaction. This expensive operation is not necessary if the transaction
+has been doomed. A non-doomed transaction should return False:
+
+.. doctest::
+
+ >>> txn.isDoomed()
+ False
+
+We can doom a transaction by calling .doom() on it:
+
+.. doctest::
+
+ >>> txn.doom()
+ >>> txn.isDoomed()
+ True
+
+We can doom it again if we like:
+
+.. doctest::
+
+ >>> txn.doom()
+
+The data manager is unchanged at this point:
+
+.. doctest::
+
+ >>> dm.total()
+ 0
+
+Attempting to commit a doomed transaction any number of times raises a
+DoomedTransaction:
+
+.. doctest::
+
+ >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ DoomedTransaction: transaction doomed, cannot commit
+ >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ DoomedTransaction: transaction doomed, cannot commit
+
+But still leaves the data manager unchanged:
+
+.. doctest::
+
+ >>> dm.total()
+ 0
+
+But the doomed transaction can be aborted:
+
+.. doctest::
+
+ >>> txn.abort()
+
+Which aborts the data manager:
+
+.. doctest::
+
+ >>> dm.total()
+ 1
+ >>> dm.attr_counter['abort']
+ 1
+
+Dooming the current transaction can also be done directly from the transaction
+module. We can also begin a new transaction directly after dooming the old one:
+
+.. doctest::
+
+ >>> txn = transaction.begin()
+ >>> transaction.isDoomed()
+ False
+ >>> transaction.doom()
+ >>> transaction.isDoomed()
+ True
+ >>> txn = transaction.begin()
+
+After committing a transaction we get an assertion error if we try to doom the
+transaction. This could be made more specific, but trying to doom a transaction
+after it's been committed is probably a programming error:
+
+.. doctest::
+
+ >>> txn = transaction.begin()
+ >>> txn.commit()
+ >>> txn.doom()
+ Traceback (most recent call last):
+ ...
+ ValueError: non-doomable
+
+A doomed transaction should act the same as an active transaction, so we should
+be able to join it:
+
+.. doctest::
+
+ >>> txn = transaction.begin()
+ >>> txn.doom()
+ >>> dm2 = DataManager()
+ >>> txn.join(dm2)
+
+Clean up:
+
+.. doctest::
+
+ >>> txn = transaction.begin()
+ >>> txn.abort()
Copied: transaction/trunk/docs/hooks.rst (from rev 128756, transaction/branches/sphinx/docs/hooks.rst)
===================================================================
--- transaction/trunk/docs/hooks.rst (rev 0)
+++ transaction/trunk/docs/hooks.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,384 @@
+Hooking the Transaction Machinery
+=================================
+
+The :meth:`addBeforeCommitHook` Method
+--------------------------------------
+
+Let's define a hook to call, and a way to see that it was called.
+
+.. doctest::
+
+ >>> log = []
+ >>> def reset_log():
+ ... del log[:]
+
+ >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+ ... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
+
+Now register the hook with a transaction.
+
+.. doctest::
+
+ >>> from transaction import begin
+ >>> from transaction._compat import func_name
+ >>> import transaction
+ >>> t = begin()
+ >>> t.addBeforeCommitHook(hook, '1')
+
+We can see that the hook is indeed registered.
+
+.. doctest::
+
+ >>> [(func_name(hook), args, kws)
+ ... for hook, args, kws in t.getBeforeCommitHooks()]
+ [('hook', ('1',), {})]
+
+When transaction commit starts, the hook is called, with its
+arguments.
+
+.. doctest::
+
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+A hook's registration is consumed whenever the hook is called. Since
+the hook above was called, it's no longer registered:
+
+.. doctest::
+
+ >>> from transaction import commit
+ >>> len(list(t.getBeforeCommitHooks()))
+ 0
+ >>> commit()
+ >>> log
+ []
+
+The hook is only called for a full commit, not for a savepoint.
+
+.. doctest::
+
+ >>> t = begin()
+ >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
+ >>> dummy = t.savepoint()
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["arg 'A' kw1 'B' kw2 'no_kw2'"]
+ >>> reset_log()
+
+If a transaction is aborted, no hook is called.
+
+.. doctest::
+
+ >>> from transaction import abort
+ >>> t = begin()
+ >>> t.addBeforeCommitHook(hook, ["OOPS!"])
+ >>> abort()
+ >>> log
+ []
+ >>> commit()
+ >>> log
+ []
+
+The hook is called before the commit does anything, so even if the
+commit fails the hook will have been called. To provoke failures in
+commit, we'll add failing resource manager to the transaction.
+
+.. doctest::
+
+ >>> class CommitFailure(Exception):
+ ... pass
+ >>> class FailingDataManager:
+ ... def tpc_begin(self, txn, sub=False):
+ ... raise CommitFailure('failed')
+ ... def abort(self, txn):
+ ... pass
+
+ >>> t = begin()
+ >>> t.join(FailingDataManager())
+
+ >>> t.addBeforeCommitHook(hook, '2')
+
+ >>> from transaction.tests.common import DummyFile
+ >>> from transaction.tests.common import Monkey
+ >>> from transaction.tests.common import assertRaisesEx
+ >>> from transaction import _transaction
+ >>> buffer = DummyFile()
+ >>> with Monkey(_transaction, _TB_BUFFER=buffer):
+ ... err = assertRaisesEx(CommitFailure, t.commit)
+ >>> log
+ ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+Let's register several hooks.
+
+.. doctest::
+
+ >>> t = begin()
+ >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
+ >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
+
+They are returned in the same order by getBeforeCommitHooks.
+
+.. doctest::
+
+ >>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
+ ... for hook, args, kws in t.getBeforeCommitHooks()]
+ [('hook', ('4',), {'kw1': '4.1'}),
+ ('hook', ('5',), {'kw2': '5.2'})]
+
+And commit also calls them in this order.
+
+.. doctest::
+
+ >>> t.commit()
+ >>> len(log)
+ 2
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ["arg '4' kw1 '4.1' kw2 'no_kw2'",
+ "arg '5' kw1 'no_kw1' kw2 '5.2'"]
+ >>> reset_log()
+
+While executing, a hook can itself add more hooks, and they will all
+be called before the real commit starts.
+
+.. doctest::
+
+ >>> def recurse(txn, arg):
+ ... log.append('rec' + str(arg))
+ ... if arg:
+ ... txn.addBeforeCommitHook(hook, '-')
+ ... txn.addBeforeCommitHook(recurse, (txn, arg-1))
+
+ >>> t = begin()
+ >>> t.addBeforeCommitHook(recurse, (t, 3))
+ >>> commit()
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ['rec3',
+ "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec2',
+ "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec1',
+ "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec0']
+ >>> reset_log()
+
+The :meth:`addAfterCommitHook` Method
+--------------------------------------
+
+Let's define a hook to call, and a way to see that it was called.
+
+.. doctest::
+
+ >>> log = []
+ >>> def reset_log():
+ ... del log[:]
+
+ >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+ ... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
+
+Now register the hook with a transaction.
+
+.. doctest::
+
+ >>> from transaction import begin
+ >>> from transaction._compat import func_name
+ >>> t = begin()
+ >>> t.addAfterCommitHook(hook, '1')
+
+We can see that the hook is indeed registered.
+
+.. doctest::
+
+
+ >>> [(func_name(hook), args, kws)
+ ... for hook, args, kws in t.getAfterCommitHooks()]
+ [('hook', ('1',), {})]
+
+When transaction commit is done, the hook is called, with its
+arguments.
+
+.. doctest::
+
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+A hook's registration is consumed whenever the hook is called. Since
+the hook above was called, it's no longer registered:
+
+.. doctest::
+
+ >>> from transaction import commit
+ >>> len(list(t.getAfterCommitHooks()))
+ 0
+ >>> commit()
+ >>> log
+ []
+
+The hook is only called after a full commit, not for a savepoint.
+
+.. doctest::
+
+ >>> t = begin()
+ >>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
+ >>> dummy = t.savepoint()
+ >>> log
+ []
+ >>> t.commit()
+ >>> log
+ ["True arg 'A' kw1 'B' kw2 'no_kw2'"]
+ >>> reset_log()
+
+If a transaction is aborted, no hook is called.
+
+.. doctest::
+
+ >>> from transaction import abort
+ >>> t = begin()
+ >>> t.addAfterCommitHook(hook, ["OOPS!"])
+ >>> abort()
+ >>> log
+ []
+ >>> commit()
+ >>> log
+ []
+
+The hook is called after the commit is done, so even if the
+commit fails the hook will have been called. To provoke failures in
+commit, we'll add failing resource manager to the transaction.
+
+.. doctest::
+
+ >>> class CommitFailure(Exception):
+ ... pass
+ >>> class FailingDataManager:
+ ... def tpc_begin(self, txn):
+ ... raise CommitFailure('failed')
+ ... def abort(self, txn):
+ ... pass
+
+ >>> t = begin()
+ >>> t.join(FailingDataManager())
+
+ >>> t.addAfterCommitHook(hook, '2')
+ >>> from transaction.tests.common import DummyFile
+ >>> from transaction.tests.common import Monkey
+ >>> from transaction.tests.common import assertRaisesEx
+ >>> from transaction import _transaction
+ >>> buffer = DummyFile()
+ >>> with Monkey(_transaction, _TB_BUFFER=buffer):
+ ... err = assertRaisesEx(CommitFailure, t.commit)
+ >>> log
+ ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
+ >>> reset_log()
+
+Let's register several hooks.
+
+.. doctest::
+
+ >>> t = begin()
+ >>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
+ >>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
+
+They are returned in the same order by getAfterCommitHooks.
+
+.. doctest::
+
+ >>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
+ ... for hook, args, kws in t.getAfterCommitHooks()]
+ [('hook', ('4',), {'kw1': '4.1'}),
+ ('hook', ('5',), {'kw2': '5.2'})]
+
+And commit also calls them in this order.
+
+.. doctest::
+
+ >>> t.commit()
+ >>> len(log)
+ 2
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ["True arg '4' kw1 '4.1' kw2 'no_kw2'",
+ "True arg '5' kw1 'no_kw1' kw2 '5.2'"]
+ >>> reset_log()
+
+While executing, a hook can itself add more hooks, and they will all
+be called before the real commit starts.
+
+.. doctest::
+
+ >>> def recurse(status, txn, arg):
+ ... log.append('rec' + str(arg))
+ ... if arg:
+ ... txn.addAfterCommitHook(hook, '-')
+ ... txn.addAfterCommitHook(recurse, (txn, arg-1))
+
+ >>> t = begin()
+ >>> t.addAfterCommitHook(recurse, (t, 3))
+ >>> commit()
+ >>> log #doctest: +NORMALIZE_WHITESPACE
+ ['rec3',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec2',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec1',
+ "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
+ 'rec0']
+ >>> reset_log()
+
+If an after commit hook is raising an exception then it will log a
+message at error level so that if other hooks are registered they
+can be executed. We don't support execution dependencies at this level.
+
+.. doctest::
+
+ >>> from transaction import TransactionManager
+ >>> from transaction.tests.test__manager import DataObject
+ >>> mgr = TransactionManager()
+ >>> do = DataObject(mgr)
+
+ >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
+ ... raise TypeError("Fake raise")
+
+ >>> t = begin()
+
+ >>> t.addAfterCommitHook(hook, ('-', 1))
+ >>> t.addAfterCommitHook(hookRaise, ('-', 2))
+ >>> t.addAfterCommitHook(hook, ('-', 3))
+ >>> commit()
+
+ >>> log
+ ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
+
+ >>> reset_log()
+
+Test that the associated transaction manager has been cleanup when
+after commit hooks are registered
+
+.. doctest::
+
+ >>> mgr = TransactionManager()
+ >>> do = DataObject(mgr)
+
+ >>> t = begin()
+ >>> t._manager._txn is not None
+ True
+
+ >>> t.addAfterCommitHook(hook, ('-', 1))
+ >>> commit()
+
+ >>> log
+ ["True arg '-' kw1 1 kw2 'no_kw2'"]
+
+ >>> t._manager._txn is not None
+ False
+
+ >>> reset_log()
Modified: transaction/trunk/docs/index.rst
===================================================================
--- transaction/trunk/docs/index.rst 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/docs/index.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,11 +1,102 @@
:mod:`transaction` Documentation
================================
+Transaction objects manage resources for an individual activity.
+
+Compatibility issues
+--------------------
+
+The implementation of Transaction objects involves two layers of
+backwards compatibility, because this version of transaction supports
+both ZODB 3 and ZODB 4. Zope is evolving towards the ZODB4
+interfaces.
+
+Transaction has two methods for a resource manager to call to
+participate in a transaction -- register() and join(). join() takes a
+resource manager and adds it to the list of resources. register() is
+for backwards compatibility. It takes a persistent object and
+registers its _p_jar attribute. TODO: explain adapter
+
+Two-phase commit
+----------------
+
+A transaction commit involves an interaction between the transaction
+object and one or more resource managers. The transaction manager
+calls the following four methods on each resource manager; it calls
+tpc_begin() on each resource manager before calling commit() on any of
+them.
+
+ 1. tpc_begin(txn)
+ 2. commit(txn)
+ 3. tpc_vote(txn)
+ 4. tpc_finish(txn)
+
+Before-commit hook
+------------------
+
+Sometimes, applications want to execute some code when a transaction is
+committed. For example, one might want to delay object indexing until a
+transaction commits, rather than indexing every time an object is changed.
+Or someone might want to check invariants only after a set of operations. A
+pre-commit hook is available for such use cases: use addBeforeCommitHook(),
+passing it a callable and arguments. The callable will be called with its
+arguments at the start of the commit (but not for substransaction commits).
+
+After-commit hook
+------------------
+
+Sometimes, applications want to execute code after a transaction commit
+attempt succeeds or aborts. For example, one might want to launch non
+transactional code after a successful commit. Or still someone might
+want to launch asynchronous code after. A post-commit hook is
+available for such use cases: use addAfterCommitHook(), passing it a
+callable and arguments. The callable will be called with a Boolean
+value representing the status of the commit operation as first
+argument (true if successfull or false iff aborted) preceding its
+arguments at the start of the commit (but not for substransaction
+commits). Commit hooks are not called for transaction.abort().
+
+Error handling
+--------------
+
+When errors occur during two-phase commit, the transaction manager
+aborts all the resource managers. The specific methods it calls
+depend on whether the error occurs before or after the call to
+tpc_vote() on that transaction manager.
+
+If the resource manager has not voted, then the resource manager will
+have one or more uncommitted objects. There are two cases that lead
+to this state; either the transaction manager has not called commit()
+for any objects on this resource manager or the call that failed was a
+commit() for one of the objects of this resource manager. For each
+uncommitted object, including the object that failed in its commit(),
+call abort().
+
+Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
+called on each resource manager.
+
+Synchronization
+---------------
+
+You can register sychronization objects (synchronizers) with the
+tranasction manager. The synchronizer must implement
+beforeCompletion() and afterCompletion() methods. The transaction
+manager calls beforeCompletion() when it starts a top-level two-phase
+commit. It calls afterCompletion() when a top-level transaction is
+committed or aborted. The methods are passed the current Transaction
+as their only argument.
+
Contents:
.. toctree::
:maxdepth: 2
+ convenience
+ doom
+ savepoint
+ hooks
+ datamanager
+ resourcemanager
api
Copied: transaction/trunk/docs/resourcemanager.rst (from rev 128756, transaction/branches/sphinx/docs/resourcemanager.rst)
===================================================================
--- transaction/trunk/docs/resourcemanager.rst (rev 0)
+++ transaction/trunk/docs/resourcemanager.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,399 @@
+Writing a Resource Manager
+==========================
+
+Simple Resource Manager
+-----------------------
+
+.. doctest::
+
+ >>> from transaction.tests.examples import ResourceManager
+
+This :class:`transaction.tests.examples.ResourceManager`
+class provides a trivial resource-manager implementation and doc
+strings to illustrate the protocol and to provide a tool for writing
+tests.
+
+Our sample resource manager has state that is updated through an inc
+method and through transaction operations.
+
+When we create a sample resource manager:
+
+.. doctest::
+
+ >>> rm = ResourceManager()
+
+It has two pieces state, state and delta, both initialized to 0:
+
+.. doctest::
+
+ >>> rm.state
+ 0
+ >>> rm.delta
+ 0
+
+state is meant to model committed state, while delta represents
+tentative changes within a transaction. We change the state by
+calling inc:
+
+.. doctest::
+
+ >>> rm.inc()
+
+which updates delta:
+
+.. doctest::
+
+ >>> rm.delta
+ 1
+
+but state isn't changed until we commit the transaction:
+
+.. doctest::
+
+ >>> rm.state
+ 0
+
+To commit the changes, we use 2-phase commit. We execute the first
+stage by calling prepare. We need to pass a transation. Our
+sample resource managers don't really use the transactions for much,
+so we'll be lazy and use strings for transactions. The sample
+resource manager updates the state when we call tpc_vote:
+
+
+.. doctest::
+
+ >>> t1 = '1'
+ >>> rm.tpc_begin(t1)
+ >>> rm.state, rm.delta
+ (0, 1)
+
+ >>> rm.tpc_vote(t1)
+ >>> rm.state, rm.delta
+ (1, 1)
+
+ Now if we call tpc_finish:
+
+ >>> rm.tpc_finish(t1)
+
+Our changes are "permanent". The state reflects the changes and the
+delta has been reset to 0.
+
+.. doctest::
+
+ >>> rm.state, rm.delta
+ (1, 0)
+
+
+The :meth:`tpc_begin` Method
+-----------------------------
+
+Called by the transaction manager to ask the RM to prepare to commit data.
+
+.. doctest::
+
+ >>> rm = ResourceManager()
+ >>> rm.inc()
+ >>> t1 = '1'
+ >>> rm.tpc_begin(t1)
+ >>> rm.tpc_vote(t1)
+ >>> rm.tpc_finish(t1)
+ >>> rm.state
+ 1
+ >>> rm.inc()
+ >>> t2 = '2'
+ >>> rm.tpc_begin(t2)
+ >>> rm.tpc_vote(t2)
+ >>> rm.tpc_abort(t2)
+ >>> rm.state
+ 1
+
+It is an error to call tpc_begin more than once without completing
+two-phase commit:
+
+.. doctest::
+
+ >>> rm.tpc_begin(t1)
+
+ >>> rm.tpc_begin(t1)
+ Traceback (most recent call last):
+ ...
+ ValueError: txn in state 'tpc_begin' but expected one of (None,)
+ >>> rm.tpc_abort(t1)
+
+If there was a preceeding savepoint, the transaction must match:
+
+.. doctest::
+
+ >>> rollback = rm.savepoint(t1)
+ >>> rm.tpc_begin(t2)
+ Traceback (most recent call last):
+ ,,,
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> rm.tpc_begin(t1)
+
+
+The :meth:`tpc_vote` Method
+---------------------------
+
+Verify that a data manager can commit the transaction.
+
+This is the last chance for a data manager to vote 'no'. A
+data manager votes 'no' by raising an exception.
+
+Passed `transaction`, which is the ITransaction instance associated with the
+transaction being committed.
+
+
+The :meth:`tpc_finish` Method
+-----------------------------
+
+Complete two-phase commit
+
+.. doctest::
+
+ >>> rm = ResourceManager()
+ >>> rm.state
+ 0
+ >>> rm.inc()
+
+ We start two-phase commit by calling prepare:
+
+ >>> t1 = '1'
+ >>> rm.tpc_begin(t1)
+ >>> rm.tpc_vote(t1)
+
+ We complete it by calling tpc_finish:
+
+ >>> rm.tpc_finish(t1)
+ >>> rm.state
+ 1
+
+It is an error ro call tpc_finish without calling tpc_vote:
+
+.. doctest::
+
+ >>> rm.inc()
+ >>> t2 = '2'
+ >>> rm.tpc_begin(t2)
+ >>> rm.tpc_finish(t2)
+ Traceback (most recent call last):
+ ...
+ ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
+
+ >>> rm.tpc_abort(t2) # clean slate
+
+ >>> rm.tpc_begin(t2)
+ >>> rm.tpc_vote(t2)
+ >>> rm.tpc_finish(t2)
+
+Of course, the transactions given to tpc_begin and tpc_finish must
+be the same:
+
+.. doctest::
+
+ >>> rm.inc()
+ >>> t3 = '3'
+ >>> rm.tpc_begin(t3)
+ >>> rm.tpc_vote(t3)
+ >>> rm.tpc_finish(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '3')
+
+
+The :meth:`tpc_abort` Method
+-----------------------------
+
+Abort a transaction
+
+The abort method can be called before two-phase commit to
+throw away work done in the transaction:
+
+.. doctest::
+
+ >>> rm = ResourceManager()
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (0, 1)
+ >>> t1 = '1'
+ >>> rm.tpc_abort(t1)
+ >>> rm.state, rm.delta
+ (0, 0)
+
+The abort method also throws away work done in savepoints:
+
+.. doctest::
+
+ >>> rm.inc()
+ >>> r = rm.savepoint(t1)
+ >>> rm.inc()
+ >>> r = rm.savepoint(t1)
+ >>> rm.state, rm.delta
+ (0, 2)
+ >>> rm.tpc_abort(t1)
+ >>> rm.state, rm.delta
+ (0, 0)
+
+If savepoints are used, abort must be passed the same
+transaction:
+
+.. doctest::
+
+ >>> rm.inc()
+ >>> r = rm.savepoint(t1)
+ >>> t2 = '2'
+ >>> rm.tpc_abort(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> rm.tpc_abort(t1)
+
+The abort method is also used to abort a two-phase commit:
+
+.. doctest::
+
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (0, 1)
+ >>> rm.tpc_begin(t1)
+ >>> rm.state, rm.delta
+ (0, 1)
+ >>> rm.tpc_vote(t1)
+ >>> rm.state, rm.delta
+ (1, 1)
+ >>> rm.tpc_abort(t1)
+ >>> rm.state, rm.delta
+ (0, 0)
+
+Of course, the transactions passed to prepare and abort must
+match:
+
+.. doctest::
+
+ >>> rm.tpc_begin(t1)
+ >>> rm.tpc_abort(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> rm.tpc_abort(t1)
+
+This should never fail.
+
+
+The :meth:`savepoint` Method
+----------------------------
+
+Provide the ability to rollback transaction state
+
+Savepoints provide a way to:
+
+ - Save partial transaction work. For some resource managers, this
+ could allow resources to be used more efficiently.
+
+ - Provide the ability to revert state to a point in a
+ transaction without aborting the entire transaction. In
+ other words, savepoints support partial aborts.
+
+Savepoints don't use two-phase commit. If there are errors in
+setting or rolling back to savepoints, the application should
+abort the containing transaction. This is *not* the
+responsibility of the resource manager.
+
+Savepoints are always associated with a transaction. Any work
+done in a savepoint's transaction is tentative until the
+transaction is committed using two-phase commit.
+
+.. doctest::
+
+ >>> rm = ResourceManager()
+ >>> rm.inc()
+ >>> t1 = '1'
+ >>> r = rm.savepoint(t1)
+ >>> rm.state, rm.delta
+ (0, 1)
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (0, 2)
+ >>> r.rollback()
+ >>> rm.state, rm.delta
+ (0, 1)
+ >>> rm.tpc_begin(t1)
+ >>> rm.tpc_vote(t1)
+ >>> rm.tpc_finish(t1)
+ >>> rm.state, rm.delta
+ (1, 0)
+
+Savepoints must have the same transaction:
+
+.. doctest::
+
+ >>> r1 = rm.savepoint(t1)
+ >>> rm.state, rm.delta
+ (1, 0)
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (1, 1)
+ >>> t2 = '2'
+ >>> r2 = rm.savepoint(t2)
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Transaction missmatch', '2', '1')
+
+ >>> r2 = rm.savepoint(t1)
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (1, 2)
+
+If we rollback to an earlier savepoint, we discard all work
+done later:
+
+.. doctest::
+
+ >>> r1.rollback()
+ >>> rm.state, rm.delta
+ (1, 0)
+
+and we can no longer rollback to the later savepoint:
+
+.. doctest::
+
+ >>> r2.rollback()
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Attempt to roll back to invalid save point', 3, 2)
+
+We can roll back to a savepoint as often as we like:
+
+.. doctest::
+
+ >>> r1.rollback()
+ >>> r1.rollback()
+ >>> r1.rollback()
+ >>> rm.state, rm.delta
+ (1, 0)
+
+ >>> rm.inc()
+ >>> rm.inc()
+ >>> rm.inc()
+ >>> rm.state, rm.delta
+ (1, 3)
+ >>> r1.rollback()
+ >>> rm.state, rm.delta
+ (1, 0)
+
+But we can't rollback to a savepoint after it has been
+committed:
+
+.. doctest::
+
+ >>> rm.tpc_begin(t1)
+ >>> rm.tpc_vote(t1)
+ >>> rm.tpc_finish(t1)
+
+ >>> r1.rollback()
+ Traceback (most recent call last):
+ ...
+ TypeError: Attempt to rollback stale rollback
Copied: transaction/trunk/docs/savepoint.rst (from rev 128495, transaction/trunk/transaction/tests/savepoint.txt)
===================================================================
--- transaction/trunk/docs/savepoint.rst (rev 0)
+++ transaction/trunk/docs/savepoint.rst 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,326 @@
+Savepoints
+==========
+
+Savepoints provide a way to save to disk intermediate work done during
+a transaction allowing:
+
+- partial transaction (subtransaction) rollback (abort)
+
+- state of saved objects to be freed, freeing on-line memory for other
+ uses
+
+Savepoints make it possible to write atomic subroutines that don't
+make top-level transaction commitments.
+
+
+Applications
+------------
+
+To demonstrate how savepoints work with transactions, we've provided a sample
+data manager implementation that provides savepoint support. The primary
+purpose of this data manager is to provide code that can be read to understand
+how savepoints work. The secondary purpose is to provide support for
+demonstrating the correct operation of savepoint support within the
+transaction system. This data manager is very simple. It provides flat
+storage of named immutable values, like strings and numbers.
+
+.. doctest::
+
+ >>> import transaction
+ >>> from transaction.tests import savepointsample
+ >>> dm = savepointsample.SampleSavepointDataManager()
+ >>> dm['name'] = 'bob'
+
+As with other data managers, we can commit changes:
+
+.. doctest::
+
+ >>> transaction.commit()
+ >>> dm['name']
+ 'bob'
+
+and abort changes:
+
+.. doctest::
+
+ >>> dm['name'] = 'sally'
+ >>> dm['name']
+ 'sally'
+ >>> transaction.abort()
+ >>> dm['name']
+ 'bob'
+
+Now, let's look at an application that manages funds for people. It allows
+deposits and debits to be entered for multiple people. It accepts a sequence
+of entries and generates a sequence of status messages. For each entry, it
+applies the change and then validates the user's account. If the user's
+account is invalid, we roll back the change for that entry. The success or
+failure of an entry is indicated in the output status. First we'll initialize
+some accounts:
+
+.. doctest::
+
+ >>> dm['bob-balance'] = 0.0
+ >>> dm['bob-credit'] = 0.0
+ >>> dm['sally-balance'] = 0.0
+ >>> dm['sally-credit'] = 100.0
+ >>> transaction.commit()
+
+Now, we'll define a validation function to validate an account:
+
+.. doctest::
+
+ >>> def validate_account(name):
+ ... if dm[name+'-balance'] + dm[name+'-credit'] < 0:
+ ... raise ValueError('Overdrawn', name)
+
+And a function to apply entries. If the function fails in some unexpected
+way, it rolls back all of its changes and prints the error:
+
+.. doctest::
+
+ >>> def apply_entries(entries):
+ ... savepoint = transaction.savepoint()
+ ... try:
+ ... for name, amount in entries:
+ ... entry_savepoint = transaction.savepoint()
+ ... try:
+ ... dm[name+'-balance'] += amount
+ ... validate_account(name)
+ ... except ValueError as error:
+ ... entry_savepoint.rollback()
+ ... print("%s %s" % ('Error', str(error)))
+ ... else:
+ ... print("%s %s" % ('Updated', name))
+ ... except Exception as error:
+ ... savepoint.rollback()
+ ... print("%s" % ('Unexpected exception'))
+
+Now let's try applying some entries:
+
+.. doctest::
+
+ >>> apply_entries([
+ ... ('bob', 10.0),
+ ... ('sally', 10.0),
+ ... ('bob', 20.0),
+ ... ('sally', 10.0),
+ ... ('bob', -100.0),
+ ... ('sally', -100.0),
+ ... ])
+ Updated bob
+ Updated sally
+ Updated bob
+ Updated sally
+ Error ('Overdrawn', 'bob')
+ Updated sally
+
+ >>> dm['bob-balance']
+ 30.0
+
+ >>> dm['sally-balance']
+ -80.0
+
+If we provide entries that cause an unexpected error:
+
+.. doctest::
+
+ >>> apply_entries([
+ ... ('bob', 10.0),
+ ... ('sally', 10.0),
+ ... ('bob', '20.0'),
+ ... ('sally', 10.0),
+ ... ])
+ Updated bob
+ Updated sally
+ Unexpected exception
+
+Because the apply_entries used a savepoint for the entire function, it was
+able to rollback the partial changes without rolling back changes made in the
+previous call to ``apply_entries``:
+
+.. doctest::
+
+ >>> dm['bob-balance']
+ 30.0
+
+ >>> dm['sally-balance']
+ -80.0
+
+If we now abort the outer transactions, the earlier changes will go
+away:
+
+.. doctest::
+
+ >>> transaction.abort()
+
+ >>> dm['bob-balance']
+ 0.0
+
+ >>> dm['sally-balance']
+ 0.0
+
+Savepoint invalidation
+----------------------
+
+A savepoint can be used any number of times:
+
+.. doctest::
+
+ >>> dm['bob-balance'] = 100.0
+ >>> dm['bob-balance']
+ 100.0
+ >>> savepoint = transaction.savepoint()
+
+ >>> dm['bob-balance'] = 200.0
+ >>> dm['bob-balance']
+ 200.0
+ >>> savepoint.rollback()
+ >>> dm['bob-balance']
+ 100.0
+
+ >>> savepoint.rollback() # redundant, but should be harmless
+ >>> dm['bob-balance']
+ 100.0
+
+ >>> dm['bob-balance'] = 300.0
+ >>> dm['bob-balance']
+ 300.0
+ >>> savepoint.rollback()
+ >>> dm['bob-balance']
+ 100.0
+
+However, using a savepoint invalidates any savepoints that come after it:
+
+.. doctest::
+
+ >>> dm['bob-balance'] = 200.0
+ >>> dm['bob-balance']
+ 200.0
+ >>> savepoint1 = transaction.savepoint()
+
+ >>> dm['bob-balance'] = 300.0
+ >>> dm['bob-balance']
+ 300.0
+ >>> savepoint2 = transaction.savepoint()
+
+ >>> savepoint.rollback()
+ >>> dm['bob-balance']
+ 100.0
+
+ >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ InvalidSavepointRollbackError: invalidated by a later savepoint
+
+ >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ InvalidSavepointRollbackError: invalidated by a later savepoint
+
+ >>> transaction.abort()
+
+
+Databases without savepoint support
+-----------------------------------
+
+Normally it's an error to use savepoints with databases that don't support
+savepoints:
+
+.. doctest::
+
+ >>> dm_no_sp = savepointsample.SampleDataManager()
+ >>> dm_no_sp['name'] = 'bob'
+ >>> transaction.commit()
+ >>> dm_no_sp['name'] = 'sally'
+ >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Savepoints unsupported', {'name': 'bob'})
+
+ >>> transaction.abort()
+
+However, a flag can be passed to the transaction savepoint method to indicate
+that databases without savepoint support should be tolerated until a savepoint
+is rolled back. This allows transactions to proceed if there are no reasons
+to roll back:
+
+.. doctest::
+
+ >>> dm_no_sp['name'] = 'sally'
+ >>> savepoint = transaction.savepoint(1)
+ >>> dm_no_sp['name'] = 'sue'
+ >>> transaction.commit()
+ >>> dm_no_sp['name']
+ 'sue'
+
+ >>> dm_no_sp['name'] = 'sam'
+ >>> savepoint = transaction.savepoint(1)
+ >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Savepoints unsupported', {'name': 'sam'})
+
+
+Failures
+--------
+
+If a failure occurs when creating or rolling back a savepoint, the transaction
+state will be uncertain and the transaction will become uncommitable. From
+that point on, most transaction operations, including commit, will fail until
+the transaction is aborted.
+
+In the previous example, we got an error when we tried to rollback the
+savepoint. If we try to commit the transaction, the commit will fail:
+
+.. doctest::
+
+ >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ TransactionFailedError: An operation previously failed, with traceback:
+ ...
+ TypeError: ('Savepoints unsupported', {'name': 'sam'})
+ <BLANKLINE>
+
+We have to abort it to make any progress:
+
+.. doctest::
+
+ >>> transaction.abort()
+
+Similarly, in our earlier example, where we tried to take a savepoint with a
+data manager that didn't support savepoints:
+
+.. doctest::
+
+ >>> dm_no_sp['name'] = 'sally'
+ >>> dm['name'] = 'sally'
+ >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ TypeError: ('Savepoints unsupported', {'name': 'sue'})
+
+ >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
+ Traceback (most recent call last):
+ ...
+ TransactionFailedError: An operation previously failed, with traceback:
+ ...
+ TypeError: ('Savepoints unsupported', {'name': 'sue'})
+ <BLANKLINE>
+
+ >>> transaction.abort()
+
+After clearing the transaction with an abort, we can get on with new
+transactions:
+
+.. doctest::
+
+ >>> dm_no_sp['name'] = 'sally'
+ >>> dm['name'] = 'sally'
+ >>> transaction.commit()
+ >>> dm_no_sp['name']
+ 'sally'
+ >>> dm['name']
+ 'sally'
+
Modified: transaction/trunk/setup.py
===================================================================
--- transaction/trunk/setup.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/setup.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -60,7 +60,7 @@
'zope.interface',
],
extras_require = {
- 'docs': ['Sphinx'],
+ 'docs': ['Sphinx', 'repoze.sphinx.autointerface'],
'testing': ['nose', 'coverage'],
},
entry_points = """\
Copied: transaction/trunk/transaction/_compat.py (from rev 128495, transaction/trunk/transaction/compat.py)
===================================================================
--- transaction/trunk/transaction/_compat.py (rev 0)
+++ transaction/trunk/transaction/_compat.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,90 @@
+import sys
+import types
+
+PY3 = sys.version_info[0] == 3
+
+if PY3: # pragma: no cover
+ string_types = str,
+ integer_types = int,
+ class_types = type,
+ text_type = str
+ binary_type = bytes
+ long = int
+else:
+ string_types = basestring,
+ integer_types = (int, long)
+ class_types = (type, types.ClassType)
+ text_type = unicode
+ binary_type = str
+ long = long
+
+def bytes_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+ if isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ return s
+
+if PY3: # pragma: no cover
+ def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+ if isinstance(s, text_type):
+ return s
+ return str(s, encoding, errors)
+else:
+ def native_(s, encoding='latin-1', errors='strict'): #pragma NO COVER
+ if isinstance(s, text_type):
+ return s.encode(encoding, errors)
+ return str(s)
+
+if PY3: #pragma NO COVER
+ from io import StringIO
+else:
+ from io import BytesIO as StringIO
+
+if PY3: #pragma NO COVER
+ from collections import MutableMapping
+else:
+ from UserDict import UserDict as MutableMapping
+
+if PY3: # pragma: no cover
+ import builtins
+ exec_ = getattr(builtins, "exec")
+
+
+ def reraise(tp, value, tb=None): #pragma NO COVER
+ if value.__traceback__ is not tb:
+ raise value.with_traceback(tb)
+ raise value
+
+else: # pragma: no cover
+ def exec_(code, globs=None, locs=None): #pragma NO COVER
+ """Execute code in a namespace."""
+ if globs is None:
+ frame = sys._getframe(1)
+ globs = frame.f_globals
+ if locs is None:
+ locs = frame.f_locals
+ del frame
+ elif locs is None:
+ locs = globs
+ exec("""exec code in globs, locs""")
+
+ exec_("""def reraise(tp, value, tb=None):
+ raise tp, value, tb
+""")
+
+
+if PY3: #pragma NO COVER
+ try:
+ from threading import get_ident as get_thread_ident
+ except ImportError:
+ from threading import _get_ident as get_thread_ident
+else:
+ from thread import get_ident as get_thread_ident
+
+
+if PY3:
+ def func_name(func): #pragma NO COVER
+ return func.__name__
+else:
+ def func_name(func): #pragma NO COVER
+ return func.func_name
+
Modified: transaction/trunk/transaction/_manager.py
===================================================================
--- transaction/trunk/transaction/_manager.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/_manager.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -21,11 +21,11 @@
from zope.interface import implementer
+from transaction.interfaces import ITransactionManager
+from transaction.interfaces import TransientError
from transaction.weakset import WeakSet
+from transaction._compat import reraise
from transaction._transaction import Transaction
-from transaction.interfaces import ITransactionManager
-from transaction.interfaces import TransientError
-from transaction.compat import reraise
# We have to remember sets of synch objects, especially Connections.
@@ -54,6 +54,7 @@
# so that Transactions "see" synchronizers that get registered after the
# Transaction object is constructed.
+
@implementer(ITransactionManager)
class TransactionManager(object):
@@ -80,7 +81,8 @@
return self._txn
def free(self, txn):
- assert txn is self._txn
+ if txn is not self._txn:
+ raise ValueError("Foreign transaction")
self._txn = None
def registerSynch(self, synch):
@@ -125,7 +127,8 @@
return self.get().savepoint(optimistic)
def attempts(self, number=3):
- assert number > 0
+ if number <= 0:
+ raise ValueError("number must be positive")
while number:
number -= 1
if number:
@@ -149,6 +152,7 @@
Each thread is associated with a unique transaction.
"""
+
class Attempt(object):
def __init__(self, manager):
@@ -160,7 +164,7 @@
if retry:
return retry # suppress the exception if necessary
reraise(t, v, tb) # otherwise reraise the exception
-
+
def __enter__(self):
return self.manager.__enter__()
@@ -172,4 +176,3 @@
return self._retry_or_raise(*sys.exc_info())
else:
return self._retry_or_raise(t, v, tb)
-
Modified: transaction/trunk/transaction/_transaction.py
===================================================================
--- transaction/trunk/transaction/_transaction.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/_transaction.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,92 +11,6 @@
# FOR A PARTICULAR PURPOSE.
#
############################################################################
-"""Transaction objects manage resources for an individual activity.
-
-Compatibility issues
---------------------
-
-The implementation of Transaction objects involves two layers of
-backwards compatibility, because this version of transaction supports
-both ZODB 3 and ZODB 4. Zope is evolving towards the ZODB4
-interfaces.
-
-Transaction has two methods for a resource manager to call to
-participate in a transaction -- register() and join(). join() takes a
-resource manager and adds it to the list of resources. register() is
-for backwards compatibility. It takes a persistent object and
-registers its _p_jar attribute. TODO: explain adapter
-
-Two-phase commit
-----------------
-
-A transaction commit involves an interaction between the transaction
-object and one or more resource managers. The transaction manager
-calls the following four methods on each resource manager; it calls
-tpc_begin() on each resource manager before calling commit() on any of
-them.
-
- 1. tpc_begin(txn)
- 2. commit(txn)
- 3. tpc_vote(txn)
- 4. tpc_finish(txn)
-
-Before-commit hook
-------------------
-
-Sometimes, applications want to execute some code when a transaction is
-committed. For example, one might want to delay object indexing until a
-transaction commits, rather than indexing every time an object is changed.
-Or someone might want to check invariants only after a set of operations. A
-pre-commit hook is available for such use cases: use addBeforeCommitHook(),
-passing it a callable and arguments. The callable will be called with its
-arguments at the start of the commit (but not for substransaction commits).
-
-After-commit hook
-------------------
-
-Sometimes, applications want to execute code after a transaction commit
-attempt succeeds or aborts. For example, one might want to launch non
-transactional code after a successful commit. Or still someone might
-want to launch asynchronous code after. A post-commit hook is
-available for such use cases: use addAfterCommitHook(), passing it a
-callable and arguments. The callable will be called with a Boolean
-value representing the status of the commit operation as first
-argument (true if successfull or false iff aborted) preceding its
-arguments at the start of the commit (but not for substransaction
-commits). Commit hooks are not called for transaction.abort().
-
-Error handling
---------------
-
-When errors occur during two-phase commit, the transaction manager
-aborts all the resource managers. The specific methods it calls
-depend on whether the error occurs before or after the call to
-tpc_vote() on that transaction manager.
-
-If the resource manager has not voted, then the resource manager will
-have one or more uncommitted objects. There are two cases that lead
-to this state; either the transaction manager has not called commit()
-for any objects on this resource manager or the call that failed was a
-commit() for one of the objects of this resource manager. For each
-uncommitted object, including the object that failed in its commit(),
-call abort().
-
-Once uncommitted objects are aborted, tpc_abort() or abort_sub() is
-called on each resource manager.
-
-Synchronization
----------------
-
-You can register sychronization objects (synchronizers) with the
-tranasction manager. The synchronizer must implement
-beforeCompletion() and afterCompletion() methods. The transaction
-manager calls beforeCompletion() when it starts a top-level two-phase
-commit. It calls afterCompletion() when a top-level transaction is
-committed or aborted. The methods are passed the current Transaction
-as their only argument.
-"""
-
import binascii
import logging
import sys
@@ -105,17 +19,30 @@
from zope.interface import implementer
-from transaction.compat import reraise
-from transaction.compat import get_thread_ident
-from transaction.compat import native_
-from transaction.compat import bytes_
-from transaction.compat import StringIO
from transaction.weakset import WeakSet
from transaction.interfaces import TransactionFailedError
from transaction import interfaces
+from transaction._compat import reraise
+from transaction._compat import get_thread_ident
+from transaction._compat import native_
+from transaction._compat import bytes_
+from transaction._compat import StringIO
_marker = object()
+_TB_BUFFER = None #unittests may hook
+def _makeTracebackBuffer(): #pragma NO COVER
+ if _TB_BUFFER is not None:
+ return _TB_BUFFER
+ return StringIO()
+
+_LOGGER = None #unittests may hook
+def _makeLogger(): #pragma NO COVER
+ if _LOGGER is not None:
+ return _LOGGER
+ return logging.getLogger("txn.%d" % get_thread_ident())
+
+
# The point of this is to avoid hiding exceptions (which the builtin
# hasattr() does).
def myhasattr(obj, attr):
@@ -177,7 +104,7 @@
# directly by storages, leading underscore notwithstanding.
self._extension = {}
- self.log = logging.getLogger("txn.%d" % get_thread_ident())
+ self.log = _makeLogger()
self.log.debug("new transaction")
# If a commit fails, the traceback is saved in _failure_traceback.
@@ -203,7 +130,7 @@
if self.status is not Status.ACTIVE:
# should not doom transactions in the middle,
# or after, a commit
- raise AssertionError()
+ raise ValueError('non-doomable')
self.status = Status.DOOMED
# Raise TransactionFailedError, due to commit()/join()/register()
@@ -307,7 +234,6 @@
# be stored when the transaction commits. For other
# objects, the object implements the standard two-phase
# commit protocol.
-
manager = getattr(obj, "_p_jar", obj)
if manager is None:
raise ValueError("Register with no manager")
@@ -364,7 +290,7 @@
def _saveAndGetCommitishError(self):
self.status = Status.COMMITFAILED
# Save the traceback for TransactionFailedError.
- ft = self._failure_traceback = StringIO()
+ ft = self._failure_traceback = _makeTracebackBuffer()
t = None
v = None
tb = None
@@ -379,7 +305,6 @@
return t, v, tb
finally:
del t, v, tb
-
def _saveAndRaiseCommitishError(self):
t = None
@@ -390,7 +315,6 @@
reraise(t, v, tb)
finally:
del t, v, tb
-
def getBeforeCommitHooks(self):
""" See ITransaction.
@@ -566,6 +490,7 @@
# TODO: We need a better name for the adapters.
+
class MultiObjectResourceAdapter(object):
"""Adapt the old-style register() call to the new-style join().
@@ -573,7 +498,6 @@
the transaction manager. With register(), an individual object
is passed to register().
"""
-
def __init__(self, jar):
self.manager = jar
self.objects = []
@@ -624,6 +548,7 @@
finally:
del t, v, tb
+
def rm_key(rm):
func = getattr(rm, 'sortKey', None)
if func is not None:
@@ -634,13 +559,14 @@
This function does not raise an exception.
"""
-
# We should always be able to get __class__.
klass = o.__class__.__name__
- # oid would be great, but may this isn't a persistent object.
+ # oid would be great, but maybe this isn't a persistent object.
oid = getattr(o, "_p_oid", _marker)
if oid is not _marker:
oid = oid_repr(oid)
+ else:
+ oid = 'None'
return "%s oid=%s" % (klass, oid)
def oid_repr(oid):
@@ -657,6 +583,7 @@
else:
return repr(oid)
+
# TODO: deprecate for 3.6.
class DataManagerAdapter(object):
"""Adapt zodb 4-style data managers to zodb3 style
@@ -700,6 +627,7 @@
def sortKey(self):
return self._datamanager.sortKey()
+
@implementer(interfaces.ISavepoint)
class Savepoint:
"""Transaction savepoint.
@@ -742,6 +670,7 @@
# Mark the transaction as failed.
transaction._saveAndRaiseCommitishError() # reraises!
+
class AbortSavepoint:
def __init__(self, datamanager, transaction):
@@ -752,6 +681,7 @@
self.datamanager.abort(self.transaction)
self.transaction._unjoin(self.datamanager)
+
class NoRollbackSavepoint:
def __init__(self, datamanager):
Deleted: transaction/trunk/transaction/compat.py
===================================================================
--- transaction/trunk/transaction/compat.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/compat.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,95 +0,0 @@
-import sys
-import types
-
-PY3 = sys.version_info[0] == 3
-
-if PY3: # pragma: no cover
- string_types = str,
- integer_types = int,
- class_types = type,
- text_type = str
- binary_type = bytes
- long = int
-else:
- string_types = basestring,
- integer_types = (int, long)
- class_types = (type, types.ClassType)
- text_type = unicode
- binary_type = str
- long = long
-
-def text_(s, encoding='latin-1', errors='strict'):
- if isinstance(s, binary_type):
- return s.decode(encoding, errors)
- return s # pragma: no cover
-
-def bytes_(s, encoding='latin-1', errors='strict'):
- if isinstance(s, text_type):
- return s.encode(encoding, errors)
- return s
-
-if PY3: # pragma: no cover
- def native_(s, encoding='latin-1', errors='strict'):
- if isinstance(s, text_type):
- return s
- return str(s, encoding, errors)
-else:
- def native_(s, encoding='latin-1', errors='strict'):
- if isinstance(s, text_type):
- return s.encode(encoding, errors)
- return str(s)
-
-if PY3:
- from io import StringIO
-else:
- from io import BytesIO as StringIO
-
-if PY3:
- from collections import MutableMapping
-else:
- from UserDict import UserDict as MutableMapping
-
-if PY3: # pragma: no cover
- import builtins
- exec_ = getattr(builtins, "exec")
-
-
- def reraise(tp, value, tb=None):
- if value.__traceback__ is not tb:
- raise value.with_traceback(tb)
- raise value
-
-else: # pragma: no cover
- def exec_(code, globs=None, locs=None):
- """Execute code in a namespace."""
- if globs is None:
- frame = sys._getframe(1)
- globs = frame.f_globals
- if locs is None:
- locs = frame.f_locals
- del frame
- elif locs is None:
- locs = globs
- exec("""exec code in globs, locs""")
-
- exec_("""def reraise(tp, value, tb=None):
- raise tp, value, tb
-""")
-
-
-if PY3:
- try:
- from threading import get_ident as get_thread_ident
- except ImportError:
- from threading import _get_ident as get_thread_ident
-else:
- from thread import get_ident as get_thread_ident
-
-
-if PY3:
- def func_name(func):
- return func.__name__
-else:
- def func_name(func):
- return func.func_name
-
Copied: transaction/trunk/transaction/tests/common.py (from rev 128756, transaction/branches/sphinx/transaction/tests/common.py)
===================================================================
--- transaction/trunk/transaction/tests/common.py (rev 0)
+++ transaction/trunk/transaction/tests/common.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,65 @@
+##############################################################################
+#
+# Copyright (c) 2012 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+
+class DummyFile(object):
+ def __init__(self):
+ self._lines = []
+ def write(self, text):
+ self._lines.append(text)
+ def writelines(self, lines):
+ self._lines.extend(lines)
+
+
+class DummyLogger(object):
+ def __init__(self):
+ self._clear()
+ def _clear(self):
+ self._log = []
+ def log(self, level, msg, *args, **kw):
+ if args:
+ self._log.append((level, msg % args))
+ elif kw:
+ self._log.append((level, msg % kw))
+ else:
+ self._log.append((level, msg))
+ def debug(self, msg, *args, **kw):
+ self.log('debug', msg, *args, **kw)
+ def error(self, msg, *args, **kw):
+ self.log('error', msg, *args, **kw)
+ def critical(self, msg, *args, **kw):
+ self.log('critical', msg, *args, **kw)
+
+
+class Monkey(object):
+ # context-manager for replacing module names in the scope of a test.
+ def __init__(self, module, **kw):
+ self.module = module
+ self.to_restore = dict([(key, getattr(module, key)) for key in kw])
+ for key, value in kw.items():
+ setattr(module, key, value)
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ for key, value in self.to_restore.items():
+ setattr(self.module, key, value)
+
+def assertRaisesEx(e_type, checked, *args, **kw):
+ try:
+ checked(*args, **kw)
+ except e_type as e:
+ return e
+ raise AssertionError("Didn't raise: %s" % e_type.__name__)
Deleted: transaction/trunk/transaction/tests/convenience.txt
===================================================================
--- transaction/trunk/transaction/tests/convenience.txt 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/convenience.txt 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,165 +0,0 @@
-Transaction convenience support
-===============================
-
-(We *really* need to write proper documentation for the transaction
- package, but I don't want to block the conveniences documented here
- for that.)
-
-with support
-------------
-
-We can now use the with statement to define transaction boundaries.
-
- >>> import transaction.tests.savepointsample
- >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
- >>> list(dm.keys())
- []
-
-We can use it with a manager:
-
- >>> with transaction.manager as t:
- ... dm['z'] = 3
- ... t.note('test 3')
-
- >>> dm['z']
- 3
-
- >>> dm.last_note
- 'test 3'
-
- >>> with transaction.manager: #doctest ELLIPSIS
- ... dm['z'] = 4
- ... xxx
- Traceback (most recent call last):
- ...
- NameError: ... name 'xxx' is not defined
-
- >>> dm['z']
- 3
-
-On Python 2, you can also abbreviate ``with transaction.manager:`` as ``with
-transaction:``. This does not work on Python 3 (see see
-http://bugs.python.org/issue12022).
-
-Retries
--------
-
-Commits can fail for transient reasons, especially conflicts.
-Applications will often retry transactions some number of times to
-overcome transient failures. This typically looks something like::
-
- for i in range(3):
- try:
- with transaction.manager:
- ... some something ...
- except SomeTransientException:
- contine
- else:
- break
-
-This is rather ugly.
-
-Transaction managers provide a helper for this case. To show this,
-we'll use a contrived example:
-
-
- >>> ntry = 0
- >>> with transaction.manager:
- ... dm['ntry'] = 0
-
- >>> import transaction.interfaces
- >>> class Retry(transaction.interfaces.TransientError):
- ... pass
-
- >>> for attempt in transaction.manager.attempts():
- ... with attempt as t:
- ... t.note('test')
- ... print("%s %s" % (dm['ntry'], ntry))
- ... ntry += 1
- ... dm['ntry'] = ntry
- ... if ntry % 3:
- ... raise Retry(ntry)
- 0 0
- 0 1
- 0 2
-
-The raising of a subclass of TransientError is critical here. It's
-what signals that the transaction should be retried. It is generally
-up to the data manager to signal that a transaction should try again
-by raising a subclass of TransientError (or TransientError itself, of
-course).
-
-You shouldn't make any assumptions about the object returned by the
-iterator. (It isn't a transaction or transaction manager, as far as
-you know. :) If you use the ``as`` keyword in the ``with`` statement,
-a transaction object will be assigned to the variable named.
-
-By default, it tries 3 times. We can tell it how many times to try:
-
- >>> for attempt in transaction.manager.attempts(2):
- ... with attempt:
- ... ntry += 1
- ... if ntry % 3:
- ... raise Retry(ntry)
- Traceback (most recent call last):
- ...
- Retry: 5
-
-It it doesn't succeed in that many times, the exception will be
-propagated.
-
-Of course, other errors are propagated directly:
-
- >>> ntry = 0
- >>> for attempt in transaction.manager.attempts():
- ... with attempt:
- ... ntry += 1
- ... if ntry == 3:
- ... raise ValueError(ntry)
- Traceback (most recent call last):
- ...
- ValueError: 3
-
-We can use the default transaction manager:
-
- >>> for attempt in transaction.attempts():
- ... with attempt as t:
- ... t.note('test')
- ... print("%s %s" % (dm['ntry'], ntry))
- ... ntry += 1
- ... dm['ntry'] = ntry
- ... if ntry % 3:
- ... raise Retry(ntry)
- 3 3
- 3 4
- 3 5
-
-Sometimes, a data manager doesn't raise exceptions directly, but
-wraps other other systems that raise exceptions outside of it's
-control. Data managers can provide a should_retry method that takes
-an exception instance and returns True if the transaction should be
-attempted again.
-
- >>> class DM(transaction.tests.savepointsample.SampleSavepointDataManager):
- ... def should_retry(self, e):
- ... if 'should retry' in str(e):
- ... return True
-
- >>> ntry = 0
- >>> dm2 = DM()
- >>> with transaction.manager:
- ... dm2['ntry'] = 0
- >>> for attempt in transaction.manager.attempts():
- ... with attempt:
- ... print("%s %s" % (dm['ntry'], ntry))
- ... ntry += 1
- ... dm['ntry'] = ntry
- ... dm2['ntry'] = ntry
- ... if ntry % 3:
- ... raise ValueError('we really should retry this')
- 6 0
- 6 1
- 6 2
-
- >>> dm2['ntry']
- 3
Deleted: transaction/trunk/transaction/tests/doom.txt
===================================================================
--- transaction/trunk/transaction/tests/doom.txt 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/doom.txt 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,136 +0,0 @@
-Dooming Transactions
-====================
-
-A doomed transaction behaves exactly the same way as an active transaction but
-raises an error on any attempt to commit it, thus forcing an abort.
-
-Doom is useful in places where abort is unsafe and an exception cannot be
-raised. This occurs when the programmer wants the code following the doom to
-run but not commit. It is unsafe to abort in these circumstances as a following
-get() may implicitly open a new transaction.
-
-Any attempt to commit a doomed transaction will raise a DoomedTransaction
-exception.
-
-An example of such a use case can be found in
-zope/app/form/browser/editview.py. Here a form validation failure must doom
-the transaction as committing the transaction may have side-effects. However,
-the form code must continue to calculate a form containing the error messages
-to return.
-
-For Zope in general, code running within a request should always doom
-transactions rather than aborting them. It is the responsibilty of the
-publication to either abort() or commit() the transaction. Application code can
-use savepoints and doom() safely.
-
-To see how it works we first need to create a stub data manager:
-
- >>> from transaction.interfaces import IDataManager
- >>> from zope.interface import implementer
- >>> @implementer(IDataManager)
- ... class DataManager:
- ... def __init__(self):
- ... self.attr_counter = {}
- ... def __getattr__(self, name):
- ... def f(transaction):
- ... self.attr_counter[name] = self.attr_counter.get(name, 0) + 1
- ... return f
- ... def total(self):
- ... count = 0
- ... for access_count in self.attr_counter.values():
- ... count += access_count
- ... return count
- ... def sortKey(self):
- ... return 1
-
-Start a new transaction:
-
- >>> import transaction
- >>> txn = transaction.begin()
- >>> dm = DataManager()
- >>> txn.join(dm)
-
-We can ask a transaction if it is doomed to avoid expensive operations. An
-example of a use case is an object-relational mapper where a pre-commit hook
-sends all outstanding SQL to a relational database for objects changed during
-the transaction. This expensive operation is not necessary if the transaction
-has been doomed. A non-doomed transaction should return False:
-
- >>> txn.isDoomed()
- False
-
-We can doom a transaction by calling .doom() on it:
-
- >>> txn.doom()
- >>> txn.isDoomed()
- True
-
-We can doom it again if we like:
-
- >>> txn.doom()
-
-The data manager is unchanged at this point:
-
- >>> dm.total()
- 0
-
-Attempting to commit a doomed transaction any number of times raises a
-DoomedTransaction:
-
- >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- DoomedTransaction: transaction doomed, cannot commit
- >>> txn.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- DoomedTransaction: transaction doomed, cannot commit
-
-But still leaves the data manager unchanged:
-
- >>> dm.total()
- 0
-
-But the doomed transaction can be aborted:
-
- >>> txn.abort()
-
-Which aborts the data manager:
-
- >>> dm.total()
- 1
- >>> dm.attr_counter['abort']
- 1
-
-Dooming the current transaction can also be done directly from the transaction
-module. We can also begin a new transaction directly after dooming the old one:
-
- >>> txn = transaction.begin()
- >>> transaction.isDoomed()
- False
- >>> transaction.doom()
- >>> transaction.isDoomed()
- True
- >>> txn = transaction.begin()
-
-After committing a transaction we get an assertion error if we try to doom the
-transaction. This could be made more specific, but trying to doom a transaction
-after it's been committed is probably a programming error:
-
- >>> txn = transaction.begin()
- >>> txn.commit()
- >>> txn.doom()
- Traceback (most recent call last):
- ...
- AssertionError
-
-A doomed transaction should act the same as an active transaction, so we should
-be able to join it:
-
- >>> txn = transaction.begin()
- >>> txn.doom()
- >>> dm2 = DataManager()
- >>> txn.join(dm2)
-
-Clean up:
-
- >>> txn = transaction.begin()
- >>> txn.abort()
Copied: transaction/trunk/transaction/tests/examples.py (from rev 128756, transaction/branches/sphinx/transaction/tests/examples.py)
===================================================================
--- transaction/trunk/transaction/tests/examples.py (rev 0)
+++ transaction/trunk/transaction/tests/examples.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,181 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+"""Sample objects for use in tests
+
+"""
+
+
+class DataManager(object):
+ """Sample data manager
+
+ Used by the 'datamanager' chapter in the Sphinx docs.
+ """
+ def __init__(self):
+ self.state = 0
+ self.sp = 0
+ self.transaction = None
+ self.delta = 0
+ self.prepared = False
+
+ def inc(self, n=1):
+ self.delta += n
+
+ def prepare(self, transaction):
+ if self.prepared:
+ raise TypeError('Already prepared')
+ self._checkTransaction(transaction)
+ self.prepared = True
+ self.transaction = transaction
+ self.state += self.delta
+
+ def _checkTransaction(self, transaction):
+ if (transaction is not self.transaction
+ and self.transaction is not None):
+ raise TypeError("Transaction missmatch",
+ transaction, self.transaction)
+
+ def abort(self, transaction):
+ self._checkTransaction(transaction)
+ if self.transaction is not None:
+ self.transaction = None
+
+ if self.prepared:
+ self.state -= self.delta
+ self.prepared = False
+
+ self.delta = 0
+
+ def commit(self, transaction):
+ if not self.prepared:
+ raise TypeError('Not prepared to commit')
+ self._checkTransaction(transaction)
+ self.delta = 0
+ self.transaction = None
+ self.prepared = False
+
+ def savepoint(self, transaction):
+ if self.prepared:
+ raise TypeError("Can't get savepoint during two-phase commit")
+ self._checkTransaction(transaction)
+ self.transaction = transaction
+ self.sp += 1
+ return Rollback(self)
+
+
+class Rollback(object):
+
+ def __init__(self, dm):
+ self.dm = dm
+ self.sp = dm.sp
+ self.delta = dm.delta
+ self.transaction = dm.transaction
+
+ def rollback(self):
+ if self.transaction is not self.dm.transaction:
+ raise TypeError("Attempt to rollback stale rollback")
+ if self.dm.sp < self.sp:
+ raise TypeError("Attempt to roll back to invalid save point",
+ self.sp, self.dm.sp)
+ self.dm.sp = self.sp
+ self.dm.delta = self.delta
+
+
+class ResourceManager(object):
+ """ Sample resource manager.
+
+ Used by the 'resourcemanager' chapter in the Sphinx docs.
+ """
+ def __init__(self):
+ self.state = 0
+ self.sp = 0
+ self.transaction = None
+ self.delta = 0
+ self.txn_state = None
+
+ def _check_state(self, *ok_states):
+ if self.txn_state not in ok_states:
+ raise ValueError("txn in state %r but expected one of %r" %
+ (self.txn_state, ok_states))
+
+ def _checkTransaction(self, transaction):
+ if (transaction is not self.transaction
+ and self.transaction is not None):
+ raise TypeError("Transaction missmatch",
+ transaction, self.transaction)
+
+ def inc(self, n=1):
+ self.delta += n
+
+ def tpc_begin(self, transaction):
+ self._checkTransaction(transaction)
+ self._check_state(None)
+ self.transaction = transaction
+ self.txn_state = 'tpc_begin'
+
+ def tpc_vote(self, transaction):
+ self._checkTransaction(transaction)
+ self._check_state('tpc_begin')
+ self.state += self.delta
+ self.txn_state = 'tpc_vote'
+
+ def tpc_finish(self, transaction):
+ self._checkTransaction(transaction)
+ self._check_state('tpc_vote')
+ self.delta = 0
+ self.transaction = None
+ self.prepared = False
+ self.txn_state = None
+
+ def tpc_abort(self, transaction):
+ self._checkTransaction(transaction)
+ if self.transaction is not None:
+ self.transaction = None
+
+ if self.txn_state == 'tpc_vote':
+ self.state -= self.delta
+
+ self.txn_state = None
+ self.delta = 0
+
+ def savepoint(self, transaction):
+ if self.txn_state is not None:
+ raise TypeError("Can't get savepoint during two-phase commit")
+ self._checkTransaction(transaction)
+ self.transaction = transaction
+ self.sp += 1
+ return SavePoint(self)
+
+ def discard(self, transaction):
+ pass
+
+
+class SavePoint(object):
+
+ def __init__(self, rm):
+ self.rm = rm
+ self.sp = rm.sp
+ self.delta = rm.delta
+ self.transaction = rm.transaction
+
+ def rollback(self):
+ if self.transaction is not self.rm.transaction:
+ raise TypeError("Attempt to rollback stale rollback")
+ if self.rm.sp < self.sp:
+ raise TypeError("Attempt to roll back to invalid save point",
+ self.sp, self.rm.sp)
+ self.rm.sp = self.sp
+ self.rm.delta = self.delta
+
+ def discard(self):
+ pass
Deleted: transaction/trunk/transaction/tests/sampledm.py
===================================================================
--- transaction/trunk/transaction/tests/sampledm.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/sampledm.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,412 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-
-$Id: sampledm.py 29896 2005-04-07 04:48:06Z tim_one $
-"""
-
-class DataManager(object):
- """Sample data manager
-
- This class provides a trivial data-manager implementation and doc
- strings to illustrate the the protocol and to provide a tool for
- writing tests.
-
- Our sample data manager has state that is updated through an inc
- method and through transaction operations.
-
- When we create a sample data manager:
-
- >>> dm = DataManager()
-
- It has two bits of state, state:
-
- >>> dm.state
- 0
-
- and delta:
-
- >>> dm.delta
- 0
-
- Both of which are initialized to 0. state is meant to model
- committed state, while delta represents tentative changes within a
- transaction. We change the state by calling inc:
-
- >>> dm.inc()
-
- which updates delta:
-
- >>> dm.delta
- 1
-
- but state isn't changed until we commit the transaction:
-
- >>> dm.state
- 0
-
- To commit the changes, we use 2-phase commit. We execute the first
- stage by calling prepare. We need to pass a transation. Our
- sample data managers don't really use the transactions for much,
- so we'll be lazy and use strings for transactions:
-
- >>> t1 = '1'
- >>> dm.prepare(t1)
-
- The sample data manager updates the state when we call prepare:
-
- >>> dm.state
- 1
- >>> dm.delta
- 1
-
- This is mainly so we can detect some affect of calling the methods.
-
- Now if we call commit:
-
- >>> dm.commit(t1)
-
- Our changes are"permanent". The state reflects the changes and the
- delta has been reset to 0.
-
- >>> dm.state
- 1
- >>> dm.delta
- 0
- """
-
- def __init__(self):
- self.state = 0
- self.sp = 0
- self.transaction = None
- self.delta = 0
- self.prepared = False
-
- def inc(self, n=1):
- self.delta += n
-
- def prepare(self, transaction):
- """Prepare to commit data
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> t1 = '1'
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
- >>> dm.state
- 1
- >>> dm.inc()
- >>> t2 = '2'
- >>> dm.prepare(t2)
- >>> dm.abort(t2)
- >>> dm.state
- 1
-
- It is en error to call prepare more than once without an intervening
- commit or abort:
-
- >>> dm.prepare(t1)
-
- >>> dm.prepare(t1)
- Traceback (most recent call last):
- ...
- TypeError: Already prepared
-
- >>> dm.prepare(t2)
- Traceback (most recent call last):
- ...
- TypeError: Already prepared
-
- >>> dm.abort(t1)
-
- If there was a preceeding savepoint, the transaction must match:
-
- >>> rollback = dm.savepoint(t1)
- >>> dm.prepare(t2)
- Traceback (most recent call last):
- ,,,
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.prepare(t1)
-
- """
- if self.prepared:
- raise TypeError('Already prepared')
- self._checkTransaction(transaction)
- self.prepared = True
- self.transaction = transaction
- self.state += self.delta
-
- def _checkTransaction(self, transaction):
- if (transaction is not self.transaction
- and self.transaction is not None):
- raise TypeError("Transaction missmatch",
- transaction, self.transaction)
-
- def abort(self, transaction):
- """Abort a transaction
-
- The abort method can be called before two-phase commit to
- throw away work done in the transaction:
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 1)
- >>> t1 = '1'
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- The abort method also throws away work done in savepoints:
-
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (0, 2)
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- If savepoints are used, abort must be passed the same
- transaction:
-
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> t2 = '2'
- >>> dm.abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.abort(t1)
-
- The abort method is also used to abort a two-phase commit:
-
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.prepare(t1)
- >>> dm.state, dm.delta
- (1, 1)
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- Of course, the transactions passed to prepare and abort must
- match:
-
- >>> dm.prepare(t1)
- >>> dm.abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.abort(t1)
-
-
- """
- self._checkTransaction(transaction)
- if self.transaction is not None:
- self.transaction = None
-
- if self.prepared:
- self.state -= self.delta
- self.prepared = False
-
- self.delta = 0
-
- def commit(self, transaction):
- """Complete two-phase commit
-
- >>> dm = DataManager()
- >>> dm.state
- 0
- >>> dm.inc()
-
- We start two-phase commit by calling prepare:
-
- >>> t1 = '1'
- >>> dm.prepare(t1)
-
- We complete it by calling commit:
-
- >>> dm.commit(t1)
- >>> dm.state
- 1
-
- It is an error ro call commit without calling prepare first:
-
- >>> dm.inc()
- >>> t2 = '2'
- >>> dm.commit(t2)
- Traceback (most recent call last):
- ...
- TypeError: Not prepared to commit
-
- >>> dm.prepare(t2)
- >>> dm.commit(t2)
-
- If course, the transactions given to prepare and commit must
- be the same:
-
- >>> dm.inc()
- >>> t3 = '3'
- >>> dm.prepare(t3)
- >>> dm.commit(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '3')
-
- """
- if not self.prepared:
- raise TypeError('Not prepared to commit')
- self._checkTransaction(transaction)
- self.delta = 0
- self.transaction = None
- self.prepared = False
-
- def savepoint(self, transaction):
- """Provide the ability to rollback transaction state
-
- Savepoints provide a way to:
-
- - Save partial transaction work. For some data managers, this
- could allow resources to be used more efficiently.
-
- - Provide the ability to revert state to a point in a
- transaction without aborting the entire transaction. In
- other words, savepoints support partial aborts.
-
- Savepoints don't use two-phase commit. If there are errors in
- setting or rolling back to savepoints, the application should
- abort the containing transaction. This is *not* the
- responsibility of the data manager.
-
- Savepoints are always associated with a transaction. Any work
- done in a savepoint's transaction is tentative until the
- transaction is committed using two-phase commit.
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> t1 = '1'
- >>> r = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 2)
- >>> r.rollback()
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
- >>> dm.state, dm.delta
- (1, 0)
-
- Savepoints must have the same transaction:
-
- >>> r1 = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (1, 0)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 1)
- >>> t2 = '2'
- >>> r2 = dm.savepoint(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> r2 = dm.savepoint(t1)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 2)
-
- If we rollback to an earlier savepoint, we discard all work
- done later:
-
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- and we can no longer rollback to the later savepoint:
-
- >>> r2.rollback()
- Traceback (most recent call last):
- ...
- TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
- We can roll back to a savepoint as often as we like:
-
- >>> r1.rollback()
- >>> r1.rollback()
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- >>> dm.inc()
- >>> dm.inc()
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 3)
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- But we can't rollback to a savepoint after it has been
- committed:
-
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
-
- >>> r1.rollback()
- Traceback (most recent call last):
- ...
- TypeError: Attempt to rollback stale rollback
-
- """
- if self.prepared:
- raise TypeError("Can't get savepoint during two-phase commit")
- self._checkTransaction(transaction)
- self.transaction = transaction
- self.sp += 1
- return Rollback(self)
-
-class Rollback(object):
-
- def __init__(self, dm):
- self.dm = dm
- self.sp = dm.sp
- self.delta = dm.delta
- self.transaction = dm.transaction
-
- def rollback(self):
- if self.transaction is not self.dm.transaction:
- raise TypeError("Attempt to rollback stale rollback")
- if self.dm.sp < self.sp:
- raise TypeError("Attempt to roll back to invalid save point",
- self.sp, self.dm.sp)
- self.dm.sp = self.sp
- self.dm.delta = self.delta
-
-
-def test_suite():
- from doctest import DocTestSuite
- return DocTestSuite()
-
-if __name__ == '__main__':
- unittest.main()
Deleted: transaction/trunk/transaction/tests/savepoint.txt
===================================================================
--- transaction/trunk/transaction/tests/savepoint.txt 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/savepoint.txt 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,290 +0,0 @@
-Savepoints
-==========
-
-Savepoints provide a way to save to disk intermediate work done during
-a transaction allowing:
-
-- partial transaction (subtransaction) rollback (abort)
-
-- state of saved objects to be freed, freeing on-line memory for other
- uses
-
-Savepoints make it possible to write atomic subroutines that don't
-make top-level transaction commitments.
-
-
-Applications
-------------
-
-To demonstrate how savepoints work with transactions, we've provided a sample
-data manager implementation that provides savepoint support. The primary
-purpose of this data manager is to provide code that can be read to understand
-how savepoints work. The secondary purpose is to provide support for
-demonstrating the correct operation of savepoint support within the
-transaction system. This data manager is very simple. It provides flat
-storage of named immutable values, like strings and numbers.
-
- >>> import transaction
- >>> from transaction.tests import savepointsample
- >>> dm = savepointsample.SampleSavepointDataManager()
- >>> dm['name'] = 'bob'
-
-As with other data managers, we can commit changes:
-
- >>> transaction.commit()
- >>> dm['name']
- 'bob'
-
-and abort changes:
-
- >>> dm['name'] = 'sally'
- >>> dm['name']
- 'sally'
- >>> transaction.abort()
- >>> dm['name']
- 'bob'
-
-Now, let's look at an application that manages funds for people. It allows
-deposits and debits to be entered for multiple people. It accepts a sequence
-of entries and generates a sequence of status messages. For each entry, it
-applies the change and then validates the user's account. If the user's
-account is invalid, we roll back the change for that entry. The success or
-failure of an entry is indicated in the output status. First we'll initialize
-some accounts:
-
- >>> dm['bob-balance'] = 0.0
- >>> dm['bob-credit'] = 0.0
- >>> dm['sally-balance'] = 0.0
- >>> dm['sally-credit'] = 100.0
- >>> transaction.commit()
-
-Now, we'll define a validation function to validate an account:
-
- >>> def validate_account(name):
- ... if dm[name+'-balance'] + dm[name+'-credit'] < 0:
- ... raise ValueError('Overdrawn', name)
-
-And a function to apply entries. If the function fails in some unexpected
-way, it rolls back all of its changes and prints the error:
-
- >>> def apply_entries(entries):
- ... savepoint = transaction.savepoint()
- ... try:
- ... for name, amount in entries:
- ... entry_savepoint = transaction.savepoint()
- ... try:
- ... dm[name+'-balance'] += amount
- ... validate_account(name)
- ... except ValueError as error:
- ... entry_savepoint.rollback()
- ... print("%s %s" % ('Error', str(error)))
- ... else:
- ... print("%s %s" % ('Updated', name))
- ... except Exception as error:
- ... savepoint.rollback()
- ... print("%s" % ('Unexpected exception'))
-
-Now let's try applying some entries:
-
- >>> apply_entries([
- ... ('bob', 10.0),
- ... ('sally', 10.0),
- ... ('bob', 20.0),
- ... ('sally', 10.0),
- ... ('bob', -100.0),
- ... ('sally', -100.0),
- ... ])
- Updated bob
- Updated sally
- Updated bob
- Updated sally
- Error ('Overdrawn', 'bob')
- Updated sally
-
- >>> dm['bob-balance']
- 30.0
-
- >>> dm['sally-balance']
- -80.0
-
-If we provide entries that cause an unexpected error:
-
- >>> apply_entries([
- ... ('bob', 10.0),
- ... ('sally', 10.0),
- ... ('bob', '20.0'),
- ... ('sally', 10.0),
- ... ])
- Updated bob
- Updated sally
- Unexpected exception
-
-Because the apply_entries used a savepoint for the entire function, it was
-able to rollback the partial changes without rolling back changes made in the
-previous call to ``apply_entries``:
-
- >>> dm['bob-balance']
- 30.0
-
- >>> dm['sally-balance']
- -80.0
-
-If we now abort the outer transactions, the earlier changes will go
-away:
-
- >>> transaction.abort()
-
- >>> dm['bob-balance']
- 0.0
-
- >>> dm['sally-balance']
- 0.0
-
-Savepoint invalidation
-----------------------
-
-A savepoint can be used any number of times:
-
- >>> dm['bob-balance'] = 100.0
- >>> dm['bob-balance']
- 100.0
- >>> savepoint = transaction.savepoint()
-
- >>> dm['bob-balance'] = 200.0
- >>> dm['bob-balance']
- 200.0
- >>> savepoint.rollback()
- >>> dm['bob-balance']
- 100.0
-
- >>> savepoint.rollback() # redundant, but should be harmless
- >>> dm['bob-balance']
- 100.0
-
- >>> dm['bob-balance'] = 300.0
- >>> dm['bob-balance']
- 300.0
- >>> savepoint.rollback()
- >>> dm['bob-balance']
- 100.0
-
-However, using a savepoint invalidates any savepoints that come after it:
-
- >>> dm['bob-balance'] = 200.0
- >>> dm['bob-balance']
- 200.0
- >>> savepoint1 = transaction.savepoint()
-
- >>> dm['bob-balance'] = 300.0
- >>> dm['bob-balance']
- 300.0
- >>> savepoint2 = transaction.savepoint()
-
- >>> savepoint.rollback()
- >>> dm['bob-balance']
- 100.0
-
- >>> savepoint2.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- InvalidSavepointRollbackError: invalidated by a later savepoint
-
- >>> savepoint1.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- InvalidSavepointRollbackError: invalidated by a later savepoint
-
- >>> transaction.abort()
-
-
-Databases without savepoint support
------------------------------------
-
-Normally it's an error to use savepoints with databases that don't support
-savepoints:
-
- >>> dm_no_sp = savepointsample.SampleDataManager()
- >>> dm_no_sp['name'] = 'bob'
- >>> transaction.commit()
- >>> dm_no_sp['name'] = 'sally'
- >>> transaction.savepoint() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- TypeError: ('Savepoints unsupported', {'name': 'bob'})
-
- >>> transaction.abort()
-
-However, a flag can be passed to the transaction savepoint method to indicate
-that databases without savepoint support should be tolerated until a savepoint
-is rolled back. This allows transactions to proceed if there are no reasons
-to roll back:
-
- >>> dm_no_sp['name'] = 'sally'
- >>> savepoint = transaction.savepoint(1)
- >>> dm_no_sp['name'] = 'sue'
- >>> transaction.commit()
- >>> dm_no_sp['name']
- 'sue'
-
- >>> dm_no_sp['name'] = 'sam'
- >>> savepoint = transaction.savepoint(1)
- >>> savepoint.rollback() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- TypeError: ('Savepoints unsupported', {'name': 'sam'})
-
-
-Failures
---------
-
-If a failure occurs when creating or rolling back a savepoint, the transaction
-state will be uncertain and the transaction will become uncommitable. From
-that point on, most transaction operations, including commit, will fail until
-the transaction is aborted.
-
-In the previous example, we got an error when we tried to rollback the
-savepoint. If we try to commit the transaction, the commit will fail:
-
- >>> transaction.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- TransactionFailedError: An operation previously failed, with traceback:
- ...
- TypeError: ('Savepoints unsupported', {'name': 'sam'})
- <BLANKLINE>
-
-We have to abort it to make any progress:
-
- >>> transaction.abort()
-
-Similarly, in our earlier example, where we tried to take a savepoint with a
-data manager that didn't support savepoints:
-
- >>> dm_no_sp['name'] = 'sally'
- >>> dm['name'] = 'sally'
- >>> savepoint = transaction.savepoint() # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- TypeError: ('Savepoints unsupported', {'name': 'sue'})
-
- >>> transaction.commit() # doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- TransactionFailedError: An operation previously failed, with traceback:
- ...
- TypeError: ('Savepoints unsupported', {'name': 'sue'})
- <BLANKLINE>
-
- >>> transaction.abort()
-
-After clearing the transaction with an abort, we can get on with new
-transactions:
-
- >>> dm_no_sp['name'] = 'sally'
- >>> dm['name'] = 'sally'
- >>> transaction.commit()
- >>> dm_no_sp['name']
- 'sally'
- >>> dm['name']
- 'sally'
-
Modified: transaction/trunk/transaction/tests/savepointsample.py
===================================================================
--- transaction/trunk/transaction/tests/savepointsample.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/savepointsample.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -16,7 +16,7 @@
Sample data manager implementation that illustrates how to implement
savepoints.
-See savepoint.txt in the transaction package.
+Used by savepoint.rst in the Sphinx docs.
"""
from zope.interface import implementer
Deleted: transaction/trunk/transaction/tests/test_SampleDataManager.py
===================================================================
--- transaction/trunk/transaction/tests/test_SampleDataManager.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_SampleDataManager.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,413 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-"""
-from doctest import DocTestSuite
-
-class DataManager(object):
- """Sample data manager
-
- This class provides a trivial data-manager implementation and doc
- strings to illustrate the the protocol and to provide a tool for
- writing tests.
-
- Our sample data manager has state that is updated through an inc
- method and through transaction operations.
-
- When we create a sample data manager:
-
- >>> dm = DataManager()
-
- It has two bits of state, state:
-
- >>> dm.state
- 0
-
- and delta:
-
- >>> dm.delta
- 0
-
- Both of which are initialized to 0. state is meant to model
- committed state, while delta represents tentative changes within a
- transaction. We change the state by calling inc:
-
- >>> dm.inc()
-
- which updates delta:
-
- >>> dm.delta
- 1
-
- but state isn't changed until we commit the transaction:
-
- >>> dm.state
- 0
-
- To commit the changes, we use 2-phase commit. We execute the first
- stage by calling prepare. We need to pass a transation. Our
- sample data managers don't really use the transactions for much,
- so we'll be lazy and use strings for transactions:
-
- >>> t1 = '1'
- >>> dm.prepare(t1)
-
- The sample data manager updates the state when we call prepare:
-
- >>> dm.state
- 1
- >>> dm.delta
- 1
-
- This is mainly so we can detect some affect of calling the methods.
-
- Now if we call commit:
-
- >>> dm.commit(t1)
-
- Our changes are"permanent". The state reflects the changes and the
- delta has been reset to 0.
-
- >>> dm.state
- 1
- >>> dm.delta
- 0
- """
-
- def __init__(self):
- self.state = 0
- self.sp = 0
- self.transaction = None
- self.delta = 0
- self.prepared = False
-
- def inc(self, n=1):
- self.delta += n
-
- def prepare(self, transaction):
- """Prepare to commit data
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> t1 = '1'
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
- >>> dm.state
- 1
- >>> dm.inc()
- >>> t2 = '2'
- >>> dm.prepare(t2)
- >>> dm.abort(t2)
- >>> dm.state
- 1
-
- It is en error to call prepare more than once without an intervening
- commit or abort:
-
- >>> dm.prepare(t1)
-
- >>> dm.prepare(t1)
- Traceback (most recent call last):
- ...
- TypeError: Already prepared
-
- >>> dm.prepare(t2)
- Traceback (most recent call last):
- ...
- TypeError: Already prepared
-
- >>> dm.abort(t1)
-
- If there was a preceeding savepoint, the transaction must match:
-
- >>> rollback = dm.savepoint(t1)
- >>> dm.prepare(t2)
- Traceback (most recent call last):
- ,,,
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.prepare(t1)
-
- """
- if self.prepared:
- raise TypeError('Already prepared')
- self._checkTransaction(transaction)
- self.prepared = True
- self.transaction = transaction
- self.state += self.delta
-
- def _checkTransaction(self, transaction):
- if (transaction is not self.transaction
- and self.transaction is not None):
- raise TypeError("Transaction missmatch",
- transaction, self.transaction)
-
- def abort(self, transaction):
- """Abort a transaction
-
- The abort method can be called before two-phase commit to
- throw away work done in the transaction:
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 1)
- >>> t1 = '1'
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- The abort method also throws away work done in savepoints:
-
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (0, 2)
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- If savepoints are used, abort must be passed the same
- transaction:
-
- >>> dm.inc()
- >>> r = dm.savepoint(t1)
- >>> t2 = '2'
- >>> dm.abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.abort(t1)
-
- The abort method is also used to abort a two-phase commit:
-
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.prepare(t1)
- >>> dm.state, dm.delta
- (1, 1)
- >>> dm.abort(t1)
- >>> dm.state, dm.delta
- (0, 0)
-
- Of course, the transactions passed to prepare and abort must
- match:
-
- >>> dm.prepare(t1)
- >>> dm.abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> dm.abort(t1)
-
-
- """
- self._checkTransaction(transaction)
- if self.transaction is not None:
- self.transaction = None
-
- if self.prepared:
- self.state -= self.delta
- self.prepared = False
-
- self.delta = 0
-
- def commit(self, transaction):
- """Complete two-phase commit
-
- >>> dm = DataManager()
- >>> dm.state
- 0
- >>> dm.inc()
-
- We start two-phase commit by calling prepare:
-
- >>> t1 = '1'
- >>> dm.prepare(t1)
-
- We complete it by calling commit:
-
- >>> dm.commit(t1)
- >>> dm.state
- 1
-
- It is an error ro call commit without calling prepare first:
-
- >>> dm.inc()
- >>> t2 = '2'
- >>> dm.commit(t2)
- Traceback (most recent call last):
- ...
- TypeError: Not prepared to commit
-
- >>> dm.prepare(t2)
- >>> dm.commit(t2)
-
- If course, the transactions given to prepare and commit must
- be the same:
-
- >>> dm.inc()
- >>> t3 = '3'
- >>> dm.prepare(t3)
- >>> dm.commit(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '3')
-
- """
- if not self.prepared:
- raise TypeError('Not prepared to commit')
- self._checkTransaction(transaction)
- self.delta = 0
- self.transaction = None
- self.prepared = False
-
- def savepoint(self, transaction):
- """Provide the ability to rollback transaction state
-
- Savepoints provide a way to:
-
- - Save partial transaction work. For some data managers, this
- could allow resources to be used more efficiently.
-
- - Provide the ability to revert state to a point in a
- transaction without aborting the entire transaction. In
- other words, savepoints support partial aborts.
-
- Savepoints don't use two-phase commit. If there are errors in
- setting or rolling back to savepoints, the application should
- abort the containing transaction. This is *not* the
- responsibility of the data manager.
-
- Savepoints are always associated with a transaction. Any work
- done in a savepoint's transaction is tentative until the
- transaction is committed using two-phase commit.
-
- >>> dm = DataManager()
- >>> dm.inc()
- >>> t1 = '1'
- >>> r = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (0, 2)
- >>> r.rollback()
- >>> dm.state, dm.delta
- (0, 1)
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
- >>> dm.state, dm.delta
- (1, 0)
-
- Savepoints must have the same transaction:
-
- >>> r1 = dm.savepoint(t1)
- >>> dm.state, dm.delta
- (1, 0)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 1)
- >>> t2 = '2'
- >>> r2 = dm.savepoint(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> r2 = dm.savepoint(t1)
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 2)
-
- If we rollback to an earlier savepoint, we discard all work
- done later:
-
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- and we can no longer rollback to the later savepoint:
-
- >>> r2.rollback()
- Traceback (most recent call last):
- ...
- TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
- We can roll back to a savepoint as often as we like:
-
- >>> r1.rollback()
- >>> r1.rollback()
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- >>> dm.inc()
- >>> dm.inc()
- >>> dm.inc()
- >>> dm.state, dm.delta
- (1, 3)
- >>> r1.rollback()
- >>> dm.state, dm.delta
- (1, 0)
-
- But we can't rollback to a savepoint after it has been
- committed:
-
- >>> dm.prepare(t1)
- >>> dm.commit(t1)
-
- >>> r1.rollback()
- Traceback (most recent call last):
- ...
- TypeError: Attempt to rollback stale rollback
-
- """
- if self.prepared:
- raise TypeError("Can't get savepoint during two-phase commit")
- self._checkTransaction(transaction)
- self.transaction = transaction
- self.sp += 1
- return Rollback(self)
-
-class Rollback(object):
-
- def __init__(self, dm):
- self.dm = dm
- self.sp = dm.sp
- self.delta = dm.delta
- self.transaction = dm.transaction
-
- def rollback(self):
- if self.transaction is not self.dm.transaction:
- raise TypeError("Attempt to rollback stale rollback")
- if self.dm.sp < self.sp:
- raise TypeError("Attempt to roll back to invalid save point",
- self.sp, self.dm.sp)
- self.dm.sp = self.sp
- self.dm.delta = self.delta
-
-
-def test_suite():
- return DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
- unittest.main()
Deleted: transaction/trunk/transaction/tests/test_SampleResourceManager.py
===================================================================
--- transaction/trunk/transaction/tests/test_SampleResourceManager.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_SampleResourceManager.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,438 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-"""Sample objects for use in tests
-
-$Id$
-"""
-
-class ResourceManager(object):
- """Sample resource manager.
-
- This class provides a trivial resource-manager implementation and doc
- strings to illustrate the protocol and to provide a tool for writing
- tests.
-
- Our sample resource manager has state that is updated through an inc
- method and through transaction operations.
-
- When we create a sample resource manager:
-
- >>> rm = ResourceManager()
-
- It has two pieces state, state and delta, both initialized to 0:
-
- >>> rm.state
- 0
- >>> rm.delta
- 0
-
- state is meant to model committed state, while delta represents
- tentative changes within a transaction. We change the state by
- calling inc:
-
- >>> rm.inc()
-
- which updates delta:
-
- >>> rm.delta
- 1
-
- but state isn't changed until we commit the transaction:
-
- >>> rm.state
- 0
-
- To commit the changes, we use 2-phase commit. We execute the first
- stage by calling prepare. We need to pass a transation. Our
- sample resource managers don't really use the transactions for much,
- so we'll be lazy and use strings for transactions. The sample
- resource manager updates the state when we call tpc_vote:
-
-
- >>> t1 = '1'
- >>> rm.tpc_begin(t1)
- >>> rm.state, rm.delta
- (0, 1)
-
- >>> rm.tpc_vote(t1)
- >>> rm.state, rm.delta
- (1, 1)
-
- Now if we call tpc_finish:
-
- >>> rm.tpc_finish(t1)
-
- Our changes are "permanent". The state reflects the changes and the
- delta has been reset to 0.
-
- >>> rm.state, rm.delta
- (1, 0)
- """
-
- def __init__(self):
- self.state = 0
- self.sp = 0
- self.transaction = None
- self.delta = 0
- self.txn_state = None
-
- def _check_state(self, *ok_states):
- if self.txn_state not in ok_states:
- raise ValueError("txn in state %r but expected one of %r" %
- (self.txn_state, ok_states))
-
- def _checkTransaction(self, transaction):
- if (transaction is not self.transaction
- and self.transaction is not None):
- raise TypeError("Transaction missmatch",
- transaction, self.transaction)
-
- def inc(self, n=1):
- self.delta += n
-
- def tpc_begin(self, transaction):
- """Prepare to commit data.
-
- >>> rm = ResourceManager()
- >>> rm.inc()
- >>> t1 = '1'
- >>> rm.tpc_begin(t1)
- >>> rm.tpc_vote(t1)
- >>> rm.tpc_finish(t1)
- >>> rm.state
- 1
- >>> rm.inc()
- >>> t2 = '2'
- >>> rm.tpc_begin(t2)
- >>> rm.tpc_vote(t2)
- >>> rm.tpc_abort(t2)
- >>> rm.state
- 1
-
- It is an error to call tpc_begin more than once without completing
- two-phase commit:
-
- >>> rm.tpc_begin(t1)
-
- >>> rm.tpc_begin(t1)
- Traceback (most recent call last):
- ...
- ValueError: txn in state 'tpc_begin' but expected one of (None,)
- >>> rm.tpc_abort(t1)
-
- If there was a preceeding savepoint, the transaction must match:
-
- >>> rollback = rm.savepoint(t1)
- >>> rm.tpc_begin(t2)
- Traceback (most recent call last):
- ,,,
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> rm.tpc_begin(t1)
-
- """
- self._checkTransaction(transaction)
- self._check_state(None)
- self.transaction = transaction
- self.txn_state = 'tpc_begin'
-
- def tpc_vote(self, transaction):
- """Verify that a data manager can commit the transaction.
-
- This is the last chance for a data manager to vote 'no'. A
- data manager votes 'no' by raising an exception.
-
- transaction is the ITransaction instance associated with the
- transaction being committed.
- """
- self._checkTransaction(transaction)
- self._check_state('tpc_begin')
- self.state += self.delta
- self.txn_state = 'tpc_vote'
-
- def tpc_finish(self, transaction):
- """Complete two-phase commit
-
- >>> rm = ResourceManager()
- >>> rm.state
- 0
- >>> rm.inc()
-
- We start two-phase commit by calling prepare:
-
- >>> t1 = '1'
- >>> rm.tpc_begin(t1)
- >>> rm.tpc_vote(t1)
-
- We complete it by calling tpc_finish:
-
- >>> rm.tpc_finish(t1)
- >>> rm.state
- 1
-
- It is an error ro call tpc_finish without calling tpc_vote:
-
- >>> rm.inc()
- >>> t2 = '2'
- >>> rm.tpc_begin(t2)
- >>> rm.tpc_finish(t2)
- Traceback (most recent call last):
- ...
- ValueError: txn in state 'tpc_begin' but expected one of ('tpc_vote',)
-
- >>> rm.tpc_abort(t2) # clean slate
-
- >>> rm.tpc_begin(t2)
- >>> rm.tpc_vote(t2)
- >>> rm.tpc_finish(t2)
-
- Of course, the transactions given to tpc_begin and tpc_finish must
- be the same:
-
- >>> rm.inc()
- >>> t3 = '3'
- >>> rm.tpc_begin(t3)
- >>> rm.tpc_vote(t3)
- >>> rm.tpc_finish(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '3')
- """
- self._checkTransaction(transaction)
- self._check_state('tpc_vote')
- self.delta = 0
- self.transaction = None
- self.prepared = False
- self.txn_state = None
-
- def tpc_abort(self, transaction):
- """Abort a transaction
-
- The abort method can be called before two-phase commit to
- throw away work done in the transaction:
-
- >>> rm = ResourceManager()
- >>> rm.inc()
- >>> rm.state, rm.delta
- (0, 1)
- >>> t1 = '1'
- >>> rm.tpc_abort(t1)
- >>> rm.state, rm.delta
- (0, 0)
-
- The abort method also throws away work done in savepoints:
-
- >>> rm.inc()
- >>> r = rm.savepoint(t1)
- >>> rm.inc()
- >>> r = rm.savepoint(t1)
- >>> rm.state, rm.delta
- (0, 2)
- >>> rm.tpc_abort(t1)
- >>> rm.state, rm.delta
- (0, 0)
-
- If savepoints are used, abort must be passed the same
- transaction:
-
- >>> rm.inc()
- >>> r = rm.savepoint(t1)
- >>> t2 = '2'
- >>> rm.tpc_abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> rm.tpc_abort(t1)
-
- The abort method is also used to abort a two-phase commit:
-
- >>> rm.inc()
- >>> rm.state, rm.delta
- (0, 1)
- >>> rm.tpc_begin(t1)
- >>> rm.state, rm.delta
- (0, 1)
- >>> rm.tpc_vote(t1)
- >>> rm.state, rm.delta
- (1, 1)
- >>> rm.tpc_abort(t1)
- >>> rm.state, rm.delta
- (0, 0)
-
- Of course, the transactions passed to prepare and abort must
- match:
-
- >>> rm.tpc_begin(t1)
- >>> rm.tpc_abort(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> rm.tpc_abort(t1)
-
- This should never fail.
- """
-
- self._checkTransaction(transaction)
- if self.transaction is not None:
- self.transaction = None
-
- if self.txn_state == 'tpc_vote':
- self.state -= self.delta
-
- self.txn_state = None
- self.delta = 0
-
- def savepoint(self, transaction):
- """Provide the ability to rollback transaction state
-
- Savepoints provide a way to:
-
- - Save partial transaction work. For some resource managers, this
- could allow resources to be used more efficiently.
-
- - Provide the ability to revert state to a point in a
- transaction without aborting the entire transaction. In
- other words, savepoints support partial aborts.
-
- Savepoints don't use two-phase commit. If there are errors in
- setting or rolling back to savepoints, the application should
- abort the containing transaction. This is *not* the
- responsibility of the resource manager.
-
- Savepoints are always associated with a transaction. Any work
- done in a savepoint's transaction is tentative until the
- transaction is committed using two-phase commit.
-
- >>> rm = ResourceManager()
- >>> rm.inc()
- >>> t1 = '1'
- >>> r = rm.savepoint(t1)
- >>> rm.state, rm.delta
- (0, 1)
- >>> rm.inc()
- >>> rm.state, rm.delta
- (0, 2)
- >>> r.rollback()
- >>> rm.state, rm.delta
- (0, 1)
- >>> rm.tpc_begin(t1)
- >>> rm.tpc_vote(t1)
- >>> rm.tpc_finish(t1)
- >>> rm.state, rm.delta
- (1, 0)
-
- Savepoints must have the same transaction:
-
- >>> r1 = rm.savepoint(t1)
- >>> rm.state, rm.delta
- (1, 0)
- >>> rm.inc()
- >>> rm.state, rm.delta
- (1, 1)
- >>> t2 = '2'
- >>> r2 = rm.savepoint(t2)
- Traceback (most recent call last):
- ...
- TypeError: ('Transaction missmatch', '2', '1')
-
- >>> r2 = rm.savepoint(t1)
- >>> rm.inc()
- >>> rm.state, rm.delta
- (1, 2)
-
- If we rollback to an earlier savepoint, we discard all work
- done later:
-
- >>> r1.rollback()
- >>> rm.state, rm.delta
- (1, 0)
-
- and we can no longer rollback to the later savepoint:
-
- >>> r2.rollback()
- Traceback (most recent call last):
- ...
- TypeError: ('Attempt to roll back to invalid save point', 3, 2)
-
- We can roll back to a savepoint as often as we like:
-
- >>> r1.rollback()
- >>> r1.rollback()
- >>> r1.rollback()
- >>> rm.state, rm.delta
- (1, 0)
-
- >>> rm.inc()
- >>> rm.inc()
- >>> rm.inc()
- >>> rm.state, rm.delta
- (1, 3)
- >>> r1.rollback()
- >>> rm.state, rm.delta
- (1, 0)
-
- But we can't rollback to a savepoint after it has been
- committed:
-
- >>> rm.tpc_begin(t1)
- >>> rm.tpc_vote(t1)
- >>> rm.tpc_finish(t1)
-
- >>> r1.rollback()
- Traceback (most recent call last):
- ...
- TypeError: Attempt to rollback stale rollback
-
- """
- if self.txn_state is not None:
- raise TypeError("Can't get savepoint during two-phase commit")
- self._checkTransaction(transaction)
- self.transaction = transaction
- self.sp += 1
- return SavePoint(self)
-
- def discard(self, transaction):
- pass
-
-class SavePoint(object):
-
- def __init__(self, rm):
- self.rm = rm
- self.sp = rm.sp
- self.delta = rm.delta
- self.transaction = rm.transaction
-
- def rollback(self):
- if self.transaction is not self.rm.transaction:
- raise TypeError("Attempt to rollback stale rollback")
- if self.rm.sp < self.sp:
- raise TypeError("Attempt to roll back to invalid save point",
- self.sp, self.rm.sp)
- self.rm.sp = self.sp
- self.rm.delta = self.delta
-
- def discard(self):
- pass
-
-def test_suite():
- from doctest import DocTestSuite
- return DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
- unittest.main()
Copied: transaction/trunk/transaction/tests/test__manager.py (from rev 128756, transaction/branches/sphinx/transaction/tests/test__manager.py)
===================================================================
--- transaction/trunk/transaction/tests/test__manager.py (rev 0)
+++ transaction/trunk/transaction/tests/test__manager.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,608 @@
+##############################################################################
+#
+# Copyright (c) 2012 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+import unittest
+
+
+class TransactionManagerTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction import TransactionManager
+ return TransactionManager
+
+ def _makeOne(self):
+ return self._getTargetClass()()
+
+ def _makePopulated(self):
+ mgr = self._makeOne()
+ sub1 = DataObject(mgr)
+ sub2 = DataObject(mgr)
+ sub3 = DataObject(mgr)
+ nosub1 = DataObject(mgr, nost=1)
+ return mgr, sub1, sub2, sub3, nosub1
+
+ def test_ctor(self):
+ tm = self._makeOne()
+ self.assertTrue(tm._txn is None)
+ self.assertEqual(len(tm._synchs), 0)
+
+ def test_begin_wo_existing_txn_wo_synchs(self):
+ from transaction._transaction import Transaction
+ tm = self._makeOne()
+ tm.begin()
+ self.assertTrue(isinstance(tm._txn, Transaction))
+
+ def test_begin_wo_existing_txn_w_synchs(self):
+ from transaction._transaction import Transaction
+ tm = self._makeOne()
+ synch = DummySynch()
+ tm.registerSynch(synch)
+ tm.begin()
+ self.assertTrue(isinstance(tm._txn, Transaction))
+ self.assertTrue(tm._txn in synch._txns)
+
+ def test_begin_w_existing_txn(self):
+ class Existing(object):
+ _aborted = False
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ tm.begin()
+ self.assertFalse(tm._txn is txn)
+ self.assertTrue(txn._aborted)
+
+ def test_get_wo_existing_txn(self):
+ from transaction._transaction import Transaction
+ tm = self._makeOne()
+ txn = tm.get()
+ self.assertTrue(isinstance(txn, Transaction))
+
+ def test_get_w_existing_txn(self):
+ class Existing(object):
+ _aborted = False
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ self.assertTrue(tm.get() is txn)
+
+ def test_free_w_other_txn(self):
+ from transaction._transaction import Transaction
+ tm = self._makeOne()
+ txn = Transaction()
+ tm.begin()
+ self.assertRaises(ValueError, tm.free, txn)
+
+ def test_free_w_existing_txn(self):
+ class Existing(object):
+ _aborted = False
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ tm.free(txn)
+ self.assertTrue(tm._txn is None)
+
+ def test_registerSynch(self):
+ tm = self._makeOne()
+ synch = DummySynch()
+ tm.registerSynch(synch)
+ self.assertEqual(len(tm._synchs), 1)
+ self.assertTrue(synch in tm._synchs)
+
+ def test_unregisterSynch(self):
+ tm = self._makeOne()
+ synch1 = DummySynch()
+ synch2 = DummySynch()
+ tm.registerSynch(synch1)
+ tm.registerSynch(synch2)
+ tm.unregisterSynch(synch1)
+ self.assertEqual(len(tm._synchs), 1)
+ self.assertFalse(synch1 in tm._synchs)
+ self.assertTrue(synch2 in tm._synchs)
+
+ def test_isDoomed_wo_existing_txn(self):
+ tm = self._makeOne()
+ self.assertFalse(tm.isDoomed())
+ tm._txn.doom()
+ self.assertTrue(tm.isDoomed())
+
+ def test_isDoomed_w_existing_txn(self):
+ class Existing(object):
+ _doomed = False
+ def isDoomed(self):
+ return self._doomed
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ self.assertFalse(tm.isDoomed())
+ txn._doomed = True
+ self.assertTrue(tm.isDoomed())
+
+ def test_doom(self):
+ tm = self._makeOne()
+ txn = tm.get()
+ self.assertFalse(txn.isDoomed())
+ tm.doom()
+ self.assertTrue(txn.isDoomed())
+ self.assertTrue(tm.isDoomed())
+
+ def test_commit_w_existing_txn(self):
+ class Existing(object):
+ _committed = False
+ def commit(self):
+ self._committed = True
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ tm.commit()
+ self.assertTrue(txn._committed)
+
+ def test_abort_w_existing_txn(self):
+ class Existing(object):
+ _aborted = False
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ tm._txn = txn = Existing()
+ tm.abort()
+ self.assertTrue(txn._aborted)
+
+ def test_as_context_manager_wo_error(self):
+ class _Test(object):
+ _committed = False
+ _aborted = False
+ def commit(self):
+ self._committed = True
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ with tm:
+ tm._txn = txn = _Test()
+ self.assertTrue(txn._committed)
+ self.assertFalse(txn._aborted)
+
+ def test_as_context_manager_w_error(self):
+ class _Test(object):
+ _committed = False
+ _aborted = False
+ def commit(self):
+ self._committed = True
+ def abort(self):
+ self._aborted = True
+ tm = self._makeOne()
+ try:
+ with tm:
+ tm._txn = txn = _Test()
+ 1/0
+ except ZeroDivisionError:
+ pass
+ self.assertFalse(txn._committed)
+ self.assertTrue(txn._aborted)
+
+ def test_savepoint_default(self):
+ class _Test(object):
+ _sp = None
+ def savepoint(self, optimistic):
+ self._sp = optimistic
+ tm = self._makeOne()
+ tm._txn = txn = _Test()
+ tm.savepoint()
+ self.assertFalse(txn._sp)
+
+ def test_savepoint_explicit(self):
+ class _Test(object):
+ _sp = None
+ def savepoint(self, optimistic):
+ self._sp = optimistic
+ tm = self._makeOne()
+ tm._txn = txn = _Test()
+ tm.savepoint(True)
+ self.assertTrue(txn._sp)
+
+ def test_attempts_w_invalid_count(self):
+ tm = self._makeOne()
+ self.assertRaises(ValueError, list, tm.attempts(0))
+ self.assertRaises(ValueError, list, tm.attempts(-1))
+ self.assertRaises(ValueError, list, tm.attempts(-10))
+
+ def test_attempts_w_valid_count(self):
+ tm = self._makeOne()
+ found = list(tm.attempts(1))
+ self.assertEqual(len(found), 1)
+ self.assertTrue(found[0] is tm)
+
+ def test_attempts_w_default_count(self):
+ from transaction._manager import Attempt
+ tm = self._makeOne()
+ found = list(tm.attempts())
+ self.assertEqual(len(found), 3)
+ for attempt in found[:-1]:
+ self.assertTrue(isinstance(attempt, Attempt))
+ self.assertTrue(attempt.manager is tm)
+ self.assertTrue(found[-1] is tm)
+
+ def test__retryable_w_transient_error(self):
+ from transaction.interfaces import TransientError
+ tm = self._makeOne()
+ self.assertTrue(tm._retryable(TransientError, object()))
+
+ def test__retryable_w_transient_subclass(self):
+ from transaction.interfaces import TransientError
+ class _Derived(TransientError):
+ pass
+ tm = self._makeOne()
+ self.assertTrue(tm._retryable(_Derived, object()))
+
+ def test__retryable_w_normal_exception_no_resources(self):
+ tm = self._makeOne()
+ self.assertFalse(tm._retryable(Exception, object()))
+
+ def test__retryable_w_normal_exception_w_resource_voting_yes(self):
+ class _Resource(object):
+ def should_retry(self, err):
+ return True
+ tm = self._makeOne()
+ tm.get()._resources.append(_Resource())
+ self.assertTrue(tm._retryable(Exception, object()))
+
+ # basic tests with two sub trans jars
+ # really we only need one, so tests for
+ # sub1 should identical to tests for sub2
+ def test_commit_normal(self):
+
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1.modify()
+ sub2.modify()
+
+ mgr.commit()
+
+ assert sub1._p_jar.ccommit_sub == 0
+ assert sub1._p_jar.ctpc_finish == 1
+
+ def test_abort_normal(self):
+
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1.modify()
+ sub2.modify()
+
+ mgr.abort()
+
+ assert sub2._p_jar.cabort == 1
+
+
+ # repeat adding in a nonsub trans jars
+
+ def test_commit_w_nonsub_jar(self):
+
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ nosub1.modify()
+
+ mgr.commit()
+
+ assert nosub1._p_jar.ctpc_finish == 1
+
+ def test_abort_w_nonsub_jar(self):
+
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ nosub1.modify()
+
+ mgr.abort()
+
+ assert nosub1._p_jar.ctpc_finish == 0
+ assert nosub1._p_jar.cabort == 1
+
+
+ ### Failure Mode Tests
+ #
+ # ok now we do some more interesting
+ # tests that check the implementations
+ # error handling by throwing errors from
+ # various jar methods
+ ###
+
+ # first the recoverable errors
+
+ def test_abort_w_broken_jar(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1._p_jar = BasicJar(errors='abort')
+ nosub1.modify()
+ sub1.modify(nojar=1)
+ sub2.modify()
+ try:
+ mgr.abort()
+ except TestTxnException:
+ pass
+
+ assert nosub1._p_jar.cabort == 1
+ assert sub2._p_jar.cabort == 1
+
+ def test_commit_w_broken_jar_commit(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1._p_jar = BasicJar(errors='commit')
+ nosub1.modify()
+ sub1.modify(nojar=1)
+ try:
+ mgr.commit()
+ except TestTxnException:
+ pass
+
+ assert nosub1._p_jar.ctpc_finish == 0
+ assert nosub1._p_jar.ccommit == 1
+ assert nosub1._p_jar.ctpc_abort == 1
+
+ def test_commit_w_broken_jar_tpc_vote(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1._p_jar = BasicJar(errors='tpc_vote')
+ nosub1.modify()
+ sub1.modify(nojar=1)
+ try:
+ mgr.commit()
+ except TestTxnException:
+ pass
+
+ assert nosub1._p_jar.ctpc_finish == 0
+ assert nosub1._p_jar.ccommit == 1
+ assert nosub1._p_jar.ctpc_abort == 1
+ assert sub1._p_jar.ctpc_abort == 1
+
+ def test_commit_w_broken_jar_tpc_begin(self):
+ # ok this test reveals a bug in the TM.py
+ # as the nosub tpc_abort there is ignored.
+
+ # nosub calling method tpc_begin
+ # nosub calling method commit
+ # sub calling method tpc_begin
+ # sub calling method abort
+ # sub calling method tpc_abort
+ # nosub calling method tpc_abort
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1._p_jar = BasicJar(errors='tpc_begin')
+ nosub1.modify()
+ sub1.modify(nojar=1)
+ try:
+ mgr.commit()
+ except TestTxnException:
+ pass
+
+ assert nosub1._p_jar.ctpc_abort == 1
+ assert sub1._p_jar.ctpc_abort == 1
+
+ def test_commit_w_broken_jar_tpc_abort_tpc_vote(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ mgr, sub1, sub2, sub3, nosub1 = self._makePopulated()
+ sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
+ nosub1.modify()
+ sub1.modify(nojar=1)
+ try:
+ mgr.commit()
+ except TestTxnException:
+ pass
+
+ assert nosub1._p_jar.ctpc_abort == 1
+
+
+class AttemptTests(unittest.TestCase):
+
+ def _makeOne(self, manager):
+ from transaction._manager import Attempt
+ return Attempt(manager)
+
+ def test___enter__(self):
+ manager = DummyManager()
+ inst = self._makeOne(manager)
+ inst.__enter__()
+ self.assertTrue(manager.entered)
+
+ def test___exit__no_exc_no_commit_exception(self):
+ manager = DummyManager()
+ inst = self._makeOne(manager)
+ result = inst.__exit__(None, None, None)
+ self.assertFalse(result)
+ self.assertTrue(manager.committed)
+
+ def test___exit__no_exc_nonretryable_commit_exception(self):
+ manager = DummyManager(raise_on_commit=ValueError)
+ inst = self._makeOne(manager)
+ self.assertRaises(ValueError, inst.__exit__, None, None, None)
+ self.assertTrue(manager.committed)
+ self.assertTrue(manager.aborted)
+
+ def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
+ manager = DummyManager(raise_on_abort=ValueError,
+ raise_on_commit=KeyError)
+ inst = self._makeOne(manager)
+ self.assertRaises(ValueError, inst.__exit__, None, None, None)
+ self.assertTrue(manager.committed)
+ self.assertTrue(manager.aborted)
+
+ def test___exit__no_exc_retryable_commit_exception(self):
+ from transaction.interfaces import TransientError
+ manager = DummyManager(raise_on_commit=TransientError)
+ inst = self._makeOne(manager)
+ result = inst.__exit__(None, None, None)
+ self.assertTrue(result)
+ self.assertTrue(manager.committed)
+ self.assertTrue(manager.aborted)
+
+ def test___exit__with_exception_value_retryable(self):
+ from transaction.interfaces import TransientError
+ manager = DummyManager()
+ inst = self._makeOne(manager)
+ result = inst.__exit__(TransientError, TransientError(), None)
+ self.assertTrue(result)
+ self.assertFalse(manager.committed)
+ self.assertTrue(manager.aborted)
+
+ def test___exit__with_exception_value_nonretryable(self):
+ manager = DummyManager()
+ inst = self._makeOne(manager)
+ self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
+ self.assertFalse(manager.committed)
+ self.assertTrue(manager.aborted)
+
+
+class DummyManager(object):
+ entered = False
+ committed = False
+ aborted = False
+
+ def __init__(self, raise_on_commit=None, raise_on_abort=None):
+ self.raise_on_commit = raise_on_commit
+ self.raise_on_abort = raise_on_abort
+
+ def _retryable(self, t, v):
+ from transaction._manager import TransientError
+ return issubclass(t, TransientError)
+
+ def __enter__(self):
+ self.entered = True
+
+ def abort(self):
+ self.aborted = True
+ if self.raise_on_abort:
+ raise self.raise_on_abort
+
+ def commit(self):
+ self.committed = True
+ if self.raise_on_commit:
+ raise self.raise_on_commit
+
+
+class DataObject:
+
+ def __init__(self, transaction_manager, nost=0):
+ self.transaction_manager = transaction_manager
+ self.nost = nost
+ self._p_jar = None
+
+ def modify(self, nojar=0, tracing=0):
+ if not nojar:
+ if self.nost:
+ self._p_jar = BasicJar(tracing=tracing)
+ else:
+ self._p_jar = BasicJar(tracing=tracing)
+ self.transaction_manager.get().join(self._p_jar)
+
+
+class TestTxnException(Exception):
+ pass
+
+
+class BasicJar:
+
+ def __init__(self, errors=(), tracing=0):
+ if not isinstance(errors, tuple):
+ errors = errors,
+ self.errors = errors
+ self.tracing = tracing
+ self.cabort = 0
+ self.ccommit = 0
+ self.ctpc_begin = 0
+ self.ctpc_abort = 0
+ self.ctpc_vote = 0
+ self.ctpc_finish = 0
+ self.cabort_sub = 0
+ self.ccommit_sub = 0
+
+ def __repr__(self):
+ return "<%s %X %s>" % (self.__class__.__name__,
+ positive_id(self),
+ self.errors)
+
+ def sortKey(self):
+ # All these jars use the same sort key, and Python's list.sort()
+ # is stable. These two
+ return self.__class__.__name__
+
+ def check(self, method):
+ if self.tracing:
+ print('%s calling method %s'%(str(self.tracing),method))
+
+ if method in self.errors:
+ raise TestTxnException("error %s" % method)
+
+ ## basic jar txn interface
+
+ def abort(self, *args):
+ self.check('abort')
+ self.cabort += 1
+
+ def commit(self, *args):
+ self.check('commit')
+ self.ccommit += 1
+
+ def tpc_begin(self, txn, sub=0):
+ self.check('tpc_begin')
+ self.ctpc_begin += 1
+
+ def tpc_vote(self, *args):
+ self.check('tpc_vote')
+ self.ctpc_vote += 1
+
+ def tpc_abort(self, *args):
+ self.check('tpc_abort')
+ self.ctpc_abort += 1
+
+ def tpc_finish(self, *args):
+ self.check('tpc_finish')
+ self.ctpc_finish += 1
+
+
+class DummySynch(object):
+ def __init__(self):
+ self._txns = set()
+ def newTransaction(self, txn):
+ self._txns.add(txn)
+
+
+def positive_id(obj):
+ """Return id(obj) as a non-negative integer."""
+ import struct
+ _ADDRESS_MASK = 256 ** struct.calcsize('P')
+
+ result = id(obj)
+ if result < 0:
+ result += _ADDRESS_MASK
+ assert result > 0
+ return result
+
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(TransactionManagerTests),
+ unittest.makeSuite(AttemptTests),
+ ))
Copied: transaction/trunk/transaction/tests/test__transaction.py (from rev 128495, transaction/trunk/transaction/tests/test_transaction.py)
===================================================================
--- transaction/trunk/transaction/tests/test__transaction.py (rev 0)
+++ transaction/trunk/transaction/tests/test__transaction.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -0,0 +1,1439 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+"""Test transaction behavior for variety of cases.
+
+I wrote these unittests to investigate some odd transaction
+behavior when doing unittests of integrating non sub transaction
+aware objects, and to insure proper txn behavior. these
+tests test the transaction system independent of the rest of the
+zodb.
+
+you can see the method calls to a jar by passing the
+keyword arg tracing to the modify method of a dataobject.
+the value of the arg is a prefix used for tracing print calls
+to that objects jar.
+
+the number of times a jar method was called can be inspected
+by looking at an attribute of the jar that is the method
+name prefixed with a c (count/check).
+
+i've included some tracing examples for tests that i thought
+were illuminating as doc strings below.
+
+TODO
+
+ add in tests for objects which are modified multiple times,
+ for example an object that gets modified in multiple sub txns.
+"""
+import unittest
+
+
+class TransactionTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import Transaction
+ return Transaction
+
+ def _makeOne(self, synchronizers=None, manager=None):
+ return self._getTargetClass()(synchronizers, manager)
+
+ def test_ctor_defaults(self):
+ from transaction.weakset import WeakSet
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ self.assertTrue(isinstance(txn._synchronizers, WeakSet))
+ self.assertEqual(len(txn._synchronizers), 0)
+ self.assertTrue(txn._manager is None)
+ self.assertEqual(txn.user, "")
+ self.assertEqual(txn.description, "")
+ self.assertTrue(txn._savepoint2index is None)
+ self.assertEqual(txn._savepoint_index, 0)
+ self.assertEqual(txn._resources, [])
+ self.assertEqual(txn._adapters, {})
+ self.assertEqual(txn._voted, {})
+ self.assertEqual(txn._extension, {})
+ self.assertTrue(txn.log is logger)
+ self.assertEqual(len(logger._log), 1)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'new transaction')
+ self.assertTrue(txn._failure_traceback is None)
+ self.assertEqual(txn._before_commit, [])
+ self.assertEqual(txn._after_commit, [])
+
+ def test_ctor_w_syncs(self):
+ from transaction.weakset import WeakSet
+ synchs = WeakSet()
+ txn = self._makeOne(synchronizers=synchs)
+ self.assertTrue(txn._synchronizers is synchs)
+
+ def test_isDoomed(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ self.assertFalse(txn.isDoomed())
+ txn.status = Status.DOOMED
+ self.assertTrue(txn.isDoomed())
+
+ def test_doom_active(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.doom()
+ self.assertTrue(txn.isDoomed())
+ self.assertEqual(txn.status, Status.DOOMED)
+
+ def test_doom_invalid(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ for status in Status.COMMITTING, Status.COMMITTED, Status.COMMITFAILED:
+ txn.status = status
+ self.assertRaises(ValueError, txn.doom)
+
+ def test_doom_already_doomed(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.status = Status.DOOMED
+ self.assertTrue(txn.isDoomed())
+ self.assertEqual(txn.status, Status.DOOMED)
+
+ def test__prior_operation_failed(self):
+ from transaction.interfaces import TransactionFailedError
+ from transaction.tests.common import assertRaisesEx
+ class _Traceback(object):
+ def getvalue(self):
+ return 'TRACEBACK'
+ txn = self._makeOne()
+ txn._failure_traceback = _Traceback()
+ err = assertRaisesEx(TransactionFailedError,
+ txn._prior_operation_failed)
+ self.assertTrue(str(err).startswith('An operation previously failed'))
+ self.assertTrue(str(err).endswith( "with traceback:\n\nTRACEBACK"))
+
+ def test_join_COMMITFAILED(self):
+ from transaction.interfaces import TransactionFailedError
+ from transaction._transaction import Status
+ class _Traceback(object):
+ def getvalue(self):
+ return 'TRACEBACK'
+ txn = self._makeOne()
+ txn.status = Status.COMMITFAILED
+ txn._failure_traceback = _Traceback()
+ self.assertRaises(TransactionFailedError, txn.join, object())
+
+ def test_join_COMMITTING(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.status = Status.COMMITTING
+ self.assertRaises(ValueError, txn.join, object())
+
+ def test_join_COMMITTED(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.status = Status.COMMITTED
+ self.assertRaises(ValueError, txn.join, object())
+
+ def test_join_DOOMED_non_preparing_wo_sp2index(self):
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.status = Status.DOOMED
+ resource = object()
+ txn.join(resource)
+ self.assertEqual(txn._resources, [resource])
+
+ def test_join_ACTIVE_w_preparing_w_sp2index(self):
+ from transaction._transaction import AbortSavepoint
+ from transaction._transaction import DataManagerAdapter
+ class _TSP(object):
+ def __init__(self):
+ self._savepoints = []
+ class _DM(object):
+ def prepare(self):
+ pass
+ txn = self._makeOne()
+ tsp = _TSP()
+ txn._savepoint2index = {tsp: object()}
+ dm = _DM
+ txn.join(dm)
+ self.assertEqual(len(txn._resources), 1)
+ dma = txn._resources[0]
+ self.assertTrue(isinstance(dma, DataManagerAdapter))
+ self.assertTrue(txn._resources[0]._datamanager is dm)
+ self.assertEqual(len(tsp._savepoints), 1)
+ self.assertTrue(isinstance(tsp._savepoints[0], AbortSavepoint))
+ self.assertTrue(tsp._savepoints[0].datamanager is dma)
+ self.assertTrue(tsp._savepoints[0].transaction is txn)
+
+ def test__unjoin_miss(self):
+ txn = self._makeOne()
+ txn._unjoin(object()) #no raise
+
+ def test__unjoin_hit(self):
+ txn = self._makeOne()
+ resource = object()
+ txn._resources.append(resource)
+ txn._unjoin(resource)
+ self.assertEqual(txn._resources, [])
+
+ def test_savepoint_COMMITFAILED(self):
+ from transaction.interfaces import TransactionFailedError
+ from transaction._transaction import Status
+ class _Traceback(object):
+ def getvalue(self):
+ return 'TRACEBACK'
+ txn = self._makeOne()
+ txn.status = Status.COMMITFAILED
+ txn._failure_traceback = _Traceback()
+ self.assertRaises(TransactionFailedError, txn.savepoint)
+
+ def test_savepoint_empty(self):
+ from weakref import WeakKeyDictionary
+ from transaction import _transaction
+ from transaction._transaction import Savepoint
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ sp = txn.savepoint()
+ self.assertTrue(isinstance(sp, Savepoint))
+ self.assertTrue(sp.transaction is txn)
+ self.assertEqual(sp._savepoints, [])
+ self.assertEqual(txn._savepoint_index, 1)
+ self.assertTrue(isinstance(txn._savepoint2index, WeakKeyDictionary))
+ self.assertEqual(txn._savepoint2index[sp], 1)
+
+ def test_savepoint_non_optimistc_resource_wo_support(self):
+ from transaction import _transaction
+ from transaction._transaction import Status
+ from transaction._compat import StringIO
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ resource = object()
+ txn._resources.append(resource)
+ self.assertRaises(TypeError, txn.savepoint)
+ self.assertEqual(txn.status, Status.COMMITFAILED)
+ self.assertTrue(isinstance(txn._failure_traceback, StringIO))
+ self.assertTrue('TypeError' in txn._failure_traceback.getvalue())
+ self.assertEqual(len(logger._log), 2)
+ self.assertEqual(logger._log[0][0], 'error')
+ self.assertTrue(logger._log[0][1].startswith('Error in abort'))
+ self.assertEqual(logger._log[1][0], 'error')
+ self.assertTrue(logger._log[1][1].startswith('Error in tpc_abort'))
+
+ def test__remove_and_invalidate_after_miss(self):
+ from weakref import WeakKeyDictionary
+ txn = self._makeOne()
+ txn._savepoint2index = WeakKeyDictionary()
+ class _SP(object):
+ def __init__(self, txn):
+ self.transaction = txn
+ holdme = []
+ for i in range(10):
+ sp = _SP(txn)
+ holdme.append(sp) #prevent gc
+ txn._savepoint2index[sp] = i
+ self.assertEqual(len(txn._savepoint2index), 10)
+ self.assertRaises(KeyError, txn._remove_and_invalidate_after, _SP(txn))
+ self.assertEqual(len(txn._savepoint2index), 10)
+
+ def test__remove_and_invalidate_after_hit(self):
+ from weakref import WeakKeyDictionary
+ txn = self._makeOne()
+ txn._savepoint2index = WeakKeyDictionary()
+ class _SP(object):
+ def __init__(self, txn, index):
+ self.transaction = txn
+ self._index = index
+ def __lt__(self, other):
+ return self._index < other._index
+ def __repr__(self):
+ return '_SP: %d' % self._index
+ holdme = []
+ for i in range(10):
+ sp = _SP(txn, i)
+ holdme.append(sp) #prevent gc
+ txn._savepoint2index[sp] = i
+ self.assertEqual(len(txn._savepoint2index), 10)
+ txn._remove_and_invalidate_after(holdme[1])
+ self.assertEqual(sorted(txn._savepoint2index), sorted(holdme[:2]))
+
+ def test__invalidate_all_savepoints(self):
+ from weakref import WeakKeyDictionary
+ txn = self._makeOne()
+ txn._savepoint2index = WeakKeyDictionary()
+ class _SP(object):
+ def __init__(self, txn, index):
+ self.transaction = txn
+ self._index = index
+ def __repr__(self):
+ return '_SP: %d' % self._index
+ holdme = []
+ for i in range(10):
+ sp = _SP(txn, i)
+ holdme.append(sp) #prevent gc
+ txn._savepoint2index[sp] = i
+ self.assertEqual(len(txn._savepoint2index), 10)
+ txn._invalidate_all_savepoints()
+ self.assertEqual(list(txn._savepoint2index), [])
+
+ def test_register_wo_jar(self):
+ class _Dummy(object):
+ _p_jar = None
+ txn = self._makeOne()
+ self.assertRaises(ValueError, txn.register, _Dummy())
+
+ def test_register_w_jar(self):
+ class _Manager(object):
+ pass
+ mgr = _Manager()
+ class _Dummy(object):
+ _p_jar = mgr
+ txn = self._makeOne()
+ dummy = _Dummy()
+ txn.register(dummy)
+ resources = list(txn._resources)
+ self.assertEqual(len(resources), 1)
+ adapter = resources[0]
+ self.assertTrue(adapter.manager is mgr)
+ self.assertTrue(dummy in adapter.objects)
+ items = list(txn._adapters.items())
+ self.assertEqual(len(items), 1)
+ self.assertTrue(items[0][0] is mgr)
+ self.assertTrue(items[0][1] is adapter)
+
+ def test_register_w_jar_already_adapted(self):
+ class _Adapter(object):
+ def __init__(self):
+ self.objects = []
+ class _Manager(object):
+ pass
+ mgr = _Manager()
+ class _Dummy(object):
+ _p_jar = mgr
+ txn = self._makeOne()
+ txn._adapters[mgr] = adapter = _Adapter()
+ dummy = _Dummy()
+ txn.register(dummy)
+ self.assertTrue(dummy in adapter.objects)
+
+ def test_commit_DOOMED(self):
+ from transaction.interfaces import DoomedTransaction
+ from transaction._transaction import Status
+ txn = self._makeOne()
+ txn.status = Status.DOOMED
+ self.assertRaises(DoomedTransaction, txn.commit)
+
+ def test_commit_COMMITFAILED(self):
+ from transaction._transaction import Status
+ from transaction.interfaces import TransactionFailedError
+ class _Traceback(object):
+ def getvalue(self):
+ return 'TRACEBACK'
+ txn = self._makeOne()
+ txn.status = Status.COMMITFAILED
+ txn._failure_traceback = _Traceback()
+ self.assertRaises(TransactionFailedError, txn.commit)
+
+ def test_commit_wo_savepoints_wo_hooks_wo_synchronizers(self):
+ from transaction._transaction import Status
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Mgr(object):
+ def __init__(self, txn):
+ self._txn = txn
+ def free(self, txn):
+ assert txn is self._txn
+ self._txn = None
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ mgr = txn._manager = _Mgr(txn)
+ txn.commit()
+ self.assertEqual(txn.status, Status.COMMITTED)
+ self.assertTrue(mgr._txn is None)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'commit')
+
+ def test_commit_w_savepoints(self):
+ from weakref import WeakKeyDictionary
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _SP(object):
+ def __init__(self, txn, index):
+ self.transaction = txn
+ self._index = index
+ def __repr__(self):
+ return '_SP: %d' % self._index
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._savepoint2index = WeakKeyDictionary()
+ holdme = []
+ for i in range(10):
+ sp = _SP(txn, i)
+ holdme.append(sp) #prevent gc
+ txn._savepoint2index[sp] = i
+ logger._clear()
+ txn.commit()
+ self.assertEqual(list(txn._savepoint2index), [])
+
+ def test_commit_w_beforeCommitHooks(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._before_commit.append((_hook2, (), {}))
+ logger._clear()
+ txn.commit()
+ self.assertEqual(_hooked1, [(('one',), {'uno': 1})])
+ self.assertEqual(_hooked2, [((), {})])
+ self.assertEqual(txn._before_commit, [])
+
+ def test_commit_w_synchronizers(self):
+ from transaction.weakset import WeakSet
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Synch(object):
+ _before = _after = False
+ def beforeCompletion(self, txn):
+ self._before = txn
+ def afterCompletion(self, txn):
+ self._after = txn
+ synchs = [_Synch(), _Synch(), _Synch()]
+ ws = WeakSet()
+ for synch in synchs:
+ ws.add(synch)
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne(synchronizers=ws)
+ logger._clear()
+ txn.commit()
+ for synch in synchs:
+ self.assertTrue(synch._before is txn)
+ self.assertTrue(synch._after is txn)
+
+ def test_commit_w_afterCommitHooks(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._after_commit.append((_hook2, (), {}))
+ logger._clear()
+ txn.commit()
+ self.assertEqual(_hooked1, [((True, 'one',), {'uno': 1})])
+ self.assertEqual(_hooked2, [((True,), {})])
+ self.assertEqual(txn._after_commit, [])
+
+ def test_commit_error_w_afterCompleteHooks(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ class BrokenResource(object):
+ def sortKey(self):
+ return 'zzz'
+ def tpc_begin(self, txn):
+ raise ValueError('test')
+ broken = BrokenResource()
+ resource = Resource('aaa')
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._after_commit.append((_hook2, (), {}))
+ txn._resources.append(broken)
+ txn._resources.append(resource)
+ logger._clear()
+ self.assertRaises(ValueError, txn.commit)
+ self.assertEqual(_hooked1, [((False, 'one',), {'uno': 1})])
+ self.assertEqual(_hooked2, [((False,), {})])
+ self.assertEqual(txn._after_commit, [])
+ self.assertTrue(resource._b)
+ self.assertFalse(resource._c)
+ self.assertFalse(resource._v)
+ self.assertFalse(resource._f)
+ self.assertTrue(resource._a)
+ self.assertTrue(resource._x)
+
+ def test_commit_error_w_synchronizers(self):
+ from transaction.weakset import WeakSet
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Synch(object):
+ _before = _after = False
+ def beforeCompletion(self, txn):
+ self._before = txn
+ def afterCompletion(self, txn):
+ self._after = txn
+ synchs = [_Synch(), _Synch(), _Synch()]
+ ws = WeakSet()
+ for synch in synchs:
+ ws.add(synch)
+ class BrokenResource(object):
+ def sortKey(self):
+ return 'zzz'
+ def tpc_begin(self, txn):
+ raise ValueError('test')
+ broken = BrokenResource()
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne(synchronizers=ws)
+ logger._clear()
+ txn._resources.append(broken)
+ self.assertRaises(ValueError, txn.commit)
+ for synch in synchs:
+ self.assertTrue(synch._before is txn)
+ self.assertTrue(synch._after is txn) #called in _cleanup
+
+ def test_getBeforeCommitHooks_empty(self):
+ txn = self._makeOne()
+ self.assertEqual(list(txn.getBeforeCommitHooks()), [])
+
+ def test_addBeforeCommitHook(self):
+ def _hook(*args, **kw):
+ pass
+ txn = self._makeOne()
+ txn.addBeforeCommitHook(_hook, ('one',), dict(uno=1))
+ self.assertEqual(list(txn.getBeforeCommitHooks()),
+ [(_hook, ('one',), {'uno': 1})])
+
+ def test_addBeforeCommitHook_w_kws(self):
+ def _hook(*args, **kw):
+ pass
+ txn = self._makeOne()
+ txn.addBeforeCommitHook(_hook, ('one',))
+ self.assertEqual(list(txn.getBeforeCommitHooks()),
+ [(_hook, ('one',), {})])
+
+ def test_getAfterCommitHooks_empty(self):
+ txn = self._makeOne()
+ self.assertEqual(list(txn.getAfterCommitHooks()), [])
+
+ def test_addAfterCommitHook(self):
+ def _hook(*args, **kw):
+ pass
+ txn = self._makeOne()
+ txn.addAfterCommitHook(_hook, ('one',), dict(uno=1))
+ self.assertEqual(list(txn.getAfterCommitHooks()),
+ [(_hook, ('one',), {'uno': 1})])
+
+ def test_addAfterCommitHook_wo_kws(self):
+ def _hook(*args, **kw):
+ pass
+ txn = self._makeOne()
+ txn.addAfterCommitHook(_hook, ('one',))
+ self.assertEqual(list(txn.getAfterCommitHooks()),
+ [(_hook, ('one',), {})])
+
+ def test_callAfterCommitHook_w_error(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked2 = []
+ def _hook1(*args, **kw):
+ raise ValueError()
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn.addAfterCommitHook(_hook1, ('one',))
+ txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
+ txn._callAfterCommitHooks()
+ # second hook gets called even if first raises
+ self.assertEqual(_hooked2, [((True, 'two',), {'dos': 2})])
+ self.assertEqual(len(logger._log), 1)
+ self.assertEqual(logger._log[0][0], 'error')
+ self.assertTrue(logger._log[0][1].startswith(
+ "Error in after commit hook"))
+
+ def test_callAfterCommitHook_w_abort(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked2 = []
+ def _hook1(*args, **kw):
+ raise ValueError()
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn.addAfterCommitHook(_hook1, ('one',))
+ txn.addAfterCommitHook(_hook2, ('two',), dict(dos=2))
+ txn._callAfterCommitHooks()
+ self.assertEqual(logger._log[0][0], 'error')
+ self.assertTrue(logger._log[0][1].startswith(
+ "Error in after commit hook"))
+
+ def test__commitResources_normal(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ resources = [Resource('bbb'), Resource('aaa')]
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn._resources.extend(resources)
+ txn._commitResources()
+ self.assertEqual(len(txn._voted), 2)
+ for r in resources:
+ self.assertTrue(r._b and r._c and r._v and r._f)
+ self.assertFalse(r._a and r._x)
+ self.assertTrue(id(r) in txn._voted)
+ self.assertEqual(len(logger._log), 2)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+ self.assertEqual(logger._log[1][0], 'debug')
+ self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+
+ def test__commitResources_error_in_tpc_begin(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ resources = [Resource('bbb', 'tpc_begin'), Resource('aaa')]
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn._resources.extend(resources)
+ self.assertRaises(ValueError, txn._commitResources)
+ for r in resources:
+ if r._key == 'aaa':
+ self.assertTrue(r._b)
+ else:
+ self.assertFalse(r._b)
+ self.assertFalse(r._c and r._v and r._f)
+ self.assertTrue(r._a and r._x)
+ self.assertEqual(len(logger._log), 0)
+
+ def test__commitResources_error_in_commit(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ resources = [Resource('bbb', 'commit'), Resource('aaa')]
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn._resources.extend(resources)
+ self.assertRaises(ValueError, txn._commitResources)
+ for r in resources:
+ self.assertTrue(r._b)
+ if r._key == 'aaa':
+ self.assertTrue(r._c)
+ else:
+ self.assertFalse(r._c)
+ self.assertFalse(r._v and r._f)
+ self.assertTrue(r._a and r._x)
+ self.assertEqual(len(logger._log), 1)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+
+ def test__commitResources_error_in_tpc_vote(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ resources = [Resource('bbb', 'tpc_vote'), Resource('aaa')]
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn._resources.extend(resources)
+ self.assertRaises(ValueError, txn._commitResources)
+ self.assertEqual(len(txn._voted), 1)
+ for r in resources:
+ self.assertTrue(r._b and r._c)
+ if r._key == 'aaa':
+ self.assertTrue(id(r) in txn._voted)
+ self.assertTrue(r._v)
+ self.assertFalse(r._f)
+ self.assertFalse(r._a)
+ self.assertTrue(r._x)
+ else:
+ self.assertFalse(id(r) in txn._voted)
+ self.assertFalse(r._v)
+ self.assertFalse(r._f)
+ self.assertTrue(r._a and r._x)
+ self.assertEqual(len(logger._log), 2)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+ self.assertEqual(logger._log[1][0], 'debug')
+ self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+
+ def test__commitResources_error_in_tpc_finish(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ resources = [Resource('bbb', 'tpc_finish'), Resource('aaa')]
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ txn._resources.extend(resources)
+ self.assertRaises(ValueError, txn._commitResources)
+ for r in resources:
+ self.assertTrue(r._b and r._c and r._v)
+ self.assertTrue(id(r) in txn._voted)
+ if r._key == 'aaa':
+ self.assertTrue(r._f)
+ else:
+ self.assertFalse(r._f)
+ self.assertFalse(r._a and r._x) #no cleanup if tpc_finish raises
+ self.assertEqual(len(logger._log), 3)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'commit Resource: aaa')
+ self.assertEqual(logger._log[1][0], 'debug')
+ self.assertEqual(logger._log[1][1], 'commit Resource: bbb')
+ self.assertEqual(logger._log[2][0], 'critical')
+ self.assertTrue(logger._log[2][1].startswith(
+ 'A storage error occurred'))
+
+ def test_abort_wo_savepoints_wo_hooks_wo_synchronizers(self):
+ from transaction._transaction import Status
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Mgr(object):
+ def __init__(self, txn):
+ self._txn = txn
+ def free(self, txn):
+ assert txn is self._txn
+ self._txn = None
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ logger._clear()
+ mgr = txn._manager = _Mgr(txn)
+ txn.abort()
+ self.assertEqual(txn.status, Status.ACTIVE)
+ self.assertTrue(mgr._txn is None)
+ self.assertEqual(logger._log[0][0], 'debug')
+ self.assertEqual(logger._log[0][1], 'abort')
+
+ def test_abort_w_savepoints(self):
+ from weakref import WeakKeyDictionary
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _SP(object):
+ def __init__(self, txn, index):
+ self.transaction = txn
+ self._index = index
+ def __repr__(self):
+ return '_SP: %d' % self._index
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._savepoint2index = WeakKeyDictionary()
+ holdme = []
+ for i in range(10):
+ sp = _SP(txn, i)
+ holdme.append(sp) #prevent gc
+ txn._savepoint2index[sp] = i
+ logger._clear()
+ txn.abort()
+ self.assertEqual(list(txn._savepoint2index), [])
+
+ def test_abort_w_beforeCommitHooks(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._before_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._before_commit.append((_hook2, (), {}))
+ logger._clear()
+ txn.abort()
+ self.assertEqual(_hooked1, [])
+ self.assertEqual(_hooked2, [])
+ # Hooks are neither called nor cleared on abort
+ self.assertEqual(list(txn.getBeforeCommitHooks()),
+ [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+
+ def test_abort_w_synchronizers(self):
+ from transaction.weakset import WeakSet
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Synch(object):
+ _before = _after = False
+ def beforeCompletion(self, txn):
+ self._before = txn
+ def afterCompletion(self, txn):
+ self._after = txn
+ synchs = [_Synch(), _Synch(), _Synch()]
+ ws = WeakSet()
+ for synch in synchs:
+ ws.add(synch)
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne(synchronizers=ws)
+ logger._clear()
+ txn.abort()
+ for synch in synchs:
+ self.assertTrue(synch._before is txn)
+ self.assertTrue(synch._after is txn)
+
+ def test_abort_w_afterCommitHooks(self):
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._after_commit.append((_hook2, (), {}))
+ logger._clear()
+ txn.abort()
+ # Hooks are neither called nor cleared on abort
+ self.assertEqual(_hooked1, [])
+ self.assertEqual(_hooked2, [])
+ self.assertEqual(list(txn.getAfterCommitHooks()),
+ [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+
+ def test_abort_error_w_afterCompleteHooks(self):
+ from transaction import _transaction
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ class BrokenResource(object):
+ def sortKey(self):
+ return 'zzz'
+ def abort(self, txn):
+ raise ValueError('test')
+ broken = BrokenResource()
+ resource = Resource('aaa')
+ _hooked1, _hooked2 = [], []
+ def _hook1(*args, **kw):
+ _hooked1.append((args, kw))
+ def _hook2(*args, **kw):
+ _hooked2.append((args, kw))
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ txn = self._makeOne()
+ txn._after_commit.append((_hook1, ('one',), {'uno': 1}))
+ txn._after_commit.append((_hook2, (), {}))
+ txn._resources.append(broken)
+ txn._resources.append(resource)
+ logger._clear()
+ self.assertRaises(ValueError, txn.abort)
+ # Hooks are neither called nor cleared on abort
+ self.assertEqual(_hooked1, [])
+ self.assertEqual(_hooked2, [])
+ self.assertEqual(list(txn.getAfterCommitHooks()),
+ [(_hook1, ('one',), {'uno': 1}), (_hook2, (), {})])
+ self.assertTrue(resource._a)
+ self.assertFalse(resource._x)
+
+ def test_abort_error_w_synchronizers(self):
+ from transaction.weakset import WeakSet
+ from transaction.tests.common import DummyLogger
+ from transaction.tests.common import Monkey
+ from transaction import _transaction
+ class _Synch(object):
+ _before = _after = False
+ def beforeCompletion(self, txn):
+ self._before = txn
+ def afterCompletion(self, txn):
+ self._after = txn
+ synchs = [_Synch(), _Synch(), _Synch()]
+ ws = WeakSet()
+ for synch in synchs:
+ ws.add(synch)
+ class BrokenResource(object):
+ def sortKey(self):
+ return 'zzz'
+ def abort(self, txn):
+ raise ValueError('test')
+ broken = BrokenResource()
+ logger = DummyLogger()
+ with Monkey(_transaction, _LOGGER=logger):
+ t = self._makeOne(synchronizers=ws)
+ logger._clear()
+ t._resources.append(broken)
+ self.assertRaises(ValueError, t.abort)
+ for synch in synchs:
+ self.assertTrue(synch._before is t)
+ self.assertTrue(synch._after is t) #called in _cleanup
+
+ def test_note(self):
+ txn = self._makeOne()
+ try:
+ txn.note('This is a note.')
+ self.assertEqual(txn.description, 'This is a note.')
+ txn.note('Another.')
+ self.assertEqual(txn.description, 'This is a note.\nAnother.')
+ finally:
+ txn.abort()
+
+ def test_setUser_default_path(self):
+ txn = self._makeOne()
+ txn.setUser('phreddy')
+ self.assertEqual(txn.user, '/ phreddy')
+
+ def test_setUser_explicit_path(self):
+ txn = self._makeOne()
+ txn.setUser('phreddy', '/bedrock')
+ self.assertEqual(txn.user, '/bedrock phreddy')
+
+ def test_setExtendedInfo_single(self):
+ txn = self._makeOne()
+ txn.setExtendedInfo('frob', 'qux')
+ self.assertEqual(txn._extension, {'frob': 'qux'})
+
+ def test_setExtendedInfo_multiple(self):
+ txn = self._makeOne()
+ txn.setExtendedInfo('frob', 'qux')
+ txn.setExtendedInfo('baz', 'spam')
+ txn.setExtendedInfo('frob', 'quxxxx')
+ self.assertEqual(txn._extension, {'frob': 'quxxxx', 'baz': 'spam'})
+
+
+class MultiObjectResourceAdapterTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import MultiObjectResourceAdapter
+ return MultiObjectResourceAdapter
+
+ def _makeOne(self, jar):
+ return self._getTargetClass()(jar)
+
+ def _makeJar(self, key):
+ class _Resource(Resource):
+ def __init__(self, key):
+ super(_Resource, self).__init__(key)
+ self._c = []
+ self._a = []
+ def commit(self, obj, txn):
+ self._c.append((obj, txn))
+ def abort(self, obj, txn):
+ self._a.append((obj, txn))
+ return _Resource(key)
+
+ def _makeDummy(self, kind, name):
+ class _Dummy(object):
+ def __init__(self, kind, name):
+ self._kind = kind
+ self._name = name
+ def __repr__(self):
+ return '<%s: %s>' % (self._kind, self._name)
+ return _Dummy(kind, name)
+
+ def test_ctor(self):
+ jar = self._makeJar('aaa')
+ mora = self._makeOne(jar)
+ self.assertTrue(mora.manager is jar)
+ self.assertEqual(mora.objects, [])
+ self.assertEqual(mora.ncommitted, 0)
+
+ def test___repr__(self):
+ jar = self._makeJar('bbb')
+ mora = self._makeOne(jar)
+ self.assertEqual(repr(mora),
+ '<MultiObjectResourceAdapter '
+ 'for Resource: bbb at %s>' % id(mora))
+
+ def test_sortKey(self):
+ jar = self._makeJar('ccc')
+ mora = self._makeOne(jar)
+ self.assertEqual(mora.sortKey(), 'ccc')
+
+ def test_tpc_begin(self):
+ jar = self._makeJar('ddd')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_begin(txn)
+ self.assertTrue(jar._b)
+
+ def test_commit(self):
+ jar = self._makeJar('eee')
+ objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
+ mora = self._makeOne(jar)
+ mora.objects.extend(objects)
+ txn = self._makeDummy('txn', 'c')
+ mora.commit(txn)
+ self.assertEqual(jar._c, [(objects[0], txn), (objects[1], txn)])
+
+ def test_tpc_vote(self):
+ jar = self._makeJar('fff')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_vote(txn)
+ self.assertTrue(jar._v)
+
+ def test_tpc_finish(self):
+ jar = self._makeJar('ggg')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_finish(txn)
+ self.assertTrue(jar._f)
+
+ def test_abort(self):
+ jar = self._makeJar('hhh')
+ objects = [self._makeDummy('obj', 'a'), self._makeDummy('obj', 'b')]
+ mora = self._makeOne(jar)
+ mora.objects.extend(objects)
+ txn = self._makeDummy('txn', 'c')
+ mora.abort(txn)
+ self.assertEqual(jar._a, [(objects[0], txn), (objects[1], txn)])
+
+ def test_abort_w_error(self):
+ from transaction.tests.common import DummyLogger
+ jar = self._makeJar('hhh')
+ objects = [self._makeDummy('obj', 'a'),
+ self._makeDummy('obj', 'b'),
+ self._makeDummy('obj', 'c'),
+ ]
+ _old_abort = jar.abort
+ def _abort(obj, txn):
+ if obj._name == 'b':
+ raise ValueError()
+ _old_abort(obj, txn)
+ jar.abort = _abort
+ mora = self._makeOne(jar)
+ mora.objects.extend(objects)
+ txn = self._makeDummy('txn', 'c')
+ txn.log = log = DummyLogger()
+ self.assertRaises(ValueError, mora.abort, txn)
+ self.assertEqual(jar._a, [(objects[0], txn), (objects[2], txn)])
+
+ def test_tpc_abort(self):
+ jar = self._makeJar('iii')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_abort(txn)
+ self.assertTrue(jar._x)
+
+
+class Test_rm_key(unittest.TestCase):
+
+ def _callFUT(self, oid):
+ from transaction._transaction import rm_key
+ return rm_key(oid)
+
+ def test_miss(self):
+ self.assertTrue(self._callFUT(object()) is None)
+
+ def test_hit(self):
+ self.assertEqual(self._callFUT(Resource('zzz')), 'zzz')
+
+
+class Test_object_hint(unittest.TestCase):
+
+ def _callFUT(self, oid):
+ from transaction._transaction import object_hint
+ return object_hint(oid)
+
+ def test_miss(self):
+ class _Test(object):
+ pass
+ test = _Test()
+ self.assertEqual(self._callFUT(test), "_Test oid=None")
+
+ def test_hit(self):
+ class _Test(object):
+ pass
+ test = _Test()
+ test._p_oid = 'OID'
+ self.assertEqual(self._callFUT(test), "_Test oid='OID'")
+
+
+class Test_oid_repr(unittest.TestCase):
+
+ def _callFUT(self, oid):
+ from transaction._transaction import oid_repr
+ return oid_repr(oid)
+
+ def test_as_nonstring(self):
+ self.assertEqual(self._callFUT(123), '123')
+
+ def test_as_string_not_8_chars(self):
+ self.assertEqual(self._callFUT('a'), "'a'")
+
+ def test_as_string_z64(self):
+ s = '\0'*8
+ self.assertEqual(self._callFUT(s), '0x00')
+
+ def test_as_string_all_Fs(self):
+ s = '\1'*8
+ self.assertEqual(self._callFUT(s), '0x0101010101010101')
+
+
+class DataManagerAdapterTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import DataManagerAdapter
+ return DataManagerAdapter
+
+ def _makeOne(self, jar):
+ return self._getTargetClass()(jar)
+
+ def _makeJar(self, key):
+ class _Resource(Resource):
+ _p = False
+ def prepare(self, txn):
+ self._p = True
+ return _Resource(key)
+
+ def _makeDummy(self, kind, name):
+ class _Dummy(object):
+ def __init__(self, kind, name):
+ self._kind = kind
+ self._name = name
+ def __repr__(self):
+ return '<%s: %s>' % (self._kind, self._name)
+ return _Dummy(kind, name)
+
+ def test_ctor(self):
+ jar = self._makeJar('aaa')
+ dma = self._makeOne(jar)
+ self.assertTrue(dma._datamanager is jar)
+
+ def test_commit(self):
+ jar = self._makeJar('bbb')
+ mora = self._makeOne(jar)
+ txn = self._makeDummy('txn', 'c')
+ mora.commit(txn)
+ self.assertFalse(jar._c) #no-op
+
+ def test_abort(self):
+ jar = self._makeJar('ccc')
+ mora = self._makeOne(jar)
+ txn = self._makeDummy('txn', 'c')
+ mora.abort(txn)
+ self.assertTrue(jar._a)
+
+ def test_tpc_begin(self):
+ jar = self._makeJar('ddd')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_begin(txn)
+ self.assertFalse(jar._b) #no-op
+
+ def test_tpc_abort(self):
+ jar = self._makeJar('eee')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_abort(txn)
+ self.assertFalse(jar._f)
+ self.assertTrue(jar._a)
+
+ def test_tpc_finish(self):
+ jar = self._makeJar('fff')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_finish(txn)
+ self.assertFalse(jar._f)
+ self.assertTrue(jar._c)
+
+ def test_tpc_vote(self):
+ jar = self._makeJar('ggg')
+ mora = self._makeOne(jar)
+ txn = object()
+ mora.tpc_vote(txn)
+ self.assertFalse(jar._v)
+ self.assertTrue(jar._p)
+
+ def test_sortKey(self):
+ jar = self._makeJar('hhh')
+ mora = self._makeOne(jar)
+ self.assertEqual(mora.sortKey(), 'hhh')
+
+
+class SavepointTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import Savepoint
+ return Savepoint
+
+ def _makeOne(self, txn, optimistic, *resources):
+ return self._getTargetClass()(txn, optimistic, *resources)
+
+ def test_ctor_w_savepoint_oblivious_resource_non_optimistic(self):
+ txn = object()
+ resource = object()
+ self.assertRaises(TypeError, self._makeOne, txn, False, resource)
+
+ def test_ctor_w_savepoint_oblivious_resource_optimistic(self):
+ from transaction._transaction import NoRollbackSavepoint
+ txn = object()
+ resource = object()
+ sp = self._makeOne(txn, True, resource)
+ self.assertEqual(len(sp._savepoints), 1)
+ self.assertTrue(isinstance(sp._savepoints[0], NoRollbackSavepoint))
+ self.assertTrue(sp._savepoints[0].datamanager is resource)
+
+ def test_ctor_w_savepoint_aware_resources(self):
+ class _Aware(object):
+ def savepoint(self):
+ return self
+ txn = object()
+ one = _Aware()
+ another = _Aware()
+ sp = self._makeOne(txn, True, one, another)
+ self.assertEqual(len(sp._savepoints), 2)
+ self.assertTrue(isinstance(sp._savepoints[0], _Aware))
+ self.assertTrue(sp._savepoints[0] is one)
+ self.assertTrue(isinstance(sp._savepoints[1], _Aware))
+ self.assertTrue(sp._savepoints[1] is another)
+
+ def test_rollback_w_txn_None(self):
+ from transaction.interfaces import InvalidSavepointRollbackError
+ txn = None
+ class _Aware(object):
+ def savepoint(self):
+ return self
+ resource = _Aware()
+ sp = self._makeOne(txn, False, resource)
+ self.assertRaises(InvalidSavepointRollbackError, sp.rollback)
+
+ def test_rollback_w_sp_error(self):
+ class _TXN(object):
+ _sarce = False
+ _raia = None
+ def _saveAndRaiseCommitishError(self):
+ import sys
+ from transaction._compat import reraise
+ self._sarce = True
+ reraise(*sys.exc_info())
+ def _remove_and_invalidate_after(self, sp):
+ self._raia = sp
+ class _Broken(object):
+ def rollback(self):
+ raise ValueError()
+ _broken = _Broken()
+ class _GonnaRaise(object):
+ def savepoint(self):
+ return _broken
+ txn = _TXN()
+ resource = _GonnaRaise()
+ sp = self._makeOne(txn, False, resource)
+ self.assertRaises(ValueError, sp.rollback)
+ self.assertTrue(txn._raia is sp)
+ self.assertTrue(txn._sarce)
+
+
+class AbortSavepointTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import AbortSavepoint
+ return AbortSavepoint
+
+ def _makeOne(self, datamanager, transaction):
+ return self._getTargetClass()(datamanager, transaction)
+
+ def test_ctor(self):
+ dm = object()
+ txn = object()
+ asp = self._makeOne(dm, txn)
+ self.assertTrue(asp.datamanager is dm)
+ self.assertTrue(asp.transaction is txn)
+
+ def test_rollback(self):
+ class _DM(object):
+ _aborted = None
+ def abort(self, txn):
+ self._aborted = txn
+ class _TXN(object):
+ _unjoined = None
+ def _unjoin(self, datamanager):
+ self._unjoin = datamanager
+ dm = _DM()
+ txn = _TXN()
+ asp = self._makeOne(dm, txn)
+ asp.rollback()
+ self.assertTrue(dm._aborted is txn)
+ self.assertTrue(txn._unjoin is dm)
+
+
+class NoRollbackSavepointTests(unittest.TestCase):
+
+ def _getTargetClass(self):
+ from transaction._transaction import NoRollbackSavepoint
+ return NoRollbackSavepoint
+
+ def _makeOne(self, datamanager):
+ return self._getTargetClass()(datamanager)
+
+ def test_ctor(self):
+ dm = object()
+ nrsp = self._makeOne(dm)
+ self.assertTrue(nrsp.datamanager is dm)
+
+ def test_rollback(self):
+ dm = object()
+ nrsp = self._makeOne(dm)
+ self.assertRaises(TypeError, nrsp.rollback)
+
+
+class MiscellaneousTests(unittest.TestCase):
+
+ def test_BBB_join(self):
+ # The join method is provided for "backward-compatability" with ZODB 4
+ # data managers.
+ from transaction import Transaction
+ from transaction.tests.examples import DataManager
+ from transaction._transaction import DataManagerAdapter
+ # The argument to join must be a zodb4 data manager,
+ # transaction.interfaces.IDataManager.
+ txn = Transaction()
+ dm = DataManager()
+ txn.join(dm)
+ # The end result is that a data manager adapter is one of the
+ # transaction's objects:
+ self.assertTrue(isinstance(txn._resources[0], DataManagerAdapter))
+ self.assertTrue(txn._resources[0]._datamanager is dm)
+
+ def test_bug239086(self):
+ # The original implementation of thread transaction manager made
+ # invalid assumptions about thread ids.
+ import threading
+ import transaction
+ import transaction.tests.savepointsample as SPS
+ dm = SPS.SampleSavepointDataManager()
+ self.assertEqual(list(dm.keys()), [])
+
+ class Sync:
+ def __init__(self, label):
+ self.label = label
+ self.log = []
+ def beforeCompletion(self, txn):
+ self.log.append('%s %s' % (self.label, 'before'))
+ def afterCompletion(self, txn):
+ self.log.append('%s %s' % (self.label, 'after'))
+ def newTransaction(self, txn):
+ self.log.append('%s %s' % (self.label, 'new'))
+
+ def run_in_thread(f):
+ txn = threading.Thread(target=f)
+ txn.start()
+ txn.join()
+
+ sync = Sync(1)
+ @run_in_thread
+ def first():
+ transaction.manager.registerSynch(sync)
+ transaction.manager.begin()
+ dm['a'] = 1
+ self.assertEqual(sync.log, ['1 new'])
+
+ @run_in_thread
+ def second():
+ transaction.abort() # should do nothing.
+ self.assertEqual(sync.log, ['1 new'])
+ self.assertEqual(list(dm.keys()), ['a'])
+
+ dm = SPS.SampleSavepointDataManager()
+ self.assertEqual(list(dm.keys()), [])
+
+ @run_in_thread
+ def third():
+ dm['a'] = 1
+ self.assertEqual(sync.log, ['1 new'])
+
+ transaction.abort() # should do nothing
+ self.assertEqual(list(dm.keys()), ['a'])
+
+class Resource(object):
+ _b = _c = _v = _f = _a = _x = False
+ def __init__(self, key, error=None):
+ self._key = key
+ self._error = error
+ def __repr__(self):
+ return 'Resource: %s' % self._key
+ def sortKey(self):
+ return self._key
+ def tpc_begin(self, txn):
+ if self._error == 'tpc_begin':
+ raise ValueError()
+ self._b = True
+ def commit(self, txn):
+ if self._error == 'commit':
+ raise ValueError()
+ self._c = True
+ def tpc_vote(self, txn):
+ if self._error == 'tpc_vote':
+ raise ValueError()
+ self._v = True
+ def tpc_finish(self, txn):
+ if self._error == 'tpc_finish':
+ raise ValueError()
+ self._f = True
+ def abort(self, txn):
+ if self._error == 'abort':
+ raise ValueError()
+ self._a = True
+ def tpc_abort(self, txn):
+ if self._error == 'tpc_abort':
+ raise ValueError()
+ self._x = True
+
+def test_suite():
+ return unittest.TestSuite((
+ unittest.makeSuite(TransactionTests),
+ unittest.makeSuite(MultiObjectResourceAdapterTests),
+ unittest.makeSuite(Test_rm_key),
+ unittest.makeSuite(Test_object_hint),
+ unittest.makeSuite(Test_oid_repr),
+ unittest.makeSuite(DataManagerAdapterTests),
+ unittest.makeSuite(SavepointTests),
+ unittest.makeSuite(AbortSavepointTests),
+ unittest.makeSuite(NoRollbackSavepointTests),
+ unittest.makeSuite(MiscellaneousTests),
+ ))
Deleted: transaction/trunk/transaction/tests/test_attempt.py
===================================================================
--- transaction/trunk/transaction/tests/test_attempt.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_attempt.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,85 +0,0 @@
-import unittest
-
-class TestAttempt(unittest.TestCase):
- def _makeOne(self, manager):
- from transaction._manager import Attempt
- return Attempt(manager)
-
- def test___enter__(self):
- manager = DummyManager()
- inst = self._makeOne(manager)
- inst.__enter__()
- self.assertTrue(manager.entered)
-
- def test___exit__no_exc_no_commit_exception(self):
- manager = DummyManager()
- inst = self._makeOne(manager)
- result = inst.__exit__(None, None, None)
- self.assertFalse(result)
- self.assertTrue(manager.committed)
-
- def test___exit__no_exc_nonretryable_commit_exception(self):
- manager = DummyManager(raise_on_commit=ValueError)
- inst = self._makeOne(manager)
- self.assertRaises(ValueError, inst.__exit__, None, None, None)
- self.assertTrue(manager.committed)
- self.assertTrue(manager.aborted)
-
- def test___exit__no_exc_abort_exception_after_nonretryable_commit_exc(self):
- manager = DummyManager(raise_on_abort=ValueError,
- raise_on_commit=KeyError)
- inst = self._makeOne(manager)
- self.assertRaises(ValueError, inst.__exit__, None, None, None)
- self.assertTrue(manager.committed)
- self.assertTrue(manager.aborted)
-
- def test___exit__no_exc_retryable_commit_exception(self):
- from transaction.interfaces import TransientError
- manager = DummyManager(raise_on_commit=TransientError)
- inst = self._makeOne(manager)
- result = inst.__exit__(None, None, None)
- self.assertTrue(result)
- self.assertTrue(manager.committed)
- self.assertTrue(manager.aborted)
-
- def test___exit__with_exception_value_retryable(self):
- from transaction.interfaces import TransientError
- manager = DummyManager()
- inst = self._makeOne(manager)
- result = inst.__exit__(TransientError, TransientError(), None)
- self.assertTrue(result)
- self.assertFalse(manager.committed)
- self.assertTrue(manager.aborted)
-
- def test___exit__with_exception_value_nonretryable(self):
- manager = DummyManager()
- inst = self._makeOne(manager)
- self.assertRaises(KeyError, inst.__exit__, KeyError, KeyError(), None)
- self.assertFalse(manager.committed)
- self.assertTrue(manager.aborted)
-
-class DummyManager(object):
- entered = False
- committed = False
- aborted = False
-
- def __init__(self, raise_on_commit=None, raise_on_abort=None):
- self.raise_on_commit = raise_on_commit
- self.raise_on_abort = raise_on_abort
-
- def _retryable(self, t, v):
- from transaction._manager import TransientError
- return issubclass(t, TransientError)
-
- def __enter__(self):
- self.entered = True
-
- def abort(self):
- self.aborted = True
- if self.raise_on_abort:
- raise self.raise_on_abort
-
- def commit(self):
- self.committed = True
- if self.raise_on_commit:
- raise self.raise_on_commit
Modified: transaction/trunk/transaction/tests/test_register_compat.py
===================================================================
--- transaction/trunk/transaction/tests/test_register_compat.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_register_compat.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -23,77 +23,64 @@
These tests use a TestConnection object that implements the old API.
They check that the right methods are called and in roughly the right
order.
+"""
+import unittest
-Common cases
-------------
-First, check that a basic transaction commit works.
+class BBBTests(unittest.TestCase):
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(Object())
->>> cn.register(Object())
->>> transaction.commit()
->>> len(cn.committed)
-3
->>> len(cn.aborted)
-0
->>> cn.calls
-['begin', 'vote', 'finish']
+ def setUp(self):
+ from transaction import abort
+ abort()
+ tearDown = setUp
-Second, check that a basic transaction abort works. If the
-application calls abort(), then the transaction never gets into the
-two-phase commit. It just aborts each object.
+ def test_basic_commit(self):
+ import transaction
+ cn = TestConnection()
+ cn.register(Object())
+ cn.register(Object())
+ cn.register(Object())
+ transaction.commit()
+ self.assertEqual(len(cn.committed), 3)
+ self.assertEqual(len(cn.aborted), 0)
+ self.assertEqual(cn.calls, ['begin', 'vote', 'finish'])
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(Object())
->>> cn.register(Object())
->>> transaction.abort()
->>> len(cn.committed)
-0
->>> len(cn.aborted)
-3
->>> cn.calls
-[]
+ def test_basic_abort(self):
+ # If the application calls abort(), then the transaction never gets
+ # into the two-phase commit. It just aborts each object.
+ import transaction
+ cn = TestConnection()
+ cn.register(Object())
+ cn.register(Object())
+ cn.register(Object())
+ transaction.abort()
+ self.assertEqual(len(cn.committed), 0)
+ self.assertEqual(len(cn.aborted), 3)
+ self.assertEqual(cn.calls, [])
-Error handling
---------------
+ def test_tpc_error(self):
+ # The tricky part of the implementation is recovering from an error
+ # that occurs during the two-phase commit. We override the commit()
+ # and abort() methods of Object to cause errors during commit.
-The tricky part of the implementation is recovering from an error that
-occurs during the two-phase commit. We override the commit() and
-abort() methods of Object to cause errors during commit.
+ # Note that the implementation uses lists internally, so that objects
+ # are committed in the order they are registered. (In the presence
+ # of multiple resource managers, objects from a single resource
+ # manager are committed in order. I'm not sure if this is an
+ # accident of the implementation or a feature that should be
+ # supported by any implementation.)
-Note that the implementation uses lists internally, so that objects
-are committed in the order they are registered. (In the presence of
-multiple resource managers, objects from a single resource manager are
-committed in order. I'm not sure if this is an accident of the
-implementation or a feature that should be supported by any
-implementation.)
+ # The order of resource managers depends on sortKey().
+ import transaction
+ cn = TestConnection()
+ cn.register(Object())
+ cn.register(CommitError())
+ cn.register(Object())
+ self.assertRaises(RuntimeError, transaction.commit)
+ self.assertEqual(len(cn.committed), 1)
+ self.assertEqual(len(cn.aborted), 3)
-The order of resource managers depends on sortKey().
->>> cn = TestConnection()
->>> cn.register(Object())
->>> cn.register(CommitError())
->>> cn.register(Object())
->>> transaction.commit()
-Traceback (most recent call last):
- ...
-RuntimeError: commit
->>> len(cn.committed)
-1
->>> len(cn.aborted)
-3
-
-Clean up:
-
->>> transaction.abort()
-"""
-
-import doctest
-import transaction
-
class Object(object):
def commit(self):
@@ -102,27 +89,32 @@
def abort(self):
pass
+
class CommitError(Object):
def commit(self):
raise RuntimeError("commit")
+
class AbortError(Object):
def abort(self):
raise RuntimeError("abort")
+
class BothError(CommitError, AbortError):
pass
-class TestConnection:
+class TestConnection(object):
+
def __init__(self):
self.committed = []
self.aborted = []
self.calls = []
def register(self, obj):
+ import transaction
obj._p_jar = self
transaction.get().register(obj)
@@ -150,7 +142,6 @@
self.aborted.append(obj)
def test_suite():
- return doctest.DocTestSuite()
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
+ return unittest.TestSuite((
+ unittest.makeSuite(BBBTests),
+ ))
Modified: transaction/trunk/transaction/tests/test_savepoint.py
===================================================================
--- transaction/trunk/transaction/tests/test_savepoint.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_savepoint.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,82 +11,56 @@
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
-"""Tests of savepoint feature
-"""
import unittest
-import doctest
-def testRollbackRollsbackDataManagersThatJoinedLater():
- """
+class SavepointTests(unittest.TestCase):
-A savepoint needs to not just rollback it's savepoints, but needs to
-rollback savepoints for data managers that joined savepoints after the
-savepoint:
+ def testRollbackRollsbackDataManagersThatJoinedLater(self):
+ # A savepoint needs to not just rollback it's savepoints, but needs
+ # to # rollback savepoints for data managers that joined savepoints
+ # after the savepoint:
+ import transaction
+ from transaction.tests import savepointsample
+ dm = savepointsample.SampleSavepointDataManager()
+ dm['name'] = 'bob'
+ sp1 = transaction.savepoint()
+ dm['job'] = 'geek'
+ sp2 = transaction.savepoint()
+ dm['salary'] = 'fun'
+ dm2 = savepointsample.SampleSavepointDataManager()
+ dm2['name'] = 'sally'
- >>> import transaction
- >>> from transaction.tests import savepointsample
- >>> dm = savepointsample.SampleSavepointDataManager()
- >>> dm['name'] = 'bob'
- >>> sp1 = transaction.savepoint()
- >>> dm['job'] = 'geek'
- >>> sp2 = transaction.savepoint()
- >>> dm['salary'] = 'fun'
- >>> dm2 = savepointsample.SampleSavepointDataManager()
- >>> dm2['name'] = 'sally'
+ self.assertTrue('name' in dm)
+ self.assertTrue('job' in dm)
+ self.assertTrue('salary' in dm)
+ self.assertTrue('name' in dm2)
- >>> 'name' in dm
- True
- >>> 'job' in dm
- True
- >>> 'salary' in dm
- True
- >>> 'name' in dm2
- True
+ sp1.rollback()
- >>> sp1.rollback()
+ self.assertTrue('name' in dm)
+ self.assertFalse('job' in dm)
+ self.assertFalse('salary' in dm)
+ self.assertFalse('name' in dm2)
- >>> 'name' in dm
- True
- >>> 'job' in dm
- False
- >>> 'salary' in dm
- False
- >>> 'name' in dm2
- False
+ def test_commit_after_rollback_for_dm_that_joins_after_savepoint(self):
+ # There was a problem handling data managers that joined after a
+ # savepoint. If the savepoint was rolled back and then changes
+ # made, the dm would end up being joined twice, leading to extra
+ # tpc calls and pain.
+ import transaction
+ from transaction.tests import savepointsample
+ sp = transaction.savepoint()
+ dm = savepointsample.SampleSavepointDataManager()
+ dm['name'] = 'bob'
+ sp.rollback()
+ dm['name'] = 'Bob'
+ transaction.commit()
+ self.assertEqual(dm['name'], 'Bob')
-"""
-def test_commit_after_rollback_for_dm_that_joins_after_savepoint():
- """
-There was a problem handling data managers that joined after a
-savepoint. If the savepoint was rolled back and then changes made,
-the dm would end up being joined twice, leading to extra tpc calls and pain.
-
- >>> import transaction
- >>> sp = transaction.savepoint()
- >>> from transaction.tests import savepointsample
- >>> dm = savepointsample.SampleSavepointDataManager()
- >>> dm['name'] = 'bob'
- >>> sp.rollback()
- >>> dm['name'] = 'Bob'
- >>> transaction.commit()
- >>> dm['name']
- 'Bob'
- """
-
-
-
def test_suite():
return unittest.TestSuite((
- doctest.DocFileSuite('savepoint.txt'),
- doctest.DocTestSuite(),
+ unittest.makeSuite(SavepointTests),
))
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
- unittest.main(defaultTest='test_suite')
-
Deleted: transaction/trunk/transaction/tests/test_transaction.py
===================================================================
--- transaction/trunk/transaction/tests/test_transaction.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_transaction.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -1,781 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2001, 2002, 2005 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-#
-##############################################################################
-"""Test transaction behavior for variety of cases.
-
-I wrote these unittests to investigate some odd transaction
-behavior when doing unittests of integrating non sub transaction
-aware objects, and to insure proper txn behavior. these
-tests test the transaction system independent of the rest of the
-zodb.
-
-you can see the method calls to a jar by passing the
-keyword arg tracing to the modify method of a dataobject.
-the value of the arg is a prefix used for tracing print calls
-to that objects jar.
-
-the number of times a jar method was called can be inspected
-by looking at an attribute of the jar that is the method
-name prefixed with a c (count/check).
-
-i've included some tracing examples for tests that i thought
-were illuminating as doc strings below.
-
-TODO
-
- add in tests for objects which are modified multiple times,
- for example an object that gets modified in multiple sub txns.
-"""
-from doctest import DocTestSuite, DocFileSuite, IGNORE_EXCEPTION_DETAIL
-
-import struct
-import sys
-import unittest
-import transaction
-
-_ADDRESS_MASK = 256 ** struct.calcsize('P')
-def positive_id(obj):
- """Return id(obj) as a non-negative integer."""
-
- result = id(obj)
- if result < 0:
- result += _ADDRESS_MASK
- assert result > 0
- return result
-
-class TransactionTests(unittest.TestCase):
-
- def setUp(self):
- mgr = self.transaction_manager = transaction.TransactionManager()
- self.sub1 = DataObject(mgr)
- self.sub2 = DataObject(mgr)
- self.sub3 = DataObject(mgr)
- self.nosub1 = DataObject(mgr, nost=1)
-
- # basic tests with two sub trans jars
- # really we only need one, so tests for
- # sub1 should identical to tests for sub2
- def testTransactionCommit(self):
-
- self.sub1.modify()
- self.sub2.modify()
-
- self.transaction_manager.commit()
-
- assert self.sub1._p_jar.ccommit_sub == 0
- assert self.sub1._p_jar.ctpc_finish == 1
-
- def testTransactionAbort(self):
-
- self.sub1.modify()
- self.sub2.modify()
-
- self.transaction_manager.abort()
-
- assert self.sub2._p_jar.cabort == 1
-
- def testTransactionNote(self):
-
- t = self.transaction_manager.get()
-
- t.note('This is a note.')
- self.assertEqual(t.description, 'This is a note.')
- t.note('Another.')
- self.assertEqual(t.description, 'This is a note.\nAnother.')
-
- t.abort()
-
-
- # repeat adding in a nonsub trans jars
-
- def testNSJTransactionCommit(self):
-
- self.nosub1.modify()
-
- self.transaction_manager.commit()
-
- assert self.nosub1._p_jar.ctpc_finish == 1
-
- def testNSJTransactionAbort(self):
-
- self.nosub1.modify()
-
- self.transaction_manager.abort()
-
- assert self.nosub1._p_jar.ctpc_finish == 0
- assert self.nosub1._p_jar.cabort == 1
-
-
- ### Failure Mode Tests
- #
- # ok now we do some more interesting
- # tests that check the implementations
- # error handling by throwing errors from
- # various jar methods
- ###
-
- # first the recoverable errors
-
- def testExceptionInAbort(self):
-
- self.sub1._p_jar = BasicJar(errors='abort')
-
- self.nosub1.modify()
- self.sub1.modify(nojar=1)
- self.sub2.modify()
-
- try:
- self.transaction_manager.abort()
- except TestTxnException: pass
-
- assert self.nosub1._p_jar.cabort == 1
- assert self.sub2._p_jar.cabort == 1
-
- def testExceptionInCommit(self):
-
- self.sub1._p_jar = BasicJar(errors='commit')
-
- self.nosub1.modify()
- self.sub1.modify(nojar=1)
-
- try:
- self.transaction_manager.commit()
- except TestTxnException: pass
-
- assert self.nosub1._p_jar.ctpc_finish == 0
- assert self.nosub1._p_jar.ccommit == 1
- assert self.nosub1._p_jar.ctpc_abort == 1
-
- def testExceptionInTpcVote(self):
-
- self.sub1._p_jar = BasicJar(errors='tpc_vote')
-
- self.nosub1.modify()
- self.sub1.modify(nojar=1)
-
- try:
- self.transaction_manager.commit()
- except TestTxnException: pass
-
- assert self.nosub1._p_jar.ctpc_finish == 0
- assert self.nosub1._p_jar.ccommit == 1
- assert self.nosub1._p_jar.ctpc_abort == 1
- assert self.sub1._p_jar.ctpc_abort == 1
-
- def testExceptionInTpcBegin(self):
- """
- ok this test reveals a bug in the TM.py
- as the nosub tpc_abort there is ignored.
-
- nosub calling method tpc_begin
- nosub calling method commit
- sub calling method tpc_begin
- sub calling method abort
- sub calling method tpc_abort
- nosub calling method tpc_abort
- """
- self.sub1._p_jar = BasicJar(errors='tpc_begin')
-
- self.nosub1.modify()
- self.sub1.modify(nojar=1)
-
- try:
- self.transaction_manager.commit()
- except TestTxnException:
- pass
-
- assert self.nosub1._p_jar.ctpc_abort == 1
- assert self.sub1._p_jar.ctpc_abort == 1
-
- def testExceptionInTpcAbort(self):
- self.sub1._p_jar = BasicJar(errors=('tpc_abort', 'tpc_vote'))
-
- self.nosub1.modify()
- self.sub1.modify(nojar=1)
-
- try:
- self.transaction_manager.commit()
- except TestTxnException:
- pass
-
- assert self.nosub1._p_jar.ctpc_abort == 1
-
- # last test, check the hosing mechanism
-
-## def testHoserStoppage(self):
-## # It's hard to test the "hosed" state of the database, where
-## # hosed means that a failure occurred in the second phase of
-## # the two phase commit. It's hard because the database can
-## # recover from such an error if it occurs during the very first
-## # tpc_finish() call of the second phase.
-
-## for obj in self.sub1, self.sub2:
-## j = HoserJar(errors='tpc_finish')
-## j.reset()
-## obj._p_jar = j
-## obj.modify(nojar=1)
-
-## try:
-## transaction.commit()
-## except TestTxnException:
-## pass
-
-## self.assert_(Transaction.hosed)
-
-## self.sub2.modify()
-
-## try:
-## transaction.commit()
-## except Transaction.POSException.TransactionError:
-## pass
-## else:
-## self.fail("Hosed Application didn't stop commits")
-
-
-class Test_oid_repr(unittest.TestCase):
- def _callFUT(self, oid):
- from transaction._transaction import oid_repr
- return oid_repr(oid)
-
- def test_as_nonstring(self):
- self.assertEqual(self._callFUT(123), '123')
-
- def test_as_string_not_8_chars(self):
- self.assertEqual(self._callFUT('a'), "'a'")
-
- def test_as_string_z64(self):
- s = '\0'*8
- self.assertEqual(self._callFUT(s), '0x00')
-
- def test_as_string_all_Fs(self):
- s = '\1'*8
- self.assertEqual(self._callFUT(s), '0x0101010101010101')
-
-class DataObject:
-
- def __init__(self, transaction_manager, nost=0):
- self.transaction_manager = transaction_manager
- self.nost = nost
- self._p_jar = None
-
- def modify(self, nojar=0, tracing=0):
- if not nojar:
- if self.nost:
- self._p_jar = BasicJar(tracing=tracing)
- else:
- self._p_jar = BasicJar(tracing=tracing)
- self.transaction_manager.get().join(self._p_jar)
-
-class TestTxnException(Exception):
- pass
-
-class BasicJar:
-
- def __init__(self, errors=(), tracing=0):
- if not isinstance(errors, tuple):
- errors = errors,
- self.errors = errors
- self.tracing = tracing
- self.cabort = 0
- self.ccommit = 0
- self.ctpc_begin = 0
- self.ctpc_abort = 0
- self.ctpc_vote = 0
- self.ctpc_finish = 0
- self.cabort_sub = 0
- self.ccommit_sub = 0
-
- def __repr__(self):
- return "<%s %X %s>" % (self.__class__.__name__,
- positive_id(self),
- self.errors)
-
- def sortKey(self):
- # All these jars use the same sort key, and Python's list.sort()
- # is stable. These two
- return self.__class__.__name__
-
- def check(self, method):
- if self.tracing:
- print('%s calling method %s'%(str(self.tracing),method))
-
- if method in self.errors:
- raise TestTxnException("error %s" % method)
-
- ## basic jar txn interface
-
- def abort(self, *args):
- self.check('abort')
- self.cabort += 1
-
- def commit(self, *args):
- self.check('commit')
- self.ccommit += 1
-
- def tpc_begin(self, txn, sub=0):
- self.check('tpc_begin')
- self.ctpc_begin += 1
-
- def tpc_vote(self, *args):
- self.check('tpc_vote')
- self.ctpc_vote += 1
-
- def tpc_abort(self, *args):
- self.check('tpc_abort')
- self.ctpc_abort += 1
-
- def tpc_finish(self, *args):
- self.check('tpc_finish')
- self.ctpc_finish += 1
-
-class HoserJar(BasicJar):
-
- # The HoserJars coordinate their actions via the class variable
- # committed. The check() method will only raise its exception
- # if committed > 0.
-
- committed = 0
-
- def reset(self):
- # Calling reset() on any instance will reset the class variable.
- HoserJar.committed = 0
-
- def check(self, method):
- if HoserJar.committed > 0:
- BasicJar.check(self, method)
-
- def tpc_finish(self, *args):
- self.check('tpc_finish')
- self.ctpc_finish += 1
- HoserJar.committed += 1
-
-
-def test_join():
- """White-box test of the join method
-
- The join method is provided for "backward-compatability" with ZODB 4
- data managers.
-
- The argument to join must be a zodb4 data manager,
- transaction.interfaces.IDataManager.
-
- >>> from transaction.tests.sampledm import DataManager
- >>> from transaction._transaction import DataManagerAdapter
- >>> t = transaction.Transaction()
- >>> dm = DataManager()
- >>> t.join(dm)
-
- The end result is that a data manager adapter is one of the
- transaction's objects:
-
- >>> isinstance(t._resources[0], DataManagerAdapter)
- True
- >>> t._resources[0]._datamanager is dm
- True
-
- """
-
-def hook():
- pass
-
-def test_addBeforeCommitHook():
- """Test addBeforeCommitHook.
-
- Let's define a hook to call, and a way to see that it was called.
-
- >>> log = []
- >>> def reset_log():
- ... del log[:]
-
- >>> def hook(arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
- ... log.append("arg %r kw1 %r kw2 %r" % (arg, kw1, kw2))
-
- Now register the hook with a transaction.
-
- >>> from transaction.compat import func_name
- >>> import transaction
- >>> t = transaction.begin()
- >>> t.addBeforeCommitHook(hook, '1')
-
- We can see that the hook is indeed registered.
-
- >>> [(func_name(hook), args, kws)
- ... for hook, args, kws in t.getBeforeCommitHooks()]
- [('hook', ('1',), {})]
-
- When transaction commit starts, the hook is called, with its
- arguments.
-
- >>> log
- []
- >>> t.commit()
- >>> log
- ["arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
- >>> reset_log()
-
- A hook's registration is consumed whenever the hook is called. Since
- the hook above was called, it's no longer registered:
-
- >>> len(list(t.getBeforeCommitHooks()))
- 0
- >>> transaction.commit()
- >>> log
- []
-
- The hook is only called for a full commit, not for a savepoint.
-
- >>> t = transaction.begin()
- >>> t.addBeforeCommitHook(hook, 'A', dict(kw1='B'))
- >>> dummy = t.savepoint()
- >>> log
- []
- >>> t.commit()
- >>> log
- ["arg 'A' kw1 'B' kw2 'no_kw2'"]
- >>> reset_log()
-
- If a transaction is aborted, no hook is called.
-
- >>> t = transaction.begin()
- >>> t.addBeforeCommitHook(hook, ["OOPS!"])
- >>> transaction.abort()
- >>> log
- []
- >>> transaction.commit()
- >>> log
- []
-
- The hook is called before the commit does anything, so even if the
- commit fails the hook will have been called. To provoke failures in
- commit, we'll add failing resource manager to the transaction.
-
- >>> class CommitFailure(Exception):
- ... pass
- >>> class FailingDataManager:
- ... def tpc_begin(self, txn, sub=False):
- ... raise CommitFailure('failed')
- ... def abort(self, txn):
- ... pass
-
- >>> t = transaction.begin()
- >>> t.join(FailingDataManager())
-
- >>> t.addBeforeCommitHook(hook, '2')
- >>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- CommitFailure: failed
- >>> log
- ["arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
- >>> reset_log()
-
- Let's register several hooks.
-
- >>> t = transaction.begin()
- >>> t.addBeforeCommitHook(hook, '4', dict(kw1='4.1'))
- >>> t.addBeforeCommitHook(hook, '5', dict(kw2='5.2'))
-
- They are returned in the same order by getBeforeCommitHooks.
-
- >>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
- ... for hook, args, kws in t.getBeforeCommitHooks()]
- [('hook', ('4',), {'kw1': '4.1'}),
- ('hook', ('5',), {'kw2': '5.2'})]
-
- And commit also calls them in this order.
-
- >>> t.commit()
- >>> len(log)
- 2
- >>> log #doctest: +NORMALIZE_WHITESPACE
- ["arg '4' kw1 '4.1' kw2 'no_kw2'",
- "arg '5' kw1 'no_kw1' kw2 '5.2'"]
- >>> reset_log()
-
- While executing, a hook can itself add more hooks, and they will all
- be called before the real commit starts.
-
- >>> def recurse(txn, arg):
- ... log.append('rec' + str(arg))
- ... if arg:
- ... txn.addBeforeCommitHook(hook, '-')
- ... txn.addBeforeCommitHook(recurse, (txn, arg-1))
-
- >>> t = transaction.begin()
- >>> t.addBeforeCommitHook(recurse, (t, 3))
- >>> transaction.commit()
- >>> log #doctest: +NORMALIZE_WHITESPACE
- ['rec3',
- "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec2',
- "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec1',
- "arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec0']
- >>> reset_log()
-
- """
-
-def test_addAfterCommitHook():
- """Test addAfterCommitHook.
-
- Let's define a hook to call, and a way to see that it was called.
-
- >>> log = []
- >>> def reset_log():
- ... del log[:]
-
- >>> def hook(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
- ... log.append("%r arg %r kw1 %r kw2 %r" % (status, arg, kw1, kw2))
-
- Now register the hook with a transaction.
-
- >>> from transaction.compat import func_name
- >>> import transaction
- >>> t = transaction.begin()
- >>> t.addAfterCommitHook(hook, '1')
-
- We can see that the hook is indeed registered.
-
- >>> [(func_name(hook), args, kws)
- ... for hook, args, kws in t.getAfterCommitHooks()]
- [('hook', ('1',), {})]
-
- When transaction commit is done, the hook is called, with its
- arguments.
-
- >>> log
- []
- >>> t.commit()
- >>> log
- ["True arg '1' kw1 'no_kw1' kw2 'no_kw2'"]
- >>> reset_log()
-
- A hook's registration is consumed whenever the hook is called. Since
- the hook above was called, it's no longer registered:
-
- >>> len(list(t.getAfterCommitHooks()))
- 0
- >>> transaction.commit()
- >>> log
- []
-
- The hook is only called after a full commit, not for a savepoint.
-
- >>> t = transaction.begin()
- >>> t.addAfterCommitHook(hook, 'A', dict(kw1='B'))
- >>> dummy = t.savepoint()
- >>> log
- []
- >>> t.commit()
- >>> log
- ["True arg 'A' kw1 'B' kw2 'no_kw2'"]
- >>> reset_log()
-
- If a transaction is aborted, no hook is called.
-
- >>> t = transaction.begin()
- >>> t.addAfterCommitHook(hook, ["OOPS!"])
- >>> transaction.abort()
- >>> log
- []
- >>> transaction.commit()
- >>> log
- []
-
- The hook is called after the commit is done, so even if the
- commit fails the hook will have been called. To provoke failures in
- commit, we'll add failing resource manager to the transaction.
-
- >>> class CommitFailure(Exception):
- ... pass
- >>> class FailingDataManager:
- ... def tpc_begin(self, txn):
- ... raise CommitFailure('failed')
- ... def abort(self, txn):
- ... pass
-
- >>> t = transaction.begin()
- >>> t.join(FailingDataManager())
-
- >>> t.addAfterCommitHook(hook, '2')
- >>> t.commit() #doctest: +IGNORE_EXCEPTION_DETAIL
- Traceback (most recent call last):
- ...
- CommitFailure: failed
- >>> log
- ["False arg '2' kw1 'no_kw1' kw2 'no_kw2'"]
- >>> reset_log()
-
- Let's register several hooks.
-
- >>> t = transaction.begin()
- >>> t.addAfterCommitHook(hook, '4', dict(kw1='4.1'))
- >>> t.addAfterCommitHook(hook, '5', dict(kw2='5.2'))
-
- They are returned in the same order by getAfterCommitHooks.
-
- >>> [(func_name(hook), args, kws) #doctest: +NORMALIZE_WHITESPACE
- ... for hook, args, kws in t.getAfterCommitHooks()]
- [('hook', ('4',), {'kw1': '4.1'}),
- ('hook', ('5',), {'kw2': '5.2'})]
-
- And commit also calls them in this order.
-
- >>> t.commit()
- >>> len(log)
- 2
- >>> log #doctest: +NORMALIZE_WHITESPACE
- ["True arg '4' kw1 '4.1' kw2 'no_kw2'",
- "True arg '5' kw1 'no_kw1' kw2 '5.2'"]
- >>> reset_log()
-
- While executing, a hook can itself add more hooks, and they will all
- be called before the real commit starts.
-
- >>> def recurse(status, txn, arg):
- ... log.append('rec' + str(arg))
- ... if arg:
- ... txn.addAfterCommitHook(hook, '-')
- ... txn.addAfterCommitHook(recurse, (txn, arg-1))
-
- >>> t = transaction.begin()
- >>> t.addAfterCommitHook(recurse, (t, 3))
- >>> transaction.commit()
- >>> log #doctest: +NORMALIZE_WHITESPACE
- ['rec3',
- "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec2',
- "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec1',
- "True arg '-' kw1 'no_kw1' kw2 'no_kw2'",
- 'rec0']
- >>> reset_log()
-
- If an after commit hook is raising an exception then it will log a
- message at error level so that if other hooks are registered they
- can be executed. We don't support execution dependencies at this level.
-
- >>> mgr = transaction.TransactionManager()
- >>> do = DataObject(mgr)
-
- >>> def hookRaise(status, arg='no_arg', kw1='no_kw1', kw2='no_kw2'):
- ... raise TypeError("Fake raise")
-
- >>> t = transaction.begin()
-
- >>> t.addAfterCommitHook(hook, ('-', 1))
- >>> t.addAfterCommitHook(hookRaise, ('-', 2))
- >>> t.addAfterCommitHook(hook, ('-', 3))
- >>> transaction.commit()
-
- >>> log
- ["True arg '-' kw1 1 kw2 'no_kw2'", "True arg '-' kw1 3 kw2 'no_kw2'"]
-
- >>> reset_log()
-
- Test that the associated transaction manager has been cleanup when
- after commit hooks are registered
-
- >>> mgr = transaction.TransactionManager()
- >>> do = DataObject(mgr)
-
- >>> t = transaction.begin()
- >>> t._manager._txn is not None
- True
-
- >>> t.addAfterCommitHook(hook, ('-', 1))
- >>> transaction.commit()
-
- >>> log
- ["True arg '-' kw1 1 kw2 'no_kw2'"]
-
- >>> t._manager._txn is not None
- False
-
- >>> reset_log()
- """
-
-def bug239086():
- """
- The original implementation of thread transaction manager made
- invalid assumptions about thread ids.
-
- >>> import transaction.tests.savepointsample
- >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
- >>> list(dm.keys())
- []
-
- >>> class Sync:
- ... def __init__(self, label):
- ... self.label = label
- ... def beforeCompletion(self, t):
- ... print('%s %s' % (self.label, 'before'))
- ... def afterCompletion(self, t):
- ... print('%s %s' % (self.label, 'after'))
- ... def newTransaction(self, t):
- ... print('%s %s' % (self.label, 'new'))
- >>> sync = Sync(1)
-
- >>> import threading
- >>> def run_in_thread(f):
- ... t = threading.Thread(target=f)
- ... t.start()
- ... t.join()
-
- >>> @run_in_thread
- ... def first():
- ... transaction.manager.registerSynch(sync)
- ... transaction.manager.begin()
- ... dm['a'] = 1
- 1 new
-
- >>> @run_in_thread
- ... def second():
- ... transaction.abort() # should do nothing.
-
- >>> list(dm.keys())
- ['a']
-
- >>> dm = transaction.tests.savepointsample.SampleSavepointDataManager()
- >>> list(dm.keys())
- []
-
- >>> @run_in_thread
- ... def first():
- ... dm['a'] = 1
-
- >>> transaction.abort() # should do nothing
- >>> list(dm.keys())
- ['a']
-
- """
-
-def test_suite():
- suite = unittest.TestSuite((
- DocFileSuite('doom.txt'),
- DocTestSuite(),
- unittest.makeSuite(TransactionTests),
- unittest.makeSuite(Test_oid_repr),
- ))
- if sys.version_info >= (2, 6):
- suite.addTest(DocFileSuite('convenience.txt',
- optionflags=IGNORE_EXCEPTION_DETAIL))
-
- return suite
-
-# additional_tests is for setuptools "setup.py test" support
-additional_tests = test_suite
-
-if __name__ == '__main__':
- unittest.TextTestRunner().run(test_suite())
Modified: transaction/trunk/transaction/tests/test_weakset.py
===================================================================
--- transaction/trunk/transaction/tests/test_weakset.py 2012-12-18 05:25:11 UTC (rev 128756)
+++ transaction/trunk/transaction/tests/test_weakset.py 2012-12-18 05:26:55 UTC (rev 128757)
@@ -11,15 +11,12 @@
# FOR A PARTICULAR PURPOSE
#
##############################################################################
-
import unittest
-from transaction.weakset import WeakSet
-class Dummy:
- pass
class WeakSetTests(unittest.TestCase):
def test_contains(self):
+ from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
w.add(dummy)
@@ -29,6 +26,7 @@
def test_len(self):
import gc
+ from transaction.weakset import WeakSet
w = WeakSet()
d1 = Dummy()
d2 = Dummy()
@@ -40,6 +38,7 @@
self.assertEqual(len(w), 1)
def test_remove(self):
+ from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
w.add(dummy)
@@ -49,6 +48,7 @@
def test_as_weakref_list(self):
import gc
+ from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
dummy2 = Dummy()
@@ -64,6 +64,7 @@
self.assertEqual(set(L), set([dummy, dummy2]))
def test_map(self):
+ from transaction.weakset import WeakSet
w = WeakSet()
dummy = Dummy()
dummy2 = Dummy()
@@ -77,10 +78,10 @@
for thing in dummy, dummy2, dummy3:
self.assertEqual(thing.poked, 1)
+
+class Dummy:
+ pass
+
def test_suite():
return unittest.makeSuite(WeakSetTests)
-
-if __name__ == '__main__':
- unittest.main()
-
More information about the checkins
mailing list