mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
tester: make Result JSON round-trippable (#8946)
## Summary - add a concrete JSON unmarshal path for tester.Result errors - preserve the existing marshaled output byte-for-byte - reconstruct structured topdown errors from opa test --format json output Fixes #8014 ## Testing - go test -count=1 ./v1/tester/... ./tester/... - go test -count=1 ./cmd/... - go build ./... - go vet ./v1/tester/... ./tester/... ./cmd/... - gofmt check on the changed Go files Signed-off-by: Victor Solano <victor.solanonunez@gmail.com>
This commit is contained in:
@@ -175,6 +175,50 @@ func newResult(loc *ast.Location, pkg, name string, duration time.Duration, trac
|
||||
}
|
||||
}
|
||||
|
||||
// resultError mirrors the JSON shape *topdown.Error marshals to, which is the
|
||||
// form errors take in the output of `opa test --format json`.
|
||||
type resultError struct {
|
||||
Code string `json:"code,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Location *ast.Location `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
func (e *resultError) error() error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if e.Code != "" {
|
||||
return &topdown.Error{Code: e.Code, Message: e.Message, Location: e.Location}
|
||||
}
|
||||
|
||||
return errors.New(e.Message)
|
||||
}
|
||||
|
||||
// resultAlias drops the methods of Result so that unmarshalling into it below
|
||||
// doesn't recurse. The anonymous struct shadows the embedded error interface
|
||||
// field, which is marshallable but not unmarshallable, with a concrete type.
|
||||
type resultAlias Result
|
||||
|
||||
// UnmarshalJSON reads back the JSON produced for a Result. Marshalling is left
|
||||
// to the default encoder so that existing output is unchanged.
|
||||
func (r *Result) UnmarshalJSON(bs []byte) error {
|
||||
aux := struct {
|
||||
*resultAlias
|
||||
Error *resultError `json:"error,omitempty"`
|
||||
}{
|
||||
resultAlias: (*resultAlias)(r),
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(bs, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Error = aux.Error.error()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Pass returns true if the test case passed.
|
||||
func (r *Result) Pass() bool {
|
||||
return !r.Fail && !r.Skip && r.Error == nil
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
@@ -1198,3 +1199,88 @@ func TestReporterFormatsWithExplicitParallel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultJSONRoundTrip(t *testing.T) {
|
||||
loc := ast.NewLocation([]byte("data.foo"), "test.rego", 3, 5)
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
err error
|
||||
// exp is the error the round-tripped Result is expected to carry. It
|
||||
// is nil for errors that have no JSON representation of their own:
|
||||
// those marshal to an empty object, so only the presence of an error
|
||||
// survives the round trip.
|
||||
exp error
|
||||
}{
|
||||
{
|
||||
note: "plain error",
|
||||
err: errors.New("test error"),
|
||||
},
|
||||
{
|
||||
note: "topdown error",
|
||||
err: &topdown.Error{Code: topdown.CancelErr, Message: "context deadline exceeded"},
|
||||
exp: &topdown.Error{Code: topdown.CancelErr, Message: "context deadline exceeded"},
|
||||
},
|
||||
{
|
||||
note: "topdown error with location",
|
||||
err: &topdown.Error{Code: topdown.InternalErr, Message: "boom", Location: loc},
|
||||
exp: &topdown.Error{Code: topdown.InternalErr, Message: "boom", Location: loc},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
result := &tester.Result{Name: "test_a", Error: tc.err}
|
||||
|
||||
var act tester.Result
|
||||
if err := util.UnmarshalJSON(util.MustMarshalJSON(result), &act); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if act.Error == nil {
|
||||
t.Fatal("Expected error to survive the round trip, got nil")
|
||||
}
|
||||
|
||||
if tc.exp == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if act.Error.Error() != tc.exp.Error() {
|
||||
t.Fatalf("Expected error %q, got %q", tc.exp.Error(), act.Error.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResultUnmarshalJSONEvalError(t *testing.T) {
|
||||
// Payload as emitted by `opa test --format json` when a test errors out.
|
||||
bs := []byte(`{
|
||||
"location": {"file": "main_test.rego", "row": 285, "col": 1},
|
||||
"package": "data.regal.main_test",
|
||||
"name": "test_lint_from_stdin",
|
||||
"error": {"code": "eval_cancel_error", "message": "context deadline exceeded"},
|
||||
"duration": 5009256675
|
||||
}`)
|
||||
|
||||
var result tester.Result
|
||||
if err := util.UnmarshalJSON(bs, &result); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.Error == nil {
|
||||
t.Fatal("Expected error, got nil")
|
||||
}
|
||||
|
||||
var tdErr *topdown.Error
|
||||
if !errors.As(result.Error, &tdErr) {
|
||||
t.Fatalf("Expected *topdown.Error, got %T", result.Error)
|
||||
}
|
||||
|
||||
if tdErr.Code != topdown.CancelErr {
|
||||
t.Errorf("Expected code %q, got %q", topdown.CancelErr, tdErr.Code)
|
||||
}
|
||||
|
||||
if exp := "context deadline exceeded"; tdErr.Message != exp {
|
||||
t.Errorf("Expected message %q, got %q", exp, tdErr.Message)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user