blob: e25fe1086d45cf188d05554f0edb5200dc5e3675 [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
12 "source.monogon.dev/metropolis/test/launch"
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020013)
14
15// TestEventual creates a new subtest looping the given function until it
16// either doesn't return an error anymore or the timeout is exceeded. The last
17// returned non-context-related error is being used as the test error.
18func TestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) {
Serge Bazanski05f813b2023-03-16 17:58:39 +010019 start := time.Now()
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020020 ctx, cancel := context.WithTimeout(ctx, timeout)
21 t.Helper()
Serge Bazanski05f813b2023-03-16 17:58:39 +010022 launch.Log("Test: %s: starting...", name)
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020023 t.Run(name, func(t *testing.T) {
24 defer cancel()
25 var lastErr = errors.New("test didn't run to completion at least once")
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020026 for {
27 err := f(ctx)
28 if err == nil {
Serge Bazanski05f813b2023-03-16 17:58:39 +010029 launch.Log("Test: %s: okay after %.1f seconds", name, time.Since(start).Seconds())
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020030 return
31 }
32 if err == ctx.Err() {
33 t.Fatal(lastErr)
34 }
Serge Bazanski9104e382023-04-04 20:08:21 +020035 if errors.Is(err, &PermanentError{}) {
36 t.Fatal(err)
37 }
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020038 lastErr = err
39 select {
40 case <-ctx.Done():
41 t.Fatal(lastErr)
42 case <-time.After(1 * time.Second):
43 }
44 }
45 })
46}
Serge Bazanski9104e382023-04-04 20:08:21 +020047
48// PermanentError can be returned inside TestEventual to indicate that the test
49// is 'stuck', that it will not make progress anymore and that it should be
50// failed immediately.
51type PermanentError struct {
52 Err error
53}
54
55func (p *PermanentError) Error() string {
56 return fmt.Sprintf("test permanently failed: %v", p.Err)
57}
58
59func (p *PermanentError) Unwrap() error {
60 return p.Err
61}
62
63func (p *PermanentError) Is(o error) bool {
64 op, ok := o.(*PermanentError)
65 if !ok {
66 return false
67 }
68 if p.Err == nil || op.Err == nil {
69 return true
70 }
71 return errors.Is(p.Err, op.Err)
72}
73
74// Permanent wraps the given error into a PermanentError, which will cause
75// TestEventual to immediately fail the test it's returned within.
76func Permanent(err error) error {
77 return &PermanentError{
78 Err: err,
79 }
80}