blob: 1a72ad8950cf0a8a5fd6997d62db339583e07515 [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package supervisor
import (
"context"
"errors"
"log"
"sort"
"testing"
"time"
"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 supervision tree will also be cleaned up and the test
// will block until all runnables have exited.
//
// 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.TB, r func(ctx context.Context) error) (context.CancelFunc, *logtree.LogTree) {
t.Helper()
ctx, ctxC := context.WithCancel(context.Background())
lt := logtree.New()
// Only log to stderr when we're running in a test, not in a fuzz harness or a
// benchmark - otherwise we just waste CPU cycles.
verbose := false
if _, ok := t.(*testing.T); ok {
verbose = true
}
if verbose {
logtree.PipeAllToTest(t, lt)
}
sup := New(ctx, func(ctx context.Context) error {
Logger(ctx).Infof("Starting test %s...", t.Name())
if err := r(ctx); err != nil && !errors.Is(err, ctx.Err()) {
t.Errorf("Supervised runnable in harness returned error: %v", err)
return err
}
return nil
}, WithExistingLogtree(lt))
t.Cleanup(func() {
ctxC()
if verbose {
log.Printf("supervisor.TestHarness: Waiting for supervisor runnables to die...")
}
timeoutNag := time.Now().Add(5 * time.Second)
for {
live := sup.liveRunnables()
if len(live) == 0 {
if verbose {
log.Printf("supervisor.TestHarness: All done.")
}
return
}
if time.Now().After(timeoutNag) {
timeoutNag = time.Now().Add(5 * time.Second)
sort.Strings(live)
if verbose {
log.Printf("supervisor.TestHarness: Still live:")
for _, l := range live {
log.Printf("supervisor.TestHarness: - %s", l)
}
}
}
time.Sleep(time.Second)
}
})
return ctxC, lt
}