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}