blob: 4a8a1e9978a955e4f3ee1556c2b2abb2b646d36d [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"
Serge Bazanski05f813b2023-03-16 17:58:39 +010014
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020015 "source.monogon.dev/osbase/test/launch"
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020016)
17
Serge Bazanskic16f0482024-03-21 11:57:41 +010018// TestEventual creates a new subtest looping the given function until it either
19// doesn't return an error anymore, the timeout is exceeded or PermanentError is
20// returned. The last returned non-context-related error is being used as the
21// test error.
22func 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 +010023 start := time.Now()
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020024 ctx, cancel := context.WithTimeout(ctx, timeout)
25 t.Helper()
Serge Bazanski05f813b2023-03-16 17:58:39 +010026 launch.Log("Test: %s: starting...", name)
Serge Bazanskic16f0482024-03-21 11:57:41 +010027 return t.Run(name, func(t *testing.T) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020028 defer cancel()
29 var lastErr = errors.New("test didn't run to completion at least once")
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020030 for {
31 err := f(ctx)
32 if err == nil {
Serge Bazanski05f813b2023-03-16 17:58:39 +010033 launch.Log("Test: %s: okay after %.1f seconds", name, time.Since(start).Seconds())
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020034 return
35 }
Tim Windelschmidtd5f851b2024-04-23 14:59:37 +020036 if errors.Is(err, ctx.Err()) {
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020037 t.Fatal(lastErr)
38 }
Serge Bazanski9104e382023-04-04 20:08:21 +020039 if errors.Is(err, &PermanentError{}) {
40 t.Fatal(err)
41 }
Mateusz Zalegaddf19b42022-06-22 12:27:37 +020042 lastErr = err
43 select {
44 case <-ctx.Done():
45 t.Fatal(lastErr)
46 case <-time.After(1 * time.Second):
47 }
48 }
49 })
50}
Serge Bazanski9104e382023-04-04 20:08:21 +020051
Serge Bazanskic16f0482024-03-21 11:57:41 +010052// MustTestEventual is like TestEventual, but aborts the `t` test with Fatal if a
53// timeout occurred or PermanentError was returned.
54func MustTestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) {
55 if !TestEventual(t, name, ctx, timeout, f) {
56 t.Fatalf("Test: %s: fatal failure", name)
57 }
58}
59
Serge Bazanski9104e382023-04-04 20:08:21 +020060// PermanentError can be returned inside TestEventual to indicate that the test
61// is 'stuck', that it will not make progress anymore and that it should be
62// failed immediately.
63type PermanentError struct {
64 Err error
65}
66
67func (p *PermanentError) Error() string {
68 return fmt.Sprintf("test permanently failed: %v", p.Err)
69}
70
71func (p *PermanentError) Unwrap() error {
72 return p.Err
73}
74
75func (p *PermanentError) Is(o error) bool {
Tim Windelschmidtaf821c82024-04-23 15:03:52 +020076 var op *PermanentError
77 if !errors.As(o, &op) {
Serge Bazanski9104e382023-04-04 20:08:21 +020078 return false
79 }
80 if p.Err == nil || op.Err == nil {
81 return true
82 }
83 return errors.Is(p.Err, op.Err)
84}
85
86// Permanent wraps the given error into a PermanentError, which will cause
87// TestEventual to immediately fail the test it's returned within.
88func Permanent(err error) error {
89 return &PermanentError{
90 Err: err,
91 }
92}