Skip to content

Commit d6cb780

Browse files
committed
feat: use a custom error so the caller can retrieve the cause
1 parent caf9aad commit d6cb780

2 files changed

Lines changed: 53 additions & 34 deletions

File tree

starlark/eval.go

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,22 @@ import (
2525
"go.starlark.net/syntax"
2626
)
2727

28-
var (
29-
// When a Starlark function call is canceled by a call to [Thread.Cancel] or
30-
// [Thread.CancelWithError], the EvalError returned by [Call] will respond
31-
// to errors.Is(err, ErrCanceled).
32-
ErrCanceled = errors.New("Starlark computation canceled")
33-
)
28+
// When a Starlark function call is canceled by a call to [Thread.Cancel] or
29+
// [Thread.CancelWithError], the EvalError returned by [Call] will respond
30+
// to errors.As(err, &canceledErr).
31+
type CanceledError struct {
32+
err error
33+
}
34+
35+
func (ce *CanceledError) Unwrap() error {
36+
return ce.err
37+
}
38+
39+
func (ce *CanceledError) Error() string {
40+
return fmt.Sprintf("Starlark computation canceled: %s", ce.err.Error())
41+
}
42+
43+
var _ error = (*CanceledError)(nil)
3444

3545
// A Thread contains the state of a Starlark thread,
3646
// such as its call stack and thread-local storage.
@@ -55,8 +65,9 @@ type Thread struct {
5565
// See example_test.go for some example implementations of Load.
5666
Load func(thread *Thread, module string) (StringDict, error)
5767

58-
// OnMaxSteps is called when the thread reaches the limit set by SetMaxExecutionSteps.
59-
// The default behavior is to call thread.Cancel("too many steps").
68+
// OnMaxSteps is called when the thread reaches the limit set by
69+
// SetMaxExecutionSteps. The default behavior is to call
70+
// thread.CancelWithError(starlark.ErrTooManySteps).
6071
OnMaxSteps func(thread *Thread)
6172

6273
// Steps a count of abstract computation steps executed
@@ -87,7 +98,7 @@ func (thread *Thread) ExecutionSteps() uint64 {
8798
// computation steps that may be executed by this thread. If the
8899
// thread's step counter exceeds this limit, the interpreter calls
89100
// the optional OnMaxSteps function or the default behavior
90-
// of calling thread.Cancel("too many steps").
101+
// of calling thread.CancelWithError(starlark.ErrTooManySteps).
91102
func (thread *Thread) SetMaxExecutionSteps(max uint64) {
92103
thread.maxSteps = max
93104
}
@@ -114,10 +125,11 @@ func (thread *Thread) Cancel(reason string) {
114125
}
115126

116127
func (thread *Thread) CancelWithError(err error) {
117-
// Wrap the user's error so that errors.Is will find both the user's error
128+
// Wrap the user's error so that errors.As will find both the user's error
118129
// and ErrCanceled as causes.
119-
if !errors.Is(err, ErrCanceled) {
120-
err = fmt.Errorf("%w: %w", ErrCanceled, err)
130+
var canceledErr *CanceledError
131+
if !errors.As(err, &canceledErr) {
132+
err = &CanceledError{err: err}
121133
}
122134
// Atomically set cancelReason, preserving earlier reason if any.
123135
thread.cancelReason.CompareAndSwap(nil, &err)

starlark/eval_test.go

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,17 +1001,18 @@ func TestCancel(t *testing.T) {
10011001
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
10021002
t.Errorf("ExecFile returned error %q, want cancellation", err)
10031003
}
1004-
if !errors.Is(err, starlark.ErrCanceled) {
1005-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1004+
var canceledErr *starlark.CanceledError
1005+
if !errors.As(err, &canceledErr) {
1006+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
10061007
}
10071008

10081009
// cancellation is sticky
10091010
_, err = starlark.ExecFile(thread, "precancel.star", `x = 1//0`, nil)
10101011
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
10111012
t.Errorf("ExecFile returned error %q, want cancellation", err)
10121013
}
1013-
if !errors.Is(err, starlark.ErrCanceled) {
1014-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1014+
if !errors.As(err, &canceledErr) {
1015+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
10151016
}
10161017
}
10171018
// A thread canceled during a built-in executes no more code.
@@ -1027,8 +1028,9 @@ func TestCancel(t *testing.T) {
10271028
if fmt.Sprint(err) != `Starlark computation canceled: "nope"` {
10281029
t.Errorf("ExecFile returned error %q, want cancellation", err)
10291030
}
1030-
if !errors.Is(err, starlark.ErrCanceled) {
1031-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1031+
var cancelErr *starlark.CanceledError
1032+
if !errors.As(err, &cancelErr) {
1033+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
10321034
}
10331035
}
10341036
// An external cancelation returns a wrapped starlark.ErrCanceled and the
@@ -1057,8 +1059,9 @@ f()
10571059
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
10581060
t.Errorf("ExecFile returned error %q, want cancellation", err)
10591061
}
1060-
if !errors.Is(err, starlark.ErrCanceled) {
1061-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1062+
var canceledErr *starlark.CanceledError
1063+
if !errors.As(err, &canceledErr) {
1064+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
10621065
}
10631066
evalErr, ok := err.(*starlark.EvalError)
10641067
if !ok {
@@ -1086,10 +1089,11 @@ func TestCancelWithError(t *testing.T) {
10861089
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
10871090
t.Errorf("ExecFile returned error %q, want cancellation", err)
10881091
}
1089-
if !errors.Is(err, starlark.ErrCanceled) {
1090-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1092+
var canceledErr *starlark.CanceledError
1093+
if !errors.As(err, &canceledErr) {
1094+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
10911095
}
1092-
if !errors.Is(err, cancelErr) {
1096+
if !errors.Is(err, cancelErr) || errors.Unwrap(canceledErr) != cancelErr {
10931097
t.Errorf("ExecFile didn't return expected error")
10941098
}
10951099

@@ -1098,10 +1102,10 @@ func TestCancelWithError(t *testing.T) {
10981102
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
10991103
t.Errorf("ExecFile returned error %q, want cancellation", err)
11001104
}
1101-
if !errors.Is(err, starlark.ErrCanceled) {
1102-
t.Errorf("ExecFile: !errors.Is(err, starlark.ErrCanceled); got %T, %v", err, err)
1105+
if !errors.As(err, &canceledErr) {
1106+
t.Errorf("ExecFile: !errors.Is(err, starlark.CanceledError); got %T, %v", err, err)
11031107
}
1104-
if !errors.Is(err, cancelErr) {
1108+
if !errors.Is(err, cancelErr) || errors.Unwrap(canceledErr) != cancelErr {
11051109
t.Errorf("ExecFile didn't return expected error")
11061110
}
11071111
}
@@ -1120,10 +1124,11 @@ func TestCancelWithError(t *testing.T) {
11201124
if fmt.Sprint(err) != `Starlark computation canceled: "nope"` {
11211125
t.Errorf("ExecFile returned error %q, want cancellation", err)
11221126
}
1123-
if !errors.Is(err, starlark.ErrCanceled) {
1124-
t.Errorf("ExecFile: !errors.Is(err, starlark.ErrCanceled); got %T, %v", err, err)
1127+
var canceledErr *starlark.CanceledError
1128+
if !errors.As(err, &canceledErr) {
1129+
t.Errorf("ExecFile: !errors.Is(err, starlark.CanceledError); got %T, %v", err, err)
11251130
}
1126-
if !errors.Is(err, cancelErr) {
1131+
if !errors.Is(err, cancelErr) || errors.Unwrap(canceledErr) != cancelErr {
11271132
t.Errorf("ExecFile didn't return expected error")
11281133
}
11291134
}
@@ -1154,10 +1159,11 @@ f()
11541159
if fmt.Sprint(err) != "Starlark computation canceled: nope" {
11551160
t.Errorf("ExecFile returned error %q, want cancellation", err)
11561161
}
1157-
if !errors.Is(err, starlark.ErrCanceled) {
1158-
t.Errorf("ExecFile didn't return error starlark.ErrCanceled")
1162+
var canceledErr *starlark.CanceledError
1163+
if !errors.As(err, &canceledErr) {
1164+
t.Errorf("ExecFile didn't return error starlark.CanceledError")
11591165
}
1160-
if !errors.Is(err, cancelErr) {
1166+
if !errors.Is(err, cancelErr) || errors.Unwrap(canceledErr) != cancelErr {
11611167
t.Errorf("ExecFile didn't return expected error")
11621168
}
11631169
evalErr, ok := err.(*starlark.EvalError)
@@ -1203,8 +1209,9 @@ func TestExecutionSteps(t *testing.T) {
12031209
if fmt.Sprint(err) != "Starlark computation canceled: too many steps" {
12041210
t.Errorf("execution returned error %q, want cancellation", err)
12051211
}
1206-
if !errors.Is(err, starlark.ErrCanceled) {
1207-
t.Errorf("exectution didn't return error starlark.ErrCanceled")
1212+
var canceledErr *starlark.CanceledError
1213+
if !errors.As(err, &canceledErr) {
1214+
t.Errorf("execution didn't return error starlark.CanceledError")
12081215
}
12091216

12101217
thread.Steps = 0

0 commit comments

Comments
 (0)