Commit e47f2923 authored by Julien Muchembled's avatar Julien Muchembled

CMFActivity: remove non-executable message state (-3)

When an object is deleted, higher level code used to flush its messages (without
invoking them). However, a concurrent and very long transaction may be about to
activate such an object, without conflict. We already experienced false -3
errors that could prevent other messages to be validated.

Because there is no efficient and reliable way to flush absolutely all messages,
messages on deleted objects are now ignored and deleted without any email
notification. There's only a WARNING in logs. But for performance reasons,
there's still a flush on object deletion.

To simplify code, messages that went to -3 for other reasons, like a
non-existing method, now go to -2. In fact, this was already the case for
grouped messages.

In case that a path is recycled, it may still be possible for a message to be
executed on a wrong object (the new one), instead of being ignored (because the
activated object was deleted). So in such scenario, developer should make sure
not to delete an object that may be activated in a concurrent transaction.
If the original object has an OID at the moment it is activated, an assertion
will make sure the message is not executed on another object.
parent fcce7b97
......@@ -41,10 +41,6 @@ DEFAULT_ACTIVITY = 'SQLDict'
# Processing node are used to store processing state or processing node
DISTRIBUTABLE_STATE = -1
INVOKE_ERROR_STATE = -2
VALIDATE_ERROR_STATE = -3
STOP_STATE = -4
# Special state which allows to select positive nodes
POSITIVE_NODE_STATE = 'Positive Node State'
_DEFAULT_ACTIVATE_PARAMETER_KEY = 'default_activate_parameter'
......@@ -145,12 +141,6 @@ class ActiveObject(ExtensionClass.Base):
"""
return self.hasActivity(processing_node = INVOKE_ERROR_STATE)
security.declareProtected( permissions.View, 'hasInvalidActivity' )
def hasInvalidActivity(self, **kw):
"""Tells if there is invalied activities for this object.
"""
return self.hasActivity(processing_node = VALIDATE_ERROR_STATE)
def getActiveProcess(self):
path = getActivityRuntimeEnvironment()._message.active_process
if path:
......
......@@ -33,8 +33,7 @@ from Products.ERP5Type.Base import Base
from Products.ERP5Type import PropertySheet
from Products.ERP5Type.ConflictFree import ConflictFreeLog
from BTrees.Length import Length
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE, \
VALIDATE_ERROR_STATE
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
from random import randrange
from .ActiveResult import ActiveResult
......@@ -150,13 +149,6 @@ class ActiveProcess(Base):
"""
return self.hasActivity(processing_node = INVOKE_ERROR_STATE)
security.declareProtected( CMFCorePermissions.View, 'hasInvalidActivity' )
def hasInvalidActivity(self, **kw):
"""
Tells if an object if active
"""
return self.hasActivity(processing_node = VALIDATE_ERROR_STATE)
def getCreationDate(self):
"""
Define a Creation Date for an active process
......
......@@ -34,8 +34,7 @@ from zLOG import LOG, TRACE, INFO, WARNING, ERROR, PANIC
from ZODB.POSException import ConflictError
from Products.CMFActivity.ActivityTool import (
MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED)
from Products.CMFActivity.ActiveObject import (
INVOKE_ERROR_STATE, VALIDATE_ERROR_STATE)
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
from Products.CMFActivity.ActivityRuntimeEnvironment import (
ActivityRuntimeEnvironment, getTransactionalVariable)
from Queue import Queue, VALIDATION_ERROR_DELAY, VALID, INVALID_PATH
......@@ -278,7 +277,7 @@ class SQLBase(Queue):
# Count the number of objects to prevent too many objects.
cost = m.activity_kw.get('group_method_cost', .01)
assert 0 < cost <= 1, (self.sql_table, uid)
count = len(m.getObjectList(activity_tool))
count = m.getObjectCount(activity_tool)
# this is heuristic (messages with same group_method_id
# are likely to have the same group_method_cost)
limit = int(1. / cost + 1 - count)
......@@ -294,7 +293,7 @@ class SQLBase(Queue):
uid_to_duplicate_uid_list_dict[uid] += uid_list
continue
uid_to_duplicate_uid_list_dict[uid] = uid_list
cost += len(m.getObjectList(activity_tool)) * \
cost += m.getObjectCount(activity_tool) * \
m.activity_kw.get('group_method_cost', .01)
message_list.append(m)
if cost >= 1:
......@@ -415,7 +414,6 @@ class SQLBase(Queue):
final_error_uid_list = []
make_available_uid_list = []
notify_user_list = []
non_executable_message_list = []
executed_uid_list = deletable_uid_list
if uid_to_duplicate_uid_list_dict is not None:
for m in message_list:
......@@ -461,12 +459,14 @@ class SQLBase(Queue):
except:
self._log(WARNING, 'Failed to reactivate %r' % uid)
make_available_uid_list.append(uid)
else:
# Internal CMFActivity error: the message can not be executed because
# something is missing (context object cannot be found, method cannot
# be accessed on object).
non_executable_message_list.append(uid)
notify_user_list.append((m, False))
else: # MESSAGE_NOT_EXECUTABLE
# 'path' does not point to any object. Activities are normally flushed
# (without invoking them) when an object is deleted, but this is only
# an optimisation. There is no efficient and reliable way to do such
# this, because a concurrent and very long transaction may be about to
# activate this object, without conflict.
# So we have to clean up any remaining activity.
deletable_uid_list.append(uid)
if deletable_uid_list:
try:
self._retryOnLockError(activity_tool.SQLBase_delMessage,
......@@ -490,13 +490,6 @@ class SQLBase(Queue):
except:
self._log(ERROR, 'Failed to set message to error state for %r'
% final_error_uid_list)
if non_executable_message_list:
try:
activity_tool.SQLBase_assignMessage(table=self.sql_table,
uid=non_executable_message_list, processing_node=VALIDATE_ERROR_STATE)
except:
self._log(ERROR, 'Failed to set message to invalid path state for %r'
% non_executable_message_list)
if make_available_uid_list:
try:
self.makeMessageListAvailable(activity_tool=activity_tool,
......
......@@ -169,6 +169,7 @@ class Message(BaseMessage):
is_executed = MESSAGE_NOT_EXECUTED
processing = None
traceback = None
oid = None
def __init__(self, obj, active_process, activity_kw, method_id, args, kw):
if isinstance(obj, str):
......@@ -177,6 +178,12 @@ class Message(BaseMessage):
else:
self.object_path = obj.getPhysicalPath()
activity_creation_trace = obj.getPortalObject().portal_activities.activity_creation_trace
try:
self.oid = aq_base(obj)._p_oid
# Note that it's too early to get the OID of a newly created object,
# so at this point, self.oid may still be None.
except AttributeError:
pass
if active_process is not None:
self.active_process = active_process.getPhysicalPath()
self.active_process_uid = active_process.getUid()
......@@ -216,29 +223,38 @@ class Message(BaseMessage):
def getObject(self, activity_tool):
"""return the object referenced in this message."""
return activity_tool.unrestrictedTraverse(self.object_path)
def getObjectList(self, activity_tool):
"""return the list of object that can be expanded from this message."""
object_list = []
try:
object_list.append(self.getObject(activity_tool))
obj = activity_tool.unrestrictedTraverse(self.object_path)
except KeyError:
pass
LOG('CMFActivity', WARNING, "Message dropped (no object found at path %r)"
% (self.object_path,), error=sys.exc_info())
self.setExecutionState(MESSAGE_NOT_EXECUTABLE)
else:
if self.hasExpandMethod():
expand_method_id = self.activity_kw['expand_method_id']
# FIXME: how to pass parameters?
object_list = getattr(object_list[0], expand_method_id)()
return object_list
def hasExpandMethod(self):
"""return true if the message has an expand method.
if self.oid and self.oid != getattr(aq_base(obj), '_p_oid', None):
raise ValueError("OID mismatch for %r" % obj)
return obj
def getObjectList(self, activity_tool):
"""return the list of object that can be expanded from this message
An expand method is used to expand the list of objects and to turn a
big recursive transaction affecting many objects into multiple
transactions affecting only one object at a time (this can prevent
duplicated method calls)."""
return self.activity_kw.has_key('expand_method_id')
obj = self.getObject(activity_tool)
if obj is None:
return ()
if 'expand_method_id' in self.activity_kw:
return getattr(obj, self.activity_kw['expand_method_id'])()
return obj,
def getObjectCount(self, activity_tool):
if 'expand_method_id' in self.activity_kw:
try:
obj = activity_tool.unrestrictedTraverse(self.object_path)
return len(getattr(obj, self.activity_kw['expand_method_id'])())
except StandardError:
pass
return 1
def changeUser(self, user_name, activity_tool):
"""restore the security context for the calling user."""
......@@ -280,39 +296,21 @@ class Message(BaseMessage):
def __call__(self, activity_tool):
try:
obj = self.getObject(activity_tool)
except KeyError:
exc_info = sys.exc_info()
LOG('CMFActivity', ERROR,
'Message failed in getting an object from the path %r'
% (self.object_path,), error=exc_info)
self.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info,
context=activity_tool)
else:
try:
if obj is not None:
old_security_manager = getSecurityManager()
try:
# Change user if required (TO BE DONE)
# We will change the user only in order to execute this method
self.changeUser(self.user_name, activity_tool)
try:
# XXX: There is no check to see if user is allowed to access
# that method !
method = getattr(obj, self.method_id)
except Exception:
exc_info = sys.exc_info()
LOG('CMFActivity', ERROR,
'Message failed in getting a method %r from an object %r'
% (self.method_id, obj), error=exc_info)
method = None
self.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info,
context=activity_tool)
# XXX: There is no check to see if user is allowed to access
# that method !
method = getattr(obj, self.method_id)
# Store site info
setSite(activity_tool.getParentValue())
if activity_tool.activity_timing_log:
result = activity_timing_method(method, self.args, self.kw)
else:
# Store site info
setSite(activity_tool.getParentValue())
if activity_tool.activity_timing_log:
result = activity_timing_method(method, self.args, self.kw)
else:
result = method(*self.args, **self.kw)
result = method(*self.args, **self.kw)
finally:
setSecurityManager(old_security_manager)
......@@ -322,8 +320,8 @@ class Message(BaseMessage):
activity_tool.unrestrictedTraverse(self.active_process),
result, obj)
self.setExecutionState(MESSAGE_EXECUTED)
except:
self.setExecutionState(MESSAGE_NOT_EXECUTED, context=activity_tool)
except:
self.setExecutionState(MESSAGE_NOT_EXECUTED, context=activity_tool)
def validate(self, activity, activity_tool, check_order_validation=1):
return activity.validate(activity_tool, self,
......@@ -338,9 +336,7 @@ class Message(BaseMessage):
email_from_name = portal.getProperty('email_from_name',
portal.getProperty('email_from_address'))
fail_count = self.line.retry + 1
if self.getExecutionState() == MESSAGE_NOT_EXECUTABLE:
message = "Not executable activity"
elif retry:
if retry:
message = "Pending activity already failed %s times" % fail_count
else:
message = "Activity failed"
......@@ -371,7 +367,7 @@ Named Parameters: %r
def reactivate(self, activity_tool, activity=DEFAULT_ACTIVITY):
# Reactivate the original object.
obj= self.getObject(activity_tool)
obj = activity_tool.unrestrictedTraverse(self.object_path)
old_security_manager = getSecurityManager()
try:
# Change user if required (TO BE DONE)
......@@ -410,7 +406,7 @@ Named Parameters: %r
"""
assert is_executed in (MESSAGE_NOT_EXECUTED, MESSAGE_EXECUTED, MESSAGE_NOT_EXECUTABLE)
self.is_executed = is_executed
if is_executed != MESSAGE_EXECUTED:
if is_executed == MESSAGE_NOT_EXECUTED:
if not exc_info:
exc_info = sys.exc_info()
if self.on_error_callback is not None:
......@@ -1189,21 +1185,11 @@ class ActivityTool (Folder, UniqueObject):
# alternate method is used to segregate objects which cannot be grouped.
alternate_method_id = m.activity_kw.get('alternate_method_id')
try:
obj = m.getObject(self)
except KeyError:
exc_info = sys.exc_info()
LOG('CMFActivity', ERROR,
'Message failed in getting an object from the path %r'
% (m.object_path,), error=exc_info)
m.setExecutionState(MESSAGE_NOT_EXECUTABLE, exc_info, context=self)
continue
try:
if m.hasExpandMethod():
subobject_list = m.getObjectList(self)
else:
subobject_list = (obj,)
object_list = m.getObjectList(self)
if object_list is None:
continue
message_dict[m] = expanded_object_list = []
for subobj in subobject_list:
for subobj in object_list:
if merge_duplicate:
path = subobj.getPath()
if path in path_set:
......
......@@ -26,18 +26,16 @@
#
##############################################################################
import inspect
import unittest
from Products.ERP5Type.tests.utils import LogInterceptor
from Products.ERP5Type.tests.backportUnittest import skip
from Testing import ZopeTestCase
from Products.ERP5Type.tests.ERP5TypeTestCase import ERP5TypeTestCase
from Products.ERP5Type.tests.utils import DummyMailHost
from Products.ERP5Type.TransactionalVariable import getTransactionalVariable
from Products.ERP5Type.Base import Base
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE,\
VALIDATE_ERROR_STATE
from Products.CMFActivity.ActiveObject import INVOKE_ERROR_STATE
from Products.CMFActivity.Activity.Queue import VALIDATION_ERROR_DELAY
from Products.CMFActivity.Activity.SQLDict import SQLDict
import Products.CMFActivity.ActivityTool
......@@ -433,60 +431,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
self.tic()
self.assertEquals(o.getTitle(), 'acb')
def ExpandedMethodWithDeletedSubObject(self, activity):
"""
Do recursiveReindexObject, then delete a
subobject an see if there is only one activity
in the queue
"""
portal = self.getPortal()
organisation_module = self.getOrganisationModule()
if not(organisation_module.hasContent(self.company_id2)):
o2 = organisation_module.newContent(id=self.company_id2)
o1 = portal.organisation._getOb(self.company_id)
o2 = portal.organisation._getOb(self.company_id2)
for o in (o1,o2):
if not(o.hasContent('1')):
o.newContent(portal_type='Email',id='1')
if not(o.hasContent('2')):
o.newContent(portal_type='Email',id='2')
o1.recursiveReindexObject()
o2.recursiveReindexObject()
o1._delOb('2')
self.commit()
portal.portal_activities.distribute()
portal.portal_activities.tic()
self.commit()
message_list = portal.portal_activities.getMessageList()
self.assertEquals(len(message_list),1)
def ExpandedMethodWithDeletedObject(self, activity):
"""
Do recursiveReindexObject, then delete a
subobject an see if there is only one activity
in the queue
"""
portal = self.getPortal()
organisation_module = self.getOrganisationModule()
if not(organisation_module.hasContent(self.company_id2)):
o2 = organisation_module.newContent(id=self.company_id2)
o1 = portal.organisation._getOb(self.company_id)
o2 = portal.organisation._getOb(self.company_id2)
for o in (o1,o2):
if not(o.hasContent('1')):
o.newContent(portal_type='Email',id='1')
if not(o.hasContent('2')):
o.newContent(portal_type='Email',id='2')
o1.recursiveReindexObject()
o2.recursiveReindexObject()
organisation_module._delOb(self.company_id2)
self.commit()
portal.portal_activities.distribute()
portal.portal_activities.tic()
self.commit()
message_list = portal.portal_activities.getMessageList()
self.assertEquals(len(message_list),1)
def TryAfterTag(self, activity):
"""
Ensure the order of an execution by a tag
......@@ -1061,24 +1005,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
# Check if what we did was executed as toto
self.assertEquals(email.getOwnerInfo()['id'],'toto')
def test_57_ExpandedMethodWithDeletedSubObject(self, quiet=0, run=run_all_test):
# Test if after_method_id can be used
if not run: return
if not quiet:
message = '\nTry Expanded Method With Deleted Sub Object'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.ExpandedMethodWithDeletedSubObject('SQLDict')
def test_58_ExpandedMethodWithDeletedObject(self, quiet=0, run=run_all_test):
# Test if after_method_id can be used
if not run: return
if not quiet:
message = '\nTry Expanded Method With Deleted Object'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.ExpandedMethodWithDeletedObject('SQLDict')
def test_59_TryAfterTagWithSQLDict(self, quiet=0, run=run_all_test):
# Test if after_tag can be used
if not run: return
......@@ -1162,8 +1088,7 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
finished = 1
for message in activity_tool.getMessageList():
if message.processing_node not in (INVOKE_ERROR_STATE,
VALIDATE_ERROR_STATE):
if message.processing_node != INVOKE_ERROR_STATE:
finished = 0
activity_tool.timeShift(3 * VALIDATION_ERROR_DELAY)
......@@ -1969,38 +1894,33 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
delattr(Organisation, 'checkMarkerValue')
def TryUserNotificationOnActivityFailure(self, activity):
message_list = self.portal.MailHost._message_list
del message_list[:]
obj = self.portal.organisation_module.newContent(portal_type='Organisation')
self.tic()
obj = self.getPortal().organisation_module.newContent(portal_type='Organisation')
self.tic()
# Use a mutable variable to be able to modify the same instance from
# monkeypatch method.
notification_done = []
from Products.CMFActivity.ActivityTool import Message
def fake_notifyUser(self, *args, **kw):
notification_done.append(True)
original_notifyUser = Message.notifyUser
def failingMethod(self):
raise ValueError, 'This method always fail'
Message.notifyUser = fake_notifyUser
def failingMethod(self): raise ValueError('This method always fails')
Organisation.failingMethod = failingMethod
try:
# MESSAGE_NOT_EXECUTED
obj.activate(activity=activity).failingMethod()
self.commit()
self.assertEqual(len(notification_done), 0)
self.assertFalse(message_list)
self.flushAllActivities(silent=1, loop_size=100)
self.assertEqual(len(notification_done), 1)
# Check there is a traceback in the email notification
sender, recipients, mail = message_list.pop()
self.assertTrue("Module %s, line %s, in failingMethod" % (
__name__, inspect.getsourcelines(failingMethod)[1]) in mail, mail)
self.assertTrue("ValueError:" in mail, mail)
# MESSAGE_NOT_EXECUTABLE
obj.getParentValue()._delObject(obj.getId())
obj.activate(activity=activity).getId()
obj.activate(activity=activity).failingMethod()
self.commit()
self.assertEqual(len(notification_done), 1)
self.flushAllActivities(silent=1, loop_size=100)
self.assertEqual(len(notification_done), 2)
self.assertTrue(obj.hasActivity())
self.tic()
self.assertFalse(obj.hasActivity())
self.assertFalse(message_list)
finally:
Message.notifyUser = original_notifyUser
delattr(Organisation, 'failingMethod')
del Organisation.failingMethod
def test_90_userNotificationOnActivityFailureWithSQLDict(self, quiet=0, run=run_all_test):
"""
......@@ -2579,89 +2499,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
finally:
delattr(Organisation, 'checkAbsoluteUrl')
def CheckMissingActivityContextObject(self, activity):
"""
Check that a message whose context has ben deleted goes to -3
processing_node.
This must happen on first message execution, without any delay.
"""
activity_tool = self.getActivityTool()
container = self.getPortal().organisation_module
organisation = container.newContent(portal_type='Organisation')
self.tic()
organisation.activate(activity=activity).getTitle()
self.commit()
self.assertEqual(len(activity_tool.getMessageList()), 1)
# Here, we delete the subobject using most low-level method, to avoid
# pending activity to be removed.
organisation_id = organisation.id
container._delOb(organisation_id)
del organisation # Avoid keeping a reference to a deleted object.
self.commit()
self.assertEqual(getattr(container, organisation_id, None), None)
self.assertEqual(len(activity_tool.getMessageList()), 1)
activity_tool.distribute()
self.assertEqual([], activity_tool.getMessageList(activity=activity,
processing_node=-3))
activity_tool.tic()
self.assertEqual(1, len(activity_tool.getMessageList(activity=activity,
processing_node=-3)))
def test_109_checkMissingActivityContextObjectSQLDict(self, quiet=0,
run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck missing activity context object (SQLDict)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.CheckMissingActivityContextObject('SQLDict')
def test_110_checkMissingActivityContextObjectSQLQueue(self, quiet=0,
run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck missing activity context object (SQLQueue)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
self.CheckMissingActivityContextObject('SQLQueue')
def test_111_checkMissingActivityContextObjectSQLDict(self, quiet=0,
run=run_all_test):
"""
This is similar to tst 108, but here the object will be missing for an
activity with a group_method_id.
"""
if not run: return
if not quiet:
message = '\nCheck missing activity context object with ' \
'group_method_id (SQLDict)'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
activity_tool = self.getActivityTool()
container = self.getPortalObject().organisation_module
organisation = container.newContent(portal_type='Organisation')
organisation_2 = container.newContent(portal_type='Organisation')
self.tic()
organisation.reindexObject()
organisation_2.reindexObject()
self.commit()
self.assertEqual(len(activity_tool.getMessageList()), 2)
# Here, we delete the subobject using most low-level method, to avoid
# pending activity to be removed.
organisation_id = organisation.id
container._delOb(organisation_id)
del organisation # Avoid keeping a reference to a deleted object.
self.commit()
self.assertEqual(getattr(container, organisation_id, None), None)
self.assertEqual(len(activity_tool.getMessageList()), 2)
activity_tool.distribute()
self.assertEqual([], activity_tool.getMessageList(activity="SQLDict",
processing_node=-3))
activity_tool.tic()
message, = activity_tool.getMessageList()
# The message excuted on "organisation_2" must have succeeded.
self.assertEqual(message.processing_node, -3)
def CheckLocalizerWorks(self, activity):
FROM_STRING = 'Foo'
TO_STRING = 'Bar'
......@@ -2712,69 +2549,6 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
LOG('Testing... ',0,message)
self.CheckLocalizerWorks('SQLDict')
def testMessageContainsFailureTraceback(self, quiet=0, run=run_all_test):
if not run: return
if not quiet:
message = '\nCheck message contains failure traceback'
ZopeTestCase._print(message)
LOG('Testing... ',0,message)
portal = self.getPortalObject()
activity_tool = self.getActivityTool()
def checkMessage(message, exception_type):
self.assertNotEqual(message.getExecutionState(), 1) # 1 == MESSAGE_EXECUTED
self.assertEqual(message.exc_type, exception_type)
self.assertNotEqual(message.traceback, None)
# With Message.__call__
# 1: activity context does not exist when activity is executed
organisation = portal.organisation_module.newContent(portal_type='Organisation')
self.tic()
organisation.activate().getTitle() # This generates the mssage we want to test.
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
portal.organisation_module._delOb(organisation.id)
message(activity_tool)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# 2: activity method does not exist when activity is executed
portal.organisation_module.activate().this_method_does_not_exist()
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
message(activity_tool)
checkMessage(message, AttributeError)
activity_tool.manageCancel(message.object_path, message.method_id)
# With ActivityTool.invokeGroup
# 1: activity context does not exist when activity is executed
organisation = portal.organisation_module.newContent(portal_type='Organisation')
self.tic()
organisation.activate().getTitle() # This generates the mssage we want to test.
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
portal.organisation_module._delOb(organisation.id)
activity_tool.invokeGroup('getTitle', [message], 'SQLDict', True)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# 2: activity method does not exist when activity is executed
portal.organisation_module.activate().this_method_does_not_exist()
self.commit()
message_list = activity_tool.getMessageList()
self.assertEqual(len(message_list), 1)
message = message_list[0]
activity_tool.invokeGroup('this_method_does_not_exist',
[message], 'SQLDict', True)
checkMessage(message, KeyError)
activity_tool.manageCancel(message.object_path, message.method_id)
# Unadressed error paths (in both cases):
# 3: activity commit raises
# 4: activity raises
def test_114_checkSQLQueueActivitySucceedsAfterActivityChangingSkin(self,
quiet=0, run=run_all_test):
if not run: return
......@@ -3620,6 +3394,41 @@ class TestCMFActivity(ERP5TypeTestCase, LogInterceptor):
self.tic()
test()
def test_MessageNonExecutable(self):
message_list = self.portal.MailHost._message_list
del message_list[:]
activity_tool = self.portal.portal_activities
kw = {}
self._catch_log_errors(subsystem='CMFActivity')
try:
for kw['activity'] in 'SQLDict', 'SQLQueue':
for kw['group_method_id'] in '', None:
obj = activity_tool.newActiveProcess()
self.tic()
obj.activate(**kw).getId()
activity_tool._delOb(obj.getId())
obj = activity_tool.newActiveProcess(id=obj.getId(),
is_indexable=False)
self.commit()
self.assertEqual(1, activity_tool.countMessage())
self.flushAllActivities()
sender, recipients, mail = message_list.pop()
self.assertTrue('OID mismatch' in mail, mail)
m, = activity_tool.getMessageList()
self.assertEqual(m.processing_node, INVOKE_ERROR_STATE)
obj.flushActivity()
obj.activate(**kw).getId()
activity_tool._delOb(obj.getId())
self.commit()
self.assertEqual(1, activity_tool.countMessage())
activity_tool.tic()
self.assertTrue('no object found' in self.logged.pop().getMessage())
finally:
self._ignore_log_errors()
self.assertFalse(self.logged)
self.assertFalse(message_list, message_list)
def test_suite():
suite = unittest.TestSuite()
suite.addTest(unittest.makeSuite(TestCMFActivity))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment