Merge remote-tracking branch 'origin/main' into feature/script-unit-test-subprocesses

This commit is contained in:
burnettk 2022-10-03 12:21:25 -04:00
commit c567cae994
10 changed files with 150 additions and 112 deletions

View File

@ -1087,7 +1087,7 @@ def script_unit_test_run(
input_json = get_required_parameter_or_raise("input_json", body)
expected_output_json = get_required_parameter_or_raise("expected_output_json", body)
result = ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
python_script, input_json, expected_output_json
)
return make_response(jsonify(result), 200)

View File

@ -0,0 +1,36 @@
"""Fact_service."""
from typing import Any
from SpiffWorkflow import Task as SpiffTask # type: ignore
from spiffworkflow_backend.scripts.script import Script
class FactService(Script):
"""FactService."""
def get_description(self) -> str:
"""Get_description."""
return """Just your basic class that can pull in data from a few api endpoints and
do a basic task."""
def run(
self, task: SpiffTask, environment_identifier: str, *args: Any, **kwargs: Any
) -> Any:
"""Run."""
if "type" not in kwargs:
raise Exception("Please specify a 'type' of fact as a keyword argument.")
else:
fact = kwargs["type"]
if fact == "cat":
details = "The cat in the hat" # self.get_cat()
elif fact == "norris":
details = "Chuck Norris doesnt read books. He stares them down until he gets the information he wants."
elif fact == "buzzword":
details = "Move the Needle." # self.get_buzzword()
else:
details = "unknown fact type."
# self.add_data_to_task(task, details)
return details

View File

@ -0,0 +1,20 @@
"""Get_env."""
from typing import Any
from SpiffWorkflow import Task as SpiffTask # type: ignore
from spiffworkflow_backend.scripts.script import Script
class GetEnv(Script):
"""GetEnv."""
def get_description(self) -> str:
"""Get_description."""
return """Returns the current environment - ie testing, staging, production."""
def run(
self, task: SpiffTask, environment_identifier: str, *_args: Any, **kwargs: Any
) -> Any:
"""Run."""
return environment_identifier

View File

@ -9,9 +9,7 @@ from typing import Any
from typing import Callable
from flask_bpmn.api.api_error import ApiError
from spiffworkflow_backend.models.task import Task
from SpiffWorkflow import Task as SpiffTask # type: ignore
# Generally speaking, having some global in a flask app is TERRIBLE.
# This is here, because after loading the application this will never change under
@ -22,76 +20,30 @@ SCRIPT_SUB_CLASSES = None
class Script:
"""Provides an abstract class that defines how scripts should work, this must be extended in all Script Tasks."""
def get_description(self) -> None:
@abstractmethod
def get_description(self) -> str:
"""Get_description."""
raise ApiError("invalid_script", "This script does not supply a description.")
@abstractmethod
def do_task(
self, task: Task, workflow_id: int, *args: list[Any], **kwargs: dict[Any, Any]
) -> None:
"""Do_task."""
def run(
self,
task: SpiffTask,
environment_identifier: str,
*args: Any,
**kwargs: Any,
) -> Any:
"""Run."""
raise ApiError(
"invalid_script",
"This is an internal error. The script you are trying to execute '%s' "
% self.__class__.__name__
+ "does not properly implement the do_task function.",
)
@abstractmethod
def do_task_validate_only(
self, task: Task, workflow_id: int, *args: list[Any], **kwargs: dict[Any, Any]
) -> None:
"""Do_task_validate_only."""
raise ApiError(
"invalid_script",
"This is an internal error. The script you are trying to execute '%s' "
% self.__class__.__name__
+ "does must provide a validate_only option that mimics the do_task, "
+ "but does not make external calls or database updates.",
+ "does not properly implement the run function.",
)
@staticmethod
def generate_augmented_list(task: Task, workflow_id: int) -> dict[str, Callable]:
"""This makes a dictionary of lambda functions that are closed over the class instance that they represent.
This is passed into PythonScriptParser as a list of helper functions that are
available for running. In general, they maintain the do_task call structure that they had, but
they always return a value rather than updating the task data.
We may be able to remove the task for each of these calls if we are not using it other than potentially
updating the task data.
"""
def make_closure(
subclass: type[Script], task: Task, workflow_id: int
) -> Callable:
"""Yes - this is black magic.
Essentially, we want to build a list of all of the submodules (i.e. email, user_data_get, etc)
and a function that is assocated with them.
This basically creates an Instance of the class and returns a function that calls do_task
on the instance of that class.
the next for x in range, then grabs the name of the module and associates it with the function
that we created.
"""
instance = subclass()
return lambda *ar, **kw: subclass.do_task(
instance, task, workflow_id, *ar, **kw
)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
subclass = subclasses[x]
execlist[subclass.__module__.split(".")[-1]] = make_closure(
subclass, task, workflow_id
)
return execlist
@staticmethod
def generate_augmented_validate_list(
task: Task, workflow_id: int
def generate_augmented_list(
task: SpiffTask, environment_identifier: str
) -> dict[str, Callable]:
"""This makes a dictionary of lambda functions that are closed over the class instance that they represent.
@ -103,21 +55,33 @@ class Script:
updating the task data.
"""
def make_closure_validate(
subclass: type[Script], task: Task, workflow_id: int
def make_closure(
subclass: type[Script], task: SpiffTask, environment_identifier: str
) -> Callable:
"""Make_closure_validate."""
"""Yes - this is black magic.
Essentially, we want to build a list of all of the submodules (i.e. email, user_data_get, etc)
and a function that is assocated with them.
This basically creates an Instance of the class and returns a function that calls do_task
on the instance of that class.
the next for x in range, then grabs the name of the module and associates it with the function
that we created.
"""
instance = subclass()
return lambda *a, **b: subclass.do_task_validate_only(
instance, task, workflow_id, *a, **b
return lambda *ar, **kw: subclass.run(
instance,
task,
environment_identifier,
*ar,
**kw,
)
execlist = {}
subclasses = Script.get_all_subclasses()
for x in range(len(subclasses)):
subclass = subclasses[x]
execlist[subclass.__module__.split(".")[-1]] = make_closure_validate(
subclass, task, workflow_id
execlist[subclass.__module__.split(".")[-1]] = make_closure(
subclass, task=task, environment_identifier=environment_identifier
)
return execlist
@ -146,29 +110,3 @@ class Script:
all_subclasses.extend(Script._get_all_subclasses(subclass))
return all_subclasses
def add_data_to_task(self, task: Task, data: Any) -> None:
"""Add_data_to_task."""
key = self.__class__.__name__
if task.data is None:
task.data = {}
if key in task.data:
task.data[key].update(data)
else:
task.data[key] = data
class ScriptValidationError:
"""ScriptValidationError."""
def __init__(self, code: str, message: str):
"""__init__."""
self.code = code
self.message = message
@classmethod
def from_api_error(cls, api_error: ApiError) -> ScriptValidationError:
"""From_api_error."""
return cls(api_error.code, api_error.message)

View File

@ -4,6 +4,7 @@ import logging
import os
import time
from typing import Any
from typing import Callable
from typing import Dict
from typing import List
from typing import NewType
@ -68,6 +69,7 @@ from spiffworkflow_backend.models.process_model import ProcessModelInfo
from spiffworkflow_backend.models.task_event import TaskAction
from spiffworkflow_backend.models.task_event import TaskEventModel
from spiffworkflow_backend.models.user import UserModelSchema
from spiffworkflow_backend.scripts.script import Script
from spiffworkflow_backend.services.file_system_service import FileSystemService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
from spiffworkflow_backend.services.service_task_service import ServiceTaskService
@ -86,6 +88,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
scripts directory available for execution.
"""
def __get_augment_methods(self, task: SpiffTask) -> Dict[str, Callable]:
"""__get_augment_methods."""
return Script.generate_augmented_list(task, current_app.env)
def evaluate(self, task: SpiffTask, expression: str) -> Any:
"""Evaluate."""
return self._evaluate(expression, task.data, task)
@ -97,11 +103,14 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
task: Optional[SpiffTask] = None,
external_methods: Optional[Dict[str, Any]] = None,
) -> Any:
"""_evaluate."""
methods = self.__get_augment_methods(task)
if external_methods:
methods.update(external_methods)
"""Evaluate the given expression, within the context of the given task and return the result."""
try:
return super()._evaluate(
expression, context, external_methods=external_methods
)
return super()._evaluate(expression, context, external_methods=methods)
except Exception as exception:
if task is None:
raise ProcessInstanceProcessorError(
@ -120,7 +129,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore
) -> None:
"""Execute."""
try:
super().execute(task, script, external_methods)
methods = self.__get_augment_methods(task)
if external_methods:
methods.update(external_methods)
super().execute(task, script, methods)
except WorkflowException as e:
raise e
except Exception as e:

View File

@ -33,7 +33,7 @@ class ScriptUnitTestRunner:
_script_engine = CustomBpmnScriptEngine()
@classmethod
def run_with_task_and_script_and_pre_post_contexts(
def run_with_script_and_pre_post_contexts(
cls,
script: str,
input_context: PythonScriptContext,
@ -116,6 +116,6 @@ class ScriptUnitTestRunner:
)
script = task.task_spec.script
return cls.run_with_task_and_script_and_pre_post_contexts(
return cls.run_with_script_and_pre_post_contexts(
script, input_context, expected_output_context
)

View File

@ -1,5 +1,5 @@
<?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_1kbzkan" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="4.2.0">
<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_1kbzkan" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0">
<bpmn:process id="Process_SimpleScript" name="Simple Script" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1k9q28c</bpmn:outgoing>
@ -15,7 +15,8 @@
<bpmn:outgoing>Flow_1fviiob</bpmn:outgoing>
<bpmn:script>a = 1
b = 2
c = a + b</bpmn:script>
c = a + b
norris=fact_service(type='norris')</bpmn:script>
</bpmn:scriptTask>
<bpmn:manualTask id="Activity_DisplayData" name="Display Data">
<bpmn:documentation>## Display Data

View File

@ -0,0 +1,23 @@
"""Test_environment_var_script."""
from flask import Flask
from tests.spiffworkflow_backend.helpers.base_test import BaseTest
from spiffworkflow_backend.services.process_instance_processor import (
ProcessInstanceProcessor,
)
class TestEnvironmentVarScript(BaseTest):
"""TestEnvironmentVarScript."""
# it's not totally obvious we want to keep this test/file
def test_script_engine_can_use_custom_scripts(
self,
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_script_engine_takes_data_and_returns_expected_results."""
with app.app_context():
script_engine = ProcessInstanceProcessor._script_engine
result = script_engine._evaluate("get_env()", {})
assert result == "testing"

View File

@ -21,3 +21,15 @@ class TestProcessInstanceProcessor(BaseTest):
result = script_engine._evaluate("a", {"a": 1})
assert result == 1
def test_script_engine_can_use_custom_scripts(
app: Flask,
with_db_and_bpmn_file_cleanup: None,
) -> None:
"""Test_script_engine_takes_data_and_returns_expected_results."""
script_engine = ProcessInstanceProcessor._script_engine
result = script_engine._evaluate("fact_service(type='norris')", {})
assert (
result
== "Chuck Norris doesnt read books. He stares them down until he gets the information he wants."
)

View File

@ -38,10 +38,8 @@ class TestScriptUnitTestRunner(BaseTest):
expected_output_context: PythonScriptContext = {"a": 2}
script = "a = 2"
unit_test_result = (
ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
script, input_context, expected_output_context
)
unit_test_result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
script, input_context, expected_output_context
)
assert unit_test_result.result
@ -72,10 +70,8 @@ class TestScriptUnitTestRunner(BaseTest):
expected_output_context: PythonScriptContext = {"a": 2, "b": 3}
script = "a = 2"
unit_test_result = (
ScriptUnitTestRunner.run_with_task_and_script_and_pre_post_contexts(
script, input_context, expected_output_context
)
unit_test_result = ScriptUnitTestRunner.run_with_script_and_pre_post_contexts(
script, input_context, expected_output_context
)
assert unit_test_result.result is not True