diff --git a/docs/images/error_boundary_event.png b/docs/images/error_boundary_event.png new file mode 100644 index 00000000..e4ddd0a2 Binary files /dev/null and b/docs/images/error_boundary_event.png differ diff --git a/docs/images/event_sub_process.png b/docs/images/event_sub_process.png new file mode 100644 index 00000000..fd39b59b Binary files /dev/null and b/docs/images/event_sub_process.png differ diff --git a/docs/supported-elements.md b/docs/supported-elements.md index 1dc70a1e..9bca69b9 100644 --- a/docs/supported-elements.md +++ b/docs/supported-elements.md @@ -28,6 +28,12 @@ There are some comments as well, which describe the level of support per each el * get & set variables from/to context (of the instance) is possible * variable mapping is supported (for input and output, see [Variables](#variables)) +## Error Boundary Event +![](images/error_boundary_event.png){: .width-60pt } + +* business errors raised by a task which requires modeling. +* multiple end events are supported as well. + ## Sub Process ![](images/sub_process.png){: .width-60pt } @@ -37,6 +43,13 @@ There are some comments as well, which describe the level of support per each el * sub-processes can have their own start and end events. * supports variable mapping for input and output, similar to tasks. * can be used to handle repetitive or complex logic within a process. +* +## Event Sub Process +![](images/event_sub_process.png){: .width-60pt } + +* Only Error Event Sub Process is supported +* event subprocess is a subprocess triggered by an event +* it must have an event based start event ## Gateways diff --git a/pkg/bpmn_engine/command.go b/pkg/bpmn_engine/command.go index 1ea76376..8ac06e63 100644 --- a/pkg/bpmn_engine/command.go +++ b/pkg/bpmn_engine/command.go @@ -5,10 +5,12 @@ import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20" type commandType string const ( - flowTransitionType commandType = "flowTransition" - activityType commandType = "activity" - continueActivityType commandType = "continueActivity" + flowTransitionType commandType = "flowTransition" + activityType commandType = "activity" + continueActivityType commandType = "continueActivity" + // A command that there is a technical error and the engine should fail the process instance errorType commandType = "error" + eventSubProcessCompletedType commandType = "eventSubProcessCompletedType" checkExclusiveGatewayDoneType commandType = "checkExclusiveGatewayDone" ) @@ -72,3 +74,12 @@ type checkExclusiveGatewayDoneCommand struct { func (t checkExclusiveGatewayDoneCommand) Type() commandType { return checkExclusiveGatewayDoneType } + +type eventSubProcessCompletedCommand struct { + // activity reference to the event sub-process, which has completed + activity activity +} + +func (t eventSubProcessCompletedCommand) Type() commandType { + return eventSubProcessCompletedType +} diff --git a/pkg/bpmn_engine/engine.go b/pkg/bpmn_engine/engine.go index b2ab1d0d..21637db2 100644 --- a/pkg/bpmn_engine/engine.go +++ b/pkg/bpmn_engine/engine.go @@ -203,11 +203,16 @@ func (state *BpmnEngineState) run(process BPMN20.ProcessElement, instance *proce instance.ActivityState = Failed // *activityState = Failed // TODO: check if meaningful break + case eventSubProcessCompletedType: + subProcessActivity := cmd.(eventSubProcessCompletedCommand).activity + instance.SetState(subProcessActivity.State()) + state.exportElementEvent(process, *instance, process, exporter.ElementCompleted) + break case checkExclusiveGatewayDoneType: activity := cmd.(checkExclusiveGatewayDoneCommand).gatewayActivity state.checkExclusiveGatewayDone(activity) default: - panic("[invariant check] command type check not fully implemented") + return newEngineErrorf("[invariant check] command type check not fully implemented") } } @@ -239,12 +244,41 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a state.exportElementEvent(process, *instance, *element, exporter.ElementCompleted) // special case here, to end the instance case BPMN20.ServiceTask: taskElement := (*element).(BPMN20.TaskElement) - _, activity = state.handleServiceTask(process, instance, &taskElement) - createFlowTransitions = activity.State() == Completed + _, job, jobErr := state.handleServiceTask(process, instance, &taskElement) + err = jobErr + activity = job + if err != nil { + nextCommands = append(nextCommands, errorCommand{ + err: err, + elementId: (*element).GetId(), + elementName: (*element).GetName(), + }) + } else if job.ErrorCode != "" { + // The current process will remain ACTIVE until the event sub-processes have completed. + nextCommands = handleErrorEvent(process, instance, element, job.ErrorCode) + createFlowTransitions = false // TODO confirm + } else { + // Only follow sequence flow if there are no Technical or Business Errors + createFlowTransitions = activity.State() == Completed + } case BPMN20.UserTask: taskElement := (*element).(BPMN20.TaskElement) - activity = state.handleUserTask(process, instance, &taskElement) - createFlowTransitions = activity.State() == Completed + job, jobErr := state.handleUserTask(process, instance, &taskElement) + err = jobErr + activity = job + if err != nil { + nextCommands = append(nextCommands, errorCommand{ + err: err, + elementId: (*element).GetId(), + elementName: (*element).GetName(), + }) + } else if job.ErrorCode != "" { + nextCommands = handleErrorEvent(process, instance, element, job.ErrorCode) + createFlowTransitions = false + } else { + // Only follow sequence flow if there are no Technical or Business Errors + createFlowTransitions = activity.State() == Completed + } case BPMN20.IntermediateCatchEvent: ice := (*element).(BPMN20.TIntermediateCatchEvent) createFlowTransitions, activity, err = state.handleIntermediateCatchEvent(process, instance, ice, originActivity) @@ -292,17 +326,31 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a createFlowTransitions = true case BPMN20.SubProcess: subProcessElement := (*element).(BPMN20.TSubProcess) - activity, err = state.handleSubProcess(instance, &subProcessElement) + subProcess, subProcessErr := state.handleSubProcess(instance, &subProcessElement) + activity = subProcess + err = subProcessErr if err != nil { nextCommands = append(nextCommands, errorCommand{ err: err, elementId: (*element).GetId(), elementName: (*element).GetName(), }) + } else if subProcessElement.TriggeredByEvent { + // We need to complete the parent process when an event sub-process has completed. but we cant do it here + nextCommands = append(nextCommands, eventSubProcessCompletedCommand{ + activity: subProcess, + }) } createFlowTransitions = activity.State() == Completed + case BPMN20.BoundaryEvent: + boundary := (*element).(BPMN20.TBoundaryEvent) + activity, err = state.handleBoundaryEvent(&boundary, instance) default: - panic(fmt.Sprintf("[invariant check] unsupported element: id=%s, type=%s", (*element).GetId(), (*element).GetType())) + nextCommands = append(nextCommands, errorCommand{ + err: newEngineErrorf("[invariant check] unsupported element: id=%s, type=%s", (*element).GetId(), (*element).GetType()), + elementId: (*element).GetId(), + elementName: (*element).GetName(), + }) } if createFlowTransitions && err == nil { nextCommands = append(nextCommands, createNextCommands(process, instance, element, activity)...) @@ -310,6 +358,115 @@ func (state *BpmnEngineState) handleElement(process BPMN20.ProcessElement, act a return nextCommands } +func handleErrorEvent(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.BaseElement, errorCode string) []command { + // Find the error by code on the process + if errT, found := findErrorDefinition(instance.ProcessInfo.definitions, errorCode); found { + + // Find the boundary events for the task + boundaryEvents := findBoundaryEventsForTypeAndReference(instance.ProcessInfo.definitions, BPMN20.ErrorBoundary, (*element).GetId()) + if boundaryEvent, foundBoundary := findBoundaryEventForError(boundaryEvents, errT.Id); foundBoundary { + return []command{ + activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](boundaryEvent)}, + } + } + + // If we still haven't found a command then we should look to see if there is an event sub process we can follow + if subProcess, subFound := findEventSubprocessForError(process, errT.Id); subFound { + return []command{ + activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](subProcess)}, + } + } + + // If not see if there is a catch-all boundary event + if boundaryEvent, foundBoundary := findBoundaryEventForError(boundaryEvents, ""); foundBoundary { + return []command{ + activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](boundaryEvent)}, + } + } + + // If not find an event sub process matching catchall + if subProcess, subFound := findEventSubprocessForError(process, ""); subFound { + return []command{ + activityCommand{element: BPMN20.Ptr[BPMN20.BaseElement](subProcess)}, + } + } + + // TODO continue lookup up to the parent process if this is a sub process - not supported yet + + return []command{ + errorCommand{ + err: newEngineErrorf("Could not find suitable handler for ErrorCode event id=%s, code=%s", errT.Id, errT.ErrorCode), + elementId: (*element).GetId(), + elementName: (*element).GetName(), + }, + } + } else { + return []command{ + errorCommand{ + err: newEngineErrorf("Could not find error definition \"%s\"", errorCode), + elementId: (*element).GetId(), + elementName: (*element).GetName(), + }, + } + } +} + +func findEventSubprocessForError(process BPMN20.ProcessElement, errorReferenceID string) (BPMN20.TSubProcess, bool) { + // Look for event sub-processes in the process + for _, subProcess := range process.GetSubProcess() { + // Check if this is an event sub-process (triggered by event) + if subProcess.TriggeredByEvent { + // Look for start events in the sub-process + for _, startEvent := range subProcess.StartEvents { + // Check if this start event has an error event definition + if startEvent.ErrorEventDefinition.ErrorRef == errorReferenceID { + // We found an event sub-process with an error start event + return subProcess, true + } + } + } + } + + // No matching event sub-process found + return BPMN20.TSubProcess{}, false +} + +// findBoundaryEventsForReference finds all boundary events attached to the provided element +func findBoundaryEventsForTypeAndReference(definitions BPMN20.TDefinitions, boundaryType BPMN20.BoundaryType, referenceID string) []BPMN20.TBoundaryEvent { + boundaryEvents := make([]BPMN20.TBoundaryEvent, 0) + for _, boundary := range definitions.Process.BoundaryEvent { + if boundary.AttachedToRef == referenceID && boundary.GetBoundaryType() == boundaryType { + boundaryEvents = append(boundaryEvents, boundary) + } + } + return boundaryEvents +} + +func findBoundaryEventForError(boundaryEvents []BPMN20.TBoundaryEvent, errorID string) (BPMN20.TBoundaryEvent, bool) { + for _, boundaryEvent := range boundaryEvents { + // Check if this boundary event has an error event definition + if boundaryEvent.ErrorEventDefinition.ErrorRef == errorID { + return boundaryEvent, true + } + } + return BPMN20.TBoundaryEvent{}, false +} + +func findErrorDefinition(definitions BPMN20.TDefinitions, errorCode string) (BPMN20.TError, bool) { + + // Iterate through all errors in the definitions + for _, err := range definitions.Errors { + // Check if the error code matches the requested code + if err.ErrorCode == errorCode { + return err, true + } + } + + // Return empty error if not found + return BPMN20.TError{}, false + +} + func createCheckExclusiveGatewayDoneCommand(originActivity activity) (cmds []command) { if (*originActivity.Element()).GetType() == BPMN20.EventBasedGateway { evtBasedGatewayActivity := originActivity.(*eventBasedGatewayActivity) @@ -473,3 +630,19 @@ func (state *BpmnEngineState) findCreatedTimers(instance *processInstanceInfo) ( } return result } + +func (state *BpmnEngineState) handleBoundaryEvent(element *BPMN20.TBoundaryEvent, instance *processInstanceInfo) (activity, error) { + var be BPMN20.BaseElement = element + activity := &elementActivity{ + key: state.generateKey(), + state: Completed, + element: &be, + } + variableHolder := NewVarHolder(&instance.VariableHolder, nil) + err := propagateProcessInstanceVariables(&variableHolder, element.GetOutputMapping()) + if err != nil { + instance.ActivityState = Failed + } + + return activity, err +} diff --git a/pkg/bpmn_engine/engine_test.go b/pkg/bpmn_engine/engine_test.go index 00f3a669..ce421737 100644 --- a/pkg/bpmn_engine/engine_test.go +++ b/pkg/bpmn_engine/engine_test.go @@ -1,6 +1,8 @@ package bpmn_engine import ( + "fmt" + "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine/exporter" "testing" "time" @@ -22,6 +24,41 @@ func (callPath *CallPath) TaskHandler(job ActivatedJob) { job.Complete() } +func pathString(paths []string) string { + path := "" + for _, p := range paths { + path += p + "\n" + } + return path +} + +// PathRecordingEventExporter records the paths taken during the execution of a process +// into a string. +type PathRecordingEventExporter struct { + paths []string +} + +func (e *PathRecordingEventExporter) String() string { + return pathString(e.paths) +} + +// NewEventLogExporter creates a new instance of a PathRecordingEventExporter +func NewPathRecordingEventExporter() *PathRecordingEventExporter { + return &PathRecordingEventExporter{ + paths: make([]string, 0), + } +} + +func (*PathRecordingEventExporter) NewProcessEvent(_ *exporter.ProcessEvent) {} + +func (*PathRecordingEventExporter) EndProcessEvent(_ *exporter.ProcessInstanceEvent) {} + +func (*PathRecordingEventExporter) NewProcessInstanceEvent(_ *exporter.ProcessInstanceEvent) {} + +func (e *PathRecordingEventExporter) NewElementEvent(_ *exporter.ProcessInstanceEvent, elementInfo *exporter.ElementInfo) { + e.paths = append(e.paths, fmt.Sprintf("%s(%s)", elementInfo.ElementId, elementInfo.Intent)) +} + func Test_BpmnEngine_interfaces_implemented(t *testing.T) { var _ BpmnEngine = &BpmnEngineState{} } @@ -215,6 +252,8 @@ func Test_CreateInstanceById_uses_latest_process_version(t *testing.T) { func Test_CreateAndRunInstanceById_uses_latest_process_version(t *testing.T) { // setup engine := New() + engine.NewTaskHandler().Id("id").Handler(jobCompleteHandler) + engine.NewTaskHandler().Id("test-2").Handler(jobCompleteHandler) // when v1, err := engine.LoadFromFile("../../test-cases/simple_task.bpmn") diff --git a/pkg/bpmn_engine/exporter/logging.go b/pkg/bpmn_engine/exporter/logging.go new file mode 100644 index 00000000..716ca551 --- /dev/null +++ b/pkg/bpmn_engine/exporter/logging.go @@ -0,0 +1,30 @@ +package exporter + +import "fmt" + +// LoggingEventExported writes all events to a log file +type LoggingEventExported struct { +} + +// NewEventLogExporter creates a new instance of a LoggingEventExported +func NewEventLogExporter() *LoggingEventExported { + return &LoggingEventExported{} +} + +func (*LoggingEventExported) NewProcessEvent(event *ProcessEvent) { + fmt.Printf("New Process event version: %d, processKey: %d, processID: %s\n", event.Version, event.ProcessKey, event.ProcessId) +} + +func (*LoggingEventExported) EndProcessEvent(event *ProcessInstanceEvent) { + fmt.Printf("End Process event version: %d, processKey: %d, processID: %s, processInstanceKey: %d\n", event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey) +} + +func (*LoggingEventExported) NewProcessInstanceEvent(event *ProcessInstanceEvent) { + fmt.Printf("New Process Instance version: %d, processKey: %d, processID: %s, processInstanceKey: %d\n", event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey) +} + +func (*LoggingEventExported) NewElementEvent(event *ProcessInstanceEvent, elementInfo *ElementInfo) { + fmt.Printf("New Element event version: %d, processKey: %d, processID: %s, processInstanceKey: %d, elementType: %s, elementId: %s, intent: %s\n", + event.Version, event.ProcessKey, event.ProcessId, event.ProcessInstanceKey, + elementInfo.BpmnElementType, elementInfo.ElementId, elementInfo.Intent) +} diff --git a/pkg/bpmn_engine/jobs.go b/pkg/bpmn_engine/jobs.go index e03117be..153df515 100644 --- a/pkg/bpmn_engine/jobs.go +++ b/pkg/bpmn_engine/jobs.go @@ -14,6 +14,10 @@ type job struct { JobState ActivityState `json:"s"` CreatedAt time.Time `json:"c"` baseElement *BPMN20.BaseElement + // Failure returned by a handler with job.Fail(string) + Failure string `json:"f,omitempty"` + // ErrorCode event thrown by a handler with job.ThrowError(string) + ErrorCode string `json:"ec,omitempty"` } func (j job) Key() int64 { diff --git a/pkg/bpmn_engine/jobs_activated.go b/pkg/bpmn_engine/jobs_activated.go index a16b38a0..4399a240 100644 --- a/pkg/bpmn_engine/jobs_activated.go +++ b/pkg/bpmn_engine/jobs_activated.go @@ -9,6 +9,7 @@ type activatedJob struct { processInstanceInfo *processInstanceInfo completeHandler func() failHandler func(reason string) + errorHandler func(errorCode string) key int64 processInstanceKey int64 bpmnProcessId string @@ -53,11 +54,15 @@ type ActivatedJob interface { CreatedAt() time.Time // Fail does set the State the worker missed completing the job - // Fail and Complete mutual exclude each other + // ThrowError, Fail and Complete mutual exclude each other Fail(reason string) + // ThrowError throws an error event + // ThrowError, Fail and Complete mutual exclude each other + ThrowError(errorCode string) + // Complete does set the State the worker successfully completing the job - // Fail and Complete mutual exclude each other + // ThrowError, Fail and Complete mutual exclude each other Complete() } @@ -116,6 +121,11 @@ func (aj *activatedJob) Fail(reason string) { aj.failHandler(reason) } +// ThrowError implements ActivatedJob +func (aj *activatedJob) ThrowError(errorCode string) { + aj.errorHandler(errorCode) +} + // Complete implements ActivatedJob func (aj *activatedJob) Complete() { aj.completeHandler() diff --git a/pkg/bpmn_engine/jobs_test.go b/pkg/bpmn_engine/jobs_test.go index 12460b38..0c1e2d25 100644 --- a/pkg/bpmn_engine/jobs_test.go +++ b/pkg/bpmn_engine/jobs_test.go @@ -1,6 +1,8 @@ package bpmn_engine import ( + "github.com/corbym/gocrest" + "github.com/nitram509/lib-bpmn-engine/pkg/bpmn_engine/exporter" "testing" "github.com/corbym/gocrest/has" @@ -33,7 +35,7 @@ func Test_job_implements_Activity(t *testing.T) { var _ activity = &job{} } -func Test_a_job_can_fail_and_keeps_the_instance_in_active_state(t *testing.T) { +func Test_a_job_can_fail_and_keeps_fails_the_instance(t *testing.T) { // setup bpmnEngine := New() process, _ := bpmnEngine.LoadFromFile("../../test-cases/simple_task.bpmn") @@ -41,7 +43,7 @@ func Test_a_job_can_fail_and_keeps_the_instance_in_active_state(t *testing.T) { instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) - then.AssertThat(t, instance.ActivityState, is.EqualTo(Active)) + then.AssertThat(t, instance.ActivityState, is.EqualTo(Failed)) } // Test_simple_count_loop requires correct Task-Output-Mapping in the BPMN file @@ -155,7 +157,7 @@ func Test_instance_fails_on_Invalid_Input_mapping(t *testing.T) { // when pi, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) - then.AssertThat(t, err, is.Nil()) + then.AssertThat(t, err.Error(), is.Not(is.Nil())) // then then.AssertThat(t, cp.CallPath, is.EqualTo("")) @@ -292,8 +294,9 @@ func Test_task_no_output_variables_mapping_on_failure(t *testing.T) { job.Fail("because I can") }) - instance, _ := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) - then.AssertThat(t, instance.ActivityState, is.EqualTo(Active)) + instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) + then.AssertThat(t, instance.ActivityState, is.EqualTo(Failed)) + then.AssertThat(t, err.Error(), is.EqualTo("because I can")) then.AssertThat(t, instance.GetVariable("aVariable"), is.Nil()) } @@ -380,3 +383,734 @@ func Test_multiple_instances_with_same_user_task_ids(t *testing.T) { then.AssertThat(t, called[instance1.GetInstanceKey()], has.Length(4)) then.AssertThat(t, called[instance2.GetInstanceKey()], has.Length(4)) } + +func Test_error_boundary_event(t *testing.T) { + + type handler struct { + fn func(job ActivatedJob) + id string + } + + type varAssertion struct { + assertion *gocrest.Matcher + key string + } + + type args struct { + file string + handlers []handler + } + + type wants struct { + instanceState ActivityState + processError *gocrest.Matcher + varAssertions []varAssertion + paths []string + } + + tests := []struct { + name string + args args + wants wants + }{ + { + name: "Single boundary error event", + args: args{ + file: "../../test-cases/error-boundary-event.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error1") + }, + }, + { + id: "handle-error-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-boundary-event(ELEMENT_ACTIVATED)", + "error-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-error(SEQUENCE_FLOW_TAKEN)", + "handle-error-task(ELEMENT_ACTIVATED)", + "handle-error-task(ELEMENT_COMPLETED)", + "flow-handled-error(SEQUENCE_FLOW_TAKEN)", + "handled-error-end(ELEMENT_ACTIVATED)", + "handled-error-end(ELEMENT_COMPLETED)", + "handled-error-end(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Catchall boundary error event with unknown error", + args: args{ + file: "../../test-cases/error-boundary-event-catchall.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("unknown_error") + }, + }, + { + id: "handle-error-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Failed, + processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Catchall boundary error event", + args: args{ + file: "../../test-cases/error-boundary-event-catchall.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error1") + }, + }, + { + id: "handle-error-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-boundary-event(ELEMENT_ACTIVATED)", + "error-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-error(SEQUENCE_FLOW_TAKEN)", + "handle-error-task(ELEMENT_ACTIVATED)", + "handle-error-task(ELEMENT_COMPLETED)", + "flow-handled-error(SEQUENCE_FLOW_TAKEN)", + "handled-error-end(ELEMENT_ACTIVATED)", + "handled-error-end(ELEMENT_COMPLETED)", + "handled-error-end(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Single boundary error event with output", + args: args{ + file: "../../test-cases/error-boundary-event-outputs.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.SetVariable("errorValue", "something broke") + job.ThrowError("error1") + }, + }, + { + id: "handle-error-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + { + key: "errorValue", + assertion: is.EqualTo("something broke"), + }, + { + key: "mappedErrorValue", + assertion: is.EqualTo("something broke"), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-boundary-event(ELEMENT_ACTIVATED)", + "error-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-error(SEQUENCE_FLOW_TAKEN)", + "handle-error-task(ELEMENT_ACTIVATED)", + "handle-error-task(ELEMENT_COMPLETED)", + "flow-handled-error(SEQUENCE_FLOW_TAKEN)", + "handled-error-end(ELEMENT_ACTIVATED)", + "handled-error-end(ELEMENT_COMPLETED)", + "handled-error-end(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Single boundary error event unknown error", + args: args{ + file: "../../test-cases/error-boundary-event.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("unknown_error") + }, + }, + }, + }, + wants: wants{ + instanceState: Failed, + processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Multi boundary error event unknown error", + args: args{ + file: "../../test-cases/error-boundary-event-multiple.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("unknown_error") + }, + }, + }, + }, + wants: wants{ + instanceState: Failed, + processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Multi boundary error event", + args: args{ + file: "../../test-cases/error-boundary-event-multiple.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error1") + }, + }, + { + id: "handle-error-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-boundary-event(ELEMENT_ACTIVATED)", + "error-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-error(SEQUENCE_FLOW_TAKEN)", + "handle-error-task(ELEMENT_ACTIVATED)", + "handle-error-task(ELEMENT_COMPLETED)", + "flow-handled-error(SEQUENCE_FLOW_TAKEN)", + "handled-error-end(ELEMENT_ACTIVATED)", + "handled-error-end(ELEMENT_COMPLETED)", + "handled-error-end(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Multi boundary error event catchall", + args: args{ + file: "../../test-cases/error-boundary-event-multiple.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error2") + }, + }, + { + id: "handle-all-task", + fn: func(job ActivatedJob) { + job.Complete() + }, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "all-boundary-event(ELEMENT_ACTIVATED)", + "all-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-all(SEQUENCE_FLOW_TAKEN)", + "handle-all-task(ELEMENT_ACTIVATED)", + "handle-all-task(ELEMENT_COMPLETED)", + "flow-handled-all(SEQUENCE_FLOW_TAKEN)", + "handled-all-end(ELEMENT_ACTIVATED)", + "handled-all-end(ELEMENT_COMPLETED)", + "handled-all-end(ELEMENT_COMPLETED)", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup + bpmnEngine := New() + pathRecordingExporter := NewPathRecordingEventExporter() + bpmnEngine.AddEventExporter(pathRecordingExporter) + bpmnEngine.AddEventExporter(exporter.NewEventLogExporter()) + process, _ := bpmnEngine.LoadFromFile(tt.args.file) + for _, handler := range tt.args.handlers { + bpmnEngine.NewTaskHandler().Id(handler.id).Handler(handler.fn) + } + instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) + then.AssertThat(t, instance.ActivityState, is.EqualTo(tt.wants.instanceState)) + then.AssertThat(t, err, tt.wants.processError) + then.AssertThat(t, pathRecordingExporter.String(), is.EqualTo(pathString(tt.wants.paths))) + + for _, varAssert := range tt.wants.varAssertions { + then.AssertThat(t, instance.GetVariable(varAssert.key), varAssert.assertion) + } + }) + } +} + +func Test_error_event_subprocess(t *testing.T) { + + type handler struct { + fn func(job ActivatedJob) + id string + } + + type varAssertion struct { + assertion *gocrest.Matcher + key string + } + + type args struct { + file string + handlers []handler + } + + type wants struct { + instanceState ActivityState + processError *gocrest.Matcher + varAssertions []varAssertion + paths []string + } + + tests := []struct { + name string + args args + wants wants + }{ + { + name: "Single boundary error event", + args: args{ + file: "../../test-cases/error-event-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error1") + }, + }, + { + id: "handle-error-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-sub-process(ELEMENT_ACTIVATED)", + "error1-start-event(ELEMENT_ACTIVATED)", + "error1-start-event(ELEMENT_COMPLETED)", + "flow-error1(SEQUENCE_FLOW_TAKEN)", + "handle-error-task(ELEMENT_ACTIVATED)", + "handle-error-task(ELEMENT_COMPLETED)", + "flow-error1-end(SEQUENCE_FLOW_TAKEN)", + "end-error1(ELEMENT_ACTIVATED)", + "end-error1(ELEMENT_COMPLETED)", + "end-error1(ELEMENT_COMPLETED)", + "error-sub-process(ELEMENT_COMPLETED)", + "error-event-subprocess(ELEMENT_COMPLETED)", + }, + }, + }, + { + name: "Single boundary error event catchall", + args: args{ + file: "../../test-cases/error-event-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error2") + }, + }, + { + id: "handle-catchall-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "catchall-sub-process(ELEMENT_ACTIVATED)", + "catchall-start-event(ELEMENT_ACTIVATED)", + "catchall-start-event(ELEMENT_COMPLETED)", + "flow-catchall(SEQUENCE_FLOW_TAKEN)", + "handle-catchall-task(ELEMENT_ACTIVATED)", + "handle-catchall-task(ELEMENT_COMPLETED)", + "flow-catchall-end(SEQUENCE_FLOW_TAKEN)", + "end-catchall(ELEMENT_ACTIVATED)", + "end-catchall(ELEMENT_COMPLETED)", + "end-catchall(ELEMENT_COMPLETED)", + "catchall-sub-process(ELEMENT_COMPLETED)", + "error-event-subprocess(ELEMENT_COMPLETED)", + }, + }, + }, + { + // In this test we expect that the error boundary event is triggered + name: "Multiple boundary error events and Subprocesses error1", + args: args{ + file: "../../test-cases/error-boundary-event-and-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error1") + }, + }, + { + id: "handle-error1-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-boundary-event(ELEMENT_ACTIVATED)", + "error-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-error(SEQUENCE_FLOW_TAKEN)", + "handle-error1-task(ELEMENT_ACTIVATED)", + "handle-error1-task(ELEMENT_COMPLETED)", + "flow-handled-error(SEQUENCE_FLOW_TAKEN)", + "handled-error-end(ELEMENT_ACTIVATED)", + "handled-error-end(ELEMENT_COMPLETED)", + "handled-error-end(ELEMENT_COMPLETED)", + }, + }, + }, + { + // In this test we expect that the error2 event subprocess is triggered + name: "Multiple boundary error events and Subprocesses error2", + args: args{ + file: "../../test-cases/error-boundary-event-and-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error2") + }, + }, + { + id: "handle-error2-sub-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "error-sub-process(ELEMENT_ACTIVATED)", + "error1-start-event(ELEMENT_ACTIVATED)", + "error1-start-event(ELEMENT_COMPLETED)", + "flow-error2(SEQUENCE_FLOW_TAKEN)", + "handle-error2-sub-task(ELEMENT_ACTIVATED)", + "handle-error2-sub-task(ELEMENT_COMPLETED)", + "flow-error2-end(SEQUENCE_FLOW_TAKEN)", + "end-error1(ELEMENT_ACTIVATED)", + "end-error1(ELEMENT_COMPLETED)", + "end-error1(ELEMENT_COMPLETED)", + "error-sub-process(ELEMENT_COMPLETED)", + "error-boundary-event-and-subprocess(ELEMENT_COMPLETED)", + }, + }, + }, + { + // In this test we expect that the catchall error boundary event is triggered + name: "Multiple boundary error events and Subprocesses error3", + args: args{ + file: "../../test-cases/error-boundary-event-and-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("error3") + }, + }, + { + id: "handle-all-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Completed, + processError: is.Nil(), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + "all-boundary-event(ELEMENT_ACTIVATED)", + "all-boundary-event(ELEMENT_COMPLETED)", + "flow-handle-all(SEQUENCE_FLOW_TAKEN)", + "handle-all-task(ELEMENT_ACTIVATED)", + "handle-all-task(ELEMENT_COMPLETED)", + "flow-handled-all(SEQUENCE_FLOW_TAKEN)", + "handled-all-end(ELEMENT_ACTIVATED)", + "handled-all-end(ELEMENT_COMPLETED)", + "handled-all-end(ELEMENT_COMPLETED)", + }, + }, + }, { + name: "Single boundary error event unknown error", + args: args{ + file: "../../test-cases/error-event-subprocess.bpmn", + handlers: []handler{ + { + id: "error-task", + fn: func(job ActivatedJob) { + job.SetVariable("aVariable", true) + job.ThrowError("unknown_error") + }, + }, + { + id: "handle-error-task", + fn: jobCompleteHandler, + }, + }, + }, + wants: wants{ + instanceState: Failed, + processError: is.EqualTo(newEngineErrorf("Could not find error definition \"unknown_error\"")), + varAssertions: []varAssertion{ + { + key: "aVariable", + assertion: is.True(), + }, + }, + paths: []string{ + "start(ELEMENT_ACTIVATED)", + "start(ELEMENT_COMPLETED)", + "flow1(SEQUENCE_FLOW_TAKEN)", + "error-task(ELEMENT_ACTIVATED)", + "error-task(ELEMENT_COMPLETED)", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // setup + bpmnEngine := New() + pathExporter := NewPathRecordingEventExporter() + bpmnEngine.AddEventExporter(pathExporter) + bpmnEngine.AddEventExporter(exporter.NewEventLogExporter()) + process, _ := bpmnEngine.LoadFromFile(tt.args.file) + for _, handler := range tt.args.handlers { + bpmnEngine.NewTaskHandler().Id(handler.id).Handler(handler.fn) + } + instance, err := bpmnEngine.CreateAndRunInstance(process.ProcessKey, nil) + then.AssertThat(t, instance.ActivityState, is.EqualTo(tt.wants.instanceState)) + then.AssertThat(t, err, tt.wants.processError) + then.AssertThat(t, pathExporter.String(), is.EqualTo(pathString(tt.wants.paths))) + + for _, varAssert := range tt.wants.varAssertions { + then.AssertThat(t, instance.GetVariable(varAssert.key), varAssert.assertion) + } + }) + } + +} + +// TODO boundaryEvent and eventSubProcesses where specific boundary event is selected +// TODO boundaryEvent and eventSubProcesses where specific sub process is selected +// TODO boundaryEvent and eventSubProcesses where boundaryevent catch all is selected diff --git a/pkg/bpmn_engine/tasks.go b/pkg/bpmn_engine/tasks.go index cb6a9a0c..a27f41b5 100644 --- a/pkg/bpmn_engine/tasks.go +++ b/pkg/bpmn_engine/tasks.go @@ -1,8 +1,10 @@ package bpmn_engine -import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20" +import ( + "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20" +) -func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (bool, *job) { +func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (bool, *job, error) { job := findOrCreateJob(&state.jobs, element, instance, state.generateKey) handler := state.findTaskHandler(element) @@ -10,8 +12,15 @@ func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, i job.JobState = Active variableHolder := NewVarHolder(&instance.VariableHolder, nil) activatedJob := &activatedJob{ - processInstanceInfo: instance, - failHandler: func(reason string) { job.JobState = Failed }, + processInstanceInfo: instance, + failHandler: func(reason string) { + job.Failure = reason + job.JobState = Failed + }, + errorHandler: func(error string) { + job.ErrorCode = error + job.JobState = Terminated + }, completeHandler: func() { job.JobState = Completed }, key: state.generateKey(), processInstanceKey: instance.InstanceKey, @@ -25,22 +34,26 @@ func (state *BpmnEngineState) handleServiceTask(process BPMN20.ProcessElement, i if err := evaluateLocalVariables(&variableHolder, (*element).GetInputMapping()); err != nil { job.JobState = Failed instance.ActivityState = Failed - return false, job + return false, job, err } handler(activatedJob) - if job.JobState == Completed { + if job.JobState == Completed || job.JobState == Terminated { if err := propagateProcessInstanceVariables(&variableHolder, (*element).GetOutputMapping()); err != nil { job.JobState = Failed instance.ActivityState = Failed } + } else if job.JobState == Failed { + // If there is a technical error with the job, fail the instance + instance.ActivityState = Failed + return false, job, newEngineErrorf(job.Failure) } } - return job.JobState == Completed, job + return job.JobState == Completed, job, nil } -func (state *BpmnEngineState) handleUserTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) *job { +func (state *BpmnEngineState) handleUserTask(process BPMN20.ProcessElement, instance *processInstanceInfo, element *BPMN20.TaskElement) (*job, error) { // TODO consider different handlers, since Service Tasks are different in their definition than user tasks - _, j := state.handleServiceTask(process, instance, element) - return j + _, j, err := state.handleServiceTask(process, instance, element) + return j, err } diff --git a/pkg/spec/BPMN20/bpmn_structs.go b/pkg/spec/BPMN20/bpmn_structs.go index 1e643711..b6a1833c 100644 --- a/pkg/spec/BPMN20/bpmn_structs.go +++ b/pkg/spec/BPMN20/bpmn_structs.go @@ -12,6 +12,18 @@ type TDefinitions struct { ExporterVersion string `xml:"exporterVersion,attr"` Process TProcess `xml:"process"` Messages []TMessage `xml:"message"` + Errors []TError `xml:"error"` +} + +type TError struct { + Id string `xml:"id,attr"` + Name string `xml:"name,attr"` + ErrorCode string `xml:"errorCode,attr"` +} + +type TErrorEventDefinition struct { + Id string `xml:"id,attr"` + ErrorRef string `xml:"errorRef,attr"` } type TCallableElement struct { @@ -37,6 +49,16 @@ type TProcess struct { IntermediateTrowEvent []TIntermediateThrowEvent `xml:"intermediateThrowEvent"` EventBasedGateway []TEventBasedGateway `xml:"eventBasedGateway"` InclusiveGateway []TInclusiveGateway `xml:"inclusiveGateway"` + BoundaryEvent []TBoundaryEvent `xml:"boundaryEvent"` +} + +type TBoundaryEvent struct { + TBaseElement + Name string `xml:"name,attr"` + AttachedToRef string `xml:"attachedToRef,attr"` + OutgoingAssociation []string `xml:"outgoing"` + ErrorEventDefinition *TErrorEventDefinition `xml:"errorEventDefinition,omitempty"` + Output []extensions.TIoMapping `xml:"extensionElements>ioMapping>output"` } type TSubProcess struct { @@ -54,6 +76,7 @@ type TSubProcess struct { IntermediateTrowEvent []TIntermediateThrowEvent `xml:"intermediateThrowEvent"` EventBasedGateway []TEventBasedGateway `xml:"eventBasedGateway"` InclusiveGateway []TInclusiveGateway `xml:"inclusiveGateway"` + BoundaryEvent []TBoundaryEvent `xml:"boundaryEvent"` } // TBaseElement is an "abstract" struct @@ -111,12 +134,14 @@ type TExpression struct { type TStartEvent struct { TCatchEvent - IsInterrupting bool `xml:"isInterrupting,attr"` - ParallelMultiple bool `xml:"parallelMultiple,attr"` + IsInterrupting bool `xml:"isInterrupting,attr"` + ParallelMultiple bool `xml:"parallelMultiple,attr"` + ErrorEventDefinition TErrorEventDefinition `xml:"errorEventDefinition"` } type TEndEvent struct { TThrowEvent + ErrorEventDefinition TErrorEventDefinition `xml:"errorEventDefinition"` } type TServiceTask struct { diff --git a/pkg/spec/BPMN20/elements.go b/pkg/spec/BPMN20/elements.go index 0f312013..93ab9d81 100644 --- a/pkg/spec/BPMN20/elements.go +++ b/pkg/spec/BPMN20/elements.go @@ -4,6 +4,7 @@ import "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/extensions" type ElementType string type GatewayDirection string +type BoundaryType string const ( Process ElementType = "PROCESS" @@ -18,13 +19,17 @@ const ( IntermediateThrowEvent ElementType = "INTERMEDIATE_THROW_EVENT" EventBasedGateway ElementType = "EVENT_BASED_GATEWAY" InclusiveGateway ElementType = "INCLUSIVE_GATEWAY" - - SequenceFlow ElementType = "SEQUENCE_FLOW" + SequenceFlow ElementType = "SEQUENCE_FLOW" + BoundaryEvent ElementType = "BOUNDARY_EVENT" Unspecified GatewayDirection = "Unspecified" Converging GatewayDirection = "Converging" Diverging GatewayDirection = "Diverging" Mixed GatewayDirection = "Mixed" + + // Type of boundary event, error, message, timer, etc + ErrorBoundary BoundaryType = "Error" + UnknownBoundary BoundaryType = "Unknown" ) type BaseElement interface { @@ -65,6 +70,7 @@ type ProcessElement interface { GetEventBasedGateway() []TEventBasedGateway GetSubProcess() []TSubProcess GetInclusiveGateway() []TInclusiveGateway + GetBoundaryEvent() []TBoundaryEvent } func (startEvent TStartEvent) GetId() string { @@ -428,6 +434,10 @@ func (process TProcess) GetInclusiveGateway() []TInclusiveGateway { return process.InclusiveGateway } +func (process TProcess) GetBoundaryEvent() []TBoundaryEvent { + return process.BoundaryEvent +} + func (subProcess TSubProcess) GetId() string { return subProcess.Id } @@ -495,3 +505,38 @@ func (subProcess TSubProcess) GetSubProcess() []TSubProcess { func (subProcess TSubProcess) GetInclusiveGateway() []TInclusiveGateway { return subProcess.InclusiveGateway } + +func (process TSubProcess) GetBoundaryEvent() []TBoundaryEvent { + return process.BoundaryEvent +} + +func (be TBoundaryEvent) GetId() string { + return be.Id +} + +func (be TBoundaryEvent) GetName() string { + return be.Name +} + +func (be TBoundaryEvent) GetIncomingAssociation() []string { + return []string{} +} + +func (be TBoundaryEvent) GetOutgoingAssociation() []string { + return be.OutgoingAssociation +} + +func (be TBoundaryEvent) GetType() ElementType { + return BoundaryEvent +} + +func (be TBoundaryEvent) GetBoundaryType() BoundaryType { + if be.ErrorEventDefinition != nil { + return ErrorBoundary + } + return UnknownBoundary +} + +func (be TBoundaryEvent) GetOutputMapping() []extensions.TIoMapping { + return be.Output +} diff --git a/pkg/spec/BPMN20/elements_test.go b/pkg/spec/BPMN20/elements_test.go index e3b1e08e..bccae29f 100644 --- a/pkg/spec/BPMN20/elements_test.go +++ b/pkg/spec/BPMN20/elements_test.go @@ -1,6 +1,13 @@ package BPMN20 -import "testing" +import ( + "github.com/corbym/gocrest/has" + "github.com/nitram509/lib-bpmn-engine/pkg/spec/BPMN20/extensions" + "testing" + + "github.com/corbym/gocrest/is" + "github.com/corbym/gocrest/then" +) // tests to get quick compiler warnings, when interface is not correctly implemented @@ -18,4 +25,55 @@ func Test_all_interfaces_implemented(t *testing.T) { var _ BaseElement = &TIntermediateThrowEvent{} var _ BaseElement = &TEventBasedGateway{} var _ BaseElement = &TInclusiveGateway{} + var _ BaseElement = &TBoundaryEvent{} +} + +func Test_ErrorBoundaryEvent(t *testing.T) { + + event := TBoundaryEvent{ + TBaseElement: TBaseElement{Id: "event_1", Documentation: "Documentation"}, + Name: "Boundary Event", + AttachedToRef: "task_1", + OutgoingAssociation: []string{"flow_1"}, + ErrorEventDefinition: &TErrorEventDefinition{ + Id: "errorDef_1", + ErrorRef: "error_1", + }, + Output: []extensions.TIoMapping{ + { + Source: "ioSource", + Target: "ioTarget", + }, + }, + } + then.AssertThat(t, event.GetId(), is.EqualTo("event_1")) + then.AssertThat(t, event.GetName(), is.EqualTo("Boundary Event")) + then.AssertThat(t, event.GetIncomingAssociation(), has.Length(0)) + then.AssertThat(t, event.GetOutgoingAssociation(), has.Length(1)) + then.AssertThat(t, event.GetType(), is.EqualTo(BoundaryEvent)) + then.AssertThat(t, event.GetBoundaryType(), is.EqualTo(ErrorBoundary)) + then.AssertThat(t, event.GetOutputMapping(), has.Length(1)) +} + +func Test_UnknownBoundaryEvent(t *testing.T) { + + event := TBoundaryEvent{ + TBaseElement: TBaseElement{Id: "event_1", Documentation: "Documentation"}, + Name: "Boundary Event", + AttachedToRef: "task_1", + OutgoingAssociation: []string{"flow_1"}, + Output: []extensions.TIoMapping{ + { + Source: "ioSource", + Target: "ioTarget", + }, + }, + } + then.AssertThat(t, event.GetId(), is.EqualTo("event_1")) + then.AssertThat(t, event.GetName(), is.EqualTo("Boundary Event")) + then.AssertThat(t, event.GetIncomingAssociation(), has.Length(0)) + then.AssertThat(t, event.GetOutgoingAssociation(), has.Length(1)) + then.AssertThat(t, event.GetType(), is.EqualTo(BoundaryEvent)) + then.AssertThat(t, event.GetBoundaryType(), is.EqualTo(UnknownBoundary)) + then.AssertThat(t, event.GetOutputMapping(), has.Length(1)) } diff --git a/pkg/spec/BPMN20/helper.go b/pkg/spec/BPMN20/helper.go index 7199c4fc..d3b14012 100644 --- a/pkg/spec/BPMN20/helper.go +++ b/pkg/spec/BPMN20/helper.go @@ -76,6 +76,9 @@ func FindBaseElementsById(processElement ProcessElement, id string) (elements [] for _, inclusiveGateway := range processElement.GetInclusiveGateway() { appendWhenIdMatches(Ptr[BaseElement](inclusiveGateway)) } + for _, boundaryEvent := range processElement.GetBoundaryEvent() { + appendWhenIdMatches(Ptr[BaseElement](boundaryEvent)) + } for _, subProcess := range processElement.GetSubProcess() { appendWhenIdMatches(Ptr[BaseElement](subProcess)) // search recursively for further elements diff --git a/test-cases/error-boundary-event-and-subprocess.bpmn b/test-cases/error-boundary-event-and-subprocess.bpmn new file mode 100644 index 00000000..270fffd6 --- /dev/null +++ b/test-cases/error-boundary-event-and-subprocess.bpmn @@ -0,0 +1,204 @@ + + + + + + flow-catchall + + + + flow-catchall-end + + + + + + + + flow-catchall + flow-catchall-end + + + + + flow-error2-end + + + flow-error2 + + + + + + + + + flow-error2 + flow-error2-end + + + + flow1 + + + + + + flow1 + flow-success + + + flow-handled-error + + + flow-success + + + + + + flow-handle-error + flow-handled-error + + + flow-handled-all + + + + + + flow-handle-all + flow-handled-all + + + + + + + + flow-handle-all + + + + + + + + + flow-handle-error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-cases/error-boundary-event-catchall.bpmn b/test-cases/error-boundary-event-catchall.bpmn new file mode 100644 index 00000000..ad049f14 --- /dev/null +++ b/test-cases/error-boundary-event-catchall.bpmn @@ -0,0 +1,82 @@ + + + + + flow1 + + + + + + flow1 + flow-success + + + + + + + + + flow-handle-error + + + + flow-handled-error + + + + flow-success + + + + + + + flow-handle-error + flow-handled-error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-cases/error-boundary-event-multiple.bpmn b/test-cases/error-boundary-event-multiple.bpmn new file mode 100644 index 00000000..dd4960d2 --- /dev/null +++ b/test-cases/error-boundary-event-multiple.bpmn @@ -0,0 +1,125 @@ + + + + + flow1 + + + + + + flow1 + flow-success + + + + + + + + + flow-handle-error + + + + flow-handled-error + + + + flow-success + + + + + + + flow-handle-error + flow-handled-error + + + + + + + + + flow-handle-all + + + + + flow-handled-all + + + + + + + flow-handle-all + flow-handled-all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-cases/error-boundary-event-outputs.bpmn b/test-cases/error-boundary-event-outputs.bpmn new file mode 100644 index 00000000..02902c83 --- /dev/null +++ b/test-cases/error-boundary-event-outputs.bpmn @@ -0,0 +1,82 @@ + + + + + flow1 + + + + + + flow1 + flow-success + + + + + + + + + flow-handle-error + + + + flow-handled-error + + + + flow-success + + + + + + + flow-handle-error + flow-handled-error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test-cases/error-boundary-event.bpmn b/test-cases/error-boundary-event.bpmn new file mode 100644 index 00000000..2c1e287c --- /dev/null +++ b/test-cases/error-boundary-event.bpmn @@ -0,0 +1,77 @@ + + + + + flow1 + + + + + + flow1 + flow-success + + + + flow-handle-error + + + + flow-handled-error + + + + flow-success + + + + + + + flow-handle-error + flow-handled-error + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-cases/error-event-subprocess.bpmn b/test-cases/error-event-subprocess.bpmn new file mode 100644 index 00000000..b3a0fb34 --- /dev/null +++ b/test-cases/error-event-subprocess.bpmn @@ -0,0 +1,122 @@ + + + + + + flow-catchall + + + + flow-catchall-end + + + + + + + + flow-catchall + flow-catchall-end + + + + flow-success + + + + + + flow1 + flow-success + + + + flow-error1-end + + + flow-error1 + + + + + + + + + flow-error1 + flow-error1-end + + + + + + flow1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +