From df6e065606b6b5b930754697f3332c4daebb4c9e Mon Sep 17 00:00:00 2001 From: jasquat Date: Fri, 16 Dec 2022 13:23:00 -0500 Subject: [PATCH] Squashed 'SpiffWorkflow/' changes from ffb16867..841bd630 841bd630 Merge pull request #273 from sartography/bugfix/catch-timers-in-event-gateways 103c70d0 hacks to handle timer events like regular events git-subtree-dir: SpiffWorkflow git-subtree-split: 841bd63017bb1d92858456393f144b4e5b23c994 --- SpiffWorkflow/bpmn/parser/event_parsers.py | 7 +-- .../bpmn/specs/events/IntermediateEvent.py | 3 ++ .../bpmn/specs/events/event_definitions.py | 44 ++++++++++++++----- .../bpmn/specs/events/event_types.py | 4 +- .../spiff/serializer/task_spec_converters.py | 6 ++- .../bpmn/events/EventBasedGatewayTest.py | 16 ++++++- 6 files changed, 63 insertions(+), 17 deletions(-) diff --git a/SpiffWorkflow/bpmn/parser/event_parsers.py b/SpiffWorkflow/bpmn/parser/event_parsers.py index d7878369..177d755a 100644 --- a/SpiffWorkflow/bpmn/parser/event_parsers.py +++ b/SpiffWorkflow/bpmn/parser/event_parsers.py @@ -81,17 +81,18 @@ class EventDefinitionParser(TaskParser): """Parse the timerEventDefinition node and return an instance of TimerEventDefinition.""" try: + label = self.node.get('name', self.node.get('id')) time_date = first(self.xpath('.//bpmn:timeDate')) if time_date is not None: - return TimerEventDefinition(self.node.get('name'), time_date.text) + return TimerEventDefinition(label, time_date.text) time_duration = first(self.xpath('.//bpmn:timeDuration')) if time_duration is not None: - return TimerEventDefinition(self.node.get('name'), time_duration.text) + return TimerEventDefinition(label, time_duration.text) time_cycle = first(self.xpath('.//bpmn:timeCycle')) if time_cycle is not None: - return CycleTimerEventDefinition(self.node.get('name'), time_cycle.text) + return CycleTimerEventDefinition(label, time_cycle.text) raise ValidationException("Unknown Time Specification", node=self.node, filename=self.filename) except Exception as e: raise ValidationException("Time Specification Error. " + str(e), node=self.node, filename=self.filename) diff --git a/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py b/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py index e502f86e..798c04c6 100644 --- a/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py +++ b/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py @@ -156,6 +156,9 @@ class EventBasedGateway(CatchingEvent): def spec_type(self): return 'Event Based Gateway' + def _predict_hook(self, my_task): + my_task._sync_children(self.outputs, state=TaskState.MAYBE) + def _on_complete_hook(self, my_task): for child in my_task.children: if not child.task_spec.event_definition.has_fired(child): diff --git a/SpiffWorkflow/bpmn/specs/events/event_definitions.py b/SpiffWorkflow/bpmn/specs/events/event_definitions.py index 6f48801a..fc1cb2f0 100644 --- a/SpiffWorkflow/bpmn/specs/events/event_definitions.py +++ b/SpiffWorkflow/bpmn/specs/events/event_definitions.py @@ -20,6 +20,7 @@ import datetime from copy import deepcopy +from SpiffWorkflow.task import TaskState class EventDefinition(object): """ @@ -307,6 +308,11 @@ class TimerEventDefinition(EventDefinition): The Timer is considered to have fired if the evaluated dateTime expression is before datetime.datetime.now() """ + + if my_task.internal_data.get('event_fired'): + # If we manually send this event, this will be set + return True + dt = my_task.workflow.script_engine.evaluate(my_task, self.dateTime) if isinstance(dt,datetime.timedelta): if my_task._get_internal_data('start_time',None) is not None: @@ -330,6 +336,9 @@ class TimerEventDefinition(EventDefinition): now = datetime.date.today() return now > dt + def __eq__(self, other): + return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label + def serialize(self): retdict = super(TimerEventDefinition, self).serialize() retdict['label'] = self.label @@ -363,6 +372,10 @@ class CycleTimerEventDefinition(EventDefinition): # We will fire this timer whenever a cycle completes # The task itself will manage counting how many times it fires + if my_task.internal_data.get('event_fired'): + # If we manually send this event, this will be set + return True + repeat, delta = my_task.workflow.script_engine.evaluate(my_task, self.cycle_definition) # This is the first time we've entered this event @@ -393,6 +406,9 @@ class CycleTimerEventDefinition(EventDefinition): my_task.internal_data['start_time'] = None super(CycleTimerEventDefinition, self).reset(my_task) + def __eq__(self, other): + return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label + def serialize(self): retdict = super(CycleTimerEventDefinition, self).serialize() retdict['label'] = self.label @@ -411,19 +427,27 @@ class MultipleEventDefinition(EventDefinition): def event_type(self): return 'Multiple' - def catch(self, my_task, event_definition=None): - event_definition.catch(my_task, event_definition) + def has_fired(self, my_task): + + seen_events = my_task.internal_data.get('seen_events', []) + for event in self.event_definitions: + if isinstance(event, (TimerEventDefinition, CycleTimerEventDefinition)): + child = [c for c in my_task.children if c.task_spec.event_definition == event] + child[0].task_spec._update_hook(child[0]) + child[0]._set_state(TaskState.MAYBE) + if event.has_fired(my_task): + seen_events.append(event) + if self.parallel: # Parallel multiple need to match all events - seen_events = my_task.internal_data.get('seen_events', []) + [event_definition] - my_task._set_internal_data(seen_events=seen_events) - if all(event in seen_events for event in self.event_definitions): - my_task._set_internal_data(event_fired=True) - else: - my_task._set_internal_data(event_fired=False) + return all(event in seen_events for event in self.event_definitions) else: - # Otherwise, matching one is sufficient - my_task._set_internal_data(event_fired=True) + return len(seen_events) > 0 + + def catch(self, my_task, event_definition=None): + event_definition.catch(my_task, event_definition) + seen_events = my_task.internal_data.get('seen_events', []) + [event_definition] + my_task._set_internal_data(seen_events=seen_events) def reset(self, my_task): my_task.internal_data.pop('seen_events', None) diff --git a/SpiffWorkflow/bpmn/specs/events/event_types.py b/SpiffWorkflow/bpmn/specs/events/event_types.py index 05b97303..f2aa6d21 100644 --- a/SpiffWorkflow/bpmn/specs/events/event_types.py +++ b/SpiffWorkflow/bpmn/specs/events/event_types.py @@ -54,7 +54,7 @@ class CatchingEvent(Simple, BpmnSpecMixin): my_task._ready() super(CatchingEvent, self)._update_hook(my_task) - def _on_ready(self, my_task): + def _on_ready_hook(self, my_task): # None events don't propogate, so as soon as we're ready, we fire our event if isinstance(self.event_definition, NoneEventDefinition): @@ -63,7 +63,7 @@ class CatchingEvent(Simple, BpmnSpecMixin): # If we have not seen the event we're waiting for, enter the waiting state if not self.event_definition.has_fired(my_task): my_task._set_state(TaskState.WAITING) - super(CatchingEvent, self)._on_ready(my_task) + super(CatchingEvent, self)._on_ready_hook(my_task) def _on_complete_hook(self, my_task): diff --git a/SpiffWorkflow/spiff/serializer/task_spec_converters.py b/SpiffWorkflow/spiff/serializer/task_spec_converters.py index a1c0525c..abf3614b 100644 --- a/SpiffWorkflow/spiff/serializer/task_spec_converters.py +++ b/SpiffWorkflow/spiff/serializer/task_spec_converters.py @@ -3,7 +3,7 @@ from functools import partial from SpiffWorkflow.bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent -from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent +from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, IntermediateCatchEvent, BoundaryEvent, EventBasedGateway from SpiffWorkflow.spiff.specs.none_task import NoneTask from SpiffWorkflow.spiff.specs.manual_task import ManualTask from SpiffWorkflow.spiff.specs.user_task import UserTask @@ -164,3 +164,7 @@ class ReceiveTaskConverter(SpiffEventConverter): dct['prescript'] = spec.prescript dct['postscript'] = spec.postscript return dct + +class EventBasedGatewayConverter(SpiffEventConverter): + def __init__(self, data_converter=None, typename=None): + super().__init__(EventBasedGateway, data_converter, typename) \ No newline at end of file diff --git a/tests/SpiffWorkflow/bpmn/events/EventBasedGatewayTest.py b/tests/SpiffWorkflow/bpmn/events/EventBasedGatewayTest.py index 692f4194..6e549784 100644 --- a/tests/SpiffWorkflow/bpmn/events/EventBasedGatewayTest.py +++ b/tests/SpiffWorkflow/bpmn/events/EventBasedGatewayTest.py @@ -19,7 +19,7 @@ class EventBsedGatewayTest(BpmnWorkflowTestCase): def testEventBasedGatewaySaveRestore(self): self.actual_test(True) - + def actual_test(self, save_restore=False): self.workflow.do_engine_steps() @@ -36,6 +36,20 @@ class EventBsedGatewayTest(BpmnWorkflowTestCase): self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED) self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.CANCELLED) + def testTimeout(self): + + self.workflow.do_engine_steps() + waiting_tasks = self.workflow.get_waiting_tasks() + self.assertEqual(len(waiting_tasks), 1) + timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1] + self.workflow.catch(timer_event) + self.workflow.refresh_waiting_tasks() + self.workflow.do_engine_steps() + self.assertEqual(self.workflow.is_completed(), True) + self.assertEqual(self.workflow.get_tasks_from_spec_name('message_1_event')[0].state, TaskState.CANCELLED) + self.assertEqual(self.workflow.get_tasks_from_spec_name('message_2_event')[0].state, TaskState.CANCELLED) + self.assertEqual(self.workflow.get_tasks_from_spec_name('timer_event')[0].state, TaskState.COMPLETED) + def testMultipleStart(self): spec, subprocess = self.load_workflow_spec('multiple-start-parallel.bpmn', 'main') workflow = BpmnWorkflow(spec)