blob: 95de02ab3e5c85c2dfd66a11dd95ca0f5ef4c486 [file] [log] [blame]
Mateusz Zalegaddf19b42022-06-22 12:27:37 +02001// This file implements test helper functions that augment the way any given
2// test is run.
3package util
4
5import (
6 "context"
7 "errors"
Serge Bazanski9104e382023-04-04 20:08:21 +02008 "fmt"
Mateusz Zalegaddf19b42022-06-22 12:27:37 +02009 "testing"
10 "time"
Serge Bazanski05f813b2023-03-16 17:58:39 +010011
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020012 "source.monogon.dev/osbase/test/launch"
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020013)
14
Serge Bazanskic16f0482024-03-21 11:57:41 +010015// TestEventual creates a new subtest looping the given function until it either
16// doesn't return an error anymore, the timeout is exceeded or PermanentError is
17// returned. The last returned non-context-related error is being used as the
18// test error.
19func TestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) bool {
Serge Bazanski05f813b2023-03-16 17:58:39 +010020 start := time.Now()
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020021 ctx, cancel := context.WithTimeout(ctx, timeout)
22 t.Helper()
Serge Bazanski05f813b2023-03-16 17:58:39 +010023 launch.Log("Test: %s: starting...", name)
Serge Bazanskic16f0482024-03-21 11:57:41 +010024 return t.Run(name, func(t *testing.T) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020025 defer cancel()
26 var lastErr = errors.New("test didn't run to completion at least once")
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020027 for {
28 err := f(ctx)
29 if err == nil {
Serge Bazanski05f813b2023-03-16 17:58:39 +010030 launch.Log("Test: %s: okay after %.1f seconds", name, time.Since(start).Seconds())
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020031 return
32 }
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020033 if errors.Is(err, ctx.Err()) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020034 t.Fatal(lastErr)
35 }
Serge Bazanski9104e382023-04-04 20:08:21 +020036 if errors.Is(err, &PermanentError{}) {
37 t.Fatal(err)
38 }
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020039 lastErr = err
40 select {
41 case <-ctx.Done():
42 t.Fatal(lastErr)
43 case <-time.After(1 * time.Second):
44 }
45 }
46 })
47}
Serge Bazanski9104e382023-04-04 20:08:21 +020048
Serge Bazanskic16f0482024-03-21 11:57:41 +010049// MustTestEventual is like TestEventual, but aborts the `t` test with Fatal if a
50// timeout occurred or PermanentError was returned.
51func MustTestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) {
52 if !TestEventual(t, name, ctx, timeout, f) {
53 t.Fatalf("Test: %s: fatal failure", name)
54 }
55}
56
Serge Bazanski9104e382023-04-04 20:08:21 +020057// PermanentError can be returned inside TestEventual to indicate that the test
58// is 'stuck', that it will not make progress anymore and that it should be
59// failed immediately.
60type PermanentError struct {
61 Err error
62}
63
64func (p *PermanentError) Error() string {
65 return fmt.Sprintf("test permanently failed: %v", p.Err)
66}
67
68func (p *PermanentError) Unwrap() error {
69 return p.Err
70}
71
72func (p *PermanentError) Is(o error) bool {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +020073 var op *PermanentError
74 if !errors.As(o, &op) {
Serge Bazanski9104e382023-04-04 20:08:21 +020075 return false
76 }
77 if p.Err == nil || op.Err == nil {
78 return true
79 }
80 return errors.Is(p.Err, op.Err)
81}
82
83// Permanent wraps the given error into a PermanentError, which will cause
84// TestEventual to immediately fail the test it's returned within.
85func Permanent(err error) error {
86 return &PermanentError{
87 Err: err,
88 }
89}