blob: 03e3801790bc7ed9a33d5578cb880365733cd7bd [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Mateusz Zalegaddf19b42022-06-22 12:27:37 +02004// This file implements test helper functions that augment the way any given
5// test is run.
6package util
7
8import (
9 "context"
10 "errors"
Serge Bazanski9104e382023-04-04 20:08:21 +020011 "fmt"
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020012 "testing"
13 "time"
14)
15
Serge Bazanskic16f0482024-03-21 11:57:41 +010016// TestEventual creates a new subtest looping the given function until it either
17// doesn't return an error anymore, the timeout is exceeded or PermanentError is
18// returned. The last returned non-context-related error is being used as the
19// test error.
20func 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 +010021 start := time.Now()
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020022 ctx, cancel := context.WithTimeout(ctx, timeout)
23 t.Helper()
Tim Windelschmidt47eb65b2025-03-27 16:49:27 +010024 fmt.Printf("Test: %s: starting...\n", name)
Serge Bazanskic16f0482024-03-21 11:57:41 +010025 return t.Run(name, func(t *testing.T) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020026 defer cancel()
27 var lastErr = errors.New("test didn't run to completion at least once")
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020028 for {
29 err := f(ctx)
30 if err == nil {
Tim Windelschmidt47eb65b2025-03-27 16:49:27 +010031 fmt.Printf("Test: %s: okay after %.1f seconds\n", name, time.Since(start).Seconds())
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020032 return
33 }
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020034 if errors.Is(err, ctx.Err()) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020035 t.Fatal(lastErr)
36 }
Serge Bazanski9104e382023-04-04 20:08:21 +020037 if errors.Is(err, &PermanentError{}) {
38 t.Fatal(err)
39 }
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020040 lastErr = err
41 select {
42 case <-ctx.Done():
43 t.Fatal(lastErr)
44 case <-time.After(1 * time.Second):
45 }
46 }
47 })
48}
Serge Bazanski9104e382023-04-04 20:08:21 +020049
Serge Bazanskic16f0482024-03-21 11:57:41 +010050// MustTestEventual is like TestEventual, but aborts the `t` test with Fatal if a
51// timeout occurred or PermanentError was returned.
52func MustTestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) {
53 if !TestEventual(t, name, ctx, timeout, f) {
54 t.Fatalf("Test: %s: fatal failure", name)
55 }
56}
57
Serge Bazanski9104e382023-04-04 20:08:21 +020058// PermanentError can be returned inside TestEventual to indicate that the test
59// is 'stuck', that it will not make progress anymore and that it should be
60// failed immediately.
61type PermanentError struct {
62 Err error
63}
64
65func (p *PermanentError) Error() string {
66 return fmt.Sprintf("test permanently failed: %v", p.Err)
67}
68
69func (p *PermanentError) Unwrap() error {
70 return p.Err
71}
72
73func (p *PermanentError) Is(o error) bool {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +020074 var op *PermanentError
75 if !errors.As(o, &op) {
Serge Bazanski9104e382023-04-04 20:08:21 +020076 return false
77 }
78 if p.Err == nil || op.Err == nil {
79 return true
80 }
81 return errors.Is(p.Err, op.Err)
82}
83
84// Permanent wraps the given error into a PermanentError, which will cause
85// TestEventual to immediately fail the test it's returned within.
86func Permanent(err error) error {
87 return &PermanentError{
88 Err: err,
89 }
90}