Here are some tests that storage sync() methods get called at appropriate
times in the life of a transaction.  The tested behavior is new in ZODB 3.4.

First define a lightweight storage with a sync() method:

    >>> import ZODB
    >>> from ZODB.MappingStorage import MappingStorage
    >>> import transaction

    >>> class SimpleStorage(MappingStorage):
    ...     sync_called = False
    ...
    ...     def sync(self, *args):
    ...         self.sync_called = True

Make a change locally:

    >>> st = SimpleStorage()
    >>> db = ZODB.DB(st)
    >>> cn = db.open()
    >>> rt = cn.root()
    >>> rt['a'] = 1

Sync should not have been called yet.

    >>> st.sync_called  # False before 3.4
    False


sync is called by the Connection's afterCompletion() hook after the commit
completes.

    >>> transaction.commit()
    >>> st.sync_called  # False before 3.4
    True

sync is also called by the afterCompletion() hook after an abort.

    >>> st.sync_called = False
    >>> rt['b'] = 2
    >>> transaction.abort()
    >>> st.sync_called  # False before 3.4
    True

And sync is called whenever we explicitly start a new txn, via the
newTransaction() hook.

    >>> st.sync_called = False
    >>> dummy = transaction.begin()
    >>> st.sync_called  # False before 3.4
    True

Clean up.  Closing db isn't enough -- closing a DB doesn't close its
Connections.  Leaving our Connection open here can cause the
SimpleStorage.sync() method to get called later, during another test, and
our doctest-synthesized module globals no longer exist then.  You get
a weird traceback then ;-)

    >>> cn.close()

One more, very obscure.  It was the case that if the first action a new
threaded transaction manager saw was a begin() call, then synchronizers
registered after that in the same transaction weren't communicated to
the Transaction object, and so the synchronizers' afterCompletion() hooks
weren't called when the transaction commited.  None of the test suites
(ZODB's, Zope 2.8's, or Zope3's) caught that, but apparently Zope3 takes this
path at some point when serving pages.

    >>> tm = transaction.ThreadTransactionManager()
    >>> st.sync_called = False
    >>> dummy = tm.begin()  # we're doing this _before_ opening a connection
    >>> cn = db.open(transaction_manager=tm)
    >>> rt = cn.root()      # make a change
    >>> rt['c'] = 3
    >>> st.sync_called
    False

Now ensure that cn.afterCompletion() -> st.sync() gets called by commit
despite that the Connection registered after the transaction began:

    >>> tm.commit()
    >>> st.sync_called
    True

And try the same thing with a non-threaded TM:

    >>> cn.close()
    >>> tm = transaction.TransactionManager()
    >>> st.sync_called = False
    >>> dummy = tm.begin()  # we're doing this _before_ opening a connection
    >>> cn = db.open(transaction_manager=tm)
    >>> rt = cn.root()      # make a change
    >>> rt['d'] = 4
    >>> st.sync_called
    False
    >>> tm.commit()
    >>> st.sync_called
    True

    >>> cn.close()
    >>> db.close()

