Fix the teller's integration test taking its result twice: it hung the merge check (issue 1240, review round 1)
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…

This commit is contained in:
2026-10-19 17:39:44 +02:00
parent 35ff628458
commit 8e6a31bc6a
2 changed files with 81 additions and 14 deletions
@@ -83,7 +83,7 @@ func TestTellerWaitsForTheGate(t *testing.T) { // 1240/AC2
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
broker := filepath.Join(t.TempDir(), "broker")
@@ -93,10 +93,8 @@ func TestTellerWaitsForTheGate(t *testing.T) { // 1240/AC2
}
env := map[string]string{"MESH_BROKER_FILE": broker}
logs := &syncBuffer{}
runCtx, stop := context.WithCancel(ctx)
done := make(chan error, 1)
go func() { done <- run(runCtx, func(k string) string { return env[k] }, logs) }()
t.Cleanup(func() { stop(); <-done })
rn := startRunner(ctx, func(c context.Context) error { return run(c, func(k string) string { return env[k] }, logs) })
t.Cleanup(func() { rn.finish(t, 10*time.Second) })
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline) && !strings.Contains(logs.String(), "waiting for the gate's asks"); time.Sleep(20 * time.Millisecond) {
}
@@ -104,7 +102,7 @@ func TestTellerWaitsForTheGate(t *testing.T) { // 1240/AC2
t.Fatalf("the teller said it waits %d times, want once: %q", n, logs.String())
}
select {
case err := <-done:
case err := <-rn.done:
t.Fatalf("the teller stopped: %v", err)
case <-time.After(500 * time.Millisecond):
}
@@ -119,13 +117,7 @@ func TestTellerWaitsForTheGate(t *testing.T) { // 1240/AC2
if _, err := js.KeyValue(ctx, "mesh-delivery-gate_asks"); err == nil {
t.Error("the gate's asks bucket exists")
}
stop()
select {
case err := <-done:
if err != nil {
t.Errorf("the teller stopped with %v, want a clean stop", err)
}
case <-time.After(5 * time.Second):
t.Error("the teller did not stop when asked")
if err := rn.finish(t, 10*time.Second); err != nil {
t.Errorf("the teller stopped with %v, want a clean stop", err)
}
}
@@ -0,0 +1,75 @@
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 }