merged in main and resolved conflicts w/ burnettk

This commit is contained in:
jasquat 2023-04-07 14:59:49 -04:00
commit e2b8f17a7e
No known key found for this signature in database
376 changed files with 14374 additions and 9052 deletions

View File

@ -16,10 +16,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
include: include:
# FIXME: https://github.com/mysql/mysql-connector-python/pull/86 - { python: "3.11", os: "ubuntu-latest", session: "safety" }
# put back when poetry update protobuf mysql-connector-python updates protobuf
# right now mysql is forcing protobuf to version 3
# - { python: "3.11", os: "ubuntu-latest", session: "safety" }
- { python: "3.11", os: "ubuntu-latest", session: "mypy" } - { python: "3.11", os: "ubuntu-latest", session: "mypy" }
- { python: "3.10", os: "ubuntu-latest", session: "mypy" } - { python: "3.10", os: "ubuntu-latest", session: "mypy" }
- { python: "3.9", os: "ubuntu-latest", session: "mypy" } - { python: "3.9", os: "ubuntu-latest", session: "mypy" }
@ -176,6 +173,19 @@ jobs:
name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}} name: logs-${{matrix.python}}-${{matrix.os}}-${{matrix.database}}
path: "./log/*.log" path: "./log/*.log"
# burnettk created an account at https://app.snyk.io/org/kevin-jfx
# and added his SNYK_TOKEN secret under the spiff-arena repo.
snyk:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: Run Snyk to check for vulnerabilities
uses: snyk/actions/python@master
with:
args: spiffworkflow-backend
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
run_pre_commit_checks: run_pre_commit_checks:
runs-on: ubuntu-latest runs-on: ubuntu-latest
defaults: defaults:
@ -184,9 +194,6 @@ jobs:
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
with:
# Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud
fetch-depth: 0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v4.2.0 uses: actions/setup-python@v4.2.0
with: with:
@ -205,9 +212,6 @@ jobs:
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v3.3.0 uses: actions/checkout@v3.3.0
with:
# Disabling shallow clone is recommended for improving relevancy of reporting in sonarcloud
fetch-depth: 0
- name: Checkout Samples - name: Checkout Samples
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
@ -281,7 +285,7 @@ jobs:
# so just skip everything but main # so just skip everything but main
if: github.ref_name == 'main' if: github.ref_name == 'main'
with: with:
projectBaseDir: spiffworkflow-frontend projectBaseDir: spiffworkflow-backend
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
pyrightconfig.json pyrightconfig.json
.idea/ .idea/
t t
*~
.dccache
*~

55
Jenkinsfile vendored
View File

@ -32,6 +32,11 @@ pipeline {
description: 'ID of Jenkins credential for Docker registry.', description: 'ID of Jenkins credential for Docker registry.',
defaultValue: params.DOCKER_CRED_ID ?: 'MISSING' defaultValue: params.DOCKER_CRED_ID ?: 'MISSING'
) )
string(
name: 'DISCORD_WEBHOOK_CRED',
description: 'Name of cretential with Discord webhook',
defaultValue: params.DISCORD_WEBHOOK_CRED ?: "",
)
booleanParam( booleanParam(
name: 'PUBLISH', name: 'PUBLISH',
description: 'Publish built Docker images.', description: 'Publish built Docker images.',
@ -61,6 +66,16 @@ pipeline {
image.push(env.DOCKER_TAG) image.push(env.DOCKER_TAG)
} }
} } } }
post {
success { script {
if (params.DISCORD_WEBHOOK_CRED) {
discordNotify(
header: 'SpiffWorkflow Docker image published!',
cred: params.DISCORD_WEBHOOK_CRED,
)
}
} }
}
} }
} // stages } // stages
post { post {
@ -68,3 +83,43 @@ pipeline {
cleanup { cleanWs() } cleanup { cleanWs() }
} // post } // post
} // pipeline } // pipeline
def discordNotify(Map args=[:]) {
def opts = [
header: args.header ?: 'Deployment successful!',
title: args.title ?: "${env.JOB_NAME}#${env.BUILD_NUMBER}",
cred: args.cred ?: null,
]
def repo = [
url: GIT_URL.minus('.git'),
branch: GIT_BRANCH.minus('origin/'),
commit: GIT_COMMIT.take(8),
prev: (
env.GIT_PREVIOUS_SUCCESSFUL_COMMIT ?: env.GIT_PREVIOUS_COMMIT ?: 'master'
).take(8),
]
wrap([$class: 'BuildUser']) {
BUILD_USER_ID = env.BUILD_USER_ID
}
withCredentials([
string(
credentialsId: opts.cred,
variable: 'DISCORD_WEBHOOK',
),
]) {
discordSend(
link: env.BUILD_URL,
result: currentBuild.currentResult,
webhookURL: env.DISCORD_WEBHOOK,
title: opts.title,
description: """
${opts.header}
Image: [`${params.DOCKER_NAME}:${params.DOCKER_TAG}`](https://hub.docker.com/r/${params.DOCKER_NAME}/tags?name=${params.DOCKER_TAG})
Branch: [`${repo.branch}`](${repo.url}/commits/${repo.branch})
Commit: [`${repo.commit}`](${repo.url}/commit/${repo.commit})
Diff: [`${repo.prev}...${repo.commit}`](${repo.url}/compare/${repo.prev}...${repo.commit})
By: [`${BUILD_USER_ID}`](${repo.url}/commits?author=${BUILD_USER_ID})
""",
)
}
}

View File

@ -1 +0,0 @@
"""__init.py__"""

View File

@ -72,7 +72,7 @@ def convert_timer_expressions(dct):
def add_default_condition_to_cond_task_specs(dct): def add_default_condition_to_cond_task_specs(dct):
for spec in [ts for ts in dct['spec']['task_specs'].values() if ts['typename'] == 'ExclusiveGateway']: for spec in [ts for ts in dct['spec']['task_specs'].values() if ts['typename'] == 'ExclusiveGateway']:
if (None, spec['default_task_spec']) not in spec['cond_task_specs']: if spec['default_task_spec'] is not None and (None, spec['default_task_spec']) not in spec['cond_task_specs']:
spec['cond_task_specs'].append({'condition': None, 'task_spec': spec['default_task_spec']}) spec['cond_task_specs'].append({'condition': None, 'task_spec': spec['default_task_spec']})
def create_data_objects_and_io_specs(dct): def create_data_objects_and_io_specs(dct):
@ -111,3 +111,14 @@ def check_multiinstance(dct):
specs = [ spec for spec in dct['spec']['task_specs'].values() if 'prevtaskclass' in spec ] specs = [ spec for spec in dct['spec']['task_specs'].values() if 'prevtaskclass' in spec ]
if len(specs) > 0: if len(specs) > 0:
raise VersionMigrationError("This workflow cannot be migrated because it contains MultiInstance Tasks") raise VersionMigrationError("This workflow cannot be migrated because it contains MultiInstance Tasks")
def remove_loop_reset(dct):
task_specs = [spec for spec in dct['spec']['task_specs'].values() if spec['typename'] == 'LoopResetTask']
for spec in task_specs:
if spec['typename'] == 'LoopResetTask':
tasks = [t for t in dct['tasks'].values() if t['task_spec'] == spec['name']]
for task in tasks:
dct['tasks'].pop(task['id'])
parent = dct['tasks'].get(task['parent'])
parent['children'] = [c for c in parent['children'] if c != task['id']]
dct['spec']['task_specs'].pop(spec['name'])

View File

@ -6,6 +6,7 @@ from .version_1_2 import (
add_default_condition_to_cond_task_specs, add_default_condition_to_cond_task_specs,
create_data_objects_and_io_specs, create_data_objects_and_io_specs,
check_multiinstance, check_multiinstance,
remove_loop_reset,
) )
def from_version_1_1(old): def from_version_1_1(old):
@ -23,12 +24,18 @@ def from_version_1_1(old):
Data inputs and outputs on process specs were moved inside a BPMNIOSpecification, and Data inputs and outputs on process specs were moved inside a BPMNIOSpecification, and
are now TaskDataReferences; BpmnDataSpecifications that referred to Data Objects are are now TaskDataReferences; BpmnDataSpecifications that referred to Data Objects are
now DataObjects. now DataObjects.
Multiinstance tasks were completely refactored, in a way that is simply too difficult to
migrate.
Loop reset tasks were removed.
""" """
new = deepcopy(old) new = deepcopy(old)
convert_timer_expressions(new) convert_timer_expressions(new)
add_default_condition_to_cond_task_specs(new) add_default_condition_to_cond_task_specs(new)
create_data_objects_and_io_specs(new) create_data_objects_and_io_specs(new)
check_multiinstance(new) check_multiinstance(new)
remove_loop_reset(new)
new['VERSION'] = "1.2" new['VERSION'] = "1.2"
return new return new

View File

@ -1,5 +1,4 @@
from ..specs.BpmnProcessSpec import BpmnProcessSpec from ..specs.BpmnProcessSpec import BpmnProcessSpec
from ..specs.MultiInstanceTask import MultiInstanceTask
from ..specs.events.IntermediateEvent import _BoundaryEventParent from ..specs.events.IntermediateEvent import _BoundaryEventParent
from .helpers.spec import WorkflowSpecConverter from .helpers.spec import WorkflowSpecConverter

View File

@ -2,7 +2,6 @@ from .helpers.spec import TaskSpecConverter
from ...specs.StartTask import StartTask from ...specs.StartTask import StartTask
from ...specs.Simple import Simple from ...specs.Simple import Simple
from ...specs.LoopResetTask import LoopResetTask
from ..specs.BpmnProcessSpec import _EndJoin from ..specs.BpmnProcessSpec import _EndJoin
from ..specs.BpmnSpecMixin import _BpmnCondition from ..specs.BpmnSpecMixin import _BpmnCondition
@ -27,8 +26,6 @@ from ..specs.events.IntermediateEvent import (
ReceiveTask, ReceiveTask,
) )
from ..workflow import BpmnWorkflow
class DefaultTaskSpecConverter(TaskSpecConverter): class DefaultTaskSpecConverter(TaskSpecConverter):
@ -50,23 +47,6 @@ class StartTaskConverter(DefaultTaskSpecConverter):
super().__init__(StartTask, registry) super().__init__(StartTask, registry)
class LoopResetTaskConverter(DefaultTaskSpecConverter):
def __init__(self, registry):
super().__init__(LoopResetTask, registry)
def to_dict(self, spec):
dct = super().to_dict(spec)
dct['destination_id'] = str(spec.destination_id)
dct['destination_spec_name'] = spec.destination_spec_name
return dct
def from_dict(self, dct):
spec = self.task_spec_from_dict(dct)
spec.destination_id = self.registry.convert(spec.destination_id)
return spec
class EndJoinConverter(DefaultTaskSpecConverter): class EndJoinConverter(DefaultTaskSpecConverter):
def __init__(self, registry): def __init__(self, registry):
super().__init__(_EndJoin, registry) super().__init__(_EndJoin, registry)
@ -317,7 +297,6 @@ DEFAULT_TASK_SPEC_CONVERTER_CLASSES = [
SimpleTaskConverter, SimpleTaskConverter,
StartTaskConverter, StartTaskConverter,
EndJoinConverter, EndJoinConverter,
LoopResetTaskConverter,
NoneTaskConverter, NoneTaskConverter,
UserTaskConverter, UserTaskConverter,
ManualTaskConverter, ManualTaskConverter,

View File

@ -246,7 +246,7 @@ class BpmnWorkflowSerializer:
if isinstance(task_spec, SubWorkflowTask) and task_id in top_dct.get('subprocesses', {}): if isinstance(task_spec, SubWorkflowTask) and task_id in top_dct.get('subprocesses', {}):
subprocess_spec = top.subprocess_specs[task_spec.spec] subprocess_spec = top.subprocess_specs[task_spec.spec]
subprocess = self.wf_class(subprocess_spec, {}, name=task_spec.name, parent=process) subprocess = self.wf_class(subprocess_spec, {}, name=task_spec.name, parent=process, deserializing=True)
subprocess_dct = top_dct['subprocesses'].get(task_id, {}) subprocess_dct = top_dct['subprocesses'].get(task_id, {})
subprocess.data = self.data_converter.restore(subprocess_dct.pop('data')) subprocess.data = self.data_converter.restore(subprocess_dct.pop('data'))
subprocess.success = subprocess_dct.pop('success') subprocess.success = subprocess_dct.pop('success')
@ -254,8 +254,12 @@ class BpmnWorkflowSerializer:
subprocess.completed_event.connect(task_spec._on_subworkflow_completed, task) subprocess.completed_event.connect(task_spec._on_subworkflow_completed, task)
top_level_workflow.subprocesses[task.id] = subprocess top_level_workflow.subprocesses[task.id] = subprocess
for child in [ process_dct['tasks'][c] for c in task_dict['children'] ]: for child_task_id in task_dict['children']:
self.task_tree_from_dict(process_dct, child['id'], task, process, top, top_dct) if child_task_id in process_dct['tasks']:
child = process_dct['tasks'][child_task_id]
self.task_tree_from_dict(process_dct, child_task_id, task, process, top, top_dct)
else:
raise ValueError(f"Task {task_id} ({task_spec.name}) has child {child_task_id}, but no such task exists")
return task return task

View File

@ -47,9 +47,10 @@ class _EndJoin(UnstructuredJoin):
return force or len(waiting_tasks) == 0, waiting_tasks return force or len(waiting_tasks) == 0, waiting_tasks
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
super(_EndJoin, self)._on_complete_hook(my_task) result = super(_EndJoin, self)._run_hook(my_task)
my_task.workflow.data.update(my_task.data) my_task.workflow.data.update(my_task.data)
return result
class BpmnProcessSpec(WorkflowSpec): class BpmnProcessSpec(WorkflowSpec):

View File

@ -110,11 +110,12 @@ class InclusiveGateway(MultiChoice, UnstructuredJoin):
return complete, waiting_tasks return complete, waiting_tasks
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
outputs = self._get_matching_outputs(my_task) outputs = self._get_matching_outputs(my_task)
if len(outputs) == 0: if len(outputs) == 0:
raise WorkflowTaskException(f'No conditions satisfied on gateway', task=my_task) raise WorkflowTaskException(f'No conditions satisfied on gateway', task=my_task)
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
return True
@property @property
def spec_type(self): def spec_type(self):

View File

@ -29,14 +29,14 @@ class ScriptEngineTask(Simple, BpmnSpecMixin):
"""Please override for specific Implementations, see ScriptTask below for an example""" """Please override for specific Implementations, see ScriptTask below for an example"""
pass pass
def _on_complete_hook(self, task): def _run_hook(self, task):
try: try:
self._execute(task) self._execute(task)
super(ScriptEngineTask, self)._on_complete_hook(task) super(ScriptEngineTask, self)._run_hook(task)
except Exception as exc: except Exception as exc:
task._set_state(TaskState.WAITING) task._set_state(TaskState.WAITING)
raise exc raise exc
return True
class ScriptTask(ScriptEngineTask): class ScriptTask(ScriptEngineTask):

View File

@ -25,9 +25,6 @@ class SubWorkflowTask(BpmnSpecMixin):
def spec_type(self): def spec_type(self):
return 'Subprocess' return 'Subprocess'
def _on_ready_hook(self, my_task):
super()._on_ready_hook(my_task)
def _on_subworkflow_completed(self, subworkflow, my_task): def _on_subworkflow_completed(self, subworkflow, my_task):
self.update_data(my_task, subworkflow) self.update_data(my_task, subworkflow)
my_task._set_state(TaskState.READY) my_task._set_state(TaskState.READY)

View File

@ -54,14 +54,14 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
last_changed = None last_changed = None
thread_tasks = [] thread_tasks = []
for task in split_task._find_any(self): for task in split_task._find_any(self):
# Ignore tasks from other threads.
if task.thread_id != my_task.thread_id: if task.thread_id != my_task.thread_id:
# Ignore tasks from other threads. (Do we need this condition?)
continue continue
# Ignore my outgoing branches.
if self.split_task and task._is_descendant_of(my_task):
continue
# For an inclusive join, this can happen - it's a future join
if not task.parent._is_finished(): if not task.parent._is_finished():
# For an inclusive join, this can happen - it's a future join
continue
if my_task._is_descendant_of(task):
# Skip ancestors (otherwise the branch this task is on will get dropped)
continue continue
# We have found a matching instance. # We have found a matching instance.
thread_tasks.append(task) thread_tasks.append(task)
@ -77,20 +77,13 @@ class UnstructuredJoin(Join, BpmnSpecMixin):
for task in thread_tasks: for task in thread_tasks:
collected_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.
# By setting the state to READY only, we allow for calling
# :class:`Task.complete()`, which leads to the task tree being
# (re)built underneath the node.
for task in thread_tasks: for task in thread_tasks:
if task == last_changed: if task != last_changed:
task.data.update(collected_data) task._set_state(TaskState.CANCELLED)
self.entered_event.emit(my_task.workflow, my_task)
task._ready()
else:
task._set_state(TaskState.COMPLETED)
task._drop_children() task._drop_children()
else:
task.data.update(collected_data)
def task_should_set_children_future(self, my_task): def task_should_set_children_future(self, my_task):
return True return True

View File

@ -18,7 +18,6 @@
# 02110-1301 USA # 02110-1301 USA
from .event_types import ThrowingEvent, CatchingEvent from .event_types import ThrowingEvent, CatchingEvent
from .event_definitions import CycleTimerEventDefinition
from ..BpmnSpecMixin import BpmnSpecMixin from ..BpmnSpecMixin import BpmnSpecMixin
from ....specs.Simple import Simple from ....specs.Simple import Simple
from ....task import TaskState from ....task import TaskState
@ -67,13 +66,15 @@ class _BoundaryEventParent(Simple, BpmnSpecMixin):
def spec_type(self): def spec_type(self):
return 'Boundary Event Parent' return 'Boundary Event Parent'
def _on_ready_hook(self, my_task): def _run_hook(self, my_task):
# Clear any events that our children might have received and # Clear any events that our children might have received and
# wait for new events # wait for new events
for child in my_task.children: for child in my_task.children:
if isinstance(child.task_spec, BoundaryEvent): if isinstance(child.task_spec, BoundaryEvent):
child.task_spec.event_definition.reset(child) child.task_spec.event_definition.reset(child)
child._set_state(TaskState.WAITING)
return True
def _child_complete_hook(self, child_task): def _child_complete_hook(self, child_task):
@ -123,7 +124,7 @@ class BoundaryEvent(CatchingEvent):
super(BoundaryEvent, self).catch(my_task, event_definition) super(BoundaryEvent, self).catch(my_task, event_definition)
# Would love to get rid of this statement and manage in the workflow # Would love to get rid of this statement and manage in the workflow
# However, it is not really compatible with how boundary events work. # However, it is not really compatible with how boundary events work.
my_task.complete() my_task.run()
class EventBasedGateway(CatchingEvent): class EventBasedGateway(CatchingEvent):
@ -135,8 +136,8 @@ class EventBasedGateway(CatchingEvent):
def _predict_hook(self, my_task): def _predict_hook(self, my_task):
my_task._sync_children(self.outputs, state=TaskState.MAYBE) my_task._sync_children(self.outputs, state=TaskState.MAYBE)
def _on_complete_hook(self, my_task): def _on_ready_hook(self, my_task):
seen_events = my_task.internal_data.get('seen_events', [])
for child in my_task.children: for child in my_task.children:
if not child.task_spec.event_definition.has_fired(child): if child.task_spec.event_definition not in seen_events:
child.cancel() child.cancel()
return super()._on_complete_hook(my_task)

View File

@ -20,13 +20,14 @@
import re import re
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from calendar import monthrange from calendar import monthrange
from time import timezone as tzoffset from time import timezone as tzoffset, altzone as dstoffset, daylight as isdst
from copy import deepcopy from copy import deepcopy
from SpiffWorkflow.exceptions import SpiffWorkflowException, WorkflowException from SpiffWorkflow.exceptions import WorkflowException
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
LOCALTZ = timezone(timedelta(seconds=-1 * tzoffset)) seconds_from_utc = dstoffset if isdst else tzoffset
LOCALTZ = timezone(timedelta(seconds=-1 * seconds_from_utc))
class EventDefinition(object): class EventDefinition(object):
@ -452,42 +453,38 @@ class CycleTimerEventDefinition(TimerEventDefinition):
def event_type(self): def event_type(self):
return 'Cycle Timer' return 'Cycle Timer'
def has_fired(self, my_task): def cycle_complete(self, my_task):
if not my_task._get_internal_data('event_fired'): event_value = my_task._get_internal_data('event_value')
# Only check for the next cycle when the event has not fired to prevent cycles from being skipped. if event_value is None:
event_value = my_task._get_internal_data('event_value') # Don't necessarily like this, but it's a lot more staightforward than trying to only create
if event_value is None: # a child task on loop iterations after the first
expression = my_task.workflow.script_engine.evaluate(my_task, self.expression) my_task._drop_children()
cycles, start, duration = TimerEventDefinition.parse_iso_recurring_interval(expression) expression = my_task.workflow.script_engine.evaluate(my_task, self.expression)
event_value = {'cycles': cycles, 'next': start.isoformat(), 'duration': duration.total_seconds()} 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: # When the next timer event passes, return True to allow the parent task to generate another child
next_event = datetime.fromisoformat(event_value['next']) # Use event fired to indicate that this timer has completed all cycles and the task can be completed
if next_event < datetime.now(timezone.utc): ready = False
my_task._set_internal_data(event_fired=True) if event_value['cycles'] != 0:
event_value['next'] = (next_event + timedelta(seconds=event_value['duration'])).isoformat() next_event = datetime.fromisoformat(event_value['next'])
if next_event < datetime.now(timezone.utc):
event_value['next'] = (next_event + timedelta(seconds=event_value['duration'])).isoformat()
event_value['cycles'] -= 1
ready = True
else:
my_task.internal_data.pop('event_value', None)
my_task.internal_data['event_fired'] = True
my_task._set_internal_data(event_value=event_value) my_task._set_internal_data(event_value=event_value)
return ready
return my_task._get_internal_data('event_fired', False)
def timer_value(self, my_task): def timer_value(self, my_task):
event_value = my_task._get_internal_data('event_value') event_value = my_task._get_internal_data('event_value')
if event_value is not None and event_value['cycles'] > 0: if event_value is not None and event_value['cycles'] != 0:
return event_value['next'] 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
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): class MultipleEventDefinition(EventDefinition):
@ -504,11 +501,10 @@ class MultipleEventDefinition(EventDefinition):
seen_events = my_task.internal_data.get('seen_events', []) seen_events = my_task.internal_data.get('seen_events', [])
for event in self.event_definitions: for event in self.event_definitions:
if isinstance(event, (TimerEventDefinition, CycleTimerEventDefinition)): if isinstance(event, TimerEventDefinition):
child = [c for c in my_task.children if c.task_spec.event_definition == event] child = [c for c in my_task.children if c.task_spec.event_definition == event]
child[0].task_spec._update_hook(child[0]) child[0].task_spec._update_hook(child[0])
child[0]._set_state(TaskState.MAYBE) if event.has_fired(child[0]):
if event.has_fired(my_task):
seen_events.append(event) seen_events.append(event)
if self.parallel: if self.parallel:

View File

@ -57,21 +57,22 @@ class CatchingEvent(Simple, BpmnSpecMixin):
if self.event_definition.has_fired(my_task): if self.event_definition.has_fired(my_task):
return True return True
else: elif isinstance(self.event_definition, CycleTimerEventDefinition):
if self.event_definition.cycle_complete(my_task):
for output in self.outputs:
child = my_task._add_child(output, TaskState.READY)
child.task_spec._predict(child, mask=TaskState.READY|TaskState.PREDICTED_MASK)
if my_task.state != TaskState.WAITING:
my_task._set_state(TaskState.WAITING)
elif my_task.state != TaskState.WAITING:
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
if isinstance(self.event_definition, MessageEventDefinition): if isinstance(self.event_definition, MessageEventDefinition):
self.event_definition.update_task_data(my_task) 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) self.event_definition.reset(my_task)
super(CatchingEvent, self)._on_complete_hook(my_task) return super(CatchingEvent, self)._run_hook(my_task)
# This fixes the problem of boundary events remaining cancelled if the task is reused. # This fixes the problem of boundary events remaining cancelled if the task is reused.
# It pains me to add these methods, but unless we can get rid of the loop reset task we're stuck # It pains me to add these methods, but unless we can get rid of the loop reset task we're stuck
@ -95,6 +96,7 @@ class ThrowingEvent(Simple, BpmnSpecMixin):
super(ThrowingEvent, self).__init__(wf_spec, name, **kwargs) super(ThrowingEvent, self).__init__(wf_spec, name, **kwargs)
self.event_definition = event_definition self.event_definition = event_definition
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
super(ThrowingEvent, self)._on_complete_hook(my_task) super(ThrowingEvent, self)._run_hook(my_task)
self.event_definition.throw(my_task) self.event_definition.throw(my_task)
return True

View File

@ -29,7 +29,7 @@ from .specs.events.StartEvent import StartEvent
from .specs.SubWorkflowTask import CallActivity from .specs.SubWorkflowTask import CallActivity
from ..task import TaskState, Task from ..task import TaskState, Task
from ..workflow import Workflow from ..workflow import Workflow
from ..exceptions import WorkflowException, WorkflowTaskException from ..exceptions import TaskNotFoundException, WorkflowException, WorkflowTaskException
class BpmnMessage: class BpmnMessage:
@ -251,7 +251,7 @@ class BpmnWorkflow(Workflow):
for task in engine_steps: for task in engine_steps:
if will_complete_task is not None: if will_complete_task is not None:
will_complete_task(task) will_complete_task(task)
task.complete() task.run()
if did_complete_task is not None: if did_complete_task is not None:
did_complete_task(task) did_complete_task(task)
if task.task_spec.name == exit_at: if task.task_spec.name == exit_at:
@ -271,7 +271,10 @@ class BpmnWorkflow(Workflow):
for my_task in self.get_tasks(TaskState.WAITING): for my_task in self.get_tasks(TaskState.WAITING):
if will_refresh_task is not None: if will_refresh_task is not None:
will_refresh_task(my_task) will_refresh_task(my_task)
my_task.task_spec._update(my_task) # This seems redundant, but the state could have been updated by another waiting task and no longer be waiting.
# Someday, I would like to get rid of this method, and also do_engine_steps
if my_task.state == TaskState.WAITING:
my_task.task_spec._update(my_task)
if did_refresh_task is not None: if did_refresh_task is not None:
did_refresh_task(my_task) did_refresh_task(my_task)
@ -279,10 +282,15 @@ class BpmnWorkflow(Workflow):
return [t for t in self.get_tasks(workflow=workflow) if t.task_spec.name == name] return [t for t in self.get_tasks(workflow=workflow) if t.task_spec.name == name]
def get_tasks(self, state=TaskState.ANY_MASK, workflow=None): def get_tasks(self, state=TaskState.ANY_MASK, workflow=None):
# Now that I've revisited and had to ask myself what the hell was I doing, I realize I should comment this
tasks = [] tasks = []
top = self._get_outermost_workflow() top = self._get_outermost_workflow()
wf = workflow or top # I think it makes more sense to start with the current workflow, which is probably going to be the top
for task in Workflow.get_tasks(wf): # most of the time anyway
wf = workflow or self
# We can't filter the iterator on the state because we have to subprocesses, and the subprocess task will
# almost surely be in a different state than the tasks we want
for task in Workflow.get_tasks_iterator(wf):
subprocess = top.subprocesses.get(task.id) subprocess = top.subprocesses.get(task.id)
if subprocess is not None: if subprocess is not None:
tasks.extend(subprocess.get_tasks(state, subprocess)) tasks.extend(subprocess.get_tasks(state, subprocess))
@ -290,42 +298,28 @@ class BpmnWorkflow(Workflow):
tasks.append(task) tasks.append(task)
return tasks return tasks
def _find_task(self, task_id): def get_task_from_id(self, task_id, workflow=None):
if task_id is None: for task in self.get_tasks(workflow=workflow):
raise WorkflowException('task_id is None', task_spec=self.spec)
for task in self.get_tasks():
if task.id == task_id: if task.id == task_id:
return task return task
raise WorkflowException(f'A task with the given task_id ({task_id}) was not found', task_spec=self.spec) raise TaskNotFoundException(f'A task with the given task_id ({task_id}) was not found', task_spec=self.spec)
def complete_task_from_id(self, task_id): def get_ready_user_tasks(self, lane=None, workflow=None):
# I don't even know why we use this stupid function instead of calling task.complete,
# since all it does is search the task tree and call the method
task = self._find_task(task_id)
return task.complete()
def reset_task_from_id(self, task_id):
task = self._find_task(task_id)
if task.workflow.last_task and task.workflow.last_task.data:
data = task.workflow.last_task.data
return task.reset_token(data)
def get_ready_user_tasks(self,lane=None):
"""Returns a list of User Tasks that are READY for user action""" """Returns a list of User Tasks that are READY for user action"""
if lane is not None: if lane is not None:
return [t for t in self.get_tasks(TaskState.READY) return [t for t in self.get_tasks(TaskState.READY, workflow)
if (not self._is_engine_task(t.task_spec)) if (not self._is_engine_task(t.task_spec))
and (t.task_spec.lane == lane)] and (t.task_spec.lane == lane)]
else: else:
return [t for t in self.get_tasks(TaskState.READY) return [t for t in self.get_tasks(TaskState.READY, workflow)
if not self._is_engine_task(t.task_spec)] if not self._is_engine_task(t.task_spec)]
def get_waiting_tasks(self): def get_waiting_tasks(self, workflow=None):
"""Returns a list of all WAITING tasks""" """Returns a list of all WAITING tasks"""
return self.get_tasks(TaskState.WAITING) return self.get_tasks(TaskState.WAITING, workflow)
def get_catching_tasks(self): def get_catching_tasks(self, workflow=None):
return [ task for task in self.get_tasks() if isinstance(task.task_spec, CatchingEvent) ] return [task for task in self.get_tasks(workflow=workflow) if isinstance(task.task_spec, CatchingEvent)]
def _is_engine_task(self, task_spec): def _is_engine_task(self, task_spec):
return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task()) return (not hasattr(task_spec, 'is_engine_task') or task_spec.is_engine_task())

View File

@ -5,10 +5,7 @@ from ..specs.model import DecisionTable, Rule, HitPolicy
from ..specs.model import Input, InputEntry, Output, OutputEntry from ..specs.model import Input, InputEntry, Output, OutputEntry
from ..engine.DMNEngine import DMNEngine from ..engine.DMNEngine import DMNEngine
class BusinessRuleTaskConverter(TaskSpecConverter): class BaseBusinessRuleTaskConverter(TaskSpecConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
def to_dict(self, spec): def to_dict(self, spec):
dct = self.get_default_attributes(spec) dct = self.get_default_attributes(spec)
@ -98,3 +95,8 @@ class BusinessRuleTaskConverter(TaskSpecConverter):
rule.outputEntries = [self.output_entry_from_dict(entry, outputs) rule.outputEntries = [self.output_entry_from_dict(entry, outputs)
for entry in dct['output_entries']] for entry in dct['output_entries']]
return rule return rule
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)

View File

@ -1,5 +1,4 @@
from SpiffWorkflow.exceptions import WorkflowTaskException, WorkflowException, \ from SpiffWorkflow.exceptions import WorkflowTaskException, SpiffWorkflowException
SpiffWorkflowException
from ...specs.Simple import Simple from ...specs.Simple import Simple
@ -17,7 +16,6 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
def __init__(self, wf_spec, name, dmnEngine, **kwargs): def __init__(self, wf_spec, name, dmnEngine, **kwargs):
super().__init__(wf_spec, name, **kwargs) super().__init__(wf_spec, name, **kwargs)
self.dmnEngine = dmnEngine self.dmnEngine = dmnEngine
self.resDict = None self.resDict = None
@ -25,11 +23,10 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
def spec_class(self): def spec_class(self):
return 'Business Rule Task' return 'Business Rule Task'
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
try: try:
my_task.data = DeepMerge.merge(my_task.data, my_task.data = DeepMerge.merge(my_task.data, self.dmnEngine.result(my_task))
self.dmnEngine.result(my_task)) super(BusinessRuleTask, self)._run_hook(my_task)
super(BusinessRuleTask, self)._on_complete_hook(my_task)
except SpiffWorkflowException as we: except SpiffWorkflowException as we:
we.add_note(f"Business Rule Task '{my_task.task_spec.description}'.") we.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise we raise we
@ -37,4 +34,4 @@ class BusinessRuleTask(Simple, BpmnSpecMixin):
error = WorkflowTaskException(str(e), task=my_task) error = WorkflowTaskException(str(e), task=my_task)
error.add_note(f"Business Rule Task '{my_task.task_spec.description}'.") error.add_note(f"Business Rule Task '{my_task.task_spec.description}'.")
raise error raise error
return True

View File

@ -129,3 +129,7 @@ class WorkflowTaskException(WorkflowException):
class StorageException(SpiffWorkflowException): class StorageException(SpiffWorkflowException):
pass pass
class TaskNotFoundException(WorkflowException):
pass

View File

@ -16,9 +16,7 @@ from builtins import object
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import re
from .. import operators from .. import operators
from .. import specs
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask from ..specs.CancelTask import CancelTask
@ -41,7 +39,6 @@ from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform from ..specs.Transform import Transform
from ..specs.Trigger import Trigger from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
# Create a list of tag names out of the spec names. # Create a list of tag names out of the spec names.
def spec_map(): def spec_map():
@ -68,7 +65,6 @@ def spec_map():
'transform': Transform, 'transform': Transform,
'trigger': Trigger, 'trigger': Trigger,
'workflow-spec': WorkflowSpec, 'workflow-spec': WorkflowSpec,
'loop-reset-task': LoopResetTask,
'task': Simple, 'task': Simple,
} }

View File

@ -22,8 +22,7 @@ from base64 import b64encode, b64decode
from ..workflow import Workflow from ..workflow import Workflow
from ..util.impl import get_class from ..util.impl import get_class
from ..task import Task from ..task import Task
from ..operators import (Attrib, PathAttrib, Equal, NotEqual, from ..operators import (Attrib, PathAttrib, Equal, NotEqual, Operator, GreaterThan, LessThan, Match)
Operator, GreaterThan, LessThan, Match)
from ..specs.base import TaskSpec from ..specs.base import TaskSpec
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
@ -44,10 +43,8 @@ from ..specs.SubWorkflow import SubWorkflow
from ..specs.ThreadStart import ThreadStart from ..specs.ThreadStart import ThreadStart
from ..specs.ThreadMerge import ThreadMerge from ..specs.ThreadMerge import ThreadMerge
from ..specs.ThreadSplit import ThreadSplit from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform
from ..specs.Trigger import Trigger from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
from .base import Serializer from .base import Serializer
from .exceptions import TaskNotSupportedError, MissingSpecError from .exceptions import TaskNotSupportedError, MissingSpecError
import warnings import warnings
@ -169,7 +166,6 @@ class DictionarySerializer(Serializer):
s_state['defines'] = self.serialize_dict(spec.defines) s_state['defines'] = self.serialize_dict(spec.defines)
s_state['pre_assign'] = self.serialize_list(spec.pre_assign) s_state['pre_assign'] = self.serialize_list(spec.pre_assign)
s_state['post_assign'] = self.serialize_list(spec.post_assign) s_state['post_assign'] = self.serialize_list(spec.post_assign)
s_state['locks'] = spec.locks[:]
# Note: Events are not serialized; this is documented in # Note: Events are not serialized; this is documented in
# the TaskSpec API docs. # the TaskSpec API docs.
@ -190,7 +186,6 @@ class DictionarySerializer(Serializer):
spec.pre_assign = self.deserialize_list(s_state.get('pre_assign', [])) spec.pre_assign = self.deserialize_list(s_state.get('pre_assign', []))
spec.post_assign = self.deserialize_list( spec.post_assign = self.deserialize_list(
s_state.get('post_assign', [])) s_state.get('post_assign', []))
spec.locks = s_state.get('locks', [])[:]
# We can't restore inputs and outputs yet because they may not be # We can't restore inputs and outputs yet because they may not be
# deserialized yet. So keep the names, and resolve them in the end. # deserialized yet. So keep the names, and resolve them in the end.
spec.inputs = s_state.get('inputs', [])[:] spec.inputs = s_state.get('inputs', [])[:]
@ -302,18 +297,6 @@ class DictionarySerializer(Serializer):
self.deserialize_task_spec(wf_spec, s_state, spec=spec) self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec return spec
def serialize_loop_reset_task(self, spec):
s_state = self.serialize_task_spec(spec)
s_state['destination_id'] = spec.destination_id
s_state['destination_spec_name'] = spec.destination_spec_name
return s_state
def deserialize_loop_reset_task(self, wf_spec, s_state):
spec = LoopResetTask(wf_spec, s_state['name'], s_state['destination_id'],
s_state['destination_spec_name'])
self.deserialize_task_spec(wf_spec, s_state, spec=spec)
return spec
def serialize_join(self, spec): def serialize_join(self, spec):
s_state = self.serialize_task_spec(spec) s_state = self.serialize_task_spec(spec)
s_state['split_task'] = spec.split_task s_state['split_task'] = spec.split_task
@ -555,8 +538,7 @@ class DictionarySerializer(Serializer):
del spec.task_specs['Start'] del spec.task_specs['Start']
start_task_spec_state = s_state['task_specs']['Start'] start_task_spec_state = s_state['task_specs']['Start']
start_task_spec = StartTask.deserialize( start_task_spec = StartTask.deserialize(self, spec, start_task_spec_state)
self, spec, start_task_spec_state)
spec.start = start_task_spec spec.start = start_task_spec
spec.task_specs['Start'] = start_task_spec spec.task_specs['Start'] = start_task_spec
for name, task_spec_state in list(s_state['task_specs'].items()): for name, task_spec_state in list(s_state['task_specs'].items()):
@ -602,20 +584,34 @@ class DictionarySerializer(Serializer):
s_state['wf_spec']""" s_state['wf_spec']"""
if wf_spec is None: if wf_spec is None:
# The json serializer serializes the spec as a string and then serializes it again, hence this check
# I'm not confident that this is going to actually work, but this serializer is so fundamentally flawed
# that I'm not going to put the effort in to be sure this works.
if isinstance(s_state['wf_spec'], str):
spec_dct = json.loads(s_state['wf_spec'])
else:
spec_dct = s_state['wf_spec']
reset_specs = [spec['name'] for spec in spec_dct['task_specs'].values() if spec['class'].endswith('LoopResetTask')]
for name in reset_specs:
s_state['wf_spec']['task_specs'].pop(name)
wf_spec = self.deserialize_workflow_spec(s_state['wf_spec'], **kwargs) wf_spec = self.deserialize_workflow_spec(s_state['wf_spec'], **kwargs)
else:
reset_specs = []
workflow = wf_class(wf_spec) workflow = wf_class(wf_spec)
workflow.data = self.deserialize_dict(s_state['data']) workflow.data = self.deserialize_dict(s_state['data'])
workflow.success = s_state['success'] workflow.success = s_state['success']
workflow.spec = wf_spec workflow.spec = wf_spec
workflow.task_tree = self.deserialize_task( workflow.task_tree = self.deserialize_task(workflow, s_state['task_tree'], reset_specs)
workflow, s_state['task_tree'])
# Re-connect parents # Re-connect parents
tasklist = list(workflow.get_tasks()) tasklist = workflow.get_tasks()
for task in tasklist: for task in tasklist:
task.parent = workflow.get_task(task.parent,tasklist) if task.parent is not None:
task.parent = workflow.get_task_from_id(task.parent, tasklist)
workflow.last_task = workflow.get_task(s_state['last_task'],tasklist) if workflow.last_task is not None:
workflow.last_task = workflow.get_task_from_id(s_state['last_task'],tasklist)
workflow.update_task_mapping() workflow.update_task_mapping()
return workflow return workflow
@ -636,81 +632,48 @@ class DictionarySerializer(Serializer):
" internal_data to store the subworkflow).") " internal_data to store the subworkflow).")
s_state = dict() s_state = dict()
# id
s_state['id'] = task.id s_state['id'] = task.id
# workflow
s_state['workflow_name'] = task.workflow.name s_state['workflow_name'] = task.workflow.name
# parent
s_state['parent'] = task.parent.id if task.parent is not None else None s_state['parent'] = task.parent.id if task.parent is not None else None
# children
if not skip_children: if not skip_children:
s_state['children'] = [ s_state['children'] = [
self.serialize_task(child) for child in task.children] self.serialize_task(child) for child in task.children]
# state
s_state['state'] = task.state s_state['state'] = task.state
s_state['triggered'] = task.triggered s_state['triggered'] = task.triggered
# task_spec
s_state['task_spec'] = task.task_spec.name s_state['task_spec'] = task.task_spec.name
# last_state_change
s_state['last_state_change'] = task.last_state_change s_state['last_state_change'] = task.last_state_change
# data
s_state['data'] = self.serialize_dict(task.data) s_state['data'] = self.serialize_dict(task.data)
# internal_data
s_state['internal_data'] = task.internal_data s_state['internal_data'] = task.internal_data
return s_state return s_state
def deserialize_task(self, workflow, s_state, ignored_specs=None):
def deserialize_task(self, workflow, s_state):
assert isinstance(workflow, Workflow) assert isinstance(workflow, Workflow)
splits = s_state['task_spec'].split('_') old_spec_name = s_state['task_spec']
oldtaskname = s_state['task_spec'] if old_spec_name in ignored_specs:
task_spec = workflow.get_task_spec_from_name(oldtaskname) return None
task_spec = workflow.get_task_spec_from_name(old_spec_name)
if task_spec is None: if task_spec is None:
raise MissingSpecError("Unknown task spec: " + oldtaskname) raise MissingSpecError("Unknown task spec: " + old_spec_name)
task = Task(workflow, task_spec) task = Task(workflow, task_spec)
if getattr(task_spec,'isSequential',False) and \ if getattr(task_spec,'isSequential',False) and s_state['internal_data'].get('splits') is not None:
s_state['internal_data'].get('splits') is not None:
task.task_spec.expanded = s_state['internal_data']['splits'] task.task_spec.expanded = s_state['internal_data']['splits']
# id
task.id = s_state['id'] task.id = s_state['id']
# parent
# as the task_tree might not be complete yet # as the task_tree might not be complete yet
# keep the ids so they can be processed at the end # keep the ids so they can be processed at the end
task.parent = s_state['parent'] task.parent = s_state['parent']
task.children = self._deserialize_task_children(task, s_state, ignored_specs)
# children
task.children = self._deserialize_task_children(task, s_state)
# state
task._state = s_state['state'] task._state = s_state['state']
task.triggered = s_state['triggered'] task.triggered = s_state['triggered']
# last_state_change
task.last_state_change = s_state['last_state_change'] task.last_state_change = s_state['last_state_change']
# data
task.data = self.deserialize_dict(s_state['data']) task.data = self.deserialize_dict(s_state['data'])
# internal_data
task.internal_data = s_state['internal_data'] task.internal_data = s_state['internal_data']
return task return task
def _deserialize_task_children(self, task, s_state): def _deserialize_task_children(self, task, s_state, ignored_specs):
"""This may need to be overridden if you need to support """This may need to be overridden if you need to support
deserialization of sub-workflows""" deserialization of sub-workflows"""
return [self.deserialize_task(task.workflow, c) children = [self.deserialize_task(task.workflow, c, ignored_specs) for c in s_state['children']]
for c in s_state['children']] return [c for c in children if c is not None]

View File

@ -22,24 +22,20 @@ from .dict import DictionarySerializer
class JSONSerializer(DictionarySerializer): class JSONSerializer(DictionarySerializer):
def serialize_workflow_spec(self, wf_spec, **kwargs): def serialize_workflow_spec(self, wf_spec, **kwargs):
thedict = super(JSONSerializer, self).serialize_workflow_spec( thedict = super(JSONSerializer, self).serialize_workflow_spec(wf_spec, **kwargs)
wf_spec, **kwargs)
return self._dumps(thedict) return self._dumps(thedict)
def deserialize_workflow_spec(self, s_state, **kwargs): def deserialize_workflow_spec(self, s_state, **kwargs):
thedict = self._loads(s_state) thedict = self._loads(s_state)
return super(JSONSerializer, self).deserialize_workflow_spec( return super(JSONSerializer, self).deserialize_workflow_spec(thedict, **kwargs)
thedict, **kwargs)
def serialize_workflow(self, workflow, **kwargs): def serialize_workflow(self, workflow, **kwargs):
thedict = super(JSONSerializer, self).serialize_workflow( thedict = super(JSONSerializer, self).serialize_workflow(workflow, **kwargs)
workflow, **kwargs)
return self._dumps(thedict) return self._dumps(thedict)
def deserialize_workflow(self, s_state, **kwargs): def deserialize_workflow(self, s_state, **kwargs):
thedict = self._loads(s_state) thedict = self._loads(s_state)
return super(JSONSerializer, self).deserialize_workflow( return super(JSONSerializer, self).deserialize_workflow(thedict, **kwargs)
thedict, **kwargs)
def _object_hook(self, dct): def _object_hook(self, dct):
if '__uuid__' in dct: if '__uuid__' in dct:

View File

@ -176,8 +176,7 @@ class XmlSerializer(Serializer):
threshold_field = start_node.attrib.get('threshold-field', '').lower() threshold_field = start_node.attrib.get('threshold-field', '').lower()
file_name = start_node.attrib.get('file', '').lower() file_name = start_node.attrib.get('file', '').lower()
file_field = start_node.attrib.get('file-field', '').lower() file_field = start_node.attrib.get('file-field', '').lower()
kwargs = {'lock': [], kwargs = {'data': {},
'data': {},
'defines': {}, 'defines': {},
'pre_assign': [], 'pre_assign': [],
'post_assign': []} 'post_assign': []}
@ -253,10 +252,6 @@ class XmlSerializer(Serializer):
elif not isinstance(context, list): elif not isinstance(context, list):
context = [context] context = [context]
context.append(node.text) context.append(node.text)
elif node.tag == 'lock':
if not node.text:
self.raise_parser_exception('Empty %s tag' % node.tag)
kwargs['lock'].append(node.text)
elif node.tag == 'pick': elif node.tag == 'pick':
if not node.text: if not node.text:
self.raise_parser_exception('Empty %s tag' % node.tag) self.raise_parser_exception('Empty %s tag' % node.tag)

View File

@ -15,15 +15,12 @@ from builtins import str
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
import re
import warnings import warnings
from lxml import etree from lxml import etree
from lxml.etree import SubElement from lxml.etree import SubElement
from ..workflow import Workflow from ..workflow import Workflow
from .. import specs, operators
from ..task import Task, TaskStateNames from ..task import Task, TaskStateNames
from ..operators import (Attrib, Assign, PathAttrib, Equal, NotEqual, from ..operators import (Attrib, Assign, PathAttrib, Equal, NotEqual, GreaterThan, LessThan, Match)
GreaterThan, LessThan, Match)
from ..specs.AcquireMutex import AcquireMutex from ..specs.AcquireMutex import AcquireMutex
from ..specs.Cancel import Cancel from ..specs.Cancel import Cancel
from ..specs.CancelTask import CancelTask from ..specs.CancelTask import CancelTask
@ -43,10 +40,8 @@ from ..specs.SubWorkflow import SubWorkflow
from ..specs.ThreadStart import ThreadStart from ..specs.ThreadStart import ThreadStart
from ..specs.ThreadMerge import ThreadMerge from ..specs.ThreadMerge import ThreadMerge
from ..specs.ThreadSplit import ThreadSplit from ..specs.ThreadSplit import ThreadSplit
from ..specs.Transform import Transform
from ..specs.Trigger import Trigger from ..specs.Trigger import Trigger
from ..specs.WorkflowSpec import WorkflowSpec from ..specs.WorkflowSpec import WorkflowSpec
from ..specs.LoopResetTask import LoopResetTask
from .base import Serializer, spec_map, op_map from .base import Serializer, spec_map, op_map
from .exceptions import TaskNotSupportedError from .exceptions import TaskNotSupportedError
@ -726,31 +721,17 @@ class XmlSerializer(Serializer):
workflow.task_tree = self.deserialize_task(workflow, task_tree_elem[0]) workflow.task_tree = self.deserialize_task(workflow, task_tree_elem[0])
# Re-connect parents # Re-connect parents
for task in workflow.get_tasks(): for task in workflow.get_tasks_iterator():
task.parent = workflow.get_task(task.parent) if task.parent is not None:
task.parent = workflow.get_task_from_id(task.parent)
# last_task # last_task
last_task = elem.findtext('last-task') last_task = elem.findtext('last-task')
if last_task is not None: if last_task is not None:
workflow.last_task = workflow.get_task(last_task) workflow.last_task = workflow.get_task_from_id(last_task)
return workflow return workflow
def serialize_loop_reset_task(self, spec):
elem = etree.Element('loop-reset-task')
SubElement(elem, 'destination_id').text = str(spec.destination_id)
SubElement(elem, 'destination_spec_name').text = str(spec.destination_spec_name)
return self.serialize_task_spec(spec, elem)
def deserialize_loop_reset_task(self, wf_spec, elem, cls=LoopResetTask, **kwargs):
destination_id = elem.findtext('destination_id')
destination_spec_name = elem.findtext('destination_spec_name')
task = self.deserialize_task_spec(wf_spec, elem, cls,
destination_id=destination_id,
destination_spec_name=destination_spec_name)
return task
def serialize_task(self, task, skip_children=False): def serialize_task(self, task, skip_children=False):
assert isinstance(task, Task) assert isinstance(task, Task)

View File

@ -55,9 +55,9 @@ class Cancel(TaskSpec):
if len(self.outputs) > 0: if len(self.outputs) > 0:
raise WorkflowException('Cancel with an output.', task_spec=self) raise WorkflowException('Cancel with an output.', task_spec=self)
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
my_task.workflow.cancel(self.cancel_successfully) my_task.workflow.cancel(self.cancel_successfully)
TaskSpec._on_complete_hook(self, my_task) return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_cancel(self) return serializer.serialize_cancel(self)

View File

@ -16,7 +16,6 @@
# License along with this library; if not, write to the Free Software # License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .base import TaskSpec
from .Trigger import Trigger from .Trigger import Trigger
@ -30,12 +29,12 @@ class CancelTask(Trigger):
parallel split. parallel split.
""" """
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
for task_name in self.context: for task_name in self.context:
cancel_tasks = my_task.workflow.get_task_spec_from_name(task_name) cancel_tasks = my_task.workflow.get_task_spec_from_name(task_name)
for cancel_task in my_task._get_root()._find_any(cancel_tasks): for cancel_task in my_task._get_root()._find_any(cancel_tasks):
cancel_task.cancel() cancel_task.cancel()
TaskSpec._on_complete_hook(self, my_task) return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_cancel_task(self) return serializer.serialize_cancel_task(self)

View File

@ -117,7 +117,7 @@ class Celery(TaskSpec):
self.call = call or [] self.call = call or []
self.args = call_args or {} self.args = call_args or {}
self.merge_results = merge_results self.merge_results = merge_results
skip = 'data', 'defines', 'pre_assign', 'post_assign', 'lock' skip = 'data', 'defines', 'pre_assign', 'post_assign'
self.kwargs = dict(i for i in list(kwargs.items()) if i[0] not in skip) self.kwargs = dict(i for i in list(kwargs.items()) if i[0] not in skip)
self.result_key = result_key self.result_key = result_key

View File

@ -55,7 +55,7 @@ class Choose(Trigger):
self.context = context self.context = context
self.choice = choice is not None and choice or [] self.choice = choice is not None and choice or []
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
context = my_task.workflow.get_task_spec_from_name(self.context) context = my_task.workflow.get_task_spec_from_name(self.context)
triggered = [] triggered = []
for task in my_task.workflow.task_tree: for task in my_task.workflow.task_tree:
@ -66,7 +66,7 @@ class Choose(Trigger):
triggered.append(task) triggered.append(task)
for task in triggered: for task in triggered:
context._predict(task) context._predict(task)
TaskSpec._on_complete_hook(self, my_task) return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_choose(self) return serializer.serialize_choose(self)

View File

@ -61,16 +61,7 @@ class ExclusiveChoice(MultiChoice):
if self.default_task_spec is None: if self.default_task_spec is None:
raise WorkflowException('A default output is required.', task_spec=self) raise WorkflowException('A default output is required.', task_spec=self)
def _predict_hook(self, my_task): def _run_hook(self, my_task):
# If the task's status is not predicted, we default to MAYBE
# for all it's outputs except the default choice, which is
# LIKELY.
# Otherwise, copy my own state to the children.
my_task._sync_children(self.outputs)
spec = self._wf_spec.get_task_spec_from_name(self.default_task_spec)
my_task._set_likely_task(spec)
def _on_complete_hook(self, my_task):
output = self._wf_spec.get_task_spec_from_name(self.default_task_spec) output = self._wf_spec.get_task_spec_from_name(self.default_task_spec)
for condition, spec_name in self.cond_task_specs: for condition, spec_name in self.cond_task_specs:
@ -82,6 +73,10 @@ class ExclusiveChoice(MultiChoice):
raise WorkflowException(f'No conditions satisfied for {my_task.task_spec.name}', task_spec=self) raise WorkflowException(f'No conditions satisfied for {my_task.task_spec.name}', task_spec=self)
my_task._sync_children([output], TaskState.FUTURE) my_task._sync_children([output], TaskState.FUTURE)
for child in my_task.children:
child.task_spec._predict(child, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_exclusive_choice(self) return serializer.serialize_exclusive_choice(self)

View File

@ -120,8 +120,7 @@ class Join(TaskSpec):
# If the task is predicted with less outputs than he has # If the task is predicted with less outputs than he has
# children, that means the prediction may be incomplete (for # children, that means the prediction may be incomplete (for
# example, because a prediction is not yet possible at this time). # example, because a prediction is not yet possible at this time).
if not child._is_definite() \ if child._is_predicted() and len(child.task_spec.outputs) > len(child.children):
and len(child.task_spec.outputs) > len(child.children):
return True return True
return False return False
@ -153,8 +152,8 @@ class Join(TaskSpec):
# Look at the tree to find all places where this task is used. # Look at the tree to find all places where this task is used.
tasks = [] tasks = []
for input in self.inputs: for spec in self.inputs:
tasks += my_task.workflow.task_mapping[my_task.thread_id][input] tasks.extend([ t for t in my_task.workflow.task_tree._find_any(spec) if t.thread_id == my_task.thread_id ])
# Look up which tasks have already completed. # Look up which tasks have already completed.
waiting_tasks = [] waiting_tasks = []
@ -162,7 +161,7 @@ class Join(TaskSpec):
for task in tasks: for task in tasks:
if task.parent is None or task._has_state(TaskState.COMPLETED): if task.parent is None or task._has_state(TaskState.COMPLETED):
completed += 1 completed += 1
else: elif not task._is_finished():
waiting_tasks.append(task) waiting_tasks.append(task)
# If the threshold was reached, get ready to fire. # If the threshold was reached, get ready to fire.
@ -186,8 +185,6 @@ class Join(TaskSpec):
waiting_tasks = [] waiting_tasks = []
completed = 0 completed = 0
for task in tasks: for task in tasks:
# Refresh path prediction.
task.task_spec._predict(task)
if not self._branch_may_merge_at(task): if not self._branch_may_merge_at(task):
completed += 1 completed += 1
elif self._branch_is_complete(task): elif self._branch_is_complete(task):
@ -204,16 +201,16 @@ class Join(TaskSpec):
Returns True if the threshold was reached, False otherwise. Returns True if the threshold was reached, False otherwise.
Also returns the list of tasks that yet need to be completed. Also returns the list of tasks that yet need to be completed.
""" """
# If the threshold was already reached, there is nothing else to do. if my_task._is_finished():
if my_task._has_state(TaskState.COMPLETED): return False, None
return True, None
if my_task._has_state(TaskState.READY): if my_task._has_state(TaskState.READY):
return True, None return True, None
# Check whether we may fire. # Check whether we may fire.
if self.split_task is None: if self.split_task is None:
return self._check_threshold_unstructured(my_task, force) return self._check_threshold_unstructured(my_task, force)
return self._check_threshold_structured(my_task, force) else:
return self._check_threshold_structured(my_task, force)
def _update_hook(self, my_task): def _update_hook(self, my_task):
# Check whether enough incoming branches have completed. # Check whether enough incoming branches have completed.
@ -224,22 +221,16 @@ class Join(TaskSpec):
if self.cancel_remaining: if self.cancel_remaining:
for task in waiting_tasks: for task in waiting_tasks:
task.cancel() task.cancel()
# Update the state of our child objects. # Update the state of our child objects.
self._do_join(my_task) self._do_join(my_task)
else: return True
elif not my_task._is_finished():
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
def _do_join(self, my_task): def _find_tasks(self, my_task):
split_task = self._get_split_task(my_task) split_task = self._get_split_task(my_task)
# Identify all corresponding task instances within the thread. # Identify all corresponding task instances within the thread.
# Also remember which of those instances was most recently changed,
# because we are making this one the instance that will
# continue the thread of control. In other words, we will continue
# to build the task tree underneath the most recently changed task.
last_changed = None
thread_tasks = [] thread_tasks = []
for task in split_task._find_any(self): for task in split_task._find_any(self):
# Ignore tasks from other threads. # Ignore tasks from other threads.
@ -248,27 +239,16 @@ class Join(TaskSpec):
# Ignore my outgoing branches. # Ignore my outgoing branches.
if self.split_task and task._is_descendant_of(my_task): if self.split_task and task._is_descendant_of(my_task):
continue continue
# We have found a matching instance. # We have found a matching instance.
thread_tasks.append(task) thread_tasks.append(task)
return thread_tasks
# Check whether the state of the instance was recently def _do_join(self, my_task):
# changed.
changed = task.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 # Execution will continue from this task; mark others as cancelled
# is the most recently changed task, for which we assume READY. for task in self._find_tasks(my_task):
# By setting the state to READY only, we allow for calling if task != my_task:
# :class:`Task.complete()`, which leads to the task tree being task._set_state(TaskState.CANCELLED)
# (re)built underneath the node.
for task in thread_tasks:
if task == last_changed:
self.entered_event.emit(my_task.workflow, my_task)
task._ready()
else:
task._set_state(TaskState.COMPLETED)
task._drop_children() task._drop_children()
def _on_trigger(self, my_task): def _on_trigger(self, my_task):
@ -276,10 +256,11 @@ class Join(TaskSpec):
May be called to fire the Join before the incoming branches are May be called to fire the Join before the incoming branches are
completed. completed.
""" """
for task in my_task.workflow.task_tree._find_any(self): tasks = sorted(self._find_tasks(my_task), key=lambda t: t.last_state_change)
if task.thread_id != my_task.thread_id: for task in tasks[:-1]:
continue task._set_state(TaskState.CANCELLED)
self._do_join(task) task._drop_children()
tasks[-1]._ready()
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_join(self) return serializer.serialize_join(self)

View File

@ -1,73 +0,0 @@
# -*- coding: utf-8 -*-
# Copyright (C) 2021 Sartography
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# 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 .base import TaskSpec
from ..task import TaskState
from SpiffWorkflow.exceptions import WorkflowTaskException
class LoopResetTask(TaskSpec):
"""
This task is used as a placeholder when we are going to loopback
to a previous point in the workflow. When this task is completed,
it will reset the workflow back to a previous point.
"""
def __init__(self, wf_spec, name, destination_id, destination_spec_name, **kwargs):
"""
Constructor.
:param script: the script that must be executed by the script engine.
"""
super(LoopResetTask, self).__init__(wf_spec, name, **kwargs)
self.destination_id = destination_id
self.destination_spec_name = destination_spec_name
def _on_complete_hook(self, task):
try:
# Prefer the exact task id, but if not available, use the
# last instance of the task_spec.
destination = task.workflow.get_task(self.destination_id)
if not destination:
destination = task.workflow.get_tasks_from_spec_name(
self.destination_spec_name)[-1]
destination.reset_token(task.data, reset_data=False)
except Exception as e:
# set state to WAITING (because it is definitely not COMPLETED)
# and raise WorkflowException pointing to this task because
# maybe upstream someone will be able to handle this situation
task._set_state(TaskState.WAITING)
if isinstance(e, WorkflowTaskException):
e.add_note('Error occurred during a loop back to a previous step.')
raise e
else:
raise WorkflowTaskException(
'Error during loop back:' + str(e), task=task, exception=e)
super(LoopResetTask, self)._on_complete_hook(task)
def serialize(self, serializer):
return serializer.serialize_loop_reset_task(self)
@classmethod
def deserialize(cls, serializer, wf_spec, s_state):
return serializer.deserialize_loop_reset_task(wf_spec, s_state)

View File

@ -89,32 +89,18 @@ class MultiChoice(TaskSpec):
# The caller needs to make sure that predict() is called. # The caller needs to make sure that predict() is called.
def _predict_hook(self, my_task): def _predict_hook(self, my_task):
if self.choice: conditional, unconditional = [], []
outputs = [self._wf_spec.get_task_spec_from_name(o)
for o in self.choice]
else:
outputs = self.outputs
# Default to MAYBE for all conditional outputs, default to LIKELY
# for unconditional ones. We can not default to FUTURE, because
# a call to trigger() may override the unconditional paths.
my_task._sync_children(outputs)
if not my_task._is_definite():
best_state = my_task.state
else:
best_state = TaskState.LIKELY
# Collect a list of all unconditional outputs.
outputs = []
for condition, output in self.cond_task_specs: for condition, output in self.cond_task_specs:
if condition is None: if self.choice is not None and output not in self.choice:
outputs.append(self._wf_spec.get_task_spec_from_name(output))
for child in my_task.children:
if child._is_definite():
continue continue
if child.task_spec in outputs: if condition is None:
child._set_state(best_state) unconditional.append(self._wf_spec.get_task_spec_from_name(output))
else:
conditional.append(self._wf_spec.get_task_spec_from_name(output))
state = TaskState.MAYBE if my_task.state == TaskState.MAYBE else TaskState.LIKELY
my_task._sync_children(unconditional, state)
for spec in conditional:
my_task._add_child(spec, TaskState.MAYBE)
def _get_matching_outputs(self, my_task): def _get_matching_outputs(self, my_task):
outputs = [] outputs = []
@ -125,12 +111,12 @@ class MultiChoice(TaskSpec):
outputs.append(self._wf_spec.get_task_spec_from_name(output)) outputs.append(self._wf_spec.get_task_spec_from_name(output))
return outputs return outputs
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
""" """Runs the task. Should not be called directly."""
Runs the task. Should not be called directly.
Returns True if completed, False otherwise.
"""
my_task._sync_children(self._get_matching_outputs(my_task), TaskState.FUTURE) my_task._sync_children(self._get_matching_outputs(my_task), TaskState.FUTURE)
for child in my_task.children:
child.task_spec._predict(child, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_multi_choice(self) return serializer.serialize_multi_choice(self)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels # Copyright (C) 2007 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -75,33 +74,24 @@ class MultiInstance(TaskSpec):
for output in self.outputs: for output in self.outputs:
new_task = my_task._add_child(output, state) new_task = my_task._add_child(output, state)
new_task.triggered = True new_task.triggered = True
output._predict(new_task) output._predict(new_task, mask=TaskState.FUTURE|TaskState.READY|TaskState.PREDICTED_MASK)
def _get_predicted_outputs(self, my_task): def _get_predicted_outputs(self, my_task):
split_n = int(valueof(my_task, self.times, 1)) split_n = int(valueof(my_task, self.times, 1))
return self.outputs * split_n
# Predict the outputs.
outputs = []
for i in range(split_n):
outputs += self.outputs
return outputs
def _predict_hook(self, my_task): def _predict_hook(self, my_task):
split_n = int(valueof(my_task, self.times, 1)) outputs = self._get_predicted_outputs(my_task)
my_task._set_internal_data(splits=split_n)
# Create the outgoing tasks.
outputs = []
for i in range(split_n):
outputs += self.outputs
if my_task._is_definite(): if my_task._is_definite():
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
else: else:
my_task._sync_children(outputs, TaskState.LIKELY) my_task._sync_children(outputs, TaskState.LIKELY)
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
outputs = self._get_predicted_outputs(my_task) outputs = self._get_predicted_outputs(my_task)
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
self._predict(my_task, mask=TaskState.FUTURE|TaskState.PREDICTED_MASK)
return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_multi_instance(self) return serializer.serialize_multi_instance(self)

View File

@ -47,10 +47,10 @@ class ReleaseMutex(TaskSpec):
TaskSpec.__init__(self, wf_spec, name, **kwargs) TaskSpec.__init__(self, wf_spec, name, **kwargs)
self.mutex = mutex self.mutex = mutex
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
mutex = my_task.workflow._get_mutex(self.mutex) mutex = my_task.workflow._get_mutex(self.mutex)
mutex.unlock() mutex.unlock()
TaskSpec._on_complete_hook(self, my_task) return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_release_mutex(self) return serializer.serialize_release_mutex(self)

View File

@ -98,41 +98,31 @@ class SubWorkflow(TaskSpec):
xml = etree.parse(fp).getroot() xml = etree.parse(fp).getroot()
wf_spec = WorkflowSpec.deserialize(serializer, xml, filename=file_name) wf_spec = WorkflowSpec.deserialize(serializer, xml, filename=file_name)
outer_workflow = my_task.workflow.outer_workflow outer_workflow = my_task.workflow.outer_workflow
return Workflow(wf_spec, parent=outer_workflow) subworkflow = Workflow(wf_spec, parent=outer_workflow)
my_task._sync_children(self.outputs, TaskState.FUTURE)
def _on_ready_before_hook(self, my_task):
subworkflow = self._create_subworkflow(my_task)
subworkflow.completed_event.connect(
self._on_subworkflow_completed, my_task)
self._integrate_subworkflow_tree(my_task, subworkflow)
my_task._set_internal_data(subworkflow=subworkflow)
def _integrate_subworkflow_tree(self, my_task, subworkflow):
# Integrate the tree of the subworkflow into the tree of this workflow.
my_task._sync_children(self.outputs, TaskState.LIKELY)
for child in subworkflow.task_tree.children: for child in subworkflow.task_tree.children:
my_task.children.insert(0, child) my_task.children.insert(0, child)
child.parent = my_task child.parent = my_task
child.state = TaskState.READY
subworkflow.completed_event.connect(self._on_subworkflow_completed, my_task)
my_task._set_internal_data(subworkflow=subworkflow)
my_task._set_state(TaskState.WAITING)
def _on_ready_hook(self, my_task): def _run_hook(self, my_task):
# Assign variables, if so requested. # Assign variables, if so requested.
subworkflow = my_task._get_internal_data('subworkflow') subworkflow = my_task._get_internal_data('subworkflow')
for child in subworkflow.task_tree.children: for child in subworkflow.task_tree.children:
for assignment in self.in_assign: for assignment in self.in_assign:
assignment.assign(my_task, child) assignment.assign(my_task, child)
child.task_spec._update(child) child.task_spec._update(child)
# Instead of completing immediately, we'll wait for the subworkflow to complete return True
my_task._set_state(TaskState.WAITING)
def _update_hook(self, my_task): def _update_hook(self, my_task):
super()._update_hook(my_task) super()._update_hook(my_task)
subworkflow = my_task._get_internal_data('subworkflow') subworkflow = my_task._get_internal_data('subworkflow')
if subworkflow is None: if subworkflow is None:
# On the first update, we have to create the subworkflow self._create_subworkflow(my_task)
return True
elif subworkflow.is_completed(): elif subworkflow.is_completed():
# Then wait until it finishes to complete
my_task.complete() my_task.complete()
def _on_subworkflow_completed(self, subworkflow, my_task): def _on_subworkflow_completed(self, subworkflow, my_task):

View File

@ -62,8 +62,7 @@ class ThreadMerge(Join):
# task that did the conditional parallel split. # task that did the conditional parallel split.
split_task = my_task._find_ancestor_from_name(self.split_task) split_task = my_task._find_ancestor_from_name(self.split_task)
if split_task is None: if split_task is None:
msg = 'Join with %s, which was not reached' % self.split_task raise WorkflowException(f'Join with %s, which was not reached {self.split_task}', task_spec=self)
raise WorkflowException(msg, task_spec=self)
tasks = split_task.task_spec._get_activated_threads(split_task) tasks = split_task.task_spec._get_activated_threads(split_task)
# The default threshold is the number of threads that were started. # The default threshold is the number of threads that were started.
@ -105,8 +104,7 @@ class ThreadMerge(Join):
my_task._set_state(TaskState.WAITING) my_task._set_state(TaskState.WAITING)
return return
split_task_spec = my_task.workflow.get_task_spec_from_name( split_task_spec = my_task.workflow.get_task_spec_from_name(self.split_task)
self.split_task)
split_task = my_task._find_ancestor(split_task_spec) split_task = my_task._find_ancestor(split_task_spec)
# Find the inbound task that was completed last. # Find the inbound task that was completed last.

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels # Copyright (C) 2007 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -108,31 +107,25 @@ class ThreadSplit(TaskSpec):
new_task = my_task.add_child(output, TaskState.READY) new_task = my_task.add_child(output, TaskState.READY)
new_task.triggered = True new_task.triggered = True
def _predict_hook(self, my_task): def _get_predicted_outputs(self, my_task):
split_n = int(valueof(my_task, self.times)) split_n = int(valueof(my_task, self.times))
return [self.thread_starter] * split_n
def _predict_hook(self, my_task):
# if we were created with thread_starter suppressed, connect it now. # if we were created with thread_starter suppressed, connect it now.
if self.thread_starter is None: if self.thread_starter is None:
self.thread_starter = self.outputs[0] self.thread_starter = self.outputs[0]
# Predict the outputs. outputs = self._get_predicted_outputs(my_task)
outputs = []
for i in range(split_n):
outputs.append(self.thread_starter)
if my_task._is_definite(): if my_task._is_definite():
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
else: else:
my_task._sync_children(outputs, TaskState.LIKELY) my_task._sync_children(outputs, TaskState.LIKELY)
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
# Split, and remember the number of splits in the context data. outputs = self._get_predicted_outputs(my_task)
split_n = int(valueof(my_task, self.times))
# Create the outgoing tasks.
outputs = []
for i in range(split_n):
outputs.append(self.thread_starter)
my_task._sync_children(outputs, TaskState.FUTURE) my_task._sync_children(outputs, TaskState.FUTURE)
return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_thread_split(self) return serializer.serialize_thread_split(self)

View File

@ -17,6 +17,7 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 USA # 02110-1301 USA
from .base import TaskSpec from .base import TaskSpec
from SpiffWorkflow.task import TaskState
class ThreadStart(TaskSpec): class ThreadStart(TaskSpec):
@ -42,9 +43,10 @@ class ThreadStart(TaskSpec):
TaskSpec.__init__(self, wf_spec, name, **kwargs) TaskSpec.__init__(self, wf_spec, name, **kwargs)
self.internal = True self.internal = True
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
my_task._assign_new_thread_id() my_task._assign_new_thread_id()
TaskSpec._on_complete_hook(self, my_task) my_task._sync_children(self.outputs, TaskState.READY)
return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_thread_start(self) return serializer.serialize_thread_start(self)

View File

@ -1,6 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from builtins import range
# Copyright (C) 2007 Samuel Abels # Copyright (C) 2007 Samuel Abels
# #
# This library is free software; you can redistribute it and/or # This library is free software; you can redistribute it and/or
@ -65,15 +64,14 @@ class Trigger(TaskSpec):
self.queued += 1 self.queued += 1
# All tasks that have already completed need to be put back to # All tasks that have already completed need to be put back to
# READY. # READY.
for thetask in my_task.workflow.task_tree: for task in my_task.workflow.task_tree:
if thetask.thread_id != my_task.thread_id: if task.thread_id != my_task.thread_id:
continue continue
if (thetask.task_spec == self and if task.task_spec == self and task._has_state(TaskState.COMPLETED):
thetask._has_state(TaskState.COMPLETED)): task._set_state(TaskState.FUTURE)
thetask._set_state(TaskState.FUTURE) task._ready()
thetask._ready()
def _on_complete_hook(self, my_task): def _run_hook(self, my_task):
""" """
A hook into _on_complete() that does the task specific work. A hook into _on_complete() that does the task specific work.
@ -85,10 +83,10 @@ class Trigger(TaskSpec):
times = int(valueof(my_task, self.times, 1)) + self.queued times = int(valueof(my_task, self.times, 1)) + self.queued
for i in range(times): for i in range(times):
for task_name in self.context: for task_name in self.context:
task = my_task.workflow.get_task_spec_from_name(task_name) task_spec = my_task.workflow.get_task_spec_from_name(task_name)
task._on_trigger(my_task) task_spec._on_trigger(my_task)
self.queued = 0 self.queued = 0
TaskSpec._on_complete_hook(self, my_task) return True
def serialize(self, serializer): def serialize(self, serializer):
return serializer.serialize_trigger(self) return serializer.serialize_trigger(self)

View File

@ -74,10 +74,6 @@ class TaskSpec(object):
:param wf_spec: A reference to the workflow specification that owns it. :param wf_spec: A reference to the workflow specification that owns it.
:type name: string :type name: string
:param name: A name for the task. :param name: A name for the task.
:type lock: list(str)
:param lock: A list of mutex names. The mutex is acquired
on entry of execute() and released on leave of
execute().
:type manual: bool :type manual: bool
:param manual: Whether this task requires a manual action to complete. :param manual: Whether this task requires a manual action to complete.
:type data: dict((str, object)) :type data: dict((str, object))
@ -107,7 +103,6 @@ class TaskSpec(object):
self.defines = kwargs.get('defines', {}) self.defines = kwargs.get('defines', {})
self.pre_assign = kwargs.get('pre_assign',[]) self.pre_assign = kwargs.get('pre_assign',[])
self.post_assign = kwargs.get('post_assign', []) self.post_assign = kwargs.get('post_assign', [])
self.locks = kwargs.get('lock', [])
self.lookahead = 2 # Maximum number of MAYBE predictions. self.lookahead = 2 # Maximum number of MAYBE predictions.
# Events. # Events.
@ -213,7 +208,7 @@ class TaskSpec(object):
if len(self.inputs) < 1: if len(self.inputs) < 1:
raise WorkflowException(self, 'No input task connected.') raise WorkflowException(self, 'No input task connected.')
def _predict(self, my_task, seen=None, looked_ahead=0): def _predict(self, my_task, seen=None, looked_ahead=0, mask=TaskState.PREDICTED_MASK):
""" """
Updates the branch such that all possible future routes are added. Updates the branch such that all possible future routes are added.
@ -229,26 +224,25 @@ class TaskSpec(object):
if seen is None: if seen is None:
seen = [] seen = []
self._predict_hook(my_task) if my_task._has_state(mask):
if not my_task._is_definite(): self._predict_hook(my_task)
if my_task._is_predicted():
seen.append(self) seen.append(self)
look_ahead = my_task._is_definite() or looked_ahead + 1 < self.lookahead look_ahead = my_task._is_definite() or looked_ahead + 1 < self.lookahead
for child in my_task.children: for child in my_task.children:
if not child._is_finished() and child not in seen and look_ahead: if child._has_state(mask) and child not in seen and look_ahead:
child.task_spec._predict(child, seen[:], looked_ahead + 1) child.task_spec._predict(child, seen[:], looked_ahead + 1, mask)
def _predict_hook(self, my_task): def _predict_hook(self, my_task):
# If the task's status is not predicted, we default to FUTURE for all it's outputs. # If the task's status is definite, we default to FUTURE for all it's outputs.
# Otherwise, copy my own state to the children. # Otherwise, copy my own state to the children.
if my_task._is_definite(): if my_task._is_definite():
best_state = TaskState.FUTURE best_state = TaskState.FUTURE
else: else:
best_state = my_task.state best_state = my_task.state
my_task._sync_children(self.outputs, best_state) my_task._sync_children(self.outputs, best_state)
for child in my_task.children:
if not child._is_definite():
child._set_state(best_state)
def _update(self, my_task): def _update(self, my_task):
""" """
@ -281,42 +275,13 @@ class TaskSpec(object):
assert my_task is not None assert my_task is not None
self.test() self.test()
# Acquire locks, if any.
for lock in self.locks:
mutex = my_task.workflow._get_mutex(lock)
if not mutex.testandset():
return
# Assign variables, if so requested. # Assign variables, if so requested.
for assignment in self.pre_assign: for assignment in self.pre_assign:
assignment.assign(my_task, my_task) assignment.assign(my_task, my_task)
# Run task-specific code. # Run task-specific code.
self._on_ready_before_hook(my_task)
self.reached_event.emit(my_task.workflow, my_task)
self._on_ready_hook(my_task) self._on_ready_hook(my_task)
self.reached_event.emit(my_task.workflow, my_task)
# Run user code, if any.
if self.ready_event.emit(my_task.workflow, my_task):
# Assign variables, if so requested.
for assignment in self.post_assign:
assignment.assign(my_task, my_task)
# Release locks, if any.
for lock in self.locks:
mutex = my_task.workflow._get_mutex(lock)
mutex.unlock()
self.finished_event.emit(my_task.workflow, my_task)
def _on_ready_before_hook(self, my_task):
"""
A hook into _on_ready() that does the task specific work.
:type my_task: Task
:param my_task: The associated task in the task tree.
"""
pass
def _on_ready_hook(self, my_task): def _on_ready_hook(self, my_task):
""" """
@ -327,6 +292,35 @@ class TaskSpec(object):
""" """
pass pass
def _run(self, my_task):
"""
Run the task.
:type my_task: Task
:param my_task: The associated task in the task tree.
:rtype: boolean or None
:returns: the value returned by the task spec's run method.
"""
result = self._run_hook(my_task)
# Run user code, if any.
if self.ready_event.emit(my_task.workflow, my_task):
# Assign variables, if so requested.
for assignment in self.post_assign:
assignment.assign(my_task, my_task)
self.finished_event.emit(my_task.workflow, my_task)
return result
def _run_hook(self, my_task):
"""
A hook into _run() that does the task specific work.
:type my_task: Task
:param my_task: The associated task in the task tree.
"""
return True
def _on_cancel(self, my_task): def _on_cancel(self, my_task):
""" """
May be called by another task to cancel the operation before it was May be called by another task to cancel the operation before it was
@ -359,20 +353,12 @@ class TaskSpec(object):
:rtype: boolean :rtype: boolean
:returns: True on success, False otherwise. :returns: True on success, False otherwise.
""" """
assert my_task is not None
# We have to set the last task here, because the on_complete_hook
# of a loopback task may overwrite what the last_task will be.
my_task.workflow.last_task = my_task
self._on_complete_hook(my_task) self._on_complete_hook(my_task)
for child in my_task.children: for child in my_task.children:
# Don't like this, but this is the most expedient way of preventing cancelled tasks from reactivation if not child._is_finished():
if child.state != TaskState.CANCELLED:
child.task_spec._update(child) child.task_spec._update(child)
my_task.workflow._task_completed_notify(my_task) my_task.workflow._task_completed_notify(my_task)
self.completed_event.emit(my_task.workflow, my_task) self.completed_event.emit(my_task.workflow, my_task)
return True
def _on_complete_hook(self, my_task): def _on_complete_hook(self, my_task):
""" """
@ -419,7 +405,6 @@ class TaskSpec(object):
'defines':self.defines, 'defines':self.defines,
'pre_assign':self.pre_assign, 'pre_assign':self.pre_assign,
'post_assign':self.post_assign, 'post_assign':self.post_assign,
'locks':self.locks,
'lookahead':self.lookahead, 'lookahead':self.lookahead,
} }
@ -457,7 +442,6 @@ class TaskSpec(object):
out.defines = s_state.get('defines') out.defines = s_state.get('defines')
out.pre_assign = s_state.get('pre_assign') out.pre_assign = s_state.get('pre_assign')
out.post_assign = s_state.get('post_assign') out.post_assign = s_state.get('post_assign')
out.locks = s_state.get('locks')
out.lookahead = s_state.get('lookahead') out.lookahead = s_state.get('lookahead')
return out return out

View File

@ -6,6 +6,7 @@ from SpiffWorkflow.bpmn.parser.BpmnParser import BpmnValidator, full_tag
from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent from SpiffWorkflow.bpmn.specs.events.EndEvent import EndEvent
from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent from SpiffWorkflow.bpmn.specs.events.StartEvent import StartEvent
from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, BoundaryEvent, IntermediateCatchEvent from SpiffWorkflow.bpmn.specs.events.IntermediateEvent import IntermediateThrowEvent, BoundaryEvent, IntermediateCatchEvent
from SpiffWorkflow.spiff.specs.none_task import NoneTask from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask from SpiffWorkflow.spiff.specs.manual_task import ManualTask
from SpiffWorkflow.spiff.specs.user_task import UserTask from SpiffWorkflow.spiff.specs.user_task import UserTask
@ -13,10 +14,18 @@ from SpiffWorkflow.spiff.specs.script_task import ScriptTask
from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity
from SpiffWorkflow.spiff.specs.service_task import ServiceTask from SpiffWorkflow.spiff.specs.service_task import ServiceTask
from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask
from SpiffWorkflow.spiff.parser.task_spec import SpiffTaskParser, SubWorkflowParser, CallActivityParser, ServiceTaskParser, ScriptTaskParser from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
from SpiffWorkflow.spiff.parser.task_spec import (
SpiffTaskParser,
SubWorkflowParser,
CallActivityParser,
ServiceTaskParser,
ScriptTaskParser,
BusinessRuleTaskParser
)
from SpiffWorkflow.spiff.parser.event_parsers import (SpiffStartEventParser, SpiffEndEventParser, SpiffBoundaryEventParser, from SpiffWorkflow.spiff.parser.event_parsers import (SpiffStartEventParser, SpiffEndEventParser, SpiffBoundaryEventParser,
SpiffIntermediateCatchEventParser, SpiffIntermediateThrowEventParser, SpiffSendTaskParser, SpiffReceiveTaskParser) SpiffIntermediateCatchEventParser, SpiffIntermediateThrowEventParser, SpiffSendTaskParser, SpiffReceiveTaskParser)
from SpiffWorkflow.dmn.specs import BusinessRuleTask
from SpiffWorkflow.spiff.parser.task_spec import BusinessRuleTaskParser from SpiffWorkflow.spiff.parser.task_spec import BusinessRuleTaskParser
@ -44,4 +53,3 @@ class SpiffBpmnParser(BpmnDmnParser):
full_tag('receiveTask'): (SpiffReceiveTaskParser, ReceiveTask), full_tag('receiveTask'): (SpiffReceiveTaskParser, ReceiveTask),
full_tag('businessRuleTask'): (BusinessRuleTaskParser, BusinessRuleTask) full_tag('businessRuleTask'): (BusinessRuleTaskParser, BusinessRuleTask)
} }

View File

@ -1,11 +1,11 @@
from lxml import etree from lxml import etree
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask
from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser from SpiffWorkflow.bpmn.parser.TaskParser import TaskParser
from SpiffWorkflow.bpmn.parser.task_parsers import SubprocessParser from SpiffWorkflow.bpmn.parser.task_parsers import SubprocessParser
from SpiffWorkflow.bpmn.parser.util import xpath_eval from SpiffWorkflow.bpmn.parser.util import xpath_eval
from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask
from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
SPIFFWORKFLOW_MODEL_NS = 'http://spiffworkflow.org/bpmn/schema/1.0/core' SPIFFWORKFLOW_MODEL_NS = 'http://spiffworkflow.org/bpmn/schema/1.0/core'
SPIFFWORKFLOW_MODEL_PREFIX = 'spiffworkflow' SPIFFWORKFLOW_MODEL_PREFIX = 'spiffworkflow'
@ -169,13 +169,19 @@ class BusinessRuleTaskParser(SpiffTaskParser):
def create_task(self): def create_task(self):
decision_ref = self.get_decision_ref(self.node) decision_ref = self.get_decision_ref(self.node)
return BusinessRuleTask(self.spec, extensions = self.parse_extensions()
self.get_task_spec_name(), prescript = extensions.get('preScript')
dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node), postscript = extensions.get('postScript')
lane=self.lane, return BusinessRuleTask(
position=self.position, self.spec,
description=self.node.get('name', None) self.get_task_spec_name(),
) dmnEngine=self.process_parser.parser.get_engine(decision_ref, self.node),
lane=self.lane,
position=self.position,
description=self.node.get('name', None),
prescript=prescript,
postscript=postscript,
)
@staticmethod @staticmethod
def get_decision_ref(node): def get_decision_ref(node):

View File

@ -5,7 +5,6 @@ from SpiffWorkflow.bpmn.serializer.task_spec import (
SimpleTaskConverter, SimpleTaskConverter,
StartTaskConverter, StartTaskConverter,
EndJoinConverter, EndJoinConverter,
LoopResetTaskConverter,
StartEventConverter, StartEventConverter,
EndEventConverter, EndEventConverter,
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
@ -32,6 +31,7 @@ from .task_spec import (
CallActivityTaskConverter, CallActivityTaskConverter,
ParallelMultiInstanceTaskConverter, ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter, SequentialMultiInstanceTaskConverter,
BusinessRuleTaskConverter,
) )
from SpiffWorkflow.bpmn.serializer.event_definition import MessageEventDefinitionConverter as DefaultMessageEventDefinitionConverter from SpiffWorkflow.bpmn.serializer.event_definition import MessageEventDefinitionConverter as DefaultMessageEventDefinitionConverter
@ -42,7 +42,6 @@ SPIFF_SPEC_CONFIG['task_specs'] = [
SimpleTaskConverter, SimpleTaskConverter,
StartTaskConverter, StartTaskConverter,
EndJoinConverter, EndJoinConverter,
LoopResetTaskConverter,
StartEventConverter, StartEventConverter,
EndEventConverter, EndEventConverter,
IntermediateCatchEventConverter, IntermediateCatchEventConverter,
@ -66,6 +65,7 @@ SPIFF_SPEC_CONFIG['task_specs'] = [
StandardLoopTaskConverter, StandardLoopTaskConverter,
ParallelMultiInstanceTaskConverter, ParallelMultiInstanceTaskConverter,
SequentialMultiInstanceTaskConverter, SequentialMultiInstanceTaskConverter,
BusinessRuleTaskConverter
] ]
SPIFF_SPEC_CONFIG['event_definitions'].remove(DefaultMessageEventDefinitionConverter) SPIFF_SPEC_CONFIG['event_definitions'].remove(DefaultMessageEventDefinitionConverter)
SPIFF_SPEC_CONFIG['event_definitions'].append(MessageEventDefinitionConverter) SPIFF_SPEC_CONFIG['event_definitions'].append(MessageEventDefinitionConverter)

View File

@ -1,5 +1,6 @@
from SpiffWorkflow.bpmn.serializer.helpers.spec import TaskSpecConverter from SpiffWorkflow.bpmn.serializer.helpers.spec import TaskSpecConverter
from SpiffWorkflow.bpmn.serializer.task_spec import MultiInstanceTaskConverter from SpiffWorkflow.bpmn.serializer.task_spec import MultiInstanceTaskConverter
from SpiffWorkflow.dmn.serializer.task_spec import BaseBusinessRuleTaskConverter
from SpiffWorkflow.spiff.specs.none_task import NoneTask from SpiffWorkflow.spiff.specs.none_task import NoneTask
from SpiffWorkflow.spiff.specs.manual_task import ManualTask from SpiffWorkflow.spiff.specs.manual_task import ManualTask
@ -9,6 +10,7 @@ from SpiffWorkflow.spiff.specs.service_task import ServiceTask
from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity from SpiffWorkflow.spiff.specs.subworkflow_task import SubWorkflowTask, TransactionSubprocess, CallActivity
from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask from SpiffWorkflow.spiff.specs.events.event_types import SendTask, ReceiveTask
from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask from SpiffWorkflow.spiff.specs.multiinstance_task import StandardLoopTask, ParallelMultiInstanceTask, SequentialMultiInstanceTask
from SpiffWorkflow.spiff.specs.business_rule_task import BusinessRuleTask
class SpiffBpmnTaskConverter(TaskSpecConverter): class SpiffBpmnTaskConverter(TaskSpecConverter):
@ -39,6 +41,16 @@ class UserTaskConverter(SpiffBpmnTaskConverter):
super().__init__(UserTask, registry) super().__init__(UserTask, registry)
class BusinessRuleTaskConverter(BaseBusinessRuleTaskConverter, SpiffBpmnTaskConverter):
def __init__(self, registry):
super().__init__(BusinessRuleTask, registry)
def to_dict(self, spec):
dct = BaseBusinessRuleTaskConverter.to_dict(self, spec)
dct.update(SpiffBpmnTaskConverter.to_dict(self, spec))
return dct
class SendTaskConverter(SpiffBpmnTaskConverter): class SendTaskConverter(SpiffBpmnTaskConverter):
def __init__(self, registry, typename=None): def __init__(self, registry, typename=None):

View File

@ -0,0 +1,5 @@
from SpiffWorkflow.spiff.specs.spiff_task import SpiffBpmnTask
from SpiffWorkflow.dmn.specs.BusinessRuleTask import BusinessRuleTask as DefaultBusinessRuleTask
class BusinessRuleTask(DefaultBusinessRuleTask, SpiffBpmnTask):
pass

View File

@ -77,9 +77,9 @@ class TaskState:
CANCELLED = 64 CANCELLED = 64
FINISHED_MASK = CANCELLED | COMPLETED FINISHED_MASK = CANCELLED | COMPLETED
DEFINITE_MASK = FUTURE | WAITING | READY | FINISHED_MASK DEFINITE_MASK = FUTURE | WAITING | READY
PREDICTED_MASK = FUTURE | LIKELY | MAYBE PREDICTED_MASK = LIKELY | MAYBE
NOT_FINISHED_MASK = PREDICTED_MASK | WAITING | READY NOT_FINISHED_MASK = PREDICTED_MASK | DEFINITE_MASK
ANY_MASK = FINISHED_MASK | NOT_FINISHED_MASK ANY_MASK = FINISHED_MASK | NOT_FINISHED_MASK
@ -292,43 +292,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
self.data = DeepMerge.merge(self.data, data) self.data = DeepMerge.merge(self.data, data)
data_log.info('Data update', extra=self.log_info()) data_log.info('Data update', extra=self.log_info())
def set_children_future(self):
"""
for a parallel gateway, we need to set up our
children so that the gateway figures out that it needs to join up
the inputs - otherwise our child process never gets marked as
'READY'
"""
if not self.task_spec.task_should_set_children_future(self):
return
self.task_spec.task_will_set_children_future(self)
# now we set this one to execute
self._set_state(TaskState.MAYBE)
self._sync_children(self.task_spec.outputs)
for child in self.children:
child.set_children_future()
def reset_token(self, data, reset_data=False):
"""
Resets the token to this task. This should allow a trip 'back in time'
as it were to items that have already been completed.
:type reset_data: bool
:param reset_data: Do we want to have the data be where we left of in
this task or not
"""
self.internal_data = {}
if not reset_data and self.workflow.last_task and self.workflow.last_task.data:
# This is a little sly, the data that will get inherited should
# be from the last completed task, but we don't want to alter
# the tree, so we just set the parent's data to the given data.
self.parent.data = copy.deepcopy(data)
self.workflow.last_task = self.parent
self.set_children_future() # this method actually fixes the problem
self._set_state(TaskState.FUTURE)
self.task_spec._update(self)
def __iter__(self): def __iter__(self):
return Task.Iterator(self) return Task.Iterator(self)
@ -366,9 +329,7 @@ class Task(object, metaclass=DeprecatedMetaTask):
self.children.remove(task) self.children.remove(task)
def _has_state(self, state): def _has_state(self, state):
""" """Returns True if the Task has the given state flag set."""
Returns True if the Task has the given state flag set.
"""
return (self.state & state) != 0 return (self.state & state) != 0
def _is_finished(self): def _is_finished(self):
@ -380,6 +341,43 @@ class Task(object, metaclass=DeprecatedMetaTask):
def _is_definite(self): def _is_definite(self):
return self._has_state(TaskState.DEFINITE_MASK) return self._has_state(TaskState.DEFINITE_MASK)
def set_children_future(self):
"""
for a parallel gateway, we need to set up our
children so that the gateway figures out that it needs to join up
the inputs - otherwise our child process never gets marked as
'READY'
"""
if not self.task_spec.task_should_set_children_future(self):
return
self.task_spec.task_will_set_children_future(self)
# now we set this one to execute
self._set_state(TaskState.MAYBE)
self._sync_children(self.task_spec.outputs)
for child in self.children:
child.set_children_future()
def reset_token(self, data, reset_data=False):
"""
Resets the token to this task. This should allow a trip 'back in time'
as it were to items that have already been completed.
:type reset_data: bool
:param reset_data: Do we want to have the data be where we left of in
this task or not
"""
self.internal_data = {}
if not reset_data and self.workflow.last_task and self.workflow.last_task.data:
# This is a little sly, the data that will get inherited should
# be from the last completed task, but we don't want to alter
# the tree, so we just set the parent's data to the given data.
self.parent.data = copy.deepcopy(data)
self.workflow.last_task = self.parent
self.set_children_future() # this method actually fixes the problem
self._set_state(TaskState.FUTURE)
self.task_spec._update(self)
def _add_child(self, task_spec, state=TaskState.MAYBE): def _add_child(self, task_spec, state=TaskState.MAYBE):
""" """
Adds a new child and assigns the given TaskSpec to it. Adds a new child and assigns the given TaskSpec to it.
@ -391,17 +389,60 @@ class Task(object, metaclass=DeprecatedMetaTask):
:rtype: Task :rtype: Task
:returns: The new child task. :returns: The new child task.
""" """
if task_spec is None:
raise ValueError(self, '_add_child() requires a TaskSpec')
if self._is_predicted() and state & TaskState.PREDICTED_MASK == 0: if self._is_predicted() and state & TaskState.PREDICTED_MASK == 0:
msg = 'Attempt to add non-predicted child to predicted task' raise WorkflowException('Attempt to add non-predicted child to predicted task', task_spec=self.task_spec)
raise WorkflowException(msg, task_spec=self.task_spec)
task = Task(self.workflow, task_spec, self, state=state) task = Task(self.workflow, task_spec, self, state=state)
task.thread_id = self.thread_id task.thread_id = self.thread_id
if state == TaskState.READY: if state == TaskState.READY:
task._ready() task._ready()
return task return task
def _sync_children(self, task_specs, state=TaskState.MAYBE):
"""
This method syncs up the task's children with the given list of task
specs. In other words::
- Add one child for each given TaskSpec, unless that child already
exists.
- Remove all children for which there is no spec in the given list,
unless it is a "triggered" task.
.. note::
It is an error if the task has a non-predicted child that is
not given in the TaskSpecs.
:type task_specs: list(TaskSpec)
:param task_specs: The list of task specs that may become children.
:type state: integer
:param state: The bitmask of states for the new children.
"""
if task_specs is None:
raise ValueError('"task_specs" argument is None')
new_children = task_specs[:]
# Create a list of all children that are no longer needed.
unneeded_children = []
for child in self.children:
if child.triggered:
# Triggered tasks are never removed.
pass
elif child.task_spec in new_children:
# If the task already exists, remove it from to-be-added and update its state
new_children.remove(child.task_spec)
if not child._is_finished():
child._set_state(state)
else:
if child._is_definite():
# Definite tasks must not be removed, so they HAVE to be in the given task spec list.
raise WorkflowException(f'removal of non-predicted child {child}', task_spec=self.task_spec)
unneeded_children.append(child)
# Update children accordingly
for child in unneeded_children:
self.children.remove(child)
for task_spec in new_children:
self._add_child(task_spec, state)
def _assign_new_thread_id(self, recursive=True): def _assign_new_thread_id(self, recursive=True):
""" """
Assigns a new thread id to the task. Assigns a new thread id to the task.
@ -419,78 +460,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
child.thread_id = self.thread_id child.thread_id = self.thread_id
return self.thread_id return self.thread_id
def _sync_children(self, task_specs, state=TaskState.MAYBE):
"""
This method syncs up the task's children with the given list of task
specs. In other words::
- Add one child for each given TaskSpec, unless that child already
exists.
- Remove all children for which there is no spec in the given list,
unless it is a "triggered" task.
- Handle looping back to previous tasks, so we don't end up with
an infinitely large tree.
.. note::
It is an error if the task has a non-predicted child that is
not given in the TaskSpecs.
:type task_specs: list(TaskSpec)
:param task_specs: The list of task specs that may become children.
:type state: integer
:param state: The bitmask of states for the new children.
"""
if task_specs is None:
raise ValueError('"task_specs" argument is None')
new_children = task_specs[:]
# If a child task_spec is also an ancestor, we are looping back,
# replace those specs with a loopReset task.
root_task = self._get_root()
for index, task_spec in enumerate(new_children):
ancestor_task = self._find_ancestor(task_spec)
if ancestor_task and ancestor_task != root_task:
destination = ancestor_task
new_spec = self.workflow.get_reset_task_spec(destination)
new_spec.outputs = []
new_spec.inputs = task_spec.inputs
new_children[index] = new_spec
# Create a list of all children that are no longer needed.
unneeded_children = []
for child in self.children:
# Triggered tasks are never removed.
if child.triggered:
continue
# If the task already exists, remove it from to-be-added
if child.task_spec in new_children:
new_children.remove(child.task_spec)
# We should set the state here but that breaks everything
continue
# Definite tasks must not be removed, so they HAVE to be in the given task spec list.
if child._is_definite():
raise WorkflowException(f'removal of non-predicted child {child}', task_spec=self.task_spec)
unneeded_children.append(child)
# Remove and add the children accordingly.
for child in unneeded_children:
self.children.remove(child)
for task_spec in new_children:
self._add_child(task_spec, state)
def _set_likely_task(self, task_specs):
if not isinstance(task_specs, list):
task_specs = [task_specs]
for task_spec in task_specs:
for child in self.children:
if child.task_spec != task_spec:
continue
if child._is_definite():
continue
child._set_state(TaskState.LIKELY)
def _is_descendant_of(self, parent): def _is_descendant_of(self, parent):
""" """
Returns True if parent is in the list of ancestors, returns False Returns True if parent is in the list of ancestors, returns False
@ -574,15 +543,6 @@ class Task(object, metaclass=DeprecatedMetaTask):
return self.parent return self.parent
return self.parent._find_ancestor_from_name(name) return self.parent._find_ancestor_from_name(name)
def _ready(self):
"""
Marks the task as ready for execution.
"""
if self._has_state(TaskState.COMPLETED) or self._has_state(TaskState.CANCELLED):
return
self._set_state(TaskState.READY)
self.task_spec._on_ready(self)
def get_name(self): def get_name(self):
return str(self.task_spec.name) return str(self.task_spec.name)
@ -590,14 +550,10 @@ class Task(object, metaclass=DeprecatedMetaTask):
return str(self.task_spec.description) return str(self.task_spec.description)
def get_state_name(self): def get_state_name(self):
""" """Returns a textual representation of this Task's state."""
Returns a textual representation of this Task's state.
"""
state_name = []
for state, name in list(TaskStateNames.items()): for state, name in list(TaskStateNames.items()):
if self._has_state(state): if self._has_state(state):
state_name.append(name) return name
return '|'.join(state_name)
def get_spec_data(self, name=None, default=None): def get_spec_data(self, name=None, default=None):
""" """
@ -648,37 +604,54 @@ class Task(object, metaclass=DeprecatedMetaTask):
""" """
return self.data.get(name, default) return self.data.get(name, default)
def cancel(self): def _ready(self):
""" """Marks the task as ready for execution."""
Cancels the item if it was not yet completed, and removes if self._has_state(TaskState.COMPLETED) or self._has_state(TaskState.CANCELLED):
any children that are LIKELY.
"""
if self._is_finished():
for child in self.children:
child.cancel()
return return
self._set_state(TaskState.CANCELLED) self._set_state(TaskState.READY)
self._drop_children() self.task_spec._on_ready(self)
self.task_spec._on_cancel(self)
def complete(self): def run(self):
""" """
Called by the associated task to let us know that its state Execute the task.
has changed (e.g. from FUTURE to COMPLETED.)
If the return value of task_spec._run is None, assume the task is not finished,
and move the task to WAITING.
:rtype: boolean or None
:returns: the value returned by the task spec's run method
""" """
self._set_state(TaskState.COMPLETED)
# I am taking back my previous comment about running the task after it's completed being "CRAZY"
# Turns out that tasks are in fact supposed to be complete at this point and I've been wrong all along
# about when tasks should actually be executed
start = time.time() start = time.time()
retval = self.task_spec._on_complete(self) retval = self.task_spec._run(self)
extra = self.log_info({ extra = self.log_info({
'action': 'Complete', 'action': 'Complete',
'elapsed': time.time() - start 'elapsed': time.time() - start
}) })
metrics.debug('', extra=extra) metrics.debug('', extra=extra)
if retval is None:
self._set_state(TaskState.WAITING)
else:
# If we add an error state, the we can move the task to COMPLETE or ERROR
# according to the return value.
self.complete()
return retval return retval
def cancel(self):
"""Cancels the item if it was not yet completed, and removes any children that are LIKELY."""
if self._is_finished():
for child in self.children:
child.cancel()
else:
self._set_state(TaskState.CANCELLED)
self._drop_children()
self.task_spec._on_cancel(self)
def complete(self):
"""Marks this task complete."""
self._set_state(TaskState.COMPLETED)
self.task_spec._on_complete(self)
self.workflow.last_task = self
def trigger(self, *args): def trigger(self, *args):
""" """
If recursive is True, the state is applied to the tree recursively. If recursive is True, the state is applied to the tree recursively.

View File

@ -1,30 +1,22 @@
from builtins import object from threading import Lock
try: class mutex(object):
# python 2
from mutex import mutex
except ImportError: def __init__(self):
# python 3 self.lock = Lock()
from threading import Lock
class mutex(object): def lock(self):
raise NotImplementedError
def __init__(self): def test(self):
self.lock = Lock() has = self.lock.acquire(blocking=False)
if has:
def lock(self):
raise NotImplementedError
def test(self):
has = self.lock.acquire(blocking=False)
if has:
self.lock.release()
return has
def testandset(self):
return self.lock.acquire(blocking=False)
def unlock(self):
self.lock.release() self.lock.release()
return has
def testandset(self):
return self.lock.acquire(blocking=False)
def unlock(self):
self.lock.release()

View File

@ -20,14 +20,14 @@
import logging import logging
from .specs.Simple import Simple from .specs.Simple import Simple
from .specs.LoopResetTask import LoopResetTask
from .task import Task, TaskState from .task import Task, TaskState
from .util.compat import mutex from .util.compat import mutex
from .util.event import Event from .util.event import Event
from .exceptions import WorkflowException from .exceptions import TaskNotFoundException, WorkflowException
logger = logging.getLogger('spiff') logger = logging.getLogger('spiff')
class Workflow(object): class Workflow(object):
""" """
@ -54,29 +54,25 @@ class Workflow(object):
self.outer_workflow = kwargs.get('parent', self) self.outer_workflow = kwargs.get('parent', self)
self.locks = {} self.locks = {}
self.last_task = None self.last_task = None
if deserializing: if 'Root' in workflow_spec.task_specs:
assert 'Root' in workflow_spec.task_specs root = workflow_spec.task_specs['Root']
root = workflow_spec.task_specs['Root'] # Probably deserialized
else: else:
if 'Root' in workflow_spec.task_specs: root = Simple(workflow_spec, 'Root')
root = workflow_spec.task_specs['Root']
else:
root = Simple(workflow_spec, 'Root')
logger.info('Initialize', extra=self.log_info())
# Setting TaskState.COMPLETED prevents the root task from being executed. # Setting TaskState.COMPLETED prevents the root task from being executed.
self.task_tree = Task(self, root, state=TaskState.COMPLETED) self.task_tree = Task(self, root, state=TaskState.COMPLETED)
start = self.task_tree._add_child(self.spec.start, state=TaskState.FUTURE)
self.success = True self.success = True
self.debug = False self.debug = False
# Events. # Events.
self.completed_event = Event() self.completed_event = Event()
start = self.task_tree._add_child(self.spec.start, state=TaskState.FUTURE) if not deserializing:
self._predict()
self.spec.start._predict(start) if 'parent' not in kwargs:
if 'parent' not in kwargs: start.task_spec._update(start)
start.task_spec._update(start) logger.info('Initialize', extra=self.log_info())
self.task_mapping = self._get_task_mapping() self.task_mapping = self._get_task_mapping()
@ -108,6 +104,10 @@ class Workflow(object):
return True return True
return False return False
def _predict(self, mask=TaskState.NOT_FINISHED_MASK):
for task in Workflow.get_tasks(self,TaskState.NOT_FINISHED_MASK):
task.task_spec._predict(task, mask=mask)
def _get_waiting_tasks(self): def _get_waiting_tasks(self):
waiting = Task.Iterator(self.task_tree, TaskState.WAITING) waiting = Task.Iterator(self.task_tree, TaskState.WAITING)
return [w for w in waiting] return [w for w in waiting]
@ -195,24 +195,6 @@ class Workflow(object):
""" """
return self.spec.get_task_spec_from_name(name) return self.spec.get_task_spec_from_name(name)
def get_task(self, id,tasklist=None):
"""
Returns the task with the given id.
:type id:integer
:param id: The id of a task.
:param tasklist: Optional cache of get_tasks for operations
where we are calling multiple times as when we
are deserializing the workflow
:rtype: Task
:returns: The task with the given id.
"""
if tasklist:
tasks = [task for task in tasklist if task.id == id]
else:
tasks = [task for task in self.get_tasks() if task.id == id]
return tasks[0] if len(tasks) == 1 else None
def get_tasks_from_spec_name(self, name): def get_tasks_from_spec_name(self, name):
""" """
Returns all tasks whose spec has the given name. Returns all tasks whose spec has the given name.
@ -222,15 +204,7 @@ class Workflow(object):
:rtype: list[Task] :rtype: list[Task]
:returns: A list of tasks that relate to the spec with the given name. :returns: A list of tasks that relate to the spec with the given name.
""" """
return [task for task in self.get_tasks_iterator() return [task for task in self.get_tasks_iterator() if task.task_spec.name == name]
if task.task_spec.name == name]
def empty(self,str):
if str == None:
return True
if str == '':
return True
return False
def get_tasks(self, state=TaskState.ANY_MASK): def get_tasks(self, state=TaskState.ANY_MASK):
""" """
@ -243,38 +217,6 @@ class Workflow(object):
""" """
return [t for t in Task.Iterator(self.task_tree, state)] return [t for t in Task.Iterator(self.task_tree, state)]
def reset_task_from_id(self, task_id):
"""
Runs the task with the given id.
:type task_id: integer
:param task_id: The id of the Task object.
"""
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
data = {}
if self.last_task and self.last_task.data:
data = self.last_task.data
for task in self.task_tree:
if task.id == task_id:
return task.reset_token(data)
msg = 'A task with the given task_id (%s) was not found' % task_id
raise WorkflowException(msg, task_spec=self.spec)
def get_reset_task_spec(self, destination):
"""
Returns a task, that once complete, will reset the workflow back
to a previously completed task.
:param destination: Task to reset to, on complete.
:return: TaskSpec
"""
name = "return_to_" + destination.task_spec.name
spec = self.get_task_spec_from_name(name)
if not spec:
spec = LoopResetTask(self.spec, name, destination.id,
destination.task_spec.name)
return spec
def get_tasks_iterator(self, state=TaskState.ANY_MASK): def get_tasks_iterator(self, state=TaskState.ANY_MASK):
""" """
Returns a iterator of Task objects with the given state. Returns a iterator of Task objects with the given state.
@ -286,22 +228,54 @@ class Workflow(object):
""" """
return Task.Iterator(self.task_tree, state) return Task.Iterator(self.task_tree, state)
def complete_task_from_id(self, task_id): def get_task_from_id(self, task_id, tasklist=None):
"""
Returns the task with the given id.
:type id:integer
:param id: The id of a task.
:param tasklist: Optional cache of get_tasks for operations
where we are calling multiple times as when we
are deserializing the workflow
:rtype: Task
:returns: The task with the given id.
"""
if task_id is None:
raise WorkflowException('task_id is None', task_spec=self.spec)
tasklist = tasklist or self.task_tree
for task in self.task_tree:
if task.id == task_id:
return task
msg = 'A task with the given task_id (%s) was not found' % task_id
raise TaskNotFoundException(msg, task_spec=self.spec)
def run_task_from_id(self, task_id):
""" """
Runs the task with the given id. Runs the task with the given id.
:type task_id: integer :type task_id: integer
:param task_id: The id of the Task object. :param task_id: The id of the Task object.
""" """
if task_id is None: task = self.get_task_from_id(task_id)
raise WorkflowException('task_id is None', task_spec=self.spec) return task.run()
for task in self.task_tree:
if task.id == task_id:
return task.complete()
msg = 'A task with the given task_id (%s) was not found' % task_id
raise WorkflowException(msg, task_spec=self.spec)
def complete_next(self, pick_up=True, halt_on_manual=True): def reset_task_from_id(self, task_id):
"""
Runs the task with the given id.
:type task_id: integer
:param task_id: The id of the Task object.
"""
# Given that this is a BPMN thing it's questionable whether this belongs here at all
# However, since it calls a BPMN thing on `task`, I guess I'll leave it here
# At least it's not in both places any more
data = {}
if self.last_task and self.last_task.data:
data = self.last_task.data
task = self.get_task_from_id(task_id)
return task.reset_token(data)
def run_next(self, pick_up=True, halt_on_manual=True):
""" """
Runs the next task. Runs the next task.
Returns True if completed, False otherwise. Returns True if completed, False otherwise.
@ -329,7 +303,7 @@ class Workflow(object):
self.last_task = None self.last_task = None
if task is not None: if task is not None:
if not (halt_on_manual and task.task_spec.manual): if not (halt_on_manual and task.task_spec.manual):
if task.complete(): if task.run():
self.last_task = task self.last_task = task
return True return True
blacklist.append(task) blacklist.append(task)
@ -340,7 +314,7 @@ class Workflow(object):
if task._is_descendant_of(blacklisted_task): if task._is_descendant_of(blacklisted_task):
continue continue
if not (halt_on_manual and task.task_spec.manual): if not (halt_on_manual and task.task_spec.manual):
if task.complete(): if task.run():
self.last_task = task self.last_task = task
return True return True
blacklist.append(task) blacklist.append(task)
@ -353,7 +327,7 @@ class Workflow(object):
return True return True
return False return False
def complete_all(self, pick_up=True, halt_on_manual=True): def run_all(self, pick_up=True, halt_on_manual=True):
""" """
Runs all branches until completion. This is a convenience wrapper Runs all branches until completion. This is a convenience wrapper
around :meth:`complete_next`, and the pick_up argument is passed around :meth:`complete_next`, and the pick_up argument is passed
@ -366,7 +340,7 @@ class Workflow(object):
complete any tasks that have manual=True. complete any tasks that have manual=True.
See :meth:`SpiffWorkflow.specs.TaskSpec.__init__` See :meth:`SpiffWorkflow.specs.TaskSpec.__init__`
""" """
while self.complete_next(pick_up, halt_on_manual): while self.run_next(pick_up, halt_on_manual):
pass pass
def get_dump(self): def get_dump(self):

View File

@ -19,7 +19,7 @@ workflow = Workflow(spec)
# Execute until all tasks are done or require manual intervention. # Execute until all tasks are done or require manual intervention.
# For the sake of this tutorial, we ignore the "manual" flag on the # For the sake of this tutorial, we ignore the "manual" flag on the
# tasks. In practice, you probably don't want to do that. # tasks. In practice, you probably don't want to do that.
workflow.complete_all(halt_on_manual=False) workflow.run_all(halt_on_manual=False)
# Alternatively, this is what a UI would do for a manual task. # Alternatively, this is what a UI would do for a manual task.
#workflow.complete_task_from_id(...) #workflow.complete_task_from_id(...)

View File

@ -21,7 +21,10 @@ setup(name='SpiffWorkflow',
author_email='dan@sartography.com', author_email='dan@sartography.com',
license='lGPLv2', license='lGPLv2',
packages=find_packages(exclude=['tests', 'tests.*']), packages=find_packages(exclude=['tests', 'tests.*']),
package_data={'SpiffWorkflow.bpmn.parser.schema': ['*.xsd']}, package_data={
'SpiffWorkflow.bpmn.parser': ['schema/*.xsd'],
'SpiffWorkflow.dmn.parser': ['schema/*.xsd'],
},
install_requires=['configparser', 'lxml', 'celery', install_requires=['configparser', 'lxml', 'celery',
# required for python 3.7 - https://stackoverflow.com/a/73932581 # required for python 3.7 - https://stackoverflow.com/a/73932581
'importlib-metadata<5.0; python_version <= "3.7"'], 'importlib-metadata<5.0; python_version <= "3.7"'],

View File

@ -1,103 +0,0 @@
# -*- coding: utf-8 -*-
from builtins import object
import sys
import unittest
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
from lxml import etree
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
from SpiffWorkflow.task import Task
from SpiffWorkflow.serializer.prettyxml import XmlSerializer
from tests.SpiffWorkflow.util import run_workflow
class WorkflowTestData(object):
def __init__(self, filename, spec, path, data):
self.filename = filename
self.spec = spec
self.path = path
self.data = data
class PatternTest(unittest.TestCase):
maxDiff = None
def setUp(self):
Task.id_pool = 0
Task.thread_id_pool = 0
self.xml_path = ['data/spiff/control-flow',
'data/spiff/data',
'data/spiff/resource',
'data/spiff']
self.workflows = []
for basedir in self.xml_path:
dirname = os.path.join(os.path.dirname(__file__), basedir)
for filename in os.listdir(dirname):
if not filename.endswith(('.xml', '.py')):
continue
if filename.endswith('__.py'):
continue
filename = os.path.join(dirname, filename)
self.load_workflow_spec(filename)
def load_workflow_spec(self, filename):
# Load the .path file.
path_file = os.path.splitext(filename)[0] + '.path'
if os.path.exists(path_file):
with open(path_file) as fp:
expected_path = fp.read()
else:
expected_path = None
# Load the .data file.
data_file = os.path.splitext(filename)[0] + '.data'
if os.path.exists(data_file):
with open(data_file) as fp:
expected_data = fp.read()
else:
expected_data = None
# Test patterns that are defined in XML format.
if filename.endswith('.xml'):
with open(filename) as fp:
xml = etree.parse(fp).getroot()
serializer = XmlSerializer()
wf_spec = WorkflowSpec.deserialize(
serializer, xml, filename=filename)
# Test patterns that are defined in Python.
elif filename.endswith('.py'):
with open(filename) as fp:
code = compile(fp.read(), filename, 'exec')
thedict = {}
result = eval(code, thedict)
wf_spec = thedict['TestWorkflowSpec']()
else:
raise Exception('unsuported specification format', filename)
test_data = WorkflowTestData(
filename, wf_spec, expected_path, expected_data)
self.workflows.append(test_data)
def testWorkflowSpec(self):
for test in self.workflows:
print(test.filename)
run_workflow(self, test.spec, test.path, test.data)
def suite():
return unittest.TestLoader().loadTestsFromTestCase(PatternTest)
if __name__ == '__main__':
if len(sys.argv) == 2:
test = PatternTest('run_pattern')
test.setUp()
test.run_pattern(sys.argv[1])
sys.exit(0)
unittest.TextTestRunner(verbosity=2).run(suite())

View File

@ -2,7 +2,7 @@
import unittest import unittest
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from .BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'matth' __author__ = 'matth'

View File

@ -1,10 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from builtins import range
import unittest
import logging import logging
from SpiffWorkflow.task import TaskState from SpiffWorkflow.task import TaskState
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'matth' __author__ = 'matth'
@ -28,13 +25,9 @@ class BaseParallelTestCase(BpmnWorkflowTestCase):
"Doing step '%s' (with choice='%s')", s, choice) "Doing step '%s' (with choice='%s')", s, choice)
else: else:
logging.info("Doing step '%s'", s) logging.info("Doing step '%s'", s)
# logging.debug(self.workflow.get_dump()) self.do_next_named_step(s, choice=choice, only_one_instance=only_one_instance)
self.do_next_named_step(
s, choice=choice, only_one_instance=only_one_instance)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
if save_restore: if save_restore:
# logging.debug("Before SaveRestore: \n%s" %
# self.workflow.get_dump())
self.save_restore() self.save_restore()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()

View File

@ -34,7 +34,7 @@ class TestUserTask(UserTask):
def do_choice(self, task, choice): def do_choice(self, task, choice):
task.set_data(choice=choice) task.set_data(choice=choice)
task.complete() task.run()
class TestExclusiveGatewayParser(ConditionalGatewayParser): class TestExclusiveGatewayParser(ConditionalGatewayParser):

View File

@ -64,7 +64,7 @@ class BpmnWorkflowTestCase(unittest.TestCase):
def switch_workflow(p): def switch_workflow(p):
for task_id, sp in p.workflow._get_outermost_workflow().subprocesses.items(): for task_id, sp in p.workflow._get_outermost_workflow().subprocesses.items():
if p in sp.get_tasks(workflow=sp): if p in sp.get_tasks(workflow=sp):
return p.workflow.get_task(task_id) return p.workflow.get_task_from_id(task_id)
def is_match(t): def is_match(t):
if not (t.task_spec.name == step_name_path[-1] or t.task_spec.description == step_name_path[-1]): if not (t.task_spec.name == step_name_path[-1] or t.task_spec.description == step_name_path[-1]):
@ -116,7 +116,7 @@ class BpmnWorkflowTestCase(unittest.TestCase):
if set_attribs: if set_attribs:
tasks[0].set_data(**set_attribs) tasks[0].set_data(**set_attribs)
tasks[0].complete() tasks[0].run()
def save_restore(self): def save_restore(self):

View File

@ -6,7 +6,7 @@ from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.exceptions import WorkflowTaskException from SpiffWorkflow.exceptions import WorkflowTaskException
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase from .BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'kellym' __author__ = 'kellym'

View File

@ -82,7 +82,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
workflow.do_engine_steps() workflow.do_engine_steps()
for idx, task in enumerate(workflow.get_ready_user_tasks()): for idx, task in enumerate(workflow.get_ready_user_tasks()):
task.data['task_num'] = idx task.data['task_num'] = idx
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
ready_tasks = workflow.get_ready_user_tasks() ready_tasks = workflow.get_ready_user_tasks()
waiting = workflow.get_tasks_from_spec_name('get_response') waiting = workflow.get_tasks_from_spec_name('get_response')
@ -94,7 +94,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
# Now copy the task_num that was sent into a new variable # Now copy the task_num that was sent into a new variable
for task in ready_tasks: for task in ready_tasks:
task.data.update(init_id=task.data['task_num']) task.data.update(init_id=task.data['task_num'])
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
# If the messages were routed properly, the id should match # If the messages were routed properly, the id should match
for task in workflow.get_tasks_from_spec_name('subprocess_end'): for task in workflow.get_tasks_from_spec_name('subprocess_end'):
@ -108,7 +108,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
workflow.do_engine_steps() workflow.do_engine_steps()
for idx, task in enumerate(workflow.get_ready_user_tasks()): for idx, task in enumerate(workflow.get_ready_user_tasks()):
task.data['task_num'] = idx task.data['task_num'] = idx
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
# Two processes should have been started and two corresponding catch events should be waiting # Two processes should have been started and two corresponding catch events should be waiting
@ -121,12 +121,12 @@ class CollaborationTest(BpmnWorkflowTestCase):
# Now copy the task_num that was sent into a new variable # Now copy the task_num that was sent into a new variable
for task in ready_tasks: for task in ready_tasks:
task.data.update(init_id=task.data['task_num']) task.data.update(init_id=task.data['task_num'])
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
# Complete dummy tasks # Complete dummy tasks
for task in workflow.get_ready_user_tasks(): for task in workflow.get_ready_user_tasks():
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
# Repeat for the other process, using a different mapped name # Repeat for the other process, using a different mapped name
@ -136,7 +136,7 @@ class CollaborationTest(BpmnWorkflowTestCase):
self.assertEqual(len(waiting), 2) self.assertEqual(len(waiting), 2)
for task in ready_tasks: for task in ready_tasks:
task.data.update(subprocess=task.data['task_num']) task.data.update(subprocess=task.data['task_num'])
task.complete() task.run()
workflow.do_engine_steps() workflow.do_engine_steps()
# If the messages were routed properly, the id should match # If the messages were routed properly, the id should match

View File

@ -22,13 +22,13 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Add the data so that we can advance the workflow # Add the data so that we can advance the workflow
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello' } ready_tasks[0].data = { 'obj_1': 'hello' }
ready_tasks[0].complete() ready_tasks[0].run()
# Remove the data before advancing # Remove the data before advancing
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.workflow.data.pop('obj_1') self.workflow.data.pop('obj_1')
with self.assertRaises(WorkflowDataException) as exc: with self.assertRaises(WorkflowDataException) as exc:
ready_tasks[0].complete() ready_tasks[0].run()
self.assertEqual(exc.data_output.name, 'obj_1') self.assertEqual(exc.data_output.name, 'obj_1')
def testMissingDataOutput(self): def testMissingDataOutput(self):
@ -37,7 +37,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
with self.assertRaises(WorkflowDataException) as exc: with self.assertRaises(WorkflowDataException) as exc:
ready_tasks[0].complete() ready_tasks[0].run()
self.assertEqual(exc.data_output.name, 'obj_1') self.assertEqual(exc.data_output.name, 'obj_1')
def actual_test(self, save_restore): def actual_test(self, save_restore):
@ -48,7 +48,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Set up the data # Set up the data
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello' } ready_tasks[0].data = { 'obj_1': 'hello' }
ready_tasks[0].complete() ready_tasks[0].run()
# After task completion, obj_1 should be copied out of the task into the workflow # After task completion, obj_1 should be copied out of the task into the workflow
self.assertNotIn('obj_1', ready_tasks[0].data) self.assertNotIn('obj_1', ready_tasks[0].data)
self.assertIn('obj_1', self.workflow.data) self.assertIn('obj_1', self.workflow.data)
@ -59,14 +59,14 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
# Set a value for obj_1 in the task data again # Set a value for obj_1 in the task data again
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].data = { 'obj_1': 'hello again' } ready_tasks[0].data = { 'obj_1': 'hello again' }
ready_tasks[0].complete() ready_tasks[0].run()
# Check to make sure we use the workflow value instead of the value we set # Check to make sure we use the workflow value instead of the value we set
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(ready_tasks[0].data['obj_1'], 'hello') self.assertEqual(ready_tasks[0].data['obj_1'], 'hello')
# Modify the value in the task # Modify the value in the task
ready_tasks[0].data = { 'obj_1': 'hello again' } ready_tasks[0].data = { 'obj_1': 'hello again' }
ready_tasks[0].complete() ready_tasks[0].run()
# We did not set an output data reference so obj_1 should remain unchanged in the workflow data # We did not set an output data reference so obj_1 should remain unchanged in the workflow data
# and be removed from the task data # and be removed from the task data
self.assertNotIn('obj_1', ready_tasks[0].data) self.assertNotIn('obj_1', ready_tasks[0].data)
@ -77,7 +77,7 @@ class DataObjectReferenceTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(ready_tasks[0].data['obj_1'], 'hello') self.assertEqual(ready_tasks[0].data['obj_1'], 'hello')
ready_tasks[0].data['obj_1'] = 'hello again' ready_tasks[0].data['obj_1'] = 'hello again'
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
sp = self.workflow.get_tasks_from_spec_name('subprocess')[0] sp = self.workflow.get_tasks_from_spec_name('subprocess')[0]
# It was copied out # It was copied out

View File

@ -76,7 +76,7 @@ class CallActivityDataTest(BpmnWorkflowTestCase):
waiting = self.workflow.get_tasks(TaskState.WAITING) waiting = self.workflow.get_tasks(TaskState.WAITING)
while len(waiting) == 0: while len(waiting) == 0:
next_task = self.workflow.get_tasks(TaskState.READY)[0] next_task = self.workflow.get_tasks(TaskState.READY)[0]
next_task.complete() next_task.run()
waiting = self.workflow.get_tasks(TaskState.WAITING) waiting = self.workflow.get_tasks(TaskState.WAITING)
def complete_subprocess(self): def complete_subprocess(self):
@ -84,7 +84,7 @@ class CallActivityDataTest(BpmnWorkflowTestCase):
waiting = self.workflow.get_tasks(TaskState.WAITING) waiting = self.workflow.get_tasks(TaskState.WAITING)
while len(waiting) > 0: while len(waiting) > 0:
next_task = self.workflow.get_tasks(TaskState.READY)[0] next_task = self.workflow.get_tasks(TaskState.READY)[0]
next_task.complete() next_task.run()
waiting = self.workflow.get_tasks(TaskState.WAITING) waiting = self.workflow.get_tasks(TaskState.WAITING)
@ -113,7 +113,7 @@ class IOSpecOnTaskTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks_from_spec_name('any_task')[0] task = self.workflow.get_tasks_from_spec_name('any_task')[0]
task.data.update({'out_1': 1}) task.data.update({'out_1': 1})
with self.assertRaises(WorkflowDataException) as exc: with self.assertRaises(WorkflowDataException) as exc:
task.complete() task.run()
self.assertEqual(exc.exception.data_output.name, 'out_2') self.assertEqual(exc.exception.data_output.name, 'out_2')
def actual_test(self, save_restore=False): def actual_test(self, save_restore=False):
@ -124,6 +124,6 @@ class IOSpecOnTaskTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks_from_spec_name('any_task')[0] task = self.workflow.get_tasks_from_spec_name('any_task')[0]
self.assertDictEqual(task.data, {'in_1': 1, 'in_2': 'hello world'}) self.assertDictEqual(task.data, {'in_1': 1, 'in_2': 'hello world'})
task.data.update({'out_1': 1, 'out_2': 'bye', 'extra': True}) task.data.update({'out_1': 1, 'out_2': 'bye', 'extra': True})
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertDictEqual(self.workflow.last_task.data, {'out_1': 1, 'out_2': 'bye'}) self.assertDictEqual(self.workflow.last_task.data, {'out_1': 1, 'out_2': 'bye'})

View File

@ -36,4 +36,4 @@ class InclusiveGatewayTest(BpmnWorkflowTestCase):
def set_data(self, value): def set_data(self, value):
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
task.data = value task.data = value
task.complete() task.run()

View File

@ -26,7 +26,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(task.task_spec.name, 'any_task [child]') self.assertEqual(task.task_spec.name, 'any_task [child]')
self.assertIn('input_item', task.data) self.assertIn('input_item', task.data)
task.data['output_item'] = task.data['input_item'] * 2 task.data['output_item'] = task.data['input_item'] * 2
task.complete() task.run()
if save_restore: if save_restore:
self.save_restore() self.save_restore()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
@ -47,7 +47,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(len(ready_tasks), 3) self.assertEqual(len(ready_tasks), 3)
task = [t for t in ready_tasks if t.data['input_item'] == 2][0] task = [t for t in ready_tasks if t.data['input_item'] == 2][0]
task.data['output_item'] = task.data['input_item'] * 2 task.data['output_item'] = task.data['input_item'] * 2
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()

View File

@ -20,7 +20,7 @@ class ParallelOrderTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertFalse(self.workflow.is_completed()) self.assertFalse(self.workflow.is_completed())
self.assertEquals(4, len(self.workflow.get_ready_user_tasks())) self.assertEqual(4, len(self.workflow.get_ready_user_tasks()))
tasks = self.workflow.get_ready_user_tasks() tasks = self.workflow.get_ready_user_tasks()
self.assertEquals("Task 1", tasks[0].get_description()) self.assertEquals("Task 1", tasks[0].get_description())
self.assertEquals("Task 2", tasks[1].get_description()) self.assertEquals("Task 2", tasks[1].get_description())

View File

@ -51,7 +51,7 @@ class ParallelThroughSameTaskTest(BpmnWorkflowTestCase):
self.assertEqual(2, len(ready_tasks)) self.assertEqual(2, len(ready_tasks))
self.assertEqual( self.assertEqual(
'Repeated Task', ready_tasks[0].task_spec.description) 'Repeated Task', ready_tasks[0].task_spec.description)
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# The inclusive gateway allows us through here, because there is no route for the other thread # The inclusive gateway allows us through here, because there is no route for the other thread
# that doesn't use the same sequence flow # that doesn't use the same sequence flow
@ -82,7 +82,7 @@ class ParallelThroughSameTaskTest(BpmnWorkflowTestCase):
self.assertEqual(2, len(ready_tasks)) self.assertEqual(2, len(ready_tasks))
self.assertEqual( self.assertEqual(
'Repeated Task', ready_tasks[0].task_spec.description) 'Repeated Task', ready_tasks[0].task_spec.description)
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
# The inclusive gateway allows us through here, because there is no route for the other thread # The inclusive gateway allows us through here, because there is no route for the other thread

View File

@ -35,7 +35,7 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
top_level_task = self.workflow.get_ready_user_tasks()[0] top_level_task = self.workflow.get_ready_user_tasks()[0]
self.workflow.complete_task_from_id(top_level_task.id) self.workflow.run_task_from_id(top_level_task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.save_restore() self.save_restore()
@ -50,11 +50,11 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertEqual(1, len(self.workflow.get_ready_user_tasks())) self.assertEqual(1, len(self.workflow.get_ready_user_tasks()))
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'SubTask2') self.assertEqual(task.get_name(),'SubTask2')
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_tasks_from_spec_name('Task1')[0] task = self.workflow.get_tasks_from_spec_name('Task1')[0]
task.reset_token(self.workflow.last_task.data) task.reset_token(self.workflow.last_task.data)
@ -62,19 +62,19 @@ class ResetSubProcessTest(BpmnWorkflowTestCase):
self.reload_save_restore() self.reload_save_restore()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Task1') self.assertEqual(task.get_name(),'Task1')
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Subtask2') self.assertEqual(task.get_name(),'Subtask2')
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Subtask2A') self.assertEqual(task.get_name(),'Subtask2A')
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(task.get_name(),'Task2') self.assertEqual(task.get_name(),'Task2')
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())

View File

@ -1,28 +0,0 @@
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from SpiffWorkflow.task import TaskState
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
class ResetTimerTest(BpmnWorkflowTestCase):
def test_timer(self):
spec, subprocess = self.load_workflow_spec('reset_timer.bpmn', 'main')
self.workflow = BpmnWorkflow(spec, subprocess)
self.workflow.do_engine_steps()
task_1 = self.workflow.get_tasks_from_spec_name('task_1')[0]
timer = self.workflow.get_tasks_from_spec_name('timer')[0]
original_timer = timer.internal_data.get('event_value')
# This returns us to the task
task_1.data['modify'] = True
task_1.complete()
self.workflow.do_engine_steps()
# The timer should be waiting and the time should have been updated
self.assertEqual(task_1.state, TaskState.READY)
self.assertEqual(timer.state, TaskState.WAITING)
self.assertGreater(timer.internal_data.get('event_value'), original_timer)
task_1.data['modify'] = False
task_1.complete()
self.workflow.do_engine_steps()
self.assertEqual(timer.state, TaskState.CANCELLED)
self.assertTrue(self.workflow.is_completed())

View File

@ -27,7 +27,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(task.task_spec.name, 'any_task [child]') self.assertEqual(task.task_spec.name, 'any_task [child]')
self.assertIn('input_item', task.data) self.assertIn('input_item', task.data)
task.data['output_item'] = task.data['input_item'] * 2 task.data['output_item'] = task.data['input_item'] * 2
task.complete() task.run()
if save_restore: if save_restore:
self.save_restore() self.save_restore()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
@ -54,7 +54,7 @@ class BaseTestCase(BpmnWorkflowTestCase):
self.assertEqual(ready.task_spec.name, 'any_task [child]') self.assertEqual(ready.task_spec.name, 'any_task [child]')
self.assertIn('input_item', ready.data) self.assertIn('input_item', ready.data)
ready.data['output_item'] = ready.data['input_item'] * 2 ready.data['output_item'] = ready.data['input_item'] * 2
ready.complete() ready.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()

View File

@ -21,7 +21,7 @@ class StandardLoopTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1) self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].data[str(idx)] = True ready_tasks[0].data[str(idx)] = True
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())
@ -36,7 +36,7 @@ class StandardLoopTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1) self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].data['done'] = True ready_tasks[0].data['done'] = True
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())

View File

@ -35,7 +35,7 @@ class SwimLaneTest(BpmnWorkflowTestCase):
self.assertEqual(0, len(btasks)) self.assertEqual(0, len(btasks))
task = atasks[0] task = atasks[0]
self.assertEqual('Activity_A1', task.task_spec.name) self.assertEqual('Activity_A1', task.task_spec.name)
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
atasks = self.workflow.get_ready_user_tasks(lane="A") atasks = self.workflow.get_ready_user_tasks(lane="A")
btasks = self.workflow.get_ready_user_tasks(lane="B") btasks = self.workflow.get_ready_user_tasks(lane="B")
@ -44,10 +44,10 @@ class SwimLaneTest(BpmnWorkflowTestCase):
# Complete the gateway and the two tasks in B Lane # Complete the gateway and the two tasks in B Lane
btasks[0].data = {'NeedClarification': False} btasks[0].data = {'NeedClarification': False}
self.workflow.complete_task_from_id(btasks[0].id) self.workflow.run_task_from_id(btasks[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
btasks = self.workflow.get_ready_user_tasks(lane="B") btasks = self.workflow.get_ready_user_tasks(lane="B")
self.workflow.complete_task_from_id(btasks[0].id) self.workflow.run_task_from_id(btasks[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# Assert we are in lane C # Assert we are in lane C
@ -56,7 +56,7 @@ class SwimLaneTest(BpmnWorkflowTestCase):
self.assertEqual(tasks[0].task_spec.lane, "C") self.assertEqual(tasks[0].task_spec.lane, "C")
# Step into the sub-process, assure that is also in lane C # Step into the sub-process, assure that is also in lane C
self.workflow.complete_task_from_id(tasks[0].id) self.workflow.run_task_from_id(tasks[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
tasks = self.workflow.get_ready_user_tasks() tasks = self.workflow.get_ready_user_tasks()
self.assertEqual("SubProcessTask", tasks[0].task_spec.description) self.assertEqual("SubProcessTask", tasks[0].task_spec.description)

View File

@ -1,86 +0,0 @@
# -*- coding: utf-8 -*-
import datetime
import unittest
from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine
from SpiffWorkflow.bpmn.PythonScriptEngineEnvironment import TaskDataEnvironment
from SpiffWorkflow.bpmn.workflow import BpmnWorkflow
from tests.SpiffWorkflow.bpmn.BpmnWorkflowTestCase import BpmnWorkflowTestCase
__author__ = 'sartography'
class CustomScriptEngine(PythonScriptEngine):
"""This is a custom script processor that can be easily injected into Spiff Workflow.
It will execute python code read in from the bpmn. It will also make any scripts in the
scripts directory available for execution. """
def __init__(self):
environment = TaskDataEnvironment({
'timedelta': datetime.timedelta,
})
super().__init__(environment=environment)
class TooManyLoopsTest(BpmnWorkflowTestCase):
"""Looping back around many times would cause the tree of tasks to grow
for each loop, doing this a 100 or 1000 times would cause the system to
run fail in various ways. This assures that is no longer the case."""
def testRunThroughHappy(self):
self.actual_test(save_restore=False)
def testThroughSaveRestore(self):
self.actual_test(save_restore=True)
def actual_test(self,save_restore = False):
spec, subprocesses = self.load_workflow_spec('too_many_loops*.bpmn', 'loops')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
counter = 0
data = {}
while not self.workflow.is_completed():
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
if (self.workflow.last_task.data != data):
data = self.workflow.last_task.data
counter += 1 # There is a 10 millisecond wait task.
if save_restore:
self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
self.assertEqual(20, self.workflow.last_task.data['counter'])
def test_with_sub_process(self):
# Found an issue where looping back would fail when it happens
# right after a sub-process. So assuring this is fixed.
counter = 0
spec, subprocesses = self.load_workflow_spec('too_many_loops_sub_process.bpmn', 'loops_sub')
self.workflow = BpmnWorkflow(spec, subprocesses, script_engine=CustomScriptEngine())
data = {}
while not self.workflow.is_completed():
self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks()
if (self.workflow.last_task.data != data):
data = self.workflow.last_task.data
counter += 1 # There is a 10 millisecond wait task.
# self.save_restore()
self.assertEqual(20, self.workflow.last_task.data['counter'])
# One less, because we don't go back through once the first counter
# hits 20.
self.assertEqual(19, self.workflow.last_task.data['counter2'])
def test_with_two_call_activities(self):
spec, subprocess = self.load_workflow_spec('sub_in_loop*.bpmn', 'main')
self.workflow = BpmnWorkflow(spec, subprocess, script_engine=CustomScriptEngine())
self.workflow.do_engine_steps()
for loop in range(3):
ready = self.workflow.get_ready_user_tasks()
ready[0].data = { 'done': True if loop == 3 else False }
ready[0].complete()
self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps()
self.save_restore()
self.workflow.script_engine = CustomScriptEngine()
def suite():
return unittest.TestLoader().loadTestsFromTestCase(TooManyLoopsTest)
if __name__ == '__main__':
unittest.TextTestRunner(verbosity=2).run(suite())

View File

@ -1,291 +1,303 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:signavio="http://www.signavio.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" exporter="Signavio Process Editor, http://www.signavio.com" exporterVersion="" expressionLanguage="http://www.w3.org/1999/XPath" id="sid-fafc406d-50f2-41f2-b75b-ad652445d52e" targetNamespace="http://www.signavio.com/bpmn20" typeLanguage="http://www.w3.org/2001/XMLSchema" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd"> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:signavio="http://www.signavio.com" id="sid-fafc406d-50f2-41f2-b75b-ad652445d52e" targetNamespace="http://www.signavio.com/bpmn20" exporter="Camunda Modeler" exporterVersion="4.11.1" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd">
<collaboration id="sid-5801c79c-823f-4040-b680-417ef5bcb3a2"> <collaboration id="sid-5801c79c-823f-4040-b680-417ef5bcb3a2">
<participant id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" name="Parallel Looping After Join" processRef="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5"> <participant id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" name="Parallel Looping After Join" processRef="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
</participant> </participant>
</collaboration> </collaboration>
<process id="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5" isClosed="false" isExecutable="false" name="Parallel Looping After Join" processType="None"> <process id="sid-41eb2b6c-08bc-4a61-b38b-5f32052139c5" name="Parallel Looping After Join" processType="None" isClosed="false" isExecutable="false">
<laneSet id="sid-127ca06b-aba8-45aa-bd8a-7be70ec94b2c"> <laneSet id="sid-127ca06b-aba8-45aa-bd8a-7be70ec94b2c">
<lane id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" name="Tester"> <lane id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" name="Tester">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue=""/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="" />
</extensionElements> </extensionElements>
<flowNodeRef>sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE</flowNodeRef> <flowNodeRef>start</flowNodeRef>
<flowNodeRef>sid-349F8C0C-45EA-489C-84DD-1D944F48D778</flowNodeRef> <flowNodeRef>first_split</flowNodeRef>
<flowNodeRef>sid-57463471-693A-42A2-9EC6-6460BEDECA86</flowNodeRef> <flowNodeRef>one</flowNodeRef>
<flowNodeRef>sid-CA089240-802A-4C32-9130-FB1A33DDCCC3</flowNodeRef> <flowNodeRef>two</flowNodeRef>
<flowNodeRef>sid-E976FBC2-266E-420F-8D4D-C8FBC6199090</flowNodeRef> <flowNodeRef>join_of_first</flowNodeRef>
<flowNodeRef>sid-F3A979E3-F586-4807-8223-1FAB5A5647B0</flowNodeRef> <flowNodeRef>retry</flowNodeRef>
<flowNodeRef>sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB</flowNodeRef> <flowNodeRef>end</flowNodeRef>
<flowNodeRef>sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897</flowNodeRef> <flowNodeRef>second_split</flowNodeRef>
<flowNodeRef>sid-ABD788A3-CD57-4280-A22A-260B3AEEE138</flowNodeRef> <flowNodeRef>two_a</flowNodeRef>
<flowNodeRef>sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7</flowNodeRef> <flowNodeRef>two_b</flowNodeRef>
<flowNodeRef>sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58</flowNodeRef> <flowNodeRef>join_of_second_split</flowNodeRef>
<flowNodeRef>sid-1946C635-7886-4687-844F-C644FA6222B8</flowNodeRef> <flowNodeRef>two_done</flowNodeRef>
<flowNodeRef>sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1</flowNodeRef> <flowNodeRef>exclusive</flowNodeRef>
<flowNodeRef>sid-55C018B8-C073-4292-9ED0-79BDE50E7498</flowNodeRef> <flowNodeRef>done</flowNodeRef>
<flowNodeRef>sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9</flowNodeRef> <flowNodeRef>go</flowNodeRef>
</lane> </lane>
</laneSet> </laneSet>
<startEvent id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" name=""> <startEvent id="start" name="">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<outgoing>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</outgoing> <outgoing>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</outgoing>
</startEvent> </startEvent>
<parallelGateway gatewayDirection="Diverging" id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" name="First Split"> <parallelGateway id="first_split" name="First Split" gatewayDirection="Diverging">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</incoming> <incoming>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</incoming>
<outgoing>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</outgoing> <outgoing>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</outgoing>
<outgoing>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</outgoing> <outgoing>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</outgoing>
</parallelGateway> </parallelGateway>
<task completionQuantity="1" id="sid-57463471-693A-42A2-9EC6-6460BEDECA86" isForCompensation="false" name="1" startQuantity="1"> <task id="one" name="1">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</incoming> <incoming>sid-7E15C71B-DE9E-4788-B140-A647C99FDC94</incoming>
<outgoing>sid-607CB05E-8762-41B6-AD43-C3970661A99D</outgoing> <outgoing>join_of_first_split</outgoing>
</task> </task>
<task completionQuantity="1" id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" isForCompensation="false" name="2" startQuantity="1"> <task id="two" name="2">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</incoming> <incoming>sid-B6E22A74-A691-453A-A789-B9F8AF787D7C</incoming>
<outgoing>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</outgoing> <outgoing>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</outgoing>
</task> </task>
<parallelGateway gatewayDirection="Converging" id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" name="Join of First Split"> <parallelGateway id="join_of_first" name="Join of First Split" gatewayDirection="Converging">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-231F8A51-752F-4CB3-8FD1-23D153238344</incoming> <incoming>sid-231F8A51-752F-4CB3-8FD1-23D153238344</incoming>
<incoming>sid-607CB05E-8762-41B6-AD43-C3970661A99D</incoming> <incoming>join_of_first_split</incoming>
<outgoing>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</outgoing> <outgoing>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</outgoing>
</parallelGateway> </parallelGateway>
<task completionQuantity="1" id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" isForCompensation="false" name="Retry?" startQuantity="1"> <task id="retry" name="Retry?">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</incoming> <incoming>sid-0895E09C-077C-4D12-8C11-31F28CBC7740</incoming>
<outgoing>sid-2668AC98-39E4-4B12-9052-930528086CAC</outgoing> <outgoing>sid-2668AC98-39E4-4B12-9052-930528086CAC</outgoing>
</task> </task>
<endEvent id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB" name=""> <endEvent id="end" name="">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</incoming> <incoming>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</incoming>
</endEvent> </endEvent>
<parallelGateway gatewayDirection="Diverging" id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" name="Second Split"> <parallelGateway id="second_split" name="Second Split" gatewayDirection="Diverging">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</incoming> <incoming>sid-CAEAD081-6E73-4C98-8656-C67DA18F5140</incoming>
<outgoing>sid-918C653D-0960-4223-9C28-78114F238BCC</outgoing> <outgoing>sid-918C653D-0960-4223-9C28-78114F238BCC</outgoing>
<outgoing>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</outgoing> <outgoing>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</outgoing>
</parallelGateway> </parallelGateway>
<task completionQuantity="1" id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" isForCompensation="false" name="2A" startQuantity="1"> <task id="two_a" name="2A">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-918C653D-0960-4223-9C28-78114F238BCC</incoming> <incoming>sid-918C653D-0960-4223-9C28-78114F238BCC</incoming>
<outgoing>sid-961AF51C-9935-410E-AAA4-105B19186F5E</outgoing> <outgoing>sid-961AF51C-9935-410E-AAA4-105B19186F5E</outgoing>
</task> </task>
<task completionQuantity="1" id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" isForCompensation="false" name="2B" startQuantity="1"> <task id="two_b" name="2B">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</incoming> <incoming>sid-FD82C2A6-7C54-4890-901E-A7E864F7605C</incoming>
<outgoing>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</outgoing> <outgoing>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</outgoing>
</task> </task>
<parallelGateway gatewayDirection="Converging" id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" name="Join of Second Split"> <parallelGateway id="join_of_second_split" name="Join of Second Split" gatewayDirection="Converging">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-961AF51C-9935-410E-AAA4-105B19186F5E</incoming> <incoming>sid-961AF51C-9935-410E-AAA4-105B19186F5E</incoming>
<incoming>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</incoming> <incoming>sid-47947925-21CD-46FF-8D3F-294B235AA4CF</incoming>
<outgoing>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</outgoing> <outgoing>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</outgoing>
</parallelGateway> </parallelGateway>
<task completionQuantity="1" id="sid-1946C635-7886-4687-844F-C644FA6222B8" isForCompensation="false" name="2 Done" startQuantity="1"> <task id="two_done" name="2 Done">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</incoming> <incoming>sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0</incoming>
<outgoing>sid-231F8A51-752F-4CB3-8FD1-23D153238344</outgoing> <outgoing>sid-231F8A51-752F-4CB3-8FD1-23D153238344</outgoing>
</task> </task>
<exclusiveGateway gatewayDirection="Diverging" id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" name=""> <exclusiveGateway id="exclusive" name="" gatewayDirection="Diverging">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffff" />
</extensionElements> </extensionElements>
<incoming>sid-2668AC98-39E4-4B12-9052-930528086CAC</incoming> <incoming>sid-2668AC98-39E4-4B12-9052-930528086CAC</incoming>
<outgoing>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</outgoing> <outgoing>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</outgoing>
<outgoing>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</outgoing> <outgoing>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</outgoing>
</exclusiveGateway> </exclusiveGateway>
<task completionQuantity="1" id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" isForCompensation="false" name="Done" startQuantity="1"> <task id="done" name="Done">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</incoming> <incoming>sid-08D6385B-C6BB-45FC-A6BD-2369F392868D</incoming>
<outgoing>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</outgoing> <outgoing>sid-F6160C0E-216C-4D72-98D1-CC5549327D55</outgoing>
</task> </task>
<task completionQuantity="1" id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" isForCompensation="false" name="Go" startQuantity="1"> <task id="go" name="Go">
<extensionElements> <extensionElements>
<signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc"/> <signavio:signavioMetaData metaKey="bgcolor" metaValue="#ffffcc" />
</extensionElements> </extensionElements>
<incoming>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</incoming> <incoming>sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459</incoming>
<incoming>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</incoming> <incoming>sid-F3994F51-FE54-4910-A1F4-E5895AA1A612</incoming>
<outgoing>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</outgoing> <outgoing>sid-3E0EBE59-75C8-465C-90CC-197CE808A96E</outgoing>
</task> </task>
<sequenceFlow id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" name="" sourceRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" targetRef="sid-57463471-693A-42A2-9EC6-6460BEDECA86"/> <sequenceFlow id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" name="" sourceRef="first_split" targetRef="one" />
<sequenceFlow id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" name="" sourceRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" targetRef="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3"/> <sequenceFlow id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" name="" sourceRef="first_split" targetRef="two" />
<sequenceFlow id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" name="" sourceRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" targetRef="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0"/> <sequenceFlow id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" name="" sourceRef="join_of_first" targetRef="retry" />
<sequenceFlow id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" name="" sourceRef="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" targetRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897"/> <sequenceFlow id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" name="" sourceRef="two" targetRef="second_split" />
<sequenceFlow id="sid-918C653D-0960-4223-9C28-78114F238BCC" name="" sourceRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" targetRef="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138"/> <sequenceFlow id="sid-918C653D-0960-4223-9C28-78114F238BCC" name="" sourceRef="second_split" targetRef="two_a" />
<sequenceFlow id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" name="" sourceRef="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" targetRef="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7"/> <sequenceFlow id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" name="" sourceRef="second_split" targetRef="two_b" />
<sequenceFlow id="sid-961AF51C-9935-410E-AAA4-105B19186F5E" name="" sourceRef="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" targetRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58"/> <sequenceFlow id="sid-961AF51C-9935-410E-AAA4-105B19186F5E" name="" sourceRef="two_a" targetRef="join_of_second_split" />
<sequenceFlow id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" name="" sourceRef="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" targetRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58"/> <sequenceFlow id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" name="" sourceRef="two_b" targetRef="join_of_second_split" />
<sequenceFlow id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" name="" sourceRef="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" targetRef="sid-1946C635-7886-4687-844F-C644FA6222B8"/> <sequenceFlow id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" name="" sourceRef="join_of_second_split" targetRef="two_done" />
<sequenceFlow id="sid-231F8A51-752F-4CB3-8FD1-23D153238344" name="" sourceRef="sid-1946C635-7886-4687-844F-C644FA6222B8" targetRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090"/> <sequenceFlow id="sid-231F8A51-752F-4CB3-8FD1-23D153238344" name="" sourceRef="two_done" targetRef="join_of_first" />
<sequenceFlow id="sid-607CB05E-8762-41B6-AD43-C3970661A99D" name="" sourceRef="sid-57463471-693A-42A2-9EC6-6460BEDECA86" targetRef="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090"/> <sequenceFlow id="join_of_first_split" name="" sourceRef="one" targetRef="join_of_first" />
<sequenceFlow id="sid-2668AC98-39E4-4B12-9052-930528086CAC" name="" sourceRef="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" targetRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1"/> <sequenceFlow id="sid-2668AC98-39E4-4B12-9052-930528086CAC" name="" sourceRef="retry" targetRef="exclusive" />
<sequenceFlow id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" name="No" sourceRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" targetRef="sid-55C018B8-C073-4292-9ED0-79BDE50E7498"/> <sequenceFlow id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" name="No" sourceRef="exclusive" targetRef="done" />
<sequenceFlow id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" name="" sourceRef="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" targetRef="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB"/> <sequenceFlow id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" name="" sourceRef="done" targetRef="end" />
<sequenceFlow id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" name="Yes" sourceRef="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" targetRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9"/> <sequenceFlow id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" name="Yes" sourceRef="exclusive" targetRef="go" />
<sequenceFlow id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" name="" sourceRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" targetRef="sid-349F8C0C-45EA-489C-84DD-1D944F48D778"/> <sequenceFlow id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" name="" sourceRef="go" targetRef="first_split" />
<sequenceFlow id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" name="" sourceRef="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" targetRef="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9"/> <sequenceFlow id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" name="" sourceRef="start" targetRef="go" />
</process> </process>
<bpmndi:BPMNDiagram id="sid-162a2324-4820-489a-9df8-04591b6b429a"> <bpmndi:BPMNDiagram id="sid-162a2324-4820-489a-9df8-04591b6b429a">
<bpmndi:BPMNPlane bpmnElement="sid-5801c79c-823f-4040-b680-417ef5bcb3a2" id="sid-4c9e28b7-9050-4a64-8a52-634f8f2febc3"> <bpmndi:BPMNPlane id="sid-4c9e28b7-9050-4a64-8a52-634f8f2febc3" bpmnElement="sid-5801c79c-823f-4040-b680-417ef5bcb3a2">
<bpmndi:BPMNShape bpmnElement="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00_gui" isHorizontal="true"> <bpmndi:BPMNShape id="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00_gui" bpmnElement="sid-B2E5AD50-035A-4CE8-B8A3-B175A6767B00" isHorizontal="true">
<omgdc:Bounds height="619.0" width="794.0" x="120.0" y="90.0"/> <omgdc:Bounds x="120" y="90" width="794" height="619" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142_gui" isHorizontal="true"> <bpmndi:BPMNShape id="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142_gui" bpmnElement="sid-72009BCE-46B4-4B4B-AEAE-1E7199522142" isHorizontal="true">
<omgdc:Bounds height="619.0" width="764.0" x="150.0" y="90.0"/> <omgdc:Bounds x="150" y="90" width="764" height="619" />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE" id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE_gui"> <bpmndi:BPMNEdge id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612_gui" bpmnElement="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612">
<omgdc:Bounds height="30.0" width="30.0" x="190.0" y="122.0"/> <omgdi:waypoint x="220" y="137" />
</bpmndi:BPMNShape> <omgdi:waypoint x="394" y="137" />
<bpmndi:BPMNShape bpmnElement="sid-349F8C0C-45EA-489C-84DD-1D944F48D778" id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778_gui"> <omgdi:waypoint x="467" y="142" />
<omgdc:Bounds height="40.0" width="40.0" x="374.0" y="180.0"/> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E_gui" bpmnElement="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E">
<bpmndi:BPMNShape bpmnElement="sid-57463471-693A-42A2-9EC6-6460BEDECA86" id="sid-57463471-693A-42A2-9EC6-6460BEDECA86_gui"> <omgdi:waypoint x="467" y="167" />
<omgdc:Bounds height="80.0" width="100.0" x="510.0" y="214.0"/> <omgdi:waypoint x="414" y="191" />
</bpmndi:BPMNShape> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape bpmnElement="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3" id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3_gui"> <bpmndi:BPMNEdge id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459_gui" bpmnElement="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459">
<omgdc:Bounds height="80.0" width="100.0" x="231.0" y="214.0"/> <omgdi:waypoint x="794" y="405" />
</bpmndi:BPMNShape> <omgdi:waypoint x="794.5" y="145" />
<bpmndi:BPMNShape bpmnElement="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090" id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090_gui"> <omgdi:waypoint x="567" y="145" />
<omgdc:Bounds height="40.0" width="40.0" x="540.0" y="405.0"/> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55_gui" bpmnElement="sid-F6160C0E-216C-4D72-98D1-CC5549327D55">
<bpmndi:BPMNShape bpmnElement="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0" id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0_gui"> <omgdi:waypoint x="794" y="577" />
<omgdc:Bounds height="80.0" width="100.0" x="615.0" y="385.0"/> <omgdi:waypoint x="794" y="630" />
</bpmndi:BPMNShape> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape bpmnElement="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB" id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB_gui"> <bpmndi:BPMNEdge id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D_gui" bpmnElement="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D">
<omgdc:Bounds height="28.0" width="28.0" x="780.0" y="630.0"/> <omgdi:waypoint x="794" y="445" />
</bpmndi:BPMNShape> <omgdi:waypoint x="794" y="497" />
<bpmndi:BPMNShape bpmnElement="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897" id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897_gui"> </bpmndi:BPMNEdge>
<omgdc:Bounds height="40.0" width="40.0" x="261.0" y="338.0"/> <bpmndi:BPMNEdge id="sid-2668AC98-39E4-4B12-9052-930528086CAC_gui" bpmnElement="sid-2668AC98-39E4-4B12-9052-930528086CAC">
</bpmndi:BPMNShape> <omgdi:waypoint x="715" y="425" />
<bpmndi:BPMNShape bpmnElement="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138" id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138_gui"> <omgdi:waypoint x="774" y="425" />
<omgdc:Bounds height="80.0" width="100.0" x="177.0" y="417.0"/> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="sid-607CB05E-8762-41B6-AD43-C3970661A99D_gui" bpmnElement="join_of_first_split">
<bpmndi:BPMNShape bpmnElement="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7" id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7_gui"> <omgdi:waypoint x="560" y="294" />
<omgdc:Bounds height="80.0" width="100.0" x="344.0" y="408.0"/> <omgdi:waypoint x="560" y="405" />
</bpmndi:BPMNShape> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape bpmnElement="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58" id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58_gui"> <bpmndi:BPMNEdge id="sid-231F8A51-752F-4CB3-8FD1-23D153238344_gui" bpmnElement="sid-231F8A51-752F-4CB3-8FD1-23D153238344">
<omgdc:Bounds height="40.0" width="40.0" x="285.0" y="555.0"/> <omgdi:waypoint x="475" y="535" />
</bpmndi:BPMNShape> <omgdi:waypoint x="491" y="425" />
<bpmndi:BPMNShape bpmnElement="sid-1946C635-7886-4687-844F-C644FA6222B8" id="sid-1946C635-7886-4687-844F-C644FA6222B8_gui"> <omgdi:waypoint x="540" y="425" />
<omgdc:Bounds height="80.0" width="100.0" x="420.0" y="535.0"/> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0_gui" bpmnElement="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0">
<bpmndi:BPMNShape bpmnElement="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1" id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1_gui" isMarkerVisible="true"> <omgdi:waypoint x="325" y="575" />
<omgdc:Bounds height="40.0" width="40.0" x="774.0" y="405.0"/> <omgdi:waypoint x="420" y="575" />
</bpmndi:BPMNShape> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape bpmnElement="sid-55C018B8-C073-4292-9ED0-79BDE50E7498" id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498_gui"> <bpmndi:BPMNEdge id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF_gui" bpmnElement="sid-47947925-21CD-46FF-8D3F-294B235AA4CF">
<omgdc:Bounds height="80.0" width="100.0" x="744.0" y="497.0"/> <omgdi:waypoint x="344" y="448" />
</bpmndi:BPMNShape> <omgdi:waypoint x="305" y="448" />
<bpmndi:BPMNShape bpmnElement="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9" id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9_gui"> <omgdi:waypoint x="305" y="555" />
<omgdc:Bounds height="80.0" width="100.0" x="467.0" y="105.0"/> </bpmndi:BPMNEdge>
</bpmndi:BPMNShape> <bpmndi:BPMNEdge id="sid-961AF51C-9935-410E-AAA4-105B19186F5E_gui" bpmnElement="sid-961AF51C-9935-410E-AAA4-105B19186F5E">
<bpmndi:BPMNEdge bpmnElement="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94" id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94_gui"> <omgdi:waypoint x="277" y="457" />
<omgdi:waypoint x="414.0" y="206.0"/> <omgdi:waypoint x="305" y="457" />
<omgdi:waypoint x="510.0" y="238.0"/> <omgdi:waypoint x="305" y="555" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D" id="sid-08D6385B-C6BB-45FC-A6BD-2369F392868D_gui"> <bpmndi:BPMNEdge id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C_gui" bpmnElement="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C">
<omgdi:waypoint x="794.0" y="445.0"/> <omgdi:waypoint x="301" y="358" />
<omgdi:waypoint x="794.0" y="497.0"/> <omgdi:waypoint x="394" y="358.5" />
</bpmndi:BPMNEdge> <omgdi:waypoint x="394" y="408" />
<bpmndi:BPMNEdge bpmnElement="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612" id="sid-F3994F51-FE54-4910-A1F4-E5895AA1A612_gui"> </bpmndi:BPMNEdge>
<omgdi:waypoint x="220.0" y="137.0"/> <bpmndi:BPMNEdge id="sid-918C653D-0960-4223-9C28-78114F238BCC_gui" bpmnElement="sid-918C653D-0960-4223-9C28-78114F238BCC">
<omgdi:waypoint x="394.0" y="137.0"/> <omgdi:waypoint x="261" y="358" />
<omgdi:waypoint x="467.0" y="142.0"/> <omgdi:waypoint x="227" y="358.5" />
</bpmndi:BPMNEdge> <omgdi:waypoint x="227" y="417" />
<bpmndi:BPMNEdge bpmnElement="sid-231F8A51-752F-4CB3-8FD1-23D153238344" id="sid-231F8A51-752F-4CB3-8FD1-23D153238344_gui"> </bpmndi:BPMNEdge>
<omgdi:waypoint x="475.0" y="535.0"/> <bpmndi:BPMNEdge id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140_gui" bpmnElement="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140">
<omgdi:waypoint x="491.0" y="425.0"/> <omgdi:waypoint x="281" y="294" />
<omgdi:waypoint x="540.0" y="425.0"/> <omgdi:waypoint x="281" y="338" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-F6160C0E-216C-4D72-98D1-CC5549327D55" id="sid-F6160C0E-216C-4D72-98D1-CC5549327D55_gui"> <bpmndi:BPMNEdge id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740_gui" bpmnElement="sid-0895E09C-077C-4D12-8C11-31F28CBC7740">
<omgdi:waypoint x="794.0" y="577.0"/> <omgdi:waypoint x="580" y="425" />
<omgdi:waypoint x="794.0" y="630.0"/> <omgdi:waypoint x="615" y="425" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C" id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C_gui"> <bpmndi:BPMNEdge id="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C_gui" bpmnElement="sid-B6E22A74-A691-453A-A789-B9F8AF787D7C">
<omgdi:waypoint x="394.0" y="220.0"/> <omgdi:waypoint x="394" y="220" />
<omgdi:waypoint x="394.5" y="254.0"/> <omgdi:waypoint x="394.5" y="254" />
<omgdi:waypoint x="331.0" y="254.0"/> <omgdi:waypoint x="331" y="254" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-2668AC98-39E4-4B12-9052-930528086CAC" id="sid-2668AC98-39E4-4B12-9052-930528086CAC_gui"> <bpmndi:BPMNEdge id="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94_gui" bpmnElement="sid-7E15C71B-DE9E-4788-B140-A647C99FDC94">
<omgdi:waypoint x="715.0" y="425.0"/> <omgdi:waypoint x="414" y="206" />
<omgdi:waypoint x="774.0" y="425.0"/> <omgdi:waypoint x="510" y="238" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140" id="sid-CAEAD081-6E73-4C98-8656-C67DA18F5140_gui"> <bpmndi:BPMNShape id="sid-B33EE043-AB93-4343-A1D4-7B267E2DAFBE_gui" bpmnElement="start">
<omgdi:waypoint x="281.0" y="294.0"/> <omgdc:Bounds x="190" y="122" width="30" height="30" />
<omgdi:waypoint x="281.0" y="338.0"/> </bpmndi:BPMNShape>
</bpmndi:BPMNEdge> <bpmndi:BPMNShape id="sid-349F8C0C-45EA-489C-84DD-1D944F48D778_gui" bpmnElement="first_split">
<bpmndi:BPMNEdge bpmnElement="sid-918C653D-0960-4223-9C28-78114F238BCC" id="sid-918C653D-0960-4223-9C28-78114F238BCC_gui"> <omgdc:Bounds x="374" y="180" width="40" height="40" />
<omgdi:waypoint x="261.0" y="358.0"/> <bpmndi:BPMNLabel>
<omgdi:waypoint x="227.0" y="358.5"/> <omgdc:Bounds x="371" y="220" width="46" height="14" />
<omgdi:waypoint x="227.0" y="417.0"/> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C" id="sid-FD82C2A6-7C54-4890-901E-A7E864F7605C_gui"> <bpmndi:BPMNShape id="sid-57463471-693A-42A2-9EC6-6460BEDECA86_gui" bpmnElement="one">
<omgdi:waypoint x="301.0" y="358.0"/> <omgdc:Bounds x="510" y="214" width="100" height="80" />
<omgdi:waypoint x="394.0" y="358.5"/> </bpmndi:BPMNShape>
<omgdi:waypoint x="394.0" y="408.0"/> <bpmndi:BPMNShape id="sid-CA089240-802A-4C32-9130-FB1A33DDCCC3_gui" bpmnElement="two">
</bpmndi:BPMNEdge> <omgdc:Bounds x="231" y="214" width="100" height="80" />
<bpmndi:BPMNEdge bpmnElement="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459" id="sid-41205B5D-4DBA-4155-A0EE-7D71CE9AA459_gui"> </bpmndi:BPMNShape>
<omgdi:waypoint x="794.0" y="405.0"/> <bpmndi:BPMNShape id="sid-E976FBC2-266E-420F-8D4D-C8FBC6199090_gui" bpmnElement="join_of_first">
<omgdi:waypoint x="794.5" y="145.0"/> <omgdc:Bounds x="540" y="405" width="40" height="40" />
<omgdi:waypoint x="567.0" y="145.0"/> <bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> <omgdc:Bounds x="519" y="445" width="82" height="14" />
<bpmndi:BPMNEdge bpmnElement="sid-607CB05E-8762-41B6-AD43-C3970661A99D" id="sid-607CB05E-8762-41B6-AD43-C3970661A99D_gui"> </bpmndi:BPMNLabel>
<omgdi:waypoint x="560.0" y="294.0"/> </bpmndi:BPMNShape>
<omgdi:waypoint x="560.0" y="405.0"/> <bpmndi:BPMNShape id="sid-F3A979E3-F586-4807-8223-1FAB5A5647B0_gui" bpmnElement="retry">
</bpmndi:BPMNEdge> <omgdc:Bounds x="615" y="385" width="100" height="80" />
<bpmndi:BPMNEdge bpmnElement="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0" id="sid-AFA38469-CD5C-42A2-9473-2EAEBA61F0C0_gui"> </bpmndi:BPMNShape>
<omgdi:waypoint x="325.0" y="575.0"/> <bpmndi:BPMNShape id="sid-51816945-79BF-47F9-BA3C-E95ABAE3D1DB_gui" bpmnElement="end">
<omgdi:waypoint x="420.0" y="575.0"/> <omgdc:Bounds x="780" y="630" width="28" height="28" />
</bpmndi:BPMNEdge> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-961AF51C-9935-410E-AAA4-105B19186F5E" id="sid-961AF51C-9935-410E-AAA4-105B19186F5E_gui"> <bpmndi:BPMNShape id="sid-4F7F3AA6-4E8E-458D-BBEF-E03FC0646897_gui" bpmnElement="second_split">
<omgdi:waypoint x="277.0" y="457.0"/> <omgdc:Bounds x="261" y="338" width="40" height="40" />
<omgdi:waypoint x="305.0" y="457.0"/> <bpmndi:BPMNLabel>
<omgdi:waypoint x="305.0" y="555.0"/> <omgdc:Bounds x="250" y="378" width="62" height="14" />
</bpmndi:BPMNEdge> </bpmndi:BPMNLabel>
<bpmndi:BPMNEdge bpmnElement="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E" id="sid-3E0EBE59-75C8-465C-90CC-197CE808A96E_gui"> </bpmndi:BPMNShape>
<omgdi:waypoint x="467.0" y="167.0"/> <bpmndi:BPMNShape id="sid-ABD788A3-CD57-4280-A22A-260B3AEEE138_gui" bpmnElement="two_a">
<omgdi:waypoint x="414.0" y="191.0"/> <omgdc:Bounds x="177" y="417" width="100" height="80" />
</bpmndi:BPMNEdge> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="sid-0895E09C-077C-4D12-8C11-31F28CBC7740" id="sid-0895E09C-077C-4D12-8C11-31F28CBC7740_gui"> <bpmndi:BPMNShape id="sid-E7B8898A-0D14-4E98-B3D7-736B94EE3FA7_gui" bpmnElement="two_b">
<omgdi:waypoint x="580.0" y="425.0"/> <omgdc:Bounds x="344" y="408" width="100" height="80" />
<omgdi:waypoint x="615.0" y="425.0"/> </bpmndi:BPMNShape>
</bpmndi:BPMNEdge> <bpmndi:BPMNShape id="sid-A1609BD5-1E4A-47AE-8648-1DD41D1B1D58_gui" bpmnElement="join_of_second_split">
<bpmndi:BPMNEdge bpmnElement="sid-47947925-21CD-46FF-8D3F-294B235AA4CF" id="sid-47947925-21CD-46FF-8D3F-294B235AA4CF_gui"> <omgdc:Bounds x="285" y="555" width="40" height="40" />
<omgdi:waypoint x="344.0" y="448.0"/> <bpmndi:BPMNLabel>
<omgdi:waypoint x="305.0" y="448.0"/> <omgdc:Bounds x="269" y="595" width="73" height="27" />
<omgdi:waypoint x="305.0" y="555.0"/> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNShape>
</bpmndi:BPMNPlane> <bpmndi:BPMNShape id="sid-1946C635-7886-4687-844F-C644FA6222B8_gui" bpmnElement="two_done">
</bpmndi:BPMNDiagram> <omgdc:Bounds x="420" y="535" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-4C3B3C16-91DB-43E3-A816-FFEE572E61E1_gui" bpmnElement="exclusive" isMarkerVisible="true">
<omgdc:Bounds x="774" y="405" width="40" height="40" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-55C018B8-C073-4292-9ED0-79BDE50E7498_gui" bpmnElement="done">
<omgdc:Bounds x="744" y="497" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="sid-E7904BFA-1F17-478E-91C9-C8A5B64190C9_gui" bpmnElement="go">
<omgdc:Bounds x="467" y="105" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions> </definitions>

View File

@ -1,104 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1svhxil" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
<bpmn:process id="main" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0j648np</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:exclusiveGateway id="Gateway_1hq5zma" default="Flow_13cp5nc">
<bpmn:incoming>Flow_0j648np</bpmn:incoming>
<bpmn:incoming>modify</bpmn:incoming>
<bpmn:outgoing>Flow_13cp5nc</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_0j648np" sourceRef="StartEvent_1" targetRef="Gateway_1hq5zma" />
<bpmn:task id="task_1" name="Task 1">
<bpmn:incoming>Flow_13cp5nc</bpmn:incoming>
<bpmn:outgoing>Flow_1r81vou</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_13cp5nc" sourceRef="Gateway_1hq5zma" targetRef="task_1" />
<bpmn:task id="task_2" name="Task 2">
<bpmn:incoming>Flow_0m5s7t9</bpmn:incoming>
<bpmn:outgoing>Flow_0p7c88x</bpmn:outgoing>
</bpmn:task>
<bpmn:endEvent id="Event_07pdq0w">
<bpmn:incoming>Flow_1gm7381</bpmn:incoming>
<bpmn:incoming>Flow_0p7c88x</bpmn:incoming>
</bpmn:endEvent>
<bpmn:boundaryEvent id="timer" attachedToRef="task_1">
<bpmn:outgoing>Flow_0m5s7t9</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0hu2ovu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT60S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<bpmn:sequenceFlow id="Flow_0m5s7t9" sourceRef="timer" targetRef="task_2" />
<bpmn:exclusiveGateway id="Gateway_123uzx5" default="Flow_1gm7381">
<bpmn:incoming>Flow_1r81vou</bpmn:incoming>
<bpmn:outgoing>modify</bpmn:outgoing>
<bpmn:outgoing>Flow_1gm7381</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1r81vou" sourceRef="task_1" targetRef="Gateway_123uzx5" />
<bpmn:sequenceFlow id="modify" name="Modify&#10;" sourceRef="Gateway_123uzx5" targetRef="Gateway_1hq5zma">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">modify</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1gm7381" sourceRef="Gateway_123uzx5" targetRef="Event_07pdq0w" />
<bpmn:sequenceFlow id="Flow_0p7c88x" sourceRef="task_2" targetRef="Event_07pdq0w" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="main">
<bpmndi:BPMNEdge id="Flow_0j648np_di" bpmnElement="Flow_0j648np">
<di:waypoint x="215" y="197" />
<di:waypoint x="265" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_13cp5nc_di" bpmnElement="Flow_13cp5nc">
<di:waypoint x="315" y="197" />
<di:waypoint x="370" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0m5s7t9_di" bpmnElement="Flow_0m5s7t9">
<di:waypoint x="420" y="255" />
<di:waypoint x="420" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1r81vou_di" bpmnElement="Flow_1r81vou">
<di:waypoint x="470" y="197" />
<di:waypoint x="525" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1l30w6o_di" bpmnElement="modify">
<di:waypoint x="550" y="172" />
<di:waypoint x="550" y="100" />
<di:waypoint x="290" y="100" />
<di:waypoint x="290" y="172" />
<bpmndi:BPMNLabel>
<dc:Bounds x="404" y="82" width="33" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gm7381_di" bpmnElement="Flow_1gm7381">
<di:waypoint x="575" y="197" />
<di:waypoint x="632" y="197" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0p7c88x_di" bpmnElement="Flow_0p7c88x">
<di:waypoint x="470" y="340" />
<di:waypoint x="650" y="340" />
<di:waypoint x="650" y="215" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="179" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1hq5zma_di" bpmnElement="Gateway_1hq5zma" isMarkerVisible="true">
<dc:Bounds x="265" y="172" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1f3jg2c_di" bpmnElement="task_1">
<dc:Bounds x="370" y="157" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1r0ra56_di" bpmnElement="task_2">
<dc:Bounds x="370" y="300" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_123uzx5_di" bpmnElement="Gateway_123uzx5" isMarkerVisible="true">
<dc:Bounds x="525" y="172" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_07pdq0w_di" bpmnElement="Event_07pdq0w">
<dc:Bounds x="632" y="179" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1g1bbcs_di" bpmnElement="timer">
<dc:Bounds x="402" y="219" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,600 @@
{
"serializer_version": "1.1",
"data": {},
"last_task": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"success": true,
"tasks": {
"fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332": {
"id": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"parent": null,
"children": [
"e9523ea2-0474-4c36-a7c2-24ff56633ed7"
],
"last_state_change": 1678818080.7799659,
"state": 32,
"task_spec": "Root",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"e9523ea2-0474-4c36-a7c2-24ff56633ed7": {
"id": "e9523ea2-0474-4c36-a7c2-24ff56633ed7",
"parent": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"children": [
"af4a1c32-12a7-46a7-b985-284fef1cb993"
],
"last_state_change": 1678818080.7888825,
"state": 32,
"task_spec": "Start",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"af4a1c32-12a7-46a7-b985-284fef1cb993": {
"id": "af4a1c32-12a7-46a7-b985-284fef1cb993",
"parent": "e9523ea2-0474-4c36-a7c2-24ff56633ed7",
"children": [
"65ef57f0-3fbf-4851-b7c8-a03de9a9062d"
],
"last_state_change": 1678818080.7926495,
"state": 32,
"task_spec": "StartEvent_1",
"triggered": false,
"workflow_name": "loops",
"internal_data": {
"event_fired": true
},
"data": {}
},
"65ef57f0-3fbf-4851-b7c8-a03de9a9062d": {
"id": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"parent": "af4a1c32-12a7-46a7-b985-284fef1cb993",
"children": [
"b5200074-8196-40d3-8f83-204cf132856c"
],
"last_state_change": 1678818080.7970355,
"state": 32,
"task_spec": "initialize",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {
"counter": 0
}
},
"b5200074-8196-40d3-8f83-204cf132856c": {
"id": "b5200074-8196-40d3-8f83-204cf132856c",
"parent": "65ef57f0-3fbf-4851-b7c8-a03de9a9062d",
"children": [
"2ab8ec7f-a9ee-4891-a7a9-20250e2ab816"
],
"last_state_change": 1678818091.420421,
"state": 16,
"task_spec": "TIMER_EVENT",
"triggered": false,
"workflow_name": "loops",
"internal_data": {
"event_value": "2023-03-14T18:21:20.809141+00:00",
"event_fired": true
},
"data": {
"counter": 0
}
},
"2ab8ec7f-a9ee-4891-a7a9-20250e2ab816": {
"id": "2ab8ec7f-a9ee-4891-a7a9-20250e2ab816",
"parent": "b5200074-8196-40d3-8f83-204cf132856c",
"children": [
"259de884-e162-4e0b-8c86-d4827870e2ca"
],
"last_state_change": 1678818080.7815213,
"state": 4,
"task_spec": "increment_counter",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"259de884-e162-4e0b-8c86-d4827870e2ca": {
"id": "259de884-e162-4e0b-8c86-d4827870e2ca",
"parent": "2ab8ec7f-a9ee-4891-a7a9-20250e2ab816",
"children": [
"bd7c4868-142b-487f-9ed3-6a384762dd55"
],
"last_state_change": 1678818080.7818034,
"state": 4,
"task_spec": "Activity_0w5u4k4",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"bd7c4868-142b-487f-9ed3-6a384762dd55": {
"id": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"parent": "259de884-e162-4e0b-8c86-d4827870e2ca",
"children": [
"2fc6691e-f99b-46f2-bebc-7ba729880d13",
"3a14ea66-76b3-4442-8cb3-71a89b9b92fd"
],
"last_state_change": 1678818080.782314,
"state": 4,
"task_spec": "Gateway_over_20",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"2fc6691e-f99b-46f2-bebc-7ba729880d13": {
"id": "2fc6691e-f99b-46f2-bebc-7ba729880d13",
"parent": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"children": [
"655b5f82-14dd-4edd-b88d-94e8cce02e78"
],
"last_state_change": 1678818080.783402,
"state": 1,
"task_spec": "end_event5",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"655b5f82-14dd-4edd-b88d-94e8cce02e78": {
"id": "655b5f82-14dd-4edd-b88d-94e8cce02e78",
"parent": "2fc6691e-f99b-46f2-bebc-7ba729880d13",
"children": [],
"last_state_change": 1678818080.7840528,
"state": 1,
"task_spec": "loops.EndJoin",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
},
"3a14ea66-76b3-4442-8cb3-71a89b9b92fd": {
"id": "3a14ea66-76b3-4442-8cb3-71a89b9b92fd",
"parent": "bd7c4868-142b-487f-9ed3-6a384762dd55",
"children": [],
"last_state_change": 1678818080.7835343,
"state": 1,
"task_spec": "return_to_TIMER_EVENT",
"triggered": false,
"workflow_name": "loops",
"internal_data": {},
"data": {}
}
},
"root": "fcccd5d5-8e9c-4dba-91c3-5b2ed44bb332",
"spec": {
"name": "loops",
"description": "loops",
"file": "/home/essweine/work/sartography/code/SpiffWorkflow/tests/SpiffWorkflow/bpmn/data/too_many_loops.bpmn",
"task_specs": {
"Start": {
"id": "loops_1",
"name": "Start",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [
"StartEvent_1"
],
"typename": "StartTask"
},
"loops.EndJoin": {
"id": "loops_2",
"name": "loops.EndJoin",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"end_event5"
],
"outputs": [
"End"
],
"typename": "_EndJoin"
},
"End": {
"id": "loops_3",
"name": "End",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"loops.EndJoin"
],
"outputs": [],
"typename": "Simple"
},
"StartEvent_1": {
"id": "loops_4",
"name": "StartEvent_1",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Start"
],
"outputs": [
"initialize"
],
"lane": null,
"documentation": null,
"position": {
"x": 152.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "StartEvent",
"extensions": {}
},
"initialize": {
"id": "loops_5",
"name": "initialize",
"description": "initialize",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"StartEvent_1"
],
"outputs": [
"TIMER_EVENT"
],
"lane": null,
"documentation": null,
"position": {
"x": 250.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter = 0",
"typename": "ScriptTask",
"extensions": {}
},
"TIMER_EVENT": {
"id": "loops_6",
"name": "TIMER_EVENT",
"description": "Wait",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20",
"initialize"
],
"outputs": [
"increment_counter"
],
"lane": null,
"documentation": null,
"position": {
"x": 412.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": true,
"external": true,
"name": "Wait",
"expression": "\"PT.01S\"",
"typename": "DurationTimerEventDefinition"
},
"typename": "IntermediateCatchEvent",
"extensions": {}
},
"increment_counter": {
"id": "loops_7",
"name": "increment_counter",
"description": "increment counter",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"TIMER_EVENT"
],
"outputs": [
"Activity_0w5u4k4"
],
"lane": null,
"documentation": null,
"position": {
"x": 480.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter = counter + 1",
"typename": "ScriptTask",
"extensions": {}
},
"Activity_0w5u4k4": {
"id": "loops_8",
"name": "Activity_0w5u4k4",
"description": "call something",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"increment_counter"
],
"outputs": [
"Gateway_over_20"
],
"lane": null,
"documentation": null,
"position": {
"x": 620.0,
"y": 137.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"spec": "loops_ca",
"typename": "CallActivity",
"extensions": {}
},
"Gateway_over_20": {
"id": "loops_9",
"name": "Gateway_over_20",
"description": "is > 20",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Activity_0w5u4k4"
],
"outputs": [
"end_event5",
"TIMER_EVENT"
],
"lane": null,
"documentation": null,
"position": {
"x": 755.0,
"y": 152.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"cond_task_specs": [
{
"condition": "counter >= 20",
"task_spec": "end_event5"
},
{
"condition": "counter < 20",
"task_spec": "TIMER_EVENT"
}
],
"choice": null,
"default_task_spec": null,
"typename": "ExclusiveGateway",
"extensions": {}
},
"end_event5": {
"id": "loops_10",
"name": "end_event5",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20"
],
"outputs": [
"loops.EndJoin"
],
"lane": null,
"documentation": "### Results\nSubmission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.\n\nThe HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.\n\n### Metrics\n\n\nDays elapsed: {{days_delta }}",
"position": {
"x": 932.0,
"y": 159.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "EndEvent",
"extensions": {}
},
"Root": {
"id": "loops_11",
"name": "Root",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [],
"typename": "Simple"
},
"return_to_TIMER_EVENT": {
"id": "loops_12",
"name": "return_to_TIMER_EVENT",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Gateway_over_20",
"initialize"
],
"outputs": [],
"destination_id": "b5200074-8196-40d3-8f83-204cf132856c",
"destination_spec_name": "TIMER_EVENT",
"typename": "LoopResetTask"
}
},
"io_specification": null,
"data_objects": {},
"correlation_keys": {},
"typename": "BpmnProcessSpec"
},
"subprocess_specs": {
"loops_ca": {
"name": "loops_ca",
"description": "loops_ca",
"file": "/home/essweine/work/sartography/code/SpiffWorkflow/tests/SpiffWorkflow/bpmn/data/too_many_loops_call_activity.bpmn",
"task_specs": {
"Start": {
"id": "loops_ca_1",
"name": "Start",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [],
"outputs": [
"StartEvent_1"
],
"typename": "StartTask"
},
"loops_ca.EndJoin": {
"id": "loops_ca_2",
"name": "loops_ca.EndJoin",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"end_event5"
],
"outputs": [
"End"
],
"typename": "_EndJoin"
},
"End": {
"id": "loops_ca_3",
"name": "End",
"description": "",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"loops_ca.EndJoin"
],
"outputs": [],
"typename": "Simple"
},
"StartEvent_1": {
"id": "loops_ca_4",
"name": "StartEvent_1",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"Start"
],
"outputs": [
"increment_counter"
],
"lane": null,
"documentation": null,
"position": {
"x": 152.0,
"y": 109.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "StartEvent",
"extensions": {}
},
"increment_counter": {
"id": "loops_ca_5",
"name": "increment_counter",
"description": "increment counter",
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"StartEvent_1"
],
"outputs": [
"end_event5"
],
"lane": null,
"documentation": null,
"position": {
"x": 220.0,
"y": 87.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"script": "counter2 = 1000",
"typename": "ScriptTask",
"extensions": {}
},
"end_event5": {
"id": "loops_ca_6",
"name": "end_event5",
"description": null,
"manual": false,
"internal": false,
"lookahead": 2,
"inputs": [
"increment_counter"
],
"outputs": [
"loops_ca.EndJoin"
],
"lane": null,
"documentation": "### Results\nSubmission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.\n\nThe HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.\n\n### Metrics\n\n\nDays elapsed: {{days_delta }}",
"position": {
"x": 362.0,
"y": 109.0
},
"data_input_associations": [],
"data_output_associations": [],
"io_specification": null,
"event_definition": {
"internal": false,
"external": false,
"typename": "NoneEventDefinition"
},
"typename": "EndEvent",
"extensions": {}
}
},
"io_specification": null,
"data_objects": {},
"correlation_keys": {},
"typename": "BpmnProcessSpec"
}
},
"subprocesses": {},
"bpmn_messages": [],
"correlations": {}
}

View File

@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0hx6iqe" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
<bpmn:process id="main" name="Main" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0nlj5lh</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:task id="initialize" name="Initialize">
<bpmn:incoming>Flow_0nlj5lh</bpmn:incoming>
<bpmn:incoming>Flow_16vai1a</bpmn:incoming>
<bpmn:outgoing>Flow_1lkecht</bpmn:outgoing>
</bpmn:task>
<bpmn:sequenceFlow id="Flow_1lkecht" sourceRef="initialize" targetRef="subprocess_in_loop" />
<bpmn:callActivity id="subprocess_in_loop" name="Subprocess in Loop" calledElement="subprocess">
<bpmn:incoming>Flow_1lkecht</bpmn:incoming>
<bpmn:outgoing>Flow_1vci114</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_1vci114" sourceRef="subprocess_in_loop" targetRef="call_2" />
<bpmn:exclusiveGateway id="Gateway_158gdvg" default="Flow_16vai1a">
<bpmn:incoming>Flow_0iui938</bpmn:incoming>
<bpmn:outgoing>Flow_0ew7zdi</bpmn:outgoing>
<bpmn:outgoing>Flow_16vai1a</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:endEvent id="Event_0l6q7ei">
<bpmn:incoming>Flow_0ew7zdi</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0ew7zdi" sourceRef="Gateway_158gdvg" targetRef="Event_0l6q7ei">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">done</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0iui938" sourceRef="call_2" targetRef="Gateway_158gdvg" />
<bpmn:callActivity id="call_2" name="Call Subprocess again" calledElement="subprocess">
<bpmn:incoming>Flow_1vci114</bpmn:incoming>
<bpmn:outgoing>Flow_0iui938</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_0nlj5lh" sourceRef="StartEvent_1" targetRef="initialize" />
<bpmn:sequenceFlow id="Flow_16vai1a" sourceRef="Gateway_158gdvg" targetRef="initialize" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="main">
<bpmndi:BPMNEdge id="Flow_1lkecht_di" bpmnElement="Flow_1lkecht">
<di:waypoint x="350" y="117" />
<di:waypoint x="420" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1vci114_di" bpmnElement="Flow_1vci114">
<di:waypoint x="520" y="117" />
<di:waypoint x="580" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0ew7zdi_di" bpmnElement="Flow_0ew7zdi">
<di:waypoint x="785" y="117" />
<di:waypoint x="862" y="117" />
<bpmndi:BPMNLabel>
<dc:Bounds x="1104" y="159" width="60" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0iui938_di" bpmnElement="Flow_0iui938">
<di:waypoint x="680" y="117" />
<di:waypoint x="735" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0nlj5lh_di" bpmnElement="Flow_0nlj5lh">
<di:waypoint x="188" y="117" />
<di:waypoint x="250" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_16vai1a_di" bpmnElement="Flow_16vai1a">
<di:waypoint x="760" y="142" />
<di:waypoint x="760" y="240" />
<di:waypoint x="300" y="240" />
<di:waypoint x="300" y="157" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0721i5u_di" bpmnElement="initialize">
<dc:Bounds x="250" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1lcptwy_di" bpmnElement="call_2">
<dc:Bounds x="580" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1stre5m_di" bpmnElement="subprocess_in_loop">
<dc:Bounds x="420" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_158gdvg_di" bpmnElement="Gateway_158gdvg" isMarkerVisible="true">
<dc:Bounds x="735" y="92" width="50" height="50" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0l6q7ei_di" bpmnElement="Event_0l6q7ei">
<dc:Bounds x="862" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0vwjlip" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.15.0">
<bpmn:process id="subprocess" name="Subprocess" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1dbtwxp</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1dbtwxp" sourceRef="StartEvent_1" targetRef="Activity_1g5e8v7" />
<bpmn:endEvent id="Event_1ukgnws">
<bpmn:incoming>Flow_1t99mly</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1t99mly" sourceRef="Activity_1g5e8v7" targetRef="Event_1ukgnws" />
<bpmn:task id="Activity_1g5e8v7" name="Subprocess Task">
<bpmn:incoming>Flow_1dbtwxp</bpmn:incoming>
<bpmn:outgoing>Flow_1t99mly</bpmn:outgoing>
</bpmn:task>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="subprocess">
<bpmndi:BPMNEdge id="Flow_1dbtwxp_di" bpmnElement="Flow_1dbtwxp">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1t99mly_di" bpmnElement="Flow_1t99mly">
<di:waypoint x="370" y="117" />
<di:waypoint x="432" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ukgnws_di" bpmnElement="Event_1ukgnws">
<dc:Bounds x="432" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0hql87y_di" bpmnElement="Activity_1g5e8v7">
<dc:Bounds x="270" y="77" width="100" height="80" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,125 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
<bpmn:process id="loops" isExecutable="true">
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_1gb8wca</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter = counter + 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="Activity_0w5u4k4" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1tj9oz1</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1gb8wca" sourceRef="TIMER_EVENT" targetRef="increment_counter" />
<bpmn:intermediateCatchEvent id="TIMER_EVENT" name="Wait">
<bpmn:incoming>Flow_15jw6a4</bpmn:incoming>
<bpmn:incoming>Flow_0op1a19</bpmn:incoming>
<bpmn:outgoing>Flow_1gb8wca</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0x6divu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT.01S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:exclusiveGateway id="Gateway_over_20" name="is &#62; 20">
<bpmn:incoming>Flow_0mxlkif</bpmn:incoming>
<bpmn:outgoing>Flow_1tj9oz1</bpmn:outgoing>
<bpmn:outgoing>Flow_0op1a19</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1tj9oz1" name="Yes" sourceRef="Gateway_over_20" targetRef="end_event5">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &gt;= 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0op1a19" name="No" sourceRef="Gateway_over_20" targetRef="TIMER_EVENT">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &lt; 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_15jw6a4" sourceRef="initialize" targetRef="TIMER_EVENT" />
<bpmn:scriptTask id="initialize" name="initialize">
<bpmn:incoming>Flow_0q7fkb7</bpmn:incoming>
<bpmn:outgoing>Flow_15jw6a4</bpmn:outgoing>
<bpmn:script>counter = 0</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_0mxlkif" sourceRef="Activity_0w5u4k4" targetRef="Gateway_over_20" />
<bpmn:callActivity id="Activity_0w5u4k4" name="call something" calledElement="loops_ca">
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
<bpmn:outgoing>Flow_0mxlkif</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0q7fkb7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0q7fkb7" sourceRef="StartEvent_1" targetRef="initialize" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops">
<bpmndi:BPMNEdge id="Flow_0q7fkb7_di" bpmnElement="Flow_0q7fkb7">
<di:waypoint x="188" y="177" />
<di:waypoint x="250" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0mxlkif_di" bpmnElement="Flow_0mxlkif">
<di:waypoint x="720" y="177" />
<di:waypoint x="755" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_15jw6a4_di" bpmnElement="Flow_15jw6a4">
<di:waypoint x="350" y="177" />
<di:waypoint x="412" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0op1a19_di" bpmnElement="Flow_0op1a19">
<di:waypoint x="780" y="152" />
<di:waypoint x="780" y="80" />
<di:waypoint x="430" y="80" />
<di:waypoint x="430" y="159" />
<bpmndi:BPMNLabel>
<dc:Bounds x="598" y="62" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tj9oz1_di" bpmnElement="Flow_1tj9oz1">
<di:waypoint x="805" y="177" />
<di:waypoint x="932" y="177" />
<bpmndi:BPMNLabel>
<dc:Bounds x="818" y="159" width="19" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gb8wca_di" bpmnElement="Flow_1gb8wca">
<di:waypoint x="448" y="177" />
<di:waypoint x="480" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="580" y="177" />
<di:waypoint x="620" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="480" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="932" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0whindt_di" bpmnElement="TIMER_EVENT">
<dc:Bounds x="412" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="419" y="202" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0gr4bqq_di" bpmnElement="Gateway_over_20" isMarkerVisible="true">
<dc:Bounds x="755" y="152" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="768" y="209" width="33" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0whalo9_di" bpmnElement="initialize">
<dc:Bounds x="250" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0mmgsiw_di" bpmnElement="Activity_0w5u4k4">
<dc:Bounds x="620" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.10.0">
<bpmn:process id="loops_ca" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_175n91v</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_175n91v</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter2 = 1000</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="end_event5" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_175n91v" sourceRef="StartEvent_1" targetRef="increment_counter" />
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops_ca">
<bpmndi:BPMNEdge id="Flow_175n91v_di" bpmnElement="Flow_175n91v">
<di:waypoint x="188" y="127" />
<di:waypoint x="220" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="320" y="127" />
<di:waypoint x="362" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="220" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="362" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -1,157 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_d4f3442" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.11.1">
<bpmn:process id="loops_sub" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0q7fkb7</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0q7fkb7" sourceRef="StartEvent_1" targetRef="initialize" />
<bpmn:scriptTask id="increment_counter" name="increment counter">
<bpmn:incoming>Flow_1gb8wca</bpmn:incoming>
<bpmn:outgoing>Flow_1d2usdq</bpmn:outgoing>
<bpmn:script>counter = counter + 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1d2usdq" sourceRef="increment_counter" targetRef="Gateway_over_20" />
<bpmn:endEvent id="end_event5">
<bpmn:documentation>### Results
Submission for Pre-Review was sent to the HSR-IRB on {{ sent_local_date_str }} at {{ sent_local_time_str }}.
The HSR-IRB started the Pre-Review process on {{ end_local_date_str }} at {{ end_local_time_str }} and assigned {{ irb_info.IRB_ADMINISTRATIVE_REVIEWER }} as the reviewer.
### Metrics
Days elapsed: {{days_delta }}</bpmn:documentation>
<bpmn:incoming>Flow_1tj9oz1</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1gb8wca" sourceRef="TIMER_EVENT" targetRef="increment_counter" />
<bpmn:intermediateCatchEvent id="TIMER_EVENT" name="Wait">
<bpmn:incoming>Flow_15jw6a4</bpmn:incoming>
<bpmn:incoming>Flow_1ivr6d7</bpmn:incoming>
<bpmn:outgoing>Flow_1gb8wca</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_0x6divu">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">"PT0.01S"</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:intermediateCatchEvent>
<bpmn:exclusiveGateway id="Gateway_over_20" name="is &#62; 20">
<bpmn:incoming>Flow_1d2usdq</bpmn:incoming>
<bpmn:outgoing>Flow_1tj9oz1</bpmn:outgoing>
<bpmn:outgoing>Flow_0op1a19</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:sequenceFlow id="Flow_1tj9oz1" name="Yes" sourceRef="Gateway_over_20" targetRef="end_event5">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &gt;= 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0op1a19" name="No" sourceRef="Gateway_over_20" targetRef="my_sub_process">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">counter &lt; 20</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_15jw6a4" sourceRef="initialize" targetRef="TIMER_EVENT" />
<bpmn:scriptTask id="initialize" name="initialize">
<bpmn:incoming>Flow_0q7fkb7</bpmn:incoming>
<bpmn:outgoing>Flow_15jw6a4</bpmn:outgoing>
<bpmn:script>counter = 0
counter2 = 0
counter3 = 0</bpmn:script>
</bpmn:scriptTask>
<bpmn:sequenceFlow id="Flow_1ivr6d7" sourceRef="my_sub_process" targetRef="TIMER_EVENT" />
<bpmn:subProcess id="my_sub_process" name="Sub Process">
<bpmn:incoming>Flow_0op1a19</bpmn:incoming>
<bpmn:outgoing>Flow_1ivr6d7</bpmn:outgoing>
<bpmn:startEvent id="Event_0ubr6g5">
<bpmn:outgoing>Flow_1fcanuu</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1fcanuu" sourceRef="Event_0ubr6g5" targetRef="Activity_05x1bpx" />
<bpmn:scriptTask id="Activity_05x1bpx" name="Increment Counter #2">
<bpmn:incoming>Flow_1fcanuu</bpmn:incoming>
<bpmn:outgoing>Flow_04le6u5</bpmn:outgoing>
<bpmn:script>counter2 += 1</bpmn:script>
</bpmn:scriptTask>
<bpmn:endEvent id="Event_0umaw1x">
<bpmn:incoming>Flow_04le6u5</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_04le6u5" sourceRef="Activity_05x1bpx" targetRef="Event_0umaw1x" />
</bpmn:subProcess>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loops_sub">
<bpmndi:BPMNEdge id="Flow_1ivr6d7_di" bpmnElement="Flow_1ivr6d7">
<di:waypoint x="490" y="310" />
<di:waypoint x="430" y="310" />
<di:waypoint x="430" y="145" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_15jw6a4_di" bpmnElement="Flow_15jw6a4">
<di:waypoint x="350" y="127" />
<di:waypoint x="412" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0op1a19_di" bpmnElement="Flow_0op1a19">
<di:waypoint x="920" y="152" />
<di:waypoint x="920" y="310" />
<di:waypoint x="840" y="310" />
<bpmndi:BPMNLabel>
<dc:Bounds x="863" y="292" width="15" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1tj9oz1_di" bpmnElement="Flow_1tj9oz1">
<di:waypoint x="945" y="127" />
<di:waypoint x="1072" y="127" />
<bpmndi:BPMNLabel>
<dc:Bounds x="958" y="109" width="19" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1gb8wca_di" bpmnElement="Flow_1gb8wca">
<di:waypoint x="448" y="127" />
<di:waypoint x="625" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1d2usdq_di" bpmnElement="Flow_1d2usdq">
<di:waypoint x="725" y="127" />
<di:waypoint x="895" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0q7fkb7_di" bpmnElement="Flow_0q7fkb7">
<di:waypoint x="188" y="127" />
<di:waypoint x="250" y="127" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="152" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1fgfg5d_di" bpmnElement="increment_counter">
<dc:Bounds x="625" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1tseamj_di" bpmnElement="end_event5">
<dc:Bounds x="1072" y="109" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0whindt_di" bpmnElement="TIMER_EVENT">
<dc:Bounds x="412" y="109" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="419" y="85" width="22" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0gr4bqq_di" bpmnElement="Gateway_over_20" isMarkerVisible="true">
<dc:Bounds x="895" y="102" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="908" y="159" width="33" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0whalo9_di" bpmnElement="initialize">
<dc:Bounds x="250" y="87" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0v3bik3_di" bpmnElement="my_sub_process" isExpanded="true">
<dc:Bounds x="490" y="210" width="350" height="200" />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_04le6u5_di" bpmnElement="Flow_04le6u5">
<di:waypoint x="720" y="310" />
<di:waypoint x="782" y="310" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1fcanuu_di" bpmnElement="Flow_1fcanuu">
<di:waypoint x="566" y="310" />
<di:waypoint x="620" y="310" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Event_0ubr6g5_di" bpmnElement="Event_0ubr6g5">
<dc:Bounds x="530" y="292" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_08o251p_di" bpmnElement="Activity_05x1bpx">
<dc:Bounds x="620" y="270" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0umaw1x_di" bpmnElement="Event_0umaw1x">
<dc:Bounds x="782" y="292" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -51,7 +51,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
task.set_data(should_escalate=True) task.set_data(should_escalate=True)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
self.workflow.complete_all() self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed()) self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set) self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)
@ -81,7 +81,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
task.set_data(should_escalate=False) task.set_data(should_escalate=False)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
self.workflow.complete_all() self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed()) self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set) self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)
@ -109,7 +109,7 @@ class CallActivityEscalationTest(BpmnWorkflowTestCase):
track_workflow(self.spec, completed_set) track_workflow(self.spec, completed_set)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
self.workflow.complete_all() self.workflow.run_all()
self.assertEqual(True, self.workflow.is_completed()) self.assertEqual(True, self.workflow.is_completed())
self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set) self.assertEqual(True, 'EndEvent_specific1_noninterrupting_normal' in completed_set)

View File

@ -28,7 +28,7 @@ class EventBasedGatewayTest(BpmnWorkflowTestCase):
if save_restore: if save_restore:
self.save_restore() self.save_restore()
self.workflow.script_engine = self.script_engine self.workflow.script_engine = self.script_engine
self.assertEqual(len(waiting_tasks), 1) self.assertEqual(len(waiting_tasks), 2)
self.workflow.catch(MessageEventDefinition('message_1')) self.workflow.catch(MessageEventDefinition('message_1'))
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
@ -41,7 +41,7 @@ class EventBasedGatewayTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
waiting_tasks = self.workflow.get_waiting_tasks() waiting_tasks = self.workflow.get_waiting_tasks()
self.assertEqual(len(waiting_tasks), 1) self.assertEqual(len(waiting_tasks), 2)
timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1] timer_event = waiting_tasks[0].task_spec.event_definition.event_definitions[-1]
self.workflow.catch(timer_event) self.workflow.catch(timer_event)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()

View File

@ -33,7 +33,7 @@ class MultipleEventsTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks(TaskState.READY)[0] task = self.workflow.get_tasks(TaskState.READY)[0]
# Move to User Task 1 # Move to User Task 1
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0] task = self.workflow.get_tasks(TaskState.READY)[0]
self.assertEqual('UserTaskOne', task.get_name()) self.assertEqual('UserTaskOne', task.get_name())
@ -52,10 +52,10 @@ class MultipleEventsTest(BpmnWorkflowTestCase):
task = self.workflow.get_tasks(TaskState.READY)[0] task = self.workflow.get_tasks(TaskState.READY)[0]
# Move to User Task 2 # Move to User Task 2
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0] task = self.workflow.get_tasks(TaskState.READY)[0]
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
task = self.workflow.get_tasks(TaskState.READY)[0] task = self.workflow.get_tasks(TaskState.READY)[0]
self.assertEqual('UserTaskTwo', task.get_name()) self.assertEqual('UserTaskTwo', task.get_name())

View File

@ -42,6 +42,6 @@ class MultipleThrowEventStartsEventTest(BpmnWorkflowTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.assertEqual(len(ready_tasks), 1) self.assertEqual(len(ready_tasks), 1)
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(), True) self.assertEqual(self.workflow.is_completed(), True)

View File

@ -62,7 +62,7 @@ class NITimerDurationTest(BpmnWorkflowTestCase):
task.data['delay_reason'] = 'Just Because' task.data['delay_reason'] = 'Just Because'
elif task.task_spec.name == 'Activity_Work': elif task.task_spec.name == 'Activity_Work':
task.data['work_done'] = 'Yes' task.data['work_done'] = 'Yes'
task.complete() task.run()
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(),True) self.assertEqual(self.workflow.is_completed(),True)

View File

@ -55,11 +55,11 @@ class TimerCycleTest(BpmnWorkflowTestCase):
time.sleep(0.05) time.sleep(0.05)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
events = self.workflow.waiting_events() events = self.workflow.waiting_events()
if loopcount == 0: if loopcount < 2:
# Wait time is 0.1s, so the first time through, there should still be a waiting event # Wait time is 0.1s, two child tasks are created
self.assertEqual(len(events), 1) self.assertEqual(len(events), 1)
else: else:
# By the second iteration, both should be complete # By the third iteration, the event should no longer be waiting
self.assertEqual(len(events), 0) self.assertEqual(len(events), 0)
# Get coffee still ready # Get coffee still ready

View File

@ -39,7 +39,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
# Make sure the task can still be called. # Make sure the task can still be called.
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())

View File

@ -24,7 +24,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
def actual_test(self,save_restore = False): def actual_test(self,save_restore = False):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
loopcount = 0 loopcount = 0
@ -43,7 +43,7 @@ class TimerDurationTest(BpmnWorkflowTestCase):
self.assertEqual(subworkflow.state, TaskState.CANCELLED) self.assertEqual(subworkflow.state, TaskState.CANCELLED)
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
while len(ready_tasks) > 0: while len(ready_tasks) > 0:
ready_tasks[0].complete() ready_tasks[0].run()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())

View File

@ -19,11 +19,11 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'}) ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'quantity': 2}) ready_tasks[0].update_data({'quantity': 2})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertIn('value', self.workflow.last_task.data) self.assertIn('value', self.workflow.last_task.data)
@ -48,7 +48,7 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
# If value == '', we cancel # If value == '', we cancel
ready_tasks[0].update_data({'value': ''}) ready_tasks[0].update_data({'value': ''})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# If the subprocess gets cancelled, verify that data set there does not persist # If the subprocess gets cancelled, verify that data set there does not persist
@ -72,13 +72,13 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'}) ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
# If quantity == 0, we throw an error with no error code # If quantity == 0, we throw an error with no error code
ready_tasks[0].update_data({'quantity': 0}) ready_tasks[0].update_data({'quantity': 0})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# We formerly checked that subprocess data does not persist, but I think it should persist # We formerly checked that subprocess data does not persist, but I think it should persist
@ -103,13 +103,13 @@ class TransactionSubprocessTest(BpmnWorkflowTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
ready_tasks[0].update_data({'value': 'asdf'}) ready_tasks[0].update_data({'value': 'asdf'})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
# If quantity < 0, we throw 'Error 1' # If quantity < 0, we throw 'Error 1'
ready_tasks[0].update_data({'quantity': -1}) ready_tasks[0].update_data({'quantity': -1})
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# The cancel boundary event should be cancelled # The cancel boundary event should be cancelled

View File

@ -142,7 +142,7 @@ class BpmnWorkflowSerializerTest(BaseTestCase):
def test_serialize_workflow_where_script_task_includes_function(self): def test_serialize_workflow_where_script_task_includes_function(self):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_ready_user_tasks() ready_tasks = self.workflow.get_ready_user_tasks()
ready_tasks[0].complete() ready_tasks[0].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
results = self.serializer.serialize_json(self.workflow) results = self.serializer.serialize_json(self.workflow)
assert self.workflow.is_completed() assert self.workflow.is_completed()
@ -161,7 +161,7 @@ class BpmnWorkflowSerializerTest(BaseTestCase):
self.assertEqual(w1.data, w2.data) self.assertEqual(w1.data, w2.data)
self.assertEqual(w1.name, w2.name) self.assertEqual(w1.name, w2.name)
for task in w1.get_ready_user_tasks(): for task in w1.get_ready_user_tasks():
w2_task = w2.get_task(task.id) w2_task = w2.get_task_from_id(task.id)
self.assertIsNotNone(w2_task) self.assertIsNotNone(w2_task)
self.assertEqual(task.data, w2_task.data) self.assertEqual(task.data, w2_task.data)

View File

@ -16,11 +16,12 @@ class Version_1_0_Test(BaseTestCase):
def test_convert_subprocess(self): def test_convert_subprocess(self):
# The serialization used here comes from NestedSubprocessTest saved at line 25 with version 1.0 # 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') fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.0.json')
wf = self.serializer.deserialize_json(open(fn).read()) with open(fn) as fh:
wf = self.serializer.deserialize_json(fh.read())
# We should be able to finish the workflow from this point # We should be able to finish the workflow from this point
ready_tasks = wf.get_tasks(TaskState.READY) ready_tasks = wf.get_tasks(TaskState.READY)
self.assertEqual('Action3', ready_tasks[0].task_spec.description) self.assertEqual('Action3', ready_tasks[0].task_spec.description)
ready_tasks[0].complete() ready_tasks[0].run()
wf.do_engine_steps() wf.do_engine_steps()
self.assertEqual(True, wf.is_completed()) self.assertEqual(True, wf.is_completed())
@ -49,7 +50,7 @@ class Version_1_1_Test(BaseTestCase):
self.assertEqual(len(task.task_spec.cond_task_specs), 2) self.assertEqual(len(task.task_spec.cond_task_specs), 2)
ready_task = wf.get_ready_user_tasks()[0] ready_task = wf.get_ready_user_tasks()[0]
ready_task.data['NeedClarification'] = 'Yes' ready_task.data['NeedClarification'] = 'Yes'
ready_task.complete() ready_task.run()
wf.do_engine_steps() wf.do_engine_steps()
ready_task = wf.get_ready_user_tasks()[0] ready_task = wf.get_ready_user_tasks()[0]
self.assertEqual(ready_task.task_spec.name, 'Activity_A2') self.assertEqual(ready_task.task_spec.name, 'Activity_A2')
@ -58,4 +59,15 @@ class Version_1_1_Test(BaseTestCase):
fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.1-multi.json') fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.1-multi.json')
with self.assertRaises(VersionMigrationError) as ctx: with self.assertRaises(VersionMigrationError) as ctx:
wf = self.serializer.deserialize_json(open(fn).read()) wf = self.serializer.deserialize_json(open(fn).read())
self.assertEqual(ctx.exception.message, "This workflow cannot be migrated because it contains MultiInstance Tasks") self.assertEqual(ctx.exception.message, "This workflow cannot be migrated because it contains MultiInstance Tasks")
def test_remove_loop_reset(self):
fn = os.path.join(self.DATA_DIR, 'serialization', 'v1.1-loop-reset.json')
wf = self.serializer.deserialize_json(open(fn).read())
# Allow 3 seconds max to allow this test to complete (there are 20 loops with a 0.1s timer)
end = time.time() + 3
while not wf.is_completed() and time.time() < end:
wf.do_engine_steps()
wf.refresh_waiting_tasks()
self.assertTrue(wf.is_completed())
self.assertEqual(wf.last_task.data['counter'], 20)

View File

@ -39,7 +39,7 @@ class CallActivityMessageTest(BaseTestCase):
current_task = ready_tasks[0] current_task = ready_tasks[0]
self.assertEqual(current_task.task_spec.name,step[0]) self.assertEqual(current_task.task_spec.name,step[0])
current_task.update_data(step[1]) current_task.update_data(step[1])
current_task.complete() current_task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()
if save_restore: self.save_restore() if save_restore: self.save_restore()

View File

@ -52,7 +52,7 @@ class ClashingNameTest(BaseTestCase):
firsttaskid = task.id firsttaskid = task.id
self.assertEqual(step['taskname'], task.task_spec.name) self.assertEqual(step['taskname'], task.task_spec.name)
task.update_data({step['formvar']: step['answer']}) task.update_data({step['formvar']: step['answer']})
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
if save_restore: self.save_restore() if save_restore: self.save_restore()
@ -68,7 +68,7 @@ class ClashingNameTest(BaseTestCase):
task = self.workflow.get_ready_user_tasks()[0] task = self.workflow.get_ready_user_tasks()[0]
self.assertEqual(step['taskname'], task.task_spec.name) self.assertEqual(step['taskname'], task.task_spec.name)
task.update_data({step['formvar']: step['answer']}) task.update_data({step['formvar']: step['answer']})
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
if save_restore: self.save_restore() if save_restore: self.save_restore()

View File

@ -29,7 +29,7 @@ class DMNCustomScriptTest(BaseTestCase):
def complete_manual_task(self): def complete_manual_task(self):
manual_task = self.workflow.get_tasks_from_spec_name('manual_task')[0] manual_task = self.workflow.get_tasks_from_spec_name('manual_task')[0]
self.workflow.complete_task_from_id(manual_task.id) self.workflow.run_task_from_id(manual_task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
def testDmnHappy(self): def testDmnHappy(self):

View File

@ -16,7 +16,7 @@ class DMNDictTest(BaseTestCase):
self.workflow = BpmnWorkflow(self.spec) self.workflow = BpmnWorkflow(self.spec)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
x = self.workflow.get_ready_user_tasks() x = self.workflow.get_ready_user_tasks()
self.workflow.complete_task_from_id(x[0].id) self.workflow.run_task_from_id(x[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertDictEqual(self.workflow.last_task.data, self.expectedResult) self.assertDictEqual(self.workflow.last_task.data, self.expectedResult)
@ -25,7 +25,7 @@ class DMNDictTest(BaseTestCase):
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
x = self.workflow.get_ready_user_tasks() x = self.workflow.get_ready_user_tasks()
self.workflow.complete_task_from_id(x[0].id) self.workflow.run_task_from_id(x[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()
self.assertDictEqual(self.workflow.last_task.data, self.expectedResult) self.assertDictEqual(self.workflow.last_task.data, self.expectedResult)

View File

@ -41,7 +41,7 @@ class ExternalMessageBoundaryTest(BaseTestCase):
self.assertEqual(True, ready_tasks[1].data['caughtinterrupt']) self.assertEqual(True, ready_tasks[1].data['caughtinterrupt'])
self.assertEqual('Meaningless User Task',ready_tasks[0].task_spec.description) self.assertEqual('Meaningless User Task',ready_tasks[0].task_spec.description)
self.assertEqual(False, ready_tasks[0].data['caughtinterrupt']) self.assertEqual(False, ready_tasks[0].data['caughtinterrupt'])
ready_tasks[1].complete() ready_tasks[1].run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# what I think is going on here is that when we hit the reset, it is updating the # what I think is going on here is that when we hit the reset, it is updating the
# last_task and appending the data to whatever happened there, so it would make sense that # last_task and appending the data to whatever happened there, so it would make sense that
@ -52,7 +52,7 @@ class ExternalMessageBoundaryTest(BaseTestCase):
# The user activity was cancelled and we should continue from the boundary event # The user activity was cancelled and we should continue from the boundary event
self.assertEqual(1, len(ready_tasks),'Expected to have two ready tasks') self.assertEqual(1, len(ready_tasks),'Expected to have two ready tasks')
event = self.workflow.get_tasks_from_spec_name('Event_19detfv')[0] event = self.workflow.get_tasks_from_spec_name('Event_19detfv')[0]
event.complete() event.run()
self.assertEqual('SomethingDrastic', event.data['reset_var']) self.assertEqual('SomethingDrastic', event.data['reset_var'])
self.assertEqual(False, event.data['caughtinterrupt']) self.assertEqual(False, event.data['caughtinterrupt'])

View File

@ -31,7 +31,7 @@ class BusinessRuleTaskParserTest(BaseTestCase):
self.assertTrue(True, "An error was raised..") self.assertTrue(True, "An error was raised..")
self.assertEqual("InvalidDecisionTaskId", we.task_spec.name) self.assertEqual("InvalidDecisionTaskId", we.task_spec.name)
self.maxDiff = 1000 self.maxDiff = 1000
self.assertEquals("Error evaluating expression 'spam= 1'. Rule failed on row 1. Business Rule Task 'Invalid Decision'.", str(we)) self.assertEqual("Error evaluating expression 'spam= 1'. Rule failed on row 1. Business Rule Task 'Invalid Decision'.", str(we))
def suite(): def suite():
return unittest.TestLoader().loadTestsFromTestCase(BusinessRuleTaskParserTest) return unittest.TestLoader().loadTestsFromTestCase(BusinessRuleTaskParserTest)

View File

@ -41,7 +41,7 @@ class MessageBoundaryTest(BaseTestCase):
if task.task_spec.name == step[0]: if task.task_spec.name == step[0]:
task.update_data(step[1]) task.update_data(step[1])
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
time.sleep(.01) time.sleep(.01)
self.workflow.refresh_waiting_tasks() self.workflow.refresh_waiting_tasks()

View File

@ -23,10 +23,10 @@ class MultiInstanceDMNTest(BaseTestCase):
self.save_restore() self.save_restore()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.complete_next() self.workflow.run_next()
self.save_restore() self.save_restore()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.workflow.complete_next() self.workflow.run_next()
self.save_restore() self.save_restore()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.save_restore() self.save_restore()

View File

@ -29,7 +29,7 @@ class NIMessageBoundaryTest(BaseTestCase):
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
self.assertEqual(1, len(ready_tasks)) self.assertEqual(1, len(ready_tasks))
self.workflow.complete_task_from_id(ready_tasks[0].id) self.workflow.run_task_from_id(ready_tasks[0].id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# first we run through a couple of steps where we answer No to each # first we run through a couple of steps where we answer No to each
@ -45,7 +45,7 @@ class NIMessageBoundaryTest(BaseTestCase):
'We got a ready task that we did not expect - %s'%( 'We got a ready task that we did not expect - %s'%(
task.task_spec.name)) task.task_spec.name))
task.data[response[0]] = response[1] task.data[response[0]] = response[1]
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
# if we have a list of tasks - that list becomes invalid # if we have a list of tasks - that list becomes invalid
# after we do a save restore, so I'm completing the list # after we do a save restore, so I'm completing the list
@ -66,7 +66,7 @@ class NIMessageBoundaryTest(BaseTestCase):
'We got a ready task that we did not expect - %s'%( 'We got a ready task that we did not expect - %s'%(
task.task_spec.name)) task.task_spec.name))
task.data[response[0]] = response[1] task.data[response[0]] = response[1]
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
if save_restore: self.save_restore() if save_restore: self.save_restore()
@ -75,14 +75,14 @@ class NIMessageBoundaryTest(BaseTestCase):
task = ready_tasks[0] task = ready_tasks[0]
self.assertEqual(task.task_spec.name,'Activity_DoWork') self.assertEqual(task.task_spec.name,'Activity_DoWork')
task.data['work_done'] = 'Yes' task.data['work_done'] = 'Yes'
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
ready_tasks = self.workflow.get_tasks(TaskState.READY) ready_tasks = self.workflow.get_tasks(TaskState.READY)
self.assertEqual(len(ready_tasks), 1) self.assertEqual(len(ready_tasks), 1)
task = ready_tasks[0] task = ready_tasks[0]
self.assertEqual(task.task_spec.name, 'Activity_WorkCompleted') self.assertEqual(task.task_spec.name, 'Activity_WorkCompleted')
task.data['work_completed'] = 'Lots of Stuff' task.data['work_completed'] = 'Lots of Stuff'
self.workflow.complete_task_from_id(task.id) self.workflow.run_task_from_id(task.id)
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertEqual(self.workflow.is_completed(),True) self.assertEqual(self.workflow.is_completed(),True)
self.assertEqual(self.workflow.last_task.data,{'Event_InterruptBoundary_Response': 'Youre late!', self.assertEqual(self.workflow.last_task.data,{'Event_InterruptBoundary_Response': 'Youre late!',

View File

@ -32,7 +32,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3) self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks: for task in ready_tasks:
task.data['output_item'] = task.data['output_item'] * 2 task.data['output_item'] = task.data['output_item'] * 2
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())
@ -58,7 +58,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3) self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks: for task in ready_tasks:
task.data['output_item'] = task.data['output_item'] * 2 task.data['output_item'] = task.data['output_item'] * 2
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())
@ -84,7 +84,7 @@ class ParseMultiInstanceTest(BaseTestCase):
self.assertEqual(len(ready_tasks), 3) self.assertEqual(len(ready_tasks), 3)
for task in ready_tasks: for task in ready_tasks:
task.data['input_item'] = task.data['input_item'] * 2 task.data['input_item'] = task.data['input_item'] * 2
task.complete() task.run()
self.workflow.do_engine_steps() self.workflow.do_engine_steps()
self.assertTrue(self.workflow.is_completed()) self.assertTrue(self.workflow.is_completed())

Some files were not shown because too many files have changed in this diff Show More