Merge pull request #275 from sartography/feature/process_model_unit_tests
Feature/process model unit tests
This commit is contained in:
commit
2db6b10b7d
|
@ -23,6 +23,11 @@ if [[ -z "${SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR:-}" ]]; then
|
||||||
export SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR
|
export SPIFFWORKFLOW_BACKEND_BPMN_SPEC_ABSOLUTE_DIR
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
database_host="localhost"
|
||||||
|
if [[ -n "${SPIFFWORKFLOW_BACKEND_DATABASE_URI:-}" ]]; then
|
||||||
|
database_host=$(grep -oP "^[^:]+://.*@\K(.+?)[:/]" <<<"$SPIFFWORKFLOW_BACKEND_DATABASE_URI" | sed -E 's/[:\/]$//')
|
||||||
|
fi
|
||||||
|
|
||||||
tasks=""
|
tasks=""
|
||||||
if [[ "${1:-}" == "clean" ]]; then
|
if [[ "${1:-}" == "clean" ]]; then
|
||||||
subcommand="${2:-}"
|
subcommand="${2:-}"
|
||||||
|
@ -37,8 +42,8 @@ if [[ "${1:-}" == "clean" ]]; then
|
||||||
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" != "mysql" ]]; then
|
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" != "mysql" ]]; then
|
||||||
rm -f ./src/instance/*.sqlite3
|
rm -f ./src/instance/*.sqlite3
|
||||||
else
|
else
|
||||||
mysql -uroot -e "DROP DATABASE IF EXISTS spiffworkflow_backend_local_development"
|
mysql -h "$database_host" -uroot -e "DROP DATABASE IF EXISTS spiffworkflow_backend_local_development"
|
||||||
mysql -uroot -e "DROP DATABASE IF EXISTS spiffworkflow_backend_unit_testing"
|
mysql -h "$database_host" -uroot -e "DROP DATABASE IF EXISTS spiffworkflow_backend_unit_testing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# TODO: check to see if the db already exists and we can connect to it. also actually clean it up.
|
# TODO: check to see if the db already exists and we can connect to it. also actually clean it up.
|
||||||
|
@ -74,8 +79,8 @@ else
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" == "mysql" ]]; then
|
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" == "mysql" ]]; then
|
||||||
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_local_development"
|
mysql -h "$database_host" -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_local_development"
|
||||||
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_unit_testing"
|
mysql -h "$database_host" -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_unit_testing"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for task in $tasks; do
|
for task in $tasks; do
|
||||||
|
@ -85,7 +90,7 @@ done
|
||||||
SPIFFWORKFLOW_BACKEND_ENV=unit_testing FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
|
SPIFFWORKFLOW_BACKEND_ENV=unit_testing FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
|
||||||
if [[ -n "${SPIFFWORKFLOW_BACKEND_ENV:-}" ]] && ! grep -Eq '^(local_development|unit_testing)$' <<< "$SPIFFWORKFLOW_BACKEND_ENV"; then
|
if [[ -n "${SPIFFWORKFLOW_BACKEND_ENV:-}" ]] && ! grep -Eq '^(local_development|unit_testing)$' <<< "$SPIFFWORKFLOW_BACKEND_ENV"; then
|
||||||
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" == "mysql" ]]; then
|
if [[ "${SPIFFWORKFLOW_BACKEND_DATABASE_TYPE:-mysql}" == "mysql" ]]; then
|
||||||
mysql -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_$SPIFFWORKFLOW_BACKEND_ENV"
|
mysql -h "$database_host" -uroot -e "CREATE DATABASE IF NOT EXISTS spiffworkflow_backend_$SPIFFWORKFLOW_BACKEND_ENV"
|
||||||
fi
|
fi
|
||||||
FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
|
FLASK_APP=src/spiffworkflow_backend poetry run flask db upgrade
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -48,9 +48,8 @@ def with_db_and_bpmn_file_cleanup() -> None:
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
finally:
|
finally:
|
||||||
process_model_service = ProcessModelService()
|
if os.path.exists(ProcessModelService.root_path()):
|
||||||
if os.path.exists(process_model_service.root_path()):
|
shutil.rmtree(ProcessModelService.root_path())
|
||||||
shutil.rmtree(process_model_service.root_path())
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|
|
@ -451,6 +451,48 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ProcessModel"
|
$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:
|
/process-models/{modified_process_model_identifier}/files:
|
||||||
parameters:
|
parameters:
|
||||||
- name: modified_process_model_identifier
|
- name: modified_process_model_identifier
|
||||||
|
|
|
@ -10,7 +10,7 @@ from spiffworkflow_backend.services.logging_service import setup_logger
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(Exception):
|
class ConfigurationError(Exception):
|
||||||
"""ConfigurationError."""
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_database_configs(app: Flask) -> None:
|
def setup_database_configs(app: Flask) -> None:
|
||||||
|
|
|
@ -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)
|
process_group_id = _un_modify_modified_process_model_id(modified_process_group_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ProcessModelService().process_group_delete(process_group_id)
|
ProcessModelService.process_group_delete(process_group_id)
|
||||||
except ProcessModelWithInstancesNotDeletableError as exception:
|
except ProcessModelWithInstancesNotDeletableError as exception:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
error_code="existing_instances",
|
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
|
process_group_identifier: Optional[str] = None, page: int = 1, per_page: int = 100
|
||||||
) -> flask.wrappers.Response:
|
) -> flask.wrappers.Response:
|
||||||
process_groups = ProcessModelService.get_process_groups_for_api(process_group_identifier)
|
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
|
pages = len(process_groups) // per_page
|
||||||
remainder = len(process_groups) % per_page
|
remainder = len(process_groups) % per_page
|
||||||
if remainder > 0:
|
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:
|
def process_group_move(modified_process_group_identifier: str, new_location: str) -> flask.wrappers.Response:
|
||||||
"""Process_group_move."""
|
"""Process_group_move."""
|
||||||
original_process_group_id = _un_modify_modified_process_model_id(modified_process_group_identifier)
|
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(
|
_commit_and_push_to_git(
|
||||||
f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}"
|
f"User: {g.user.username} moved process group {original_process_group_id} to {new_process_group.id}"
|
||||||
)
|
)
|
||||||
|
|
|
@ -30,6 +30,7 @@ from spiffworkflow_backend.routes.process_api_blueprint import _get_process_mode
|
||||||
from spiffworkflow_backend.routes.process_api_blueprint import (
|
from spiffworkflow_backend.routes.process_api_blueprint import (
|
||||||
_un_modify_modified_process_model_id,
|
_un_modify_modified_process_model_id,
|
||||||
)
|
)
|
||||||
|
from spiffworkflow_backend.services.file_system_service import FileSystemService
|
||||||
from spiffworkflow_backend.services.git_service import GitCommandError
|
from spiffworkflow_backend.services.git_service import GitCommandError
|
||||||
from spiffworkflow_backend.services.git_service import GitService
|
from spiffworkflow_backend.services.git_service import GitService
|
||||||
from spiffworkflow_backend.services.git_service import MissingGitConfigsError
|
from spiffworkflow_backend.services.git_service import MissingGitConfigsError
|
||||||
|
@ -43,6 +44,7 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer
|
||||||
from spiffworkflow_backend.services.process_model_service import (
|
from spiffworkflow_backend.services.process_model_service import (
|
||||||
ProcessModelWithInstancesNotDeletableError,
|
ProcessModelWithInstancesNotDeletableError,
|
||||||
)
|
)
|
||||||
|
from spiffworkflow_backend.services.process_model_test_runner_service import ProcessModelTestRunner
|
||||||
from spiffworkflow_backend.services.spec_file_service import (
|
from spiffworkflow_backend.services.spec_file_service import (
|
||||||
ProcessModelFileInvalidError,
|
ProcessModelFileInvalidError,
|
||||||
)
|
)
|
||||||
|
@ -104,7 +106,7 @@ def process_model_delete(
|
||||||
"""Process_model_delete."""
|
"""Process_model_delete."""
|
||||||
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
process_model_identifier = modified_process_model_identifier.replace(":", "/")
|
||||||
try:
|
try:
|
||||||
ProcessModelService().process_model_delete(process_model_identifier)
|
ProcessModelService.process_model_delete(process_model_identifier)
|
||||||
except ProcessModelWithInstancesNotDeletableError as exception:
|
except ProcessModelWithInstancesNotDeletableError as exception:
|
||||||
raise ApiError(
|
raise ApiError(
|
||||||
error_code="existing_instances",
|
error_code="existing_instances",
|
||||||
|
@ -182,7 +184,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:
|
def process_model_move(modified_process_model_identifier: str, new_location: str) -> flask.wrappers.Response:
|
||||||
"""Process_model_move."""
|
"""Process_model_move."""
|
||||||
original_process_model_id = _un_modify_modified_process_model_id(modified_process_model_identifier)
|
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(
|
_commit_and_push_to_git(
|
||||||
f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}"
|
f"User: {g.user.username} moved process model {original_process_model_id} to {new_process_model.id}"
|
||||||
)
|
)
|
||||||
|
@ -219,7 +221,7 @@ def process_model_list(
|
||||||
recursive=recursive,
|
recursive=recursive,
|
||||||
filter_runnable_by_user=filter_runnable_by_user,
|
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:
|
if include_parent_groups:
|
||||||
process_group_cache = IdToProcessGroupMapping({})
|
process_group_cache = IdToProcessGroupMapping({})
|
||||||
|
@ -314,6 +316,29 @@ def process_model_file_show(modified_process_model_identifier: str, file_name: s
|
||||||
return make_response(jsonify(file), 200)
|
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 \
|
# "natural_language_text": "Create a bug tracker process model \
|
||||||
# with a bug-details form that collects summary, description, and priority"
|
# with a bug-details form that collects summary, description, and priority"
|
||||||
|
|
|
@ -49,13 +49,12 @@ class FileSystemService:
|
||||||
"""Id_string_to_relative_path."""
|
"""Id_string_to_relative_path."""
|
||||||
return id_string.replace("/", os.sep)
|
return id_string.replace("/", os.sep)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def process_group_path(name: str) -> str:
|
def full_path_from_id(cls, id: str) -> str:
|
||||||
"""Category_path."""
|
|
||||||
return os.path.abspath(
|
return os.path.abspath(
|
||||||
os.path.join(
|
os.path.join(
|
||||||
FileSystemService.root_path(),
|
cls.root_path(),
|
||||||
FileSystemService.id_string_to_relative_path(name),
|
cls.id_string_to_relative_path(id),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,36 +64,35 @@ class FileSystemService:
|
||||||
return os.path.join(FileSystemService.root_path(), relative_path)
|
return os.path.join(FileSystemService.root_path(), relative_path)
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
"""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:
|
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
|
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())
|
return os.path.relpath(workflow_path, start=FileSystemService.root_path())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_group_path_for_spec(spec: ProcessModelInfo) -> str:
|
def process_group_path_for_spec(process_model: ProcessModelInfo) -> str:
|
||||||
"""Category_path_for_spec."""
|
|
||||||
# os.path.split apparently returns 2 element tulple like: (first/path, last_item)
|
# os.path.split apparently returns 2 element tulple like: (first/path, last_item)
|
||||||
process_group_id, _ = os.path.split(spec.id_for_file_path())
|
process_group_id, _ = os.path.split(process_model.id_for_file_path())
|
||||||
return FileSystemService.process_group_path(process_group_id)
|
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
|
@staticmethod
|
||||||
def workflow_path(spec: ProcessModelInfo) -> str:
|
def full_path_to_process_model_file(process_model: 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:
|
|
||||||
"""Full_path_to_process_model_file."""
|
"""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."""
|
"""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):
|
if os.path.exists(path):
|
||||||
return len(next(os.walk(path))[1])
|
return len(next(os.walk(path))[1])
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -420,7 +420,6 @@ class ProcessInstanceProcessor:
|
||||||
)
|
)
|
||||||
|
|
||||||
self.process_instance_model = process_instance_model
|
self.process_instance_model = process_instance_model
|
||||||
self.process_model_service = ProcessModelService()
|
|
||||||
bpmn_process_spec = None
|
bpmn_process_spec = None
|
||||||
self.full_bpmn_process_dict: dict = {}
|
self.full_bpmn_process_dict: dict = {}
|
||||||
|
|
||||||
|
@ -1018,7 +1017,7 @@ class ProcessInstanceProcessor:
|
||||||
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
ready_or_waiting_tasks = self.get_all_ready_or_waiting_tasks()
|
||||||
|
|
||||||
process_model_display_name = ""
|
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
|
self.process_instance_model.process_model_identifier
|
||||||
)
|
)
|
||||||
if process_model_info is not None:
|
if process_model_info is not None:
|
||||||
|
|
|
@ -196,8 +196,7 @@ class ProcessInstanceService:
|
||||||
"""
|
"""
|
||||||
# navigation = processor.bpmn_process_instance.get_deep_nav_list()
|
# navigation = processor.bpmn_process_instance.get_deep_nav_list()
|
||||||
# ProcessInstanceService.update_navigation(navigation, processor)
|
# ProcessInstanceService.update_navigation(navigation, processor)
|
||||||
process_model_service = ProcessModelService()
|
ProcessModelService.get_process_model(processor.process_model_identifier)
|
||||||
process_model_service.get_process_model(processor.process_model_identifier)
|
|
||||||
process_instance_api = ProcessInstanceApi(
|
process_instance_api = ProcessInstanceApi(
|
||||||
id=processor.get_process_instance_id(),
|
id=processor.get_process_instance_id(),
|
||||||
status=processor.get_status(),
|
status=processor.get_status(),
|
||||||
|
|
|
@ -60,12 +60,7 @@ class ProcessModelService(FileSystemService):
|
||||||
def is_process_group_identifier(cls, process_group_identifier: str) -> bool:
|
def is_process_group_identifier(cls, process_group_identifier: str) -> bool:
|
||||||
"""Is_process_group_identifier."""
|
"""Is_process_group_identifier."""
|
||||||
if os.path.exists(FileSystemService.root_path()):
|
if os.path.exists(FileSystemService.root_path()):
|
||||||
process_group_path = os.path.abspath(
|
process_group_path = FileSystemService.full_path_from_id(process_group_identifier)
|
||||||
os.path.join(
|
|
||||||
FileSystemService.root_path(),
|
|
||||||
FileSystemService.id_string_to_relative_path(process_group_identifier),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return cls.is_process_group(process_group_path)
|
return cls.is_process_group(process_group_path)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -82,12 +77,7 @@ class ProcessModelService(FileSystemService):
|
||||||
def is_process_model_identifier(cls, process_model_identifier: str) -> bool:
|
def is_process_model_identifier(cls, process_model_identifier: str) -> bool:
|
||||||
"""Is_process_model_identifier."""
|
"""Is_process_model_identifier."""
|
||||||
if os.path.exists(FileSystemService.root_path()):
|
if os.path.exists(FileSystemService.root_path()):
|
||||||
process_model_path = os.path.abspath(
|
process_model_path = FileSystemService.full_path_from_id(process_model_identifier)
|
||||||
os.path.join(
|
|
||||||
FileSystemService.root_path(),
|
|
||||||
FileSystemService.id_string_to_relative_path(process_model_identifier),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return cls.is_process_model(process_model_path)
|
return cls.is_process_model(process_model_path)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -104,7 +94,6 @@ class ProcessModelService(FileSystemService):
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
per_page: int = 10,
|
per_page: int = 10,
|
||||||
) -> list[T]:
|
) -> list[T]:
|
||||||
"""Get_batch."""
|
|
||||||
start = (page - 1) * per_page
|
start = (page - 1) * per_page
|
||||||
end = start + per_page
|
end = start + per_page
|
||||||
return items[start:end]
|
return items[start:end]
|
||||||
|
@ -139,8 +128,8 @@ class ProcessModelService(FileSystemService):
|
||||||
cls.write_json_file(json_path, json_data)
|
cls.write_json_file(json_path, json_data)
|
||||||
process_model.id = process_model_id
|
process_model.id = process_model_id
|
||||||
|
|
||||||
def process_model_delete(self, process_model_id: str) -> None:
|
@classmethod
|
||||||
"""Delete Procecss Model."""
|
def process_model_delete(cls, process_model_id: str) -> None:
|
||||||
instances = ProcessInstanceModel.query.filter(
|
instances = ProcessInstanceModel.query.filter(
|
||||||
ProcessInstanceModel.process_model_identifier == process_model_id
|
ProcessInstanceModel.process_model_identifier == process_model_id
|
||||||
).all()
|
).all()
|
||||||
|
@ -148,19 +137,19 @@ class ProcessModelService(FileSystemService):
|
||||||
raise ProcessModelWithInstancesNotDeletableError(
|
raise ProcessModelWithInstancesNotDeletableError(
|
||||||
f"We cannot delete the model `{process_model_id}`, there are existing instances that depend on it."
|
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)
|
process_model = cls.get_process_model(process_model_id)
|
||||||
path = self.workflow_path(process_model)
|
path = cls.process_model_full_path(process_model)
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
|
|
||||||
def process_model_move(self, original_process_model_id: str, new_location: str) -> ProcessModelInfo:
|
@classmethod
|
||||||
"""Process_model_move."""
|
def process_model_move(cls, original_process_model_id: str, new_location: str) -> ProcessModelInfo:
|
||||||
process_model = self.get_process_model(original_process_model_id)
|
process_model = cls.get_process_model(original_process_model_id)
|
||||||
original_model_path = self.workflow_path(process_model)
|
original_model_path = cls.process_model_full_path(process_model)
|
||||||
_, model_id = os.path.split(original_model_path)
|
_, model_id = os.path.split(original_model_path)
|
||||||
new_relative_path = os.path.join(new_location, model_id)
|
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))
|
new_model_path = os.path.abspath(os.path.join(FileSystemService.root_path(), new_relative_path))
|
||||||
shutil.move(original_model_path, new_model_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
|
return new_process_model
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -314,12 +303,7 @@ class ProcessModelService(FileSystemService):
|
||||||
def get_process_group(cls, process_group_id: str, find_direct_nested_items: bool = True) -> ProcessGroup:
|
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."""
|
"""Look for a given process_group, and return it."""
|
||||||
if os.path.exists(FileSystemService.root_path()):
|
if os.path.exists(FileSystemService.root_path()):
|
||||||
process_group_path = os.path.abspath(
|
process_group_path = FileSystemService.full_path_from_id(process_group_id)
|
||||||
os.path.join(
|
|
||||||
FileSystemService.root_path(),
|
|
||||||
FileSystemService.id_string_to_relative_path(process_group_id),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if cls.is_process_group(process_group_path):
|
if cls.is_process_group(process_group_path):
|
||||||
return cls.find_or_create_process_group(
|
return cls.find_or_create_process_group(
|
||||||
process_group_path,
|
process_group_path,
|
||||||
|
@ -330,13 +314,11 @@ class ProcessModelService(FileSystemService):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
|
def add_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
|
||||||
"""Add_process_group."""
|
|
||||||
return cls.update_process_group(process_group)
|
return cls.update_process_group(process_group)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
|
def update_process_group(cls, process_group: ProcessGroup) -> ProcessGroup:
|
||||||
"""Update_process_group."""
|
cat_path = cls.full_path_from_id(process_group.id)
|
||||||
cat_path = cls.process_group_path(process_group.id)
|
|
||||||
os.makedirs(cat_path, exist_ok=True)
|
os.makedirs(cat_path, exist_ok=True)
|
||||||
json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE)
|
json_path = os.path.join(cat_path, cls.PROCESS_GROUP_JSON_FILE)
|
||||||
serialized_process_group = process_group.serialized
|
serialized_process_group = process_group.serialized
|
||||||
|
@ -346,33 +328,33 @@ class ProcessModelService(FileSystemService):
|
||||||
cls.write_json_file(json_path, serialized_process_group)
|
cls.write_json_file(json_path, serialized_process_group)
|
||||||
return process_group
|
return process_group
|
||||||
|
|
||||||
def process_group_move(self, original_process_group_id: str, new_location: str) -> ProcessGroup:
|
@classmethod
|
||||||
"""Process_group_move."""
|
def process_group_move(cls, original_process_group_id: str, new_location: str) -> ProcessGroup:
|
||||||
original_group_path = self.process_group_path(original_process_group_id)
|
original_group_path = cls.full_path_from_id(original_process_group_id)
|
||||||
_, original_group_id = os.path.split(original_group_path)
|
_, original_group_id = os.path.split(original_group_path)
|
||||||
new_root = os.path.join(FileSystemService.root_path(), new_location)
|
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))
|
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)
|
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
|
return new_process_group
|
||||||
|
|
||||||
def __get_all_nested_models(self, group_path: str) -> list:
|
@classmethod
|
||||||
"""__get_all_nested_models."""
|
def __get_all_nested_models(cls, group_path: str) -> list:
|
||||||
all_nested_models = []
|
all_nested_models = []
|
||||||
for _root, dirs, _files in os.walk(group_path):
|
for _root, dirs, _files in os.walk(group_path):
|
||||||
for dir in dirs:
|
for dir in dirs:
|
||||||
model_dir = os.path.join(group_path, dir)
|
model_dir = os.path.join(group_path, dir)
|
||||||
if ProcessModelService.is_process_model(model_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)
|
all_nested_models.append(process_model)
|
||||||
return all_nested_models
|
return all_nested_models
|
||||||
|
|
||||||
def process_group_delete(self, process_group_id: str) -> None:
|
@classmethod
|
||||||
"""Delete_process_group."""
|
def process_group_delete(cls, process_group_id: str) -> None:
|
||||||
problem_models = []
|
problem_models = []
|
||||||
path = self.process_group_path(process_group_id)
|
path = cls.full_path_from_id(process_group_id)
|
||||||
if os.path.exists(path):
|
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:
|
for process_model in nested_models:
|
||||||
instances = ProcessInstanceModel.query.filter(
|
instances = ProcessInstanceModel.query.filter(
|
||||||
ProcessInstanceModel.process_model_identifier == process_model.id
|
ProcessInstanceModel.process_model_identifier == process_model.id
|
||||||
|
@ -386,15 +368,15 @@ class ProcessModelService(FileSystemService):
|
||||||
f" {problem_models}"
|
f" {problem_models}"
|
||||||
)
|
)
|
||||||
shutil.rmtree(path)
|
shutil.rmtree(path)
|
||||||
self.cleanup_process_group_display_order()
|
cls._cleanup_process_group_display_order()
|
||||||
|
|
||||||
def cleanup_process_group_display_order(self) -> List[Any]:
|
@classmethod
|
||||||
"""Cleanup_process_group_display_order."""
|
def _cleanup_process_group_display_order(cls) -> List[Any]:
|
||||||
process_groups = self.get_process_groups() # Returns an ordered list
|
process_groups = cls.get_process_groups() # Returns an ordered list
|
||||||
index = 0
|
index = 0
|
||||||
for process_group in process_groups:
|
for process_group in process_groups:
|
||||||
process_group.display_order = index
|
process_group.display_order = index
|
||||||
self.update_process_group(process_group)
|
cls.update_process_group(process_group)
|
||||||
index += 1
|
index += 1
|
||||||
return process_groups
|
return process_groups
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,407 @@
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class UnrunnableTestCaseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingBpmnFileForTestCaseError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoTestCasesFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MissingInputTaskData(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestCaseErrorDetails:
|
||||||
|
error_messages: list[str]
|
||||||
|
task_error_line: Optional[str] = None
|
||||||
|
task_trace: Optional[list[str]] = None
|
||||||
|
task_bpmn_identifier: Optional[str] = None
|
||||||
|
task_bpmn_name: Optional[str] = None
|
||||||
|
task_line_number: Optional[int] = None
|
||||||
|
stacktrace: Optional[list[str]] = None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TestCaseResult:
|
||||||
|
passed: bool
|
||||||
|
bpmn_file: str
|
||||||
|
test_case_identifier: str
|
||||||
|
test_case_error_details: Optional[TestCaseErrorDetails] = 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",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
JSON file name:
|
||||||
|
The name should be in format "test_BPMN_FILE_NAME_IT_TESTS.json".
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
DATA for tasks:
|
||||||
|
This is an array of task data. This allows for the task to
|
||||||
|
be called multiple times and given different data each time.
|
||||||
|
This is useful for testing loops where each iteration needs
|
||||||
|
different input. The test will fail if the task is called
|
||||||
|
multiple times without task data input for each call.
|
||||||
|
|
||||||
|
JSON file format:
|
||||||
|
{
|
||||||
|
TEST_CASE_NAME: {
|
||||||
|
"tasks": {
|
||||||
|
BPMN_TASK_IDENTIIFER: {
|
||||||
|
"data": [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,
|
||||||
|
process_model_directory_for_test_discovery: Optional[str] = None,
|
||||||
|
instantiate_executer_callback: Optional[Callable[[str], 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,
|
||||||
|
) -> 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.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] = {}
|
||||||
|
|
||||||
|
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 = 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 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.test_case_error_details is not None:
|
||||||
|
msg = "\n\t\t".join(failing_test.test_case_error_details.error_messages)
|
||||||
|
formatted_tests.append(f"\t{failing_test.bpmn_file}: {failing_test.test_case_identifier}: {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}"
|
||||||
|
)
|
||||||
|
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())
|
||||||
|
|
||||||
|
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:
|
||||||
|
self._add_test_result(False, bpmn_file, test_case_identifier, exception=ex)
|
||||||
|
|
||||||
|
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:
|
||||||
|
test_case_task_properties = None
|
||||||
|
test_case_task_key = next_task.task_spec.bpmn_id
|
||||||
|
if "tasks" in test_case_contents:
|
||||||
|
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:
|
||||||
|
raise UnrunnableTestCaseError(
|
||||||
|
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)
|
||||||
|
next_task = self._get_next_task(bpmn_process_instance)
|
||||||
|
|
||||||
|
error_message = None
|
||||||
|
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_identifier, error_message)
|
||||||
|
|
||||||
|
def _discover_process_model_test_cases(
|
||||||
|
self,
|
||||||
|
) -> dict[str, str]:
|
||||||
|
test_mappings = {}
|
||||||
|
|
||||||
|
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_norm = os.path.normpath(file)
|
||||||
|
file_dir = os.path.dirname(file_norm)
|
||||||
|
json_file_name = os.path.basename(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(
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
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)
|
||||||
|
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: 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)
|
||||||
|
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:
|
||||||
|
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 _default_get_next_task(self, bpmn_process_instance: BpmnWorkflow) -> Optional[SpiffTask]:
|
||||||
|
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_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_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"])
|
||||||
|
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:
|
||||||
|
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 _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:
|
||||||
|
error_messages = str(exception).split("\n")
|
||||||
|
test_case_error_details = TestCaseErrorDetails(error_messages=error_messages)
|
||||||
|
if isinstance(exception, WorkflowTaskException):
|
||||||
|
test_case_error_details.task_error_line = exception.error_line
|
||||||
|
test_case_error_details.task_trace = exception.task_trace
|
||||||
|
test_case_error_details.task_line_number = exception.line_number
|
||||||
|
test_case_error_details.task_bpmn_identifier = exception.task_spec.bpmn_id
|
||||||
|
test_case_error_details.task_bpmn_name = exception.task_spec.bpmn_name
|
||||||
|
else:
|
||||||
|
test_case_error_details.stacktrace = traceback.format_exc().split("\n")
|
||||||
|
|
||||||
|
return test_case_error_details
|
||||||
|
|
||||||
|
def _add_test_result(
|
||||||
|
self,
|
||||||
|
passed: bool,
|
||||||
|
bpmn_file: str,
|
||||||
|
test_case_identifier: str,
|
||||||
|
error_messages: Optional[list[str]] = None,
|
||||||
|
exception: Optional[Exception] = None,
|
||||||
|
) -> None:
|
||||||
|
test_case_error_details = None
|
||||||
|
if exception is not None:
|
||||||
|
test_case_error_details = self._exception_to_test_case_error_details(exception)
|
||||||
|
elif error_messages:
|
||||||
|
test_case_error_details = TestCaseErrorDetails(error_messages=error_messages)
|
||||||
|
|
||||||
|
bpmn_file_relative = self._get_relative_path_of_bpmn_file(bpmn_file)
|
||||||
|
test_result = TestCaseResult(
|
||||||
|
passed=passed,
|
||||||
|
bpmn_file=bpmn_file_relative,
|
||||||
|
test_case_identifier=test_case_identifier,
|
||||||
|
test_case_error_details=test_case_error_details,
|
||||||
|
)
|
||||||
|
self.test_case_results.append(test_result)
|
||||||
|
|
||||||
|
|
||||||
|
class BpmnFileMissingExecutableProcessError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessModelTestRunnerService:
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
self.process_model_test_runner.run()
|
|
@ -221,37 +221,37 @@ class SpecFileService(FileSystemService):
|
||||||
return spec_file_data
|
return spec_file_data
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def full_file_path(spec: ProcessModelInfo, file_name: str) -> str:
|
def full_file_path(process_model: ProcessModelInfo, file_name: str) -> str:
|
||||||
"""File_path."""
|
"""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
|
@staticmethod
|
||||||
def last_modified(spec: ProcessModelInfo, file_name: str) -> datetime:
|
def last_modified(process_model: ProcessModelInfo, file_name: str) -> datetime:
|
||||||
"""Last_modified."""
|
"""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)
|
return FileSystemService._last_modified(full_file_path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def timestamp(spec: ProcessModelInfo, file_name: str) -> float:
|
def timestamp(process_model: ProcessModelInfo, file_name: str) -> float:
|
||||||
"""Timestamp."""
|
"""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)
|
return FileSystemService._timestamp(full_file_path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_file(spec: ProcessModelInfo, file_name: str) -> None:
|
def delete_file(process_model: ProcessModelInfo, file_name: str) -> None:
|
||||||
"""Delete_file."""
|
"""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()
|
# lookup_files = session.query(LookupFileModel).filter_by(file_model_id=file_id).all()
|
||||||
# for lf in lookup_files:
|
# for lf in lookup_files:
|
||||||
# session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete()
|
# session.query(LookupDataModel).filter_by(lookup_file_model_id=lf.id).delete()
|
||||||
# session.query(LookupFileModel).filter_by(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)
|
os.remove(full_file_path)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def delete_all_files(spec: ProcessModelInfo) -> None:
|
def delete_all_files(process_model: ProcessModelInfo) -> None:
|
||||||
"""Delete_all_files."""
|
"""Delete_all_files."""
|
||||||
dir_path = SpecFileService.workflow_path(spec)
|
dir_path = SpecFileService.process_model_full_path(process_model)
|
||||||
if os.path.exists(dir_path):
|
if os.path.exists(dir_path):
|
||||||
shutil.rmtree(dir_path)
|
shutil.rmtree(dir_path)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="Process_FailingProcess" name="Failing Process" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_1xkc1ru</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1xkc1ru" sourceRef="StartEvent_1" targetRef="Activity_FailingTask" />
|
||||||
|
<bpmn:endEvent id="Event_00iauxo">
|
||||||
|
<bpmn:incoming>Flow_0tkkq9s</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0tkkq9s" sourceRef="Activity_FailingTask" targetRef="Event_00iauxo" />
|
||||||
|
<bpmn:scriptTask id="Activity_FailingTask" name="Failing Task">
|
||||||
|
<bpmn:incoming>Flow_1xkc1ru</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0tkkq9s</bpmn:outgoing>
|
||||||
|
<bpmn:script>a = 1
|
||||||
|
b = a + 'two'</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_FailingProcess">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_00iauxo_di" bpmnElement="Event_00iauxo">
|
||||||
|
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0ecfxz2_di" bpmnElement="Activity_FailingTask">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1xkc1ru_di" bpmnElement="Flow_1xkc1ru">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0tkkq9s_di" bpmnElement="Flow_0tkkq9s">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="432" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"test_case_2": {}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="CallActivityProcess" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_0ext5lt</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0ext5lt" sourceRef="StartEvent_1" targetRef="Activity_0irfg4l" />
|
||||||
|
<bpmn:endEvent id="Event_0bz40ol">
|
||||||
|
<bpmn:incoming>Flow_1hzwssi</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1hzwssi" sourceRef="Activity_0irfg4l" targetRef="Event_0bz40ol" />
|
||||||
|
<bpmn:callActivity id="Activity_0irfg4l" name="Call Activity" calledElement="ManualTaskProcess">
|
||||||
|
<bpmn:incoming>Flow_0ext5lt</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1hzwssi</bpmn:outgoing>
|
||||||
|
</bpmn:callActivity>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_test_a42_A_4_2_bd2e724">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_0bz40ol_di" bpmnElement="Event_0bz40ol">
|
||||||
|
<dc:Bounds x="422" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0148g78_di" bpmnElement="Activity_0irfg4l">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0ext5lt_di" bpmnElement="Flow_0ext5lt">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1hzwssi_di" bpmnElement="Flow_1hzwssi">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="422" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"expected_output_json": {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"title": "Choose Your Branch",
|
||||||
|
"description": "",
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "branch"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"ui:order": [
|
||||||
|
"branch"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="exclusive_gateway_based_on_user_task_process" name="ExclusiveGatewayBasedOnUserTaskProcess" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_19j3jcx</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_19j3jcx" sourceRef="StartEvent_1" targetRef="user_task_choose_branch" />
|
||||||
|
<bpmn:exclusiveGateway id="Gateway_0xwvfep" default="Flow_10m4g0q">
|
||||||
|
<bpmn:incoming>Flow_0qa66xz</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1ww41l3</bpmn:outgoing>
|
||||||
|
<bpmn:outgoing>Flow_10m4g0q</bpmn:outgoing>
|
||||||
|
</bpmn:exclusiveGateway>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0qa66xz" sourceRef="user_task_choose_branch" targetRef="Gateway_0xwvfep" />
|
||||||
|
<bpmn:sequenceFlow id="Flow_1ww41l3" sourceRef="Gateway_0xwvfep" targetRef="script_task_branch_a">
|
||||||
|
<bpmn:conditionExpression>branch == 'a'</bpmn:conditionExpression>
|
||||||
|
</bpmn:sequenceFlow>
|
||||||
|
<bpmn:sequenceFlow id="Flow_10m4g0q" sourceRef="Gateway_0xwvfep" targetRef="script_task_branch_b" />
|
||||||
|
<bpmn:endEvent id="Event_05ovp79">
|
||||||
|
<bpmn:incoming>Flow_1oxbb75</bpmn:incoming>
|
||||||
|
<bpmn:incoming>Flow_1ck9lfk</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1oxbb75" sourceRef="script_task_branch_b" targetRef="Event_05ovp79" />
|
||||||
|
<bpmn:sequenceFlow id="Flow_1ck9lfk" sourceRef="script_task_branch_a" targetRef="Event_05ovp79" />
|
||||||
|
<bpmn:userTask id="user_task_choose_branch" name="User Task Choose Branch">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:properties>
|
||||||
|
<spiffworkflow:property name="formJsonSchemaFilename" value="choose-your-branch-schema.json" />
|
||||||
|
<spiffworkflow:property name="formUiSchemaFilename" value="choose-your-branch-uischema.json" />
|
||||||
|
</spiffworkflow:properties>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_19j3jcx</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0qa66xz</bpmn:outgoing>
|
||||||
|
</bpmn:userTask>
|
||||||
|
<bpmn:scriptTask id="script_task_branch_a" name="Script Task Branch A">
|
||||||
|
<bpmn:incoming>Flow_1ww41l3</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1ck9lfk</bpmn:outgoing>
|
||||||
|
<bpmn:script>chosen_branch = 'A'</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:scriptTask id="script_task_branch_b" name="Script Task Branch B">
|
||||||
|
<bpmn:incoming>Flow_10m4g0q</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1oxbb75</bpmn:outgoing>
|
||||||
|
<bpmn:script>chosen_branch = 'B'</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="exclusive_gateway_based_on_user_task_process">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Gateway_0xwvfep_di" bpmnElement="Gateway_0xwvfep" isMarkerVisible="true">
|
||||||
|
<dc:Bounds x="425" y="152" width="50" height="50" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_05ovp79_di" bpmnElement="Event_05ovp79">
|
||||||
|
<dc:Bounds x="562" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_15rk06j_di" bpmnElement="user_task_choose_branch">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0oy60uv_di" bpmnElement="script_task_branch_a">
|
||||||
|
<dc:Bounds x="500" y="20" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_02hkehe_di" bpmnElement="script_task_branch_b">
|
||||||
|
<dc:Bounds x="500" y="260" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_19j3jcx_di" bpmnElement="Flow_19j3jcx">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0qa66xz_di" bpmnElement="Flow_0qa66xz">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="425" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1ww41l3_di" bpmnElement="Flow_1ww41l3">
|
||||||
|
<di:waypoint x="450" y="152" />
|
||||||
|
<di:waypoint x="450" y="60" />
|
||||||
|
<di:waypoint x="500" y="60" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_10m4g0q_di" bpmnElement="Flow_10m4g0q">
|
||||||
|
<di:waypoint x="450" y="202" />
|
||||||
|
<di:waypoint x="450" y="300" />
|
||||||
|
<di:waypoint x="500" y="300" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1oxbb75_di" bpmnElement="Flow_1oxbb75">
|
||||||
|
<di:waypoint x="550" y="260" />
|
||||||
|
<di:waypoint x="550" y="233" />
|
||||||
|
<di:waypoint x="580" y="233" />
|
||||||
|
<di:waypoint x="580" y="195" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1ck9lfk_di" bpmnElement="Flow_1ck9lfk">
|
||||||
|
<di:waypoint x="550" y="100" />
|
||||||
|
<di:waypoint x="550" y="130" />
|
||||||
|
<di:waypoint x="580" y="130" />
|
||||||
|
<di:waypoint x="580" y="159" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -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"}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="loopback_to_user_task_process" name="Loopback To User Task Process" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_12xxe7w</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_12xxe7w" sourceRef="StartEvent_1" targetRef="set_variable" />
|
||||||
|
<bpmn:exclusiveGateway id="Gateway_1gap20a" default="Flow_1sg0c65">
|
||||||
|
<bpmn:incoming>Flow_1s3znr2</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0utss6p</bpmn:outgoing>
|
||||||
|
<bpmn:outgoing>Flow_1sg0c65</bpmn:outgoing>
|
||||||
|
</bpmn:exclusiveGateway>
|
||||||
|
<bpmn:sequenceFlow id="Flow_08tc3r7" sourceRef="set_variable" targetRef="user_task_enter_increment" />
|
||||||
|
<bpmn:endEvent id="Event_1il3y5o">
|
||||||
|
<bpmn:incoming>Flow_0utss6p</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0utss6p" sourceRef="Gateway_1gap20a" targetRef="Event_1il3y5o">
|
||||||
|
<bpmn:conditionExpression>counter == 3</bpmn:conditionExpression>
|
||||||
|
</bpmn:sequenceFlow>
|
||||||
|
<bpmn:scriptTask id="set_variable" name="Set Variable">
|
||||||
|
<bpmn:incoming>Flow_12xxe7w</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_08tc3r7</bpmn:outgoing>
|
||||||
|
<bpmn:script>counter = 1
|
||||||
|
the_var = 0</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0wnc5ju" sourceRef="user_task_enter_increment" targetRef="add_user_input_to_variable" />
|
||||||
|
<bpmn:sequenceFlow id="Flow_1sg0c65" sourceRef="Gateway_1gap20a" targetRef="user_task_enter_increment" />
|
||||||
|
<bpmn:userTask id="user_task_enter_increment" name="User Task Enter Increment">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser />
|
||||||
|
<spiffworkflow:properties>
|
||||||
|
<spiffworkflow:property name="formJsonSchemaFilename" value="user-input-schema.json" />
|
||||||
|
<spiffworkflow:property name="formUiSchemaFilename" value="user-input-uischema.json" />
|
||||||
|
</spiffworkflow:properties>
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_08tc3r7</bpmn:incoming>
|
||||||
|
<bpmn:incoming>Flow_1sg0c65</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0wnc5ju</bpmn:outgoing>
|
||||||
|
</bpmn:userTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1s3znr2" sourceRef="add_user_input_to_variable" targetRef="Gateway_1gap20a" />
|
||||||
|
<bpmn:scriptTask id="add_user_input_to_variable" name="Add User Input to Variable">
|
||||||
|
<bpmn:incoming>Flow_0wnc5ju</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1s3znr2</bpmn:outgoing>
|
||||||
|
<bpmn:script>the_var = user_input_variable + the_var
|
||||||
|
counter += 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:textAnnotation id="TextAnnotation_09y70ug">
|
||||||
|
<bpmn:text>loop back if a < 3</bpmn:text>
|
||||||
|
</bpmn:textAnnotation>
|
||||||
|
<bpmn:association id="Association_0470wt9" sourceRef="Flow_1sg0c65" targetRef="TextAnnotation_09y70ug" />
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loopback_to_user_task_process">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Gateway_1gap20a_di" bpmnElement="Gateway_1gap20a" isMarkerVisible="true">
|
||||||
|
<dc:Bounds x="625" y="152" width="50" height="50" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1il3y5o_di" bpmnElement="Event_1il3y5o">
|
||||||
|
<dc:Bounds x="712" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0hrsdn8_di" bpmnElement="set_variable">
|
||||||
|
<dc:Bounds x="250" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0katfaf_di" bpmnElement="user_task_enter_increment">
|
||||||
|
<dc:Bounds x="380" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0a6owe7_di" bpmnElement="add_user_input_to_variable">
|
||||||
|
<dc:Bounds x="500" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="TextAnnotation_09y70ug_di" bpmnElement="TextAnnotation_09y70ug">
|
||||||
|
<dc:Bounds x="610" y="55" width="130" height="30" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_12xxe7w_di" bpmnElement="Flow_12xxe7w">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="250" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_08tc3r7_di" bpmnElement="Flow_08tc3r7">
|
||||||
|
<di:waypoint x="350" y="177" />
|
||||||
|
<di:waypoint x="380" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0utss6p_di" bpmnElement="Flow_0utss6p">
|
||||||
|
<di:waypoint x="675" y="177" />
|
||||||
|
<di:waypoint x="712" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0wnc5ju_di" bpmnElement="Flow_0wnc5ju">
|
||||||
|
<di:waypoint x="480" y="177" />
|
||||||
|
<di:waypoint x="500" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1sg0c65_di" bpmnElement="Flow_1sg0c65">
|
||||||
|
<di:waypoint x="650" y="150" />
|
||||||
|
<di:waypoint x="550" y="70" />
|
||||||
|
<di:waypoint x="475" y="139" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1s3znr2_di" bpmnElement="Flow_1s3znr2">
|
||||||
|
<di:waypoint x="600" y="177" />
|
||||||
|
<di:waypoint x="625" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Association_0470wt9_di" bpmnElement="Association_0470wt9">
|
||||||
|
<di:waypoint x="579" y="93" />
|
||||||
|
<di:waypoint x="610" y="81" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"title": "User Input",
|
||||||
|
"description": "",
|
||||||
|
"properties": {
|
||||||
|
"user_input_variable": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "user_input_variable"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": []
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"ui:order": [
|
||||||
|
"user_input_variable"
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="loopback_process" name="Loopback Process" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_12xxe7w</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_12xxe7w" sourceRef="StartEvent_1" targetRef="set_variable" />
|
||||||
|
<bpmn:exclusiveGateway id="Gateway_1gap20a" default="Flow_1sg0c65">
|
||||||
|
<bpmn:incoming>Flow_0wnc5ju</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0utss6p</bpmn:outgoing>
|
||||||
|
<bpmn:outgoing>Flow_1sg0c65</bpmn:outgoing>
|
||||||
|
</bpmn:exclusiveGateway>
|
||||||
|
<bpmn:sequenceFlow id="Flow_08tc3r7" sourceRef="set_variable" targetRef="increment_variable" />
|
||||||
|
<bpmn:endEvent id="Event_1il3y5o">
|
||||||
|
<bpmn:incoming>Flow_0utss6p</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0utss6p" sourceRef="Gateway_1gap20a" targetRef="Event_1il3y5o">
|
||||||
|
<bpmn:conditionExpression>a == 3</bpmn:conditionExpression>
|
||||||
|
</bpmn:sequenceFlow>
|
||||||
|
<bpmn:scriptTask id="set_variable" name="Set Variable">
|
||||||
|
<bpmn:incoming>Flow_12xxe7w</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_08tc3r7</bpmn:outgoing>
|
||||||
|
<bpmn:script>a = 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0wnc5ju" sourceRef="increment_variable" targetRef="Gateway_1gap20a" />
|
||||||
|
<bpmn:scriptTask id="increment_variable" name="Increment Variable">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:instructionsForEndUser />
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_08tc3r7</bpmn:incoming>
|
||||||
|
<bpmn:incoming>Flow_1sg0c65</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0wnc5ju</bpmn:outgoing>
|
||||||
|
<bpmn:script>a += 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1sg0c65" sourceRef="Gateway_1gap20a" targetRef="increment_variable" />
|
||||||
|
<bpmn:textAnnotation id="TextAnnotation_09y70ug">
|
||||||
|
<bpmn:text>loop back if a < 3</bpmn:text>
|
||||||
|
</bpmn:textAnnotation>
|
||||||
|
<bpmn:association id="Association_0470wt9" sourceRef="Flow_1sg0c65" targetRef="TextAnnotation_09y70ug" />
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="loopback_process">
|
||||||
|
<bpmndi:BPMNShape id="TextAnnotation_09y70ug_di" bpmnElement="TextAnnotation_09y70ug">
|
||||||
|
<dc:Bounds x="610" y="55" width="130" height="30" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Gateway_1gap20a_di" bpmnElement="Gateway_1gap20a" isMarkerVisible="true">
|
||||||
|
<dc:Bounds x="535" y="152" width="50" height="50" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1il3y5o_di" bpmnElement="Event_1il3y5o">
|
||||||
|
<dc:Bounds x="632" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0hrsdn8_di" bpmnElement="set_variable">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_1g5b8wo_di" bpmnElement="increment_variable">
|
||||||
|
<dc:Bounds x="400" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Association_0470wt9_di" bpmnElement="Association_0470wt9">
|
||||||
|
<di:waypoint x="567.1081954098089" y="89.9595613114437" />
|
||||||
|
<di:waypoint x="610" y="81" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_12xxe7w_di" bpmnElement="Flow_12xxe7w">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_08tc3r7_di" bpmnElement="Flow_08tc3r7">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="400" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0utss6p_di" bpmnElement="Flow_0utss6p">
|
||||||
|
<di:waypoint x="585" y="177" />
|
||||||
|
<di:waypoint x="632" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0wnc5ju_di" bpmnElement="Flow_0wnc5ju">
|
||||||
|
<di:waypoint x="500" y="177" />
|
||||||
|
<di:waypoint x="535" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1sg0c65_di" bpmnElement="Flow_1sg0c65">
|
||||||
|
<di:waypoint x="560" y="150" />
|
||||||
|
<di:waypoint x="610" y="140" />
|
||||||
|
<di:waypoint x="550" y="70" />
|
||||||
|
<di:waypoint x="489" y="137" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"expected_output_json": { "a": 3 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="ManualTaskProcess" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_0gz6i84</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0gz6i84" sourceRef="StartEvent_1" targetRef="manual_task_one" />
|
||||||
|
<bpmn:endEvent id="Event_0ynlmo7">
|
||||||
|
<bpmn:incoming>Flow_0ikklg6</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0ikklg6" sourceRef="manual_task_one" targetRef="Event_0ynlmo7" />
|
||||||
|
<bpmn:manualTask id="manual_task_one" name="Manual">
|
||||||
|
<bpmn:incoming>Flow_0gz6i84</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0ikklg6</bpmn:outgoing>
|
||||||
|
</bpmn:manualTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="ManualTaskProcess">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_0ynlmo7_di" bpmnElement="Event_0ynlmo7">
|
||||||
|
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0sji2rz_di" bpmnElement="manualt_task_one">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0gz6i84_di" bpmnElement="Flow_0gz6i84">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0ikklg6_di" bpmnElement="Flow_0ikklg6">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="432" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"tasks": {
|
||||||
|
"manual_task_one": {
|
||||||
|
"data": [{}]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expected_output_json": {}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="ProcessA" name="ProcessA" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_0jk46kf</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0jk46kf" sourceRef="StartEvent_1" targetRef="Activity_0e9rl60" />
|
||||||
|
<bpmn:endEvent id="Event_1srknca">
|
||||||
|
<bpmn:incoming>Flow_0pw6euz</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0pw6euz" sourceRef="Activity_0e9rl60" targetRef="Event_1srknca" />
|
||||||
|
<bpmn:scriptTask id="Activity_0e9rl60">
|
||||||
|
<bpmn:incoming>Flow_0jk46kf</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_0pw6euz</bpmn:outgoing>
|
||||||
|
<bpmn:script>a = 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="ProcessA">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1srknca_di" bpmnElement="Event_1srknca">
|
||||||
|
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0yxs81w_di" bpmnElement="Activity_0e9rl60">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0jk46kf_di" bpmnElement="Flow_0jk46kf">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0pw6euz_di" bpmnElement="Flow_0pw6euz">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="432" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="Process_39edgqg" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_1qgv480</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1qgv480" sourceRef="StartEvent_1" targetRef="Activity_1kral0x" />
|
||||||
|
<bpmn:endEvent id="ProcessB" name="ProcessB">
|
||||||
|
<bpmn:incoming>Flow_1sbj39z</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1sbj39z" sourceRef="Activity_1kral0x" targetRef="ProcessB" />
|
||||||
|
<bpmn:scriptTask id="Activity_1kral0x">
|
||||||
|
<bpmn:incoming>Flow_1qgv480</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1sbj39z</bpmn:outgoing>
|
||||||
|
<bpmn:script>b = 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_39edgqg">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_12lq7sg_di" bpmnElement="ProcessB">
|
||||||
|
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||||
|
<bpmndi:BPMNLabel>
|
||||||
|
<dc:Bounds x="427" y="202" width="48" height="14" />
|
||||||
|
</bpmndi:BPMNLabel>
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0pkm1sr_di" bpmnElement="Activity_1kral0x">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1qgv480_di" bpmnElement="Flow_1qgv480">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1sbj39z_di" bpmnElement="Flow_1sbj39z">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="432" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"expected_output_json": { "a": 1 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"expected_output_json": { "b": 1 }
|
||||||
|
},
|
||||||
|
"test_case_2": {
|
||||||
|
"expected_output_json": { "b": 1 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"admin": false,
|
||||||
|
"description": "",
|
||||||
|
"display_name": "Expected To Pass",
|
||||||
|
"display_order": 0,
|
||||||
|
"parent_groups": null,
|
||||||
|
"process_groups": [],
|
||||||
|
"process_models": []
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="Process_Script_Task" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_0qfycuk</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_0qfycuk" sourceRef="StartEvent_1" targetRef="Activity_1qdbp6x" />
|
||||||
|
<bpmn:endEvent id="Event_1kumwb5">
|
||||||
|
<bpmn:incoming>Flow_1auiekw</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1auiekw" sourceRef="Activity_1qdbp6x" targetRef="Event_1kumwb5" />
|
||||||
|
<bpmn:scriptTask id="Activity_1qdbp6x" name="Script">
|
||||||
|
<bpmn:incoming>Flow_0qfycuk</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1auiekw</bpmn:outgoing>
|
||||||
|
<bpmn:script>a = 1</bpmn:script>
|
||||||
|
</bpmn:scriptTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_Script_Task">
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_1kumwb5_di" bpmnElement="Event_1kumwb5">
|
||||||
|
<dc:Bounds x="432" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_0ii0b3p_di" bpmnElement="Activity_1qdbp6x">
|
||||||
|
<dc:Bounds x="270" y="137" width="100" height="80" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0qfycuk_di" bpmnElement="Flow_0qfycuk">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1auiekw_di" bpmnElement="Flow_1auiekw">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="432" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"expected_output_json": { "a": 1 }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:spiffworkflow="http://spiffworkflow.org/bpmn/schema/1.0/core" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
|
||||||
|
<bpmn:process id="ServiceTaskProcess" isExecutable="true">
|
||||||
|
<bpmn:startEvent id="StartEvent_1">
|
||||||
|
<bpmn:outgoing>Flow_19ephzh</bpmn:outgoing>
|
||||||
|
</bpmn:startEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_19ephzh" sourceRef="StartEvent_1" targetRef="service_task_one" />
|
||||||
|
<bpmn:endEvent id="Event_132m0z7">
|
||||||
|
<bpmn:incoming>Flow_1dsxn78</bpmn:incoming>
|
||||||
|
</bpmn:endEvent>
|
||||||
|
<bpmn:sequenceFlow id="Flow_1dsxn78" sourceRef="service_task_one" targetRef="Event_132m0z7" />
|
||||||
|
<bpmn:serviceTask id="service_task_one" name="Task 2">
|
||||||
|
<bpmn:extensionElements>
|
||||||
|
<spiffworkflow:serviceTaskOperator id="http/GetRequest" resultVariable="bamboo_get_employee">
|
||||||
|
<spiffworkflow:parameters>
|
||||||
|
<spiffworkflow:parameter id="basic_auth_password" type="str" value=""x"" />
|
||||||
|
<spiffworkflow:parameter id="basic_auth_username" type="str" value=""x"" />
|
||||||
|
<spiffworkflow:parameter id="headers" type="any" value="{"Accept": "application/json"}" />
|
||||||
|
<spiffworkflow:parameter id="params" type="any" value="{}" />
|
||||||
|
<spiffworkflow:parameter id="url" type="str" value="f"https://example.com/api/user"" />
|
||||||
|
</spiffworkflow:parameters>
|
||||||
|
</spiffworkflow:serviceTaskOperator>
|
||||||
|
<spiffworkflow:instructionsForEndUser>This is the Service Task Unit Test Screen.</spiffworkflow:instructionsForEndUser>
|
||||||
|
<spiffworkflow:postScript />
|
||||||
|
</bpmn:extensionElements>
|
||||||
|
<bpmn:incoming>Flow_0xx2kop</bpmn:incoming>
|
||||||
|
<bpmn:outgoing>Flow_1dsxn78</bpmn:outgoing>
|
||||||
|
</bpmn:serviceTask>
|
||||||
|
</bpmn:process>
|
||||||
|
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
|
||||||
|
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="ServiceTaskProcess">
|
||||||
|
<bpmndi:BPMNEdge id="Flow_19ephzh_di" bpmnElement="Flow_19ephzh">
|
||||||
|
<di:waypoint x="215" y="177" />
|
||||||
|
<di:waypoint x="270" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_0xx2kop_di" bpmnElement="Flow_0xx2kop">
|
||||||
|
<di:waypoint x="370" y="177" />
|
||||||
|
<di:waypoint x="430" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNEdge id="Flow_1dsxn78_di" bpmnElement="Flow_1dsxn78">
|
||||||
|
<di:waypoint x="530" y="177" />
|
||||||
|
<di:waypoint x="592" y="177" />
|
||||||
|
</bpmndi:BPMNEdge>
|
||||||
|
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
|
||||||
|
<dc:Bounds x="179" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Event_132m0z7_di" bpmnElement="Event_132m0z7">
|
||||||
|
<dc:Bounds x="592" y="159" width="36" height="36" />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
<bpmndi:BPMNShape id="Activity_1nlg9cc_di" bpmnElement="service_task_one">
|
||||||
|
<dc:Bounds x="430" y="137" width="100" height="80" />
|
||||||
|
<bpmndi:BPMNLabel />
|
||||||
|
</bpmndi:BPMNShape>
|
||||||
|
</bpmndi:BPMNPlane>
|
||||||
|
</bpmndi:BPMNDiagram>
|
||||||
|
</bpmn:definitions>
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"test_case_1": {
|
||||||
|
"tasks": {
|
||||||
|
"ServiceTaskProcess:service_task_one": {
|
||||||
|
"data": [{ "the_result": "result_from_service" }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"expected_output_json": { "the_result": "result_from_service" }
|
||||||
|
}
|
||||||
|
}
|
|
@ -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:
|
def assure_process_group_exists(process_group_id: Optional[str] = None) -> ProcessGroup:
|
||||||
"""Assure_process_group_exists."""
|
|
||||||
process_group = None
|
process_group = None
|
||||||
process_model_service = ProcessModelService()
|
|
||||||
if process_group_id is not None:
|
if process_group_id is not None:
|
||||||
try:
|
try:
|
||||||
process_group = process_model_service.get_process_group(process_group_id)
|
process_group = ProcessModelService.get_process_group(process_group_id)
|
||||||
except ProcessEntityNotFoundError:
|
except ProcessEntityNotFoundError:
|
||||||
process_group = None
|
process_group = None
|
||||||
|
|
||||||
|
@ -31,7 +29,7 @@ def assure_process_group_exists(process_group_id: Optional[str] = None) -> Proce
|
||||||
admin=False,
|
admin=False,
|
||||||
display_order=0,
|
display_order=0,
|
||||||
)
|
)
|
||||||
process_model_service.add_process_group(process_group)
|
ProcessModelService.add_process_group(process_group)
|
||||||
return process_group
|
return process_group
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from flask import current_app
|
||||||
|
from flask import Flask
|
||||||
|
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
|
||||||
|
|
||||||
|
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):
|
||||||
|
def test_can_test_a_simple_process_model(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> None:
|
||||||
|
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(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> None:
|
||||||
|
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
|
||||||
|
|
||||||
|
def test_can_test_multiple_process_models_with_all_passing_tests(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> 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,
|
||||||
|
) -> None:
|
||||||
|
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,
|
||||||
|
) -> 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,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> 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
|
||||||
|
|
||||||
|
def test_can_test_process_model_with_service_task(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> 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
|
||||||
|
|
||||||
|
def test_can_test_process_model_with_loopback_to_user_task(
|
||||||
|
self,
|
||||||
|
app: Flask,
|
||||||
|
with_db_and_bpmn_file_cleanup: None,
|
||||||
|
) -> 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 = "expected-to-pass",
|
||||||
|
test_case_file: Optional[str] = None,
|
||||||
|
test_case_identifier: Optional[str] = None,
|
||||||
|
) -> ProcessModelTestRunner:
|
||||||
|
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]
|
||||||
|
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()
|
||||||
|
|
||||||
|
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()
|
||||||
|
return process_model_test_runner
|
||||||
|
|
||||||
|
def root_path(self) -> str:
|
||||||
|
return os.path.join(
|
||||||
|
current_app.instance_path,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"tests",
|
||||||
|
"data",
|
||||||
|
"bpmn_unit_test_process_models",
|
||||||
|
)
|
|
@ -4,6 +4,7 @@ import {
|
||||||
ErrorForDisplay,
|
ErrorForDisplay,
|
||||||
ProcessInstanceEventErrorDetail,
|
ProcessInstanceEventErrorDetail,
|
||||||
ProcessInstanceLogEntry,
|
ProcessInstanceLogEntry,
|
||||||
|
TestCaseErrorDetails,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
|
||||||
function errorDetailDisplay(
|
function errorDetailDisplay(
|
||||||
|
@ -40,6 +41,22 @@ export const errorForDisplayFromProcessInstanceErrorDetail = (
|
||||||
return errorForDisplay;
|
return errorForDisplay;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const errorForDisplayFromTestCaseErrorDetails = (
|
||||||
|
testCaseErrorDetails: TestCaseErrorDetails
|
||||||
|
) => {
|
||||||
|
const errorForDisplay: ErrorForDisplay = {
|
||||||
|
message: testCaseErrorDetails.error_messages.join('\n'),
|
||||||
|
messageClassName: 'failure-string',
|
||||||
|
task_name: testCaseErrorDetails.task_bpmn_name,
|
||||||
|
task_id: testCaseErrorDetails.task_bpmn_identifier,
|
||||||
|
line_number: testCaseErrorDetails.task_line_number,
|
||||||
|
error_line: testCaseErrorDetails.task_line_contents,
|
||||||
|
task_trace: testCaseErrorDetails.task_trace,
|
||||||
|
stacktrace: testCaseErrorDetails.stacktrace,
|
||||||
|
};
|
||||||
|
return errorForDisplay;
|
||||||
|
};
|
||||||
|
|
||||||
export const childrenForErrorObject = (errorObject: ErrorForDisplay) => {
|
export const childrenForErrorObject = (errorObject: ErrorForDisplay) => {
|
||||||
let sentryLinkTag = null;
|
let sentryLinkTag = null;
|
||||||
if (errorObject.sentry_link) {
|
if (errorObject.sentry_link) {
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
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, TestCaseResult, TestCaseResults } from '../interfaces';
|
||||||
|
import {
|
||||||
|
childrenForErrorObject,
|
||||||
|
errorForDisplayFromTestCaseErrorDetails,
|
||||||
|
} from './ErrorDisplay';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
processModelFile?: ProcessFile;
|
||||||
|
buttonType?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ProcessModelTestRun({
|
||||||
|
processModelFile,
|
||||||
|
buttonType = 'icon',
|
||||||
|
}: OwnProps) {
|
||||||
|
const [testCaseResults, setTestCaseResults] =
|
||||||
|
useState<TestCaseResults | null>(null);
|
||||||
|
const [showTestCaseResultsModal, setShowTestCaseResultsModal] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const { targetUris } = useUriListForPermissions();
|
||||||
|
|
||||||
|
const onProcessModelTestRunSuccess = (result: TestCaseResults) => {
|
||||||
|
setTestCaseResults(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
const processModelTestRunResultTag = () => {
|
||||||
|
if (testCaseResults) {
|
||||||
|
if (testCaseResults.all_passed) {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
kind="ghost"
|
||||||
|
className="green-icon"
|
||||||
|
renderIcon={Checkmark}
|
||||||
|
iconDescription="All BPMN unit tests passed"
|
||||||
|
hasIconOnly
|
||||||
|
size="lg"
|
||||||
|
onClick={() => setShowTestCaseResultsModal(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
kind="ghost"
|
||||||
|
className="red-icon"
|
||||||
|
renderIcon={Close}
|
||||||
|
iconDescription="BPMN unit tests failed"
|
||||||
|
hasIconOnly
|
||||||
|
size="lg"
|
||||||
|
onClick={() => setShowTestCaseResultsModal(true)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onProcessModelTestRun = () => {
|
||||||
|
const httpMethod = 'POST';
|
||||||
|
setTestCaseResults(null);
|
||||||
|
|
||||||
|
let queryParams = '';
|
||||||
|
if (processModelFile) {
|
||||||
|
queryParams = `?test_case_file=${processModelFile.name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpService.makeCallToBackend({
|
||||||
|
path: `${targetUris.processModelTestsPath}${queryParams}`,
|
||||||
|
successCallback: onProcessModelTestRunSuccess,
|
||||||
|
httpMethod,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCaseFormattedResultTag = () => {
|
||||||
|
if (!testCaseResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const passingRows: any[] = [];
|
||||||
|
const failingRows: any[] = [];
|
||||||
|
|
||||||
|
testCaseResults.passing.forEach((testCaseResult: TestCaseResult) => {
|
||||||
|
passingRows.push(<p>{testCaseResult.test_case_identifier}</p>);
|
||||||
|
});
|
||||||
|
|
||||||
|
testCaseResults.failing
|
||||||
|
.slice(0, 2)
|
||||||
|
.forEach((testCaseResult: TestCaseResult) => {
|
||||||
|
if (testCaseResult.test_case_error_details) {
|
||||||
|
const errorForDisplay = errorForDisplayFromTestCaseErrorDetails(
|
||||||
|
testCaseResult.test_case_error_details
|
||||||
|
);
|
||||||
|
const errorChildren = childrenForErrorObject(errorForDisplay);
|
||||||
|
failingRows.push(
|
||||||
|
<>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
Test Case:{' '}
|
||||||
|
<strong>{testCaseResult.test_case_identifier}</strong>
|
||||||
|
</p>
|
||||||
|
{errorChildren}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>Passing: {testCaseResults.passing.length}</p>
|
||||||
|
<p>Failing: {testCaseResults.failing.length}</p>
|
||||||
|
<br />
|
||||||
|
{failingRows.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p>Failure Details:</p>
|
||||||
|
{failingRows}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
{passingRows.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<p>Successful Test Cases:</p>
|
||||||
|
{passingRows}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testCaseResultsModal = () => {
|
||||||
|
if (!testCaseResults) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let modalHeading = 'All Tests PASSED';
|
||||||
|
if (!testCaseResults.all_passed) {
|
||||||
|
modalHeading = 'Some Tests FAILED';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={showTestCaseResultsModal}
|
||||||
|
data-qa="test-case-results-modal"
|
||||||
|
modalHeading={modalHeading}
|
||||||
|
modalLabel="Test Case Rsults"
|
||||||
|
primaryButtonText="OK"
|
||||||
|
onRequestSubmit={() => setShowTestCaseResultsModal(false)}
|
||||||
|
onRequestClose={() => setShowTestCaseResultsModal(false)}
|
||||||
|
>
|
||||||
|
{testCaseFormattedResultTag()}
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonElement = () => {
|
||||||
|
if (buttonType === 'icon') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
kind="ghost"
|
||||||
|
renderIcon={PlayOutline}
|
||||||
|
iconDescription="Run BPMN unit tests defined in this file"
|
||||||
|
hasIconOnly
|
||||||
|
size="lg"
|
||||||
|
onClick={() => onProcessModelTestRun()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (buttonType === 'text') {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={() => onProcessModelTestRun()}
|
||||||
|
title="Run all BPMN unit tests for this process model"
|
||||||
|
>
|
||||||
|
Run Unit Tests
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{testCaseResultsModal()}
|
||||||
|
{buttonElement()}
|
||||||
|
{processModelTestRunResultTag()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -29,6 +29,7 @@ export const useUriListForPermissions = () => {
|
||||||
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
|
processModelFileShowPath: `/v1.0/process-models/${params.process_model_id}/files/${params.file_name}`,
|
||||||
processModelPublishPath: `/v1.0/process-model-publish/${params.process_model_id}`,
|
processModelPublishPath: `/v1.0/process-model-publish/${params.process_model_id}`,
|
||||||
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
processModelShowPath: `/v1.0/process-models/${params.process_model_id}`,
|
||||||
|
processModelTestsPath: `/v1.0/process-model-tests/${params.process_model_id}`,
|
||||||
secretListPath: `/v1.0/secrets`,
|
secretListPath: `/v1.0/secrets`,
|
||||||
userSearch: `/v1.0/users/search`,
|
userSearch: `/v1.0/users/search`,
|
||||||
userExists: `/v1.0/users/exists/by-username`,
|
userExists: `/v1.0/users/exists/by-username`,
|
||||||
|
|
|
@ -418,6 +418,12 @@ td.actions-cell {
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cds--btn--ghost:not([disabled]).red-icon svg {
|
||||||
|
fill: red;
|
||||||
|
}
|
||||||
|
.cds--btn--ghost:not([disabled]).green-icon svg {
|
||||||
|
fill: #198038;
|
||||||
|
}
|
||||||
.cds--btn--ghost:not([disabled]) svg.red-icon {
|
.cds--btn--ghost:not([disabled]) svg.red-icon {
|
||||||
fill: red;
|
fill: red;
|
||||||
}
|
}
|
||||||
|
|
|
@ -365,3 +365,26 @@ export interface InterstitialPageResponse {
|
||||||
task?: ProcessInstanceTask;
|
task?: ProcessInstanceTask;
|
||||||
process_instance?: ProcessInstance;
|
process_instance?: ProcessInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TestCaseErrorDetails {
|
||||||
|
error_messages: string[];
|
||||||
|
stacktrace?: string[];
|
||||||
|
task_bpmn_identifier?: string;
|
||||||
|
task_bpmn_name?: string;
|
||||||
|
task_line_contents?: string;
|
||||||
|
task_line_number?: number;
|
||||||
|
task_trace?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCaseResult {
|
||||||
|
bpmn_file: string;
|
||||||
|
passed: boolean;
|
||||||
|
test_case_identifier: string;
|
||||||
|
test_case_error_details?: TestCaseErrorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TestCaseResults {
|
||||||
|
all_passed: boolean;
|
||||||
|
failing: TestCaseResult[];
|
||||||
|
passing: TestCaseResult[];
|
||||||
|
}
|
||||||
|
|
|
@ -2,11 +2,11 @@ import { useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate, useParams } from 'react-router-dom';
|
import { Link, useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Add,
|
Add,
|
||||||
Upload,
|
|
||||||
Download,
|
Download,
|
||||||
TrashCan,
|
|
||||||
Favorite,
|
|
||||||
Edit,
|
Edit,
|
||||||
|
Favorite,
|
||||||
|
TrashCan,
|
||||||
|
Upload,
|
||||||
View,
|
View,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/icons-react';
|
} from '@carbon/icons-react';
|
||||||
|
@ -14,18 +14,18 @@ import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionItem,
|
AccordionItem,
|
||||||
Button,
|
Button,
|
||||||
Grid,
|
|
||||||
Column,
|
|
||||||
Stack,
|
|
||||||
ButtonSet,
|
ButtonSet,
|
||||||
Modal,
|
Column,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
|
Grid,
|
||||||
|
Modal,
|
||||||
|
Stack,
|
||||||
Table,
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
TableHead,
|
TableHead,
|
||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
TableCell,
|
|
||||||
TableBody,
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from '@carbon/react';
|
} from '@carbon/react';
|
||||||
import { Can } from '@casl/react';
|
import { Can } from '@casl/react';
|
||||||
|
@ -49,6 +49,7 @@ import { usePermissionFetcher } from '../hooks/PermissionService';
|
||||||
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
import { useUriListForPermissions } from '../hooks/UriListForPermissions';
|
||||||
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
import ProcessInstanceRun from '../components/ProcessInstanceRun';
|
||||||
import { Notification } from '../components/Notification';
|
import { Notification } from '../components/Notification';
|
||||||
|
import ProcessModelTestRun from '../components/ProcessModelTestRun';
|
||||||
|
|
||||||
export default function ProcessModelShow() {
|
export default function ProcessModelShow() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
@ -68,6 +69,7 @@ export default function ProcessModelShow() {
|
||||||
const { targetUris } = useUriListForPermissions();
|
const { targetUris } = useUriListForPermissions();
|
||||||
const permissionRequestData: PermissionsToCheck = {
|
const permissionRequestData: PermissionsToCheck = {
|
||||||
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
|
[targetUris.processModelShowPath]: ['PUT', 'DELETE'],
|
||||||
|
[targetUris.processModelTestsPath]: ['POST'],
|
||||||
[targetUris.processModelPublishPath]: ['POST'],
|
[targetUris.processModelPublishPath]: ['POST'],
|
||||||
[targetUris.processInstanceListPath]: ['GET'],
|
[targetUris.processInstanceListPath]: ['GET'],
|
||||||
[targetUris.processInstanceCreatePath]: ['POST'],
|
[targetUris.processInstanceCreatePath]: ['POST'],
|
||||||
|
@ -81,6 +83,18 @@ export default function ProcessModelShow() {
|
||||||
`${params.process_model_id}`
|
`${params.process_model_id}`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let hasTestCaseFiles: boolean = false;
|
||||||
|
|
||||||
|
const isTestCaseFile = (processModelFile: ProcessFile) => {
|
||||||
|
return processModelFile.name.match(/^test_.*\.json$/);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (processModel) {
|
||||||
|
hasTestCaseFiles = !!processModel.files.find(
|
||||||
|
(processModelFile: ProcessFile) => isTestCaseFile(processModelFile)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const processResult = (result: ProcessModel) => {
|
const processResult = (result: ProcessModel) => {
|
||||||
setProcessModel(result);
|
setProcessModel(result);
|
||||||
|
@ -308,6 +322,13 @@ export default function ProcessModelShow() {
|
||||||
</Can>
|
</Can>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (isTestCaseFile(processModelFile)) {
|
||||||
|
elements.push(
|
||||||
|
<Can I="POST" a={targetUris.processModelTestsPath} ability={ability}>
|
||||||
|
<ProcessModelTestRun processModelFile={processModelFile} />
|
||||||
|
</Can>
|
||||||
|
);
|
||||||
|
}
|
||||||
return elements;
|
return elements;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -647,6 +668,11 @@ export default function ProcessModelShow() {
|
||||||
Publish Changes
|
Publish Changes
|
||||||
</Button>
|
</Button>
|
||||||
</Can>
|
</Can>
|
||||||
|
<Can I="POST" a={targetUris.processModelTestsPath} ability={ability}>
|
||||||
|
{hasTestCaseFiles ? (
|
||||||
|
<ProcessModelTestRun buttonType="text" />
|
||||||
|
) : null}
|
||||||
|
</Can>
|
||||||
</Stack>
|
</Stack>
|
||||||
{processModelFilesSection()}
|
{processModelFilesSection()}
|
||||||
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
|
<Can I="GET" a={targetUris.processInstanceListPath} ability={ability}>
|
||||||
|
|
Loading…
Reference in New Issue