carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostfiles
2
3import (
4 "bytes"
5 "context"
6 "errors"
7 "log"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12)
13
14// relayLog captures the package's log lines for one test.
15type relayLog struct {
16 mu sync.Mutex
17 buf bytes.Buffer
18}
19
20func (l *relayLog) Write(p []byte) (int, error) {
21 l.mu.Lock()
22 defer l.mu.Unlock()
23 return l.buf.Write(p)
24}
25
26func (l *relayLog) count(substr string) int {
27 l.mu.Lock()
28 defer l.mu.Unlock()
29 return strings.Count(l.buf.String(), substr)
30}
31
32func captureRelayLog(t *testing.T) *relayLog {
33 t.Helper()
34 l := &relayLog{}
35 prev := log.Writer()
36 log.SetOutput(l)
37 t.Cleanup(func() { log.SetOutput(prev) })
38 return l
39}
40
41type flakyStager struct {
42 mu sync.Mutex
43 fail int
44 staged [][]byte
45 block chan struct{}
46}
47
48func (f *flakyStager) StagePlatform(_ context.Context, name string, body []byte) error {
49 if f.block != nil {
50 <-f.block
51 }
52 f.mu.Lock()
53 defer f.mu.Unlock()
54 if f.fail > 0 {
55 f.fail--
56 return errors.New("store unreachable")
57 }
58 f.staged = append(f.staged, append([]byte(nil), body...))
59 return nil
60}
61
62func (f *flakyStager) last() []byte {
63 f.mu.Lock()
64 defer f.mu.Unlock()
65 if len(f.staged) == 0 {
66 return nil
67 }
68 return f.staged[len(f.staged)-1]
69}
70
71// Ruling 22: Publish never waits on the store; Run stages the latest body,
72// retrying a failed stage, and stages nothing again once it has. Two
73// failed stages log one "failing" line and the stage that follows one
74// "recovered" line: transitions, as run.go logs a scan, not every retry.
75func TestPlatformRelayStagesTheLatestBodyWithoutBlockingItsCaller(t *testing.T) {
76 logs := captureRelayLog(t)
77 st := &flakyStager{fail: 2, block: make(chan struct{})}
78 r := &PlatformRelay{Name: "instance-key-adoption.json", Stage: st, Retry: 10 * time.Millisecond}
79 ctx, cancel := context.WithCancel(context.Background())
80 defer cancel()
81 done := make(chan error, 1)
82 go func() { done <- r.Run(ctx) }()
83 published := make(chan struct{})
84 go func() { r.Publish([]byte("one")); r.Publish([]byte("two")); close(published) }()
85 select {
86 case <-published:
87 case <-time.After(time.Second):
88 t.Fatal("Publish waited on a store that does not answer")
89 }
90 close(st.block)
91 deadline := time.Now().Add(2 * time.Second)
92 for string(st.last()) != "two" {
93 if time.Now().After(deadline) {
94 t.Fatalf("staged %q, want the latest body after two failed stages", st.last())
95 }
96 time.Sleep(5 * time.Millisecond)
97 }
98 time.Sleep(50 * time.Millisecond)
99 st.mu.Lock()
100 n := len(st.staged)
101 st.mu.Unlock()
102 if n != 1 {
103 t.Fatalf("staged %d times, want once", n)
104 }
105 if failing, recovered := logs.count(r.Name+": stage failing"), logs.count(r.Name+": stage recovered"); failing != 1 || recovered != 1 {
106 t.Fatalf("logged %d failing and %d recovered lines across two failed stages, want one of each", failing, recovered)
107 }
108 cancel()
109 if err := <-done; !errors.Is(err, context.Canceled) {
110 t.Fatalf("Run = %v", err)
111 }
112}
113