Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve test outputs error handling #2404

Merged
merged 2 commits into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ components:
type: string
value:
type: string
error:
type: string

metadata:
type: object
Expand Down
36 changes: 36 additions & 0 deletions cli/openapi/model_test_run_outputs_inner.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/executor/assertion_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func (e *defaultAssertionRunner) validateOutputResolution(ctx context.Context, r
return nil
}

anotherErr := e.eventEmitter.Emit(ctx, events.TestOutputGenerationWarning(request.Test.ID, request.Run.ID, outputName))
anotherErr := e.eventEmitter.Emit(ctx, events.TestOutputGenerationWarning(request.Test.ID, request.Run.ID, outputModel.Error, outputName))
if anotherErr != nil {
log.Printf("[AssertionRunner] Test %s Run %d: fail to emit TestOutputGenerationWarning event: %s\n", request.Test.ID, request.Run.ID, anotherErr.Error())
}
Expand Down
30 changes: 27 additions & 3 deletions server/executor/outputs_processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,29 @@ func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Out
}

err = parsed.ForEach(func(key string, out parsedOutput) error {
if out.err != nil {
res, err = res.Add(key, model.RunOutput{
Value: "",
SpanID: "",
Name: key,
Resolved: false,
Error: out.err,
})
if err != nil {
return fmt.Errorf(`cannot process output "%s": %w`, key, err)
}

return nil
}

spans := out.selector.Filter(tr)

stores := append([]expression.DataStore{expression.MetaAttributesDataStore{SelectedSpans: spans}}, ds...)

value := ""
spanId := ""
resolved := false
var outputError error = nil
spans.
ForEach(func(_ int, span model.Span) bool {
value = extractAttr(span, stores, out.expr)
Expand All @@ -77,13 +93,15 @@ func outputProcessor(ctx context.Context, outputs maps.Ordered[string, model.Out
OrEmpty(func() {
value = extractAttr(model.Span{}, stores, out.expr)
resolved = false
outputError = fmt.Errorf(`cannot find matching spans for output "%s"`, key)
})

res, err = res.Add(key, model.RunOutput{
Value: value,
SpanID: spanId,
Name: key,
Resolved: resolved,
Error: outputError,
})
if err != nil {
return fmt.Errorf(`cannot process output "%s": %w`, key, err)
Expand Down Expand Up @@ -112,25 +130,31 @@ func extractAttr(span model.Span, ds []expression.DataStore, expr expression.Exp
type parsedOutput struct {
selector selectors.Selector
expr expression.Expr
err error
}

func parseOutputs(outputs maps.Ordered[string, model.Output]) (maps.Ordered[string, parsedOutput], error) {
var parsed maps.Ordered[string, parsedOutput]

parseErr := outputs.ForEach(func(key string, out model.Output) error {
var selector selectors.Selector
var expr expression.Expr
var outputErr error = nil

expr, err := expression.Parse(out.Value)
if err != nil {
return fmt.Errorf(`cannot parse output "%s" value "%s": %w`, key, out.Value, err)
outputErr = fmt.Errorf(`cannot parse output "%s" value "%s": %w`, key, out.Value, err)
}

selector, err := selectors.New(string(out.Selector))
selector, err = selectors.New(string(out.Selector))
if err != nil {
return fmt.Errorf(`cannot parse output "%s" selector "%s": %w`, key, string(out.Selector), err)
outputErr = fmt.Errorf(`cannot parse output "%s" selector "%s": %w`, key, string(out.Selector), err)
}

parsed, _ = parsed.Add(key, parsedOutput{
selector: selector,
expr: expr,
err: outputErr,
})
return nil
})
Expand Down
2 changes: 2 additions & 0 deletions server/http/mappings/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ func (m OpenAPI) RunOutputs(in maps.Ordered[string, model.RunOutput]) []openapi.
Name: key,
Value: val.Value,
SpanId: val.SpanID,
Error: errToString(val.Error),
})
return nil
})
Expand Down Expand Up @@ -496,6 +497,7 @@ func (m Model) RunOutputs(in []openapi.TestRunOutputsInner) maps.Ordered[string,
Value: output.Value,
Name: output.Name,
SpanID: output.SpanId,
Error: fmt.Errorf(output.Error),
})
}

Expand Down
4 changes: 2 additions & 2 deletions server/model/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,14 +307,14 @@ func TraceStoppedInfo(testID id.ID, runID int) model.TestRunEvent {
}
}

func TestOutputGenerationWarning(testID id.ID, runID int, output string) model.TestRunEvent {
func TestOutputGenerationWarning(testID id.ID, runID int, err error, output string) model.TestRunEvent {
return model.TestRunEvent{
TestID: testID,
RunID: runID,
Stage: model.StageTest,
Type: "OUTPUT_GENERATION_WARNING",
Title: fmt.Sprintf(`Output '%s' not generated`, output),
Description: fmt.Sprintf(`The value for the output '%s' could not be generated`, output),
Description: fmt.Sprintf(`The output '%s' returned an error. Error: %s`, output, err.Error()),
CreatedAt: time.Now(),
DataStoreConnection: model.ConnectionResult{},
Polling: model.PollingInfo{},
Expand Down
39 changes: 39 additions & 0 deletions server/model/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,45 @@ import (
"go.opentelemetry.io/otel/trace"
)

func (ro RunOutput) MarshalJSON() ([]byte, error) {
return json.Marshal(&struct {
Name string
Value string
SpanID string
Resolved bool
Error string
}{
Name: ro.Name,
Value: ro.Value,
SpanID: ro.SpanID,
Resolved: ro.Resolved,
Error: errToString(ro.Error),
})
}

func (ro *RunOutput) UnmarshalJSON(data []byte) error {
aux := struct {
Name string
Value string
SpanID string
Resolved bool
Error string
}{}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}

ro.Name = aux.Name
ro.Value = aux.Value
ro.SpanID = aux.SpanID
ro.Resolved = aux.Resolved
if err := stringToErr(aux.Error); err != nil {
ro.Error = err
}

return nil
}

func (sar SpanAssertionResult) MarshalJSON() ([]byte, error) {
sid := ""
if sar.SpanID != nil {
Expand Down
1 change: 1 addition & 0 deletions server/model/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type (
Value string
SpanID string
Resolved bool
Error error
}

AssertionResult struct {
Expand Down
2 changes: 2 additions & 0 deletions server/openapi/model_test_run_outputs_inner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ type TestRunOutputsInner struct {
SpanId string `json:"spanId,omitempty"`

Value string `json:"value,omitempty"`

Error string `json:"error,omitempty"`
}

// AssertTestRunOutputsInnerRequired checks if the required fields are not zero-ed
Expand Down
6 changes: 5 additions & 1 deletion web/src/components/TestOutput/TestOutput.styled.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {MoreOutlined} from '@ant-design/icons';
import {InfoCircleFilled, MoreOutlined} from '@ant-design/icons';
import {Typography} from 'antd';
import styled from 'styled-components';

Expand Down Expand Up @@ -60,3 +60,7 @@ export const ActionsContainer = styled.div`
display: flex;
justify-content: flex-end;
`;

export const IconWarning = styled(InfoCircleFilled)`
color: ${({theme}) => theme.color.error};
`;
9 changes: 7 additions & 2 deletions web/src/components/TestOutput/TestOutput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Tag} from 'antd';
import {Tag, Tooltip} from 'antd';
import {useCallback} from 'react';
import AttributeValue from 'components/AttributeValue';
import {useTestOutput} from 'providers/TestOutput/TestOutput.provider';
Expand All @@ -18,7 +18,7 @@ interface IProps {

const TestOutput = ({
index,
output: {id, name, isDeleted, isDraft, spanId, selector, value, valueRun, valueRunDraft},
output: {id, name, isDeleted, isDraft, spanId, selector, value, valueRun, valueRunDraft, error},
output,
onEdit,
onDelete,
Expand Down Expand Up @@ -75,6 +75,11 @@ const TestOutput = ({
<AttributeValue value={valueRunDraft} />
</>
)}
{error && (
<Tooltip title={error}>
<S.IconWarning />
</Tooltip>
)}
</S.Entry>
</S.Row>
</S.Container>
Expand Down
2 changes: 2 additions & 0 deletions web/src/models/TestOutput.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type TestOutput = {
valueRunDraft: string;
id: number;
spanId: string;
error: string;
};

function TestOutput({name = '', selector = {}, value = ''}: TRawTestOutput, id = -1): TestOutput {
Expand All @@ -24,6 +25,7 @@ function TestOutput({name = '', selector = {}, value = ''}: TRawTestOutput, id =
valueRun: '',
valueRunDraft: '',
spanId: '',
error: '',
};
}

Expand Down
4 changes: 3 additions & 1 deletion web/src/models/TestRunOutput.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@ export type TRawTestRunOutput = {
name?: string;
value?: string;
spanId?: string;
error?: string;
};
type TestRunOutput = Model<TRawTestRunOutput, {}>;

const TestRunOutput = ({name = '', value = '', spanId = ''}: TRawTestRunOutput): TestRunOutput => {
const TestRunOutput = ({name = '', value = '', spanId = '', error = ''}: TRawTestRunOutput): TestRunOutput => {
return {
name,
value,
spanId,
error,
};
};

Expand Down
1 change: 1 addition & 0 deletions web/src/redux/testOutputs/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const testOutputsSlice = createSlice({
...output,
valueRun: runOutputs[index]?.value ?? '',
spanId: runOutputs[index]?.spanId ?? '',
error: runOutputs[index]?.error ?? '',
}));
},
},
Expand Down