func.gno
4.77 Kb · 219 lines
1package expect
2
3import "gno.land/p/nt/ufmt/v0"
4
5type (
6 // Fn defines a type for generic functions.
7 Fn = func()
8
9 // ErrorFn defines a type for generic functions that return an error.
10 ErrorFn = func() error
11
12 // AnyFn defines a type for generic functions that returns a value.
13 AnyFn = func() any
14
15 // AnyErrorFn defines a type for generic functions that return a value and an error.
16 AnyErrorFn = func() (any, error)
17)
18
19// Func creates a new checker for functions.
20func Func(t TestingT, fn any) FuncChecker {
21 return FuncChecker{
22 ctx: NewContext(t),
23 fn: fn,
24 }
25}
26
27// FuncChecker asserts function panics, errors and returned value.
28type FuncChecker struct {
29 ctx Context
30 fn any
31}
32
33// WithFailPrefix assigns a prefix that will be prefixed to testing errors when an assertion fails.
34func (c FuncChecker) WithFailPrefix(prefix string) FuncChecker {
35 c.ctx.prefix = prefix
36 return c
37}
38
39// Not negates the next called expectation.
40func (c FuncChecker) Not() FuncChecker {
41 c.ctx.negated = !c.ctx.negated
42 return c
43}
44
45// ToFail return an error checker to assert if current function returns an error.
46func (c FuncChecker) ToFail() ErrorChecker {
47 c.ctx.T().Helper()
48
49 var err error
50 switch fn := c.fn.(type) {
51 case ErrorFn:
52 err = fn()
53 case AnyErrorFn:
54 _, err = fn()
55 default:
56 c.ctx.Fail("Unsupported error func type\nGot: %T", c.fn)
57 return ErrorChecker{}
58 }
59
60 c.ctx.CheckExpectation(err != nil, func(ctx Context) string {
61 if !ctx.IsNegated() {
62 return "Expected func to return an error"
63 }
64 return ufmt.Sprintf("Func failed with error\nGot: %s", err.Error())
65 })
66
67 return NewErrorChecker(c.ctx, err)
68}
69
70// ToPanic return an message checker to assert if current function panicked.
71// This assertion is handled within the same realm, to assert panics when crossing
72// to another realm use the `ToAbort()` assertion.
73//
74// Example usage:
75//
76// func TestFoo(t *testing.T) {
77// expect.Func(t, func() {
78// Foo(cross)
79// }).Not().ToCrossPanic()
80// }
81func (c FuncChecker) ToPanic() MessageChecker {
82 c.ctx.T().Helper()
83
84 var (
85 msg string
86 panicked bool
87 )
88
89 switch fn := c.fn.(type) {
90 case Fn:
91 msg, panicked = handlePanic(fn)
92 case ErrorFn:
93 msg, panicked = handlePanic(func() { _ = fn() })
94 case AnyFn:
95 msg, panicked = handlePanic(func() { _ = fn() })
96 case AnyErrorFn:
97 msg, panicked = handlePanic(func() { _, _ = fn() })
98 default:
99 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
100 return MessageChecker{}
101 }
102
103 c.ctx.CheckExpectation(panicked, func(ctx Context) string {
104 if !ctx.IsNegated() {
105 return "Expected function to panic"
106 }
107 return ufmt.Sprintf("Expected func not to panic\nGot: %s", msg)
108 })
109
110 return NewMessageChecker(c.ctx, msg, MessageTypePanic)
111}
112
113// ToCrossPanic return an message checker to assert if current function panicked when crossing.
114// This assertion is handled only when making a crossing call to another realm, when asserting
115// within the same realm use `ToPanic()`.
116func (c FuncChecker) ToCrossPanic() MessageChecker {
117 c.ctx.T().Helper()
118
119 var (
120 msg string
121 panicked bool
122 )
123
124 switch fn := c.fn.(type) {
125 case Fn:
126 msg, panicked = handleCrossPanic(fn)
127 case ErrorFn:
128 msg, panicked = handleCrossPanic(func() { _ = fn() })
129 case AnyFn:
130 msg, panicked = handleCrossPanic(func() { _ = fn() })
131 case AnyErrorFn:
132 msg, panicked = handleCrossPanic(func() { _, _ = fn() })
133 default:
134 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
135 return MessageChecker{}
136 }
137
138 c.ctx.CheckExpectation(panicked, func(ctx Context) string {
139 if !ctx.IsNegated() {
140 return "Expected function to cross panic"
141 }
142 return ufmt.Sprintf("Expected func not to cross panic\nGot: %s", msg)
143 })
144
145 return NewMessageChecker(c.ctx, msg, MessageTypeCrossPanic)
146}
147
148// ToReturn asserts that current function returned a value equal to an expected value.
149func (c FuncChecker) ToReturn(value any) {
150 c.ctx.T().Helper()
151
152 var (
153 err error
154 v any
155 )
156
157 if fn, ok := c.fn.(AnyFn); ok {
158 v = fn()
159 } else if fn, ok := c.fn.(AnyErrorFn); ok {
160 v, err = fn()
161 } else {
162 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
163 return
164 }
165
166 if err != nil {
167 c.ctx.Fail("Function returned unexpected error\nGot: %s", err.Error())
168 return
169 }
170
171 if c.ctx.negated {
172 Value(c.ctx.T(), v).Not().ToEqual(value)
173 } else {
174 Value(c.ctx.T(), v).ToEqual(value)
175 }
176}
177
178func handlePanic(fn func()) (msg string, panicked bool) {
179 defer func() {
180 r := recover()
181 if r == nil {
182 return
183 }
184
185 panicked = true
186
187 if err, ok := r.(error); ok {
188 msg = err.Error()
189 return
190 }
191
192 if s, ok := r.(string); ok {
193 msg = s
194 return
195 }
196
197 msg = "unsupported panic type"
198 }()
199
200 fn()
201 return
202}
203
204func handleCrossPanic(fn func()) (string, bool) {
205 r := revive(fn)
206 if r == nil {
207 return "", false
208 }
209
210 if err, ok := r.(error); ok {
211 return err.Error(), true
212 }
213
214 if s, ok := r.(string); ok {
215 return s, true
216 }
217
218 return "unsupported panic type", true
219}