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}