m/t/e2e: move testEventual to common test util pkg

testEventual, among other implementation, will be reused in metroctl
tests.

Change-Id: I24df31a72034b707e3906889e7a569c8e97669ad
Reviewed-on: https://review.monogon.dev/c/monogon/+/788
Tested-by: Jenkins CI
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
diff --git a/metropolis/test/util/runners.go b/metropolis/test/util/runners.go
new file mode 100644
index 0000000..47bf59f
--- /dev/null
+++ b/metropolis/test/util/runners.go
@@ -0,0 +1,38 @@
+// This file implements test helper functions that augment the way any given
+// test is run.
+package util
+
+import (
+	"context"
+	"errors"
+	"testing"
+	"time"
+)
+
+// TestEventual creates a new subtest looping the given function until it
+// either doesn't return an error anymore or the timeout is exceeded. The last
+// returned non-context-related error is being used as the test error.
+func TestEventual(t *testing.T, name string, ctx context.Context, timeout time.Duration, f func(context.Context) error) {
+	ctx, cancel := context.WithTimeout(ctx, timeout)
+	t.Helper()
+	t.Run(name, func(t *testing.T) {
+		defer cancel()
+		var lastErr = errors.New("test didn't run to completion at least once")
+		t.Parallel()
+		for {
+			err := f(ctx)
+			if err == nil {
+				return
+			}
+			if err == ctx.Err() {
+				t.Fatal(lastErr)
+			}
+			lastErr = err
+			select {
+			case <-ctx.Done():
+				t.Fatal(lastErr)
+			case <-time.After(1 * time.Second):
+			}
+		}
+	})
+}