[Neo-report] r2285 vincent - in /trunk/neo: ./ client/ client/handlers/ storage/database/ ...
nobody at svn.erp5.org
nobody at svn.erp5.org
Thu Sep 2 18:29:59 CEST 2010
Author: vincent
Date: Thu Sep 2 18:29:59 2010
New Revision: 2285
Log:
Make undo implementation work with replication.
The problem with previous implementation was that each storage locally
decided what undo actually did to data. This causes problems when a
storage doesn't have a complete view of past transaction but accepts write
queries, ie when it replicates.
This implementation reduces the decision to a readable subset of storage
nodes (which are hence not replicating), and then sends that decision to
all storage nodes, hence fixing the issue.
Also, DatabaseManager.storeTransaction now consistently expects object's
value_serial to be packed, not an integer.
Modified:
trunk/neo/client/app.py
trunk/neo/client/handlers/storage.py
trunk/neo/handler.py
trunk/neo/protocol.py
trunk/neo/storage/database/manager.py
trunk/neo/storage/database/mysqldb.py
trunk/neo/storage/handlers/client.py
trunk/neo/tests/client/testClientApp.py
trunk/neo/tests/client/testStorageHandler.py
trunk/neo/tests/storage/testClientHandler.py
trunk/neo/tests/storage/testStorageMySQLdb.py
trunk/neo/tests/testProtocol.py
Modified: trunk/neo/client/app.py
==============================================================================
--- trunk/neo/client/app.py [iso-8859-1] (original)
+++ trunk/neo/client/app.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -114,8 +114,7 @@ class ThreadContext(object):
'node_tids': {},
'node_ready': False,
'asked_object': 0,
- 'undo_conflict_oid_list': [],
- 'undo_error_oid_list': [],
+ 'undo_object_tid_dict': {},
'involved_nodes': set(),
}
@@ -905,55 +904,77 @@ class Application(object):
raise NEOStorageError('undo failed')
tid = self.local_var.tid
+ oid_list = self.local_var.txn_info['oids']
- undo_conflict_oid_list = self.local_var.undo_conflict_oid_list = []
- undo_error_oid_list = self.local_var.undo_error_oid_list = []
- ask_undo_transaction = Packets.AskUndoTransaction(tid, undone_tid)
- getConnForNode = self.cp.getConnForNode
+ # Regroup objects per partition, to ask a minimum set of storage.
+ partition_oid_dict = {}
+ pt = self._getPartitionTable()
+ getPartitionFromIndex = pt.getPartitionFromIndex
+ for oid in oid_list:
+ partition = pt.getPartitionFromIndex(oid)
+ try:
+ oid_list = partition_oid_dict[partition]
+ except KeyError:
+ oid_list = partition_oid_dict[partition] = []
+ oid_list.append(oid)
+
+ # Ask storage the undo serial (serial at which object's previous data
+ # is)
+ getCellList = pt.getCellList
+ getCellSortKey = self.cp.getCellSortKey
queue = self.local_var.queue
- for storage_node in self.nm.getStorageList():
- storage_conn = getConnForNode(storage_node)
- storage_conn.ask(ask_undo_transaction, queue=queue)
- # Wait for all AnswerUndoTransaction.
- self.waitResponses()
-
- # Don't do any handling for "live" conflicts, raise
- if undo_conflict_oid_list:
- raise ConflictError(oid=undo_conflict_oid_list[0], serials=(tid,
- undone_tid), data=None)
-
- # Try to resolve undo conflicts
- for oid in undo_error_oid_list:
- def loadBefore(oid, tid):
- try:
- result = self._load(oid, tid=tid)
- except NEOStorageNotFoundError:
- raise UndoError("Object not found while resolving undo " \
- "conflict")
- return result[:2]
- # Load the latest version we are supposed to see
- data, data_tid = loadBefore(oid, tid)
- # Load the version we were undoing to
- undo_data, _ = loadBefore(oid, undone_tid)
- # Resolve conflict
+ undo_object_tid_dict = self.local_var.undo_object_tid_dict = {}
+ for partition, oid_list in partition_oid_dict.iteritems():
+ cell_list = getCellList(partition, readable=True)
+ shuffle(cell_list)
+ cell_list.sort(key=getCellSortKey)
+ storage_conn = getConnForCell(cell_list[0])
+ storage_conn.ask(Packets.AskObjectUndoSerial(tid, undone_tid,
+ oid_list), queue=queue)
+
+ # Wait for all AnswerObjectUndoSerial. We might get OidNotFoundError,
+ # meaning that objects in transaction's oid_list do not exist any
+ # longer. This is the symptom of a pack, so forbid undoing transaction
+ # when it happens, but sill keep waiting for answers.
+ failed = False
+ while True:
try:
- new_data = tryToResolveConflict(oid, data_tid, undone_tid,
- undo_data, data)
- except ConflictError:
- new_data = None
- if new_data is None:
- raise UndoError('Some data were modified by a later ' \
- 'transaction', oid)
+ self.waitResponses()
+ except NEOStorageNotFoundError:
+ failed = True
else:
- self._store(oid, data_tid, new_data)
+ break
+ if failed:
+ raise UndoError('non-undoable transaction')
- oid_list = self.local_var.txn_info['oids']
- # Consistency checking: all oids of the transaction must have been
- # reported as undone
- data_dict = self.local_var.data_dict
+ # Send undo data to all storage nodes.
for oid in oid_list:
- assert oid in data_dict, repr(oid)
- return self.local_var.tid, oid_list
+ current_serial, undo_serial, is_current = undo_object_tid_dict[oid]
+ if is_current:
+ data = None
+ else:
+ # Serial being undone is not the latest version for this
+ # object. This is an undo conflict, try to resolve it.
+ try:
+ # Load the latest version we are supposed to see
+ data = self.loadSerial(oid, current_serial)
+ # Load the version we were undoing to
+ undo_data = self.loadSerial(oid, undo_serial)
+ except NEOStorageNotFoundError:
+ raise UndoError('Object not found while resolving undo '
+ 'conflict')
+ # Resolve conflict
+ try:
+ data = tryToResolveConflict(oid, current_serial,
+ undone_tid, undo_data, data)
+ except ConflictError:
+ data = None
+ if data is None:
+ raise UndoError('Some data were modified by a later ' \
+ 'transaction', oid)
+ undo_serial = None
+ self._store(oid, current_serial, data, undo_serial)
+ return tid, oid_list
def _insertMetadata(self, txn_info, extension):
for k, v in loads(extension).items():
Modified: trunk/neo/client/handlers/storage.py
==============================================================================
--- trunk/neo/client/handlers/storage.py [iso-8859-1] (original)
+++ trunk/neo/client/handlers/storage.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -127,14 +127,8 @@ class StorageAnswersHandler(AnswerBaseHa
def answerTIDs(self, conn, tid_list):
self.app.local_var.node_tids[conn.getUUID()] = tid_list
- def answerUndoTransaction(self, conn, oid_list, error_oid_list,
- conflict_oid_list):
- local_var = self.app.local_var
- local_var.undo_conflict_oid_list.extend(conflict_oid_list)
- local_var.undo_error_oid_list.extend(error_oid_list)
- data_dict = local_var.data_dict
- for oid in oid_list:
- data_dict[oid] = ''
+ def answerObjectUndoSerial(self, conn, object_tid_dict):
+ self.app.local_var.undo_object_tid_dict.update(object_tid_dict)
def answerHasLock(self, conn, oid, status):
if status == LockState.GRANTED_TO_OTHER:
Modified: trunk/neo/handler.py
==============================================================================
--- trunk/neo/handler.py [iso-8859-1] (original)
+++ trunk/neo/handler.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -335,10 +335,10 @@ class EventHandler(object):
def notifyReplicationDone(self, conn, offset):
raise UnexpectedPacketError
- def askUndoTransaction(self, conn, tid, undone_tid):
+ def askObjectUndoSerial(self, conn, tid, undone_tid, oid_list):
raise UnexpectedPacketError
- def answerUndoTransaction(self, conn, oid_list, error_oid_list, conflict_oid_list):
+ def answerObjectUndoSerial(self, conn, object_tid_dict):
raise UnexpectedPacketError
def askHasLock(self, conn, tid, oid):
@@ -456,8 +456,8 @@ class EventHandler(object):
d[Packets.NotifyClusterInformation] = self.notifyClusterInformation
d[Packets.NotifyLastOID] = self.notifyLastOID
d[Packets.NotifyReplicationDone] = self.notifyReplicationDone
- d[Packets.AskUndoTransaction] = self.askUndoTransaction
- d[Packets.AnswerUndoTransaction] = self.answerUndoTransaction
+ d[Packets.AskObjectUndoSerial] = self.askObjectUndoSerial
+ d[Packets.AnswerObjectUndoSerial] = self.answerObjectUndoSerial
d[Packets.AskHasLock] = self.askHasLock
d[Packets.AnswerHasLock] = self.answerHasLock
Modified: trunk/neo/protocol.py
==============================================================================
--- trunk/neo/protocol.py [iso-8859-1] (original)
+++ trunk/neo/protocol.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -1528,55 +1528,76 @@ class NotifyLastOID(Packet):
(loid, ) = unpack('8s', body)
return (loid, )
-class AskUndoTransaction(Packet):
+class AskObjectUndoSerial(Packet):
"""
- Ask storage to undo given transaction
+ Ask storage the serial where object data is when undoing given transaction,
+ for a list of OIDs.
C -> S
"""
- def _encode(self, tid, undone_tid):
- return _encodeTID(tid) + _encodeTID(undone_tid)
+ _header_format = '!8s8sL'
+
+ def _encode(self, tid, undone_tid, oid_list):
+ body = StringIO()
+ write = body.write
+ write(pack(self._header_format, tid, undone_tid, len(oid_list)))
+ for oid in oid_list:
+ write(oid)
+ return body.getvalue()
def _decode(self, body):
- tid = _decodeTID(body[:8])
- undone_tid = _decodeTID(body[8:])
- return (tid, undone_tid)
-
-class AnswerUndoTransaction(Packet):
- """
- Answer an undo request, telling if undo could be done, with an oid list.
- If undo failed, the list contains oid(s) causing problems.
- If undo succeeded; the list contains all undone oids for given storage.
+ body = StringIO(body)
+ read = body.read
+ tid, undone_tid, oid_list_len = unpack(self._header_format,
+ read(self._header_len))
+ oid_list = [read(8) for _ in xrange(oid_list_len)]
+ return tid, undone_tid, oid_list
+
+class AnswerObjectUndoSerial(Packet):
+ """
+ Answer serials at which object data is when undoing a given transaction.
+ object_tid_dict has the following format:
+ key: oid
+ value: 3-tuple
+ current_serial (TID)
+ The latest serial visible to the undoing transaction.
+ undo_serial (TID)
+ Where undone data is (tid at which data is before given undo).
+ is_current (bool)
+ If current_serial's data is current on storage.
S -> C
"""
- _header_format = '!LLL'
+ _header_format = '!L'
+ _list_entry_format = '!8s8s8sB'
+ _list_entry_len = calcsize(_list_entry_format)
- def _encode(self, oid_list, error_oid_list, conflict_oid_list):
+ def _encode(self, object_tid_dict):
body = StringIO()
write = body.write
- oid_list_list = [oid_list, error_oid_list, conflict_oid_list]
- write(pack(self._header_format, *[len(x) for x in oid_list_list]))
- for oid_list in oid_list_list:
- for oid in oid_list:
- write(oid)
+ write(pack(self._header_format, len(object_tid_dict)))
+ list_entry_format = self._list_entry_format
+ for oid, (current_serial, undo_serial, is_current) in \
+ object_tid_dict.iteritems():
+ if undo_serial is None:
+ undo_serial = ZERO_TID
+ write(pack(list_entry_format, oid, current_serial, undo_serial,
+ is_current))
return body.getvalue()
def _decode(self, body):
body = StringIO(body)
read = body.read
- oid_list_len, error_oid_list_len, conflict_oid_list_len = unpack(
- self._header_format, read(self._header_len))
- oid_list = []
- error_oid_list = []
- conflict_oid_list = []
- for some_list, some_list_len in (
- (oid_list, oid_list_len),
- (error_oid_list, error_oid_list_len),
- (conflict_oid_list, conflict_oid_list_len),
- ):
- append = some_list.append
- for _ in xrange(some_list_len):
- append(read(OID_LEN))
- return (oid_list, error_oid_list, conflict_oid_list)
+ object_tid_dict = {}
+ list_entry_format = self._list_entry_format
+ list_entry_len = self._list_entry_len
+ object_tid_len = unpack(self._header_format, read(self._header_len))[0]
+ for _ in xrange(object_tid_len):
+ oid, current_serial, undo_serial, is_current = unpack(
+ list_entry_format, read(list_entry_len))
+ if undo_serial == ZERO_TID:
+ undo_serial = None
+ object_tid_dict[oid] = (current_serial, undo_serial,
+ bool(is_current))
+ return (object_tid_dict, )
class AskHasLock(Packet):
"""
@@ -1821,10 +1842,10 @@ class PacketRegistry(dict):
AnswerClusterState)
NotifyLastOID = register(0x0030, NotifyLastOID)
NotifyReplicationDone = register(0x0031, NotifyReplicationDone)
- AskUndoTransaction, AnswerUndoTransaction = register(
+ AskObjectUndoSerial, AnswerObjectUndoSerial = register(
0x0033,
- AskUndoTransaction,
- AnswerUndoTransaction)
+ AskObjectUndoSerial,
+ AnswerObjectUndoSerial)
AskHasLock, AnswerHasLock = register(
0x0034,
AskHasLock,
Modified: trunk/neo/storage/database/manager.py
==============================================================================
--- trunk/neo/storage/database/manager.py [iso-8859-1] (original)
+++ trunk/neo/storage/database/manager.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -234,13 +234,32 @@ class DatabaseManager(object):
pack state (True for packed)."""
raise NotImplementedError
- def getTransactionUndoData(self, tid, undone_tid,
- getObjectFromTransaction):
- """Undo transaction with "undone_tid" tid. "tid" is the tid of the
- transaction in which the undo happens.
- getObjectFromTransaction is a callback allowing to find object data
- stored to this storage in the same transaction (it is useful for
- example when undoing twice in the same transaction).
+ def findUndoTID(self, oid, tid, undone_tid, transaction_object):
+ """
+ oid
+ Object OID
+ tid
+ Transation doing the undo
+ undone_tid
+ Transaction to undo
+ transaction_object
+ Object data from memory, if it was modified by running
+ transaction.
+ None if is was not modified by running transaction.
+
+ Returns a 3-tuple:
+ current_tid (p64)
+ TID of most recent version of the object client's transaction can
+ see. This is used later to detect current conflicts (eg, another
+ client modifying the same object in parallel)
+ data_tid (int)
+ TID containing (without indirection) the data prior to undone
+ transaction.
+ None if object doesn't exist prior to transaction being undone
+ (its creation is being undone).
+ is_current (bool)
+ False if object was modified by later transaction (ie, data_tid is
+ not current), True otherwise.
"""
raise NotImplementedError
Modified: trunk/neo/storage/database/mysqldb.py
==============================================================================
--- trunk/neo/storage/database/mysqldb.py [iso-8859-1] (original)
+++ trunk/neo/storage/database/mysqldb.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -453,7 +453,7 @@ class MySQLDatabaseManager(DatabaseManag
if value_serial is None:
value_serial = 'NULL'
else:
- value_serial = '%d' % (value_serial, )
+ value_serial = '%d' % (u64(value_serial), )
q("""REPLACE INTO %s VALUES (%d, %d, %s, %s, %s, %s)""" \
% (obj_table, oid, tid, compression, checksum, data,
value_serial))
@@ -506,71 +506,33 @@ class MySQLDatabaseManager(DatabaseManag
result = self._getDataTIDFromData(oid, result)
return result
- def _findUndoTID(self, oid, tid, undone_tid, transaction_object):
- """
- oid, undone_tid (ints)
- Object to undo for given transaction
- tid (int)
- Client's transaction (he can't see objects past this value).
-
- Return a 2-tuple:
- current_tid (p64)
- TID of most recent version of the object client's transaction can
- see. This is used later to detect current conflicts (eg, another
- client modifying the same object in parallel)
- data_tid (int)
- TID containing (without indirection) the data prior to undone
- transaction.
- -1 if object was modified by later transaction.
- None if object doesn't exist prior to transaction being undone
- (its creation is being undone).
- """
+ def findUndoTID(self, oid, tid, undone_tid, transaction_object):
+ u64 = util.u64
+ p64 = util.p64
+ oid = u64(oid)
+ tid = u64(tid)
+ undone_tid = u64(undone_tid)
_getDataTID = self._getDataTID
if transaction_object is not None:
- # transaction_object:
- # oid, compression, ...
- # Expected value:
- # serial, next_serial, compression, ...
+ toid, tcompression, tchecksum, tdata, tvalue_serial = \
+ transaction_object
current_tid, current_data_tid = self._getDataTIDFromData(oid,
- (tid, None) + transaction_object[1:])
+ (tid, None, tcompression, tchecksum, tdata,
+ u64(tvalue_serial)))
else:
current_tid, current_data_tid = _getDataTID(oid, before_tid=tid)
- assert current_tid is not None, (oid, tid, transaction_object)
+ if current_tid is None:
+ return (None, None, False)
found_undone_tid, undone_data_tid = _getDataTID(oid, tid=undone_tid)
assert found_undone_tid is not None, (oid, undone_tid)
- if undone_data_tid not in (current_data_tid, tid):
- # data from the transaction we want to undo is modified by a later
- # transaction. It is up to the client node to decide what to do
- # (undo error of conflict resolution).
- data_tid = -1
- else:
- # Load object data as it was before given transaction.
- # It can be None, in which case it means we are undoing object
- # creation.
- _, data_tid = _getDataTID(oid, before_tid=undone_tid)
- return util.p64(current_tid), data_tid
-
- def getTransactionUndoData(self, tid, undone_tid,
- getObjectFromTransaction):
- q = self.query
- p64 = util.p64
- u64 = util.u64
- _findUndoTID = self._findUndoTID
-
- p_tid = tid
- tid = u64(tid)
- undone_tid = u64(undone_tid)
- if undone_tid > tid:
- # Replace with an exception reaching client (TIDNotFound)
- raise ValueError, 'Can\'t undo in future: %d > %d' % (
- undone_tid, tid)
- result = {}
- for (oid, ) in q("""SELECT oid FROM obj WHERE serial = %d""" % (
- undone_tid, )):
- p_oid = p64(oid)
- result[p_oid] = _findUndoTID(oid, tid, undone_tid,
- getObjectFromTransaction(p_tid, p_oid))
- return result
+ is_current = undone_data_tid in (current_data_tid, tid)
+ # Load object data as it was before given transaction.
+ # It can be None, in which case it means we are undoing object
+ # creation.
+ _, data_tid = _getDataTID(oid, before_tid=undone_tid)
+ if data_tid is not None:
+ data_tid = p64(data_tid)
+ return p64(current_tid), data_tid, is_current
def finishTransaction(self, tid):
q = self.query
Modified: trunk/neo/storage/handlers/client.py
==============================================================================
--- trunk/neo/storage/handlers/client.py [iso-8859-1] (original)
+++ trunk/neo/storage/handlers/client.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -18,7 +18,7 @@
from neo import logging
from neo import protocol
from neo.util import dump
-from neo.protocol import Packets, LockState
+from neo.protocol import Packets, LockState, Errors
from neo.storage.handlers import BaseClientAndStorageOperationHandler
from neo.storage.transactions import ConflictError, DelayedError
import time
@@ -78,6 +78,11 @@ class ClientOperationHandler(BaseClientA
compression, checksum, data, data_serial, tid):
# register the transaction
self.app.tm.register(conn.getUUID(), tid)
+ if data_serial is not None:
+ assert data == '', repr(data)
+ # Change data to None here, to do it only once, even if store gets
+ # delayed.
+ data = None
self._askStoreObject(conn, oid, serial, compression, checksum, data,
data_serial, tid, time.time())
@@ -97,41 +102,21 @@ class ClientOperationHandler(BaseClientA
app.pt.getPartitions(), partition_list)
conn.answer(Packets.AnswerTIDs(tid_list))
- def askUndoTransaction(self, conn, tid, undone_tid):
+ def askObjectUndoSerial(self, conn, tid, undone_tid, oid_list):
app = self.app
- tm = app.tm
- storeObject = tm.storeObject
- uuid = conn.getUUID()
- oid_list = []
- error_oid_list = []
- conflict_oid_list = []
-
- undo_tid_dict = app.dm.getTransactionUndoData(tid, undone_tid,
- tm.getObjectFromTransaction)
- for oid, (current_serial, undone_value_serial) in \
- undo_tid_dict.iteritems():
- if undone_value_serial == -1:
- # Some data were modified by a later transaction
- # This must be propagated to client, who will
- # attempt a conflict resolution, and store resolved
- # data.
- to_append_list = error_oid_list
- else:
- try:
- self.app.tm.register(uuid, tid)
- storeObject(tid, current_serial, oid, None,
- None, None, undone_value_serial)
- except ConflictError:
- to_append_list = conflict_oid_list
- except DelayedError:
- app.queueEvent(self.askUndoTransaction, conn, tid,
- undone_tid)
- return
- else:
- to_append_list = oid_list
- to_append_list.append(oid)
- conn.answer(Packets.AnswerUndoTransaction(oid_list, error_oid_list,
- conflict_oid_list))
+ findUndoTID = app.dm.findUndoTID
+ getObjectFromTransaction = app.tm.getObjectFromTransaction
+ object_tid_dict = {}
+ for oid in oid_list:
+ current_serial, undo_serial, is_current = findUndoTID(oid, tid,
+ undone_tid, getObjectFromTransaction(tid, oid))
+ if current_serial is None:
+ p = Errors.OidNotFound(dump(oid))
+ break
+ object_tid_dict[oid] = (current_serial, undo_serial, is_current)
+ else:
+ p = Packets.AnswerObjectUndoSerial(object_tid_dict)
+ conn.answer(p)
def askHasLock(self, conn, tid, oid):
locking_tid = self.app.tm.getLockingTID(oid)
Modified: trunk/neo/tests/client/testClientApp.py
==============================================================================
--- trunk/neo/tests/client/testClientApp.py [iso-8859-1] (original)
+++ trunk/neo/tests/client/testClientApp.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -785,59 +785,8 @@ class ClientApplicationTests(NeoTestBase
self.assertEquals(marker, [])
self.assertEquals(app.local_var.txn, old_txn)
- def test_undo2(self):
- # Three tests here :
- # undo txn2 where obj2 was modified in tid3 -> fail
- # undo txn3 where obj2 was altered from tid2 -> ok
- # txn4 is the transaction where the undo occurs
+ def _getAppForUndoTests(self, oid0, tid0, tid1, tid2):
app = self.getApp()
- app.num_partitions = 2
- oid1, oid2 = self.makeOID(1), self.makeOID(2)
- oid3 = self.makeOID(3)
- tid1, tid2 = self.makeTID(1), self.makeTID(2)
- tid3, tid4 = self.makeTID(3), self.makeTID(4)
- # commit version 1 of object 2
- txn2 = self.beginTransaction(app, tid=tid2)
- self.storeObject(app, oid=oid2, data='O1V2')
- self.voteTransaction(app)
- self.askFinishTransaction(app)
- # commit version 2 of object 2
- txn3 = self.beginTransaction(app, tid=tid3)
- self.storeObject(app, oid=oid2, data='O2V2')
- self.voteTransaction(app)
- self.askFinishTransaction(app)
- # undo 1 -> undoing non-last TID, and conflict resolution succeeded
- u1p1 = Packets.AnswerTransactionInformation(tid1, '', '', '',
- False, (oid2, ))
- u1p2 = Packets.AnswerUndoTransaction([], [oid2], [])
- # undo 2 -> undoing non-last TID, and conflict resolution failed
- u2p1 = Packets.AnswerTransactionInformation(tid2, '', '', '',
- False, (oid2, ))
- u2p2 = Packets.AnswerUndoTransaction([], [oid2], [])
- # undo 3 -> "live" conflict (another transaction modifying the object
- # we want to undo)
- u3p1 = Packets.AnswerTransactionInformation(tid3, '', '', '',
- False, (oid3, ))
- u3p2 = Packets.AnswerUndoTransaction([], [], [oid3])
- # undo 4 -> undoing last tid
- u4p1 = Packets.AnswerTransactionInformation(tid3, '', '', '',
- False, (oid1, ))
- u4p2 = Packets.AnswerUndoTransaction([oid1], [], [])
- # test logic
- packets = (u1p1, u1p2, u2p1, u2p2, u3p1, u3p2, u4p1, u4p2)
- for i, p in enumerate(packets):
- p.setId(i)
- storage_address = ('127.0.0.1', 10010)
- conn = Mock({
- 'getNextId': 1,
- 'fakeReceived': ReturnValues(
- u1p1,
- u2p1,
- u4p1,
- u3p1,
- ),
- 'getAddress': storage_address,
- })
cell = Mock({
'getAddress': 'FakeServer',
'getState': 'FakeState',
@@ -845,59 +794,161 @@ class ClientApplicationTests(NeoTestBase
app.pt = Mock({
'getCellListForTID': [cell, ],
'getCellListForOID': [cell, ],
+ 'getCellList': [cell, ],
})
+ transaction_info = Packets.AnswerTransactionInformation(tid1, '', '',
+ '', False, (oid0, ))
+ transaction_info.setId(1)
+ conn = Mock({
+ 'getNextId': 1,
+ 'fakeReceived': transaction_info,
+ 'getAddress': ('127.0.0.1', 10010),
+ })
+ app.nm.createStorage(address=conn.getAddress())
app.cp = Mock({'getConnForCell': conn, 'getConnForNode': conn})
- def tryToResolveConflict(oid, conflict_serial, serial, data,
- committedData=''):
- marker.append(1)
- return resolution_result
class Dispatcher(object):
def pending(self, queue):
return not queue.empty()
app.dispatcher = Dispatcher()
- def _load(oid, tid=None, serial=None):
- assert tid is not None
- assert serial is None, serial
- return ('dummy', oid, tid)
- app._load = _load
- app.nm.createStorage(address=storage_address)
- # all start here
+ def loadSerial(oid, tid):
+ self.assertEqual(oid, oid0)
+ return {tid0: 'dummy', tid2: 'cdummy'}[tid]
+ app.loadSerial = loadSerial
+ store_marker = []
+ def _store(oid, serial, data, data_serial=None):
+ store_marker.append((oid, serial, data, data_serial))
+ app._store = _store
app.local_var.clear()
- txn4 = self.beginTransaction(app, tid=tid4)
- marker = []
- resolution_result = 'solved'
- app.local_var.queue.put((conn, u1p2))
- app.undo(tid1, txn4, tryToResolveConflict)
- self.assertEquals(marker, [1])
+ return app, conn, store_marker
- app.local_var.clear()
- txn4 = self.beginTransaction(app, tid=tid4)
+ def test_undoWithResolutionSuccess(self):
+ """
+ Try undoing transaction tid1, which contains object oid.
+ Object oid previous revision before tid1 is tid0.
+ Transaction tid2 modified oid (and contains its data).
+
+ Undo is accepted, because conflict resolution succeeds.
+ """
+ oid0 = self.makeOID(1)
+ tid0 = self.getNextTID()
+ tid1 = self.getNextTID()
+ tid2 = self.getNextTID()
+ tid3 = self.getNextTID()
+ app, conn, store_marker = self._getAppForUndoTests(oid0, tid0, tid1,
+ tid2)
+ undo_serial = Packets.AnswerObjectUndoSerial({
+ oid0: (tid2, tid0, False)})
+ undo_serial.setId(2)
+ app.local_var.queue.put((conn, undo_serial))
marker = []
- resolution_result = None
- app.local_var.queue.put((conn, u2p2))
- self.assertRaises(UndoError, app.undo, tid2, txn4,
- tryToResolveConflict)
- self.assertEquals(marker, [1])
-
- app.local_var.clear()
- txn4 = self.beginTransaction(app, tid=tid4)
+ def tryToResolveConflict(oid, conflict_serial, serial, data,
+ committedData=''):
+ marker.append((oid, conflict_serial, serial, data, committedData))
+ return 'solved'
+ # The undo
+ txn = self.beginTransaction(app, tid=tid3)
+ app.undo(tid1, txn, tryToResolveConflict)
+ # Checking what happened
+ moid, mconflict_serial, mserial, mdata, mcommittedData = marker[0]
+ self.assertEqual(moid, oid0)
+ self.assertEqual(mconflict_serial, tid2)
+ self.assertEqual(mserial, tid1)
+ self.assertEqual(mdata, 'dummy')
+ self.assertEqual(mcommittedData, 'cdummy')
+ moid, mserial, mdata, mdata_serial = store_marker[0]
+ self.assertEqual(moid, oid0)
+ self.assertEqual(mserial, tid2)
+ self.assertEqual(mdata, 'solved')
+ self.assertEqual(mdata_serial, None)
+
+ def test_undoWithResolutionFailure(self):
+ """
+ Try undoing transaction tid1, which contains object oid.
+ Object oid previous revision before tid1 is tid0.
+ Transaction tid2 modified oid (and contains its data).
+
+ Undo is rejeced with a raise, because conflict resolution fails.
+ """
+ oid0 = self.makeOID(1)
+ tid0 = self.getNextTID()
+ tid1 = self.getNextTID()
+ tid2 = self.getNextTID()
+ tid3 = self.getNextTID()
+ undo_serial = Packets.AnswerObjectUndoSerial({
+ oid0: (tid2, tid0, False)})
+ undo_serial.setId(2)
+ app, conn, store_marker = self._getAppForUndoTests(oid0, tid0, tid1,
+ tid2)
+ app.local_var.queue.put((conn, undo_serial))
marker = []
- resolution_result = None
- app.local_var.queue.put((conn, u4p2))
- self.assertEquals(app.undo(tid3, txn4, tryToResolveConflict),
- (tid4, [oid1, ]))
- self.assertEquals(marker, [])
-
- app.local_var.clear()
- txn4 = self.beginTransaction(app, tid=tid4)
+ def tryToResolveConflict(oid, conflict_serial, serial, data,
+ committedData=''):
+ marker.append((oid, conflict_serial, serial, data, committedData))
+ return None
+ # The undo
+ txn = self.beginTransaction(app, tid=tid3)
+ self.assertRaises(UndoError, app.undo, tid1, txn, tryToResolveConflict)
+ # Checking what happened
+ moid, mconflict_serial, mserial, mdata, mcommittedData = marker[0]
+ self.assertEqual(moid, oid0)
+ self.assertEqual(mconflict_serial, tid2)
+ self.assertEqual(mserial, tid1)
+ self.assertEqual(mdata, 'dummy')
+ self.assertEqual(mcommittedData, 'cdummy')
+ self.assertEqual(len(store_marker), 0)
+ # Likewise, but conflict resolver raises a ConflictError.
+ # Still, exception raised by undo() must be UndoError.
marker = []
- resolution_result = None
- app.local_var.queue.put((conn, u3p2))
- self.assertRaises(ConflictError, app.undo, tid3, txn4,
- tryToResolveConflict)
- self.assertEquals(marker, [])
-
- self.askFinishTransaction(app)
+ def tryToResolveConflict(oid, conflict_serial, serial, data,
+ committedData=''):
+ marker.append((oid, conflict_serial, serial, data, committedData))
+ raise ConflictError
+ # The undo
+ app.local_var.queue.put((conn, undo_serial))
+ self.assertRaises(UndoError, app.undo, tid1, txn, tryToResolveConflict)
+ # Checking what happened
+ moid, mconflict_serial, mserial, mdata, mcommittedData = marker[0]
+ self.assertEqual(moid, oid0)
+ self.assertEqual(mconflict_serial, tid2)
+ self.assertEqual(mserial, tid1)
+ self.assertEqual(mdata, 'dummy')
+ self.assertEqual(mcommittedData, 'cdummy')
+ self.assertEqual(len(store_marker), 0)
+
+ def test_undo(self):
+ """
+ Try undoing transaction tid1, which contains object oid.
+ Object oid previous revision before tid1 is tid0.
+
+ Undo is accepted, because tid1 is object's current revision.
+ """
+ oid0 = self.makeOID(1)
+ tid0 = self.getNextTID()
+ tid1 = self.getNextTID()
+ tid2 = self.getNextTID()
+ tid3 = self.getNextTID()
+ transaction_info = Packets.AnswerTransactionInformation(tid1, '', '',
+ '', False, (oid0, ))
+ transaction_info.setId(1)
+ undo_serial = Packets.AnswerObjectUndoSerial({
+ oid0: (tid1, tid0, True)})
+ undo_serial.setId(2)
+ app, conn, store_marker = self._getAppForUndoTests(oid0, tid0, tid1,
+ tid2)
+ app.local_var.queue.put((conn, undo_serial))
+ def tryToResolveConflict(oid, conflict_serial, serial, data,
+ committedData=''):
+ raise Exception, 'Test called conflict resolution, but there ' \
+ 'is no conflict in this test !'
+ # The undo
+ txn = self.beginTransaction(app, tid=tid3)
+ app.undo(tid1, txn, tryToResolveConflict)
+ # Checking what happened
+ moid, mserial, mdata, mdata_serial = store_marker[0]
+ self.assertEqual(moid, oid0)
+ self.assertEqual(mserial, tid1)
+ self.assertEqual(mdata, None)
+ self.assertEqual(mdata_serial, tid0)
def test_undoLog(self):
app = self.getApp()
Modified: trunk/neo/tests/client/testStorageHandler.py
==============================================================================
--- trunk/neo/tests/client/testStorageHandler.py [iso-8859-1] (original)
+++ trunk/neo/tests/client/testStorageHandler.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -240,28 +240,25 @@ class StorageAnswerHandlerTests(NeoTestB
self.assertTrue(uuid in self.app.local_var.node_tids)
self.assertEqual(self.app.local_var.node_tids[uuid], tid_list)
- def test_answerUndoTransaction(self):
- local_var = self.app.local_var
- undo_conflict_oid_list = local_var.undo_conflict_oid_list = []
- undo_error_oid_list = local_var.undo_error_oid_list = []
- data_dict = local_var.data_dict = {}
- conn = None # Nothing is done on connection in this handler
-
- # Nothing undone, check nothing changed
- self.handler.answerUndoTransaction(conn, [], [], [])
- self.assertEqual(undo_conflict_oid_list, [])
- self.assertEqual(undo_error_oid_list, [])
- self.assertEqual(data_dict, {})
-
- # One OID for each case, check they are inserted in expected local_var
- # entries.
- oid_1 = self.getOID(0)
- oid_2 = self.getOID(1)
- oid_3 = self.getOID(2)
- self.handler.answerUndoTransaction(conn, [oid_1], [oid_2], [oid_3])
- self.assertEqual(undo_conflict_oid_list, [oid_3])
- self.assertEqual(undo_error_oid_list, [oid_2])
- self.assertEqual(data_dict, {oid_1: ''})
+ def test_answerObjectUndoSerial(self):
+ uuid = self.getNewUUID()
+ conn = self.getFakeConnection(uuid=uuid)
+ oid1 = self.getOID(1)
+ oid2 = self.getOID(2)
+ tid0 = self.getNextTID()
+ tid1 = self.getNextTID()
+ tid2 = self.getNextTID()
+ tid3 = self.getNextTID()
+ self.app.local_var.undo_object_tid_dict = undo_dict = {
+ oid1: [tid0, tid1],
+ }
+ self.handler.answerObjectUndoSerial(conn, {
+ oid2: [tid2, tid3],
+ })
+ self.assertEqual(undo_dict, {
+ oid1: [tid0, tid1],
+ oid2: [tid2, tid3],
+ })
def test_answerHasLock(self):
uuid = self.getNewUUID()
Modified: trunk/neo/tests/storage/testClientHandler.py
==============================================================================
--- trunk/neo/tests/storage/testClientHandler.py [iso-8859-1] (original)
+++ trunk/neo/tests/storage/testClientHandler.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -16,7 +16,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
import unittest
-from mock import Mock
+from mock import Mock, ReturnValues
from collections import deque
from neo.tests import NeoTestBase
from neo.storage.app import Application
@@ -215,11 +215,27 @@ class StorageClientHandlerTests(NeoTestB
conn = self._getConnection(uuid=uuid)
tid = self.getNextTID()
oid, serial, comp, checksum, data = self._getObject()
+ self.operation.askStoreObject(conn, oid, serial, comp, checksum,
+ data, None, tid)
+ self._checkStoreObjectCalled(tid, serial, oid, comp,
+ checksum, data, None)
+ pconflicting, poid, pserial = self.checkAnswerStoreObject(conn,
+ decode=True)
+ self.assertEqual(pconflicting, 0)
+ self.assertEqual(poid, oid)
+ self.assertEqual(pserial, serial)
+
+ def test_askStoreObjectWithDataTID(self):
+ # same as test_askStoreObject1, but with a non-None data_tid value
+ uuid = self.getNewUUID()
+ conn = self._getConnection(uuid=uuid)
+ tid = self.getNextTID()
+ oid, serial, comp, checksum, data = self._getObject()
data_tid = self.getNextTID()
self.operation.askStoreObject(conn, oid, serial, comp, checksum,
- data, data_tid, tid)
+ '', data_tid, tid)
self._checkStoreObjectCalled(tid, serial, oid, comp,
- checksum, data, data_tid)
+ checksum, None, data_tid)
pconflicting, poid, pserial = self.checkAnswerStoreObject(conn,
decode=True)
self.assertEqual(pconflicting, 0)
@@ -236,9 +252,8 @@ class StorageClientHandlerTests(NeoTestB
raise ConflictError(locking_tid)
self.app.tm.storeObject = fakeStoreObject
oid, serial, comp, checksum, data = self._getObject()
- data_tid = self.getNextTID()
self.operation.askStoreObject(conn, oid, serial, comp, checksum,
- data, data_tid, tid)
+ data, None, tid)
pconflicting, poid, pserial = self.checkAnswerStoreObject(conn,
decode=True)
self.assertEqual(pconflicting, 1)
@@ -253,44 +268,22 @@ class StorageClientHandlerTests(NeoTestB
self.assertEqual(len(calls), 1)
calls[0].checkArgs(tid)
- def test_askUndoTransaction(self):
- conn = self._getConnection()
+ def test_askObjectUndoSerial(self):
+ uuid = self.getNewUUID()
+ conn = self._getConnection(uuid=uuid)
tid = self.getNextTID()
undone_tid = self.getNextTID()
- oid_1 = self.getNextTID()
- oid_2 = self.getNextTID()
- oid_3 = self.getNextTID()
- oid_4 = self.getNextTID()
- def getTransactionUndoData(tid, undone_tid, getObjectFromTransaction):
- return {
- oid_1: (1, 1),
- oid_2: (1, -1),
- oid_3: (1, 2),
- oid_4: (1, 3),
- }
- self.app.dm.getTransactionUndoData = getTransactionUndoData
- original_storeObject = self.app.tm.storeObject
- def storeObject(tid, serial, oid, *args, **kw):
- if oid == oid_3:
- raise ConflictError(0)
- elif oid == oid_4 and delay_store:
- raise DelayedError
- return original_storeObject(tid, serial, oid, *args, **kw)
- self.app.tm.storeObject = storeObject
-
- # Check if delaying a store (of oid_4) is supported
- delay_store = True
- self.operation.askUndoTransaction(conn, tid, undone_tid)
- self.checkNoPacketSent(conn)
-
- delay_store = False
- self.operation.askUndoTransaction(conn, tid, undone_tid)
- oid_list_1, oid_list_2, oid_list_3 = self.checkAnswerPacket(conn,
- Packets.AnswerUndoTransaction, decode=True)
- # Compare sets as order doens't matter here.
- self.assertEqual(set(oid_list_1), set([oid_1, oid_4]))
- self.assertEqual(oid_list_2, [oid_2])
- self.assertEqual(oid_list_3, [oid_3])
+ # Keep 2 entries here, so we check findUndoTID is called only once.
+ oid_list = [self.getOID(1), self.getOID(2)]
+ obj2_data = [] # Marker
+ self.app.tm = Mock({
+ 'getObjectFromTransaction': None,
+ })
+ self.app.dm = Mock({
+ 'findUndoTID': ReturnValues((None, None, False), )
+ })
+ self.operation.askObjectUndoSerial(conn, tid, undone_tid, oid_list)
+ self.checkErrorPacket(conn)
def test_askHasLock(self):
tid_1 = self.getNextTID()
Modified: trunk/neo/tests/storage/testStorageMySQLdb.py
==============================================================================
--- trunk/neo/tests/storage/testStorageMySQLdb.py [iso-8859-1] (original)
+++ trunk/neo/tests/storage/testStorageMySQLdb.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -605,13 +605,13 @@ class StorageMySQSLdbTests(NeoTestBase):
db.storeTransaction(
tid1, (
(oid1, 0, 0, 'foo', None),
- (oid2, None, None, None, u64(tid0)),
- (oid3, None, None, None, u64(tid2)),
+ (oid2, None, None, None, tid0),
+ (oid3, None, None, None, tid2),
), None, temporary=False)
db.storeTransaction(
tid2, (
- (oid1, None, None, None, u64(tid1)),
- (oid2, None, None, None, u64(tid1)),
+ (oid1, None, None, None, tid1),
+ (oid2, None, None, None, tid1),
(oid3, 0, 0, 'bar', None),
), None, temporary=False)
@@ -689,7 +689,7 @@ class StorageMySQSLdbTests(NeoTestBase):
), None, temporary=False)
db.storeTransaction(
tid2, (
- (oid1, None, None, None, u64(tid1)),
+ (oid1, None, None, None, tid1),
), None, temporary=False)
self.assertEqual(
@@ -713,7 +713,7 @@ class StorageMySQSLdbTests(NeoTestBase):
), None, temporary=False)
db.storeTransaction(
tid2, (
- (oid1, None, None, None, u64(tid1)),
+ (oid1, None, None, None, tid1),
), None, temporary=False)
self.assertEqual(
@@ -723,7 +723,7 @@ class StorageMySQSLdbTests(NeoTestBase):
db._getDataTID(u64(oid1), tid=u64(tid2)),
(u64(tid2), u64(tid1)))
- def test__findUndoTID(self):
+ def test_findUndoTID(self):
db = self.db
db.setup(reset=True)
tid1 = self.getNextTID()
@@ -740,8 +740,8 @@ class StorageMySQSLdbTests(NeoTestBase):
# Result: current tid is tid1, data_tid is None (undoing object
# creation)
self.assertEqual(
- db._findUndoTID(u64(oid1), u64(tid4), u64(tid1), None),
- (tid1, None))
+ db.findUndoTID(oid1, tid4, tid1, None),
+ (tid1, None, True))
# Store a new transaction
db.storeTransaction(
@@ -752,14 +752,14 @@ class StorageMySQSLdbTests(NeoTestBase):
# Undoing oid1 tid2, OK: tid2 is latest
# Result: current tid is tid2, data_tid is tid1
self.assertEqual(
- db._findUndoTID(u64(oid1), u64(tid4), u64(tid2), None),
- (tid2, u64(tid1)))
+ db.findUndoTID(oid1, tid4, tid2, None),
+ (tid2, tid1, True))
# Undoing oid1 tid1, Error: tid2 is latest
# Result: current tid is tid2, data_tid is -1
self.assertEqual(
- db._findUndoTID(u64(oid1), u64(tid4), u64(tid1), None),
- (tid2, -1))
+ db.findUndoTID(oid1, tid4, tid1, None),
+ (tid2, None, False))
# Undoing oid1 tid1 with tid2 being undone in same transaction,
# OK: tid1 is latest
@@ -768,71 +768,22 @@ class StorageMySQSLdbTests(NeoTestBase):
# Explanation of transaction_object: oid1, no data but a data serial
# to tid1
self.assertEqual(
- db._findUndoTID(u64(oid1), u64(tid4), u64(tid1),
- (u64(oid1), None, None, None, u64(tid1))),
- (tid4, None))
+ db.findUndoTID(oid1, tid4, tid1,
+ (u64(oid1), None, None, None, tid1)),
+ (tid4, None, True))
# Store a new transaction
db.storeTransaction(
tid3, (
- (oid1, None, None, None, u64(tid1)),
+ (oid1, None, None, None, tid1),
), None, temporary=False)
# Undoing oid1 tid1, OK: tid3 is latest with tid1 data
# Result: current tid is tid2, data_tid is None (undoing object
# creation)
self.assertEqual(
- db._findUndoTID(u64(oid1), u64(tid4), u64(tid1), None),
- (tid3, None))
-
- def test_getTransactionUndoData(self):
- db = self.db
- db.setup(reset=True)
- tid1 = self.getNextTID()
- tid2 = self.getNextTID()
- tid3 = self.getNextTID()
- tid4 = self.getNextTID()
- tid5 = self.getNextTID()
- assert tid1 < tid2 < tid3 < tid4 < tid5
- oid1 = self.getOID(1)
- oid2 = self.getOID(2)
- oid3 = self.getOID(3)
- oid4 = self.getOID(4)
- oid5 = self.getOID(5)
- db.storeTransaction(
- tid1, (
- (oid1, 0, 0, 'foo1', None),
- (oid2, 0, 0, 'foo2', None),
- (oid3, 0, 0, 'foo3', None),
- (oid4, 0, 0, 'foo5', None),
- ), None, temporary=False)
- db.storeTransaction(
- tid2, (
- (oid1, 0, 0, 'bar1', None),
- (oid2, None, None, None, None),
- (oid3, 0, 0, 'bar3', None),
- ), None, temporary=False)
- db.storeTransaction(
- tid3, (
- (oid3, 0, 0, 'baz3', None),
- (oid5, 0, 0, 'foo6', None),
- ), None, temporary=False)
-
- def getObjectFromTransaction(tid, oid):
- return None
-
- self.assertEqual(
- db.getTransactionUndoData(tid4, tid2, getObjectFromTransaction),
- {
- oid1: (tid2, u64(tid1)), # can be undone
- oid2: (tid2, u64(tid1)), # can be undone (creation redo)
- oid3: (tid3, -1), # cannot be undone
- # oid4 & oid5: not present because not ins undone transaction
- })
-
- # Cannot undo future transaction
- self.assertRaises(ValueError, db.getTransactionUndoData, tid4, tid5,
- getObjectFromTransaction)
+ db.findUndoTID(oid1, tid4, tid1, None),
+ (tid3, None, True))
if __name__ == "__main__":
unittest.main()
Modified: trunk/neo/tests/testProtocol.py
==============================================================================
--- trunk/neo/tests/testProtocol.py [iso-8859-1] (original)
+++ trunk/neo/tests/testProtocol.py [iso-8859-1] Thu Sep 2 18:29:59 2010
@@ -482,23 +482,29 @@ class ProtocolTests(NeoTestBase):
p_offset = p.decode()[0]
self.assertEqual(p_offset, offset)
- def test_askUndoTransaction(self):
+ def test_askObjectUndoSerial(self):
tid = self.getNextTID()
undone_tid = self.getNextTID()
- p = Packets.AskUndoTransaction(tid, undone_tid)
- p_tid, p_undone_tid = p.decode()
- self.assertEqual(p_tid, tid)
- self.assertEqual(p_undone_tid, undone_tid)
-
- def test_answerUndoTransaction(self):
- oid_list_1 = [self.getNextTID()]
- oid_list_2 = [self.getNextTID(), self.getNextTID()]
- oid_list_3 = [self.getNextTID(), self.getNextTID(), self.getNextTID()]
- p = Packets.AnswerUndoTransaction(oid_list_1, oid_list_2, oid_list_3)
- p_oid_list_1, p_oid_list_2, p_oid_list_3 = p.decode()
- self.assertEqual(p_oid_list_1, oid_list_1)
- self.assertEqual(p_oid_list_2, oid_list_2)
- self.assertEqual(p_oid_list_3, oid_list_3)
+ oid_list = [self.getOID(x) for x in xrange(4)]
+ p = Packets.AskObjectUndoSerial(tid, undone_tid, oid_list)
+ ptid, pundone_tid, poid_list = p.decode()
+ self.assertEqual(tid, ptid)
+ self.assertEqual(undone_tid, pundone_tid)
+ self.assertEqual(oid_list, poid_list)
+
+ def test_answerObjectUndoSerial(self):
+ oid1 = self.getNextTID()
+ oid2 = self.getNextTID()
+ tid1 = self.getNextTID()
+ tid2 = self.getNextTID()
+ tid3 = self.getNextTID()
+ object_tid_dict = {
+ oid1: (tid1, tid2, True),
+ oid2: (tid3, None, False),
+ }
+ p = Packets.AnswerObjectUndoSerial(object_tid_dict)
+ pobject_tid_dict = p.decode()[0]
+ self.assertEqual(object_tid_dict, pobject_tid_dict)
def test_NotifyLastOID(self):
oid = self.getOID(1)
More information about the Neo-report
mailing list