mesh/delivery-group group feat/delivery-parts checking: 1 of 2 member(s) ready
mesh/repo-check head 8e6a31bc6ab4 merged into trunk 7f618177d677 beside mesh-catalog#374@75957fc3 — pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh):…
mesh/merge-gate head 8e6a31bc6ab4 merged into trunk 7f618177d677 — pass: group feat/delivery-parts composed: every machine composes with the change as it d…
76 lines
2.4 KiB
Go
76 lines
2.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// runner runs the program's run function in a goroutine and stops it, bounded: a program that does not return when its
|
|
// context ends fails the test after a deadline with a message, and the result can be taken more than once (the
|
|
// integration test takes it in its body and again in its cleanup; a plain buffered channel of one value gave the second
|
|
// taker nothing, and the cleanup waited for ever: the merge check's 10-minute timeout).
|
|
type runner struct {
|
|
stop context.CancelFunc
|
|
done chan error
|
|
}
|
|
|
|
func startRunner(parent context.Context, run func(context.Context) error) *runner {
|
|
ctx, stop := context.WithCancel(parent)
|
|
r := &runner{stop: stop, done: make(chan error, 1)}
|
|
go func() {
|
|
r.done <- run(ctx)
|
|
close(r.done) // a second taker reads the closed channel at once
|
|
}()
|
|
return r
|
|
}
|
|
|
|
// finish stops the program and waits at most d for it to return. Called again, it returns at once.
|
|
func (r *runner) finish(t testing.TB, d time.Duration) error {
|
|
t.Helper()
|
|
r.stop()
|
|
select {
|
|
case err := <-r.done:
|
|
return err
|
|
case <-time.After(d):
|
|
t.Errorf("the program did not return within %s of its context ending", d)
|
|
return errors.New("did not return")
|
|
}
|
|
}
|
|
|
|
func TestRunnerCanBeFinishedTwice(t *testing.T) { // 1240 (review round 1: body and cleanup both take the result)
|
|
r := startRunner(context.Background(), func(ctx context.Context) error { <-ctx.Done(); return nil })
|
|
if err := r.finish(t, 2*time.Second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second := make(chan struct{})
|
|
go func() { r.finish(t, 2*time.Second); close(second) }()
|
|
select {
|
|
case <-second:
|
|
case <-time.After(5 * time.Second):
|
|
t.Fatal("the second finish waits for ever: the result was taken once")
|
|
}
|
|
}
|
|
|
|
func TestRunnerReportsAProgramThatDoesNotReturn(t *testing.T) { // 1240: a hang fails fast with a message
|
|
block := make(chan struct{})
|
|
t.Cleanup(func() { close(block) })
|
|
r := startRunner(context.Background(), func(context.Context) error { <-block; return nil })
|
|
probe := &recorder{TB: t}
|
|
start := time.Now()
|
|
if err := r.finish(probe, 100*time.Millisecond); err == nil || !probe.failed {
|
|
t.Fatalf("a program that never returns was not reported (err %v)", err)
|
|
}
|
|
if time.Since(start) > 5*time.Second {
|
|
t.Fatal("the report was not prompt")
|
|
}
|
|
}
|
|
|
|
type recorder struct {
|
|
testing.TB
|
|
failed bool
|
|
}
|
|
|
|
func (r *recorder) Errorf(string, ...any) { r.failed = true }
|