func.gno
6.03 Kb ยท 251 lines
1package expect
2
3import "gno.land/p/demo/ufmt"
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 // TODO: Can't use a switch because it triggers the following VM error:
90 // "panic: should not happen, should be heapItemType: fn<()~VPBlock(1,0)>"
91 //
92 // switch fn := c.fn.(type) {
93 // case Fn:
94 // msg, panicked = handlePanic(fn)
95 // case ErrorFn:
96 // msg, panicked = handlePanic(func() { _ = fn() })
97 // case AnyFn:
98 // msg, panicked = handlePanic(func() { _ = fn() })
99 // case AnyErrorFn:
100 // msg, panicked = handlePanic(func() { _, _ = fn() })
101 // default:
102 // c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
103 // return MessageChecker{}
104 // }
105
106 if fn, ok := c.fn.(Fn); ok {
107 msg, panicked = handlePanic(fn)
108 } else if fn, ok := c.fn.(ErrorFn); ok {
109 msg, panicked = handlePanic(func() { _ = fn() })
110 } else if fn, ok := c.fn.(AnyFn); ok {
111 msg, panicked = handlePanic(func() { _ = fn() })
112 } else if fn, ok := c.fn.(AnyErrorFn); ok {
113 msg, panicked = handlePanic(func() { _, _ = fn() })
114 } else {
115 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
116 return MessageChecker{}
117 }
118
119 c.ctx.CheckExpectation(panicked, func(ctx Context) string {
120 if !ctx.IsNegated() {
121 return "Expected function to panic"
122 }
123 return ufmt.Sprintf("Expected func not to panic\nGot: %s", msg)
124 })
125
126 return NewMessageChecker(c.ctx, msg, MessageTypePanic)
127}
128
129// ToCrossPanic return an message checker to assert if current function panicked when crossing.
130// This assertion is handled only when making a crossing call to another realm, when asserting
131// within the same realm use `ToPanic()`.
132func (c FuncChecker) ToCrossPanic() MessageChecker {
133 c.ctx.T().Helper()
134
135 var (
136 msg string
137 panicked bool
138 )
139
140 // TODO: Can't use a switch because it triggers the following VM error:
141 // "panic: should not happen, should be heapItemType: fn<()~VPBlock(1,0)>"
142 //
143 // switch fn := c.fn.(type) {
144 // case Fn:
145 // msg, panicked = handleCrossPanic(fn)
146 // case ErrorFn:
147 // msg, panicked = handleCrossPanic(func() { _ = fn() })
148 // case AnyFn:
149 // msg, panicked = handleCrossPanic(func() { _ = fn() })
150 // case AnyErrorFn:
151 // msg, panicked = handleCrossPanic(func() { _, _ = fn() })
152 // default:
153 // c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
154 // return MessageChecker{}
155 // }
156
157 if fn, ok := c.fn.(Fn); ok {
158 msg, panicked = handleCrossPanic(fn)
159 } else if fn, ok := c.fn.(ErrorFn); ok {
160 msg, panicked = handleCrossPanic(func() { _ = fn() })
161 } else if fn, ok := c.fn.(AnyFn); ok {
162 msg, panicked = handleCrossPanic(func() { _ = fn() })
163 } else if fn, ok := c.fn.(AnyErrorFn); ok {
164 msg, panicked = handleCrossPanic(func() { _, _ = fn() })
165 } else {
166 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
167 return MessageChecker{}
168 }
169
170 c.ctx.CheckExpectation(panicked, func(ctx Context) string {
171 if !ctx.IsNegated() {
172 return "Expected function to cross panic"
173 }
174 return ufmt.Sprintf("Expected func not to cross panic\nGot: %s", msg)
175 })
176
177 return NewMessageChecker(c.ctx, msg, MessageTypeCrossPanic)
178}
179
180// ToReturn asserts that current function returned a value equal to an expected value.
181func (c FuncChecker) ToReturn(value any) {
182 c.ctx.T().Helper()
183
184 var (
185 err error
186 v any
187 )
188
189 if fn, ok := c.fn.(AnyFn); ok {
190 v = fn()
191 } else if fn, ok := c.fn.(AnyErrorFn); ok {
192 v, err = fn()
193 } else {
194 c.ctx.Fail("Unsupported func type\nGot: %T", c.fn)
195 return
196 }
197
198 if err != nil {
199 c.ctx.Fail("Function returned unexpected error\nGot: %s", err.Error())
200 return
201 }
202
203 if c.ctx.negated {
204 Value(c.ctx.T(), v).Not().ToEqual(value)
205 } else {
206 Value(c.ctx.T(), v).ToEqual(value)
207 }
208}
209
210func handlePanic(fn func()) (msg string, panicked bool) {
211 defer func() {
212 r := recover()
213 if r == nil {
214 return
215 }
216
217 panicked = true
218
219 if err, ok := r.(error); ok {
220 msg = err.Error()
221 return
222 }
223
224 if s, ok := r.(string); ok {
225 msg = s
226 return
227 }
228
229 msg = "unsupported panic type"
230 }()
231
232 fn()
233 return
234}
235
236func handleCrossPanic(fn func()) (string, bool) {
237 r := revive(fn)
238 if r == nil {
239 return "", false
240 }
241
242 if err, ok := r.(error); ok {
243 return err.Error(), true
244 }
245
246 if s, ok := r.(string); ok {
247 return s, true
248 }
249
250 return "unsupported panic type", true
251}