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}