diff --git a/action.go b/action.go index 37900e9..259151f 100644 --- a/action.go +++ b/action.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/project-flogo/core/data/expression/script/gocc/ast" "strings" "time" @@ -397,13 +398,20 @@ func (fa *FlowAction) Run(ctx context.Context, inputs map[string]interface{}, ha inst.SetValue(k, v) } - fa.applyAssertionInterceptor(inst) + fa.applyAssertionInterceptor(inst, flowsupport.AssertionActivity) handler.HandleResult(returnData, err) } else if inst.Status() == model.FlowStatusFailed { + hasFlowExceptionAssert := fa.applyAssertionInterceptor(inst, flowsupport.AssertionException) + if inst.TracingContext() != nil { _ = trace.GetTracer().FinishTrace(inst.TracingContext(), inst.GetError()) } + // If we are in unit testing mode and flow has assertion exception then we dont make the flow fail + // The testcase will be passed or failed based on the assertions executed. + if hasFlowExceptionAssert { + handler.HandleResult(nil, nil) + } handler.HandleResult(nil, inst.GetError()) } @@ -423,14 +431,22 @@ func (fa *FlowAction) Run(ctx context.Context, inputs map[string]interface{}, ha return nil } -func (fa *FlowAction) applyAssertionInterceptor(inst *instance.IndependentInstance) { +func (fa *FlowAction) applyAssertionInterceptor(inst *instance.IndependentInstance, assertType int) bool { + if !inst.HasInterceptor() { + return false + } + hasFlowExceptionAssertion := false if inst.GetInterceptor() != nil { interceptor := inst.GetInterceptor().GetTaskInterceptor(inst.Instance.Name() + "-_flowOutput") if interceptor != nil { interceptor.Result = flowsupport.Pass ef := expression.NewFactory(definition.GetDataResolver()) for id, assertion := range interceptor.Assertions { + if interceptor.Type != assertType { + interceptor.Assertions[id].Result = flowsupport.AssertionNotExecuted + continue + } if assertion.Expression == "" { interceptor.Assertions[id].Message = "Empty expression" interceptor.Assertions[id].Result = flowsupport.NotExecuted @@ -448,6 +464,11 @@ func (fa *FlowAction) applyAssertionInterceptor(inst *instance.IndependentInstan interceptor.Assertions[id].Result = flowsupport.Fail interceptor.Assertions[id].Message = "Failed to evaluate expression" } else { + exp, ok := expr.(ast.ExprEvalResult) + var resultData ast.ExprEvalData + if ok { + resultData = exp.Detail() + } res, _ := coerce.ToBool(result) if res { interceptor.Assertions[id].Result = flowsupport.Pass @@ -456,10 +477,16 @@ func (fa *FlowAction) applyAssertionInterceptor(inst *instance.IndependentInstan interceptor.Assertions[id].Result = flowsupport.Fail interceptor.Assertions[id].Message = "Comparison failure" } + interceptor.Assertions[id].EvalResult = resultData + if assertType == flowsupport.AssertionException { + hasFlowExceptionAssertion = true + } + } } } } + return hasFlowExceptionAssertion } diff --git a/instance/ind_instance.go b/instance/ind_instance.go index 55e439d..ed2ad25 100755 --- a/instance/ind_instance.go +++ b/instance/ind_instance.go @@ -252,6 +252,10 @@ func (inst *IndependentInstance) ApplyInterceptor(interceptor *flowsupport.Inter } } +func (inst *IndependentInstance) HasInterceptor() bool { + return inst.interceptor != nil +} + func (inst *IndependentInstance) GetInterceptor() *flowsupport.Interceptor { return inst.interceptor } @@ -415,6 +419,7 @@ func (inst *IndependentInstance) handleTaskDone(taskBehavior model.TaskBehavior, if taskInst.traceContext != nil { _ = trace.GetTracer().FinishTrace(taskInst.traceContext, nil) } + inst.addActivityToCoverage(taskInst, nil) } if err != nil { @@ -530,6 +535,7 @@ func (inst *IndependentInstance) handleTaskError(taskBehavior model.TaskBehavior // Set task status to failed for subflow activity taskInst.SetStatus(model.TaskStatusFailed) + inst.addActivityToCoverage(taskInst, err) handled, taskEntries := taskBehavior.Error(taskInst, err) containerInst := taskInst.flowInst @@ -673,6 +679,53 @@ func (inst *IndependentInstance) enterTasks(activeInst *Instance, taskEntries [] return nil } +func (inst *IndependentInstance) addActivityToCoverage(taskInst *TaskInst, err error) { + + if !inst.HasInterceptor() { + return + } + var errorObj map[string]interface{} + if err != nil { + errorObj = taskInst.getErrorObject(err) + } + + var coverage flowsupport.ActivityCoverage + if inst.GetInterceptor().CollectIO { + outputs := taskInst.outputs + if outputs == nil { + if inst.returnData != nil { + outputs = inst.returnData + } + } + coverage = flowsupport.ActivityCoverage{ + ActivityName: taskInst.taskID, + LinkFrom: inst.getLinks(taskInst.GetFromLinkInstances()), + LinkTo: inst.getLinks(taskInst.GetToLinkInstances()), + Inputs: taskInst.inputs, + Outputs: outputs, + Error: errorObj, + FlowName: taskInst.flowInst.Name(), + IsMainFlow: !inst.isHandlingError, + } + } else { + coverage = flowsupport.ActivityCoverage{ + ActivityName: taskInst.taskID, + FlowName: taskInst.flowInst.Name(), + IsMainFlow: !inst.isHandlingError, + } + } + + inst.interceptor.AddToActivityCoverage(coverage) +} + +func (inst *IndependentInstance) getLinks(instances []model.LinkInstance) []string { + var names []string + for _, linkInst := range instances { + names = append(names, linkInst.Link().Label()) + } + return names +} + ////////////////////////////////////////////////////////////////// // WorkItem describes an item of work (event for a Task) that should be executed on Step diff --git a/instance/linkinst.go b/instance/linkinst.go index ee9afb0..f71a057 100644 --- a/instance/linkinst.go +++ b/instance/linkinst.go @@ -3,6 +3,7 @@ package instance import ( "github.com/project-flogo/flow/definition" "github.com/project-flogo/flow/model" + "github.com/project-flogo/flow/support" ) // LinkInst represents data associated with an instance of a Link @@ -34,10 +35,44 @@ func (li *LinkInst) Status() model.LinkStatus { // SetStatus sets the current state indicator for the LinkInst func (li *LinkInst) SetStatus(status model.LinkStatus) { li.status = status + if status == model.LinkStatusTrue { + li.addLinkToCoverage() + } li.flowInst.master.changeTracker.LinkUpdated(li) //ld.flowInst.master.ChangeTracker.trackLinkData(ld.flowInst.subFlowId, &LinkInstChange{ChgType: CtUpd, ID: ld.link.ID(), LinkInst: ld}) } +func (li *LinkInst) addLinkToCoverage() { + + if !li.flowInst.master.HasInterceptor() { + return + } + + t := li.Link().Type() + linkType := "" + switch t { + case definition.LtExpression: + linkType = "Expression" + case definition.LtLabel: + linkType = "Label" + case definition.LtError: + linkType = "Error" + case definition.LtExprOtherwise: + linkType = "Otherwise" + } + coverage := support.TransitionCoverage{ + + TransitionName: li.Link().Label(), + TransitionType: linkType, + TransitionExpression: li.Link().Value(), + TransitionFrom: li.Link().FromTask().ID(), + TransitionTo: li.Link().ToTask().ID(), + FlowName: li.flowInst.Name(), + IsMainFlow: !li.flowInst.isHandlingError, + } + li.flowInst.master.interceptor.AddToLinkCoverage(coverage) +} + // Link returns the Link associated with ld context func (li *LinkInst) Link() *definition.Link { return li.link diff --git a/support/interceptor.go b/support/interceptor.go index 6ea7291..781d25e 100644 --- a/support/interceptor.go +++ b/support/interceptor.go @@ -31,6 +31,8 @@ type Interceptor struct { TaskInterceptors []*TaskInterceptor `json:"tasks"` taskInterceptorMap map[string]*TaskInterceptor + Coverage *Coverage `json:"coverage"` + CollectIO bool } // Init initializes the FlowInterceptor, usually called after deserialization @@ -52,6 +54,14 @@ func (pi *Interceptor) GetTaskInterceptor(taskID string) *TaskInterceptor { return pi.taskInterceptorMap[taskID] } +func (pi *Interceptor) AddToActivityCoverage(coverage ActivityCoverage) { + pi.Coverage.ActivityCoverage = append(pi.Coverage.ActivityCoverage, &coverage) +} + +func (pi *Interceptor) AddToLinkCoverage(coverage TransitionCoverage) { + pi.Coverage.TransitionCoverage = append(pi.Coverage.TransitionCoverage, &coverage) +} + // TaskInterceptor contains instance override information for a Task, such has attributes. // Also, a 'Skip' flag can be enabled to inform the runtime that the task should not // execute. @@ -76,3 +86,29 @@ type Assertion struct { Message string EvalResult ast.ExprEvalData } + +type Coverage struct { + ActivityCoverage []*ActivityCoverage `json:"activityCoverage,omitempty"` + TransitionCoverage []*TransitionCoverage `json:"transitionCoverage,omitempty"` +} + +type ActivityCoverage struct { + ActivityName string + LinkFrom []string + LinkTo []string + Inputs map[string]interface{} `json:"inputs,omitempty"` + Outputs map[string]interface{} `json:"outputs,omitempty"` + Error map[string]interface{} `json:"errors,omitempty"` + FlowName string `json:"flowName"` + IsMainFlow bool `json:"scope"` +} + +type TransitionCoverage struct { + TransitionName string `json:"transitionName"` + TransitionType string `json:"transitionType"` + TransitionFrom string `json:"transitionFrom"` + TransitionTo string `json:"transitionTo"` + TransitionExpression string `json:"transitionExpression"` + FlowName string `json:"flowName"` + IsMainFlow bool `json:"scope"` +}