uassert.gno

14.73 Kb ยท 619 lines
  1// uassert is an adapted lighter version of https://github.com/stretchr/testify/assert.
  2package uassert
  3
  4import (
  5	"std"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/demo/diff"
 10	"gno.land/p/demo/ufmt"
 11)
 12
 13// NoError asserts that a function returned no error (i.e. `nil`).
 14func NoError(t TestingT, err error, msgs ...string) bool {
 15	t.Helper()
 16	if err != nil {
 17		return fail(t, msgs, "unexpected error: %s", err.Error())
 18	}
 19	return true
 20}
 21
 22// Error asserts that a function returned an error (i.e. not `nil`).
 23func Error(t TestingT, err error, msgs ...string) bool {
 24	t.Helper()
 25	if err == nil {
 26		return fail(t, msgs, "an error is expected but got nil")
 27	}
 28	return true
 29}
 30
 31// ErrorContains asserts that a function returned an error (i.e. not `nil`)
 32// and that the error contains the specified substring.
 33func ErrorContains(t TestingT, err error, contains string, msgs ...string) bool {
 34	t.Helper()
 35
 36	if !Error(t, err, msgs...) {
 37		return false
 38	}
 39
 40	actual := err.Error()
 41	if !strings.Contains(actual, contains) {
 42		return fail(t, msgs, "error %q does not contain %q", actual, contains)
 43	}
 44
 45	return true
 46}
 47
 48// True asserts that the specified value is true.
 49func True(t TestingT, value bool, msgs ...string) bool {
 50	t.Helper()
 51	if !value {
 52		return fail(t, msgs, "should be true")
 53	}
 54	return true
 55}
 56
 57// False asserts that the specified value is false.
 58func False(t TestingT, value bool, msgs ...string) bool {
 59	t.Helper()
 60	if value {
 61		return fail(t, msgs, "should be false")
 62	}
 63	return true
 64}
 65
 66// ErrorIs asserts the given error matches the target error
 67func ErrorIs(t TestingT, err, target error, msgs ...string) bool {
 68	t.Helper()
 69
 70	if err == nil || target == nil {
 71		return err == target
 72	}
 73
 74	// XXX: if errors.Is(err, target) return true
 75
 76	if err.Error() != target.Error() {
 77		return fail(t, msgs, "error mismatch, expected %s, got %s", target.Error(), err.Error())
 78	}
 79
 80	return true
 81}
 82
 83// AbortsWithMessage asserts that the code inside the specified func aborts
 84// (panics when crossing another realm).
 85// Use PanicsWithMessage for asserting local panics within the same realm.
 86//
 87// NOTE: This relies on gno's `revive` mechanism to catch aborts.
 88func AbortsWithMessage(t TestingT, msg string, f any, msgs ...string) bool {
 89	t.Helper()
 90
 91	var didAbort bool
 92	var abortValue any
 93	var r any
 94
 95	if fn, ok := f.(func()); ok {
 96		r = revive(fn)
 97	} else if fn, ok := f.(func(realm)); ok {
 98		r = revive(func() { fn(cross) })
 99	} else {
100		panic("f must be of type func() or func(realm)")
101	}
102	if r != nil {
103		didAbort = true
104		abortValue = r
105	}
106
107	if !didAbort {
108		// If the function didn't abort as expected
109		return fail(t, msgs, "func should abort")
110	}
111
112	// Check if the abort value matches the expected message string
113	abortStr := ufmt.Sprintf("%v", abortValue)
114	if abortStr != msg {
115		return fail(t, msgs, "func should abort with message:\t%q\n\tActual abort value:\t%q", msg, abortStr)
116	}
117
118	// Success: function aborted with the expected message
119	return true
120}
121
122// NotAborts asserts that the code inside the specified func does NOT abort
123// when crossing an execution boundary.
124// Note: Consider using NotPanics which checks for both panics and aborts.
125func NotAborts(t TestingT, f any, msgs ...string) bool {
126	t.Helper()
127
128	var didAbort bool
129	var abortValue any
130	var r any
131
132	if fn, ok := f.(func()); ok {
133		r = revive(fn) // revive() captures the value passed to panic()
134	} else if fn, ok := f.(func(realm)); ok {
135		r = revive(func() { fn(cross) })
136	} else {
137		panic("f must be of type func() or func(realm)")
138	}
139	if r != nil {
140		didAbort = true
141		abortValue = r
142	}
143
144	if didAbort {
145		// Fail if the function aborted when it shouldn't have
146		// Attempt to format the abort value in the error message
147		return fail(t, msgs, "func should not abort\\n\\tAbort value:\\t%v", abortValue)
148	}
149
150	// Success: function did not abort
151	return true
152}
153
154// PanicsWithMessage asserts that the code inside the specified func panics
155// locally within the same execution realm.
156// Use AbortsWithMessage for asserting panics that cross execution boundaries (aborts).
157func PanicsWithMessage(t TestingT, msg string, f any, msgs ...string) bool {
158	t.Helper()
159
160	didPanic, panicValue := checkDidPanic(f)
161	if !didPanic {
162		return fail(t, msgs, "func should panic\n\tPanic value:\t%v", panicValue)
163	}
164
165	// Check if the abort value matches the expected message string
166	panicStr := ufmt.Sprintf("%v", panicValue)
167	if panicStr != msg {
168		return fail(t, msgs, "func should panic with message:\t%q\n\tActual panic value:\t%q", msg, panicStr)
169	}
170	return true
171}
172
173// NotPanics asserts that the code inside the specified func does NOT panic
174// (within the same realm) or abort (due to a cross-realm panic).
175func NotPanics(t TestingT, f any, msgs ...string) bool {
176	t.Helper()
177
178	var panicVal any
179	var didPanic bool
180	var abortVal any
181
182	// Use revive to catch cross-realm aborts
183	abortVal = revive(func() {
184		// Use defer+recover to catch same-realm panics
185		defer func() {
186			if r := recover(); r != nil {
187				didPanic = true
188				panicVal = r
189			}
190		}()
191		// Execute the function
192		if fn, ok := f.(func()); ok {
193			fn()
194		} else if fn, ok := f.(func(realm)); ok {
195			fn(cross)
196		} else {
197			panic("f must be of type func() or func(realm)")
198		}
199	})
200
201	// Check if revive caught an abort
202	if abortVal != nil {
203		return fail(t, msgs, "func should not abort\n\tAbort value:\t%+v", abortVal)
204	}
205
206	// Check if recover caught a panic
207	if didPanic {
208		// Format panic value for message
209		panicMsg := ""
210		if panicVal == nil {
211			panicMsg = "nil"
212		} else if err, ok := panicVal.(error); ok {
213			panicMsg = err.Error()
214		} else if str, ok := panicVal.(string); ok {
215			panicMsg = str
216		} else {
217			// Fallback for other types
218			panicMsg = "panic: unsupported type"
219		}
220		return fail(t, msgs, "func should not panic\n\tPanic value:\t%s", panicMsg)
221	}
222
223	return true // No panic or abort occurred
224}
225
226// Equal asserts that two objects are equal.
227func Equal(t TestingT, expected, actual any, msgs ...string) bool {
228	t.Helper()
229
230	if expected == nil || actual == nil {
231		return expected == actual
232	}
233
234	// XXX: errors
235	// XXX: slices
236	// XXX: pointers
237
238	equal := false
239	ok_ := false
240	es, as := "unsupported type", "unsupported type"
241
242	switch ev := expected.(type) {
243	case string:
244		if av, ok := actual.(string); ok {
245			equal = ev == av
246			ok_ = true
247			es, as = ev, av
248			if !equal {
249				dif := diff.MyersDiff(ev, av)
250				return fail(t, msgs, "uassert.Equal: strings are different\n\tDiff: %s", diff.Format(dif))
251			}
252		}
253	case std.Address:
254		if av, ok := actual.(std.Address); ok {
255			equal = ev == av
256			ok_ = true
257			es, as = string(ev), string(av)
258		}
259	case int:
260		if av, ok := actual.(int); ok {
261			equal = ev == av
262			ok_ = true
263			es, as = strconv.Itoa(ev), strconv.Itoa(av)
264		}
265	case int8:
266		if av, ok := actual.(int8); ok {
267			equal = ev == av
268			ok_ = true
269			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
270		}
271	case int16:
272		if av, ok := actual.(int16); ok {
273			equal = ev == av
274			ok_ = true
275			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
276		}
277	case int32:
278		if av, ok := actual.(int32); ok {
279			equal = ev == av
280			ok_ = true
281			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
282		}
283	case int64:
284		if av, ok := actual.(int64); ok {
285			equal = ev == av
286			ok_ = true
287			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
288		}
289	case uint:
290		if av, ok := actual.(uint); ok {
291			equal = ev == av
292			ok_ = true
293			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
294		}
295	case uint8:
296		if av, ok := actual.(uint8); ok {
297			equal = ev == av
298			ok_ = true
299			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
300		}
301	case uint16:
302		if av, ok := actual.(uint16); ok {
303			equal = ev == av
304			ok_ = true
305			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
306		}
307	case uint32:
308		if av, ok := actual.(uint32); ok {
309			equal = ev == av
310			ok_ = true
311			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
312		}
313	case uint64:
314		if av, ok := actual.(uint64); ok {
315			equal = ev == av
316			ok_ = true
317			es, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)
318		}
319	case bool:
320		if av, ok := actual.(bool); ok {
321			equal = ev == av
322			ok_ = true
323			if ev {
324				es, as = "true", "false"
325			} else {
326				es, as = "false", "true"
327			}
328		}
329	case float32:
330		if av, ok := actual.(float32); ok {
331			equal = ev == av
332			ok_ = true
333		}
334	case float64:
335		if av, ok := actual.(float64); ok {
336			equal = ev == av
337			ok_ = true
338		}
339	default:
340		return fail(t, msgs, "uassert.Equal: unsupported type")
341	}
342
343	/*
344		// XXX: implement stringer and other well known similar interfaces
345		type stringer interface{ String() string }
346		if ev, ok := expected.(stringer); ok {
347			if av, ok := actual.(stringer); ok {
348				equal = ev.String() == av.String()
349				ok_ = true
350			}
351		}
352	*/
353
354	if !ok_ {
355		return fail(t, msgs, "uassert.Equal: different types") // XXX: display the types
356	}
357	if !equal {
358		return fail(t, msgs, "uassert.Equal: same type but different value\n\texpected: %s\n\tactual:   %s", es, as)
359	}
360
361	return true
362}
363
364// NotEqual asserts that two objects are not equal.
365func NotEqual(t TestingT, expected, actual any, msgs ...string) bool {
366	t.Helper()
367
368	if expected == nil || actual == nil {
369		return expected != actual
370	}
371
372	// XXX: errors
373	// XXX: slices
374	// XXX: pointers
375
376	notEqual := false
377	ok_ := false
378	es, as := "unsupported type", "unsupported type"
379
380	switch ev := expected.(type) {
381	case string:
382		if av, ok := actual.(string); ok {
383			notEqual = ev != av
384			ok_ = true
385			es, as = ev, av
386		}
387	case std.Address:
388		if av, ok := actual.(std.Address); ok {
389			notEqual = ev != av
390			ok_ = true
391			es, as = string(ev), string(av)
392		}
393	case int:
394		if av, ok := actual.(int); ok {
395			notEqual = ev != av
396			ok_ = true
397			es, as = strconv.Itoa(ev), strconv.Itoa(av)
398		}
399	case int8:
400		if av, ok := actual.(int8); ok {
401			notEqual = ev != av
402			ok_ = true
403			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
404		}
405	case int16:
406		if av, ok := actual.(int16); ok {
407			notEqual = ev != av
408			ok_ = true
409			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
410		}
411	case int32:
412		if av, ok := actual.(int32); ok {
413			notEqual = ev != av
414			ok_ = true
415			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
416		}
417	case int64:
418		if av, ok := actual.(int64); ok {
419			notEqual = ev != av
420			ok_ = true
421			es, as = strconv.Itoa(int(ev)), strconv.Itoa(int(av))
422		}
423	case uint:
424		if av, ok := actual.(uint); ok {
425			notEqual = ev != av
426			ok_ = true
427			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
428		}
429	case uint8:
430		if av, ok := actual.(uint8); ok {
431			notEqual = ev != av
432			ok_ = true
433			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
434		}
435	case uint16:
436		if av, ok := actual.(uint16); ok {
437			notEqual = ev != av
438			ok_ = true
439			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
440		}
441	case uint32:
442		if av, ok := actual.(uint32); ok {
443			notEqual = ev != av
444			ok_ = true
445			es, as = strconv.FormatUint(uint64(ev), 10), strconv.FormatUint(uint64(av), 10)
446		}
447	case uint64:
448		if av, ok := actual.(uint64); ok {
449			notEqual = ev != av
450			ok_ = true
451			es, as = strconv.FormatUint(ev, 10), strconv.FormatUint(av, 10)
452		}
453	case bool:
454		if av, ok := actual.(bool); ok {
455			notEqual = ev != av
456			ok_ = true
457			if ev {
458				es, as = "true", "false"
459			} else {
460				es, as = "false", "true"
461			}
462		}
463	case float32:
464		if av, ok := actual.(float32); ok {
465			notEqual = ev != av
466			ok_ = true
467		}
468	case float64:
469		if av, ok := actual.(float64); ok {
470			notEqual = ev != av
471			ok_ = true
472		}
473	default:
474		return fail(t, msgs, "uassert.NotEqual: unsupported type")
475	}
476
477	/*
478		// XXX: implement stringer and other well known similar interfaces
479		type stringer interface{ String() string }
480		if ev, ok := expected.(stringer); ok {
481			if av, ok := actual.(stringer); ok {
482				notEqual = ev.String() != av.String()
483				ok_ = true
484			}
485		}
486	*/
487
488	if !ok_ {
489		return fail(t, msgs, "uassert.NotEqual: different types") // XXX: display the types
490	}
491	if !notEqual {
492		return fail(t, msgs, "uassert.NotEqual: same type and same value\n\texpected: %s\n\tactual:   %s", es, as)
493	}
494
495	return true
496}
497
498func isNumberEmpty(n any) (isNumber, isEmpty bool) {
499	switch n := n.(type) {
500	// NOTE: the cases are split individually, so that n becomes of the
501	// asserted type; the type of '0' was correctly inferred and converted
502	// to the corresponding type, int, int8, etc.
503	case int:
504		return true, n == 0
505	case int8:
506		return true, n == 0
507	case int16:
508		return true, n == 0
509	case int32:
510		return true, n == 0
511	case int64:
512		return true, n == 0
513	case uint:
514		return true, n == 0
515	case uint8:
516		return true, n == 0
517	case uint16:
518		return true, n == 0
519	case uint32:
520		return true, n == 0
521	case uint64:
522		return true, n == 0
523	case float32:
524		return true, n == 0
525	case float64:
526		return true, n == 0
527	}
528	return false, false
529}
530
531func Empty(t TestingT, obj any, msgs ...string) bool {
532	t.Helper()
533
534	isNumber, isEmpty := isNumberEmpty(obj)
535	if isNumber {
536		if !isEmpty {
537			return fail(t, msgs, "uassert.Empty: not empty number: %d", obj)
538		}
539	} else {
540		switch val := obj.(type) {
541		case string:
542			if val != "" {
543				return fail(t, msgs, "uassert.Empty: not empty string: %s", val)
544			}
545		case std.Address:
546			var zeroAddr std.Address
547			if val != zeroAddr {
548				return fail(t, msgs, "uassert.Empty: not empty std.Address: %s", string(val))
549			}
550		default:
551			return fail(t, msgs, "uassert.Empty: unsupported type")
552		}
553	}
554	return true
555}
556
557func NotEmpty(t TestingT, obj any, msgs ...string) bool {
558	t.Helper()
559	isNumber, isEmpty := isNumberEmpty(obj)
560	if isNumber {
561		if isEmpty {
562			return fail(t, msgs, "uassert.NotEmpty: empty number: %d", obj)
563		}
564	} else {
565		switch val := obj.(type) {
566		case string:
567			if val == "" {
568				return fail(t, msgs, "uassert.NotEmpty: empty string: %s", val)
569			}
570		case std.Address:
571			var zeroAddr std.Address
572			if val == zeroAddr {
573				return fail(t, msgs, "uassert.NotEmpty: empty std.Address: %s", string(val))
574			}
575		default:
576			return fail(t, msgs, "uassert.NotEmpty: unsupported type")
577		}
578	}
579	return true
580}
581
582// Nil asserts that the value is nil.
583func Nil(t TestingT, value any, msgs ...string) bool {
584	t.Helper()
585	if value != nil {
586		return fail(t, msgs, "should be nil")
587	}
588	return true
589}
590
591// NotNil asserts that the value is not nil.
592func NotNil(t TestingT, value any, msgs ...string) bool {
593	t.Helper()
594	if value == nil {
595		return fail(t, msgs, "should not be nil")
596	}
597	return true
598}
599
600// TypedNil asserts that the value is a typed-nil (nil pointer) value.
601func TypedNil(t TestingT, value any, msgs ...string) bool {
602	t.Helper()
603	if value == nil {
604		return fail(t, msgs, "should be typed-nil but got nil instead")
605	}
606	if !istypednil(value) {
607		return fail(t, msgs, "should be typed-nil")
608	}
609	return true
610}
611
612// NotTypedNil asserts that the value is not a typed-nil (nil pointer) value.
613func NotTypedNil(t TestingT, value any, msgs ...string) bool {
614	t.Helper()
615	if istypednil(value) {
616		return fail(t, msgs, "should not be typed-nil")
617	}
618	return true
619}