m/pkg/{logtree,supervisor}: add test helpers
This adds two functions:
logtree.PipeAllToStderr
supervisor.NewHarness
These are designed to simplify tests that exercise code which expects to
be run as a supervisor runnable and/or have access to a logtree
instance.
Change-Id: Ibce77aa4927515af7c273d07ced15215ff456ecc
Reviewed-on: https://review.monogon.dev/c/monogon/+/205
Reviewed-by: Leopold Schabel <leo@nexantic.com>
diff --git a/metropolis/pkg/logtree/BUILD.bazel b/metropolis/pkg/logtree/BUILD.bazel
index d325e42..f49430e 100644
--- a/metropolis/pkg/logtree/BUILD.bazel
+++ b/metropolis/pkg/logtree/BUILD.bazel
@@ -14,6 +14,7 @@
"logtree_access.go",
"logtree_entry.go",
"logtree_publisher.go",
+ "testhelpers.go",
],
importpath = "source.monogon.dev/metropolis/pkg/logtree",
visibility = ["//metropolis:__subpackages__"],
diff --git a/metropolis/pkg/logtree/testhelpers.go b/metropolis/pkg/logtree/testhelpers.go
new file mode 100644
index 0000000..a033bdb
--- /dev/null
+++ b/metropolis/pkg/logtree/testhelpers.go
@@ -0,0 +1,38 @@
+package logtree
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "testing"
+)
+
+// PipeAllToStderr starts a goroutine that will forward all logtree entries
+// into stderr, in the canonical logtree payload representation.
+//
+// It's designed to be used in tests, and will automatically stop when the
+// test/benchmark it's running in exits.
+func PipeAllToStderr(t *testing.T, lt *LogTree) {
+ t.Helper()
+
+ reader, err := lt.Read("", WithChildren(), WithStream())
+ if err != nil {
+ t.Fatalf("Failed to set up logtree reader: %v", err)
+ }
+
+ // Internal context used to cancel the goroutine. This could also be a
+ // implemented via a channel.
+ ctx, ctxC := context.WithCancel(context.Background())
+ t.Cleanup(ctxC)
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case p := <-reader.Stream:
+ fmt.Fprintf(os.Stderr, "%s\n", p.String())
+ }
+ }
+ }()
+}
diff --git a/metropolis/pkg/supervisor/supervisor.go b/metropolis/pkg/supervisor/supervisor.go
index f26732d..77c2d02 100644
--- a/metropolis/pkg/supervisor/supervisor.go
+++ b/metropolis/pkg/supervisor/supervisor.go
@@ -123,6 +123,10 @@
// New creates a new supervisor with its root running the given root runnable.
// The given context can be used to cancel the entire supervision tree.
+//
+// For tests, we reccomend using TestHarness instead, which will also stream
+// logs to stderr and take care of propagating root runnable errors to the test
+// output.
func New(ctx context.Context, rootRunnable Runnable, opts ...SupervisorOpt) *supervisor {
sup := &supervisor{
logtree: logtree.New(),
diff --git a/metropolis/pkg/supervisor/supervisor_testhelpers.go b/metropolis/pkg/supervisor/supervisor_testhelpers.go
index 5431ea1..711ed00 100644
--- a/metropolis/pkg/supervisor/supervisor_testhelpers.go
+++ b/metropolis/pkg/supervisor/supervisor_testhelpers.go
@@ -15,3 +15,44 @@
// limitations under the License.
package supervisor
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "source.monogon.dev/metropolis/pkg/logtree"
+)
+
+// TestHarness runs a supervisor in a harness designed for unit testing
+// runnables and runnable trees.
+//
+// The given runnable will be run in a new supervisor, and the logs from this
+// supervisor will be streamed to stderr. If the runnable returns a non-context
+// error, the harness will throw a test error, but will not abort the test.
+//
+// The harness also returns a context cancel function that can be used to
+// terminate the started supervisor early. Regardless of manual cancellation,
+// the supervisor will always be terminated up at the end of the test/benchmark
+// it's running in.
+//
+// The second returned value is the logtree used by this supervisor. It can be
+// used to assert some log messages are emitted in tests that exercise some
+// log-related functionality.
+func TestHarness(t *testing.T, r func(ctx context.Context) error) (context.CancelFunc, *logtree.LogTree) {
+ t.Helper()
+
+ ctx, ctxC := context.WithCancel(context.Background())
+ t.Cleanup(ctxC)
+
+ lt := logtree.New()
+ logtree.PipeAllToStderr(t, lt)
+
+ New(ctx, func(ctx context.Context) error {
+ if err := r(ctx); err != nil && !errors.Is(err, ctx.Err()) {
+ t.Errorf("Supervised runnable in harness returned error: %v", err)
+ }
+ return nil
+ }, WithExistingLogtree(lt))
+ return ctxC, lt
+}