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

uassert.gno

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