diff --git a/SpiffWorkflow/bpmn/util/diff.py b/SpiffWorkflow/bpmn/util/diff.py index 44f66629..87448022 100644 --- a/SpiffWorkflow/bpmn/util/diff.py +++ b/SpiffWorkflow/bpmn/util/diff.py @@ -1,37 +1,46 @@ +from SpiffWorkflow import TaskState from .task import BpmnTaskFilter class SpecDiff: + """This class is used to hold results for comparisions between two workflow specs. - def __init__(self, serializer, original, new): - """This class is used to hold results for comparisions between two workflow specs. + Attributes: + added (list(`TaskSpec`)): task specs from the new version that cannot be aligned + alignment (dict): a mapping of old task spec to new + comparisons (dict): a mapping of old task spec to changed attributes - Attributes: - registry: a serializer's registry - unmatched (list(`TaskSpec`)): a list of task specs that cannot be aligned - alignment (dict): a mapping of old task spec to new - updated (dict): a mapping of old task spec to changed attributes + Properties: + removed (list of `TaskSpec`: specs from the original that cannot be aligned + changed (dict): a filtered version of comparisons that contains only changed items - The chief basis for alignment is `TaskSpec.name` (ie, the BPMN ID of the spec): if the IDs are identical, - it is assumed the task specs correspond. If a spec in the old version does not have an ID in the new, - some attempt to match based on inputs and outputs is made. - The general procdedure is to attempt to align as many tasks based on ID as possible first, and then - attempt to match by other means while backing out of the traversal. + The chief basis for alignment is `TaskSpec.name` (ie, the BPMN ID of the spec): if the IDs are identical, + it is assumed the task specs correspond. If a spec in the old version does not have an ID in the new, + some attempt to match based on inputs and outputs is made. - Updates are organized primarily by the specs from the original version. - """ + The general procdedure is to attempt to align as many tasks based on ID as possible first, and then + attempt to match by other means while backing out of the traversal. + + Updates are organized primarily by the specs from the original version. + """ + + def __init__(self, registry, original, new): + """Constructor for a spec diff. + + Args: + registry (`DictionaryConverter`): a serislizer registry + original (`BpmnProcessSpec`): the original spec + new (`BpmnProcessSpec`): the spec to compare - self.registry = serializer.registry - self.unmatched = [spec for spec in new.task_specs.values() if spec.name not in original.task_specs] + Aligns specs from the original with specs from the new workflow and checks each aligned pair + for chames. + """ + self.added = [spec for spec in new.task_specs.values() if spec.name not in original.task_specs] self.alignment = {} - self.updated = {} + self.comparisons = {} + self._registry = registry self._align(original.start, new) - @property - def added(self): - """Task specs from the new version that did not exist in the old""" - return self.unmatched - @property def removed(self): """Task specs from the old version that were removed from the new""" @@ -40,14 +49,14 @@ def removed(self): @property def changed(self): """Task specs with updated attributes""" - return dict((ts, changes) for ts, changes in self.updated.items() if changes) + return dict((ts, changes) for ts, changes in self.comparisons.items() if changes) def _align(self, spec, new): candidate = new.task_specs.get(spec.name) self.alignment[spec] = candidate if candidate is not None: - self.updated[spec] = self._compare_task_specs(spec, candidate) + self.comparisons[spec] = self._compare_task_specs(spec, candidate) # Traverse the spec, prioritizing matching by name # Without this starting point, alignment would be way too difficult @@ -62,7 +71,7 @@ def _align(self, spec, new): def _search_unmatched(self, spec): # If any outputs were matched, we can use its unmatched inputs as candidates for match in self._substitutions(spec.outputs): - for parent in [ts for ts in match.inputs if ts in self.unmatched]: + for parent in [ts for ts in match.inputs if ts in self.added]: if self._aligned(spec.outputs, parent.outputs): path = [parent] # We may need to check ancestor inputs as well as this spec's inputs searched = [] # We have to keep track of what we've looked at in case of loops @@ -72,15 +81,17 @@ def _find_ancestor(self, spec, path, searched): if path[-1] not in searched: searched.append(path[-1]) # Stop if we reach a previously matched spec or if an ancestor's inputs match - if path[-1] not in self.unmatched or self._aligned(spec.inputs, path[-1].inputs): + if path[-1] not in self.added or self._aligned(spec.inputs, path[-1].inputs): self.alignment[spec] = path[0] - self.unmatched.remove(path[0]) + if path[0] in self.added: + self.added.remove(path[0]) + self.comparisons[spec] = self._compare_task_specs(spec, path[0]) else: for parent in [ts for ts in path[-1].inputs if ts not in searched]: self._find_ancestor(spec, path + [parent], searched) def _substitutions(self, spec_list, skip_unaligned=True): - subs = [self.alignment[ts] for ts in spec_list] + subs = [self.alignment.get(ts) for ts in spec_list] return [ts for ts in subs if ts is not None] if skip_unaligned else subs def _aligned(self, original, candidates): @@ -89,8 +100,8 @@ def _aligned(self, original, candidates): all(first is not None and first.name == second.name for first, second in zip(subs, candidates)) def _compare_task_specs(self, spec, candidate): - s1 = self.registry.convert(spec) - s2 = self.registry.convert(candidate) + s1 = self._registry.convert(spec) + s2 = self._registry.convert(candidate) if s1.get('typename') != s2.get('typename'): return ['typename'] else: @@ -101,41 +112,109 @@ class WorkflowDiff: to its WorkflowSpec. Attributes - workflow (`BpmnWorkflow`): a workflow instance - spec_diff (`SpecDiff`): the results of a comparision of two specs removed (list(`Task`)): a list of tasks whose specs do not exist in the new version changed (list(`Task`)): a list of tasks with aligned specs where attributes have changed alignment (dict): a mapping of old task spec to new task spec """ def __init__(self, workflow, spec_diff): - self.workflow = workflow - self.spec_diff = spec_diff self.removed = [] self.changed = [] self.alignment = {} - self._align() + self._align(workflow, spec_diff) - def filter_tasks(self, tasks, **kwargs): - """Applies task filtering arguments to a list of tasks. + def _align(self, workflow, spec_diff): + for task in workflow.get_tasks(skip_subprocesses=True): + if task.task_spec in spec_diff.changed: + self.changed.append(task) + if task.task_spec in spec_diff.removed: + self.removed.append(task) + else: + self.alignment[task] = spec_diff.alignment[task.task_spec] - Args: - tasks (list(`Task`)): a list of of tasks - Keyword Args: - any keyword arg that may be passed to `BpmnTaskFilter` +def diff_dependencies(registry, original, new): + """Helper method for comparing sets of spec dependencies. - Returns: - a list containing tasks matching the filter - """ - task_filter = BpmnTaskFilter(**kwargs) - return [t for t in tasks if task_filter.matches(t)] + Args: + registry (`DictionaryConverter`): a serislizer registry + original (dict): the name -> `BpmnProcessSpec` mapping for the original spec + new (dict): the name -> `BpmnProcessSpec` mapping for the updated spec - def _align(self): - for task in self.workflow.get_tasks(skip_subprocesses=True): - if task.task_spec in self.spec_diff.changed: - self.changed.append(task) - if task.task_spec in self.spec_diff.removed: - self.removed.append(task) - else: - self.alignment[task] = self.spec_diff.alignment[task.task_spec] + Returns: + a tuple of: + mapping from name -> `SpecDiff` (or None) for each original dependency + list of names of specs in the new dependencies that did not previously exist + """ + result = {} + subprocesses = {} + for name, spec in original.items(): + if name in new: + result[name] = SpecDiff(registry, spec, new[name]) + else: + result[name] = None + + return result, [name for name in new if name not in original] + + +def diff_workflow(registry, workflow, new_spec, new_dependencies): + """Helper method to handle diffing a workflow and all its dependencies at once. + + Args: + registry (`DictionaryConverter`): a serislizer registry + workflow (`BpmnWorkflow`): a workflow instance + new_spec (`BpmnProcessSpec`): the new top level spec + new_depedencies (dict): a dictionary of name -> `BpmnProcessSpec` + + Returns: + tuple of `WorkflowDiff` and mapping of subworkflow id -> `WorkflowDiff` + + This method checks the top level workflow against the new spec as well as any + existing subprocesses for missing or updated specs. + """ + spec_diff = SpecDiff(registry, workflow.spec, new_spec) + top_diff = WorkflowDiff(workflow, spec_diff) + sp_diffs = {} + for sp_id, sp in workflow.subprocesses.items(): + if sp.spec.name in new_dependencies: + dep_diff = SpecDiff(registry, sp.spec, new_dependencies[sp.spec.name]) + sp_diffs[sp_id] = WorkflowDiff(sp, dep_diff) + else: + sp_diffs[sp_id] = None + return top_diff, sp_diffs + +def filter_tasks(tasks, **kwargs): + """Applies task filtering arguments to a list of tasks. + + Args: + tasks (list(`Task`)): a list of of tasks + + Keyword Args: + any keyword arg that may be passed to `BpmnTaskFilter` + + Returns: + a list containing tasks matching the filter + """ + task_filter = BpmnTaskFilter(**kwargs) + return [t for t in tasks if task_filter.matches(t)] + +def migrate_workflow(diff, workflow, spec, reset_mask=None): + """Update the spec for workflow. + + Args: + diff (`WorkflowDiff`): the diff of this workflow and spec + workflow (`BpmnWorkflow` or `BpmnSubWorkflow`): the workflow + spec (`BpmnProcessSpec`): the new spec + + Keyword Args: + reset_mask (`TaskState`): reset and repredict tasks in this state + + The default rest_mask is TaskState.READY|TaskState.WAITING but can be overridden. + """ + workflow.spec = spec + for task in workflow.get_tasks(): + task.task_spec = diff.alignment.get(task) + + default_mask = TaskState.READY|TaskState.WAITING + for task in list(workflow.get_tasks(state=reset_mask or default_mask)): + task.reset_branch(None) diff --git a/doc/bpmn/diffs.rst b/doc/bpmn/diffs.rst new file mode 100644 index 00000000..12c96160 --- /dev/null +++ b/doc/bpmn/diffs.rst @@ -0,0 +1,217 @@ +Diff Utilities +============== + +.. note:: + + This is a brand new feature so it may change. + +It is possible to generate comparisions between two BPMN specs and also to compare an existing workflow instance +against a spec diff to provide information about whether the spec can be updated for the instance. + +Individual diffs provide information about a single spec or workflow and spec. There are also two helper functiond +for calculating diffs of dependencies for a top level spec or workflow and its subprocesses, and a workflow migration +function. + +Creating a diff requires a serializer :code:`registry` (see :ref:`serializing_custom_objects` for more information +about this). The serializer already needs to know about all the attributes of each task spec; it also knows how to +create dictionary representations of the objects. Therefore, we can serialize an object and just compare the output to +figure which attributes have changed. + +Let's add some of the specs we used earlier in this tutorial: + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs add -p order_product \ + -b bpmn/tutorial/task_types.bpmn \ + -d bpmn/tutorial/product_prices.dmn + + ./runner.py -e spiff_example.spiff.diffs add -p order_product \ + -b bpmn/tutorial/gateway_types.bpmn \ + -d bpmn/tutorial/{product_prices,shipping_costs}.dmn + + ./runner.py -e spiff_example.spiff.diffs add -p order_product \ + -b bpmn/tutorial/{top_level,call_activity}.bpmn \ + -d bpmn/tutorial/{shipping_costs,product_prices}.dmn + + ./runner.py -e spiff_example.spiff.diffs add -p order_product \ + -b bpmn/tutorial/{top_level_script,call_activity_script}.bpmn \ + -d bpmn/tutorial/shipping_costs.dmn + +The IDs of the specs we've added can be obtained with: + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs list_specs + + 09400c6b-5e42-499d-964a-1e9fe9673e51 order_product bpmn/tutorial/top_level.bpmn + 9da66c67-863f-4b88-96f0-76e76febccd0 order_product bpmn/tutorial/gateway_types.bpmn + e0d11baa-c5c8-43bd-bf07-fe4dece39a07 order_product bpmn/tutorial/task_types.bpmn + f679a7ca-298a-4bff-8b2f-6101948715a9 order_product bpmn/tutorial/top_level_script.bpmn + + +Model Diffs +----------- + +First we'll compare :bpmn:`task_types.bpmn` and :bpmn:`gateway_types.bpmn`. The first diagram is very basic, +containing only one of each task type; the second diagram introduces gateways. Therefore the inputs and outputs of +several tasks have changed and a number of new tasks were added. + +.. code-block: console + + ./runner.py -e spiff_example.spiff.diffs diff_spec -o e0d11baa-c5c8-43bd-bf07-fe4dece39a07 -n 9da66c67-863f-4b88-96f0-76e76febccd0 + +Those diagrams don't have dependencies, but :bpmn:`top_level.bpmn` and :bpmn:`top_level_script.bpmn` do have +dependencies (:bpmn:`call_activity.bpmn` and :bpmn:`call_activity_script.bpmn`). See +:ref:`custom_classes_and_functions` for a description of the changes. Adding the :code:`-d` will include +any dependencies in the diff output. + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs diff_spec -d + -o 09400c6b-5e42-499d-964a-1e9fe9673e51 \ + -n f679a7ca-298a-4bff-8b2f-6101948715a9 + +We pass the spec ids into our engine, which deserializes the specs and creates a :code:`SpecDiff` to return (see +:app:`engine/engine.py`. + +.. code-block:: python + + def diff_spec(self, original_id, new_id): + original, _ = self.serializer.get_workflow_spec(original_id, include_dependencies=False) + new, _ = self.serializer.get_workflow_spec(new_id, include_dependencies=False) + return SpecDiff(self.serializer.registry, original, new) + + def diff_dependencies(self, original_id, new_id): + _, original = self.serializer.get_workflow_spec(original_id, include_dependencies=True) + _, new = self.serializer.get_workflow_spec(new_id, include_dependencies=True) + return diff_dependencies(self.serializer.registry, original, new) + +The :code:`SpecDiff` object provides + +- a list of task specs that have been added in the new version +- a mapping of original task spec to a summary of changes in the new version +- an alignment of task spec from the original workflow to the task spec in the new version + +The code for displaying the output of a single spec diff is in :app:`cli/diff_result.py`. I will not go into +detail about how it works here since the bulk of it is just formatting. + +The libary also has a helper function `diff_dependencies`, which takes two dictionaries of subworkflow specs +(the output of :code:`get_subprocess_specs` method of the parser can also be used directly here). This method +returns a mapping of name -> :code:`SpecDiff` for each dependent workflow that could be matched by name and a list +of the names of specs in the new version that did not exist in the old. + +Instance Diffs +-------------- + +Suppose we save one instance of our simplest model without completing any tasks and another instance where we +proceed until our order is displayed before saving. We can list our instances with this command: + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs list_instances + + 4af0e043-6fd6-448d-85eb-d4e86067433e order_product 2024-07-02 17:46:57 2024-07-02 17:47:00 + af180ef6-0437-41fe-b745-8ec4084f3c57 order_product 2024-07-02 17:47:05 2024-07-02 17:47:30 + +If we diff each of these instances against the version in which we've added gateways, we'll see a list of +tasks whose specs have changed and their states. + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs diff_workflow \ + -s 9da66c67-863f-4b88-96f0-76e76febccd0 \ + -w 4af0e043-6fd6-448d-85eb-d4e86067433e + +We'll pass these IDs to our engine, which will return a :code:`WorkflowDiff` of the top level workflow and +a dictionary of subprocess id -> :code:`WorkflowDiff` for any existing subprocesses. + +.. code-block:: python + + def diff_workflow(self, wf_id, spec_id): + wf = self.serializer.get_workflow(wf_id) + spec, deps = self.serializer.get_workflow_spec(spec_id) + return diff_workflow(self.serializer.registry, wf, spec, deps) + +We can retrieve the current spec and its dependencies from the instantiated workflow, so we only need to pass in +the newer version of the spec and its dependencies. + +The :code:`WorkflowDiff` object provides + +- a list of tasks whose specs have been removed from the new spec +- a list of tasks whose specs have been updated in the new spec +- a mapping of task -> new task spec for each task where an alignment exists in the spec diff + +Code for displaying the results is in :app:`cli/diff_result.py`. + +If you start an instance of the first version with a subprocess and stop after customizing a product, and +compare it with the second, you'll see completed tasks from the subprocess in the workflow diff output. + +Migration Example +----------------- + +In some cases, it may be possible to migrate an existing workflow to a new spec. This is actually quite +simple to accomplish: + +.. code-block:: python + + def migrate_workflow(self, wf_id, spec_id, validate=True): + + wf = self.serializer.get_workflow(wf_id) + spec, deps = self.serializer.get_workflow_spec(spec_id) + wf_diff, sp_diffs = diff_workflow(self.serializer.registry, wf, spec, deps) + + if validate and not self.can_migrate(wf_diff, sp_diffs): + raise Exception('Workflow is not safe to migrate!') + + migrate_workflow(wf_diff, wf, spec) + for sp_id, sp in wf.subprocesses.items(): + migrate_workflow(sp_diffs[sp_id], sp, deps.get(sp.spec.name)) + wf.subprocess_specs = deps + + self.serializer.delete_workflow(wf_id) + return self.serializer.create_workflow(wf, spec_id) + +The :code:`migrate_workflow` function updates the task specs of the workflow based on the alignment in the +diff and sets the spec. We have to do this for the top level workflow as well as any subwokflows that have +been created. We also update the dependencies on the top level workflow (subworkflows do not have dependencies). + +This function has an optional :code:`reset_mask` argument that can be used to override the default mask of +:code:`TaskState.READY|TaskState.WAITING`. The children of matching tasks will be dropped and recreated based +on the new spec so that structural changes will be reflected in future tasks. + +In this application we delete the old workflow and reserialize with the new, but that's an application +based decision and it would be possible to save both. + +We can migrate the version that we did not advance with the following command: + +.. code-block:: console + + ./runner.py -e spiff_example.spiff.diffs migrate \ + -s 9da66c67-863f-4b88-96f0-76e76febccd0 \ + -w 4af0e043-6fd6-448d-85eb-d4e86067433e + +Deciding whether to migrate is the hard part. We use a simple algorithm in this application: if any tasks with +specs that have been changed or removed have completed or started running, or any subprocesses have changed, we +assume the workflow cannot be migrated. + +.. code-block:: python + + def can_migrate(self, wf_diff, sp_diffs): + + def safe(result): + mask = TaskState.COMPLETED|TaskState.STARTED + tasks = result.changed + result.removed + return len(filter_tasks(tasks, state=mask)) == 0 + + for diff in sp_diffs.values(): + if diff is None or not safe(diff): + return False + return safe(wf_diff) + +This is fairly restrictive and some workflows might be migrateable even when these conditions apply (for example, +perhaps correcting a typo in completed task shouldn't block future structural changes from being applied). However, +there isn't really a one-size-fits-all decision to be made. And it could end up being a massiveeffort to develop a +UI that allows decisions like this to be made, so I haven't done any of that in this application. + +The hope is that the `SpecDiff` and `WorkflowDiff` objects can provide the necessary information to make these +decisions. \ No newline at end of file diff --git a/doc/bpmn/index.rst b/doc/bpmn/index.rst index 10f25e03..c5294190 100644 --- a/doc/bpmn/index.rst +++ b/doc/bpmn/index.rst @@ -65,6 +65,14 @@ Custom Tasks custom_task_spec +Diffs +----- + +.. toctree:: + :maxdepth: 2 + + diffs + Logging ------- diff --git a/doc/bpmn/script_engine.rst b/doc/bpmn/script_engine.rst index cd1118f4..1d63dba8 100644 --- a/doc/bpmn/script_engine.rst +++ b/doc/bpmn/script_engine.rst @@ -50,6 +50,8 @@ You'll get an error, because imports have been restricted. two script engines (that is the only difference between the two configurations). If you've made any serializer or parser customizations, this is not likely to be possible. +.. _custom_classes_and_functions: + Making Custom Classes and Functions Available ============================================= diff --git a/tests/SpiffWorkflow/bpmn/DiffUtilTest.py b/tests/SpiffWorkflow/bpmn/DiffUtilTest.py index 3091fa4a..d4edb692 100644 --- a/tests/SpiffWorkflow/bpmn/DiffUtilTest.py +++ b/tests/SpiffWorkflow/bpmn/DiffUtilTest.py @@ -1,6 +1,6 @@ from SpiffWorkflow import TaskState from SpiffWorkflow.bpmn import BpmnWorkflow -from SpiffWorkflow.bpmn.util.diff import SpecDiff, WorkflowDiff +from SpiffWorkflow.bpmn.util.diff import SpecDiff, WorkflowDiff, diff_workflow from .BpmnWorkflowTestCase import BpmnWorkflowTestCase @@ -9,7 +9,7 @@ class CompareSpecTest(BpmnWorkflowTestCase): def test_tasks_added(self): v1_spec, v1_sp_specs = self.load_workflow_spec('diff/v1.bpmn', 'Process') v2_spec, v2_sp_specs = self.load_workflow_spec('diff/v2.bpmn', 'Process') - result = SpecDiff(self.serializer, v1_spec, v2_spec) + result = SpecDiff(self.serializer.registry, v1_spec, v2_spec) self.assertEqual(len(result.added), 3) self.assertIn(v2_spec.task_specs.get('Gateway_1618q26'), result.added) self.assertIn(v2_spec.task_specs.get('Activity_1ds7clb'), result.added) @@ -18,7 +18,7 @@ def test_tasks_added(self): def test_tasks_removed(self): v1_spec, v1_sp_specs = self.load_workflow_spec('diff/v1.bpmn', 'Process') v2_spec, v2_sp_specs = self.load_workflow_spec('diff/v2.bpmn', 'Process') - result = SpecDiff(self.serializer, v2_spec, v1_spec) + result = SpecDiff(self.serializer.registry, v2_spec, v1_spec) self.assertEqual(len(result.removed), 3) self.assertIn(v2_spec.task_specs.get('Gateway_1618q26'), result.removed) self.assertIn(v2_spec.task_specs.get('Activity_1ds7clb'), result.removed) @@ -27,7 +27,7 @@ def test_tasks_removed(self): def test_tasks_changed(self): v2_spec, v2_sp_specs = self.load_workflow_spec('diff/v2.bpmn', 'Process') v3_spec, v3_sp_specs = self.load_workflow_spec('diff/v3.bpmn', 'Process') - result = SpecDiff(self.serializer, v2_spec, v3_spec) + result = SpecDiff(self.serializer.registry, v2_spec, v3_spec) # The deafult output was changed and a the conditional output was converted to a subprocess self.assertListEqual( result.changed.get(v2_spec.task_specs.get('Gateway_1618q26')), @@ -42,7 +42,7 @@ def test_tasks_changed(self): def test_alignment(self): v2_spec, v2_sp_specs = self.load_workflow_spec('diff/v2.bpmn', 'Process') v3_spec, v3_sp_specs = self.load_workflow_spec('diff/v3.bpmn', 'Process') - result = SpecDiff(self.serializer, v2_spec, v3_spec) + result = SpecDiff(self.serializer.registry, v2_spec, v3_spec) old_end_event = v2_spec.task_specs.get('Event_0rilo47') new_end_event = v3_spec.task_specs.get('Event_18osyv3') self.assertEqual(result.alignment[old_end_event], new_end_event) @@ -53,7 +53,7 @@ def test_alignment(self): def test_multiple(self): v4_spec, v4_sp_specs = self.load_workflow_spec('diff/v4.bpmn', 'Process') v5_spec, v5_sp_specs = self.load_workflow_spec('diff/v5.bpmn', 'Process') - result = SpecDiff(self.serializer, v4_spec, v5_spec) + result = SpecDiff(self.serializer.registry, v4_spec, v5_spec) self.assertEqual(len(result.removed), 4) self.assertEqual(len(result.changed), 4) self.assertIn(v4_spec.task_specs.get('Gateway_0z1qhgl'), result.removed) @@ -78,9 +78,9 @@ class CompareWorkflowTest(BpmnWorkflowTestCase): def test_changed(self): v3_spec, v3_sp_specs = self.load_workflow_spec('diff/v3.bpmn', 'Process') v4_spec, v4_sp_specs = self.load_workflow_spec('diff/v4.bpmn', 'Process') - spec_diff = SpecDiff(self.serializer, v3_spec, v4_spec) + spec_diff = SpecDiff(self.serializer.registry, v3_spec, v4_spec) sp_spec_diff = SpecDiff( - self.serializer, + self.serializer.registry, v3_sp_specs['Activity_1ds7clb'], v4_sp_specs['Activity_1ds7clb'] ) @@ -102,9 +102,9 @@ def test_changed(self): def test_removed(self): v4_spec, v4_sp_specs = self.load_workflow_spec('diff/v4.bpmn', 'Process') v5_spec, v5_sp_specs = self.load_workflow_spec('diff/v5.bpmn', 'Process') - spec_diff = SpecDiff(self.serializer, v4_spec, v5_spec) + spec_diff = SpecDiff(self.serializer.registry, v4_spec, v5_spec) sp_spec_diff = SpecDiff( - self.serializer, + self.serializer.registry, v4_sp_specs['Activity_1ds7clb'], v5_sp_specs['Activity_1ds7clb'] ) @@ -123,3 +123,14 @@ def test_removed(self): self.assertIn(workflow.get_next_task(spec_name='Activity_11gnihu'), wf_diff.removed) self.assertIn(workflow.get_next_task(spec_name='Gateway_1acqedb'), wf_diff.removed) + def test_subprocess_changed(self): + v3_spec, v3_sp_specs = self.load_workflow_spec('diff/v3.bpmn', 'Process') + v4_spec, v4_sp_specs = self.load_workflow_spec('diff/v4.bpmn', 'Process') + workflow = BpmnWorkflow(v3_spec, v3_sp_specs) + task = workflow.get_next_task(state=TaskState.READY, manual=False) + while task is not None: + task.run() + task = workflow.get_next_task(state=TaskState.READY, manual=False) + result, sp_result = diff_workflow(self.serializer.registry, workflow, v4_spec, v4_sp_specs) + sp_task = workflow.get_next_task(spec_name='Activity_1ds7clb') + self.assertIn(sp_task.id, sp_result)