From ba67d7ad342481231aa5e11508e033f74e86d61f Mon Sep 17 00:00:00 2001 From: Dan Date: Thu, 26 Jan 2023 18:17:35 -0500 Subject: [PATCH] Squashed 'SpiffWorkflow/' changes from 450ef3bcd..98c6294f1 98c6294f1 Merge pull request #287 from sartography/feature/workflow_data_exceptions d40a1da59 Workflow Data Exceptions were broken in the previous error refactor. This assures we are getting good messages from these errors. a156378e1 Merge pull request #286 from sartography/feature/inclusive-gateway-support 7f6e398c2 bypass unnecessary checks in gateway joins ade21a894 revert a few things e1cf75202 Merge branch 'main' into feature/inclusive-gateway-support 15a0a4414 revert change to MultiChoice and handle no defaults in BPMN specs e1469e6bb add support for diverging inclusive gateways 71fd86386 really prevent non-default flows without conditions 924759d9b clean up join specs 7378639d3 Merge pull request #284 from sartography/feature/improved-timer-events dc8d139d2 remove useless method 530f23697 Merge branch 'main' into feature/improved-timer-events 307cca9c5 partially clean up existing gateways 0a344285e clean up task parsers 2cef997d1 add waiting_events method to bpmn workflow 48091c407 serializer migration script and miscellaneous fixes to serialization 61316854b store internal timer data as string/float 389c14c4c add some tests for parsing durations 582bc9482 convert timers to iso 8601 6dfd7ebe9 remove extraneous calls to update 6bd429529 clean up tests d56e9912f remove useless method git-subtree-dir: SpiffWorkflow git-subtree-split: 98c6294f1240aee599cd98bcee58d121cb57b331 --- SpiffWorkflow/bpmn/exceptions.py | 10 +- SpiffWorkflow/bpmn/parser/BpmnParser.py | 38 +- SpiffWorkflow/bpmn/parser/TaskParser.py | 7 +- SpiffWorkflow/bpmn/parser/event_parsers.py | 31 +- SpiffWorkflow/bpmn/parser/task_parsers.py | 67 +- .../bpmn/serializer/bpmn_converters.py | 44 +- .../bpmn/serializer/task_spec_converters.py | 33 +- .../bpmn/serializer/version_migration.py | 96 +- SpiffWorkflow/bpmn/serializer/workflow.py | 2 +- SpiffWorkflow/bpmn/specs/BpmnProcessSpec.py | 16 +- SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py | 6 +- SpiffWorkflow/bpmn/specs/ExclusiveGateway.py | 24 +- SpiffWorkflow/bpmn/specs/InclusiveGateway.py | 98 +- SpiffWorkflow/bpmn/specs/ParallelGateway.py | 5 +- SpiffWorkflow/bpmn/specs/SubWorkflowTask.py | 14 +- SpiffWorkflow/bpmn/specs/UnstructuredJoin.py | 88 +- .../bpmn/specs/events/IntermediateEvent.py | 10 +- SpiffWorkflow/bpmn/specs/events/StartEvent.py | 1 + .../bpmn/specs/events/event_definitions.py | 297 ++-- .../bpmn/specs/events/event_types.py | 8 +- SpiffWorkflow/bpmn/workflow.py | 36 +- SpiffWorkflow/specs/ExclusiveChoice.py | 13 +- SpiffWorkflow/specs/Join.py | 46 +- SpiffWorkflow/specs/MultiChoice.py | 23 +- .../SpiffWorkflow/bpmn/BpmnLoaderForTests.py | 7 +- tests/SpiffWorkflow/bpmn/IOSpecTest.py | 14 +- .../bpmn/InclusiveGatewayTest.py | 39 + .../bpmn/NITimerDurationBoundaryTest.py | 59 +- .../data/MultiInstanceParallelTaskCond.bpmn | 162 +-- .../Parallel-Join-Long-Inclusive.bpmn20.xml | 1282 +++++++++-------- .../Parallel-One-Path-Ends.bpmn20.xml | 384 ++--- ...lel-Then-Exclusive-No-Inclusive.bpmn20.xml | 438 +++--- .../Parallel-Then-Exclusive.bpmn20.xml | 404 +++--- .../Parallel-Through-Same-Task.bpmn20.xml | 402 +++--- tests/SpiffWorkflow/bpmn/data/boundary.bpmn | 116 +- .../bpmn/data/boundary_timer_on_task.bpmn | 48 +- .../bpmn/data/event-gateway.bpmn | 2 +- .../bpmn/data/inclusive_gateway.bpmn | 131 ++ .../bpmn/data/serialization/v1-1.json | 590 ++++++++ .../bpmn/data/timer-cycle-start.bpmn | 4 +- .../SpiffWorkflow/bpmn/data/timer-cycle.bpmn | 44 +- .../bpmn/data/timer-date-start.bpmn | 59 +- .../data/timer-non-interrupt-boundary.bpmn | 125 +- tests/SpiffWorkflow/bpmn/data/timer.bpmn | 58 +- .../bpmn/data/too_many_loops.bpmn | 30 +- .../bpmn/data/too_many_loops_sub_process.bpmn | 4 +- .../bpmn/events/ActionManagementTest.py | 2 +- .../bpmn/events/TimeDurationParseTest.py | 60 + .../bpmn/events/TimerCycleStartTest.py | 25 +- .../bpmn/events/TimerCycleTest.py | 35 +- .../bpmn/events/TimerDateTest.py | 26 +- .../events/TimerDurationBoundaryOnTaskTest.py | 39 +- .../bpmn/events/TimerDurationBoundaryTest.py | 33 +- .../bpmn/events/TimerDurationTest.py | 34 +- .../bpmn/events/TimerIntermediateTest.py | 2 +- .../bpmn/serializer/BaseTestCase.py | 27 + .../BpmnWorkflowSerializerTest.py | 42 +- .../bpmn/serializer/VersionMigrationTest.py | 30 + .../camunda/data/MessageBoundary.bpmn | 202 +-- .../camunda/data/token_trial.bpmn | 82 +- .../data/token_trial_camunda_clash.bpmn | 82 +- .../data/token_trial_parallel_simple.bpmn | 40 +- 62 files changed, 3511 insertions(+), 2665 deletions(-) create mode 100644 tests/SpiffWorkflow/bpmn/InclusiveGatewayTest.py create mode 100644 tests/SpiffWorkflow/bpmn/data/inclusive_gateway.bpmn create mode 100644 tests/SpiffWorkflow/bpmn/data/serialization/v1-1.json create mode 100644 tests/SpiffWorkflow/bpmn/events/TimeDurationParseTest.py create mode 100644 tests/SpiffWorkflow/bpmn/serializer/BaseTestCase.py rename tests/SpiffWorkflow/bpmn/{ => serializer}/BpmnWorkflowSerializerTest.py (80%) create mode 100644 tests/SpiffWorkflow/bpmn/serializer/VersionMigrationTest.py diff --git a/SpiffWorkflow/bpmn/exceptions.py b/SpiffWorkflow/bpmn/exceptions.py index 9bc44d81..df2ddd2c 100644 --- a/SpiffWorkflow/bpmn/exceptions.py +++ b/SpiffWorkflow/bpmn/exceptions.py @@ -1,16 +1,14 @@ -from SpiffWorkflow.exceptions import WorkflowException +from SpiffWorkflow.exceptions import WorkflowTaskException -class WorkflowDataException(WorkflowException): +class WorkflowDataException(WorkflowTaskException): - def __init__(self, task, data_input=None, data_output=None, message=None): + def __init__(self, message, task, data_input=None, data_output=None): """ :param task: the task that generated the error :param data_input: the spec of the input variable (if a data input) :param data_output: the spec of the output variable (if a data output) """ - super().__init__(task.task_spec, message or 'data object error') - self.task = task + super().__init__(message, task) self.data_input = data_input self.data_output = data_output - self.task_trace = self.get_task_trace(task) diff --git a/SpiffWorkflow/bpmn/parser/BpmnParser.py b/SpiffWorkflow/bpmn/parser/BpmnParser.py index 9e2db083..d74c6651 100644 --- a/SpiffWorkflow/bpmn/parser/BpmnParser.py +++ b/SpiffWorkflow/bpmn/parser/BpmnParser.py @@ -43,13 +43,23 @@ from ..specs.UserTask import UserTask from .ProcessParser import ProcessParser from .node_parser import DEFAULT_NSMAP from .util import full_tag, xpath_eval, first -from .task_parsers import (UserTaskParser, NoneTaskParser, ManualTaskParser, - ExclusiveGatewayParser, ParallelGatewayParser, InclusiveGatewayParser, - CallActivityParser, ScriptTaskParser, SubWorkflowParser, - ServiceTaskParser) -from .event_parsers import (EventBasedGatewayParser, StartEventParser, EndEventParser, BoundaryEventParser, - IntermediateCatchEventParser, IntermediateThrowEventParser, - SendTaskParser, ReceiveTaskParser) +from .TaskParser import TaskParser +from .task_parsers import ( + GatewayParser, + ConditionalGatewayParser, + CallActivityParser, + ScriptTaskParser, + SubWorkflowParser, +) +from .event_parsers import ( + EventBasedGatewayParser, + StartEventParser, EndEventParser, + BoundaryEventParser, + IntermediateCatchEventParser, + IntermediateThrowEventParser, + SendTaskParser, + ReceiveTaskParser +) XSD_PATH = os.path.join(os.path.dirname(__file__), 'schema', 'BPMN20.xsd') @@ -94,17 +104,17 @@ class BpmnParser(object): PARSER_CLASSES = { full_tag('startEvent'): (StartEventParser, StartEvent), full_tag('endEvent'): (EndEventParser, EndEvent), - full_tag('userTask'): (UserTaskParser, UserTask), - full_tag('task'): (NoneTaskParser, NoneTask), + full_tag('userTask'): (TaskParser, UserTask), + full_tag('task'): (TaskParser, NoneTask), full_tag('subProcess'): (SubWorkflowParser, CallActivity), - full_tag('manualTask'): (ManualTaskParser, ManualTask), - full_tag('exclusiveGateway'): (ExclusiveGatewayParser, ExclusiveGateway), - full_tag('parallelGateway'): (ParallelGatewayParser, ParallelGateway), - full_tag('inclusiveGateway'): (InclusiveGatewayParser, InclusiveGateway), + full_tag('manualTask'): (TaskParser, ManualTask), + full_tag('exclusiveGateway'): (ConditionalGatewayParser, ExclusiveGateway), + full_tag('parallelGateway'): (GatewayParser, ParallelGateway), + full_tag('inclusiveGateway'): (ConditionalGatewayParser, InclusiveGateway), full_tag('callActivity'): (CallActivityParser, CallActivity), full_tag('transaction'): (SubWorkflowParser, TransactionSubprocess), full_tag('scriptTask'): (ScriptTaskParser, ScriptTask), - full_tag('serviceTask'): (ServiceTaskParser, ServiceTask), + full_tag('serviceTask'): (TaskParser, ServiceTask), full_tag('intermediateCatchEvent'): (IntermediateCatchEventParser, IntermediateCatchEvent), full_tag('intermediateThrowEvent'): (IntermediateThrowEventParser, IntermediateThrowEvent), full_tag('boundaryEvent'): (BoundaryEventParser, BoundaryEvent), diff --git a/SpiffWorkflow/bpmn/parser/TaskParser.py b/SpiffWorkflow/bpmn/parser/TaskParser.py index 5c17f734..cab48777 100644 --- a/SpiffWorkflow/bpmn/parser/TaskParser.py +++ b/SpiffWorkflow/bpmn/parser/TaskParser.py @@ -28,6 +28,7 @@ from ..specs.events.event_definitions import CancelEventDefinition from ..specs.MultiInstanceTask import getDynamicMIClass from ..specs.SubWorkflowTask import CallActivity, TransactionSubprocess, SubWorkflowTask from ..specs.ExclusiveGateway import ExclusiveGateway +from ..specs.InclusiveGateway import InclusiveGateway from ...dmn.specs.BusinessRuleTask import BusinessRuleTask from ...operators import Attrib, PathAttrib from .util import one, first @@ -188,9 +189,9 @@ class TaskParser(NodeParser): children = sorted(children, key=lambda tup: float(tup[0]["y"])) default_outgoing = self.node.get('default') - if not default_outgoing: - if len(children) == 1 or not isinstance(self.task, ExclusiveGateway): - (position, c, target_node, sequence_flow) = children[0] + if len(children) == 1 and isinstance(self.task, (ExclusiveGateway, InclusiveGateway)): + (position, c, target_node, sequence_flow) = children[0] + if self.parse_condition(sequence_flow) is None: default_outgoing = sequence_flow.get('id') for (position, c, target_node, sequence_flow) in children: diff --git a/SpiffWorkflow/bpmn/parser/event_parsers.py b/SpiffWorkflow/bpmn/parser/event_parsers.py index d4099b92..f027a6a9 100644 --- a/SpiffWorkflow/bpmn/parser/event_parsers.py +++ b/SpiffWorkflow/bpmn/parser/event_parsers.py @@ -5,11 +5,19 @@ from SpiffWorkflow.bpmn.specs.events.event_definitions import CorrelationPropert from .ValidationException import ValidationException from .TaskParser import TaskParser from .util import first, one -from ..specs.events.event_definitions import (MultipleEventDefinition, TimerEventDefinition, MessageEventDefinition, - ErrorEventDefinition, EscalationEventDefinition, - SignalEventDefinition, - CancelEventDefinition, CycleTimerEventDefinition, - TerminateEventDefinition, NoneEventDefinition) +from ..specs.events.event_definitions import ( + MultipleEventDefinition, + TimeDateEventDefinition, + DurationTimerEventDefinition, + CycleTimerEventDefinition, + MessageEventDefinition, + ErrorEventDefinition, + EscalationEventDefinition, + SignalEventDefinition, + CancelEventDefinition, + TerminateEventDefinition, + NoneEventDefinition +) CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn' @@ -81,18 +89,16 @@ class EventDefinitionParser(TaskParser): """Parse the timerEventDefinition node and return an instance of TimerEventDefinition.""" try: - label = self.node.get('name', self.node.get('id')) + name = self.node.get('name', self.node.get('id')) time_date = first(self.xpath('.//bpmn:timeDate')) if time_date is not None: - return TimerEventDefinition(label, time_date.text) - + return TimeDateEventDefinition(name, time_date.text) time_duration = first(self.xpath('.//bpmn:timeDuration')) if time_duration is not None: - return TimerEventDefinition(label, time_duration.text) - + return DurationTimerEventDefinition(name, time_duration.text) time_cycle = first(self.xpath('.//bpmn:timeCycle')) if time_cycle is not None: - return CycleTimerEventDefinition(label, time_cycle.text) + return CycleTimerEventDefinition(name, time_cycle.text) raise ValidationException("Unknown Time Specification", node=self.node, file_name=self.filename) except Exception as e: raise ValidationException("Time Specification Error. " + str(e), node=self.node, file_name=self.filename) @@ -170,9 +176,6 @@ class StartEventParser(EventDefinitionParser): event_definition = self.get_event_definition([MESSAGE_EVENT_XPATH, SIGNAL_EVENT_XPATH, TIMER_EVENT_XPATH]) task = self._create_task(event_definition) self.spec.start.connect(task) - if isinstance(event_definition, CycleTimerEventDefinition): - # We are misusing cycle timers, so this is a hack whereby we will revisit ourselves if we fire. - task.connect(task) return task def handles_multiple_outgoing(self): diff --git a/SpiffWorkflow/bpmn/parser/task_parsers.py b/SpiffWorkflow/bpmn/parser/task_parsers.py index b8353765..d26a1849 100644 --- a/SpiffWorkflow/bpmn/parser/task_parsers.py +++ b/SpiffWorkflow/bpmn/parser/task_parsers.py @@ -25,41 +25,19 @@ from .util import one, DEFAULT_NSMAP CAMUNDA_MODEL_NS = 'http://camunda.org/schema/1.0/bpmn' -class UserTaskParser(TaskParser): - - """ - Base class for parsing User Tasks - """ - pass +class GatewayParser(TaskParser): + def handles_multiple_outgoing(self): + return True -class ManualTaskParser(UserTaskParser): - - """ - Base class for parsing Manual Tasks. Currently assumes that Manual Tasks - should be treated the same way as User Tasks. - """ - pass - - -class NoneTaskParser(UserTaskParser): - - """ - Base class for parsing unspecified Tasks. Currently assumes that such Tasks - should be treated the same way as User Tasks. - """ - pass - - -class ExclusiveGatewayParser(TaskParser): +class ConditionalGatewayParser(GatewayParser): """ Parses an Exclusive Gateway, setting up the outgoing conditions appropriately. """ - def connect_outgoing(self, outgoing_task, sequence_flow_node, is_default): if is_default: - super(ExclusiveGatewayParser, self).connect_outgoing(outgoing_task, sequence_flow_node, is_default) + super().connect_outgoing(outgoing_task, sequence_flow_node, is_default) else: cond = self.parse_condition(sequence_flow_node) if cond is None: @@ -69,33 +47,6 @@ class ExclusiveGatewayParser(TaskParser): self.filename) self.task.connect_outgoing_if(cond, outgoing_task) - def handles_multiple_outgoing(self): - return True - - -class ParallelGatewayParser(TaskParser): - - """ - Parses a Parallel Gateway. - """ - - def handles_multiple_outgoing(self): - return True - - -class InclusiveGatewayParser(TaskParser): - - """ - Parses an Inclusive Gateway. - """ - - def handles_multiple_outgoing(self): - """ - At the moment I haven't implemented support for diverging inclusive - gateways - """ - return False - class SubprocessParser: @@ -200,11 +151,3 @@ class ScriptTaskParser(TaskParser): f"Invalid Script Task. No Script Provided. " + str(ae), node=self.node, file_name=self.filename) - -class ServiceTaskParser(TaskParser): - - """ - Parses a ServiceTask node. - """ - pass - diff --git a/SpiffWorkflow/bpmn/serializer/bpmn_converters.py b/SpiffWorkflow/bpmn/serializer/bpmn_converters.py index 5d604fa4..c5e2e5c2 100644 --- a/SpiffWorkflow/bpmn/serializer/bpmn_converters.py +++ b/SpiffWorkflow/bpmn/serializer/bpmn_converters.py @@ -7,10 +7,21 @@ from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnDataSpecification from .dictionary import DictionaryConverter -from ..specs.events.event_definitions import MultipleEventDefinition, SignalEventDefinition, MessageEventDefinition, NoneEventDefinition -from ..specs.events.event_definitions import TimerEventDefinition, CycleTimerEventDefinition, TerminateEventDefinition -from ..specs.events.event_definitions import ErrorEventDefinition, EscalationEventDefinition, CancelEventDefinition -from ..specs.events.event_definitions import CorrelationProperty, NamedEventDefinition +from ..specs.events.event_definitions import ( + NoneEventDefinition, + MultipleEventDefinition, + SignalEventDefinition, + MessageEventDefinition, + CorrelationProperty, + TimeDateEventDefinition, + DurationTimerEventDefinition, + CycleTimerEventDefinition, + ErrorEventDefinition, + EscalationEventDefinition, + CancelEventDefinition, + TerminateEventDefinition, + NamedEventDefinition +) from ..specs.BpmnSpecMixin import BpmnSpecMixin from ...operators import Attrib, PathAttrib @@ -89,9 +100,19 @@ class BpmnTaskSpecConverter(DictionaryConverter): self.data_converter = data_converter self.typename = typename if typename is not None else spec_class.__name__ - event_definitions = [ NoneEventDefinition, CancelEventDefinition, TerminateEventDefinition, - SignalEventDefinition, MessageEventDefinition, ErrorEventDefinition, EscalationEventDefinition, - TimerEventDefinition, CycleTimerEventDefinition , MultipleEventDefinition] + event_definitions = [ + NoneEventDefinition, + CancelEventDefinition, + TerminateEventDefinition, + SignalEventDefinition, + MessageEventDefinition, + ErrorEventDefinition, + EscalationEventDefinition, + TimeDateEventDefinition, + DurationTimerEventDefinition, + CycleTimerEventDefinition, + MultipleEventDefinition + ] for event_definition in event_definitions: self.register( @@ -238,12 +259,9 @@ class BpmnTaskSpecConverter(DictionaryConverter): dct['name'] = event_definition.name if isinstance(event_definition, MessageEventDefinition): dct['correlation_properties'] = [prop.__dict__ for prop in event_definition.correlation_properties] - if isinstance(event_definition, TimerEventDefinition): - dct['label'] = event_definition.label - dct['dateTime'] = event_definition.dateTime - if isinstance(event_definition, CycleTimerEventDefinition): - dct['label'] = event_definition.label - dct['cycle_definition'] = event_definition.cycle_definition + if isinstance(event_definition, (TimeDateEventDefinition, DurationTimerEventDefinition, CycleTimerEventDefinition)): + dct['name'] = event_definition.name + dct['expression'] = event_definition.expression if isinstance(event_definition, ErrorEventDefinition): dct['error_code'] = event_definition.error_code if isinstance(event_definition, EscalationEventDefinition): diff --git a/SpiffWorkflow/bpmn/serializer/task_spec_converters.py b/SpiffWorkflow/bpmn/serializer/task_spec_converters.py index be1d810b..a6bfc829 100644 --- a/SpiffWorkflow/bpmn/serializer/task_spec_converters.py +++ b/SpiffWorkflow/bpmn/serializer/task_spec_converters.py @@ -176,51 +176,54 @@ class TransactionSubprocessTaskConverter(BpmnTaskSpecConverter): return self.task_spec_from_dict(dct) -class ExclusiveGatewayConverter(BpmnTaskSpecConverter): - - def __init__(self, data_converter=None, typename=None): - super().__init__(ExclusiveGateway, data_converter, typename) +class ConditionalGatewayConverter(BpmnTaskSpecConverter): def to_dict(self, spec): dct = self.get_default_attributes(spec) dct.update(self.get_bpmn_attributes(spec)) - dct['default_task_spec'] = spec.default_task_spec dct['cond_task_specs'] = [ self.bpmn_condition_to_dict(cond) for cond in spec.cond_task_specs ] dct['choice'] = spec.choice return dct def from_dict(self, dct): conditions = dct.pop('cond_task_specs') - default_task_spec = dct.pop('default_task_spec') spec = self.task_spec_from_dict(dct) spec.cond_task_specs = [ self.bpmn_condition_from_dict(cond) for cond in conditions ] - spec.default_task_spec = default_task_spec return spec def bpmn_condition_from_dict(self, dct): - return (_BpmnCondition(dct['condition']), dct['task_spec']) + return (_BpmnCondition(dct['condition']) if dct['condition'] is not None else None, dct['task_spec']) def bpmn_condition_to_dict(self, condition): expr, task_spec = condition return { - 'condition': expr.args[0], + 'condition': expr.args[0] if expr is not None else None, 'task_spec': task_spec } -class InclusiveGatewayConverter(BpmnTaskSpecConverter): + +class ExclusiveGatewayConverter(ConditionalGatewayConverter): def __init__(self, data_converter=None, typename=None): - super().__init__(InclusiveGateway, data_converter, typename) + super().__init__(ExclusiveGateway, data_converter, typename) def to_dict(self, spec): - dct = self.get_default_attributes(spec) - dct.update(self.get_bpmn_attributes(spec)) - dct.update(self.get_join_attributes(spec)) + dct = super().to_dict(spec) + dct['default_task_spec'] = spec.default_task_spec return dct def from_dict(self, dct): - return self.task_spec_from_dict(dct) + default_task_spec = dct.pop('default_task_spec') + spec = super().from_dict(dct) + spec.default_task_spec = default_task_spec + return spec + + +class InclusiveGatewayConverter(ConditionalGatewayConverter): + + def __init__(self, data_converter=None, typename=None): + super().__init__(InclusiveGateway, data_converter, typename) class ParallelGatewayConverter(BpmnTaskSpecConverter): diff --git a/SpiffWorkflow/bpmn/serializer/version_migration.py b/SpiffWorkflow/bpmn/serializer/version_migration.py index 1159a5f9..c97b8284 100644 --- a/SpiffWorkflow/bpmn/serializer/version_migration.py +++ b/SpiffWorkflow/bpmn/serializer/version_migration.py @@ -1,4 +1,95 @@ from copy import deepcopy +from datetime import datetime, timedelta + +from SpiffWorkflow.exceptions import WorkflowException +from SpiffWorkflow.task import TaskState +from SpiffWorkflow.bpmn.specs.events.event_definitions import LOCALTZ + +class VersionMigrationError(WorkflowException): + pass + +def version_1_1_to_1_2(old): + """ + Upgrade v1.1 serialization to v1.2. + + Expressions in timer event definitions have been converted from python expressions to + ISO 8601 expressions. + + Cycle timers no longer connect back to themselves. New children are created from a single + tasks rather than reusing previously executed tasks. + + All conditions (including the default) are included in the conditions for gateways. + """ + new = deepcopy(old) + + def td_to_iso(td): + total = td.total_seconds() + v1, seconds = total // 60, total % 60 + v2, minutes = v1 // 60, v1 % 60 + days, hours = v2 // 24, v2 % 60 + return f"P{days:.0f}DT{hours:.0f}H{minutes:.0f}M{seconds}S" + + message = "Unable to convert time specifications for {spec}. This most likely because the values are set during workflow execution." + + has_timer = lambda ts: 'event_definition' in ts and ts['event_definition']['typename'] in [ 'CycleTimerEventDefinition', 'TimerEventDefinition'] + for spec in [ ts for ts in new['spec']['task_specs'].values() if has_timer(ts) ]: + spec['event_definition']['name'] = spec['event_definition'].pop('label') + if spec['event_definition']['typename'] == 'TimerEventDefinition': + expr = spec['event_definition'].pop('dateTime') + try: + dt = eval(expr) + if isinstance(dt, datetime): + spec['event_definition']['expression'] = f"'{dt.isoformat()}'" + spec['event_definition']['typename'] = 'TimeDateEventDefinition' + elif isinstance(dt, timedelta): + spec['event_definition']['expression'] = f"'{td_to_iso(dt)}'" + spec['event_definition']['typename'] = 'DurationTimerEventDefinition' + except: + raise VersionMigrationError(message.format(spec=spec['name'])) + + if spec['event_definition']['typename'] == 'CycleTimerEventDefinition': + + tasks = [ t for t in new['tasks'].values() if t['task_spec'] == spec['name'] ] + task = tasks[0] if len(tasks) > 0 else None + + expr = spec['event_definition'].pop('cycle_definition') + try: + repeat, duration = eval(expr) + spec['event_definition']['expression'] = f"'R{repeat}/{td_to_iso(duration)}'" + if task is not None: + cycles_complete = task['data'].pop('repeat_count', 0) + start_time = task['internal_data'].pop('start_time', None) + if start_time is not None: + dt = datetime.fromisoformat(start_time) + task['internal_data']['event_value'] = { + 'cycles': repeat - cycles_complete, + 'next': datetime.combine(dt.date(), dt.time(), LOCALTZ).isoformat(), + 'duration': duration.total_seconds(), + } + except: + raise VersionMigrationError(message.format(spec=spec['name'])) + + if spec['typename'] == 'StartEvent': + spec['outputs'].remove(spec['name']) + if task is not None: + children = [ new['tasks'][c] for c in task['children'] ] + # Formerly cycles were handled by looping back and reusing the tasks so this removes the extra tasks + remove = [ c for c in children if c['task_spec'] == task['task_spec']][0] + for task_id in remove['children']: + child = new['tasks'][task_id] + if child['task_spec'].startswith('return') or child['state'] != TaskState.COMPLETED: + new['tasks'].pop(task_id) + else: + task['children'].append(task_id) + task['children'].remove(remove['id']) + new['tasks'].pop(remove['id']) + + for spec in [ts for ts in new['spec']['task_specs'].values() if ts['typename'] == 'ExclusiveGateway']: + if (None, spec['default_task_spec']) not in spec['cond_task_specs']: + spec['cond_task_specs'].append((None, spec['default_task_spec'])) + + new['VERSION'] = "1.2" + return new def version_1_0_to_1_1(old): """ @@ -47,8 +138,11 @@ def version_1_0_to_1_1(old): task['children'] = [ c for c in task['children'] if c in sp['tasks'] ] new['subprocesses'] = subprocesses - return new + new['VERSION'] = "1.1" + return version_1_1_to_1_2(new) + MIGRATIONS = { '1.0': version_1_0_to_1_1, + '1.1': version_1_1_to_1_2, } diff --git a/SpiffWorkflow/bpmn/serializer/workflow.py b/SpiffWorkflow/bpmn/serializer/workflow.py index 8449b300..ffbe8f3c 100644 --- a/SpiffWorkflow/bpmn/serializer/workflow.py +++ b/SpiffWorkflow/bpmn/serializer/workflow.py @@ -64,7 +64,7 @@ class BpmnWorkflowSerializer: # This is the default version set on the workflow, it can be overwritten # using the configure_workflow_spec_converter. - VERSION = "1.1" + VERSION = "1.2" VERSION_KEY = "serializer_version" DEFAULT_JSON_ENCODER_CLS = None DEFAULT_JSON_DECODER_CLS = None diff --git a/SpiffWorkflow/bpmn/specs/BpmnProcessSpec.py b/SpiffWorkflow/bpmn/specs/BpmnProcessSpec.py index 108370d7..d6b17c44 100644 --- a/SpiffWorkflow/bpmn/specs/BpmnProcessSpec.py +++ b/SpiffWorkflow/bpmn/specs/BpmnProcessSpec.py @@ -72,15 +72,17 @@ class BpmnDataSpecification: def get(self, my_task): """Copy a value form the workflow data to the task data.""" if self.name not in my_task.workflow.data: - message = f"Workflow variable {self.name} not found" - raise WorkflowDataException(my_task, data_input=self, message=message) + message = f"Data object '{self.name}' " \ + f"does not exist and can not be read." + raise WorkflowDataException(message, my_task, data_input=self) my_task.data[self.name] = deepcopy(my_task.workflow.data[self.name]) def set(self, my_task): """Copy a value from the task data to the workflow data""" if self.name not in my_task.data: - message = f"Task variable {self.name} not found" - raise WorkflowDataException(my_task, data_output=self, message=message) + message = f"A Data Object '{self.name}' " \ + f"could not be set, it does not exist in the task data" + raise WorkflowDataException(message, my_task, data_output=self) my_task.workflow.data[self.name] = deepcopy(my_task.data[self.name]) del my_task.data[self.name] data_log.info(f'Set workflow variable {self.name}', extra=my_task.log_info()) @@ -88,12 +90,12 @@ class BpmnDataSpecification: def copy(self, source, destination, data_input=False, data_output=False): """Copy a value from one task to another.""" if self.name not in source.data: - message = f"Unable to copy {self.name}" + message = f"'{self.name}' was not found in the task data" raise WorkflowDataException( - source, + message, + source, data_input=self if data_input else None, data_output=self if data_output else None, - message=message ) destination.data[self.name] = deepcopy(source.data[self.name]) diff --git a/SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py b/SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py index 444863ff..14c90665 100644 --- a/SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py +++ b/SpiffWorkflow/bpmn/specs/BpmnSpecMixin.py @@ -32,7 +32,6 @@ class _BpmnCondition(Operator): return task.workflow.script_engine.evaluate(task, self.args[0]) - class BpmnSpecMixin(TaskSpec): """ All BPMN spec classes should mix this superclass in. It adds a number of @@ -70,7 +69,10 @@ class BpmnSpecMixin(TaskSpec): evaluates to true. This should only be called if the task has a connect_if method (e.g. ExclusiveGateway). """ - self.connect_if(_BpmnCondition(condition), taskspec) + if condition is None: + self.connect(taskspec) + else: + self.connect_if(_BpmnCondition(condition), taskspec) def _on_ready_hook(self, my_task): super()._on_ready_hook(my_task) diff --git a/SpiffWorkflow/bpmn/specs/ExclusiveGateway.py b/SpiffWorkflow/bpmn/specs/ExclusiveGateway.py index 0dc4500d..46ceb909 100644 --- a/SpiffWorkflow/bpmn/specs/ExclusiveGateway.py +++ b/SpiffWorkflow/bpmn/specs/ExclusiveGateway.py @@ -16,38 +16,20 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from ...exceptions import WorkflowException from .BpmnSpecMixin import BpmnSpecMixin -from ...specs.base import TaskSpec from ...specs.ExclusiveChoice import ExclusiveChoice +from ...specs.MultiChoice import MultiChoice class ExclusiveGateway(ExclusiveChoice, BpmnSpecMixin): - """ Task Spec for a bpmn:exclusiveGateway node. """ def test(self): - """ - Checks whether all required attributes are set. Throws an exception - if an error was detected. - """ - # This has been overridden to allow a single default flow out (without a - # condition) - useful for the converging type - TaskSpec.test(self) -# if len(self.cond_task_specs) < 1: -# raise WorkflowException(self, 'At least one output required.') - for condition, name in self.cond_task_specs: - if name is None: - raise WorkflowException('Condition with no task spec.', task_spec=self) - task_spec = self._wf_spec.get_task_spec_from_name(name) - if task_spec is None: - msg = 'Condition leads to non-existent task ' + repr(name) - raise WorkflowException(msg, task_spec=self) - if condition is None: - continue + # Bypass the check for no default output -- this is not required in BPMN + MultiChoice.test(self) @property def spec_type(self): diff --git a/SpiffWorkflow/bpmn/specs/InclusiveGateway.py b/SpiffWorkflow/bpmn/specs/InclusiveGateway.py index 21c1cfe1..4bb5eca4 100644 --- a/SpiffWorkflow/bpmn/specs/InclusiveGateway.py +++ b/SpiffWorkflow/bpmn/specs/InclusiveGateway.py @@ -16,13 +16,14 @@ # License along with this library; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from collections import deque +from SpiffWorkflow.exceptions import WorkflowTaskException from ...task import TaskState from .UnstructuredJoin import UnstructuredJoin +from ...specs.MultiChoice import MultiChoice -class InclusiveGateway(UnstructuredJoin): +class InclusiveGateway(MultiChoice, UnstructuredJoin): """ Task Spec for a bpmn:parallelGateway node. From the specification of BPMN (http://www.omg.org/spec/BPMN/2.0/PDF - document number:formal/2011-01-03): @@ -62,58 +63,59 @@ class InclusiveGateway(UnstructuredJoin): specified, the Inclusive Gateway throws an exception. """ - @property - def spec_type(self): - return 'Inclusive Gateway' + def test(self): + MultiChoice.test(self) + UnstructuredJoin.test(self) def _check_threshold_unstructured(self, my_task, force=False): - # Look at the tree to find all ready and waiting tasks (excluding ones - # that are our completed inputs). - tasks = [] - for task in my_task.workflow.get_tasks(TaskState.READY | TaskState.WAITING): - if task.thread_id != my_task.thread_id: - continue - if task.workflow != my_task.workflow: - continue - if task.task_spec == my_task.task_spec: - continue - tasks.append(task) + completed_inputs, waiting_tasks = self._get_inputs_with_tokens(my_task) + uncompleted_inputs = [i for i in self.inputs if i not in completed_inputs] - inputs_with_tokens, waiting_tasks = self._get_inputs_with_tokens( - my_task) - inputs_without_tokens = [ - i for i in self.inputs if i not in inputs_with_tokens] + # We only have to complete a task once for it to count, even if's on multiple paths + for task in waiting_tasks: + if task.task_spec in completed_inputs: + waiting_tasks.remove(task) - waiting_tasks = [] - for task in tasks: - if (self._has_directed_path_to( - task, self, - without_using_sequence_flow_from=inputs_with_tokens) and - not self._has_directed_path_to( - task, self, - without_using_sequence_flow_from=inputs_without_tokens)): - waiting_tasks.append(task) + if force: + # If force is true, complete the task + complete = True + elif len(waiting_tasks) > 0: + # If we have waiting tasks, we're obviously not done + complete = False + else: + # Handle the case where there are paths from active tasks that must go through uncompleted inputs + tasks = my_task.workflow.get_tasks(TaskState.READY | TaskState.WAITING, workflow=my_task.workflow) + sources = [t.task_spec for t in tasks] - return force or len(waiting_tasks) == 0, waiting_tasks + # This will go back through a task spec's ancestors and return the source, if applicable + def check(spec): + for parent in spec.inputs: + return parent if parent in sources else check(parent) - def _has_directed_path_to(self, task, task_spec, - without_using_sequence_flow_from=None): - q = deque() - done = set() + # If we can get to a completed input from this task, we don't have to wait for it + for spec in completed_inputs: + source = check(spec) + if source is not None: + sources.remove(source) - without_using_sequence_flow_from = set( - without_using_sequence_flow_from or []) + # Now check the rest of the uncompleted inputs and see if they can be reached from any of the remaining tasks + unfinished_paths = [] + for spec in uncompleted_inputs: + if check(spec) is not None: + unfinished_paths.append(spec) + break - q.append(task.task_spec) - while q: - n = q.popleft() - if n == task_spec: - return True - for child in n.outputs: - if child not in done and not ( - n in without_using_sequence_flow_from and - child == task_spec): - done.add(child) - q.append(child) - return False + complete = len(unfinished_paths) == 0 + + return complete, waiting_tasks + + def _on_complete_hook(self, my_task): + outputs = self._get_matching_outputs(my_task) + if len(outputs) == 0: + raise WorkflowTaskException(f'No conditions satisfied on gateway', task=my_task) + my_task._sync_children(outputs, TaskState.FUTURE) + + @property + def spec_type(self): + return 'Inclusive Gateway' \ No newline at end of file diff --git a/SpiffWorkflow/bpmn/specs/ParallelGateway.py b/SpiffWorkflow/bpmn/specs/ParallelGateway.py index 3ade2ecc..bf2f4c88 100644 --- a/SpiffWorkflow/bpmn/specs/ParallelGateway.py +++ b/SpiffWorkflow/bpmn/specs/ParallelGateway.py @@ -42,10 +42,7 @@ class ParallelGateway(UnstructuredJoin): def _check_threshold_unstructured(self, my_task, force=False): completed_inputs, waiting_tasks = self._get_inputs_with_tokens(my_task) - - # If the threshold was reached, get ready to fire. - return (force or len(completed_inputs) >= len(self.inputs), - waiting_tasks) + return force or len(completed_inputs) >= len(self.inputs), waiting_tasks @property def spec_type(self): diff --git a/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py b/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py index 4232ec07..cbfbf502 100644 --- a/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py +++ b/SpiffWorkflow/bpmn/specs/SubWorkflowTask.py @@ -3,6 +3,7 @@ from copy import deepcopy from SpiffWorkflow.task import TaskState from .BpmnSpecMixin import BpmnSpecMixin +from ..exceptions import WorkflowDataException from ...specs.base import TaskSpec @@ -51,7 +52,11 @@ class SubWorkflowTask(BpmnSpecMixin): end = subworkflow.get_tasks_from_spec_name('End', workflow=subworkflow) # Otherwise only copy data with the specified names for var in subworkflow.spec.data_outputs: - var.copy(end[0], my_task, data_output=True) + try: + var.copy(end[0], my_task, data_output=True) + except WorkflowDataException as wde: + wde.add_note("A Data Output was not provided as promised.") + raise wde my_task._set_state(TaskState.READY) @@ -83,8 +88,11 @@ class SubWorkflowTask(BpmnSpecMixin): else: # Otherwise copy only task data with the specified names for var in subworkflow.spec.data_inputs: - var.copy(my_task, start[0], data_input=True) - + try: + var.copy(my_task, start[0], data_input=True) + except WorkflowDataException as wde: + wde.add_note("You are missing a required Data Input for a call activity.") + raise wde for child in subworkflow.task_tree.children: child.task_spec._update(child) diff --git a/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py b/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py index ba739215..8801002b 100644 --- a/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py +++ b/SpiffWorkflow/bpmn/specs/UnstructuredJoin.py @@ -17,75 +17,34 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from ...exceptions import WorkflowException - from ...task import TaskState from .BpmnSpecMixin import BpmnSpecMixin from ...specs.Join import Join - class UnstructuredJoin(Join, BpmnSpecMixin): """ A helper subclass of Join that makes it work in a slightly friendlier way for the BPMN style threading """ - - def _check_threshold_unstructured(self, my_task, force=False): - raise NotImplementedError("Please implement this in the subclass") - def _get_inputs_with_tokens(self, my_task): # Look at the tree to find all places where this task is used. - tasks = [] - for task in my_task.workflow.task_tree: - if task.thread_id != my_task.thread_id: - continue - if task.workflow != my_task.workflow: - continue - if task.task_spec != self: - continue - if task._is_finished(): - continue - tasks.append(task) + tasks = [ t for t in my_task.workflow.get_tasks_from_spec_name(self.name) if t.workflow == my_task.workflow ] - # Look up which tasks have parent's completed. + # Look up which tasks have parents completed. waiting_tasks = [] completed_inputs = set() for task in tasks: - if task.parent._has_state(TaskState.COMPLETED) and ( - task._has_state(TaskState.WAITING) or task == my_task): - if task.parent.task_spec in completed_inputs: - raise(WorkflowException - ("Unsupported looping behaviour: two threads waiting" - " on the same sequence flow.", task_spec=self)) + if task.parent.state == TaskState.COMPLETED: completed_inputs.add(task.parent.task_spec) - else: + # Ignore predicted tasks; we don't care about anything not definite + elif task.parent._has_state(TaskState.READY | TaskState.FUTURE | TaskState.WAITING): waiting_tasks.append(task.parent) return completed_inputs, waiting_tasks def _do_join(self, my_task): - # Copied from Join parent class - # This has some minor changes - - # One Join spec may have multiple corresponding Task objects:: - # - # - Due to the MultiInstance pattern. - # - Due to the ThreadSplit pattern. - # - # When using the MultiInstance pattern, we want to join across - # the resulting task instances. When using the ThreadSplit - # pattern, we only join within the same thread. (Both patterns - # may also be mixed.) - # - # We are looking for all task instances that must be joined. - # We limit our search by starting at the split point. - if self.split_task: - split_task = my_task.workflow.get_task_spec_from_name( - self.split_task) - split_task = my_task._find_ancestor(split_task) - else: - split_task = my_task.workflow.task_tree + split_task = self._get_split_task(my_task) # Identify all corresponding task instances within the thread. # Also remember which of those instances was most recently changed, @@ -98,35 +57,25 @@ class UnstructuredJoin(Join, BpmnSpecMixin): # Ignore tasks from other threads. if task.thread_id != my_task.thread_id: continue - # Ignore tasks from other subprocesses: - if task.workflow != my_task.workflow: - continue - # Ignore my outgoing branches. - if task._is_descendant_of(my_task): + if self.split_task and task._is_descendant_of(my_task): continue - # Ignore completed tasks (this is for loop handling) - if task._is_finished(): - continue - # For an inclusive join, this can happen - it's a future join if not task.parent._is_finished(): continue - # We have found a matching instance. thread_tasks.append(task) - # Check whether the state of the instance was recently - # changed. + # Check whether the state of the instance was recently changed. changed = task.parent.last_state_change - if last_changed is None\ - or changed > last_changed.parent.last_state_change: + if last_changed is None or changed > last_changed.parent.last_state_change: last_changed = task # Update data from all the same thread tasks. thread_tasks.sort(key=lambda t: t.parent.last_state_change) + collected_data = {} for task in thread_tasks: - self.data.update(task.data) + collected_data.update(task.data) # Mark the identified task instances as COMPLETED. The exception # is the most recently changed task, for which we assume READY. @@ -135,26 +84,13 @@ class UnstructuredJoin(Join, BpmnSpecMixin): # (re)built underneath the node. for task in thread_tasks: if task == last_changed: - task.data.update(self.data) + task.data.update(collected_data) self.entered_event.emit(my_task.workflow, my_task) task._ready() else: task._set_state(TaskState.COMPLETED) task._drop_children() - - def _update_hook(self, my_task): - - if not my_task.parent._is_finished(): - return - - target_state = getattr(my_task, '_bpmn_load_target_state', None) - if target_state == TaskState.WAITING: - my_task._set_state(TaskState.WAITING) - return - - super(UnstructuredJoin, self)._update_hook(my_task) - def task_should_set_children_future(self, my_task): return True diff --git a/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py b/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py index fd3c1560..09e980cf 100644 --- a/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py +++ b/SpiffWorkflow/bpmn/specs/events/IntermediateEvent.py @@ -74,12 +74,10 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin): for child in my_task.children: if isinstance(child.task_spec, BoundaryEvent): child.task_spec.event_definition.reset(child) - child._set_state(TaskState.WAITING) def _child_complete_hook(self, child_task): - # If the main child completes, or a cancelling event occurs, cancel any - # unfinished children + # If the main child completes, or a cancelling event occurs, cancel any unfinished children if child_task.task_spec == self.main_child_task_spec or child_task.task_spec.cancel_activity: for sibling in child_task.parent.children: if sibling == child_task: @@ -89,11 +87,6 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin): for t in child_task.workflow._get_waiting_tasks(): t.task_spec._update(t) - # If our event is a cycle timer, we need to set it back to waiting so it can fire again - elif isinstance(child_task.task_spec.event_definition, CycleTimerEventDefinition): - child_task._set_state(TaskState.WAITING) - child_task.task_spec._update_hook(child_task) - def _predict_hook(self, my_task): # Events attached to the main task might occur @@ -105,7 +98,6 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin): child._set_state(state) - class BoundaryEvent(CatchingEvent): """Task Spec for a bpmn:boundaryEvent node.""" diff --git a/SpiffWorkflow/bpmn/specs/events/StartEvent.py b/SpiffWorkflow/bpmn/specs/events/StartEvent.py index abd0973e..e3b1a7cf 100644 --- a/SpiffWorkflow/bpmn/specs/events/StartEvent.py +++ b/SpiffWorkflow/bpmn/specs/events/StartEvent.py @@ -40,3 +40,4 @@ class StartEvent(CatchingEvent): my_task._set_state(TaskState.WAITING) super(StartEvent, self).catch(my_task, event_definition) + diff --git a/SpiffWorkflow/bpmn/specs/events/event_definitions.py b/SpiffWorkflow/bpmn/specs/events/event_definitions.py index 3a73690e..3d1192de 100644 --- a/SpiffWorkflow/bpmn/specs/events/event_definitions.py +++ b/SpiffWorkflow/bpmn/specs/events/event_definitions.py @@ -17,11 +17,17 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -import datetime +import re +from datetime import datetime, timedelta, timezone +from calendar import monthrange +from time import timezone as tzoffset from copy import deepcopy from SpiffWorkflow.task import TaskState +LOCALTZ = timezone(timedelta(seconds=-1 * tzoffset)) + + class EventDefinition(object): """ This is the base class for Event Definitions. It implements the default throw/catch @@ -34,10 +40,6 @@ class EventDefinition(object): and external flags. Default catch behavior is to set the event to fired """ - - # Format to use for specifying dates for time based events - TIME_FORMAT = '%Y-%m-%d %H:%M:%S.%f' - def __init__(self): # Ideally I'd mke these parameters, but I don't want to them to be parameters # for any subclasses (as they are based on event type, not user choice) and @@ -251,129 +253,218 @@ class TerminateEventDefinition(EventDefinition): def event_type(self): return 'Terminate' -class TimerEventDefinition(EventDefinition): - """ - The TimerEventDefinition is the implementation of event definition used for - Catching Timer Events (Timer events aren't thrown). - """ - def __init__(self, label, dateTime): +class TimerEventDefinition(EventDefinition): + + def __init__(self, name, expression): """ Constructor. - :param label: The label of the event. Used for the description. + :param name: The description of the timer. - :param dateTime: The dateTime expression for the expiry time. This is - passed to the Script Engine and must evaluate to a datetime (in the case of - a time-date event) or a timedelta (in the case of a duration event). + :param expression: An ISO 8601 datetime or interval expression. """ - super(TimerEventDefinition, self).__init__() - self.label = label - self.dateTime = dateTime + super().__init__() + self.name = name + self.expression = expression + + @staticmethod + def get_datetime(expression): + dt = datetime.fromisoformat(expression) + if dt.tzinfo is None: + dt = datetime.combine(dt.date(), dt.time(), LOCALTZ) + return dt.astimezone(timezone.utc) + + @staticmethod + def get_timedelta_from_start(parsed_duration, start=None): + + start = start or datetime.now(timezone.utc) + years, months, days = parsed_duration.pop('years', 0), parsed_duration.pop('months', 0), parsed_duration.pop('days', 0) + months += years * 12 + + for idx in range(int(months)): + year, month = start.year + idx // 12, start.month + idx % 12 + days += monthrange(year, month)[1] + + year, month = start.year + months // 12, start.month + months % 12 + days += (months - int(months)) * monthrange(year, month)[1] + parsed_duration['days'] = days + return timedelta(**parsed_duration) + + @staticmethod + def get_timedelta_from_end(parsed_duration, end): + + years, months, days = parsed_duration.pop('years', 0), parsed_duration.pop('months', 0), parsed_duration.pop('days', 0) + months += years * 12 + + for idx in range(1, int(months) + 1): + year = end.year - (1 + (idx - end.month) // 12) + month = 1 + (end.month - idx - 1) % 12 + days += monthrange(year, month)[1] + + days += (months - int(months)) * monthrange( + end.year - (1 + (int(months)- end.month) // 12), + 1 + (end.month - months - 1) % 12)[1] + parsed_duration['days'] = days + return timedelta(**parsed_duration) + + @staticmethod + def parse_iso_duration(expression): + + # Based on https://en.wikipedia.org/wiki/ISO_8601#Time_intervals + parsed, expr_t, current = {}, False, expression.lower().strip('p').replace(',', '.') + for designator in ['years', 'months', 'weeks', 'days', 't', 'hours', 'minutes', 'seconds']: + value = current.split(designator[0], 1) + if len(value) == 2: + duration, remainder = value + if duration.isdigit(): + parsed[designator] = int(duration) + elif duration.replace('.', '').isdigit() and not remainder: + parsed[designator] = float(duration) + if designator in parsed or designator == 't': + current = remainder + if designator == 't': + expr_t = True + + date_specs, time_specs = ['years', 'months', 'days'], ['hours', 'minutes', 'seconds'] + parsed_t = len([d for d in parsed if d in time_specs]) > 0 + + if len(current) or parsed_t != expr_t or ('weeks' in parsed and any(v for v in parsed if v in date_specs)): + raise Exception('Invalid duration') + # The actual timedelta will have to be computed based on a start or end date, to account for + # months lengths, leap days, etc. This returns a dict of the parsed elements + return parsed + + @staticmethod + def parse_iso_week(expression): + # https://en.wikipedia.org/wiki/ISO_8601#Week_dates + m = re.match('(\d{4})W(\d{2})(\d)(T.+)?', expression.upper().replace('-', '')) + year, month, day, ts = m.groups() + ds = datetime.fromisocalendar(int(year), int(month), int(day)).strftime('%Y-%m-%d') + return TimerEventDefinition.get_datetime(ds + (ts or '')) + + @staticmethod + def parse_time_or_duration(expression): + if expression.upper().startswith('P'): + return TimerEventDefinition.parse_iso_duration(expression) + elif 'W' in expression.upper(): + return TimerEventDefinition.parse_iso_week(expression) + else: + return TimerEventDefinition.get_datetime(expression) + + @staticmethod + def parse_iso_recurring_interval(expression): + components = expression.upper().replace('--', '/').strip('R').split('/') + cycles = int(components[0]) if components[0] else -1 + start_or_duration = TimerEventDefinition.parse_time_or_duration(components[1]) + if len(components) == 3: + end_or_duration = TimerEventDefinition.parse_time_or_duration(components[2]) + else: + end_or_duration = None + + if isinstance(start_or_duration, datetime): + # Start time + interval duration + start = start_or_duration + duration = TimerEventDefinition.get_timedelta_from_start(end_or_duration, start_or_duration) + elif isinstance(end_or_duration, datetime): + # End time + interval duration + duration = TimerEventDefinition.get_timedelta_from_end(start_or_duration, end_or_duration) + start = end_or_duration - duration + elif end_or_duration is None: + # Just an interval duration, assume a start time of now + start = datetime.now(timezone.utc) + duration = TimeDateEventDefinition.get_timedelta_from_start(start_or_duration, start) + else: + raise Exception("Invalid recurring interval") + return cycles, start, duration + + def __eq__(self, other): + return self.__class__.__name__ == other.__class__.__name__ and self.name == other.name + + +class TimeDateEventDefinition(TimerEventDefinition): + """A Timer event represented by a specific date/time.""" @property def event_type(self): - return 'Timer' + return 'Time Date Timer' def has_fired(self, my_task): - """ - The Timer is considered to have fired if the evaluated dateTime - expression is before datetime.datetime.now() - """ + event_value = my_task._get_internal_data('event_value') + if event_value is None: + event_value = my_task.workflow.script_engine.evaluate(my_task, self.expression) + my_task._set_internal_data(event_value=event_value) + if TimerEventDefinition.parse_time_or_duration(event_value) < datetime.now(timezone.utc): + my_task._set_internal_data(event_fired=True) + return my_task._get_internal_data('event_fired', False) - 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: - start_time = datetime.datetime.strptime(my_task._get_internal_data('start_time',None), self.TIME_FORMAT) - elapsed = datetime.datetime.now() - start_time - return elapsed > dt - else: - my_task.internal_data['start_time'] = datetime.datetime.now().strftime(self.TIME_FORMAT) - return False - - if dt is None: - return False - if isinstance(dt, datetime.datetime): - if dt.tzinfo: - tz = dt.tzinfo - now = tz.fromutc(datetime.datetime.utcnow().replace(tzinfo=tz)) - else: - now = datetime.datetime.now() - else: - # assume type is a date, not datetime - now = datetime.date.today() - return now > dt - - def __eq__(self, other): - return self.__class__.__name__ == other.__class__.__name__ and self.label == other.label + def timer_value(self, my_task): + return my_task._get_internal_data('event_value') -class CycleTimerEventDefinition(EventDefinition): - """ - The TimerEventDefinition is the implementation of event definition used for - Catching Timer Events (Timer events aren't thrown). +class DurationTimerEventDefinition(TimerEventDefinition): + """A timer event represented by a duration""" - The cycle definition should evaluate to a tuple of - (n repetitions, repetition duration) - """ - def __init__(self, label, cycle_definition): + @property + def event_type(self): + return 'Duration Timer' - super(CycleTimerEventDefinition, self).__init__() - self.label = label - # The way we're using cycle timers doesn't really align with how the BPMN spec - # describes is (the example of "every monday at 9am") - # I am not sure why this isn't a subprocess with a repeat count that starts - # with a duration timer - self.cycle_definition = cycle_definition + def has_fired(self, my_task): + event_value = my_task._get_internal_data("event_value") + if event_value is None: + expression = my_task.workflow.script_engine.evaluate(my_task, self.expression) + parsed_duration = TimerEventDefinition.parse_iso_duration(expression) + event_value = (datetime.now(timezone.utc) + TimerEventDefinition.get_timedelta_from_start(parsed_duration)).isoformat() + my_task._set_internal_data(event_value=event_value) + if TimerEventDefinition.get_datetime(event_value) < datetime.now(timezone.utc): + my_task._set_internal_data(event_fired=True) + return my_task._get_internal_data('event_fired', False) + + def timer_value(self, my_task): + return my_task._get_internal_data("event_value") + + +class CycleTimerEventDefinition(TimerEventDefinition): @property def event_type(self): return 'Cycle Timer' def has_fired(self, my_task): - # 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 + if not my_task._get_internal_data('event_fired'): + # Only check for the next cycle when the event has not fired to prevent cycles from being skipped. + event_value = my_task._get_internal_data('event_value') + if event_value is None: + expression = my_task.workflow.script_engine.evaluate(my_task, self.expression) + cycles, start, duration = TimerEventDefinition.parse_iso_recurring_interval(expression) + event_value = {'cycles': cycles, 'next': start.isoformat(), 'duration': duration.total_seconds()} + + if event_value['cycles'] > 0: + next_event = datetime.fromisoformat(event_value['next']) + if next_event < datetime.now(timezone.utc): + my_task._set_internal_data(event_fired=True) + event_value['next'] = (next_event + timedelta(seconds=event_value['duration'])).isoformat() + + my_task._set_internal_data(event_value=event_value) + + return my_task._get_internal_data('event_fired', False) + + def timer_value(self, my_task): + event_value = my_task._get_internal_data('event_value') + if event_value is not None and event_value['cycles'] > 0: + return event_value['next'] + + def complete(self, my_task): + event_value = my_task._get_internal_data('event_value') + if event_value is not None and event_value['cycles'] == 0: + my_task.internal_data.pop('event_value') 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 - if my_task.internal_data.get('repeat') is None: - my_task.internal_data['repeat'] = repeat - if my_task.get_data('repeat_count') is None: - # This is now a looping task, and if we use internal data, the repeat count won't persist - my_task.set_data(repeat_count=0) - - now = datetime.datetime.now() - if my_task._get_internal_data('start_time') is None: - start_time = now - my_task.internal_data['start_time'] = now.strftime(self.TIME_FORMAT) - else: - start_time = datetime.datetime.strptime(my_task._get_internal_data('start_time'),self.TIME_FORMAT) - - if my_task.get_data('repeat_count') >= repeat or (now - start_time) < delta: - return False - return True - - def reset(self, my_task): - repeat_count = my_task.get_data('repeat_count') - if repeat_count is None: - # If this is a boundary event, then repeat count will not have been set - my_task.set_data(repeat_count=0) - else: - my_task.set_data(repeat_count=repeat_count + 1) - 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 complete_cycle(self, my_task): + # Only increment when the task completes + if my_task._get_internal_data('event_value') is not None: + my_task.internal_data['event_value']['cycles'] -= 1 class MultipleEventDefinition(EventDefinition): diff --git a/SpiffWorkflow/bpmn/specs/events/event_types.py b/SpiffWorkflow/bpmn/specs/events/event_types.py index 70739593..348ee434 100644 --- a/SpiffWorkflow/bpmn/specs/events/event_types.py +++ b/SpiffWorkflow/bpmn/specs/events/event_types.py @@ -17,7 +17,7 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from .event_definitions import MessageEventDefinition, NoneEventDefinition +from .event_definitions import MessageEventDefinition, NoneEventDefinition, CycleTimerEventDefinition from ..BpmnSpecMixin import BpmnSpecMixin from ....specs.Simple import Simple from ....task import TaskState @@ -69,6 +69,12 @@ class CatchingEvent(Simple, BpmnSpecMixin): if isinstance(self.event_definition, MessageEventDefinition): self.event_definition.update_task_data(my_task) + elif isinstance(self.event_definition, CycleTimerEventDefinition): + self.event_definition.complete_cycle(my_task) + if not self.event_definition.complete(my_task): + for output in self.outputs: + my_task._add_child(output) + my_task._set_state(TaskState.WAITING) self.event_definition.reset(my_task) super(CatchingEvent, self)._on_complete_hook(my_task) diff --git a/SpiffWorkflow/bpmn/workflow.py b/SpiffWorkflow/bpmn/workflow.py index 6cd18dba..52ebf7e0 100644 --- a/SpiffWorkflow/bpmn/workflow.py +++ b/SpiffWorkflow/bpmn/workflow.py @@ -16,7 +16,12 @@ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 USA -from SpiffWorkflow.bpmn.specs.events.event_definitions import MessageEventDefinition, MultipleEventDefinition +from SpiffWorkflow.bpmn.specs.events.event_definitions import ( + MessageEventDefinition, + MultipleEventDefinition, + NamedEventDefinition, + TimerEventDefinition, +) from .PythonScriptEngine import PythonScriptEngine from .specs.events.event_types import CatchingEvent from .specs.events.StartEvent import StartEvent @@ -155,9 +160,21 @@ class BpmnWorkflow(Workflow): event_definition.payload = payload self.catch(event_definition, correlations=correlations) - def do_engine_steps(self, exit_at = None, - will_complete_task=None, - did_complete_task=None): + def waiting_events(self): + # Ultimately I'd like to add an event class so that EventDefinitions would not so double duty as both specs + # and instantiations, and this method would return that. However, I think that's beyond the scope of the + # current request. + events = [] + for task in [t for t in self.get_waiting_tasks() if isinstance(t.task_spec, CatchingEvent)]: + event_definition = task.task_spec.event_definition + events.append({ + 'event_type': event_definition.event_type, + 'name': event_definition.name if isinstance(event_definition, NamedEventDefinition) else None, + 'value': event_definition.timer_value(task) if isinstance(event_definition, TimerEventDefinition) else None, + }) + return events + + def do_engine_steps(self, exit_at = None, will_complete_task=None, did_complete_task=None): """ Execute any READY tasks that are engine specific (for example, gateways or script tasks). This is done in a loop, so it will keep completing @@ -168,9 +185,7 @@ class BpmnWorkflow(Workflow): :param will_complete_task: Callback that will be called prior to completing a task :param did_complete_task: Callback that will be called after completing a task """ - engine_steps = list( - [t for t in self.get_tasks(TaskState.READY) - if self._is_engine_task(t.task_spec)]) + engine_steps = list([t for t in self.get_tasks(TaskState.READY) if self._is_engine_task(t.task_spec)]) while engine_steps: for task in engine_steps: if will_complete_task is not None: @@ -180,9 +195,7 @@ class BpmnWorkflow(Workflow): did_complete_task(task) if task.task_spec.name == exit_at: return task - engine_steps = list( - [t for t in self.get_tasks(TaskState.READY) - if self._is_engine_task(t.task_spec)]) + engine_steps = list([t for t in self.get_tasks(TaskState.READY) if self._is_engine_task(t.task_spec)]) def refresh_waiting_tasks(self, will_refresh_task=None, @@ -255,6 +268,3 @@ class BpmnWorkflow(Workflow): def _is_engine_task(self, task_spec): return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task()) - - def _task_completed_notify(self, task): - super(BpmnWorkflow, self)._task_completed_notify(task) diff --git a/SpiffWorkflow/specs/ExclusiveChoice.py b/SpiffWorkflow/specs/ExclusiveChoice.py index c31fcbe1..24607e30 100644 --- a/SpiffWorkflow/specs/ExclusiveChoice.py +++ b/SpiffWorkflow/specs/ExclusiveChoice.py @@ -53,16 +53,11 @@ class ExclusiveChoice(MultiChoice): :param task_spec: The following task spec. """ assert self.default_task_spec is None - self.outputs.append(task_spec) self.default_task_spec = task_spec.name - task_spec._connect_notify(self) + super().connect(task_spec) def test(self): - """ - Checks whether all required attributes are set. Throws an exception - if an error was detected. - """ - MultiChoice.test(self) + super().test() if self.default_task_spec is None: raise WorkflowException('A default output is required.', task_spec=self) @@ -76,10 +71,10 @@ class ExclusiveChoice(MultiChoice): my_task._set_likely_task(spec) def _on_complete_hook(self, my_task): - # Find the first matching condition. + output = self._wf_spec.get_task_spec_from_name(self.default_task_spec) for condition, spec_name in self.cond_task_specs: - if condition is None or condition._matches(my_task): + if condition is not None and condition._matches(my_task): output = self._wf_spec.get_task_spec_from_name(spec_name) break diff --git a/SpiffWorkflow/specs/Join.py b/SpiffWorkflow/specs/Join.py index 4a4238ed..0a16a64c 100644 --- a/SpiffWorkflow/specs/Join.py +++ b/SpiffWorkflow/specs/Join.py @@ -125,6 +125,26 @@ class Join(TaskSpec): return True return False + def _get_split_task(self, my_task): + # One Join spec may have multiple corresponding Task objects:: + # + # - Due to the MultiInstance pattern. + # - Due to the ThreadSplit pattern. + # + # When using the MultiInstance pattern, we want to join across + # the resulting task instances. When using the ThreadSplit + # pattern, we only join within the same thread. (Both patterns + # may also be mixed.) + # + # We are looking for all task instances that must be joined. + # We limit our search by starting at the split point. + if self.split_task: + task_spec = my_task.workflow.get_task_spec_from_name(self.split_task) + split_task = my_task._find_ancestor(task_spec) + else: + split_task = my_task.workflow.task_tree + return split_task + def _check_threshold_unstructured(self, my_task, force=False): # The default threshold is the number of inputs. threshold = valueof(my_task, self.threshold) @@ -168,7 +188,6 @@ class Join(TaskSpec): for task in tasks: # Refresh path prediction. task.task_spec._predict(task) - if not self._branch_may_merge_at(task): completed += 1 elif self._branch_is_complete(task): @@ -219,24 +238,8 @@ class Join(TaskSpec): self._do_join(my_task) def _do_join(self, my_task): - # One Join spec may have multiple corresponding Task objects:: - # - # - Due to the MultiInstance pattern. - # - Due to the ThreadSplit pattern. - # - # When using the MultiInstance pattern, we want to join across - # the resulting task instances. When using the ThreadSplit - # pattern, we only join within the same thread. (Both patterns - # may also be mixed.) - # - # We are looking for all task instances that must be joined. - # We limit our search by starting at the split point. - if self.split_task: - split_task = my_task.workflow.get_task_spec_from_name( - self.split_task) - split_task = my_task._find_ancestor(split_task) - else: - split_task = my_task.workflow.task_tree + + split_task = self._get_split_task(my_task) # Identify all corresponding task instances within the thread. # Also remember which of those instances was most recently changed, @@ -259,8 +262,7 @@ class Join(TaskSpec): # Check whether the state of the instance was recently # changed. changed = task.parent.last_state_change - if last_changed is None \ - or changed > last_changed.parent.last_state_change: + if last_changed is None or changed > last_changed.parent.last_state_change: last_changed = task # Mark the identified task instances as COMPLETED. The exception @@ -276,8 +278,6 @@ class Join(TaskSpec): task._set_state(TaskState.COMPLETED) task._drop_children() - - def _on_trigger(self, my_task): """ May be called to fire the Join before the incoming branches are diff --git a/SpiffWorkflow/specs/MultiChoice.py b/SpiffWorkflow/specs/MultiChoice.py index f43e6d2f..bcb998b6 100644 --- a/SpiffWorkflow/specs/MultiChoice.py +++ b/SpiffWorkflow/specs/MultiChoice.py @@ -116,24 +116,21 @@ class MultiChoice(TaskSpec): if child.task_spec in outputs: child._set_state(best_state) + def _get_matching_outputs(self, my_task): + outputs = [] + for condition, output in self.cond_task_specs: + if self.choice is not None and output not in self.choice: + continue + if condition is None or condition._matches(my_task): + outputs.append(self._wf_spec.get_task_spec_from_name(output)) + return outputs + def _on_complete_hook(self, my_task): """ Runs the task. Should not be called directly. Returns True if completed, False otherwise. """ - # Find all matching conditions. - outputs = [] - for condition, output in self.cond_task_specs: - if self.choice is not None and output not in self.choice: - continue - if condition is None: - outputs.append(self._wf_spec.get_task_spec_from_name(output)) - continue - if not condition._matches(my_task): - continue - outputs.append(self._wf_spec.get_task_spec_from_name(output)) - - my_task._sync_children(outputs, TaskState.FUTURE) + my_task._sync_children(self._get_matching_outputs(my_task), TaskState.FUTURE) def serialize(self, serializer): return serializer.serialize_multi_choice(self) diff --git a/tests/SpiffWorkflow/bpmn/BpmnLoaderForTests.py b/tests/SpiffWorkflow/bpmn/BpmnLoaderForTests.py index 54c18709..9f90268d 100644 --- a/tests/SpiffWorkflow/bpmn/BpmnLoaderForTests.py +++ b/tests/SpiffWorkflow/bpmn/BpmnLoaderForTests.py @@ -3,7 +3,8 @@ from SpiffWorkflow.bpmn.specs.ExclusiveGateway import ExclusiveGateway from SpiffWorkflow.bpmn.specs.UserTask import UserTask from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnParser -from SpiffWorkflow.bpmn.parser.task_parsers import ExclusiveGatewayParser, UserTaskParser +from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser +from SpiffWorkflow.bpmn.parser.task_parsers import ConditionalGatewayParser from SpiffWorkflow.bpmn.parser.util import full_tag from SpiffWorkflow.bpmn.serializer.bpmn_converters import BpmnTaskSpecConverter @@ -38,7 +39,7 @@ class TestUserTask(UserTask): def deserialize(self, serializer, wf_spec, s_state): return serializer.deserialize_generic(wf_spec, s_state, TestUserTask) -class TestExclusiveGatewayParser(ExclusiveGatewayParser): +class TestExclusiveGatewayParser(ConditionalGatewayParser): def parse_condition(self, sequence_flow_node): cond = super().parse_condition(sequence_flow_node) @@ -62,7 +63,7 @@ class TestUserTaskConverter(BpmnTaskSpecConverter): class TestBpmnParser(BpmnParser): OVERRIDE_PARSER_CLASSES = { - full_tag('userTask'): (UserTaskParser, TestUserTask), + full_tag('userTask'): (TaskParser, TestUserTask), full_tag('exclusiveGateway'): (TestExclusiveGatewayParser, ExclusiveGateway), full_tag('callActivity'): (CallActivityParser, CallActivity) } diff --git a/tests/SpiffWorkflow/bpmn/IOSpecTest.py b/tests/SpiffWorkflow/bpmn/IOSpecTest.py index efa6441c..6b27fc05 100644 --- a/tests/SpiffWorkflow/bpmn/IOSpecTest.py +++ b/tests/SpiffWorkflow/bpmn/IOSpecTest.py @@ -18,14 +18,17 @@ class CallActivityDataTest(BpmnWorkflowTestCase): self.actual_test(True) def testCallActivityMissingInput(self): - + self.workflow = BpmnWorkflow(self.spec, self.subprocesses) set_data = self.workflow.spec.task_specs['Activity_0haob58'] set_data.script = """in_1, unused = 1, True""" with self.assertRaises(WorkflowDataException) as exc: self.advance_to_subprocess() - self.assertEqual(exc.var.name,'in_2') + self.assertEqual("'in_2' was not found in the task data. " + "You are missing a required Data Input for a call activity.", + str(exc.exception)) + self.assertEqual(exc.exception.data_input.name,'in_2') def testCallActivityMissingOutput(self): @@ -40,7 +43,10 @@ class CallActivityDataTest(BpmnWorkflowTestCase): with self.assertRaises(WorkflowDataException) as exc: self.complete_subprocess() - self.assertEqual(exc.var.name,'out_2') + + self.assertEqual("'out_2' was not found in the task data. A Data Output was not provided as promised.", + str(exc.exception)) + self.assertEqual(exc.exception.data_output.name,'out_2') def actual_test(self, save_restore=False): @@ -85,4 +91,4 @@ class CallActivityDataTest(BpmnWorkflowTestCase): while len(waiting) > 0: next_task = self.workflow.get_tasks(TaskState.READY)[0] next_task.complete() - waiting = self.workflow.get_tasks(TaskState.WAITING) \ No newline at end of file + waiting = self.workflow.get_tasks(TaskState.WAITING) diff --git a/tests/SpiffWorkflow/bpmn/InclusiveGatewayTest.py b/tests/SpiffWorkflow/bpmn/InclusiveGatewayTest.py new file mode 100644 index 00000000..f4a0e5db --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/InclusiveGatewayTest.py @@ -0,0 +1,39 @@ +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow +from SpiffWorkflow.exceptions import WorkflowTaskException + +from .BpmnWorkflowTestCase import BpmnWorkflowTestCase + +class InclusiveGatewayTest(BpmnWorkflowTestCase): + + def setUp(self): + spec, subprocess = self.load_workflow_spec('inclusive_gateway.bpmn', 'main') + self.workflow = BpmnWorkflow(spec) + self.workflow.do_engine_steps() + + def testDefaultConditionOnly(self): + self.set_data({'v': -1, 'u': -1, 'w': -1}) + self.workflow.do_engine_steps() + self.assertTrue(self.workflow.is_completed()) + self.assertDictEqual(self.workflow.data, {'v': 0, 'u': -1, 'w': -1}) + + def testDefaultConditionOnlySaveRestore(self): + self.set_data({'v': -1, 'u': -1, 'w': -1}) + self.save_restore() + self.workflow.do_engine_steps() + self.assertTrue(self.workflow.is_completed()) + self.assertDictEqual(self.workflow.data, {'v': 0, 'u': -1, 'w': -1}) + + def testNoPathFromSecondGateway(self): + self.set_data({'v': 0, 'u': -1, 'w': -1}) + self.assertRaises(WorkflowTaskException, self.workflow.do_engine_steps) + + def testParallelCondition(self): + self.set_data({'v': 0, 'u': 1, 'w': 1}) + self.workflow.do_engine_steps() + self.assertTrue(self.workflow.is_completed()) + self.assertDictEqual(self.workflow.data, {'v': 0, 'u': 1, 'w': 1}) + + def set_data(self, value): + task = self.workflow.get_ready_user_tasks()[0] + task.data = value + task.complete() diff --git a/tests/SpiffWorkflow/bpmn/NITimerDurationBoundaryTest.py b/tests/SpiffWorkflow/bpmn/NITimerDurationBoundaryTest.py index 97553a6d..29e0f62a 100644 --- a/tests/SpiffWorkflow/bpmn/NITimerDurationBoundaryTest.py +++ b/tests/SpiffWorkflow/bpmn/NITimerDurationBoundaryTest.py @@ -5,7 +5,6 @@ import datetime import time from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.workflow import BpmnWorkflow -from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase __author__ = 'kellym' @@ -16,9 +15,8 @@ class NITimerDurationTest(BpmnWorkflowTestCase): Non-Interrupting Timer boundary test """ def setUp(self): - self.script_engine = PythonScriptEngine(default_globals={"timedelta": datetime.timedelta}) spec, subprocesses = self.load_workflow_spec('timer-non-interrupt-boundary.bpmn', 'NonInterruptTimer') - self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=self.script_engine) + self.workflow = BpmnWorkflow(spec, subprocesses) def load_spec(self): return @@ -31,57 +29,44 @@ class NITimerDurationTest(BpmnWorkflowTestCase): def actual_test(self,save_restore = False): - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.do_engine_steps() ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - ready_tasks[0].data['work_done'] = 'No' - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() + event = self.workflow.get_tasks_from_spec_name('Event_0jyy8ao')[0] + self.assertEqual(event.state, TaskState.WAITING) loopcount = 0 - # test bpmn has a timeout of .25s - # we should terminate loop before that. starttime = datetime.datetime.now() - while loopcount < 10: - ready_tasks = self.workflow.get_tasks(TaskState.READY) - if len(ready_tasks) > 1: - break + # test bpmn has a timeout of .2s; we should terminate loop before that. + # The subprocess will also wait + while len(self.workflow.get_waiting_tasks()) == 2 and loopcount < 10: if save_restore: self.save_restore() - self.workflow.script_engine = self.script_engine - #self.assertEqual(1, len(self.workflow.get_tasks(Task.WAITING))) time.sleep(0.1) - self.workflow.complete_task_from_id(ready_tasks[0].id) + ready_tasks = self.workflow.get_tasks(TaskState.READY) + # There should be one ready task until the boundary event fires + self.assertEqual(len(self.workflow.get_ready_user_tasks()), 1) self.workflow.refresh_waiting_tasks() self.workflow.do_engine_steps() - loopcount = loopcount +1 + loopcount += 1 + endtime = datetime.datetime.now() - duration = endtime-starttime - # appropriate time here is .5 seconds - # due to the .3 seconds that we loop and then - # the two conditions that we complete after the timer completes. - self.assertEqual(durationdatetime.timedelta(seconds=.2),True) + duration = endtime - starttime + # appropriate time here is .5 seconds due to the .3 seconds that we loop and then + self.assertEqual(duration < datetime.timedelta(seconds=.5), True) + self.assertEqual(duration > datetime.timedelta(seconds=.2), True) + ready_tasks = self.workflow.get_ready_user_tasks() + # Now there should be two. + self.assertEqual(len(ready_tasks), 2) for task in ready_tasks: - if task.task_spec == 'GetReason': + if task.task_spec.name == 'GetReason': task.data['delay_reason'] = 'Just Because' - else: + elif task.task_spec.name == 'Activity_Work': task.data['work_done'] = 'Yes' - self.workflow.complete_task_from_id(task.id) + task.complete() self.workflow.refresh_waiting_tasks() self.workflow.do_engine_steps() - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - ready_tasks[0].data['experience'] = 'Great!' - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() self.assertEqual(self.workflow.is_completed(),True) - self.assertEqual(self.workflow.last_task.data,{'work_done': 'Yes', 'experience': 'Great!'}) - print (self.workflow.last_task.data) - print(duration) + self.assertEqual(self.workflow.last_task.data, {'work_done': 'Yes', 'delay_reason': 'Just Because'}) def suite(): diff --git a/tests/SpiffWorkflow/bpmn/data/MultiInstanceParallelTaskCond.bpmn b/tests/SpiffWorkflow/bpmn/data/MultiInstanceParallelTaskCond.bpmn index 2945c381..53ecfd4e 100644 --- a/tests/SpiffWorkflow/bpmn/data/MultiInstanceParallelTaskCond.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/MultiInstanceParallelTaskCond.bpmn @@ -1,5 +1,5 @@ - + Flow_0t6p1sb @@ -40,7 +40,7 @@ Flow_1dah8xt Flow_0io0g18 - + Flow_0io0g18 Flow_0i1bv5g Flow_1sx7n9u @@ -52,85 +52,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -140,6 +61,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Join-Long-Inclusive.bpmn20.xml b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Join-Long-Inclusive.bpmn20.xml index 80cf2972..077c5721 100644 --- a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Join-Long-Inclusive.bpmn20.xml +++ b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Join-Long-Inclusive.bpmn20.xml @@ -1,633 +1,653 @@ - - - - - - - - - - - - - - - sid-9CAB06E6-EDCF-4193-869A-FE8328E8CBFF - sid-F4CFA154-9281-4579-B117-0859A2BFF7E8 - sid-E489DED4-8C38-4841-80BC-E514353C1B8C - sid-B88338F7-5084-4532-9ABB-7387B1E5A664 - sid-35745597-A6C0-424B-884C-C5C23B60C942 - sid-0BC1E7F7-CBDA-4591-95B9-320FCBEF6114 - sid-45841FFD-3D92-4A18-9CE3-84DC5282F570 - sid-A8E18F57-FC41-401D-A397-9264C3E48293 - sid-E98E44A0-A273-4350-BA75-B37F2FCBA1DD - sid-29C1DD4B-9E3E-4686-892D-D47927F6DA08 - sid-107A993F-6302-4391-9BE2-068C9C7B693B - sid-71AA325A-4D02-46B4-8DC9-00C90BC5337C - sid-54BB293D-91B6-41B5-A5C4-423300D74D14 - sid-D8777102-7A64-42E6-A988-D0AE3049ABB0 - sid-E6A08072-E35C-4545-9C66-B74B615F34C2 - sid-BEC02819-27FE-4484-8FDA-08450F4DE618 - sid-6576AA43-43DF-4086-98C8-FD2B22F20EB0 - sid-08397892-678C-4706-A05F-8F6DAE9B5423 - sid-3500C16F-8037-4987-9022-8E30AB6B0590 - sid-A473B421-0981-49D8-BD5A-66832BD518EC - sid-F18EA1E5-B692-484C-AB84-2F422BF7868A - sid-D67A997E-C7CF-4581-8749-4F931D8737B5 - sid-399AE395-D46F-4A30-B875-E904970AF141 - sid-738EA50B-3EB5-464B-96B8-6CA5FC30ECBA - sid-5598F421-4AC5-4C12-9239-EFAC51C5F474 - sid-CF5677F8-747F-4E95-953E-4DAB186958F4 - sid-A73FF591-2A52-42DF-97DB-6CEEF8991283 - sid-69DF31CE-D587-4BA8-8BE6-72786108D8DF - sid-38B84B23-6757-4357-9AF5-A62A5C8AC1D3 - sid-732095A1-B07A-4B08-A46B-277C12901DED - sid-23391B60-C6A7-4C9E-9F95-43EA84ECFB74 - sid-9A40D0CD-3BD0-4A0D-A6B0-60FD60265247 - sid-B2E34105-96D5-4020-85D9-C569BA42D618 - sid-3D1455CF-6B1E-4EB1-81B2-D738110BB283 - sid-75EE4F61-E8E2-441B-8818-30E3BACF140B - sid-4864A824-7467-421A-A654-83EE83F7681C - sid-6938255D-3C1A-4B94-9E83-4D467E0DDB4B - - - - - - - sid-54E118FA-9A24-434C-9E65-36F9D01FB43D - - - - - - sid-54E118FA-9A24-434C-9E65-36F9D01FB43D - sid-7BFA5A55-E297-40FC-88A6-DF1DA809A12C - sid-C7247231-5152-424E-A240-B07B76E8F5EC - - - - - - sid-C609F3E0-2D09-469C-8750-3E3BA8C926BE - sid-0204722F-5A92-4236-BBF1-C66123E14E22 - - - - - - sid-0204722F-5A92-4236-BBF1-C66123E14E22 - sid-699ED598-1AB9-4A3B-9315-9C89578FB017 - - - - - - sid-699ED598-1AB9-4A3B-9315-9C89578FB017 - sid-85116AFA-E95A-4384-9695-361C1A6070C3 - - - - - - sid-85116AFA-E95A-4384-9695-361C1A6070C3 - sid-84756278-D67A-4E65-AD96-24325F08E2D1 - - - - - - sid-AE7CFA43-AC83-4F28-BCE3-AD7BE9CE6F27 - sid-C132728C-7DAF-468C-A807-90A34847071E - - - - - - sid-C132728C-7DAF-468C-A807-90A34847071E - sid-4A3A7E6E-F79B-4842-860C-407DB9227023 - - - - - - sid-4A3A7E6E-F79B-4842-860C-407DB9227023 - sid-B45563D3-2FBE-406D-93E4-85A2DD04B1A4 - - - - - - sid-D7D86B12-A88C-4072-9852-6DD62643556A - sid-AE7CFA43-AC83-4F28-BCE3-AD7BE9CE6F27 - - - - - - sid-369B410B-EA82-4896-91FD-23FFF759494A - sid-E47AA9C3-9EB7-4B07-BB17-086388AACE0D - - - - - - sid-84756278-D67A-4E65-AD96-24325F08E2D1 - sid-0C53B343-3753-4EED-A6FE-C1A7DFBF13BC - - - - - - sid-0C53B343-3753-4EED-A6FE-C1A7DFBF13BC - sid-9CA8DF1F-1622-4F6A-B9A6-761C60C29A11 - - - - - - sid-9CA8DF1F-1622-4F6A-B9A6-761C60C29A11 - sid-13838920-8EE4-45CB-8F01-29F13CA13819 - - - - - - sid-13838920-8EE4-45CB-8F01-29F13CA13819 - sid-FAA04C3A-F55B-4947-850D-5A180D43BD61 - - - - - - sid-FAA04C3A-F55B-4947-850D-5A180D43BD61 - sid-A19043EA-D140-48AE-99A1-4B1EA3DE0E51 - - - - - - sid-A19043EA-D140-48AE-99A1-4B1EA3DE0E51 - sid-2A94D2F0-DF4B-45B6-A30D-FFB9BDF6E9D9 - - - - - - sid-2A94D2F0-DF4B-45B6-A30D-FFB9BDF6E9D9 - sid-661F5F14-5B94-4977-9827-20654AE2719B - - - - - - sid-661F5F14-5B94-4977-9827-20654AE2719B - sid-C0DC27C3-19F9-4D3D-9D04-8869DAEDEF1E - - - - - - sid-B45563D3-2FBE-406D-93E4-85A2DD04B1A4 - sid-0E826E42-8FBC-4532-96EA-C82E7340CBA4 - - - - - - sid-0E826E42-8FBC-4532-96EA-C82E7340CBA4 - sid-BE9CBE97-0E09-4A37-BD98-65592D2F2E84 - - - - - - sid-BE9CBE97-0E09-4A37-BD98-65592D2F2E84 - sid-C96EBBBD-7DDA-4875-89AC-0F030E53C2B6 - - - - - - sid-C96EBBBD-7DDA-4875-89AC-0F030E53C2B6 - sid-8449C64C-CF1D-4601-ACAE-2CD61BE2D36C - - - - - - sid-8449C64C-CF1D-4601-ACAE-2CD61BE2D36C - sid-1DD0519A-72AD-4FB1-91D6-4D18F2DA1FC8 - - - - - - sid-1DD0519A-72AD-4FB1-91D6-4D18F2DA1FC8 - sid-42540C95-8E89-4B6F-B133-F677FA72C9FF - - - - - - sid-42540C95-8E89-4B6F-B133-F677FA72C9FF - sid-108A05A6-D07C-4DA9-AAC3-8075A721B44B - - - - - - sid-108A05A6-D07C-4DA9-AAC3-8075A721B44B - sid-A8886943-1369-43FB-BFC1-FF1FF974EB5D - - - - - - sid-7BFA5A55-E297-40FC-88A6-DF1DA809A12C - sid-7F5D9083-6201-43F9-BBEE-664E7310F4F2 - - - - - - sid-7F5D9083-6201-43F9-BBEE-664E7310F4F2 - sid-C609F3E0-2D09-469C-8750-3E3BA8C926BE - sid-C06DF3AB-4CE5-4123-8033-8AACDCDF4416 - - - - - - sid-C06DF3AB-4CE5-4123-8033-8AACDCDF4416 - sid-A0A2FCFF-E2BE-4FE6-A4D2-CCB3DCF68BFB - - - - - - sid-C7247231-5152-424E-A240-B07B76E8F5EC - sid-DC398932-1111-4CA2-AEB4-D460E0E06C6E - - - - - - sid-DC398932-1111-4CA2-AEB4-D460E0E06C6E - sid-D7D86B12-A88C-4072-9852-6DD62643556A - sid-16EB4D98-7F77-4046-8CDD-E07C796542FE - - - - - - sid-16EB4D98-7F77-4046-8CDD-E07C796542FE - sid-8E17C1AF-45C2-48C7-A794-1259E2ECA43D - - - - - - sid-A8886943-1369-43FB-BFC1-FF1FF974EB5D - sid-C0DC27C3-19F9-4D3D-9D04-8869DAEDEF1E - sid-369B410B-EA82-4896-91FD-23FFF759494A - - - - - - sid-8E17C1AF-45C2-48C7-A794-1259E2ECA43D - - - - - - sid-A0A2FCFF-E2BE-4FE6-A4D2-CCB3DCF68BFB - - - - - - sid-E47AA9C3-9EB7-4B07-BB17-086388AACE0D - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + sid-9CAB06E6-EDCF-4193-869A-FE8328E8CBFF + sid-F4CFA154-9281-4579-B117-0859A2BFF7E8 + sid-E489DED4-8C38-4841-80BC-E514353C1B8C + sid-B88338F7-5084-4532-9ABB-7387B1E5A664 + sid-35745597-A6C0-424B-884C-C5C23B60C942 + sid-0BC1E7F7-CBDA-4591-95B9-320FCBEF6114 + sid-45841FFD-3D92-4A18-9CE3-84DC5282F570 + sid-A8E18F57-FC41-401D-A397-9264C3E48293 + sid-E98E44A0-A273-4350-BA75-B37F2FCBA1DD + sid-29C1DD4B-9E3E-4686-892D-D47927F6DA08 + sid-107A993F-6302-4391-9BE2-068C9C7B693B + sid-71AA325A-4D02-46B4-8DC9-00C90BC5337C + sid-54BB293D-91B6-41B5-A5C4-423300D74D14 + sid-D8777102-7A64-42E6-A988-D0AE3049ABB0 + sid-E6A08072-E35C-4545-9C66-B74B615F34C2 + sid-BEC02819-27FE-4484-8FDA-08450F4DE618 + sid-6576AA43-43DF-4086-98C8-FD2B22F20EB0 + sid-08397892-678C-4706-A05F-8F6DAE9B5423 + sid-3500C16F-8037-4987-9022-8E30AB6B0590 + sid-A473B421-0981-49D8-BD5A-66832BD518EC + sid-F18EA1E5-B692-484C-AB84-2F422BF7868A + sid-D67A997E-C7CF-4581-8749-4F931D8737B5 + sid-399AE395-D46F-4A30-B875-E904970AF141 + sid-738EA50B-3EB5-464B-96B8-6CA5FC30ECBA + sid-5598F421-4AC5-4C12-9239-EFAC51C5F474 + sid-CF5677F8-747F-4E95-953E-4DAB186958F4 + sid-A73FF591-2A52-42DF-97DB-6CEEF8991283 + sid-69DF31CE-D587-4BA8-8BE6-72786108D8DF + sid-38B84B23-6757-4357-9AF5-A62A5C8AC1D3 + sid-732095A1-B07A-4B08-A46B-277C12901DED + sid-23391B60-C6A7-4C9E-9F95-43EA84ECFB74 + sid-9A40D0CD-3BD0-4A0D-A6B0-60FD60265247 + sid-B2E34105-96D5-4020-85D9-C569BA42D618 + sid-3D1455CF-6B1E-4EB1-81B2-D738110BB283 + sid-75EE4F61-E8E2-441B-8818-30E3BACF140B + sid-4864A824-7467-421A-A654-83EE83F7681C + sid-6938255D-3C1A-4B94-9E83-4D467E0DDB4B + + + + + + + sid-54E118FA-9A24-434C-9E65-36F9D01FB43D + + + + + + sid-54E118FA-9A24-434C-9E65-36F9D01FB43D + sid-7BFA5A55-E297-40FC-88A6-DF1DA809A12C + sid-C7247231-5152-424E-A240-B07B76E8F5EC + + + + + + sid-C609F3E0-2D09-469C-8750-3E3BA8C926BE + sid-0204722F-5A92-4236-BBF1-C66123E14E22 + + + + + + sid-0204722F-5A92-4236-BBF1-C66123E14E22 + sid-699ED598-1AB9-4A3B-9315-9C89578FB017 + + + + + + sid-699ED598-1AB9-4A3B-9315-9C89578FB017 + sid-85116AFA-E95A-4384-9695-361C1A6070C3 + + + + + + sid-85116AFA-E95A-4384-9695-361C1A6070C3 + sid-84756278-D67A-4E65-AD96-24325F08E2D1 + + + + + + sid-AE7CFA43-AC83-4F28-BCE3-AD7BE9CE6F27 + sid-C132728C-7DAF-468C-A807-90A34847071E + + + + + + sid-C132728C-7DAF-468C-A807-90A34847071E + sid-4A3A7E6E-F79B-4842-860C-407DB9227023 + + + + + + sid-4A3A7E6E-F79B-4842-860C-407DB9227023 + sid-B45563D3-2FBE-406D-93E4-85A2DD04B1A4 + + + + + + sid-D7D86B12-A88C-4072-9852-6DD62643556A + sid-AE7CFA43-AC83-4F28-BCE3-AD7BE9CE6F27 + + + + + + sid-369B410B-EA82-4896-91FD-23FFF759494A + sid-E47AA9C3-9EB7-4B07-BB17-086388AACE0D + + + + + + sid-84756278-D67A-4E65-AD96-24325F08E2D1 + sid-0C53B343-3753-4EED-A6FE-C1A7DFBF13BC + + + + + + sid-0C53B343-3753-4EED-A6FE-C1A7DFBF13BC + sid-9CA8DF1F-1622-4F6A-B9A6-761C60C29A11 + + + + + + sid-9CA8DF1F-1622-4F6A-B9A6-761C60C29A11 + sid-13838920-8EE4-45CB-8F01-29F13CA13819 + + + + + + sid-13838920-8EE4-45CB-8F01-29F13CA13819 + sid-FAA04C3A-F55B-4947-850D-5A180D43BD61 + + + + + + sid-FAA04C3A-F55B-4947-850D-5A180D43BD61 + sid-A19043EA-D140-48AE-99A1-4B1EA3DE0E51 + + + + + + sid-A19043EA-D140-48AE-99A1-4B1EA3DE0E51 + sid-2A94D2F0-DF4B-45B6-A30D-FFB9BDF6E9D9 + + + + + + sid-2A94D2F0-DF4B-45B6-A30D-FFB9BDF6E9D9 + sid-661F5F14-5B94-4977-9827-20654AE2719B + + + + + + sid-661F5F14-5B94-4977-9827-20654AE2719B + sid-C0DC27C3-19F9-4D3D-9D04-8869DAEDEF1E + + + + + + sid-B45563D3-2FBE-406D-93E4-85A2DD04B1A4 + sid-0E826E42-8FBC-4532-96EA-C82E7340CBA4 + + + + + + sid-0E826E42-8FBC-4532-96EA-C82E7340CBA4 + sid-BE9CBE97-0E09-4A37-BD98-65592D2F2E84 + + + + + + sid-BE9CBE97-0E09-4A37-BD98-65592D2F2E84 + sid-C96EBBBD-7DDA-4875-89AC-0F030E53C2B6 + + + + + + sid-C96EBBBD-7DDA-4875-89AC-0F030E53C2B6 + sid-8449C64C-CF1D-4601-ACAE-2CD61BE2D36C + + + + + + sid-8449C64C-CF1D-4601-ACAE-2CD61BE2D36C + sid-1DD0519A-72AD-4FB1-91D6-4D18F2DA1FC8 + + + + + + sid-1DD0519A-72AD-4FB1-91D6-4D18F2DA1FC8 + sid-42540C95-8E89-4B6F-B133-F677FA72C9FF + + + + + + sid-42540C95-8E89-4B6F-B133-F677FA72C9FF + sid-108A05A6-D07C-4DA9-AAC3-8075A721B44B + + + + + + sid-108A05A6-D07C-4DA9-AAC3-8075A721B44B + sid-A8886943-1369-43FB-BFC1-FF1FF974EB5D + + + + + + sid-7BFA5A55-E297-40FC-88A6-DF1DA809A12C + sid-7F5D9083-6201-43F9-BBEE-664E7310F4F2 + + + + + + sid-7F5D9083-6201-43F9-BBEE-664E7310F4F2 + sid-C609F3E0-2D09-469C-8750-3E3BA8C926BE + sid-C06DF3AB-4CE5-4123-8033-8AACDCDF4416 + + + + + + sid-C06DF3AB-4CE5-4123-8033-8AACDCDF4416 + sid-A0A2FCFF-E2BE-4FE6-A4D2-CCB3DCF68BFB + + + + + + sid-C7247231-5152-424E-A240-B07B76E8F5EC + sid-DC398932-1111-4CA2-AEB4-D460E0E06C6E + + + + + + sid-DC398932-1111-4CA2-AEB4-D460E0E06C6E + sid-D7D86B12-A88C-4072-9852-6DD62643556A + sid-16EB4D98-7F77-4046-8CDD-E07C796542FE + + + + + + sid-16EB4D98-7F77-4046-8CDD-E07C796542FE + sid-8E17C1AF-45C2-48C7-A794-1259E2ECA43D + + + + + + sid-A8886943-1369-43FB-BFC1-FF1FF974EB5D + sid-C0DC27C3-19F9-4D3D-9D04-8869DAEDEF1E + sid-369B410B-EA82-4896-91FD-23FFF759494A + + + + + + sid-8E17C1AF-45C2-48C7-A794-1259E2ECA43D + + + + + + sid-A0A2FCFF-E2BE-4FE6-A4D2-CCB3DCF68BFB + + + + + + sid-E47AA9C3-9EB7-4B07-BB17-086388AACE0D + + + + + + + choice == 'No' + + + choice == 'Yes' + + + + + choice == 'No' + + + choice == 'Yes' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-One-Path-Ends.bpmn20.xml b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-One-Path-Ends.bpmn20.xml index 773ffc75..b17d9369 100644 --- a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-One-Path-Ends.bpmn20.xml +++ b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-One-Path-Ends.bpmn20.xml @@ -1,192 +1,196 @@ - - - - - - - - - - - - - - - sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE - sid-349F8C0C-45EA-489C-84DD-1D944F48D778 - sid-57463471-693A-42A2-9EC6-6460BEDECA86 - sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 - sid-E2054FDD-0C20-4939-938D-2169B317FEE7 - sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 - sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 - sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB - sid-8FFE9D52-DC83-46A8-BB36-98BA94E5FE84 - sid-40294A27-262C-4805-94A0-36AC9DFEA55A - - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - - - - - - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-E3493781-6466-4AED-BAD2-63D115E14820 - - - - - - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - - - - - - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - sid-9C753C3D-F964-45B0-AF57-234F910529EF - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - - - - - - sid-9C753C3D-F964-45B0-AF57-234F910529EF - sid-A6DA25CE-636A-46B7-8005-759577956F09 - - - - - - sid-8B2BFD35-F1B2-4C77-AC51-F15960D8791A - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - - - - - - sid-A6DA25CE-636A-46B7-8005-759577956F09 - sid-E3493781-6466-4AED-BAD2-63D115E14820 - sid-8B2BFD35-F1B2-4C77-AC51-F15960D8791A - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE + sid-349F8C0C-45EA-489C-84DD-1D944F48D778 + sid-57463471-693A-42A2-9EC6-6460BEDECA86 + sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 + sid-E2054FDD-0C20-4939-938D-2169B317FEE7 + sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 + sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB + sid-8FFE9D52-DC83-46A8-BB36-98BA94E5FE84 + sid-40294A27-262C-4805-94A0-36AC9DFEA55A + sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 + + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + + + + + + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-E3493781-6466-4AED-BAD2-63D115E14820 + + + + + + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + + + + + + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + sid-9C753C3D-F964-45B0-AF57-234F910529EF + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + + + + + + sid-9C753C3D-F964-45B0-AF57-234F910529EF + sid-A6DA25CE-636A-46B7-8005-759577956F09 + + + + + + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + + + + + + sid-A6DA25CE-636A-46B7-8005-759577956F09 + sid-E3493781-6466-4AED-BAD2-63D115E14820 + sid-8B2BFD35-F1B2-4C77-AC51-F15960D8791A + + + + + + + choice == 'Yes' + + + + + choice == 'No' + + + + + + + + sid-8B2BFD35-F1B2-4C77-AC51-F15960D8791A + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive-No-Inclusive.bpmn20.xml b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive-No-Inclusive.bpmn20.xml index 373bb7f2..deab7a40 100644 --- a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive-No-Inclusive.bpmn20.xml +++ b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive-No-Inclusive.bpmn20.xml @@ -1,219 +1,223 @@ - - - - - - - - - - - - - - - sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE - sid-349F8C0C-45EA-489C-84DD-1D944F48D778 - sid-57463471-693A-42A2-9EC6-6460BEDECA86 - sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 - sid-E2054FDD-0C20-4939-938D-2169B317FEE7 - sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 - sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 - sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 - sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB - sid-040FCBAD-0550-4251-B799-74FCDB0DC3E2 - sid-D856C519-562B-46A3-B32C-9587F394BD0F - - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - - - - - - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-E3493781-6466-4AED-BAD2-63D115E14820 - - - - - - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - - - - - - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-9C753C3D-F964-45B0-AF57-234F910529EF - - - - - - sid-9C753C3D-F964-45B0-AF57-234F910529EF - sid-A6DA25CE-636A-46B7-8005-759577956F09 - - - - - - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - - - - - - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - sid-A6DA25CE-636A-46B7-8005-759577956F09 - sid-3B450653-1657-4247-B96E-6E3E6262BB97 - - - - - - sid-E3493781-6466-4AED-BAD2-63D115E14820 - sid-3B450653-1657-4247-B96E-6E3E6262BB97 - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE + sid-349F8C0C-45EA-489C-84DD-1D944F48D778 + sid-57463471-693A-42A2-9EC6-6460BEDECA86 + sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 + sid-E2054FDD-0C20-4939-938D-2169B317FEE7 + sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 + sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 + sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 + sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB + sid-040FCBAD-0550-4251-B799-74FCDB0DC3E2 + sid-D856C519-562B-46A3-B32C-9587F394BD0F + + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + + + + + + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-E3493781-6466-4AED-BAD2-63D115E14820 + + + + + + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + + + + + + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-9C753C3D-F964-45B0-AF57-234F910529EF + + + + + + sid-9C753C3D-F964-45B0-AF57-234F910529EF + sid-A6DA25CE-636A-46B7-8005-759577956F09 + + + + + + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + + + + + + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + sid-A6DA25CE-636A-46B7-8005-759577956F09 + sid-3B450653-1657-4247-B96E-6E3E6262BB97 + + + + + + sid-E3493781-6466-4AED-BAD2-63D115E14820 + sid-3B450653-1657-4247-B96E-6E3E6262BB97 + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + + + + + + + choice == 'No' + + + choice == 'Yes' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive.bpmn20.xml b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive.bpmn20.xml index f915b4c3..5526cf68 100644 --- a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive.bpmn20.xml +++ b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Then-Exclusive.bpmn20.xml @@ -1,202 +1,206 @@ - - - - - - - - - - - - - - - sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE - sid-349F8C0C-45EA-489C-84DD-1D944F48D778 - sid-57463471-693A-42A2-9EC6-6460BEDECA86 - sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 - sid-E2054FDD-0C20-4939-938D-2169B317FEE7 - sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 - sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 - sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 - sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB - sid-EBB511F3-5AD5-4307-9B9B-85C17F8889D5 - - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - - - - - - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-E3493781-6466-4AED-BAD2-63D115E14820 - - - - - - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - - - - - - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-9C753C3D-F964-45B0-AF57-234F910529EF - - - - - - sid-9C753C3D-F964-45B0-AF57-234F910529EF - sid-A6DA25CE-636A-46B7-8005-759577956F09 - - - - - - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - - - - - - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-E3493781-6466-4AED-BAD2-63D115E14820 - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - sid-A6DA25CE-636A-46B7-8005-759577956F09 - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE + sid-349F8C0C-45EA-489C-84DD-1D944F48D778 + sid-57463471-693A-42A2-9EC6-6460BEDECA86 + sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 + sid-E2054FDD-0C20-4939-938D-2169B317FEE7 + sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 + sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 + sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 + sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB + sid-EBB511F3-5AD5-4307-9B9B-85C17F8889D5 + + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + + + + + + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-E3493781-6466-4AED-BAD2-63D115E14820 + + + + + + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + + + + + + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-9C753C3D-F964-45B0-AF57-234F910529EF + + + + + + sid-9C753C3D-F964-45B0-AF57-234F910529EF + sid-A6DA25CE-636A-46B7-8005-759577956F09 + + + + + + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + + + + + + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-E3493781-6466-4AED-BAD2-63D115E14820 + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + sid-A6DA25CE-636A-46B7-8005-759577956F09 + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + + + + + + + choice == 'No' + + + choice == 'Yes' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Through-Same-Task.bpmn20.xml b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Through-Same-Task.bpmn20.xml index fed03996..5b328cdc 100644 --- a/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Through-Same-Task.bpmn20.xml +++ b/tests/SpiffWorkflow/bpmn/data/Test-Workflows/Parallel-Through-Same-Task.bpmn20.xml @@ -1,201 +1,205 @@ - - - - - - - - - - - - - - - sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE - sid-349F8C0C-45EA-489C-84DD-1D944F48D778 - sid-57463471-693A-42A2-9EC6-6460BEDECA86 - sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 - sid-E2054FDD-0C20-4939-938D-2169B317FEE7 - sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 - sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 - sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 - sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB - sid-AF897BE2-CC07-4236-902B-DD6E1AB31842 - - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - - - - - - sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - - - - - - sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 - sid-A6DA25CE-636A-46B7-8005-759577956F09 - sid-E3493781-6466-4AED-BAD2-63D115E14820 - - - - - - sid-B6E22A74-A691-453A-A789-B9F8AF787D7C - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - - - - - - sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-9C753C3D-F964-45B0-AF57-234F910529EF - - - - - - sid-9C753C3D-F964-45B0-AF57-234F910529EF - sid-A6DA25CE-636A-46B7-8005-759577956F09 - - - - - - sid-3742C960-71D0-4342-8064-AF1BB9EECB42 - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - - - - - - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-40496205-24D7-494C-AB6B-CD42B8D606EF - - - - - - sid-E3493781-6466-4AED-BAD2-63D115E14820 - sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE - sid-0895E09C-077C-4D12-8C11-31F28CBC7740 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE + sid-349F8C0C-45EA-489C-84DD-1D944F48D778 + sid-57463471-693A-42A2-9EC6-6460BEDECA86 + sid-CA089240-802A-4C32-9130-FB1A33DDCCC3 + sid-E2054FDD-0C20-4939-938D-2169B317FEE7 + sid-34AD79D9-BE0C-4F97-AC23-7A97D238A6E5 + sid-2A302E91-F89F-4913-8F55-5C3AC5FAE4D3 + sid-F3A979E3-F586-4807-8223-1FAB5A5647B0 + sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB + sid-AF897BE2-CC07-4236-902B-DD6E1AB31842 + + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + + + + + + sid-F3994F51-FE54-4910-A1F4-E5895AA1A612 + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + + + + + + sid-7E15C71B-DE9E-4788-B140-A647C99FDC94 + sid-A6DA25CE-636A-46B7-8005-759577956F09 + sid-E3493781-6466-4AED-BAD2-63D115E14820 + + + + + + sid-B6E22A74-A691-453A-A789-B9F8AF787D7C + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + + + + + + sid-CAEAD081-6E73-4C98-8656-C67DA18F5140 + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-9C753C3D-F964-45B0-AF57-234F910529EF + + + + + + sid-9C753C3D-F964-45B0-AF57-234F910529EF + sid-A6DA25CE-636A-46B7-8005-759577956F09 + + + + + + sid-3742C960-71D0-4342-8064-AF1BB9EECB42 + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + + + + + + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-40496205-24D7-494C-AB6B-CD42B8D606EF + + + + + + sid-E3493781-6466-4AED-BAD2-63D115E14820 + sid-12F60C82-D18F-4747-B5B5-34FD40F2C8DE + sid-0895E09C-077C-4D12-8C11-31F28CBC7740 + + + + + + + choice == 'No' + + + choice == 'Yes' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/boundary.bpmn b/tests/SpiffWorkflow/bpmn/data/boundary.bpmn index 00e53ccb..773bdb7d 100644 --- a/tests/SpiffWorkflow/bpmn/data/boundary.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/boundary.bpmn @@ -1,5 +1,5 @@ - + Flow_1pbxbk9 @@ -65,77 +65,46 @@ Flow_0yzqey7 - PT0.03S + "PT0.03S" - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - - - - + + + + - - - - + + + - - + + + + + + - - - - - - + + - - + + - - - - - - - - - - - @@ -145,10 +114,39 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/boundary_timer_on_task.bpmn b/tests/SpiffWorkflow/bpmn/data/boundary_timer_on_task.bpmn index bedebecf..18901a5b 100644 --- a/tests/SpiffWorkflow/bpmn/data/boundary_timer_on_task.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/boundary_timer_on_task.bpmn @@ -1,5 +1,5 @@ - + Flow_164sojd @@ -8,7 +8,7 @@ Flow_0ac4lx5 - timedelta(milliseconds=2) + "PT0.002S" @@ -34,32 +34,23 @@ - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -69,6 +60,15 @@ + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/event-gateway.bpmn b/tests/SpiffWorkflow/bpmn/data/event-gateway.bpmn index afa986ba..1a83c7a4 100644 --- a/tests/SpiffWorkflow/bpmn/data/event-gateway.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/event-gateway.bpmn @@ -27,7 +27,7 @@ Flow_1rfbrlf Flow_0mppjk9 - timedelta(seconds=1) + "PT1S" diff --git a/tests/SpiffWorkflow/bpmn/data/inclusive_gateway.bpmn b/tests/SpiffWorkflow/bpmn/data/inclusive_gateway.bpmn new file mode 100644 index 00000000..8ea4f496 --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/data/inclusive_gateway.bpmn @@ -0,0 +1,131 @@ + + + + + Flow_1g5ma8d + + + + + Flow_1g7lmw3 + default + u_positive + w_positive + + + + u > 0 + + + w > 0 + + + + Flow_0byxq9y + Flow_0hatxr4 + Flow_17jgshs + check_v + + + + + check_v + + + v == 0 + + + Flow_1g5ma8d + Flow_1g7lmw3 + + + default + Flow_17jgshs + v += 1 + + + u_positive + Flow_0byxq9y + u += v + + + w_positive + Flow_0hatxr4 + w += v + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/serialization/v1-1.json b/tests/SpiffWorkflow/bpmn/data/serialization/v1-1.json new file mode 100644 index 00000000..22a3d914 --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/data/serialization/v1-1.json @@ -0,0 +1,590 @@ +{ + "serializer_version": "1.1", + "data": {}, + "last_task": "6bd75ef7-d765-4d0b-83ba-1158d2e9ed0a", + "success": true, + "tasks": { + "ccca8dcf-e833-494a-9281-ec133223ab75": { + "id": "ccca8dcf-e833-494a-9281-ec133223ab75", + "parent": null, + "children": [ + "6bd75ef7-d765-4d0b-83ba-1158d2e9ed0a" + ], + "last_state_change": 1673895963.536834, + "state": 32, + "task_spec": "Root", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "6bd75ef7-d765-4d0b-83ba-1158d2e9ed0a": { + "id": "6bd75ef7-d765-4d0b-83ba-1158d2e9ed0a", + "parent": "ccca8dcf-e833-494a-9281-ec133223ab75", + "children": [ + "34a70bb5-88f7-4a71-9dea-da6893437633" + ], + "last_state_change": 1673896001.8471706, + "state": 32, + "task_spec": "Start", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "34a70bb5-88f7-4a71-9dea-da6893437633": { + "id": "34a70bb5-88f7-4a71-9dea-da6893437633", + "parent": "6bd75ef7-d765-4d0b-83ba-1158d2e9ed0a", + "children": [ + "28d6b82b-64e9-41ad-9620-cc00497c859f", + "deb1eb94-6537-4cb8-a7e6-ea28c4983732" + ], + "last_state_change": 1673896001.8511543, + "state": 8, + "task_spec": "StartEvent_1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": { + "repeat": 2, + "start_time": "2023-01-16 14:06:41.847524" + }, + "data": { + "repeat_count": 0 + } + }, + "28d6b82b-64e9-41ad-9620-cc00497c859f": { + "id": "28d6b82b-64e9-41ad-9620-cc00497c859f", + "parent": "34a70bb5-88f7-4a71-9dea-da6893437633", + "children": [ + "a07abe4c-8122-494f-8b43-b3c44d3f7e34", + "975c49d4-dceb-4137-829c-64c657894701" + ], + "last_state_change": 1673895963.5375524, + "state": 4, + "task_spec": "StartEvent_1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "a07abe4c-8122-494f-8b43-b3c44d3f7e34": { + "id": "a07abe4c-8122-494f-8b43-b3c44d3f7e34", + "parent": "28d6b82b-64e9-41ad-9620-cc00497c859f", + "children": [], + "last_state_change": 1673895963.5380332, + "state": 4, + "task_spec": "return_to_StartEvent_1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "975c49d4-dceb-4137-829c-64c657894701": { + "id": "975c49d4-dceb-4137-829c-64c657894701", + "parent": "28d6b82b-64e9-41ad-9620-cc00497c859f", + "children": [ + "e937e37e-04a7-45b6-b6a4-7615876ae19f" + ], + "last_state_change": 1673895963.5380924, + "state": 4, + "task_spec": "task_1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "e937e37e-04a7-45b6-b6a4-7615876ae19f": { + "id": "e937e37e-04a7-45b6-b6a4-7615876ae19f", + "parent": "975c49d4-dceb-4137-829c-64c657894701", + "children": [ + "42036b41-f934-457a-86e7-cc3b65882073" + ], + "last_state_change": 1673895963.5384276, + "state": 4, + "task_spec": "Event_1uhhxu1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "42036b41-f934-457a-86e7-cc3b65882073": { + "id": "42036b41-f934-457a-86e7-cc3b65882073", + "parent": "e937e37e-04a7-45b6-b6a4-7615876ae19f", + "children": [ + "583f99df-db90-4237-8718-c1cf638c4391", + "24f73965-e218-4f94-8ca9-5267cbf635ad" + ], + "last_state_change": 1673895963.5386448, + "state": 4, + "task_spec": "task_2.BoundaryEventParent", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "583f99df-db90-4237-8718-c1cf638c4391": { + "id": "583f99df-db90-4237-8718-c1cf638c4391", + "parent": "42036b41-f934-457a-86e7-cc3b65882073", + "children": [ + "825b0a7b-b33e-4692-a11c-9c0fb49e20c0" + ], + "last_state_change": 1673895963.5391376, + "state": 4, + "task_spec": "task_2", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "825b0a7b-b33e-4692-a11c-9c0fb49e20c0": { + "id": "825b0a7b-b33e-4692-a11c-9c0fb49e20c0", + "parent": "583f99df-db90-4237-8718-c1cf638c4391", + "children": [ + "e5b6e43e-2ba9-42a2-90c0-25202f096273" + ], + "last_state_change": 1673895963.5393665, + "state": 4, + "task_spec": "Event_1vzwq7p", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "e5b6e43e-2ba9-42a2-90c0-25202f096273": { + "id": "e5b6e43e-2ba9-42a2-90c0-25202f096273", + "parent": "825b0a7b-b33e-4692-a11c-9c0fb49e20c0", + "children": [ + "0a5fbc15-2807-463d-9762-b7b724835a40" + ], + "last_state_change": 1673895963.5396175, + "state": 4, + "task_spec": "migration_test.EndJoin", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "0a5fbc15-2807-463d-9762-b7b724835a40": { + "id": "0a5fbc15-2807-463d-9762-b7b724835a40", + "parent": "e5b6e43e-2ba9-42a2-90c0-25202f096273", + "children": [], + "last_state_change": 1673895963.5398767, + "state": 4, + "task_spec": "End", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "24f73965-e218-4f94-8ca9-5267cbf635ad": { + "id": "24f73965-e218-4f94-8ca9-5267cbf635ad", + "parent": "42036b41-f934-457a-86e7-cc3b65882073", + "children": [], + "last_state_change": 1673895963.5390024, + "state": 1, + "task_spec": "Event_1bkh7yi", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "deb1eb94-6537-4cb8-a7e6-ea28c4983732": { + "id": "deb1eb94-6537-4cb8-a7e6-ea28c4983732", + "parent": "34a70bb5-88f7-4a71-9dea-da6893437633", + "children": [ + "56045c5b-9400-4fea-ae82-b357268672f8" + ], + "last_state_change": 1673895963.5376105, + "state": 4, + "task_spec": "task_1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "56045c5b-9400-4fea-ae82-b357268672f8": { + "id": "56045c5b-9400-4fea-ae82-b357268672f8", + "parent": "deb1eb94-6537-4cb8-a7e6-ea28c4983732", + "children": [ + "fd71446e-d8b4-42dd-980b-624a82848853" + ], + "last_state_change": 1673895963.540343, + "state": 4, + "task_spec": "Event_1uhhxu1", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "fd71446e-d8b4-42dd-980b-624a82848853": { + "id": "fd71446e-d8b4-42dd-980b-624a82848853", + "parent": "56045c5b-9400-4fea-ae82-b357268672f8", + "children": [ + "8960300d-28c1-463d-b7a1-29e872c02d98", + "a6bca2b5-7064-49c9-a172-18c1435d173d" + ], + "last_state_change": 1673895963.5405483, + "state": 4, + "task_spec": "task_2.BoundaryEventParent", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "8960300d-28c1-463d-b7a1-29e872c02d98": { + "id": "8960300d-28c1-463d-b7a1-29e872c02d98", + "parent": "fd71446e-d8b4-42dd-980b-624a82848853", + "children": [ + "6b33df30-07fd-4572-abfd-f29eb9906f0b" + ], + "last_state_change": 1673895963.540891, + "state": 4, + "task_spec": "task_2", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "6b33df30-07fd-4572-abfd-f29eb9906f0b": { + "id": "6b33df30-07fd-4572-abfd-f29eb9906f0b", + "parent": "8960300d-28c1-463d-b7a1-29e872c02d98", + "children": [ + "0d571ca2-ffb1-4d8b-9dda-4e8cbd8b8ae1" + ], + "last_state_change": 1673895963.5411036, + "state": 4, + "task_spec": "Event_1vzwq7p", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "0d571ca2-ffb1-4d8b-9dda-4e8cbd8b8ae1": { + "id": "0d571ca2-ffb1-4d8b-9dda-4e8cbd8b8ae1", + "parent": "6b33df30-07fd-4572-abfd-f29eb9906f0b", + "children": [ + "1e8fc80a-1968-4411-a10b-16cb4261f3b8" + ], + "last_state_change": 1673895963.5413618, + "state": 4, + "task_spec": "migration_test.EndJoin", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "1e8fc80a-1968-4411-a10b-16cb4261f3b8": { + "id": "1e8fc80a-1968-4411-a10b-16cb4261f3b8", + "parent": "0d571ca2-ffb1-4d8b-9dda-4e8cbd8b8ae1", + "children": [], + "last_state_change": 1673895963.541654, + "state": 4, + "task_spec": "End", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + }, + "a6bca2b5-7064-49c9-a172-18c1435d173d": { + "id": "a6bca2b5-7064-49c9-a172-18c1435d173d", + "parent": "fd71446e-d8b4-42dd-980b-624a82848853", + "children": [], + "last_state_change": 1673895963.5408392, + "state": 1, + "task_spec": "Event_1bkh7yi", + "triggered": false, + "workflow_name": "migration_test", + "internal_data": {}, + "data": {} + } + }, + "root": "ccca8dcf-e833-494a-9281-ec133223ab75", + "spec": { + "name": "migration_test", + "description": "migration_test", + "file": "/home/essweine/work/sartography/code/SpiffWorkflow/tests/SpiffWorkflow/bpmn/data/serialization/v1-1.bpmn", + "task_specs": { + "Start": { + "id": "migration_test_1", + "name": "Start", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [], + "outputs": [ + "StartEvent_1" + ], + "typename": "StartTask" + }, + "migration_test.EndJoin": { + "id": "migration_test_2", + "name": "migration_test.EndJoin", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "Event_1vzwq7p" + ], + "outputs": [ + "End" + ], + "typename": "_EndJoin" + }, + "End": { + "id": "migration_test_3", + "name": "End", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "migration_test.EndJoin" + ], + "outputs": [], + "typename": "Simple" + }, + "StartEvent_1": { + "id": "migration_test_4", + "name": "StartEvent_1", + "description": null, + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "Start", + "StartEvent_1" + ], + "outputs": [ + "StartEvent_1", + "task_1" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 179.0, + "y": 79.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "event_definition": { + "internal": true, + "external": true, + "label": "StartEvent_1", + "cycle_definition": "(2,timedelta(seconds=0.1))", + "typename": "CycleTimerEventDefinition" + }, + "typename": "StartEvent", + "extensions": {} + }, + "task_1": { + "id": "migration_test_5", + "name": "task_1", + "description": "Task 1", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "StartEvent_1" + ], + "outputs": [ + "Event_1uhhxu1" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 290.0, + "y": 57.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "script": "pass", + "typename": "ScriptTask", + "extensions": {} + }, + "Event_1uhhxu1": { + "id": "migration_test_6", + "name": "Event_1uhhxu1", + "description": null, + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "task_1" + ], + "outputs": [ + "task_2.BoundaryEventParent" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 452.0, + "y": 79.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "event_definition": { + "internal": true, + "external": true, + "label": "Event_1uhhxu1", + "dateTime": "datetime(2023, 1, 1)", + "typename": "TimerEventDefinition" + }, + "typename": "IntermediateCatchEvent", + "extensions": {} + }, + "task_2": { + "id": "migration_test_7", + "name": "task_2", + "description": "Task 2", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "task_2.BoundaryEventParent" + ], + "outputs": [ + "Event_1vzwq7p" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 560.0, + "y": 57.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "script": "time.sleep(0.5)", + "typename": "ScriptTask", + "extensions": {} + }, + "task_2.BoundaryEventParent": { + "id": "migration_test_8", + "name": "task_2.BoundaryEventParent", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "Event_1uhhxu1" + ], + "outputs": [ + "task_2", + "Event_1bkh7yi" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 0, + "y": 0 + }, + "data_input_associations": [], + "data_output_associations": [], + "main_child_task_spec": "task_2", + "typename": "_BoundaryEventParent" + }, + "Event_1bkh7yi": { + "id": "migration_test_9", + "name": "Event_1bkh7yi", + "description": null, + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "task_2.BoundaryEventParent" + ], + "outputs": [], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 592.0, + "y": 119.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "event_definition": { + "internal": true, + "external": true, + "label": "Event_1bkh7yi", + "dateTime": "timedelta(seconds=0.1)", + "typename": "TimerEventDefinition" + }, + "cancel_activity": true, + "typename": "BoundaryEvent", + "extensions": {} + }, + "Event_1vzwq7p": { + "id": "migration_test_10", + "name": "Event_1vzwq7p", + "description": null, + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "task_2" + ], + "outputs": [ + "migration_test.EndJoin" + ], + "lane": null, + "documentation": null, + "loopTask": false, + "position": { + "x": 742.0, + "y": 79.0 + }, + "data_input_associations": [], + "data_output_associations": [], + "event_definition": { + "internal": false, + "external": false, + "typename": "NoneEventDefinition" + }, + "typename": "EndEvent", + "extensions": {} + }, + "Root": { + "id": "migration_test_11", + "name": "Root", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [], + "outputs": [], + "typename": "Simple" + }, + "return_to_StartEvent_1": { + "id": "migration_test_12", + "name": "return_to_StartEvent_1", + "description": "", + "manual": false, + "internal": false, + "lookahead": 2, + "inputs": [ + "Start", + "StartEvent_1" + ], + "outputs": [], + "destination_id": "34a70bb5-88f7-4a71-9dea-da6893437633", + "destination_spec_name": "StartEvent_1", + "typename": "LoopResetTask" + } + }, + "data_inputs": [], + "data_outputs": [], + "data_objects": {}, + "correlation_keys": {}, + "typename": "BpmnProcessSpec" + }, + "subprocess_specs": {}, + "subprocesses": {}, + "bpmn_messages": [] +} diff --git a/tests/SpiffWorkflow/bpmn/data/timer-cycle-start.bpmn b/tests/SpiffWorkflow/bpmn/data/timer-cycle-start.bpmn index 9d75c9c9..a79e4778 100644 --- a/tests/SpiffWorkflow/bpmn/data/timer-cycle-start.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/timer-cycle-start.bpmn @@ -22,7 +22,7 @@ Flow_0jtfzsk - (2,timedelta(seconds=0.1)) + "R2/PT0.1S" @@ -37,7 +37,7 @@ Flow_1pahvlr Flow_05ejbm4 - timedelta(seconds=0.5) + "PT0.5S" diff --git a/tests/SpiffWorkflow/bpmn/data/timer-cycle.bpmn b/tests/SpiffWorkflow/bpmn/data/timer-cycle.bpmn index 9252d2f5..0c4a62cc 100644 --- a/tests/SpiffWorkflow/bpmn/data/timer-cycle.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/timer-cycle.bpmn @@ -1,5 +1,5 @@ - + Flow_1pahvlr @@ -32,12 +32,32 @@ Flow_1pzc4jz - (2,timedelta(seconds=0.01)) + "R2/PT0.1S" + + + + + + + + + + + + + + + + + + + + @@ -50,29 +70,9 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/tests/SpiffWorkflow/bpmn/data/timer-date-start.bpmn b/tests/SpiffWorkflow/bpmn/data/timer-date-start.bpmn index 46f907b9..f8b341c0 100644 --- a/tests/SpiffWorkflow/bpmn/data/timer-date-start.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/timer-date-start.bpmn @@ -1,17 +1,10 @@ - + Flow_1i73q45 - - Flow_1i73q45 - Flow_00e79cz - futuredate = datetime.now() + timedelta(0, 1) - timedelta(seconds=.95) -futuredate2 = datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M') - - Flow_00e79cz Flow_1bdrcxy @@ -19,54 +12,46 @@ futuredate2 = datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M')futuredate - - - Flow_1bdrcxy - Flow_0bjksyv - print('yay!') -completed = True - + - Flow_0bjksyv + Flow_1bdrcxy - + + Flow_1i73q45 + Flow_00e79cz + futuredate = (datetime.now() + timedelta(seconds=0.05)).isoformat() + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + - - - - diff --git a/tests/SpiffWorkflow/bpmn/data/timer-non-interrupt-boundary.bpmn b/tests/SpiffWorkflow/bpmn/data/timer-non-interrupt-boundary.bpmn index 25d750e2..d44078c5 100644 --- a/tests/SpiffWorkflow/bpmn/data/timer-non-interrupt-boundary.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/timer-non-interrupt-boundary.bpmn @@ -1,5 +1,5 @@ - + Flow_1hyztad @@ -38,7 +38,7 @@ Flow_03e1mfr - timedelta(seconds=.2) + "PT0.2S" @@ -59,74 +59,46 @@ Flow_0tlkkap Flow_0vper9q - + - Flow_0or6odg - - - - - - - - Flow_0vper9q - Flow_0or6odg - + - - - - - - - - - - - - - - - - - - - - - + + + - - - + + + - - - + + + - - - + + + - - + + + + + + + + + - + - - - - - - - @@ -137,32 +109,43 @@ + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + - - - - - - + + + + + diff --git a/tests/SpiffWorkflow/bpmn/data/timer.bpmn b/tests/SpiffWorkflow/bpmn/data/timer.bpmn index fa26d75f..9700df6a 100644 --- a/tests/SpiffWorkflow/bpmn/data/timer.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/timer.bpmn @@ -1,68 +1,44 @@ - + Flow_1pahvlr - - Flow_1pahvlr - Flow_1pvkgnu - - Flow_1pvkgnu + Flow_1pahvlr Flow_1elbn9u - timedelta(seconds=.25) + "PT0.25S" - - Flow_1elbn9u - Flow_1ekgt3x - - Flow_1ekgt3x + Flow_1elbn9u - - - - + + + + + + + + + + - - - - + - + - - - - + - - - - - - - - - - - - - - - - diff --git a/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn b/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn index 8353d16d..97f663c6 100644 --- a/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn @@ -1,5 +1,5 @@ - + Flow_1gb8wca @@ -25,7 +25,7 @@ Days elapsed: {{days_delta }} Flow_0op1a19 Flow_1gb8wca - timedelta(milliseconds=10) + "PT.01S" @@ -57,6 +57,14 @@ Days elapsed: {{days_delta }} + + + + + + + + @@ -85,16 +93,8 @@ Days elapsed: {{days_delta }} - - - - - - - - - - + + @@ -114,12 +114,12 @@ Days elapsed: {{days_delta }} - - - + + + diff --git a/tests/SpiffWorkflow/bpmn/data/too_many_loops_sub_process.bpmn b/tests/SpiffWorkflow/bpmn/data/too_many_loops_sub_process.bpmn index 2f6a8b53..ed1461db 100644 --- a/tests/SpiffWorkflow/bpmn/data/too_many_loops_sub_process.bpmn +++ b/tests/SpiffWorkflow/bpmn/data/too_many_loops_sub_process.bpmn @@ -1,5 +1,5 @@ - + Flow_0q7fkb7 @@ -29,7 +29,7 @@ Days elapsed: {{days_delta }} Flow_1ivr6d7 Flow_1gb8wca - timedelta(milliseconds=10) + "PT0.01S" diff --git a/tests/SpiffWorkflow/bpmn/events/ActionManagementTest.py b/tests/SpiffWorkflow/bpmn/events/ActionManagementTest.py index 1324d027..0bd5c578 100644 --- a/tests/SpiffWorkflow/bpmn/events/ActionManagementTest.py +++ b/tests/SpiffWorkflow/bpmn/events/ActionManagementTest.py @@ -15,7 +15,7 @@ class ActionManagementTest(BpmnWorkflowTestCase): FINISH_TIME_DELTA=0.10 def now_plus_seconds(self, seconds): - return datetime.datetime.now() + datetime.timedelta(seconds=seconds) + return (datetime.datetime.now() + datetime.timedelta(seconds=seconds)).isoformat() def setUp(self): self.spec, self.subprocesses = self.load_workflow_spec('Test-Workflows/Action-Management.bpmn20.xml', 'Action Management') diff --git a/tests/SpiffWorkflow/bpmn/events/TimeDurationParseTest.py b/tests/SpiffWorkflow/bpmn/events/TimeDurationParseTest.py new file mode 100644 index 00000000..cb4381a5 --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/events/TimeDurationParseTest.py @@ -0,0 +1,60 @@ +import unittest +from datetime import datetime + +from SpiffWorkflow.bpmn.specs.events.event_definitions import TimerEventDefinition + +class TimeDurationParseTest(unittest.TestCase): + "Non-exhaustive ISO durations, but hopefully covers basic support" + + def test_parse_duration(self): + + valid = [ + ("P1Y6M1DT1H1M1S", {'years': 1, 'months': 6, 'days': 1, 'hours': 1, 'minutes': 1, 'seconds': 1 }), # everything + ("P1Y6M1DT1H1M1.5S", {'years': 1, 'months': 6, 'days': 1, 'hours': 1, 'minutes': 1, 'seconds': 1.5 }), # fractional seconds + ("P1YT1H1M1S", {'years': 1, 'hours': 1, 'minutes': 1, 'seconds': 1 }), # minutes but no month + ("P1MT1H", {'months': 1, 'hours':1}), # months but no minutes + ("P4W", {'weeks': 4}), # weeks + ("P1Y6M1D", {'years': 1, 'months': 6, 'days': 1}), # no time + ("PT1H1M1S", {'hours': 1,'minutes': 1,'seconds': 1}), # time only + ("PT1.5H", {'hours': 1.5}), # alt fractional + ("T1,5H", {'hours': 1.5}), # fractional with comma + ("PDT1H1M1S", {'hours': 1, 'minutes': 1, 'seconds': 1}), # empty spec + ("PYMDT1H1M1S", {'hours': 1, 'minutes': 1, 'seconds': 1}), # multiple empty + ] + for duration, parsed_duration in valid: + result = TimerEventDefinition.parse_iso_duration(duration) + self.assertDictEqual(result, parsed_duration) + + invalid = [ + "PT1.5H30S", # fractional duration with subsequent non-fractional + "PT1,5H30S", # with comma + "P1H1M1S", # missing 't' + "P1DT", # 't' without time spec + "P1W1D", # conflicting day specs + "PT1H1M1", # trailing values + ] + for duration in invalid: + self.assertRaises(Exception, TimerEventDefinition.parse_iso_duration, duration) + + def test_calculate_timedelta_from_start(self): + + start, one_day = datetime.fromisoformat("2023-01-01"), 24 * 3600 + # Leap years + self.assertEqual(TimerEventDefinition.get_timedelta_from_start({'years': 1}, start).total_seconds(), 365 * one_day) + self.assertEqual(TimerEventDefinition.get_timedelta_from_start({'years': 2}, start).total_seconds(), (365 + 366) * one_day) + # Increment by month does not change day + for month in range(1, 13): + dt = start + TimerEventDefinition.get_timedelta_from_start({'months': month}, start) + self.assertEqual(dt.day, 1) + + def test_calculate_timedelta_from_end(self): + end, one_day = datetime.fromisoformat("2025-01-01"), 24 * 3600 + # Leap years + self.assertEqual(TimerEventDefinition.get_timedelta_from_end({'years': 1}, end).total_seconds(), 366 * one_day) + self.assertEqual(TimerEventDefinition.get_timedelta_from_end({'years': 2}, end).total_seconds(), (365 + 366) * one_day) + + dt = end - TimerEventDefinition.get_timedelta_from_end({'months': 11}, end) + # Decrement by month does not change day + for month in range(1, 13): + dt = end - TimerEventDefinition.get_timedelta_from_end({'months': month}, end) + self.assertEqual(dt.day, 1) \ No newline at end of file diff --git a/tests/SpiffWorkflow/bpmn/events/TimerCycleStartTest.py b/tests/SpiffWorkflow/bpmn/events/TimerCycleStartTest.py index 98de249c..bf89912c 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerCycleStartTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerCycleStartTest.py @@ -5,12 +5,14 @@ import unittest import time from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine -from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase __author__ = 'kellym' +# the data doesn't really propagate to the end as in a 'normal' workflow, so I call a +# custom function that records the number of times this got called so that +# we can keep track of how many times the triggered item gets called. counter = 0 def my_custom_function(): global counter @@ -41,31 +43,24 @@ class TimerCycleStartTest(BpmnWorkflowTestCase): def testThroughSaveRestore(self): self.actual_test(save_restore=True) - def actual_test(self,save_restore = False): global counter - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) # Start Event - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() - - # the data doesn't really propagate to the end as in a 'normal' workflow, so I call a - # custom function that records the number of times this got called so that - # we can keep track of how many times the triggered item gets called. counter = 0 - # We have a loop so we can continue to execute waiting tasks when # timers expire. The test workflow has a wait timer that pauses long enough to - # allow the cycle to complete twice -- otherwise the first iteration through the - # cycle process causes the remaining tasks to be cancelled. - for loopcount in range(5): + # allow the cycle to complete three times before being cancelled by the terminate + # event (the timer should only run twice, we want to make sure it doesn't keep + # executing) + for loopcount in range(6): + self.workflow.do_engine_steps() if save_restore: self.save_restore() self.workflow.script_engine = CustomScriptEngine() time.sleep(0.1) self.workflow.refresh_waiting_tasks() - self.workflow.do_engine_steps() + self.assertEqual(counter, 2) + self.assertTrue(self.workflow.is_completed()) def suite(): diff --git a/tests/SpiffWorkflow/bpmn/events/TimerCycleTest.py b/tests/SpiffWorkflow/bpmn/events/TimerCycleTest.py index 56b52730..5c61f381 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerCycleTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerCycleTest.py @@ -30,7 +30,7 @@ class CustomScriptEngine(PythonScriptEngine): -class TimerDurationTest(BpmnWorkflowTestCase): +class TimerCycleTest(BpmnWorkflowTestCase): def setUp(self): self.spec, self.subprocesses = self.load_workflow_spec('timer-cycle.bpmn', 'timer') @@ -42,31 +42,34 @@ class TimerDurationTest(BpmnWorkflowTestCase): def testThroughSaveRestore(self): self.actual_test(save_restore=True) - def actual_test(self,save_restore = False): global counter - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) # Start Event - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) # GetCoffee - - # See comments in timer cycle test for more context counter = 0 + # See comments in timer cycle test start for more context for loopcount in range(5): + self.workflow.do_engine_steps() if save_restore: self.save_restore() self.workflow.script_engine = CustomScriptEngine() - time.sleep(0.01) + time.sleep(0.05) self.workflow.refresh_waiting_tasks() - self.workflow.do_engine_steps() - - pass - #self.assertEqual(counter, 2) + events = self.workflow.waiting_events() + if loopcount == 0: + # Wait time is 0.1s, so the first time through, there should still be a waiting event + self.assertEqual(len(events), 1) + else: + # By the second iteration, both should be complete + self.assertEqual(len(events), 0) + # Get coffee still ready + coffee = self.workflow.get_tasks_from_spec_name('Get_Coffee')[0] + self.assertEqual(coffee.state, TaskState.READY) + # Timer completed + timer = self.workflow.get_tasks_from_spec_name('CatchMessage')[0] + self.assertEqual(timer.state, TaskState.COMPLETED) + self.assertEqual(counter, 2) def suite(): - return unittest.TestLoader().loadTestsFromTestCase(TimerDurationTest) + return unittest.TestLoader().loadTestsFromTestCase(TimerCycleTest) if __name__ == '__main__': unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/tests/SpiffWorkflow/bpmn/events/TimerDateTest.py b/tests/SpiffWorkflow/bpmn/events/TimerDateTest.py index d6157ecd..deebd775 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerDateTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerDateTest.py @@ -4,7 +4,6 @@ import unittest import datetime import time -from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase @@ -28,37 +27,22 @@ class TimerDateTest(BpmnWorkflowTestCase): def testThroughSaveRestore(self): self.actual_test(save_restore=True) - def actual_test(self,save_restore = False): - global counter - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) # Start Event - self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.do_engine_steps() - + self.assertEqual(len(self.workflow.waiting_events()), 1) loopcount = 0 - # test bpmn has a timeout of .05s - # we should terminate loop before that. starttime = datetime.datetime.now() - counter = 0 - while loopcount < 8: - if len(self.workflow.get_tasks(TaskState.READY)) >= 1: - break + # test bpmn has a timeout of .05s; we should terminate loop before that. + while len(self.workflow.get_waiting_tasks()) > 0 and loopcount < 8: if save_restore: self.save_restore() self.workflow.script_engine = self.script_engine - - - waiting_tasks = self.workflow.get_tasks(TaskState.WAITING) time.sleep(0.01) self.workflow.refresh_waiting_tasks() - loopcount = loopcount +1 + loopcount += 1 endtime = datetime.datetime.now() self.workflow.do_engine_steps() - testdate = datetime.datetime.strptime('2021-09-01 10:00','%Y-%m-%d %H:%M') - self.assertEqual(self.workflow.last_task.data['futuredate2'],testdate) - self.assertTrue('completed' in self.workflow.last_task.data) - self.assertTrue(self.workflow.last_task.data['completed']) + self.assertTrue(self.workflow.is_completed()) self.assertTrue((endtime-starttime) > datetime.timedelta(seconds=.02)) diff --git a/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryOnTaskTest.py b/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryOnTaskTest.py index 9bd1f322..aff5d429 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryOnTaskTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryOnTaskTest.py @@ -1,12 +1,9 @@ # -*- coding: utf-8 -*- import unittest -import datetime import time from datetime import timedelta -from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent -from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase @@ -27,45 +24,23 @@ class TimerDurationTest(BpmnWorkflowTestCase): self.actual_test(save_restore=True) def actual_test(self,save_restore = False): - # In the normal flow of things, the final end event should be the last task - self.workflow.do_engine_steps() - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() - self.assertTrue(self.workflow.is_completed()) - end_events = [] - for task in self.workflow.get_tasks(): - if isinstance(task.task_spec, EndEvent): - end_events.append(task) - self.assertEqual(1, len(end_events)) - - # In the event of a timer firing, the last task should STILL - # be the final end event. - - starttime = datetime.datetime.now() - self.workflow = BpmnWorkflow(self.spec) - self.workflow.script_engine = self.script_engine self.workflow.do_engine_steps() if save_restore: self.save_restore() self.workflow.script_engine = self.script_engine - time.sleep(0.1) + time.sleep(1) self.workflow.refresh_waiting_tasks() self.workflow.do_engine_steps() + + # Make sure the timer got called + self.assertEqual(self.workflow.last_task.data['timer_called'],True) + + # Make sure the task can still be called. task = self.workflow.get_ready_user_tasks()[0] - self.workflow.complete_task_from_id(task.id) + task.complete() self.workflow.do_engine_steps() - self.assertTrue(self.workflow.is_completed()) - end_events = [] - - for task in self.workflow.get_tasks(): - if isinstance(task.task_spec, EndEvent): - end_events.append(task) - self.assertEqual(1, len(end_events)) - def suite(): diff --git a/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryTest.py b/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryTest.py index 2297aa27..ce248dae 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerDurationBoundaryTest.py @@ -3,7 +3,6 @@ import unittest import time -from SpiffWorkflow.bpmn.FeelLikeScriptEngine import FeelLikeScriptEngine from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase @@ -23,35 +22,31 @@ class TimerDurationTest(BpmnWorkflowTestCase): self.actual_test(save_restore=True) def actual_test(self,save_restore = False): - self.workflow.script_engine = FeelLikeScriptEngine() - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.do_engine_steps() ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - ready_tasks[0].data['answer']='No' - self.workflow.complete_task_from_id(ready_tasks[0].id) + ready_tasks[0].complete() self.workflow.do_engine_steps() loopcount = 0 - # test bpmn has a timeout of .03s - # we should terminate loop before that. - - while loopcount < 11: - ready_tasks = self.workflow.get_tasks(TaskState.READY) - if len(ready_tasks) < 1: - break + # test bpmn has a timeout of .03s; we should terminate loop before that. + while len(self.workflow.get_waiting_tasks()) == 2 and loopcount < 11: if save_restore: self.save_restore() - self.workflow.script_engine = FeelLikeScriptEngine() - #self.assertEqual(1, len(self.workflow.get_tasks(Task.WAITING))) time.sleep(0.01) - self.workflow.complete_task_from_id(ready_tasks[0].id) + self.assertEqual(len(self.workflow.get_tasks(TaskState.READY)), 1) self.workflow.refresh_waiting_tasks() self.workflow.do_engine_steps() - loopcount = loopcount +1 + loopcount += 1 + self.workflow.do_engine_steps() + subworkflow = self.workflow.get_tasks_from_spec_name('Subworkflow')[0] + self.assertEqual(subworkflow.state, TaskState.CANCELLED) + ready_tasks = self.workflow.get_ready_user_tasks() + while len(ready_tasks) > 0: + ready_tasks[0].complete() + ready_tasks = self.workflow.get_ready_user_tasks() + self.workflow.do_engine_steps() + self.assertTrue(self.workflow.is_completed()) # Assure that the loopcount is less than 10, and the timer interrupt fired, rather # than allowing us to continue to loop the full 10 times. self.assertTrue(loopcount < 10) diff --git a/tests/SpiffWorkflow/bpmn/events/TimerDurationTest.py b/tests/SpiffWorkflow/bpmn/events/TimerDurationTest.py index 8026483d..c8e72fcd 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerDurationTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerDurationTest.py @@ -1,10 +1,8 @@ # -*- coding: utf-8 -*- import unittest -import datetime import time -from datetime import timedelta -from SpiffWorkflow.task import TaskState +from datetime import datetime, timedelta from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase @@ -25,35 +23,25 @@ class TimerDurationTest(BpmnWorkflowTestCase): def testThroughSaveRestore(self): self.actual_test(save_restore=True) - def actual_test(self,save_restore = False): - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - self.workflow.complete_task_from_id(ready_tasks[0].id) - self.workflow.do_engine_steps() - ready_tasks = self.workflow.get_tasks(TaskState.READY) - self.assertEqual(1, len(ready_tasks)) - self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.do_engine_steps() + self.assertEqual(len(self.workflow.waiting_events()), 1) loopcount = 0 - # test bpmn has a timeout of .25s - # we should terminate loop before that. - starttime = datetime.datetime.now() - while loopcount < 10: - if len(self.workflow.get_tasks(TaskState.READY)) >= 1: - break + starttime = datetime.now() + # test bpmn has a timeout of .25s; we should terminate loop before that. + while len(self.workflow.get_waiting_tasks()) > 0 and loopcount < 10: if save_restore: self.save_restore() self.workflow.script_engine = self.script_engine - self.assertEqual(1, len(self.workflow.get_tasks(TaskState.WAITING))) time.sleep(0.1) self.workflow.refresh_waiting_tasks() - loopcount = loopcount +1 - endtime = datetime.datetime.now() - duration = endtime-starttime - self.assertEqual(durationdatetime.timedelta(seconds=.2),True) + loopcount += 1 + endtime = datetime.now() + duration = endtime - starttime + self.assertEqual(duration < timedelta(seconds=.5), True) + self.assertEqual(duration > timedelta(seconds=.2), True) + self.assertEqual(len(self.workflow.waiting_events()), 0) def suite(): diff --git a/tests/SpiffWorkflow/bpmn/events/TimerIntermediateTest.py b/tests/SpiffWorkflow/bpmn/events/TimerIntermediateTest.py index 6d8c256a..f1dce3c5 100644 --- a/tests/SpiffWorkflow/bpmn/events/TimerIntermediateTest.py +++ b/tests/SpiffWorkflow/bpmn/events/TimerIntermediateTest.py @@ -18,7 +18,7 @@ class TimerIntermediateTest(BpmnWorkflowTestCase): def testRunThroughHappy(self): - due_time = datetime.datetime.now() + datetime.timedelta(seconds=0.01) + due_time = (datetime.datetime.now() + datetime.timedelta(seconds=0.01)).isoformat() self.assertEqual(1, len(self.workflow.get_tasks(TaskState.READY))) self.workflow.get_tasks(TaskState.READY)[0].set_data(due_time=due_time) diff --git a/tests/SpiffWorkflow/bpmn/serializer/BaseTestCase.py b/tests/SpiffWorkflow/bpmn/serializer/BaseTestCase.py new file mode 100644 index 00000000..e392e5db --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/serializer/BaseTestCase.py @@ -0,0 +1,27 @@ +import unittest +import os + +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow +from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnParser +from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer +from tests.SpiffWorkflow.bpmn.BpmnLoaderForTests import TestUserTaskConverter + + +class BaseTestCase(unittest.TestCase): + + SERIALIZER_VERSION = "100.1.ANY" + DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') + + def load_workflow_spec(self, filename, process_name): + parser = BpmnParser() + parser.add_bpmn_files_by_glob(os.path.join(self.DATA_DIR, filename)) + top_level_spec = parser.get_spec(process_name) + subprocesses = parser.get_subprocess_specs(process_name) + return top_level_spec, subprocesses + + def setUp(self): + super(BaseTestCase, self).setUp() + wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter([TestUserTaskConverter]) + self.serializer = BpmnWorkflowSerializer(wf_spec_converter, version=self.SERIALIZER_VERSION) + spec, subprocesses = self.load_workflow_spec('random_fact.bpmn', 'random_fact') + self.workflow = BpmnWorkflow(spec, subprocesses) diff --git a/tests/SpiffWorkflow/bpmn/BpmnWorkflowSerializerTest.py b/tests/SpiffWorkflow/bpmn/serializer/BpmnWorkflowSerializerTest.py similarity index 80% rename from tests/SpiffWorkflow/bpmn/BpmnWorkflowSerializerTest.py rename to tests/SpiffWorkflow/bpmn/serializer/BpmnWorkflowSerializerTest.py index e5b77156..ed547952 100644 --- a/tests/SpiffWorkflow/bpmn/BpmnWorkflowSerializerTest.py +++ b/tests/SpiffWorkflow/bpmn/serializer/BpmnWorkflowSerializerTest.py @@ -1,33 +1,18 @@ -import os import unittest +import os import json -from SpiffWorkflow.task import TaskState from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine -from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnParser from SpiffWorkflow.bpmn.serializer.workflow import BpmnWorkflowSerializer from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from tests.SpiffWorkflow.bpmn.BpmnLoaderForTests import TestUserTaskConverter +from .BaseTestCase import BaseTestCase + +class BpmnWorkflowSerializerTest(BaseTestCase): -class BpmnWorkflowSerializerTest(unittest.TestCase): - """Please note that the BpmnSerializer is Deprecated.""" SERIALIZER_VERSION = "100.1.ANY" - - def load_workflow_spec(self, filename, process_name): - f = os.path.join(os.path.dirname(__file__), 'data', filename) - parser = BpmnParser() - parser.add_bpmn_files_by_glob(f) - top_level_spec = parser.get_spec(process_name) - subprocesses = parser.get_subprocess_specs(process_name) - return top_level_spec, subprocesses - - def setUp(self): - super(BpmnWorkflowSerializerTest, self).setUp() - wf_spec_converter = BpmnWorkflowSerializer.configure_workflow_spec_converter([TestUserTaskConverter]) - self.serializer = BpmnWorkflowSerializer(wf_spec_converter, version=self.SERIALIZER_VERSION) - spec, subprocesses = self.load_workflow_spec('random_fact.bpmn', 'random_fact') - self.workflow = BpmnWorkflow(spec, subprocesses) + DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data') def testSerializeWorkflowSpec(self): spec_serialized = self.serializer.serialize_json(self.workflow) @@ -102,12 +87,6 @@ class BpmnWorkflowSerializerTest(unittest.TestCase): def testDeserializeWorkflow(self): self._compare_with_deserialized_copy(self.workflow) - def testSerializeTask(self): - self.serializer.serialize_json(self.workflow) - - def testDeserializeTask(self): - self._compare_with_deserialized_copy(self.workflow) - def testDeserializeActiveWorkflow(self): self.workflow.do_engine_steps() self._compare_with_deserialized_copy(self.workflow) @@ -161,17 +140,6 @@ class BpmnWorkflowSerializerTest(unittest.TestCase): self.assertIsNotNone(wf2.last_task) self._compare_workflows(self.workflow, wf2) - def test_convert_1_0_to_1_1(self): - # The serialization used here comes from NestedSubprocessTest saved at line 25 with version 1.0 - fn = os.path.join(os.path.dirname(__file__), 'data', 'serialization', 'v1.0.json') - wf = self.serializer.deserialize_json(open(fn).read()) - # We should be able to finish the workflow from this point - ready_tasks = wf.get_tasks(TaskState.READY) - self.assertEqual('Action3', ready_tasks[0].task_spec.description) - ready_tasks[0].complete() - wf.do_engine_steps() - self.assertEqual(True, wf.is_completed()) - def test_serialize_workflow_where_script_task_includes_function(self): self.workflow.do_engine_steps() ready_tasks = self.workflow.get_ready_user_tasks() diff --git a/tests/SpiffWorkflow/bpmn/serializer/VersionMigrationTest.py b/tests/SpiffWorkflow/bpmn/serializer/VersionMigrationTest.py new file mode 100644 index 00000000..cd38b5f8 --- /dev/null +++ b/tests/SpiffWorkflow/bpmn/serializer/VersionMigrationTest.py @@ -0,0 +1,30 @@ +import os +import time + +from SpiffWorkflow.task import TaskState +from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine + +from .BaseTestCase import BaseTestCase + +class VersionMigrationTest(BaseTestCase): + + SERIALIZER_VERSION = "1.2" + + def test_convert_1_0_to_1_1(self): + # The serialization used here comes from NestedSubprocessTest saved at line 25 with version 1.0 + fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.0.json') + wf = self.serializer.deserialize_json(open(fn).read()) + # We should be able to finish the workflow from this point + ready_tasks = wf.get_tasks(TaskState.READY) + self.assertEqual('Action3', ready_tasks[0].task_spec.description) + ready_tasks[0].complete() + wf.do_engine_steps() + self.assertEqual(True, wf.is_completed()) + + def test_convert_1_1_to_1_2(self): + fn = os.path.join(self.DATA_DIR, 'serialization', 'v1-1.json') + wf = self.serializer.deserialize_json(open(fn).read()) + wf.script_engine = PythonScriptEngine(default_globals={"time": time}) + wf.refresh_waiting_tasks() + wf.do_engine_steps() + self.assertTrue(wf.is_completed()) \ No newline at end of file diff --git a/tests/SpiffWorkflow/camunda/data/MessageBoundary.bpmn b/tests/SpiffWorkflow/camunda/data/MessageBoundary.bpmn index 5966e0fd..54a1861c 100644 --- a/tests/SpiffWorkflow/camunda/data/MessageBoundary.bpmn +++ b/tests/SpiffWorkflow/camunda/data/MessageBoundary.bpmn @@ -1,5 +1,5 @@ - + @@ -78,7 +78,7 @@ Flow_11u0pgk Flow_1rqk2v9 - timedelta(seconds=.01) + "PT0.01S" @@ -107,48 +107,29 @@ - - - - - + + - - - + + + + + - - - - - - - - - + + + + + - - - - - - - - - - - - - - - - - - - + + + + + @@ -159,76 +140,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -238,6 +195,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/SpiffWorkflow/camunda/data/token_trial.bpmn b/tests/SpiffWorkflow/camunda/data/token_trial.bpmn index 89cc6dc0..488db02b 100644 --- a/tests/SpiffWorkflow/camunda/data/token_trial.bpmn +++ b/tests/SpiffWorkflow/camunda/data/token_trial.bpmn @@ -1,10 +1,10 @@ - + Flow_03vnrmv - + Flow_0g2wjhu Flow_0ya87hl Flow_1qgke9w @@ -75,28 +75,25 @@ - - - - - - - - - - - - + + + - - - - - - - - - + + + + + + + + + + + + + + + @@ -107,18 +104,29 @@ - - - + + + + + + - - - - - - - + + + + + + + + + + + + + + + @@ -128,17 +136,9 @@ - - - - - - - - diff --git a/tests/SpiffWorkflow/camunda/data/token_trial_camunda_clash.bpmn b/tests/SpiffWorkflow/camunda/data/token_trial_camunda_clash.bpmn index ee11331b..d7aad5de 100644 --- a/tests/SpiffWorkflow/camunda/data/token_trial_camunda_clash.bpmn +++ b/tests/SpiffWorkflow/camunda/data/token_trial_camunda_clash.bpmn @@ -1,10 +1,10 @@ - + Flow_03vnrmv - + Flow_0g2wjhu Flow_0ya87hl Flow_1qgke9w @@ -73,28 +73,25 @@ - - - - - - - - - - - - + + + - - - - - - - - - + + + + + + + + + + + + + + + @@ -105,18 +102,29 @@ - - - + + + + + + - - - - - - - + + + + + + + + + + + + + + + @@ -126,17 +134,9 @@ - - - - - - - - diff --git a/tests/SpiffWorkflow/camunda/data/token_trial_parallel_simple.bpmn b/tests/SpiffWorkflow/camunda/data/token_trial_parallel_simple.bpmn index 5c365857..6d8ae517 100644 --- a/tests/SpiffWorkflow/camunda/data/token_trial_parallel_simple.bpmn +++ b/tests/SpiffWorkflow/camunda/data/token_trial_parallel_simple.bpmn @@ -1,5 +1,5 @@ - + Flow_1w2tcdp @@ -100,7 +100,7 @@ - + SequenceFlow_00fpfhi Flow_0wycgzo Flow_1vtdwmy @@ -126,6 +126,18 @@ + + + + + + + + + + + + @@ -166,18 +178,12 @@ - - - - - - - - - - - - + + + + + + @@ -202,15 +208,9 @@ - - - - - -