From c2083103e45af10c6c4370769e4adf8ca3536ede Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:24:22 -0400 Subject: [PATCH 01/15] 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 2f98891489a55b8db7edb54ef07dcac6c83c2f78 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 16 May 2023 17:32:53 -0400 Subject: [PATCH 02/15] 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 3d35dc6213e2b7008d811abb89ffcc0cc72adca6 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:01:11 -0400 Subject: [PATCH 03/15] 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 1cd2a794ebb6c46a848358643fbaae9808bb7b37 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 10:16:09 -0400 Subject: [PATCH 04/15] 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 acaf3a3c24a714c80260bec475a5981397ed1b98 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 16:35:04 -0400 Subject: [PATCH 05/15] 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 40c67f000ca119a11df4ce7cd9e7b4f8c9a2f5a0 Mon Sep 17 00:00:00 2001 From: jasquat Date: Wed, 17 May 2023 17:28:51 -0400 Subject: [PATCH 06/15] 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 0bd16283fce2763e29bf26e6d06160819542838f Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 15:11:30 -0400 Subject: [PATCH 07/15] 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 de24d76c9de8328917d3a39f111e181846bafc78 Mon Sep 17 00:00:00 2001 From: jasquat Date: Thu, 18 May 2023 17:16:58 -0400 Subject: [PATCH 08/15] 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 4ed43e505877cbc838a3e9a52ee13d82f62bf450 Mon Sep 17 00:00:00 2001 From: jasquat Date: Mon, 22 May 2023 17:36:07 -0400 Subject: [PATCH 09/15] 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 ac73ee47f6db82a2668ad2d47a05e13116a825ad Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:50:55 -0400 Subject: [PATCH 14/15] 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 5e25e591ae8acc8d3f06ba8f097dbda2f878c422 Mon Sep 17 00:00:00 2001 From: jasquat Date: Tue, 23 May 2023 15:55:27 -0400 Subject: [PATCH 15/15] 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