def test_focus_state(): @step("Step that works on substate") def substep(): return {"result": "substep"} subwf = focussteps("sub") wf = workflow("Workflow with sub workflow that focuses on sub state")( lambda: subwf(substep) >> done) log = [] pstat = create_new_process_stat(wf, {"sub": {}}) result = runwf(pstat, store(log)) assert_complete(result) assert_state(result, {"sub": {"result": "substep"}}) # Test on empty key subwf = focussteps("sub") wf = workflow("Workflow with sub workflow that focuses on sub state")( lambda: subwf(substep) >> done) log = [] pstat = create_new_process_stat(wf, {}) result = runwf(pstat, store(log)) assert_complete(result) assert_state(result, {"sub": {"result": "substep"}})
def test_exception_log_step(): wf = workflow("Failing workflow")(lambda: init >> done) def failing_store(stat, step, state): raise Exception("Failing store error") with pytest.raises(Exception) as exc_info: pstat = create_new_process_stat(wf, {"name": "init-state"}) runwf(pstat, failing_store) assert "Failing store error" in str(exc_info.value)
def test_store_all_steps(): log = [] pstat = create_new_process_stat(sample_workflow, {}) runwf(pstat, store(log)) assert [ ("Step 1", Success({"steps": [1]})), ("Step 2", Success({"steps": [1, 2]})), ("Step 3", Success({"steps": [1, 2, 3]})), ] == log
def test_resume_suspended_workflow(): wf = workflow("Workflow with user interaction")( lambda: begin >> step1 >> user_action >> step2) log = [] p = ProcessStat( pid=1, workflow=wf, state=Suspend({ "steps": [1], "name": "Jane Doe" }), log=wf.steps[1:], current_user="******", ) result = runwf(p, logstep=store(log)) assert_success(result) assert result == Success({"steps": [1, 2], "name": "Jane Doe"}) assert [ ("Input Name", Success({ "steps": [1], "name": "Jane Doe" })), ("Step 2", Success({ "steps": [1, 2], "name": "Jane Doe" })), ] == log
def test_input_in_substate() -> None: @inputstep("Input Name", assignee=Assignee.SYSTEM) def input_action(state: State) -> FormGenerator: class SubForm(FormPage): a: int class TestForm(FormPage): sub: SubForm user_input = yield TestForm return user_input.dict() wf = workflow("Workflow with user interaction")( lambda: begin >> input_action >> purestep("process inputs")(Success)) log: List[Tuple[str, Process]] = [] pid = uuid4() p = ProcessStat(pid=pid, workflow=wf, state=Suspend({"sub": { "a": 1, "b": 2 }}), log=wf.steps[1:], current_user="******") result = runwf(p, logstep=store(log)) assert_success(result) assert_state(result, {"sub": {"a": 1, "b": 2}})
def test_exec_through_all_steps(): log = [] pstat = create_new_process_stat(sample_workflow, {}) result = runwf(pstat, store(log)) assert_success(result) assert_state(result, {"steps": [1, 2, 3]})
def test_complete(): wf = workflow("WF")(lambda: init >> done) log = [] pstat = create_new_process_stat(wf, {"name": "completion"}) result = runwf(pstat, store(log)) assert_complete(result) assert_state(result, {"name": "completion"})
def start_process( workflow_key: str, user_inputs: Optional[List[State]] = None, user: str = SYSTEM_USER, broadcast_func: Optional[BroadcastFunc] = None, ) -> Tuple[UUID, Future]: """Start a process for workflow. Args: workflow_key: name of workflow user_inputs: List of form inputs from frontend user: User who starts this process broadcast_func: Optional function to broadcast process data Returns: process id """ # ATTENTION!! When modifying this function make sure you make similar changes to `run_workflow` in the test code if user_inputs is None: user_inputs = [{}] pid = uuid4() workflow = get_workflow(workflow_key) if not workflow: raise_status(HTTPStatus.NOT_FOUND, "Workflow does not exist") initial_state = { "process_id": pid, "reporter": user, "workflow_name": workflow_key, "workflow_target": workflow.target, } try: state = post_process(workflow.initial_input_form, initial_state, user_inputs) except FormValidationError: logger.exception("Validation errors", user_inputs=user_inputs) raise pstat = ProcessStat(pid, workflow=workflow, state=Success({ **state, **initial_state }), log=workflow.steps, current_user=user) _db_create_process(pstat) _safe_logstep_withfunc = partial(_safe_logstep, broadcast_func=broadcast_func) return _run_process_async(pstat.pid, lambda: runwf(pstat, _safe_logstep_withfunc))
def test_abort(): wf = workflow("Aborting workflow")(lambda: init >> abort) log = [] pstat = create_new_process_stat(wf, {"name": "aborting"}) result = runwf(pstat, store(log)) assert_aborted(result) assert_state(result, {"name": "aborting"})
def test_failed_log_step(): wf = workflow("Failing workflow")(lambda: init >> done) def failing_store(stat, step, state): return Failed(error_state_to_dict(Exception("Failure Message"))) pstat = create_new_process_stat(wf, {"name": "init-state"}) result = runwf(pstat, failing_store) assert_failed(result) assert extract_error(result) == "Failure Message"
def test_skip_step(): wf = workflow("Workflow with skipped step")( lambda: init >> purestep("Skipped")(Skipped) >> done) log = [] pstat = create_new_process_stat(wf, {}) result = runwf(pstat, store(log)) assert_complete(result) skipped = [x[1] for x in log if x[0] == "Skipped"] assert skipped[0].isskipped()
def test_suspend(): wf = workflow("Workflow with user interaction")( lambda: begin >> step1 >> user_action >> step2) log = [] pstat = create_new_process_stat(wf, {}) result = runwf(pstat, store(log)) assert_suspended(result) assert [("Step 1", Success({"steps": [1]})), ("Input Name", Suspend({"steps": [1]}))] == log
def test_recover(): log = [] p = ProcessStat( pid=1, workflow=sample_workflow, state=Success({"steps": [4]}), log=sample_workflow.steps[1:], current_user="******", ) result = runwf(p, store(log)) assert_success(result) assert_state(result, {"steps": [4, 2, 3]})
def test_error_in_focus_state(): @step("Step that works on substate") def substep(): raise ValueError("Error") subwf = focussteps("sub") wf = workflow("Workflow with sub workflow that focuses on sub state")( lambda: subwf(substep) >> done) log = [] pstat = create_new_process_stat(wf, {"sub": {}}) result = runwf(pstat, store(log)) assert_failed(result) assert extract_error(result) == "Error"
def resume_process( process: ProcessTable, *, user_inputs: Optional[List[State]] = None, user: Optional[str] = None, broadcast_func: Optional[BroadcastFunc] = None, ) -> Tuple[UUID, Future]: """Resume a failed or suspended process. Args: process: Process from database user_inputs: Optional user input from forms user: user who resumed this process broadcast_func: Optional function to broadcast process data Returns: process id """ # ATTENTION!! When modifying this function make sure you make similar changes to `resume_workflow` in the test code if user_inputs is None: user_inputs = [{}] pstat = load_process(process) if pstat.workflow == removed_workflow: raise ValueError("This workflow cannot be resumed") form = pstat.log[0].form user_input = post_process(form, pstat.state.unwrap(), user_inputs) if user: pstat.update(current_user=user) if user_input: pstat.update(state=pstat.state.map( lambda state: StateMerger.merge(state, user_input))) # enforce an update to the process status to properly show the process process.last_status = ProcessStatus.RUNNING db.session.add(process) db.session.commit() _safe_logstep_prep = partial(_safe_logstep, broadcast_func=broadcast_func) return _run_process_async(pstat.pid, lambda: runwf(pstat, _safe_logstep_prep))
def test_failed_step(): wf = workflow("Failing workflow")(lambda: init >> fail) log = [] pstat = create_new_process_stat(wf, {"name": "init-state"}) result = runwf(pstat, store(log)) assert_failed(result) assert extract_error(result) == "Failure Message" assert [ ("Start", Success({"name": "init-state"})), ("Fail", Failed({ "class": "ValueError", "error": "Failure Message", "traceback": mock.ANY })), ] == log
def test_conditionally_skip_a_step(): @step("Inc N") def inc_n(n=0): return {"n": n + 1} limit_to_10 = conditional(lambda s: s.get("n", 0) < 10) incs = [limit_to_10(inc_n) for _ in range(0, 25)] wf = workflow("Limit the number of increments")( lambda: init >> reduce(lambda acc, e: acc >> e, incs) >> done) log = [] pstat = create_new_process_stat(wf, {}) result = runwf(pstat, store(log)) assert_complete(result) # from ipdb import set_trace; set_trace() assert_state(result, {"n": 10}) assert len([x for x in log if x[1].isskipped()]) == 15, "15 steps should be skipped"
def test_waiting(): wf = workflow("Workflow with soft fail")( lambda: begin >> step1 >> soft_fail >> step2) log = [] pstat = create_new_process_stat(wf, {}) result = runwf(pstat, store(log)) assert_waiting(result) assert extract_error(result) == "Failure Message" assert [ ("Step 1", Success({"steps": [1]})), ("Waiting step", Waiting({ "class": "ValueError", "error": "Failure Message", "traceback": mock.ANY })), ] == log
def test_resume_waiting_workflow(): hack = {"error": True} @retrystep("Waiting step") def soft_fail(): if hack["error"]: raise ValueError("error") else: return {"some_key": True} wf = workflow("Workflow with soft fail")( lambda: begin >> step1 >> soft_fail >> step2) log = [] state = Waiting({"steps": [1]}) hack["error"] = False p = ProcessStat(pid=1, workflow=wf, state=state, log=wf.steps[1:], current_user="******") result = runwf(p, logstep=store(log)) assert_success(result) assert [ ("Waiting step", Success({ "steps": [1], "some_key": True })), ("Step 2", Success({ "steps": [1, 2], "some_key": True })), ] == log