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}