feat(ErrorIs): becomes a smuggler operator with errors.As feature · maxatome/go-testdeep@3638797
@@ -8,68 +8,153 @@ package td_test
8899import (
1010"fmt"
11+"io"
1112"testing"
12131314"github.com/maxatome/go-testdeep/internal/dark"
1415"github.com/maxatome/go-testdeep/internal/test"
1516"github.com/maxatome/go-testdeep/td"
1617)
171819+type errorIsSimpleErr string
20+21+func (e errorIsSimpleErr) Error() string {
22+return string(e)
23+}
24+25+type errorIsWrappedErr struct {
26+s string
27+err error
28+}
29+30+func (e errorIsWrappedErr) Error() string {
31+if e.err != nil {
32+return e.s + ": " + e.err.Error()
33+ }
34+return e.s + ": nil"
35+}
36+37+func (e errorIsWrappedErr) Unwrap() error {
38+return e.err
39+}
40+41+var _ = []error{errorIsSimpleErr(""), errorIsWrappedErr{}}
42+1843func TestErrorIs(t *testing.T) {
19-insideErr1 := fmt.Errorf("failure1")
20-insideErr2 := fmt.Errorf("failure2: %w", insideErr1)
21-insideErr3 := fmt.Errorf("failure3: %w", insideErr2)
22-err := fmt.Errorf("failure4: %w", insideErr3)
44+insideErr1 := errorIsSimpleErr("failure1")
45+insideErr2 := errorIsWrappedErr{"failure2", insideErr1}
46+insideErr3 := errorIsWrappedErr{"failure3", insideErr2}
47+err := errorIsWrappedErr{"failure4", insideErr3}
23482449checkOK(t, err, td.ErrorIs(err))
2550checkOK(t, err, td.ErrorIs(insideErr3))
2651checkOK(t, err, td.ErrorIs(insideErr2))
2752checkOK(t, err, td.ErrorIs(insideErr1))
2853checkOK(t, nil, td.ErrorIs(nil))
295455+checkOK(t, err, td.ErrorIs(td.All(
56+td.Isa(errorIsSimpleErr("")),
57+td.String("failure1"),
58+ )))
59+60+// many errorIsWrappedErr in the err's tree, so only the first
61+// encountered matches
62+checkOK(t, err, td.ErrorIs(td.All(
63+td.Isa(errorIsWrappedErr{}),
64+td.HasPrefix("failure4"),
65+ )))
66+67+// HasPrefix().TypeBehind() always returns nil
68+// so errors.As() is called with &any, so the toplevel error matches
69+checkOK(t, err, td.ErrorIs(td.HasPrefix("failure4")))
70+3071var errNil error
3172checkOK(t, &errNil, td.Ptr(td.ErrorIs(nil)))
327374+var inside errorIsSimpleErr
75+checkOK(t, err, td.ErrorIs(td.Catch(&inside, td.String("failure1"))))
76+test.EqualStr(t, string(inside), "failure1")
77+3378checkError(t, nil, td.ErrorIs(insideErr1),
3479expectedError{
35-Message: mustBe("nil value"),
3680Path: mustBe("DATA"),
81+Message: mustBe("nil value"),
3782Got: mustBe("nil"),
3883Expected: mustBe("anything implementing error interface"),
3984 })
40854186checkError(t, 45, td.ErrorIs(insideErr1),
4287expectedError{
43-Message: mustBe("int does not implement error interface"),
4488Path: mustBe("DATA"),
89+Message: mustBe("int does not implement error interface"),
4590Got: mustBe("45"),
4691Expected: mustBe("anything implementing error interface"),
4792 })
48934994checkError(t, 45, td.ErrorIs(fmt.Errorf("another")),
5095expectedError{
51-Message: mustBe("int does not implement error interface"),
5296Path: mustBe("DATA"),
97+Message: mustBe("int does not implement error interface"),
5398Got: mustBe("45"),
5499Expected: mustBe("anything implementing error interface"),
55100 })
5610157102checkError(t, err, td.ErrorIs(fmt.Errorf("another")),
58103expectedError{
59-Message: mustBe("is not the error"),
60104Path: mustBe("DATA"),
61-Got: mustBe(`(*fmt.wrapError) "failure4: failure3: failure2: failure1"`),
105+Message: mustBe("is not found in err's tree"),
106+Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`),
62107Expected: mustBe(`(*errors.errorString) "another"`),
63108 })
64109110+checkError(t, err, td.ErrorIs(td.String("nonono")),
111+expectedError{
112+Path: mustBe("DATA.ErrorIs(interface {})"),
113+Message: mustBe("does not match"),
114+Got: mustBe(`"failure4: failure3: failure2: failure1"`),
115+Expected: mustBe(`"nonono"`),
116+ })
117+118+checkError(t, err, td.ErrorIs(td.Isa(fmt.Errorf("another"))),
119+expectedError{
120+Path: mustBe("DATA"),
121+Message: mustBe("type is not found in err's tree"),
122+Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`),
123+Expected: mustBe(`*errors.errorString`),
124+ })
125+126+checkError(t, err, td.ErrorIs(td.Smuggle(io.ReadAll, td.String("xx"))),
127+expectedError{
128+Path: mustBe("DATA"),
129+Message: mustBe("type is not found in err's tree"),
130+Got: mustBe(`(td_test.errorIsWrappedErr) failure4: failure3: failure2: failure1`),
131+Expected: mustBe(`io.Reader`),
132+ })
133+65134checkError(t, err, td.ErrorIs(nil),
66135expectedError{
67-Message: mustBe("is not the error"),
68136Path: mustBe("DATA"),
69-Got: mustBe(`(*fmt.wrapError) "failure4: failure3: failure2: failure1"`),
137+Message: mustBe("is not nil"),
138+Got: mustBe(`(td_test.errorIsWrappedErr) "failure4: failure3: failure2: failure1"`),
139+Expected: mustBe(`nil`),
140+ })
141+142+// As errors.Is, it does not match
143+checkError(t, errorIsWrappedErr{"failure", nil}, td.ErrorIs(nil),
144+expectedError{
145+Path: mustBe("DATA"),
146+Message: mustBe("is not nil"),
147+Got: mustBe(`(td_test.errorIsWrappedErr) "failure: nil"`),
70148Expected: mustBe(`nil`),
71149 })
72150151+checkError(t, err, td.ErrorIs(td.Gt(0)),
152+expectedError{
153+Path: mustBe("DATA"),
154+Message: mustBe("bad usage of ErrorIs operator"),
155+Summary: mustBe(`ErrorIs(Gt): type int behind Gt operator is not an interface or does not implement error`),
156+ })
157+73158type private struct{ err error }
74159got := private{err: err}
75160for _, expErr := range []error{err, insideErr3} {
@@ -95,6 +180,9 @@ func TestErrorIs(t *testing.T) {
95180// String
96181test.EqualStr(t, td.ErrorIs(insideErr1).String(), "ErrorIs(failure1)")
97182test.EqualStr(t, td.ErrorIs(nil).String(), "ErrorIs(nil)")
183+test.EqualStr(t, td.ErrorIs(td.HasPrefix("pipo")).String(),
184+`ErrorIs(HasPrefix("pipo"))`)
185+test.EqualStr(t, td.ErrorIs(12).String(), "ErrorIs(<ERROR>)")
98186}
99187100188func TestErrorIsTypeBehind(t *testing.T) {