Files

78 lines
2.9 KiB
Go

package web
import (
"context"
"reflect"
"sync"
"testing"
"time"
)
// R-705 (v0.279.0) — the manual night chain runs dump → Tier 2 → off-site → update leg, IN ORDER, one at a
// time; it is refused while anything else runs and while a chain runs; with no off-site target it skips
// only that leg. COMPANION RED-PROOF: swap two step calls in run() → the order assertion fails; drop the
// busy() check in start() → the refusal assertion fails.
func TestR705_NightChainRunsTheLegsInOrderAndRefusesWhenBusy(t *testing.T) {
var mu sync.Mutex
var order []string
rec := func(n string) { mu.Lock(); order = append(order, n); mu.Unlock() }
release := make(chan struct{})
done := make(chan struct{})
c := nightChain{
dump: func(context.Context) error { rec("db-dump"); <-release; return nil },
tier2: func() { rec("tier2") },
offsite: func(context.Context) error { rec("offsite"); return nil },
leg: func(context.Context) { rec("update-leg"); close(done) },
busy: func() (bool, string) { return false, "" },
logf: func(string, ...interface{}) {},
}
ok, why, legs := c.start()
if !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "offsite", "update-leg"}) {
t.Fatalf("start: ok=%v why=%q legs=%v", ok, why, legs)
}
// A second press while the first chain runs is refused.
if ok, why, _ := c.start(); ok || why != "the night's chain is already running" {
t.Fatalf("second start while running: ok=%v why=%q", ok, why)
}
close(release)
select {
case <-done:
case <-time.After(5 * time.Second):
t.Fatal("the chain never reached the update leg")
}
for i := 0; i < 100 && nightChainRunning.Load(); i++ {
time.Sleep(10 * time.Millisecond)
}
if want := []string{"db-dump", "tier2", "offsite", "update-leg"}; !reflect.DeepEqual(order, want) {
t.Fatalf("order %v, want %v", order, want)
}
// Busy: refused, nothing runs.
order = nil
c.busy = func() (bool, string) { return true, "a guarded update is running" }
if ok, why, _ := c.start(); ok || why != "a guarded update is running" {
t.Fatalf("busy start: ok=%v why=%q", ok, why)
}
time.Sleep(50 * time.Millisecond)
if len(order) != 0 {
t.Fatalf("a refused chain ran legs: %v", order)
}
// No off-site target: that leg alone is skipped.
c.busy = func() (bool, string) { return false, "" }
c.offsite = nil
c.dump = func(context.Context) error { rec("db-dump"); return nil }
done2 := make(chan struct{})
c.leg = func(context.Context) { rec("update-leg"); close(done2) }
if ok, _, legs := c.start(); !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "update-leg"}) {
t.Fatalf("no-offsite start: ok=%v legs=%v", ok, legs)
}
<-done2
for i := 0; i < 100 && nightChainRunning.Load(); i++ {
time.Sleep(10 * time.Millisecond)
}
if want := []string{"db-dump", "tier2", "update-leg"}; !reflect.DeepEqual(order, want) {
t.Fatalf("no-offsite order %v, want %v", order, want)
}
}