Skip to content

Commit bc9a1ad

Browse files
committed
test(starlark): pin the FailError half of the Go-boundary taxonomy
TestFailError verifies, end to end, what doc/error_handling.md's 'The Go boundary' section promises: fail() surfaces to Go as an *EvalError wrapping a *FailError, with StarlarkError carrying the error value when fail was given one (and nil when not); module-level try produces the same shape; a Go builtin returning *FailError raises the equivalent failure; and an incidental builtin error does not match *FailError. Complements TestReturnedError, which covers the recoverable half.
1 parent 037d6c5 commit bc9a1ad

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

starlark/eval_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,110 @@ def fails()!:
10251025
})
10261026
}
10271027

1028+
// TestFailError verifies the fail-style half of the Go-boundary taxonomy
1029+
// (doc/error_handling.md, "The Go boundary"): a fail() call surfaces to Go as
1030+
// an *EvalError wrapping a *FailError — carrying the Starlark error value when
1031+
// fail was given one — and a Go builtin returning a *FailError raises the
1032+
// equivalent failure. Incidental builtin errors must not match *FailError.
1033+
func TestFailError(t *testing.T) {
1034+
t.Run("fail with an error value", func(t *testing.T) {
1035+
const src = `
1036+
errors = error_tags("Boom")
1037+
def explode()!:
1038+
return errors.Boom(message = "kaboom", extra = 7)
1039+
e = explode() catch err:
1040+
recover err
1041+
fail(e)
1042+
`
1043+
_, err := starlark.ExecFile(&starlark.Thread{}, "failerr.star", src, nil)
1044+
var evalErr *starlark.EvalError
1045+
if !errors.As(err, &evalErr) {
1046+
t.Fatalf("err = %v (%T), want *starlark.EvalError", err, err)
1047+
}
1048+
var failErr *starlark.FailError
1049+
if !errors.As(err, &failErr) {
1050+
t.Fatalf("err = %v, want it to wrap *starlark.FailError", err)
1051+
}
1052+
if failErr.StarlarkError == nil {
1053+
t.Fatal("StarlarkError = nil, want the error value passed to fail")
1054+
}
1055+
if got := failErr.StarlarkError.Tag().Name(); got != "Boom" {
1056+
t.Errorf("tag = %q, want Boom", got)
1057+
}
1058+
if got := failErr.StarlarkError.Message(); got != "kaboom" {
1059+
t.Errorf("message = %q, want kaboom", got)
1060+
}
1061+
})
1062+
1063+
t.Run("fail without an error value", func(t *testing.T) {
1064+
_, err := starlark.ExecFile(&starlark.Thread{}, "fail.star", `fail("plain")`, nil)
1065+
var failErr *starlark.FailError
1066+
if !errors.As(err, &failErr) {
1067+
t.Fatalf("err = %v, want it to wrap *starlark.FailError", err)
1068+
}
1069+
if failErr.StarlarkError != nil {
1070+
t.Errorf("StarlarkError = %v, want nil for fail with no error value", failErr.StarlarkError)
1071+
}
1072+
})
1073+
1074+
t.Run("module-level try surfaces a FailError", func(t *testing.T) {
1075+
const src = `
1076+
errors = error_tags("Cfg")
1077+
def load_config()!:
1078+
return errors.Cfg(message = "config missing")
1079+
config = try load_config()
1080+
`
1081+
_, err := starlark.ExecFile(&starlark.Thread{}, "topltry.star", src, nil)
1082+
var failErr *starlark.FailError
1083+
if !errors.As(err, &failErr) {
1084+
t.Fatalf("err = %v, want it to wrap *starlark.FailError", err)
1085+
}
1086+
if failErr.StarlarkError == nil || failErr.StarlarkError.Message() != "config missing" {
1087+
t.Errorf("StarlarkError = %v, want the error with message %q", failErr.StarlarkError, "config missing")
1088+
}
1089+
})
1090+
1091+
t.Run("builtin returning FailError mimics fail", func(t *testing.T) {
1092+
tag := starlark.NewErrorTag("Corrupt")
1093+
msg := "cache corrupted"
1094+
mustSync := starlark.NewBuiltin("must_sync", func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
1095+
return nil, &starlark.FailError{
1096+
Msg: "fail: cache corrupted",
1097+
StarlarkError: starlark.NewError(tag, &msg, nil, nil),
1098+
}
1099+
})
1100+
predeclared := starlark.StringDict{"must_sync": mustSync}
1101+
_, err := starlark.ExecFile(&starlark.Thread{}, "builtin.star", `must_sync()`, predeclared)
1102+
var evalErr *starlark.EvalError
1103+
if !errors.As(err, &evalErr) {
1104+
t.Fatalf("err = %v (%T), want *starlark.EvalError", err, err)
1105+
}
1106+
var failErr *starlark.FailError
1107+
if !errors.As(err, &failErr) {
1108+
t.Fatalf("err = %v, want it to wrap *starlark.FailError", err)
1109+
}
1110+
if failErr.StarlarkError == nil || failErr.StarlarkError.Tag() != tag {
1111+
t.Errorf("StarlarkError = %v, want the error with tag Corrupt", failErr.StarlarkError)
1112+
}
1113+
})
1114+
1115+
t.Run("incidental builtin error is not a FailError", func(t *testing.T) {
1116+
faulty := starlark.NewBuiltin("faulty", func(*starlark.Thread, *starlark.Builtin, starlark.Tuple, []starlark.Tuple) (starlark.Value, error) {
1117+
return nil, fmt.Errorf("disk on fire")
1118+
})
1119+
predeclared := starlark.StringDict{"faulty": faulty}
1120+
_, err := starlark.ExecFile(&starlark.Thread{}, "faulty.star", `faulty()`, predeclared)
1121+
var evalErr *starlark.EvalError
1122+
if !errors.As(err, &evalErr) {
1123+
t.Fatalf("err = %v (%T), want *starlark.EvalError", err, err)
1124+
}
1125+
var failErr *starlark.FailError
1126+
if errors.As(err, &failErr) {
1127+
t.Errorf("incidental builtin error incorrectly matched *FailError: %v", err)
1128+
}
1129+
})
1130+
}
1131+
10281132
// TestPlainCallFromBuiltinSurfacesReturnedError checks that when a Go builtin
10291133
// invokes a !-function via plain Call, an uncaught error comes back to the
10301134
// builtin on the error channel as a ReturnedError, rather than being deposited

0 commit comments

Comments
 (0)