Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}