package hostfiles import ( "bytes" "context" "errors" "log" "strings" "sync" "testing" "time" ) // relayLog captures the package's log lines for one test. type relayLog struct { mu sync.Mutex buf bytes.Buffer } func (l *relayLog) Write(p []byte) (int, error) { l.mu.Lock() defer l.mu.Unlock() return l.buf.Write(p) } func (l *relayLog) count(substr string) int { l.mu.Lock() defer l.mu.Unlock() return strings.Count(l.buf.String(), substr) } func captureRelayLog(t *testing.T) *relayLog { t.Helper() l := &relayLog{} prev := log.Writer() log.SetOutput(l) t.Cleanup(func() { log.SetOutput(prev) }) return l } type flakyStager struct { mu sync.Mutex fail int staged [][]byte block chan struct{} } func (f *flakyStager) StagePlatform(_ context.Context, name string, body []byte) error { if f.block != nil { <-f.block } f.mu.Lock() defer f.mu.Unlock() if f.fail > 0 { f.fail-- return errors.New("store unreachable") } f.staged = append(f.staged, append([]byte(nil), body...)) return nil } func (f *flakyStager) last() []byte { f.mu.Lock() defer f.mu.Unlock() if len(f.staged) == 0 { return nil } return f.staged[len(f.staged)-1] } // Ruling 22: Publish never waits on the store; Run stages the latest body, // retrying a failed stage, and stages nothing again once it has. Two // failed stages log one "failing" line and the stage that follows one // "recovered" line: transitions, as run.go logs a scan, not every retry. func TestPlatformRelayStagesTheLatestBodyWithoutBlockingItsCaller(t *testing.T) { logs := captureRelayLog(t) st := &flakyStager{fail: 2, block: make(chan struct{})} r := &PlatformRelay{Name: "instance-key-adoption.json", Stage: st, Retry: 10 * time.Millisecond} ctx, cancel := context.WithCancel(context.Background()) defer cancel() done := make(chan error, 1) go func() { done <- r.Run(ctx) }() published := make(chan struct{}) go func() { r.Publish([]byte("one")); r.Publish([]byte("two")); close(published) }() select { case <-published: case <-time.After(time.Second): t.Fatal("Publish waited on a store that does not answer") } close(st.block) deadline := time.Now().Add(2 * time.Second) for string(st.last()) != "two" { if time.Now().After(deadline) { t.Fatalf("staged %q, want the latest body after two failed stages", st.last()) } time.Sleep(5 * time.Millisecond) } time.Sleep(50 * time.Millisecond) st.mu.Lock() n := len(st.staged) st.mu.Unlock() if n != 1 { t.Fatalf("staged %d times, want once", n) } if failing, recovered := logs.count(r.Name+": stage failing"), logs.count(r.Name+": stage recovered"); failing != 1 || recovered != 1 { t.Fatalf("logged %d failing and %d recovered lines across two failed stages, want one of each", failing, recovered) } cancel() if err := <-done; !errors.Is(err, context.Canceled) { t.Fatalf("Run = %v", err) } }