From 0235e22f39d52c2bfaccec97f8a733f7a5c5b1c4 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:24:22 -0400 Subject: [PATCH 01/16] added some framework stuff to run process model unit tests w/ burnettk --- .../services/file_system_service.py | 38 ++-- .../services/process_model_service.py | 31 +-- .../process_model_test_runner_service.py | 180 ++++++++++++++++++ .../services/spec_file_service.py | 22 +-- .../basic_script_task/basic_script_task.bpmn | 39 ++++ .../basic_script_task/process_model.json | 11 ++ .../test_basic_script_task.json | 5 + .../test_process_model_test_runner_service.py | 42 ++++ 8 files changed, 313 insertions(+), 55 deletions(-) create mode 100644 spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py index 5cad69ad3..b4b85a744 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py @@ -49,13 +49,12 @@ class FileSystemService: """Id_string_to_relative_path.""" return id_string.replace("/", os.sep) - @staticmethod - def process_group_path(name: str) -> str: - """Category_path.""" + @classmethod + def full_path_from_id(cls, id: str) -> str: return os.path.abspath( os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(name), + cls.root_path(), + cls.id_string_to_relative_path(id), ) ) @@ -65,36 +64,33 @@ class FileSystemService: return os.path.join(FileSystemService.root_path(), relative_path) @staticmethod - def process_model_relative_path(spec: ProcessModelInfo) -> str: + def process_model_relative_path(process_model: ProcessModelInfo) -> str: """Get the file path to a process model relative to SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR. If the full path is /path/to/process-group-a/group-b/process-model-a, it will return: process-group-a/group-b/process-model-a """ - workflow_path = FileSystemService.workflow_path(spec) + workflow_path = FileSystemService.process_model_full_path(process_model) return os.path.relpath(workflow_path, start=FileSystemService.root_path()) @staticmethod - def process_group_path_for_spec(spec: ProcessModelInfo) -> str: - """Category_path_for_spec.""" + def process_group_path_for_spec(process_model: ProcessModelInfo) -> str: # os.path.split apparently returns 2 element tulple like: (first/path, last_item) - process_group_id, _ = os.path.split(spec.id_for_file_path()) - return FileSystemService.process_group_path(process_group_id) + process_group_id, _ = os.path.split(process_model.id_for_file_path()) + return FileSystemService.full_path_from_id(process_group_id) + + @classmethod + def process_model_full_path(cls, process_model: ProcessModelInfo) -> str: + return cls.full_path_from_id(process_model.id) @staticmethod - def workflow_path(spec: ProcessModelInfo) -> str: - """Workflow_path.""" - process_model_path = os.path.join(FileSystemService.root_path(), spec.id_for_file_path()) - return process_model_path - - @staticmethod - def full_path_to_process_model_file(spec: ProcessModelInfo) -> str: + def full_path_to_process_model_file(process_model: ProcessModelInfo) -> str: """Full_path_to_process_model_file.""" - return os.path.join(FileSystemService.workflow_path(spec), spec.primary_file_name) # type: ignore + return os.path.join(FileSystemService.process_model_full_path(process_model), process_model.primary_file_name) # type: ignore - def next_display_order(self, spec: ProcessModelInfo) -> int: + def next_display_order(self, process_model: ProcessModelInfo) -> int: """Next_display_order.""" - path = self.process_group_path_for_spec(spec) + path = self.process_group_path_for_spec(process_model) if os.path.exists(path): return len(next(os.walk(path))[1]) else: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index 1cfc33398..fbf6587b2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -60,12 +60,7 @@ class ProcessModelService(FileSystemService): def is_process_group_identifier(cls, process_group_identifier: str) -> bool: """Is_process_group_identifier.""" if os.path.exists(FileSystemService.root_path()): - process_group_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_group_identifier), - ) - ) + process_group_path = FileSystemService.full_path_from_id(process_group_identifier) return cls.is_process_group(process_group_path) return False @@ -82,12 +77,7 @@ class ProcessModelService(FileSystemService): def is_process_model_identifier(cls, process_model_identifier: str) -> bool: """Is_process_model_identifier.""" if os.path.exists(FileSystemService.root_path()): - process_model_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_model_identifier), - ) - ) + process_model_path = FileSystemService.full_path_from_id(process_model_identifier) return cls.is_process_model(process_model_path) return False @@ -149,13 +139,13 @@ class ProcessModelService(FileSystemService): f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it." ) process_model = self.get_process_model(process_model_id) - path = self.workflow_path(process_model) + path = self.process_model_full_path(process_model) shutil.rmtree(path) def process_model_move(self, original_process_model_id: str, new_location: str) -> ProcessModelInfo: """Process_model_move.""" process_model = self.get_process_model(original_process_model_id) - original_model_path = self.workflow_path(process_model) + original_model_path = self.process_model_full_path(process_model) _, model_id = os.path.split(original_model_path) new_relative_path = os.path.join(new_location, model_id) new_model_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_relative_path)) @@ -314,12 +304,7 @@ class ProcessModelService(FileSystemService): def get_process_group(cls, process_group_id: str, find_direct_nested_items: bool = True) -> ProcessGroup: """Look for a given process_group, and return it.""" if os.path.exists(FileSystemService.root_path()): - process_group_path = os.path.abspath( - os.path.join( - FileSystemService.root_path(), - FileSystemService.id_string_to_relative_path(process_group_id), - ) - ) + process_group_path = FileSystemService.full_path_from_id(process_group_id) if cls.is_process_group(process_group_path): return cls.find_or_create_process_group( process_group_path, @@ -336,7 +321,7 @@ class ProcessModelService(FileSystemService): @classmethod def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: """Update_process_group.""" - cat_path = cls.process_group_path(process_group.id) + cat_path = cls.full_path_from_id(process_group.id) os.makedirs(cat_path, exist_ok=True) json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE) serialized_process_group = process_group.serialized @@ -348,7 +333,7 @@ class ProcessModelService(FileSystemService): def process_group_move(self, original_process_group_id: str, new_location: str) -> ProcessGroup: """Process_group_move.""" - original_group_path = self.process_group_path(original_process_group_id) + original_group_path = self.full_path_from_id(original_process_group_id) _, original_group_id = os.path.split(original_group_path) new_root = os.path.join(FileSystemService.root_path(), new_location) new_group_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_root, original_group_id)) @@ -370,7 +355,7 @@ class ProcessModelService(FileSystemService): def process_group_delete(self, process_group_id: str) -> None: """Delete_process_group.""" problem_models = [] - path = self.process_group_path(process_group_id) + path = self.full_path_from_id(process_group_id) if os.path.exists(path): nested_models = self.__get_all_nested_models(path) for process_model in nested_models: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py new file mode 100644 index 000000000..a6ae27fda --- /dev/null +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -0,0 +1,180 @@ +from typing import List, Optional +from dataclasses import dataclass +import json +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from SpiffWorkflow.task import TaskState +from lxml import etree # type: ignore +from spiffworkflow_backend.services.spec_file_service import SpecFileService +from spiffworkflow_backend.services.custom_parser import MyCustomParser +from typing import Callable +import re +import glob +from spiffworkflow_backend.models.process_model import ProcessModelInfo +import os +from spiffworkflow_backend.services.file_system_service import FileSystemService +from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore + + +# workflow json for test case +# 1. default action is load xml from disk and use spiff like normal and get back workflow json +# 2. do stuff from disk cache + +# find all process models +# find all json test cases for each +# for each test case, fire up something like spiff +# for each task, if there is something special in the test case definition, do it (provide data for user task, mock service task, etc) +# when the thing is complete, check workflow data against expected data + + +class UnrunnableTestCaseError(Exception): + pass + + +@dataclass +class TestCaseResult: + passed: bool + test_case_name: str + error: Optional[str] = None + + +# input: +# json_file: +# { +# [TEST_CASE_NAME]: { +# "tasks": { +# [BPMN_TASK_IDENTIIFER]: [DATA] +# }, +# "expected_output_json": [DATA] +# } +# } +class ProcessModelTestRunner: + """Generic test runner code. May move into own library at some point. + + KEEP THIS GENERIC. do not add backend specific code here. + """ + def __init__( + self, + process_model_directory_path: str, + instantiate_executer_callback: Callable[[str], any], + execute_task_callback: Callable[[any, Optional[dict]], any], + get_next_task_callback: Callable[[any], any], + ) -> None: + self.process_model_directory_path = process_model_directory_path + self.test_mappings = self._discover_process_model_directories() + self.instantiate_executer_callback = instantiate_executer_callback + self.execute_task_callback = execute_task_callback + self.get_next_task_callback = get_next_task_callback + + self.test_case_results = [] + + def all_test_cases_passed(self) -> bool: + failed_tests = [t for t in self.test_case_results if t.passed is False] + return len(failed_tests) < 1 + + def run(self) -> None: + for json_test_case_file, bpmn_file in self.test_mappings.items(): + with open(json_test_case_file, 'rt') as f: + json_file_contents = json.loads(f.read()) + + for test_case_name, test_case_contents in json_file_contents.items(): + try: + self.run_test_case(bpmn_file, test_case_name, test_case_contents) + except Exception as ex: + self.test_case_results.append(TestCaseResult( + passed=False, + test_case_name=test_case_name, + error=f"Syntax error: {str(ex)}", + )) + + def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: + bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) + next_task = self.get_next_task_callback(bpmn_process_instance) + while next_task is not None: + test_case_json = None + if 'tasks' in test_case_contents: + if next_task.task_spec.bpmn_id in test_case_contents['tasks']: + test_case_json = test_case_contents['tasks'][next_task.task_spec.bpmn_id] + + task_type = next_task.task_spec.__class__.__name__ + if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: + raise UnrunnableTestCaseError( + f"Cannot run test case '{test_case_name}'. It requires task data for {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" + ) + self.execute_task_callback(next_task, test_case_json) + next_task = self.get_next_task_callback(bpmn_process_instance) + test_passed = test_case_contents['expected_output_json'] == bpmn_process_instance.data + self.test_case_results.append(TestCaseResult( + passed=test_passed, + test_case_name=test_case_name, + )) + + def _discover_process_model_directories( + self, + ) -> dict[str, str]: + test_mappings = {} + + json_test_file_glob = os.path.join(self.process_model_directory_path, "**", "test_*.json") + + for file in glob.glob(json_test_file_glob, recursive=True): + file_dir = os.path.dirname(file) + json_file_name = os.path.basename(file) + bpmn_file_name = re.sub(r'^test_(.*)\.json', r'\1.bpmn', json_file_name) + bpmn_file_path = os.path.join(file_dir, bpmn_file_name) + if os.path.isfile(bpmn_file_path): + test_mappings[file] = bpmn_file_path + return test_mappings + + +class BpmnFileMissingExecutableProcessError(Exception): + pass + + +class ProcessModelTestRunnerService: + def __init__( + self, + process_model_directory_path: str + ) -> None: + self.process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path, + instantiate_executer_callback=self._instantiate_executer_callback, + execute_task_callback=self._execute_task_callback, + get_next_task_callback=self._get_next_task_callback, + ) + + def run(self) -> None: + self.process_model_test_runner.run() + + def _execute_task_callback(self, spiff_task: SpiffTask, _test_case_json: Optional[dict]) -> None: + spiff_task.run() + + def _get_next_task_callback(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + engine_steps = self._get_ready_engine_steps(bpmn_process_instance) + if len(engine_steps) > 0: + return engine_steps[0] + return None + + def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: + tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) + + if len(tasks) > 0: + tasks = [tasks[0]] + + return tasks + + def _instantiate_executer_callback(self, bpmn_file) -> BpmnWorkflow: + parser = MyCustomParser() + data = None + with open(bpmn_file, "rb") as f_handle: + data = f_handle.read() + bpmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) + parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) + sub_parsers = list(parser.process_parsers.values()) + executable_process = None + for sub_parser in sub_parsers: + if sub_parser.process_executable: + executable_process = sub_parser.bpmn_id + if executable_process is None: + raise BpmnFileMissingExecutableProcessError(f"Executable process cannot be found in {bpmn_file}. Test cannot run.") + bpmn_process_spec = parser.get_spec(executable_process) + bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) + return bpmn_process_instance diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py index e8771738c..9169c5d60 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/spec_file_service.py @@ -221,37 +221,37 @@ class SpecFileService(FileSystemService): return spec_file_data @staticmethod - def full_file_path(spec: ProcessModelInfo, file_name: str) -> str: + def full_file_path(process_model: ProcessModelInfo, file_name: str) -> str: """File_path.""" - return os.path.abspath(os.path.join(SpecFileService.workflow_path(spec), file_name)) + return os.path.abspath(os.path.join(SpecFileService.process_model_full_path(process_model), file_name)) @staticmethod - def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime: + def last_modified(process_model: ProcessModelInfo, file_name: str) -> datetime: """Last_modified.""" - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) return FileSystemService._last_modified(full_file_path) @staticmethod - def timestamp(spec: ProcessModelInfo, file_name: str) -> float: + def timestamp(process_model: ProcessModelInfo, file_name: str) -> float: """Timestamp.""" - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) return FileSystemService._timestamp(full_file_path) @staticmethod - def delete_file(spec: ProcessModelInfo, file_name: str) -> None: + def delete_file(process_model: ProcessModelInfo, file_name: str) -> None: """Delete_file.""" - # Fixme: Remember to remove the lookup files when the spec file is removed. + # Fixme: Remember to remove the lookup files when the process_model file is removed. # lookup_files = session.query(LookupFileModel).filter_by(file_model_id=file_id).all() # for lf in lookup_files: # session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete() # session.query(LookupFileModel).filter_by(id=lf.id).delete() - full_file_path = SpecFileService.full_file_path(spec, file_name) + full_file_path = SpecFileService.full_file_path(process_model, file_name) os.remove(full_file_path) @staticmethod - def delete_all_files(spec: ProcessModelInfo) -> None: + def delete_all_files(process_model: ProcessModelInfo) -> None: """Delete_all_files.""" - dir_path = SpecFileService.workflow_path(spec) + dir_path = SpecFileService.process_model_full_path(process_model) if os.path.exists(dir_path): shutil.rmtree(dir_path) diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn new file mode 100644 index 000000000..3a5302e62 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0qfycuk + + + + Flow_1auiekw + + + + Flow_0qfycuk + Flow_1auiekw + a = 1 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json new file mode 100644 index 000000000..03d72515d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Script Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "Script.bpmn", + "primary_process_id": "Process_Script_Task" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json new file mode 100644 index 000000000..8eb2df13d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json @@ -0,0 +1,5 @@ +{ + "test_case_one": { + "expected_output_json": { "a": 1 } + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py new file mode 100644 index 000000000..b09322352 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -0,0 +1,42 @@ +from flask import Flask +import pytest +import os +from flask import current_app +from spiffworkflow_backend.models.process_model import ProcessModelInfo +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_service import ProcessModelService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec +from pytest_mock import MockerFixture + +from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel +from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel +from spiffworkflow_backend.models.task import TaskModel # noqa: F401 +from spiffworkflow_backend.models.task_definition import TaskDefinitionModel +from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor +from spiffworkflow_backend.services.task_service import TaskService + + +class TestProcessModelTestRunnerService(BaseTest): + def test_can_test_a_simple_process_model( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: any, + ) -> None: + test_runner_service = ProcessModelTestRunnerService(os.path.join(FileSystemService.root_path(), 'basic_script_task')) + test_runner_service.run() + assert test_runner_service.process_model_test_runner.all_test_cases_passed() + + @pytest.fixture() + def with_mocked_root_path(self, mocker: MockerFixture) -> None: + path = os.path.join( + current_app.instance_path, + "..", + "..", + "tests", + "data", + "bpmn_unit_test_process_models", + ) + mocker.patch.object(FileSystemService, attribute='root_path', return_value=path) From 66714ddeb6144bd10a5b7afd39b62a9a8db5f09f Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:32:53 -0400 Subject: [PATCH 02/16] added test for failing test and multiple at once w/ burnettk --- .../process_model_test_runner_service.py | 6 +++ .../basic_failing_script_task.bpmn | 41 +++++++++++++++++++ .../process_model.json | 11 +++++ .../test_basic_failing_script_task.json | 3 ++ .../test_process_model_test_runner_service.py | 10 +++++ 5 files changed, 71 insertions(+) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index a6ae27fda..a615dc5a3 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -30,6 +30,10 @@ class UnrunnableTestCaseError(Exception): pass +class MissingBpmnFileForTestCaseError(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -122,6 +126,8 @@ class ProcessModelTestRunner: bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): test_mappings[file] = bpmn_file_path + else: + raise MissingBpmnFileForTestCaseError(f"Cannot find a matching bpmn file for test case json file: '{file}'") return test_mappings diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn new file mode 100644 index 000000000..4b0463358 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn @@ -0,0 +1,41 @@ + + + + + Flow_1xkc1ru + + + + Flow_0tkkq9s + + + + Flow_1xkc1ru + Flow_0tkkq9s + a = 1 +b = a + 'two' + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json new file mode 100644 index 000000000..26f25510d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "Process that raises an exception", + "display_name": "Failing Process", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "failing_task.bpmn", + "primary_process_id": "Process_FailingProcess" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json new file mode 100644 index 000000000..553606003 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json @@ -0,0 +1,3 @@ +{ + "test_case_two": {} +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index b09322352..d202a35c5 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -29,6 +29,16 @@ class TestProcessModelTestRunnerService(BaseTest): test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() + def test_can_test_multiple_process_models( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: any, + ) -> None: + test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) + test_runner_service.run() + assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False + @pytest.fixture() def with_mocked_root_path(self, mocker: MockerFixture) -> None: path = os.path.join( From b4b1ef52c8970ac824b2e2713ea7e082650e8693 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:01:11 -0400 Subject: [PATCH 03/16] pyl --- .../spiffworkflow_backend/config/__init__.py | 8 +- .../services/file_system_service.py | 4 +- .../process_model_test_runner_service.py | 89 ++++++++++--------- .../process_model.json | 2 +- .../test_process_model_test_runner_service.py | 34 ++++--- 5 files changed, 73 insertions(+), 64 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py index 55c958977..224791108 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/config/__init__.py @@ -40,7 +40,8 @@ def setup_database_configs(app: Flask) -> None: if pool_size is not None: pool_size = int(pool_size) else: - # this one doesn't come from app config and isn't documented in default.py because we don't want to give people the impression + # this one doesn't come from app config and isn't documented in default.py + # because we don't want to give people the impression # that setting it in flask python configs will work. on the contrary, it's used by a bash # script that starts the backend, so it can only be set in the environment. threads_per_worker_config = os.environ.get("SPIFFWORKFLOW_BACKEND_THREADS_PER_WORKER") @@ -50,8 +51,9 @@ def setup_database_configs(app: Flask) -> None: # this is a sqlalchemy default, if we don't have any better ideas pool_size = 5 - app.config['SQLALCHEMY_ENGINE_OPTIONS'] = {} - app.config['SQLALCHEMY_ENGINE_OPTIONS']['pool_size'] = pool_size + app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {} + app.config["SQLALCHEMY_ENGINE_OPTIONS"]["pool_size"] = pool_size + def load_config_file(app: Flask, env_config_module: str) -> None: """Load_config_file.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py index b4b85a744..be38f886d 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/file_system_service.py @@ -86,7 +86,9 @@ class FileSystemService: @staticmethod def full_path_to_process_model_file(process_model: ProcessModelInfo) -> str: """Full_path_to_process_model_file.""" - return os.path.join(FileSystemService.process_model_full_path(process_model), process_model.primary_file_name) # type: ignore + return os.path.join( + FileSystemService.process_model_full_path(process_model), process_model.primary_file_name # type: ignore + ) def next_display_order(self, process_model: ProcessModelInfo) -> int: """Next_display_order.""" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index a615dc5a3..df6e76ef2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,18 +1,19 @@ -from typing import List, Optional -from dataclasses import dataclass -import json -from SpiffWorkflow.task import Task as SpiffTask # type: ignore -from SpiffWorkflow.task import TaskState -from lxml import etree # type: ignore -from spiffworkflow_backend.services.spec_file_service import SpecFileService -from spiffworkflow_backend.services.custom_parser import MyCustomParser -from typing import Callable -import re import glob -from spiffworkflow_backend.models.process_model import ProcessModelInfo +import json import os -from spiffworkflow_backend.services.file_system_service import FileSystemService +import re +from dataclasses import dataclass +from typing import Any +from typing import Callable +from typing import Optional + +from lxml import etree # type: ignore from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore +from SpiffWorkflow.task import Task as SpiffTask # type: ignore +from SpiffWorkflow.task import TaskState + +from spiffworkflow_backend.services.custom_parser import MyCustomParser +from spiffworkflow_backend.services.spec_file_service import SpecFileService # workflow json for test case @@ -22,7 +23,8 @@ from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore # find all process models # find all json test cases for each # for each test case, fire up something like spiff -# for each task, if there is something special in the test case definition, do it (provide data for user task, mock service task, etc) +# for each task, if there is something special in the test case definition, +# do it (provide data for user task, mock service task, etc) # when the thing is complete, check workflow data against expected data @@ -56,12 +58,13 @@ class ProcessModelTestRunner: KEEP THIS GENERIC. do not add backend specific code here. """ + def __init__( self, process_model_directory_path: str, - instantiate_executer_callback: Callable[[str], any], - execute_task_callback: Callable[[any, Optional[dict]], any], - get_next_task_callback: Callable[[any], any], + instantiate_executer_callback: Callable[[str], Any], + execute_task_callback: Callable[[Any, Optional[dict]], Any], + get_next_task_callback: Callable[[Any], Any], ) -> None: self.process_model_directory_path = process_model_directory_path self.test_mappings = self._discover_process_model_directories() @@ -69,7 +72,7 @@ class ProcessModelTestRunner: self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback - self.test_case_results = [] + self.test_case_results: list[TestCaseResult] = [] def all_test_cases_passed(self) -> bool: failed_tests = [t for t in self.test_case_results if t.passed is False] @@ -77,40 +80,45 @@ class ProcessModelTestRunner: def run(self) -> None: for json_test_case_file, bpmn_file in self.test_mappings.items(): - with open(json_test_case_file, 'rt') as f: + with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) for test_case_name, test_case_contents in json_file_contents.items(): try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self.test_case_results.append(TestCaseResult( - passed=False, - test_case_name=test_case_name, - error=f"Syntax error: {str(ex)}", - )) + self.test_case_results.append( + TestCaseResult( + passed=False, + test_case_name=test_case_name, + error=f"Syntax error: {str(ex)}", + ) + ) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) next_task = self.get_next_task_callback(bpmn_process_instance) while next_task is not None: test_case_json = None - if 'tasks' in test_case_contents: - if next_task.task_spec.bpmn_id in test_case_contents['tasks']: - test_case_json = test_case_contents['tasks'][next_task.task_spec.bpmn_id] + if "tasks" in test_case_contents: + if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: + test_case_json = test_case_contents["tasks"][next_task.task_spec.bpmn_id] task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: raise UnrunnableTestCaseError( - f"Cannot run test case '{test_case_name}'. It requires task data for {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" + f"Cannot run test case '{test_case_name}'. It requires task data for" + f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) self.execute_task_callback(next_task, test_case_json) next_task = self.get_next_task_callback(bpmn_process_instance) - test_passed = test_case_contents['expected_output_json'] == bpmn_process_instance.data - self.test_case_results.append(TestCaseResult( - passed=test_passed, - test_case_name=test_case_name, - )) + test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data + self.test_case_results.append( + TestCaseResult( + passed=test_passed, + test_case_name=test_case_name, + ) + ) def _discover_process_model_directories( self, @@ -122,12 +130,14 @@ class ProcessModelTestRunner: for file in glob.glob(json_test_file_glob, recursive=True): file_dir = os.path.dirname(file) json_file_name = os.path.basename(file) - bpmn_file_name = re.sub(r'^test_(.*)\.json', r'\1.bpmn', json_file_name) + bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): test_mappings[file] = bpmn_file_path else: - raise MissingBpmnFileForTestCaseError(f"Cannot find a matching bpmn file for test case json file: '{file}'") + raise MissingBpmnFileForTestCaseError( + f"Cannot find a matching bpmn file for test case json file: '{file}'" + ) return test_mappings @@ -136,10 +146,7 @@ class BpmnFileMissingExecutableProcessError(Exception): class ProcessModelTestRunnerService: - def __init__( - self, - process_model_directory_path: str - ) -> None: + def __init__(self, process_model_directory_path: str) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, instantiate_executer_callback=self._instantiate_executer_callback, @@ -167,7 +174,7 @@ class ProcessModelTestRunnerService: return tasks - def _instantiate_executer_callback(self, bpmn_file) -> BpmnWorkflow: + def _instantiate_executer_callback(self, bpmn_file: str) -> BpmnWorkflow: parser = MyCustomParser() data = None with open(bpmn_file, "rb") as f_handle: @@ -180,7 +187,9 @@ class ProcessModelTestRunnerService: if sub_parser.process_executable: executable_process = sub_parser.bpmn_id if executable_process is None: - raise BpmnFileMissingExecutableProcessError(f"Executable process cannot be found in {bpmn_file}. Test cannot run.") + raise BpmnFileMissingExecutableProcessError( + f"Executable process cannot be found in {bpmn_file}. Test cannot run." + ) bpmn_process_spec = parser.get_spec(executable_process) bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) return bpmn_process_instance diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json index 26f25510d..23cc190ba 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json @@ -8,4 +8,4 @@ "metadata_extraction_paths": null, "primary_file_name": "failing_task.bpmn", "primary_process_id": "Process_FailingProcess" -} \ No newline at end of file +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index d202a35c5..a993c9458 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -1,21 +1,15 @@ -from flask import Flask -import pytest import os -from flask import current_app -from spiffworkflow_backend.models.process_model import ProcessModelInfo -from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_service import ProcessModelService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService -from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from tests.spiffworkflow_backend.helpers.test_data import load_test_spec -from pytest_mock import MockerFixture +from typing import Any + +import pytest +from flask import current_app +from flask import Flask +from pytest_mock import MockerFixture +from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from spiffworkflow_backend.models.bpmn_process import BpmnProcessModel -from spiffworkflow_backend.models.bpmn_process_definition import BpmnProcessDefinitionModel from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.models.task_definition import TaskDefinitionModel -from spiffworkflow_backend.services.process_instance_processor import ProcessInstanceProcessor -from spiffworkflow_backend.services.task_service import TaskService +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunnerService class TestProcessModelTestRunnerService(BaseTest): @@ -23,9 +17,11 @@ class TestProcessModelTestRunnerService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: any, + with_mocked_root_path: Any, ) -> None: - test_runner_service = ProcessModelTestRunnerService(os.path.join(FileSystemService.root_path(), 'basic_script_task')) + test_runner_service = ProcessModelTestRunnerService( + os.path.join(FileSystemService.root_path(), "basic_script_task") + ) test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() @@ -33,7 +29,7 @@ class TestProcessModelTestRunnerService(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: any, + with_mocked_root_path: Any, ) -> None: test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) test_runner_service.run() @@ -49,4 +45,4 @@ class TestProcessModelTestRunnerService(BaseTest): "data", "bpmn_unit_test_process_models", ) - mocker.patch.object(FileSystemService, attribute='root_path', return_value=path) + mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From f694c7b1f158a180e875b5592e205387a7d6ed75 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:16:09 -0400 Subject: [PATCH 04/16] no reason to instantiate a ProcessModelService --- spiffworkflow-backend/conftest.py | 5 +- .../routes/process_groups_controller.py | 6 +-- .../routes/process_models_controller.py | 6 +-- .../services/process_instance_processor.py | 3 +- .../services/process_instance_service.py | 3 +- .../services/process_model_service.py | 53 +++++++++---------- .../helpers/test_data.py | 6 +-- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/spiffworkflow-backend/conftest.py b/spiffworkflow-backend/conftest.py index 45e9fd54d..c18c49369 100644 --- a/spiffworkflow-backend/conftest.py +++ b/spiffworkflow-backend/conftest.py @@ -55,9 +55,8 @@ def with_db_and_bpmn_file_cleanup() -> None: try: yield finally: - process_model_service = ProcessModelService() - if os.path.exists(process_model_service.root_path()): - shutil.rmtree(process_model_service.root_path()) + if os.path.exists(ProcessModelService.root_path()): + shutil.rmtree(ProcessModelService.root_path()) @pytest.fixture() diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py index 6d1a479a8..1901300d4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py @@ -53,7 +53,7 @@ def process_group_delete(modified_process_group_id: str) -> flask.wrappers.Respo process_group_id = _un_modify_modified_process_model_id(modified_process_group_id) try: - ProcessModelService().process_group_delete(process_group_id) + ProcessModelService.process_group_delete(process_group_id) except ProcessModelWithInstancesNotDeletableError as exception: raise ApiError( error_code="existing_instances", @@ -88,7 +88,7 @@ def process_group_list( process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100 ) -> flask.wrappers.Response: process_groups = ProcessModelService.get_process_groups_for_api(process_group_identifier) - batch = ProcessModelService().get_batch(items=process_groups, page=page, per_page=per_page) + batch = ProcessModelService.get_batch(items=process_groups, page=page, per_page=per_page) pages = len(process_groups) // per_page remainder = len(process_groups) % per_page if remainder > 0: @@ -128,7 +128,7 @@ def process_group_show( def process_group_move(modified_process_group_identifier: str, new_location: str) -> flask.wrappers.Response: """Process_group_move.""" original_process_group_id = _un_modify_modified_process_model_id(modified_process_group_identifier) - new_process_group = ProcessModelService().process_group_move(original_process_group_id, new_location) + new_process_group = ProcessModelService.process_group_move(original_process_group_id, new_location) _commit_and_push_to_git( f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}" ) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index eb7f2f9b8..00d82639b 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -104,7 +104,7 @@ def process_model_delete( """Process_model_delete.""" process_model_identifier = modified_process_model_identifier.replace(":", "/") try: - ProcessModelService().process_model_delete(process_model_identifier) + ProcessModelService.process_model_delete(process_model_identifier) except ProcessModelWithInstancesNotDeletableError as exception: raise ApiError( error_code="existing_instances", @@ -182,7 +182,7 @@ def process_model_show(modified_process_model_identifier: str, include_file_refe def process_model_move(modified_process_model_identifier: str, new_location: str) -> flask.wrappers.Response: """Process_model_move.""" original_process_model_id = _un_modify_modified_process_model_id(modified_process_model_identifier) - new_process_model = ProcessModelService().process_model_move(original_process_model_id, new_location) + new_process_model = ProcessModelService.process_model_move(original_process_model_id, new_location) _commit_and_push_to_git( f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}" ) @@ -219,7 +219,7 @@ def process_model_list( recursive=recursive, filter_runnable_by_user=filter_runnable_by_user, ) - process_models_to_return = ProcessModelService().get_batch(process_models, page=page, per_page=per_page) + process_models_to_return = ProcessModelService.get_batch(process_models, page=page, per_page=per_page) if include_parent_groups: process_group_cache = IdToProcessGroupMapping({}) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index b64cedfdf..1c740c2d1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -420,7 +420,6 @@ class ProcessInstanceProcessor: ) self.process_instance_model = process_instance_model - self.process_model_service = ProcessModelService() bpmn_process_spec = None self.full_bpmn_process_dict: dict = {} @@ -1018,7 +1017,7 @@ class ProcessInstanceProcessor: ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks() process_model_display_name = "" - process_model_info = self.process_model_service.get_process_model( + process_model_info = ProcessModelService.get_process_model( self.process_instance_model.process_model_identifier ) if process_model_info is not None: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py index 84c62f12c..0bb1c6898 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_service.py @@ -192,8 +192,7 @@ class ProcessInstanceService: """ # navigation = processor.bpmn_process_instance.get_deep_nav_list() # ProcessInstanceService.update_navigation(navigation, processor) - process_model_service = ProcessModelService() - process_model_service.get_process_model(processor.process_model_identifier) + ProcessModelService.get_process_model(processor.process_model_identifier) process_instance_api = ProcessInstanceApi( id=processor.get_process_instance_id(), status=processor.get_status(), diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py index fbf6587b2..dd771089a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_service.py @@ -94,7 +94,6 @@ class ProcessModelService(FileSystemService): page: int = 1, per_page: int = 10, ) -> list[T]: - """Get_batch.""" start = (page - 1) * per_page end = start + per_page return items[start:end] @@ -129,8 +128,8 @@ class ProcessModelService(FileSystemService): cls.write_json_file(json_path, json_data) process_model.id = process_model_id - def process_model_delete(self, process_model_id: str) -> None: - """Delete Procecss Model.""" + @classmethod + def process_model_delete(cls, process_model_id: str) -> None: instances = ProcessInstanceModel.query.filter( ProcessInstanceModel.process_model_identifier == process_model_id ).all() @@ -138,19 +137,19 @@ class ProcessModelService(FileSystemService): raise ProcessModelWithInstancesNotDeletableError( f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it." ) - process_model = self.get_process_model(process_model_id) - path = self.process_model_full_path(process_model) + process_model = cls.get_process_model(process_model_id) + path = cls.process_model_full_path(process_model) shutil.rmtree(path) - def process_model_move(self, original_process_model_id: str, new_location: str) -> ProcessModelInfo: - """Process_model_move.""" - process_model = self.get_process_model(original_process_model_id) - original_model_path = self.process_model_full_path(process_model) + @classmethod + def process_model_move(cls, original_process_model_id: str, new_location: str) -> ProcessModelInfo: + process_model = cls.get_process_model(original_process_model_id) + original_model_path = cls.process_model_full_path(process_model) _, model_id = os.path.split(original_model_path) new_relative_path = os.path.join(new_location, model_id) new_model_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_relative_path)) shutil.move(original_model_path, new_model_path) - new_process_model = self.get_process_model(new_relative_path) + new_process_model = cls.get_process_model(new_relative_path) return new_process_model @classmethod @@ -315,12 +314,10 @@ class ProcessModelService(FileSystemService): @classmethod def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: - """Add_process_group.""" return cls.update_process_group(process_group) @classmethod def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup: - """Update_process_group.""" cat_path = cls.full_path_from_id(process_group.id) os.makedirs(cat_path, exist_ok=True) json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE) @@ -331,33 +328,33 @@ class ProcessModelService(FileSystemService): cls.write_json_file(json_path, serialized_process_group) return process_group - def process_group_move(self, original_process_group_id: str, new_location: str) -> ProcessGroup: - """Process_group_move.""" - original_group_path = self.full_path_from_id(original_process_group_id) + @classmethod + def process_group_move(cls, original_process_group_id: str, new_location: str) -> ProcessGroup: + original_group_path = cls.full_path_from_id(original_process_group_id) _, original_group_id = os.path.split(original_group_path) new_root = os.path.join(FileSystemService.root_path(), new_location) new_group_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_root, original_group_id)) destination = shutil.move(original_group_path, new_group_path) - new_process_group = self.get_process_group(destination) + new_process_group = cls.get_process_group(destination) return new_process_group - def __get_all_nested_models(self, group_path: str) -> list: - """__get_all_nested_models.""" + @classmethod + def __get_all_nested_models(cls, group_path: str) -> list: all_nested_models = [] for _root, dirs, _files in os.walk(group_path): for dir in dirs: model_dir = os.path.join(group_path, dir) if ProcessModelService.is_process_model(model_dir): - process_model = self.get_process_model(model_dir) + process_model = cls.get_process_model(model_dir) all_nested_models.append(process_model) return all_nested_models - def process_group_delete(self, process_group_id: str) -> None: - """Delete_process_group.""" + @classmethod + def process_group_delete(cls, process_group_id: str) -> None: problem_models = [] - path = self.full_path_from_id(process_group_id) + path = cls.full_path_from_id(process_group_id) if os.path.exists(path): - nested_models = self.__get_all_nested_models(path) + nested_models = cls.__get_all_nested_models(path) for process_model in nested_models: instances = ProcessInstanceModel.query.filter( ProcessInstanceModel.process_model_identifier == process_model.id @@ -371,15 +368,15 @@ class ProcessModelService(FileSystemService): f" {problem_models}" ) shutil.rmtree(path) - self.cleanup_process_group_display_order() + cls._cleanup_process_group_display_order() - def cleanup_process_group_display_order(self) -> List[Any]: - """Cleanup_process_group_display_order.""" - process_groups = self.get_process_groups() # Returns an ordered list + @classmethod + def _cleanup_process_group_display_order(cls) -> List[Any]: + process_groups = cls.get_process_groups() # Returns an ordered list index = 0 for process_group in process_groups: process_group.display_order = index - self.update_process_group(process_group) + cls.update_process_group(process_group) index += 1 return process_groups diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py index 5a7a969ef..7af2cfdcb 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/helpers/test_data.py @@ -12,12 +12,10 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer def assure_process_group_exists(process_group_id: Optional[str] = None) -> ProcessGroup: - """Assure_process_group_exists.""" process_group = None - process_model_service = ProcessModelService() if process_group_id is not None: try: - process_group = process_model_service.get_process_group(process_group_id) + process_group = ProcessModelService.get_process_group(process_group_id) except ProcessEntityNotFoundError: process_group = None @@ -31,7 +29,7 @@ def assure_process_group_exists(process_group_id: Optional[str] = None) -> Proce admin=False, display_order=0, ) - process_model_service.add_process_group(process_group) + ProcessModelService.add_process_group(process_group) return process_group From b08386bf771dcfe1b04aa97b95bbccd10fa40746 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 16:35:04 -0400 Subject: [PATCH 05/16] support call activities in process model test runner w/ burnettk --- .../process_model_test_runner_service.py | 188 ++++++++++++++---- .../basic_call_activity.bpmn | 40 ++++ .../basic_call_activity/process_model.json | 9 + .../test_basic_call_activity.json | 5 + .../basic_manual_task/basic_manual_task.bpmn | 39 ++++ .../basic_manual_task/process_model.json | 9 + .../test_basic_manual_task.json | 10 + .../test_process_model_test_runner_service.py | 17 +- 8 files changed, 280 insertions(+), 37 deletions(-) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index df6e76ef2..4093e2f6c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -39,16 +39,26 @@ class MissingBpmnFileForTestCaseError(Exception): @dataclass class TestCaseResult: passed: bool + bpmn_file: str test_case_name: str error: Optional[str] = None +DEFAULT_NSMAP = { + 'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', + 'bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', + 'dc': 'http://www.omg.org/spec/DD/20100524/DC', +} + + # input: # json_file: # { # [TEST_CASE_NAME]: { # "tasks": { -# [BPMN_TASK_IDENTIIFER]: [DATA] +# [BPMN_TASK_IDENTIIFER]: { +# "data": [DATA] +# } # }, # "expected_output_json": [DATA] # } @@ -62,17 +72,23 @@ class ProcessModelTestRunner: def __init__( self, process_model_directory_path: str, - instantiate_executer_callback: Callable[[str], Any], - execute_task_callback: Callable[[Any, Optional[dict]], Any], - get_next_task_callback: Callable[[Any], Any], + process_model_directory_for_test_discovery: Optional[str] = None, + instantiate_executer_callback: Optional[Callable[[str], Any]] = None, + execute_task_callback: Optional[Callable[[Any, Optional[dict]], Any]] = None, + get_next_task_callback: Optional[Callable[[Any], Any]] = None, ) -> None: self.process_model_directory_path = process_model_directory_path - self.test_mappings = self._discover_process_model_directories() + self.process_model_directory_for_test_discovery = process_model_directory_for_test_discovery or process_model_directory_path self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback self.test_case_results: list[TestCaseResult] = [] + self.bpmn_processes_to_file_mappings: dict[str, str] = {} + self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} + + self.test_mappings = self._discover_process_model_test_cases() + self._discover_process_model_processes() def all_test_cases_passed(self) -> bool: failed_tests = [t for t in self.test_case_results if t.passed is False] @@ -87,59 +103,161 @@ class ProcessModelTestRunner: try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self.test_case_results.append( - TestCaseResult( - passed=False, - test_case_name=test_case_name, - error=f"Syntax error: {str(ex)}", - ) - ) + self._add_test_result(False, bpmn_file, test_case_name, f"Syntax error: {str(ex)}") def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: - bpmn_process_instance = self.instantiate_executer_callback(bpmn_file) - next_task = self.get_next_task_callback(bpmn_process_instance) + bpmn_process_instance = self._instantiate_executer(bpmn_file) + next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: - test_case_json = None + test_case_task_properties = None if "tasks" in test_case_contents: if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: - test_case_json = test_case_contents["tasks"][next_task.task_spec.bpmn_id] + test_case_task_properties = test_case_contents["tasks"][next_task.task_spec.bpmn_id] task_type = next_task.task_spec.__class__.__name__ - if task_type in ["ServiceTask", "UserTask"] and test_case_json is None: + if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: raise UnrunnableTestCaseError( f"Cannot run test case '{test_case_name}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) - self.execute_task_callback(next_task, test_case_json) - next_task = self.get_next_task_callback(bpmn_process_instance) + self._execute_task(next_task, test_case_task_properties) + next_task = self._get_next_task(bpmn_process_instance) test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data - self.test_case_results.append( - TestCaseResult( - passed=test_passed, - test_case_name=test_case_name, + error_message = None + if test_passed is False: + error_message = ( + f"Expected output did not match actual output:" + f"\nexpected: {test_case_contents['expected_output_json']}" + f"\nactual: {bpmn_process_instance.data}" ) - ) + self._add_test_result(test_passed, bpmn_file, test_case_name, error_message) - def _discover_process_model_directories( + def _discover_process_model_test_cases( self, ) -> dict[str, str]: test_mappings = {} - json_test_file_glob = os.path.join(self.process_model_directory_path, "**", "test_*.json") + json_test_file_glob = os.path.join(self.process_model_directory_for_test_discovery, "**", "test_*.json") for file in glob.glob(json_test_file_glob, recursive=True): - file_dir = os.path.dirname(file) - json_file_name = os.path.basename(file) + file_norm = os.path.normpath(file) + file_dir = os.path.dirname(file_norm) + json_file_name = os.path.basename(file_norm) bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) bpmn_file_path = os.path.join(file_dir, bpmn_file_name) if os.path.isfile(bpmn_file_path): - test_mappings[file] = bpmn_file_path + test_mappings[file_norm] = bpmn_file_path else: raise MissingBpmnFileForTestCaseError( - f"Cannot find a matching bpmn file for test case json file: '{file}'" + f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" ) return test_mappings + def _discover_process_model_processes( + self, + ) -> None: + process_model_bpmn_file_glob = os.path.join(self.process_model_directory_path, "**", "*.bpmn") + + for file in glob.glob(process_model_bpmn_file_glob, recursive=True): + file_norm = os.path.normpath(file) + if file_norm not in self.bpmn_files_to_called_element_mappings: + self.bpmn_files_to_called_element_mappings[file_norm] = [] + with open(file_norm, 'rb') as f: + file_contents = f.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + root = etree.fromstring(file_contents, parser=etree_xml_parser) + call_activities = root.findall('.//bpmn:callActivity', namespaces=DEFAULT_NSMAP) + for call_activity in call_activities: + called_element = call_activity.attrib['calledElement'] + self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) + bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) + bpmn_process_identifier = bpmn_process_element.attrib['id'] + self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm + + def _execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + if self.execute_task_callback: + self.execute_task_callback(spiff_task, test_case_task_properties) + self._default_execute_task(spiff_task, test_case_task_properties) + + def _get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + if self.get_next_task_callback: + return self.get_next_task_callback(bpmn_process_instance) + return self._default_get_next_task(bpmn_process_instance) + + def _instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + if self.instantiate_executer_callback: + return self.instantiate_executer_callback(bpmn_file) + return self._default_instantiate_executer(bpmn_file) + + def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: + tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) + if len(tasks) > 0: + tasks = [tasks[0]] + + return tasks + + def _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: + engine_steps = self._get_ready_engine_steps(bpmn_process_instance) + if len(engine_steps) > 0: + return engine_steps[0] + return None + + def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + if spiff_task.task_spec.manual: + if test_case_task_properties and 'data' in test_case_task_properties: + spiff_task.update_data(test_case_task_properties['data']) + spiff_task.complete() + else: + spiff_task.run() + + def _find_related_bpmn_files(self, bpmn_file: str) -> list[str]: + related_bpmn_files = [] + if bpmn_file in self.bpmn_files_to_called_element_mappings: + for bpmn_process_identifier in self.bpmn_files_to_called_element_mappings[bpmn_file]: + if bpmn_process_identifier in self.bpmn_processes_to_file_mappings: + new_file = self.bpmn_processes_to_file_mappings[bpmn_process_identifier] + related_bpmn_files.append(new_file) + related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) + return related_bpmn_files + + def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree.Element: + data = None + with open(bpmn_file, "rb") as f_handle: + data = f_handle.read() + etree_xml_parser = etree.XMLParser(resolve_entities=False) + return etree.fromstring(data, parser=etree_xml_parser) + + def _default_instantiate_executer(self, bpmn_file: str) -> BpmnWorkflow: + parser = MyCustomParser() + bpmn_file_etree = self._get_etree_from_bpmn_file(bpmn_file) + parser.add_bpmn_xml(bpmn_file_etree, filename=os.path.basename(bpmn_file)) + all_related = self._find_related_bpmn_files(bpmn_file) + for related_file in all_related: + related_file_etree = self._get_etree_from_bpmn_file(related_file) + parser.add_bpmn_xml(related_file_etree, filename=os.path.basename(related_file)) + sub_parsers = list(parser.process_parsers.values()) + executable_process = None + for sub_parser in sub_parsers: + if sub_parser.process_executable: + executable_process = sub_parser.bpmn_id + if executable_process is None: + raise BpmnFileMissingExecutableProcessError( + f"Executable process cannot be found in {bpmn_file}. Test cannot run." + ) + bpmn_process_spec = parser.get_spec(executable_process) + bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) + return bpmn_process_instance + + def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error: Optional[str] = None) -> None: + bpmn_file_relative = os.path.relpath(bpmn_file, start=self.process_model_directory_path) + test_result = TestCaseResult( + passed=passed, + bpmn_file=bpmn_file_relative, + test_case_name=test_case_name, + error=error, + ) + self.test_case_results.append(test_result) + class BpmnFileMissingExecutableProcessError(Exception): pass @@ -149,9 +267,9 @@ class ProcessModelTestRunnerService: def __init__(self, process_model_directory_path: str) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, - instantiate_executer_callback=self._instantiate_executer_callback, - execute_task_callback=self._execute_task_callback, - get_next_task_callback=self._get_next_task_callback, + # instantiate_executer_callback=self._instantiate_executer_callback, + # execute_task_callback=self._execute_task_callback, + # get_next_task_callback=self._get_next_task_callback, ) def run(self) -> None: @@ -168,7 +286,6 @@ class ProcessModelTestRunnerService: def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: tasks = [tasks[0]] @@ -179,7 +296,8 @@ class ProcessModelTestRunnerService: data = None with open(bpmn_file, "rb") as f_handle: data = f_handle.read() - bpmn: etree.Element = SpecFileService.get_etree_from_xml_bytes(data) + etree_xml_parser = etree.XMLParser(resolve_entities=False) + bpmn = etree.fromstring(data, parser=etree_xml_parser) parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) sub_parsers = list(parser.process_parsers.values()) executable_process = None diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn new file mode 100644 index 000000000..ab7961463 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn @@ -0,0 +1,40 @@ + + + + + Flow_0ext5lt + + + + Flow_1hzwssi + + + + + Flow_0ext5lt + Flow_1hzwssi + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json new file mode 100644 index 000000000..8f4e4a044 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "", + "display_name": "Basic Call Activity", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "basic_call_activity.bpmn", + "primary_process_id": "BasicCallActivityProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json new file mode 100644 index 000000000..0a7f76929 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json @@ -0,0 +1,5 @@ +{ + "test_case_one": { + "expected_output_json": {} + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn new file mode 100644 index 000000000..5d0bf395a --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0gz6i84 + + + + Flow_0ikklg6 + + + + Flow_0gz6i84 + Flow_0ikklg6 + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json new file mode 100644 index 000000000..743dd104d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "Baisc Manual Task", + "display_name": "Baisc Manual Task", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "baisc_manual_task.bpmn", + "primary_process_id": "BasicManualTaskProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json new file mode 100644 index 000000000..d82f5c7cd --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json @@ -0,0 +1,10 @@ +{ + "test_case_one": { + "tasks": { + "manual_task_one": { + "data": {} + } + }, + "expected_output_json": {} + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py index a993c9458..89ed9ec77 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py @@ -9,7 +9,7 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunnerService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService class TestProcessModelTestRunnerService(BaseTest): @@ -23,7 +23,7 @@ class TestProcessModelTestRunnerService(BaseTest): os.path.join(FileSystemService.root_path(), "basic_script_task") ) test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed() + assert test_runner_service.process_model_test_runner.all_test_cases_passed(), test_runner_service.process_model_test_runner.test_case_results def test_can_test_multiple_process_models( self, @@ -35,6 +35,19 @@ class TestProcessModelTestRunnerService(BaseTest): test_runner_service.run() assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False + def test_can_test_process_model_call_activity( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + test_runner_service = ProcessModelTestRunner( + process_model_directory_path=FileSystemService.root_path(), + process_model_directory_for_test_discovery=os.path.join(FileSystemService.root_path(), "basic_call_activity") + ) + test_runner_service.run() + assert test_runner_service.all_test_cases_passed() is True, test_runner_service.test_case_results + @pytest.fixture() def with_mocked_root_path(self, mocker: MockerFixture) -> None: path = os.path.join( From e8e6cac9e5a4a03945194a42dac6dfe13aad04a9 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 17:28:51 -0400 Subject: [PATCH 06/16] cleaned up process model tests and added support for service tasks w/ burnettk --- .../process_model_test_runner_service.py | 58 ++-------- .../basic_failing_script_task.bpmn | 0 .../process_model.json | 0 .../test_basic_failing_script_task.json | 0 .../basic_call_activity.bpmn | 0 .../basic_call_activity/process_model.json | 0 .../test_basic_call_activity.json | 0 .../basic_manual_task/basic_manual_task.bpmn | 0 .../basic_manual_task/process_model.json | 0 .../test_basic_manual_task.json | 0 .../basic_script_task/basic_script_task.bpmn | 0 .../basic_script_task/process_model.json | 0 .../test_basic_script_task.json | 0 .../basic_service_task.bpmn | 56 ++++++++++ .../basic_service_task/process_model.json | 10 ++ .../test_basic_service_task.json | 10 ++ .../unit/test_process_model_test_runner.py | 100 ++++++++++++++++++ .../test_process_model_test_runner_service.py | 61 ----------- 18 files changed, 186 insertions(+), 109 deletions(-) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/basic_failing_script_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => failing_tests}/basic_failing_script_task/test_basic_failing_script_task.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/basic_call_activity.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_call_activity/test_basic_call_activity.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/basic_manual_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_manual_task/test_basic_manual_task.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/basic_script_task.bpmn (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{ => passing_tests}/basic_script_task/test_basic_script_task.json (100%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json create mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py delete mode 100644 spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 4093e2f6c..7a5569e69 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -36,6 +36,10 @@ class MissingBpmnFileForTestCaseError(Exception): pass +class NoTestCasesFoundError(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -95,6 +99,8 @@ class ProcessModelTestRunner: return len(failed_tests) < 1 def run(self) -> None: + if len(self.test_mappings.items()) < 1: + raise NoTestCasesFoundError(f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}") for json_test_case_file, bpmn_file in self.test_mappings.items(): with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) @@ -189,21 +195,14 @@ class ProcessModelTestRunner: return self.instantiate_executer_callback(bpmn_file) return self._default_instantiate_executer(bpmn_file) - def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: - tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: - tasks = [tasks[0]] - - return tasks - def _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - engine_steps = self._get_ready_engine_steps(bpmn_process_instance) - if len(engine_steps) > 0: - return engine_steps[0] + ready_tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY)]) + if len(ready_tasks) > 0: + return ready_tasks[0] return None def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: - if spiff_task.task_spec.manual: + if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == 'ServiceTask': if test_case_task_properties and 'data' in test_case_task_properties: spiff_task.update_data(test_case_task_properties['data']) spiff_task.complete() @@ -274,40 +273,3 @@ class ProcessModelTestRunnerService: def run(self) -> None: self.process_model_test_runner.run() - - def _execute_task_callback(self, spiff_task: SpiffTask, _test_case_json: Optional[dict]) -> None: - spiff_task.run() - - def _get_next_task_callback(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: - engine_steps = self._get_ready_engine_steps(bpmn_process_instance) - if len(engine_steps) > 0: - return engine_steps[0] - return None - - def _get_ready_engine_steps(self, bpmn_process_instance: BpmnWorkflow) -> list[SpiffTask]: - tasks = list([t for t in bpmn_process_instance.get_tasks(TaskState.READY) if not t.task_spec.manual]) - if len(tasks) > 0: - tasks = [tasks[0]] - - return tasks - - def _instantiate_executer_callback(self, bpmn_file: str) -> BpmnWorkflow: - parser = MyCustomParser() - data = None - with open(bpmn_file, "rb") as f_handle: - data = f_handle.read() - etree_xml_parser = etree.XMLParser(resolve_entities=False) - bpmn = etree.fromstring(data, parser=etree_xml_parser) - parser.add_bpmn_xml(bpmn, filename=os.path.basename(bpmn_file)) - sub_parsers = list(parser.process_parsers.values()) - executable_process = None - for sub_parser in sub_parsers: - if sub_parser.process_executable: - executable_process = sub_parser.bpmn_id - if executable_process is None: - raise BpmnFileMissingExecutableProcessError( - f"Executable process cannot be found in {bpmn_file}. Test cannot run." - ) - bpmn_process_spec = parser.get_spec(executable_process) - bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) - return bpmn_process_instance diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/basic_failing_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_failing_script_task/test_basic_failing_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/basic_call_activity.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_call_activity/test_basic_call_activity.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/basic_manual_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_manual_task/test_basic_manual_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/basic_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/basic_script_task/test_basic_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn new file mode 100644 index 000000000..8675d5910 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn @@ -0,0 +1,56 @@ + + + + + Flow_19ephzh + + + + Flow_1dsxn78 + + + + + + + + + + + + + + This is the Service Task Unit Test Screen. + + + Flow_0xx2kop + Flow_1dsxn78 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json new file mode 100644 index 000000000..0946afc6b --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json @@ -0,0 +1,10 @@ +{ + "description": "A.1.0.2", + "display_name": "A.1.0.2 - Service Task", + "display_order": 13, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "A.1.0.2.bpmn", + "primary_process_id": "Process_test_a102_A_1_0_2_bd2e724" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json new file mode 100644 index 000000000..729d81ac2 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json @@ -0,0 +1,10 @@ +{ + "test_case_one": { + "tasks": { + "service_task_one": { + "data": { "the_result": "result_from_service" } + } + }, + "expected_output_json": { "the_result": "result_from_service" } + } +} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py new file mode 100644 index 000000000..a66c9f8a6 --- /dev/null +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -0,0 +1,100 @@ +import os +from typing import Any +from typing import Optional + +import pytest +from flask import current_app +from flask import Flask +from pytest_mock import MockerFixture +from tests.spiffworkflow_backend.helpers.base_test import BaseTest + +from spiffworkflow_backend.models.task import TaskModel # noqa: F401 +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError, ProcessModelTestRunner + + +class TestProcessModelTestRunner(BaseTest): + def test_can_test_a_simple_process_model( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests('basic_script_task') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_will_raise_if_no_tests_found( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = ProcessModelTestRunner( + os.path.join(FileSystemService.root_path(), "DNE") + ) + with pytest.raises(NoTestCasesFoundError): + process_model_test_runner.run() + assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results + + def test_can_test_multiple_process_models_with_all_passing_tests( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests() + assert len(process_model_test_runner.test_case_results) > 1 + + def test_can_test_multiple_process_models_with_failing_tests( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(parent_directory='failing_tests') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_call_activity( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_call_activity') + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_with_service_task( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_service_task') + assert len(process_model_test_runner.test_case_results) == 1 + + def _run_model_tests(self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = 'passing_tests') -> ProcessModelTestRunner: + base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] + path_segments = base_process_model_dir_path_segments + if bpmn_process_directory_name: + path_segments = path_segments + [bpmn_process_directory_name] + process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), + process_model_directory_for_test_discovery=os.path.join(*path_segments) + ) + process_model_test_runner.run() + + all_tests_expected_to_pass = parent_directory == 'passing_tests' + assert process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass, process_model_test_runner.test_case_results + return process_model_test_runner + + @pytest.fixture() + def with_mocked_root_path(self, mocker: MockerFixture) -> None: + path = os.path.join( + current_app.instance_path, + "..", + "..", + "tests", + "data", + "bpmn_unit_test_process_models", + ) + mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py deleted file mode 100644 index 89ed9ec77..000000000 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner_service.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -from typing import Any - -import pytest -from flask import current_app -from flask import Flask -from pytest_mock import MockerFixture -from tests.spiffworkflow_backend.helpers.base_test import BaseTest - -from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner, ProcessModelTestRunnerService - - -class TestProcessModelTestRunnerService(BaseTest): - def test_can_test_a_simple_process_model( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunnerService( - os.path.join(FileSystemService.root_path(), "basic_script_task") - ) - test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed(), test_runner_service.process_model_test_runner.test_case_results - - def test_can_test_multiple_process_models( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunnerService(FileSystemService.root_path()) - test_runner_service.run() - assert test_runner_service.process_model_test_runner.all_test_cases_passed() is False - - def test_can_test_process_model_call_activity( - self, - app: Flask, - with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, - ) -> None: - test_runner_service = ProcessModelTestRunner( - process_model_directory_path=FileSystemService.root_path(), - process_model_directory_for_test_discovery=os.path.join(FileSystemService.root_path(), "basic_call_activity") - ) - test_runner_service.run() - assert test_runner_service.all_test_cases_passed() is True, test_runner_service.test_case_results - - @pytest.fixture() - def with_mocked_root_path(self, mocker: MockerFixture) -> None: - path = os.path.join( - current_app.instance_path, - "..", - "..", - "tests", - "data", - "bpmn_unit_test_process_models", - ) - mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From d00d10e79e57da4224d34adc96c353610005e8c4 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 15:11:30 -0400 Subject: [PATCH 07/16] allow prepending test case data with process id and added better error formatting w/ burnettk --- .../process_model_test_runner_service.py | 120 +++++++++++++----- .../basic_call_activity.bpmn | 1 - .../test_basic_manual_task.json | 2 +- .../basic_service_task/process_model.json | 2 +- .../test_basic_service_task.json | 4 +- .../unit/test_process_model_test_runner.py | 27 ++-- 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 7a5569e69..1e0ea82a1 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -13,7 +13,6 @@ from SpiffWorkflow.task import Task as SpiffTask # type: ignore from SpiffWorkflow.task import TaskState from spiffworkflow_backend.services.custom_parser import MyCustomParser -from spiffworkflow_backend.services.spec_file_service import SpecFileService # workflow json for test case @@ -45,17 +44,24 @@ class TestCaseResult: passed: bool bpmn_file: str test_case_name: str - error: Optional[str] = None + error_messages: Optional[list[str]] = None DEFAULT_NSMAP = { - 'bpmn': 'http://www.omg.org/spec/BPMN/20100524/MODEL', - 'bpmndi': 'http://www.omg.org/spec/BPMN/20100524/DI', - 'dc': 'http://www.omg.org/spec/DD/20100524/DC', + "bpmn": "http://www.omg.org/spec/BPMN/20100524/MODEL", + "bpmndi": "http://www.omg.org/spec/BPMN/20100524/DI", + "dc": "http://www.omg.org/spec/DD/20100524/DC", } # input: +# BPMN_TASK_IDENTIIFER: +# can be either task bpmn identifier or in format: +# [BPMN_PROCESS_ID]:[TASK_BPMN_IDENTIFIER] +# example: 'BasicServiceTaskProcess:service_task_one' +# this allows for tasks to share bpmn identifiers across models +# which is useful for call activities +# # json_file: # { # [TEST_CASE_NAME]: { @@ -78,15 +84,20 @@ class ProcessModelTestRunner: process_model_directory_path: str, process_model_directory_for_test_discovery: Optional[str] = None, instantiate_executer_callback: Optional[Callable[[str], Any]] = None, - execute_task_callback: Optional[Callable[[Any, Optional[dict]], Any]] = None, + execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, ) -> None: self.process_model_directory_path = process_model_directory_path - self.process_model_directory_for_test_discovery = process_model_directory_for_test_discovery or process_model_directory_path + self.process_model_directory_for_test_discovery = ( + process_model_directory_for_test_discovery or process_model_directory_path + ) self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback + # keep track of the current task data index + self.task_data_index: dict[str, int] = {} + self.test_case_results: list[TestCaseResult] = [] self.bpmn_processes_to_file_mappings: dict[str, str] = {} self.bpmn_files_to_called_element_mappings: dict[str, list[str]] = {} @@ -95,12 +106,28 @@ class ProcessModelTestRunner: self._discover_process_model_processes() def all_test_cases_passed(self) -> bool: - failed_tests = [t for t in self.test_case_results if t.passed is False] + failed_tests = self.failing_tests() return len(failed_tests) < 1 + def failing_tests(self) -> list[TestCaseResult]: + return [t for t in self.test_case_results if t.passed is False] + + def failing_tests_formatted(self) -> str: + formatted_tests = ["FAILING TESTS:"] + for failing_test in self.failing_tests(): + msg = '' + if failing_test.error_messages: + msg = '\n\t\t'.join(failing_test.error_messages) + formatted_tests.append( + f'\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}' + ) + return '\n'.join(formatted_tests) + def run(self) -> None: if len(self.test_mappings.items()) < 1: - raise NoTestCasesFoundError(f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}") + raise NoTestCasesFoundError( + f"Could not find any test cases in given directory: {self.process_model_directory_for_test_discovery}" + ) for json_test_case_file, bpmn_file in self.test_mappings.items(): with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) @@ -109,16 +136,21 @@ class ProcessModelTestRunner: try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - self._add_test_result(False, bpmn_file, test_case_name, f"Syntax error: {str(ex)}") + ex_as_array = str(ex).split('\n') + self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: bpmn_process_instance = self._instantiate_executer(bpmn_file) next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: test_case_task_properties = None + test_case_task_key = next_task.task_spec.bpmn_id if "tasks" in test_case_contents: - if next_task.task_spec.bpmn_id in test_case_contents["tasks"]: - test_case_task_properties = test_case_contents["tasks"][next_task.task_spec.bpmn_id] + if test_case_task_key not in test_case_contents["tasks"]: + # we may need to go to the top level workflow of a given bpmn file + test_case_task_key = f"{next_task.workflow.spec.name}:{next_task.task_spec.bpmn_id}" + if test_case_task_key in test_case_contents["tasks"]: + test_case_task_properties = test_case_contents["tasks"][test_case_task_key] task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: @@ -126,17 +158,29 @@ class ProcessModelTestRunner: f"Cannot run test case '{test_case_name}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) - self._execute_task(next_task, test_case_task_properties) + self._execute_task(next_task, test_case_task_key, test_case_task_properties) next_task = self._get_next_task(bpmn_process_instance) - test_passed = test_case_contents["expected_output_json"] == bpmn_process_instance.data + error_message = None - if test_passed is False: - error_message = ( - f"Expected output did not match actual output:" - f"\nexpected: {test_case_contents['expected_output_json']}" - f"\nactual: {bpmn_process_instance.data}" - ) - self._add_test_result(test_passed, bpmn_file, test_case_name, error_message) + if bpmn_process_instance.is_completed() is False: + error_message = [ + "Expected process instance to complete but it did not.", + f"Final data was: {bpmn_process_instance.last_task.data}", + f"Last task bpmn id: {bpmn_process_instance.last_task.task_spec.bpmn_id}", + f"Last task type: {bpmn_process_instance.last_task.task_spec.__class__.__name__}", + ] + elif bpmn_process_instance.success is False: + error_message = [ + "Expected process instance to succeed but it did not.", + f"Final data was: {bpmn_process_instance.data}", + ] + elif test_case_contents["expected_output_json"] != bpmn_process_instance.data: + error_message = [ + "Expected output did not match actual output:", + f"expected: {test_case_contents['expected_output_json']}", + f"actual: {bpmn_process_instance.data}", + ] + self._add_test_result(error_message is None, bpmn_file, test_case_name, error_message) def _discover_process_model_test_cases( self, @@ -168,22 +212,22 @@ class ProcessModelTestRunner: file_norm = os.path.normpath(file) if file_norm not in self.bpmn_files_to_called_element_mappings: self.bpmn_files_to_called_element_mappings[file_norm] = [] - with open(file_norm, 'rb') as f: + with open(file_norm, "rb") as f: file_contents = f.read() etree_xml_parser = etree.XMLParser(resolve_entities=False) root = etree.fromstring(file_contents, parser=etree_xml_parser) - call_activities = root.findall('.//bpmn:callActivity', namespaces=DEFAULT_NSMAP) + call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) for call_activity in call_activities: - called_element = call_activity.attrib['calledElement'] + called_element = call_activity.attrib["calledElement"] self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) - bpmn_process_identifier = bpmn_process_element.attrib['id'] + bpmn_process_identifier = bpmn_process_element.attrib["id"] self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm - def _execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: + def _execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: if self.execute_task_callback: - self.execute_task_callback(spiff_task, test_case_task_properties) - self._default_execute_task(spiff_task, test_case_task_properties) + self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) + self._default_execute_task(spiff_task, test_case_task_key, test_case_task_properties) def _get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]: if self.get_next_task_callback: @@ -201,10 +245,13 @@ class ProcessModelTestRunner: return ready_tasks[0] return None - def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_properties: Optional[dict]) -> None: - if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == 'ServiceTask': - if test_case_task_properties and 'data' in test_case_task_properties: - spiff_task.update_data(test_case_task_properties['data']) + def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": + if test_case_task_properties and "data" in test_case_task_properties: + if test_case_task_key not in self.task_data_index: + self.task_data_index[test_case_task_key] = 0 + spiff_task.update_data(test_case_task_properties["data"][self.task_data_index[test_case_task_key]]) + self.task_data_index[test_case_task_key] += 1 spiff_task.complete() else: spiff_task.run() @@ -247,13 +294,16 @@ class ProcessModelTestRunner: bpmn_process_instance = BpmnWorkflow(bpmn_process_spec) return bpmn_process_instance - def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error: Optional[str] = None) -> None: - bpmn_file_relative = os.path.relpath(bpmn_file, start=self.process_model_directory_path) + def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: + return os.path.relpath(bpmn_file, start=self.process_model_directory_path) + + def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None) -> None: + bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, bpmn_file=bpmn_file_relative, test_case_name=test_case_name, - error=error, + error_messages=error_messages, ) self.test_case_results.append(test_result) diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn index ab7961463..f837163f5 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn @@ -10,7 +10,6 @@ - Flow_0ext5lt Flow_1hzwssi diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json index d82f5c7cd..fab44ab7b 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json @@ -2,7 +2,7 @@ "test_case_one": { "tasks": { "manual_task_one": { - "data": {} + "data": [{}] } }, "expected_output_json": {} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json index 0946afc6b..b5e63674e 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json @@ -7,4 +7,4 @@ "files": [], "primary_file_name": "A.1.0.2.bpmn", "primary_process_id": "Process_test_a102_A_1_0_2_bd2e724" -} \ No newline at end of file +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json index 729d81ac2..da0b47a11 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json @@ -1,8 +1,8 @@ { "test_case_one": { "tasks": { - "service_task_one": { - "data": { "the_result": "result_from_service" } + "BasicServiceTaskProcess:service_task_one": { + "data": [{ "the_result": "result_from_service" }] } }, "expected_output_json": { "the_result": "result_from_service" } diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index a66c9f8a6..80db51916 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -10,7 +10,8 @@ from tests.spiffworkflow_backend.helpers.base_test import BaseTest from spiffworkflow_backend.models.task import TaskModel # noqa: F401 from spiffworkflow_backend.services.file_system_service import FileSystemService -from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError, ProcessModelTestRunner +from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner class TestProcessModelTestRunner(BaseTest): @@ -20,7 +21,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests('basic_script_task') + process_model_test_runner = self._run_model_tests("basic_script_task") assert len(process_model_test_runner.test_case_results) == 1 def test_will_raise_if_no_tests_found( @@ -29,9 +30,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = ProcessModelTestRunner( - os.path.join(FileSystemService.root_path(), "DNE") - ) + process_model_test_runner = ProcessModelTestRunner(os.path.join(FileSystemService.root_path(), "DNE")) with pytest.raises(NoTestCasesFoundError): process_model_test_runner.run() assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results @@ -51,7 +50,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(parent_directory='failing_tests') + process_model_test_runner = self._run_model_tests(parent_directory="failing_tests") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_call_activity( @@ -60,7 +59,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_call_activity') + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_call_activity") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_with_service_task( @@ -69,22 +68,26 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name='basic_service_task') + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_service_task") assert len(process_model_test_runner.test_case_results) == 1 - def _run_model_tests(self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = 'passing_tests') -> ProcessModelTestRunner: + def _run_model_tests( + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "passing_tests" + ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments if bpmn_process_directory_name: path_segments = path_segments + [bpmn_process_directory_name] process_model_test_runner = ProcessModelTestRunner( process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), - process_model_directory_for_test_discovery=os.path.join(*path_segments) + process_model_directory_for_test_discovery=os.path.join(*path_segments), ) process_model_test_runner.run() - all_tests_expected_to_pass = parent_directory == 'passing_tests' - assert process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass, process_model_test_runner.test_case_results + all_tests_expected_to_pass = parent_directory == "passing_tests" + assert ( + process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass + ), process_model_test_runner.failing_tests_formatted() return process_model_test_runner @pytest.fixture() From 101ed1d46fc4a3e0ab4800dbe433d3b1b3dab7ee Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 17:16:58 -0400 Subject: [PATCH 08/16] cleaned up runner tests and rsyncd back to sample w/ burnettk --- spiffworkflow-backend/pyproject.toml | 2 +- .../process_model_test_runner_service.py | 38 ++++-- .../failing_script_task.bpmn} | 0 .../failing_script_task}/process_model.json | 0 .../test_failing_script_task.json | 3 + .../call-activity/call_activity.bpmn} | 4 +- .../call-activity/process_model.json | 9 ++ .../call-activity/test_call_activity.json} | 2 +- .../choose-your-branch-schema.json | 11 ++ .../choose-your-branch-uischema.json | 5 + .../exclusive_gateway_based_on_user_task.bpmn | 98 ++++++++++++++++ .../process_model.json | 11 ++ ..._exclusive_gateway_based_on_user_task.json | 22 ++++ .../loopback_to_user_task.bpmn | 110 ++++++++++++++++++ .../loopback-to-user-task/process_model.json | 11 ++ .../test_loopback_to_user_task.json | 13 +++ .../user-input-schema.json | 11 ++ .../user-input-uischema.json | 5 + .../expected-to-pass/loopback/loopback.bpmn | 92 +++++++++++++++ .../loopback/process_model.json | 11 ++ .../loopback/test_loopback.json | 5 + .../manual-task/manual_task.bpmn} | 4 +- .../manual-task/process_model.json | 9 ++ .../manual-task/test_manual_task.json} | 2 +- .../expected-to-pass/process_group.json | 9 ++ .../script-task}/process_model.json | 0 .../script-task/script_task.bpmn} | 0 .../script-task/test_script_task.json} | 2 +- .../service-task}/process_model.json | 0 .../service-task/service_task.bpmn} | 10 +- .../service-task/test_service_task.json} | 4 +- .../test_basic_failing_script_task.json | 3 - .../basic_call_activity/process_model.json | 9 -- .../basic_manual_task/process_model.json | 9 -- .../unit/test_process_model_test_runner.py | 21 +++- 35 files changed, 492 insertions(+), 53 deletions(-) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn => expected-to-fail/failing_script_task/failing_script_task.bpmn} (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{failing_tests/basic_failing_script_task => expected-to-fail/failing_script_task}/process_model.json (100%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_call_activity/basic_call_activity.bpmn => expected-to-pass/call-activity/call_activity.bpmn} (95%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_call_activity/test_basic_call_activity.json => expected-to-pass/call-activity/test_call_activity.json} (65%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_manual_task/basic_manual_task.bpmn => expected-to-pass/manual-task/manual_task.bpmn} (93%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_manual_task/test_basic_manual_task.json => expected-to-pass/manual-task/test_manual_task.json} (84%) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task => expected-to-pass/script-task}/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task/basic_script_task.bpmn => expected-to-pass/script-task/script_task.bpmn} (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_script_task/test_basic_script_task.json => expected-to-pass/script-task/test_script_task.json} (69%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task => expected-to-pass/service-task}/process_model.json (100%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task/basic_service_task.bpmn => expected-to-pass/service-task/service_task.bpmn} (89%) rename spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/{passing_tests/basic_service_task/test_basic_service_task.json => expected-to-pass/service-task/test_service_task.json} (69%) delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json delete mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index d0f9edfe5..39c812229 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -115,7 +115,7 @@ sphinx-click = "^4.3.0" Pygments = "^2.10.0" pyupgrade = "^3.1.0" furo = ">=2021.11.12" -myst-parser = "^0.15.1" +# myst-parser = "^0.15.1" [tool.poetry.scripts] spiffworkflow-backend = "spiffworkflow_backend.__main__:main" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 1e0ea82a1..47875e10a 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -39,6 +39,10 @@ class NoTestCasesFoundError(Exception): pass +class MissingInputTaskData(Exception): + pass + + @dataclass class TestCaseResult: passed: bool @@ -115,13 +119,11 @@ class ProcessModelTestRunner: def failing_tests_formatted(self) -> str: formatted_tests = ["FAILING TESTS:"] for failing_test in self.failing_tests(): - msg = '' + msg = "" if failing_test.error_messages: - msg = '\n\t\t'.join(failing_test.error_messages) - formatted_tests.append( - f'\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}' - ) - return '\n'.join(formatted_tests) + msg = "\n\t\t".join(failing_test.error_messages) + formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}") + return "\n".join(formatted_tests) def run(self) -> None: if len(self.test_mappings.items()) < 1: @@ -133,10 +135,11 @@ class ProcessModelTestRunner: json_file_contents = json.loads(f.read()) for test_case_name, test_case_contents in json_file_contents.items(): + self.task_data_index = {} try: self.run_test_case(bpmn_file, test_case_name, test_case_contents) except Exception as ex: - ex_as_array = str(ex).split('\n') + ex_as_array = str(ex).split("\n") self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: @@ -224,7 +227,9 @@ class ProcessModelTestRunner: bpmn_process_identifier = bpmn_process_element.attrib["id"] self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm - def _execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + def _execute_task( + self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + ) -> None: if self.execute_task_callback: self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) self._default_execute_task(spiff_task, test_case_task_key, test_case_task_properties) @@ -245,12 +250,21 @@ class ProcessModelTestRunner: return ready_tasks[0] return None - def _default_execute_task(self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict]) -> None: + def _default_execute_task( + self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + ) -> None: if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": if test_case_task_properties and "data" in test_case_task_properties: if test_case_task_key not in self.task_data_index: self.task_data_index[test_case_task_key] = 0 - spiff_task.update_data(test_case_task_properties["data"][self.task_data_index[test_case_task_key]]) + task_data_length = len(test_case_task_properties["data"]) + test_case_index = self.task_data_index[test_case_task_key] + if task_data_length <= test_case_index: + raise MissingInputTaskData( + f"Missing input task data for task: {test_case_task_key}. " + f"Only {task_data_length} given in the json but task was called {test_case_index + 1} times" + ) + spiff_task.update_data(test_case_task_properties["data"][test_case_index]) self.task_data_index[test_case_task_key] += 1 spiff_task.complete() else: @@ -297,7 +311,9 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _add_test_result(self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None) -> None: + def _add_test_result( + self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None + ) -> None: bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/failing_script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/basic_failing_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/failing_script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json new file mode 100644 index 000000000..0c81e0724 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-fail/failing_script_task/test_failing_script_task.json @@ -0,0 +1,3 @@ +{ + "test_case_2": {} +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn similarity index 95% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn index f837163f5..68a05f280 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/basic_call_activity.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/call_activity.bpmn @@ -1,6 +1,6 @@ - + Flow_0ext5lt @@ -9,7 +9,7 @@ Flow_1hzwssi - + Flow_0ext5lt Flow_1hzwssi diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json new file mode 100644 index 000000000..302fa24a1 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "", + "display_name": "Call Activity", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "call_activity.bpmn", + "primary_process_id": "CallActivityProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json similarity index 65% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json index 0a7f76929..60e638a97 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/test_basic_call_activity.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/call-activity/test_call_activity.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "expected_output_json": {} } } diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json new file mode 100644 index 000000000..0bfee44cb --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-schema.json @@ -0,0 +1,11 @@ +{ + "title": "Choose Your Branch", + "description": "", + "properties": { + "branch": { + "type": "string", + "title": "branch" + } + }, + "required": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json new file mode 100644 index 000000000..42449285d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/choose-your-branch-uischema.json @@ -0,0 +1,5 @@ +{ + "ui:order": [ + "branch" + ] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn new file mode 100644 index 000000000..63bafd46c --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/exclusive_gateway_based_on_user_task.bpmn @@ -0,0 +1,98 @@ + + + + + Flow_19j3jcx + + + + Flow_0qa66xz + Flow_1ww41l3 + Flow_10m4g0q + + + + branch == 'a' + + + + Flow_1oxbb75 + Flow_1ck9lfk + + + + + + + + + + + Flow_19j3jcx + Flow_0qa66xz + + + Flow_1ww41l3 + Flow_1ck9lfk + chosen_branch = 'A' + + + Flow_10m4g0q + Flow_1oxbb75 + chosen_branch = 'B' + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json new file mode 100644 index 000000000..448105102 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Exclusive Gateway Based on User Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "exclusive_gateway_based_on_user_task.bpmn", + "primary_process_id": "exclusive_gateway_based_on_user_task_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json new file mode 100644 index 000000000..e5d86ba05 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/exclusive-gateway-based-on-user-task/test_exclusive_gateway_based_on_user_task.json @@ -0,0 +1,22 @@ +{ + "test_case_one": { + "tasks": { + "user_task_choose_branch": { + "data": [ + { "branch": "a" } + ] + } + }, + "expected_output_json": { "branch": "a", "chosen_branch": "A"} + }, + "test_case_two": { + "tasks": { + "user_task_choose_branch": { + "data": [ + { "branch": "b" } + ] + } + }, + "expected_output_json": { "branch": "b", "chosen_branch": "B"} + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn new file mode 100644 index 000000000..0298a1d65 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/loopback_to_user_task.bpmn @@ -0,0 +1,110 @@ + + + + + Flow_12xxe7w + + + + Flow_1s3znr2 + Flow_0utss6p + Flow_1sg0c65 + + + + Flow_0utss6p + + + counter == 3 + + + Flow_12xxe7w + Flow_08tc3r7 + counter = 1 +the_var = 0 + + + + + + + + + + + + Flow_08tc3r7 + Flow_1sg0c65 + Flow_0wnc5ju + + + + Flow_0wnc5ju + Flow_1s3znr2 + the_var = user_input_variable + the_var +counter += 1 + + + loop back if a < 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json new file mode 100644 index 000000000..06e8d935d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Loopback to User Task", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "loopback_to_user_task.bpmn", + "primary_process_id": "loopback_to_user_task_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json new file mode 100644 index 000000000..b78fc373d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/test_loopback_to_user_task.json @@ -0,0 +1,13 @@ +{ + "test_case_one": { + "tasks": { + "user_task_enter_increment": { + "data": [ + { "user_input_variable": 7 }, + { "user_input_variable": 8 } + ] + } + }, + "expected_output_json": { "the_var": 15, "counter": 3, "user_input_variable": 8 } + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json new file mode 100644 index 000000000..b85521a4f --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-schema.json @@ -0,0 +1,11 @@ +{ + "title": "User Input", + "description": "", + "properties": { + "user_input_variable": { + "type": "integer", + "title": "user_input_variable" + } + }, + "required": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json new file mode 100644 index 000000000..5015ba504 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback-to-user-task/user-input-uischema.json @@ -0,0 +1,5 @@ +{ + "ui:order": [ + "user_input_variable" + ] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn new file mode 100644 index 000000000..0d742ca72 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/loopback.bpmn @@ -0,0 +1,92 @@ + + + + + Flow_12xxe7w + + + + Flow_0wnc5ju + Flow_0utss6p + Flow_1sg0c65 + + + + Flow_0utss6p + + + a == 3 + + + Flow_12xxe7w + Flow_08tc3r7 + a = 1 + + + + + + + Flow_08tc3r7 + Flow_1sg0c65 + Flow_0wnc5ju + a += 1 + + + + loop back if a < 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json new file mode 100644 index 000000000..389bfeb4d --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Loopback", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "loopback.bpmn", + "primary_process_id": "loopback_process" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json new file mode 100644 index 000000000..336551376 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/loopback/test_loopback.json @@ -0,0 +1,5 @@ +{ + "test_case_1": { + "expected_output_json": { "a": 3 } + } +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn similarity index 93% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn index 5d0bf395a..3ee85d4fc 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/basic_manual_task.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/manual_task.bpmn @@ -1,6 +1,6 @@ - + Flow_0gz6i84 @@ -15,7 +15,7 @@ - + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json new file mode 100644 index 000000000..f843745dd --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/process_model.json @@ -0,0 +1,9 @@ +{ + "description": "Manual Task", + "display_name": "Manual Task", + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "primary_file_name": "manual_task.bpmn", + "primary_process_id": "ManualTaskProcess" +} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json similarity index 84% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json index fab44ab7b..889af9374 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/test_basic_manual_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/manual-task/test_manual_task.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "tasks": { "manual_task_one": { "data": [{}] diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json new file mode 100644 index 000000000..9f562085c --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/process_group.json @@ -0,0 +1,9 @@ +{ + "admin": false, + "description": "", + "display_name": "Expected To Pass", + "display_order": 0, + "parent_groups": null, + "process_groups": [], + "process_models": [] +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/script_task.bpmn similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/basic_script_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/script_task.bpmn diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json similarity index 69% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json index 8eb2df13d..98ff465b1 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_script_task/test_basic_script_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/script-task/test_script_task.json @@ -1,5 +1,5 @@ { - "test_case_one": { + "test_case_1": { "expected_output_json": { "a": 1 } } } diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/process_model.json similarity index 100% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/process_model.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/process_model.json diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn similarity index 89% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn index 8675d5910..178c16dae 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/basic_service_task.bpmn +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/service_task.bpmn @@ -1,6 +1,6 @@ - + Flow_19ephzh @@ -14,10 +14,10 @@ - + - - + + This is the Service Task Unit Test Screen. @@ -28,7 +28,7 @@ - + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json similarity index 69% rename from spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json rename to spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json index da0b47a11..7b5aae53a 100644 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_service_task/test_basic_service_task.json +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/service-task/test_service_task.json @@ -1,7 +1,7 @@ { - "test_case_one": { + "test_case_1": { "tasks": { - "BasicServiceTaskProcess:service_task_one": { + "ServiceTaskProcess:service_task_one": { "data": [{ "the_result": "result_from_service" }] } }, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json deleted file mode 100644 index 553606003..000000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/failing_tests/basic_failing_script_task/test_basic_failing_script_task.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "test_case_two": {} -} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json deleted file mode 100644 index 8f4e4a044..000000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_call_activity/process_model.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "", - "display_name": "Basic Call Activity", - "exception_notification_addresses": [], - "fault_or_suspend_on_exception": "fault", - "files": [], - "primary_file_name": "basic_call_activity.bpmn", - "primary_process_id": "BasicCallActivityProcess" -} diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json deleted file mode 100644 index 743dd104d..000000000 --- a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/passing_tests/basic_manual_task/process_model.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "description": "Baisc Manual Task", - "display_name": "Baisc Manual Task", - "exception_notification_addresses": [], - "fault_or_suspend_on_exception": "fault", - "files": [], - "primary_file_name": "baisc_manual_task.bpmn", - "primary_process_id": "BasicManualTaskProcess" -} diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 80db51916..53abc61c7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -21,7 +21,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests("basic_script_task") + process_model_test_runner = self._run_model_tests("script-task") assert len(process_model_test_runner.test_case_results) == 1 def test_will_raise_if_no_tests_found( @@ -50,7 +50,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(parent_directory="failing_tests") + process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_call_activity( @@ -59,7 +59,7 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_call_activity") + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="call-activity") assert len(process_model_test_runner.test_case_results) == 1 def test_can_test_process_model_with_service_task( @@ -68,11 +68,20 @@ class TestProcessModelTestRunner(BaseTest): with_db_and_bpmn_file_cleanup: None, with_mocked_root_path: Any, ) -> None: - process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="basic_service_task") + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="service-task") + assert len(process_model_test_runner.test_case_results) == 1 + + def test_can_test_process_model_with_loopback_to_user_task( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="loopback-to-user-task") assert len(process_model_test_runner.test_case_results) == 1 def _run_model_tests( - self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "passing_tests" + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass" ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments @@ -84,7 +93,7 @@ class TestProcessModelTestRunner(BaseTest): ) process_model_test_runner.run() - all_tests_expected_to_pass = parent_directory == "passing_tests" + all_tests_expected_to_pass = parent_directory == "expected-to-pass" assert ( process_model_test_runner.all_test_cases_passed() is all_tests_expected_to_pass ), process_model_test_runner.failing_tests_formatted() From c6d2853b51193db787be5533df4ae9a78da145ef Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 17:36:07 -0400 Subject: [PATCH 09/16] added api to run process model unit tests w/ burnettk --- .../src/spiffworkflow_backend/api.yml | 42 +++++++++ .../routes/process_models_controller.py | 25 +++++ .../process_model_test_runner_service.py | 81 ++++++++++------ .../multiple-test-files/a.bpmn | 39 ++++++++ .../multiple-test-files/b.bpmn | 42 +++++++++ .../multiple-test-files/process_model.json | 11 +++ .../multiple-test-files/test_a.json | 5 + .../multiple-test-files/test_b.json | 8 ++ .../unit/test_process_model_test_runner.py | 23 ++++- .../src/components/ProcessModelTestRun.tsx | 93 +++++++++++++++++++ .../src/hooks/UriListForPermissions.tsx | 1 + spiffworkflow-frontend/src/index.css | 6 ++ .../src/routes/ProcessModelShow.tsx | 27 ++++-- 13 files changed, 365 insertions(+), 38 deletions(-) create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json create mode 100644 spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json create mode 100644 spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml index fd308f44e..fed169ec6 100755 --- a/spiffworkflow-backend/src/spiffworkflow_backend/api.yml +++ b/spiffworkflow-backend/src/spiffworkflow_backend/api.yml @@ -451,6 +451,48 @@ paths: schema: $ref: "#/components/schemas/ProcessModel" + /process-model-tests/{modified_process_model_identifier}: + parameters: + - name: modified_process_model_identifier + in: path + required: true + description: The process_model_id, modified to replace slashes (/) + schema: + type: string + - name: test_case_file + in: query + required: false + description: The name of the test case file to run + schema: + type: string + - name: test_case_identifier + in: query + required: false + description: The name of the test case file to run + schema: + type: string + post: + operationId: spiffworkflow_backend.routes.process_models_controller.process_model_test_run + summary: Run a test for a process model + tags: + - Process Model Tests + requestBody: + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "201": + description: Metadata about the uploaded file, but not the file content. + content: + application/json: + schema: + $ref: "#/components/schemas/File" + /process-models/{modified_process_model_identifier}/files: parameters: - name: modified_process_model_identifier diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py index 00d82639b..81e64dce2 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/process_models_controller.py @@ -1,5 +1,7 @@ """APIs for dealing with process groups, process models, and process instances.""" import json +from spiffworkflow_backend.services.file_system_service import FileSystemService +from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner import os import re from hashlib import sha256 @@ -314,6 +316,29 @@ def process_model_file_show(modified_process_model_identifier: str, file_name: s return make_response(jsonify(file), 200) +def process_model_test_run( + modified_process_model_identifier: str, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, +) -> flask.wrappers.Response: + process_model_identifier = modified_process_model_identifier.replace(":", "/") + process_model = _get_process_model(process_model_identifier) + process_model_test_runner = ProcessModelTestRunner( + process_model_directory_path=FileSystemService.root_path(), + process_model_directory_for_test_discovery=FileSystemService.full_path_from_id(process_model.id), + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, + ) + process_model_test_runner.run() + + response_json = { + "all_passed": process_model_test_runner.all_test_cases_passed(), + "passing": process_model_test_runner.passing_tests(), + "failing": process_model_test_runner.failing_tests(), + } + return make_response(jsonify(response_json), 200) + + # { # "natural_language_text": "Create a bug tracker process model \ # with a bug-details form that collects summary, description, and priority" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 47875e10a..01057d449 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -47,7 +47,7 @@ class MissingInputTaskData(Exception): class TestCaseResult: passed: bool bpmn_file: str - test_case_name: str + test_case_identifier: str error_messages: Optional[list[str]] = None @@ -90,6 +90,8 @@ class ProcessModelTestRunner: instantiate_executer_callback: Optional[Callable[[str], Any]] = None, execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, ) -> None: self.process_model_directory_path = process_model_directory_path self.process_model_directory_for_test_discovery = ( @@ -98,6 +100,8 @@ class ProcessModelTestRunner: self.instantiate_executer_callback = instantiate_executer_callback self.execute_task_callback = execute_task_callback self.get_next_task_callback = get_next_task_callback + self.test_case_file = test_case_file + self.test_case_identifier = test_case_identifier # keep track of the current task data index self.task_data_index: dict[str, int] = {} @@ -116,13 +120,16 @@ class ProcessModelTestRunner: def failing_tests(self) -> list[TestCaseResult]: return [t for t in self.test_case_results if t.passed is False] + def passing_tests(self) -> list[TestCaseResult]: + return [t for t in self.test_case_results if t.passed is True] + def failing_tests_formatted(self) -> str: formatted_tests = ["FAILING TESTS:"] for failing_test in self.failing_tests(): msg = "" if failing_test.error_messages: msg = "\n\t\t".join(failing_test.error_messages) - formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_name}: {msg}") + formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_identifier}: {msg}") return "\n".join(formatted_tests) def run(self) -> None: @@ -134,15 +141,16 @@ class ProcessModelTestRunner: with open(json_test_case_file) as f: json_file_contents = json.loads(f.read()) - for test_case_name, test_case_contents in json_file_contents.items(): - self.task_data_index = {} - try: - self.run_test_case(bpmn_file, test_case_name, test_case_contents) - except Exception as ex: - ex_as_array = str(ex).split("\n") - self._add_test_result(False, bpmn_file, test_case_name, ex_as_array) + for test_case_identifier, test_case_contents in json_file_contents.items(): + if self.test_case_identifier is None or test_case_identifier == self.test_case_identifier: + self.task_data_index = {} + try: + self.run_test_case(bpmn_file, test_case_identifier, test_case_contents) + except Exception as ex: + ex_as_array = str(ex).split("\n") + self._add_test_result(False, bpmn_file, test_case_identifier, ex_as_array) - def run_test_case(self, bpmn_file: str, test_case_name: str, test_case_contents: dict) -> None: + def run_test_case(self, bpmn_file: str, test_case_identifier: str, test_case_contents: dict) -> None: bpmn_process_instance = self._instantiate_executer(bpmn_file) next_task = self._get_next_task(bpmn_process_instance) while next_task is not None: @@ -158,7 +166,7 @@ class ProcessModelTestRunner: task_type = next_task.task_spec.__class__.__name__ if task_type in ["ServiceTask", "UserTask", "CallActivity"] and test_case_task_properties is None: raise UnrunnableTestCaseError( - f"Cannot run test case '{test_case_name}'. It requires task data for" + f"Cannot run test case '{test_case_identifier}'. It requires task data for" f" {next_task.task_spec.bpmn_id} because it is of type '{task_type}'" ) self._execute_task(next_task, test_case_task_key, test_case_task_properties) @@ -183,7 +191,7 @@ class ProcessModelTestRunner: f"expected: {test_case_contents['expected_output_json']}", f"actual: {bpmn_process_instance.data}", ] - self._add_test_result(error_message is None, bpmn_file, test_case_name, error_message) + self._add_test_result(error_message is None, bpmn_file, test_case_identifier, error_message) def _discover_process_model_test_cases( self, @@ -196,14 +204,15 @@ class ProcessModelTestRunner: file_norm = os.path.normpath(file) file_dir = os.path.dirname(file_norm) json_file_name = os.path.basename(file_norm) - bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) - bpmn_file_path = os.path.join(file_dir, bpmn_file_name) - if os.path.isfile(bpmn_file_path): - test_mappings[file_norm] = bpmn_file_path - else: - raise MissingBpmnFileForTestCaseError( - f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" - ) + if self.test_case_file is None or json_file_name == self.test_case_file: + bpmn_file_name = re.sub(r"^test_(.*)\.json", r"\1.bpmn", json_file_name) + bpmn_file_path = os.path.join(file_dir, bpmn_file_name) + if os.path.isfile(bpmn_file_path): + test_mappings[file_norm] = bpmn_file_path + else: + raise MissingBpmnFileForTestCaseError( + f"Cannot find a matching bpmn file for test case json file: '{file_norm}'" + ) return test_mappings def _discover_process_model_processes( @@ -218,14 +227,23 @@ class ProcessModelTestRunner: with open(file_norm, "rb") as f: file_contents = f.read() etree_xml_parser = etree.XMLParser(resolve_entities=False) - root = etree.fromstring(file_contents, parser=etree_xml_parser) + + # if we cannot load process model then ignore it since it can cause errors unrelated + # to the test and if it is related, it will most likely be caught further along the test + try: + root = etree.fromstring(file_contents, parser=etree_xml_parser) + except etree.XMLSyntaxError: + continue + call_activities = root.findall(".//bpmn:callActivity", namespaces=DEFAULT_NSMAP) for call_activity in call_activities: - called_element = call_activity.attrib["calledElement"] - self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) + if "calledElement" in call_activity.attrib: + called_element = call_activity.attrib["calledElement"] + self.bpmn_files_to_called_element_mappings[file_norm].append(called_element) bpmn_process_element = root.find('.//bpmn:process[@isExecutable="true"]', namespaces=DEFAULT_NSMAP) - bpmn_process_identifier = bpmn_process_element.attrib["id"] - self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm + if bpmn_process_element is not None: + bpmn_process_identifier = bpmn_process_element.attrib["id"] + self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm def _execute_task( self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] @@ -312,13 +330,13 @@ class ProcessModelTestRunner: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) def _add_test_result( - self, passed: bool, bpmn_file: str, test_case_name: str, error_messages: Optional[list[str]] = None + self, passed: bool, bpmn_file: str, test_case_identifier: str, error_messages: Optional[list[str]] = None ) -> None: bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file) test_result = TestCaseResult( passed=passed, bpmn_file=bpmn_file_relative, - test_case_name=test_case_name, + test_case_identifier=test_case_identifier, error_messages=error_messages, ) self.test_case_results.append(test_result) @@ -329,9 +347,16 @@ class BpmnFileMissingExecutableProcessError(Exception): class ProcessModelTestRunnerService: - def __init__(self, process_model_directory_path: str) -> None: + def __init__( + self, + process_model_directory_path: str, + test_case_file: Optional[str] = None, + test_case_identifier: Optional[str] = None, + ) -> None: self.process_model_test_runner = ProcessModelTestRunner( process_model_directory_path, + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, # instantiate_executer_callback=self._instantiate_executer_callback, # execute_task_callback=self._execute_task_callback, # get_next_task_callback=self._get_next_task_callback, diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn new file mode 100644 index 000000000..6eb2e3313 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/a.bpmn @@ -0,0 +1,39 @@ + + + + + Flow_0jk46kf + + + + Flow_0pw6euz + + + + Flow_0jk46kf + Flow_0pw6euz + a = 1 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn new file mode 100644 index 000000000..33eaa6084 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/b.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1qgv480 + + + + Flow_1sbj39z + + + + Flow_1qgv480 + Flow_1sbj39z + b = 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json new file mode 100644 index 000000000..f8d1350e1 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/process_model.json @@ -0,0 +1,11 @@ +{ + "description": "", + "display_name": "Multiple Test Files", + "display_order": 0, + "exception_notification_addresses": [], + "fault_or_suspend_on_exception": "fault", + "files": [], + "metadata_extraction_paths": null, + "primary_file_name": "a.bpmn", + "primary_process_id": "ProcessA" +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json new file mode 100644 index 000000000..756b774ed --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_a.json @@ -0,0 +1,5 @@ +{ + "test_case_1": { + "expected_output_json": { "a": 1 } + } +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json new file mode 100644 index 000000000..0cc7cf5f9 --- /dev/null +++ b/spiffworkflow-backend/tests/data/bpmn_unit_test_process_models/expected-to-pass/multiple-test-files/test_b.json @@ -0,0 +1,8 @@ +{ + "test_case_1": { + "expected_output_json": { "b": 1 } + }, + "test_case_2": { + "expected_output_json": { "b": 1 } + } +} \ No newline at end of file diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 53abc61c7..c0fc92006 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -53,6 +53,24 @@ class TestProcessModelTestRunner(BaseTest): process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 + def test_can_test_process_model_with_multiple_files( + self, + app: Flask, + with_db_and_bpmn_file_cleanup: None, + with_mocked_root_path: Any, + ) -> None: + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files") + assert len(process_model_test_runner.test_case_results) == 3 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_a.json') + assert len(process_model_test_runner.test_case_results) == 1 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_b.json') + assert len(process_model_test_runner.test_case_results) == 2 + + process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files", test_case_file='test_b.json', test_case_identifier='test_case_2') + assert len(process_model_test_runner.test_case_results) == 1 + def test_can_test_process_model_call_activity( self, app: Flask, @@ -81,7 +99,8 @@ class TestProcessModelTestRunner(BaseTest): assert len(process_model_test_runner.test_case_results) == 1 def _run_model_tests( - self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass" + self, bpmn_process_directory_name: Optional[str] = None, parent_directory: str = "expected-to-pass", + test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, ) -> ProcessModelTestRunner: base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments @@ -90,6 +109,8 @@ class TestProcessModelTestRunner(BaseTest): process_model_test_runner = ProcessModelTestRunner( process_model_directory_path=os.path.join(*base_process_model_dir_path_segments), process_model_directory_for_test_discovery=os.path.join(*path_segments), + test_case_file=test_case_file, + test_case_identifier=test_case_identifier, ) process_model_test_runner.run() diff --git a/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx new file mode 100644 index 000000000..c67478579 --- /dev/null +++ b/spiffworkflow-frontend/src/components/ProcessModelTestRun.tsx @@ -0,0 +1,93 @@ +import { PlayOutline, Checkmark, Close } from '@carbon/icons-react'; +import { Button, Modal } from '@carbon/react'; +import { useState } from 'react'; +import { useUriListForPermissions } from '../hooks/UriListForPermissions'; +import HttpService from '../services/HttpService'; +import { ProcessFile } from '../interfaces'; + +type OwnProps = { + processModelFile: ProcessFile; +}; + +export default function ProcessModelTestRun({ processModelFile }: OwnProps) { + const [testCaseResults, setTestCaseResults] = useState(null); + const [showTestCaseResultsModal, setShowTestCaseResultsModal] = + useState(false); + const { targetUris } = useUriListForPermissions(); + + const onProcessModelTestRunSuccess = (result: any) => { + setTestCaseResults(result); + }; + + const processModelTestRunResultTag = () => { + if (testCaseResults) { + if (testCaseResults.all_passed) { + return ( + + ); + } + return null; + }; + return ( <> {testCaseResultsModal()} - + + {hasTestCaseFiles ? ( + + ) : null} + {processModelFilesSection()} From 5e23b0b077dd1156d5aac3ab94cf2ddc71e55132 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:50:55 -0400 Subject: [PATCH 14/16] fixed tests failing for typeguard w/ burnettk --- .../process_model_test_runner_service.py | 13 +++++++---- .../unit/test_process_model_test_runner.py | 23 ++++--------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index 4b833139a..fc683ff56 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,5 +1,6 @@ import glob import json +from typing import Union import os import re import traceback @@ -89,6 +90,8 @@ JSON file format: } } """ + + class ProcessModelTestRunner: """Generic test runner code. May move into own library at some point. @@ -257,7 +260,7 @@ class ProcessModelTestRunner: self.bpmn_processes_to_file_mappings[bpmn_process_identifier] = file_norm def _execute_task( - self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] ) -> None: if self.execute_task_callback: self.execute_task_callback(spiff_task, test_case_task_key, test_case_task_properties) @@ -280,10 +283,10 @@ class ProcessModelTestRunner: return None def _default_execute_task( - self, spiff_task: SpiffTask, test_case_task_key: str, test_case_task_properties: Optional[dict] + self, spiff_task: SpiffTask, test_case_task_key: Optional[str], test_case_task_properties: Optional[dict] ) -> None: if spiff_task.task_spec.manual or spiff_task.task_spec.__class__.__name__ == "ServiceTask": - if test_case_task_properties and "data" in test_case_task_properties: + if test_case_task_key and test_case_task_properties and "data" in test_case_task_properties: if test_case_task_key not in self.task_data_index: self.task_data_index[test_case_task_key] = 0 task_data_length = len(test_case_task_properties["data"]) @@ -309,7 +312,7 @@ class ProcessModelTestRunner: related_bpmn_files.extend(self._find_related_bpmn_files(new_file)) return related_bpmn_files - def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree.Element: + def _get_etree_from_bpmn_file(self, bpmn_file: str) -> etree._Element: data = None with open(bpmn_file, "rb") as f_handle: data = f_handle.read() @@ -340,7 +343,7 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _exception_to_test_case_error_details(self, exception: Exception) -> TestCaseErrorDetails: + def _exception_to_test_case_error_details(self, exception: Union[Exception, WorkflowTaskException]) -> TestCaseErrorDetails: error_messages = str(exception).split("\n") test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) if isinstance(exception, WorkflowTaskException): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index 094cf777e..bdf8633a7 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -1,15 +1,12 @@ import os -from typing import Any +# from typing import Any from typing import Optional import pytest from flask import current_app from flask import Flask -from pytest_mock import MockerFixture from tests.spiffworkflow_backend.helpers.base_test import BaseTest -from spiffworkflow_backend.models.task import TaskModel # noqa: F401 -from spiffworkflow_backend.services.file_system_service import FileSystemService from spiffworkflow_backend.services.process_model_test_runner_service import NoTestCasesFoundError from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner @@ -19,7 +16,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests("script-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -28,9 +24,8 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: - process_model_test_runner = ProcessModelTestRunner(os.path.join(FileSystemService.root_path(), "DNE")) + process_model_test_runner = ProcessModelTestRunner(os.path.join(self.root_path(), "DNE")) with pytest.raises(NoTestCasesFoundError): process_model_test_runner.run() assert process_model_test_runner.all_test_cases_passed(), process_model_test_runner.test_case_results @@ -39,7 +34,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests() assert len(process_model_test_runner.test_case_results) > 1 @@ -48,7 +42,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(parent_directory="expected-to-fail") assert len(process_model_test_runner.test_case_results) == 1 @@ -57,7 +50,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="multiple-test-files") assert len(process_model_test_runner.test_case_results) == 3 @@ -83,7 +75,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="call-activity") assert len(process_model_test_runner.test_case_results) == 1 @@ -92,7 +83,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="service-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -101,7 +91,6 @@ class TestProcessModelTestRunner(BaseTest): self, app: Flask, with_db_and_bpmn_file_cleanup: None, - with_mocked_root_path: Any, ) -> None: process_model_test_runner = self._run_model_tests(bpmn_process_directory_name="loopback-to-user-task") assert len(process_model_test_runner.test_case_results) == 1 @@ -113,7 +102,7 @@ class TestProcessModelTestRunner(BaseTest): test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, ) -> ProcessModelTestRunner: - base_process_model_dir_path_segments = [FileSystemService.root_path(), parent_directory] + base_process_model_dir_path_segments = [self.root_path(), parent_directory] path_segments = base_process_model_dir_path_segments if bpmn_process_directory_name: path_segments = path_segments + [bpmn_process_directory_name] @@ -131,9 +120,8 @@ class TestProcessModelTestRunner(BaseTest): ), process_model_test_runner.failing_tests_formatted() return process_model_test_runner - @pytest.fixture() - def with_mocked_root_path(self, mocker: MockerFixture) -> None: - path = os.path.join( + def root_path(self) -> str: + return os.path.join( current_app.instance_path, "..", "..", @@ -141,4 +129,3 @@ class TestProcessModelTestRunner(BaseTest): "data", "bpmn_unit_test_process_models", ) - mocker.patch.object(FileSystemService, attribute="root_path", return_value=path) From 6b41229b7d53a8f2a1280430e0f76fa03ce9ea1b Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:55:27 -0400 Subject: [PATCH 15/16] pyl w/ burnettk --- .../services/process_model_test_runner_service.py | 8 +++++--- .../unit/test_process_model_test_runner.py | 1 - 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py index fc683ff56..db5a0d449 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_model_test_runner_service.py @@ -1,6 +1,5 @@ import glob import json -from typing import Union import os import re import traceback @@ -8,6 +7,7 @@ from dataclasses import dataclass from typing import Any from typing import Callable from typing import Optional +from typing import Union from lxml import etree # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskException # type: ignore @@ -103,7 +103,7 @@ class ProcessModelTestRunner: process_model_directory_path: str, process_model_directory_for_test_discovery: Optional[str] = None, instantiate_executer_callback: Optional[Callable[[str], Any]] = None, - execute_task_callback: Optional[Callable[[Any, str, Optional[dict]], Any]] = None, + execute_task_callback: Optional[Callable[[Any, Optional[str], Optional[dict]], Any]] = None, get_next_task_callback: Optional[Callable[[Any], Any]] = None, test_case_file: Optional[str] = None, test_case_identifier: Optional[str] = None, @@ -343,7 +343,9 @@ class ProcessModelTestRunner: def _get_relative_path_of_bpmn_file(self, bpmn_file: str) -> str: return os.path.relpath(bpmn_file, start=self.process_model_directory_path) - def _exception_to_test_case_error_details(self, exception: Union[Exception, WorkflowTaskException]) -> TestCaseErrorDetails: + def _exception_to_test_case_error_details( + self, exception: Union[Exception, WorkflowTaskException] + ) -> TestCaseErrorDetails: error_messages = str(exception).split("\n") test_case_error_details = TestCaseErrorDetails(error_messages=error_messages) if isinstance(exception, WorkflowTaskException): diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py index bdf8633a7..78e6b2e1e 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model_test_runner.py @@ -1,5 +1,4 @@ import os -# from typing import Any from typing import Optional import pytest From 96d7c3214107a4523a2d5c7b83f8472102a6124d Mon Sep 17 00:00:00 2001 From: burnettk Date: Tue, 23 May 2023 18:07:57 -0400 Subject: [PATCH 16/16] add docs on BPMN unit tests --- docs/how_to/bpmn_unit_tests.md | 48 +++++++++++++++++++++++++++++++++ docs/index.md | 7 +++-- docs/quick_start/quick_start.md | 2 +- 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 docs/how_to/bpmn_unit_tests.md diff --git a/docs/how_to/bpmn_unit_tests.md b/docs/how_to/bpmn_unit_tests.md new file mode 100644 index 000000000..5f1c9547d --- /dev/null +++ b/docs/how_to/bpmn_unit_tests.md @@ -0,0 +1,48 @@ +# BPMN Unit Tests + +Software Engineers test their code. +With this feature, BPMN authors can test their creations, too. +These tests can provide you with faster feedback than you would get by simply running your process model, and they allow you to mock out form input and service task connections as well as provide specific input to exercise different branches of your process model. +BPMN unit tests are designed to give you greater confidence that your process models will work as designed when they are run in the wild, both the first time they are used by real users and also after you make changes to them. + +## Creating BPMN Unit Tests + +First, create a process model that you want to test. +Navigate to the process model and add a JSON file based on the name of one of the BPMN files. +For example, if you have a process model that includes a file called `awesome_script_task.bpmn`, your test JSON file would be called `test_awesome_script_task.json`. +If you have multiple BPMN files you want to test, you can have multiple test JSON files. +The BPMN files you test do not have to be marked as the primary file for the process model in question. +The structure of your json should be as follows: + + { + "test_case_1": { + "tasks": { + "ServiceTaskProcess:service_task_one": { + "data": [{ "the_result": "result_from_service" }] + } + }, + "expected_output_json": { "the_result": "result_from_service" } + } + } + +The top-level keys should be names of unit tests. +In this example, the unit test is named "test_case_1." +Under that, you can specify "tasks" and "expected_output_json." + +Under "tasks," each key is the BPMN id of a specific task. +If you are testing a file that uses Call Activities and therefore calls other processes, there can be conflicting BPMN ids. +In this case, you can specify the unique activity by prepending the Process id (in the above example, that is "ServiceTaskProcess"). +For simple processes, "service_task_one" (for example) would be sufficient as the BPMN id. +For User Tasks, the "data" (under a specific task) represents the data that will be entered by the user in the form. +For Service Tasks, the data represents the data that will be returned by the service. +Note that all User Tasks and Service Tasks must have their BPMN ids mentioned in the JSON file (with mock task data as desired), since otherwise we won't know what to do when the flow arrives at these types of tasks. + +The "expected_output_json" represents the state of the task data that you expect when the process completes. +When the test is run, if the actual task data differs from this expectation, the test will fail. +The test will also fail if the process never completes or if an error occurs. + +## Running BPMN Unit Tests + +Go to a process model and either click “Run Unit Tests” to run all tests for the process model or click on the “play icon” next to a "test_something.json" file. +Then you will get a green check mark or a red x. +You can click on these colored icons to get more details about the passing or failing test. diff --git a/docs/index.md b/docs/index.md index c5442c2ee..599b6d71f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,16 @@ -Welcome to SpiffWorkflow's documentation! -======================================= +# Welcome to SpiffWorkflow's documentation ```{toctree} :maxdepth: 2 :caption: Contents quick_start/quick_start.md documentation/documentation.md +how_to/bpmn_unit_tests.md ``` This is great! -Indices and tables -================== +## Indices and tables * [](genindex) * [](modindex) diff --git a/docs/quick_start/quick_start.md b/docs/quick_start/quick_start.md index 0ac4c51bf..38111ddd2 100644 --- a/docs/quick_start/quick_start.md +++ b/docs/quick_start/quick_start.md @@ -307,4 +307,4 @@ Ensure that all required details have been included such as Process name, Proces By following these steps, you can request the special permissions needed to carry out your tasks effectively. -Changes added by Usama \ No newline at end of file +Changes added by Usama