Search Apps Documentation Source Content File Folder Download Copy Actions Download

uassert.gno

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