uassert.gno

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