Monorepo for Tangled
tangled.org
1package knotcompat
2
3import (
4 "strconv"
5 "sync"
6 "testing"
7 "time"
8)
9
10type fakeLatch struct {
11 mu sync.Mutex
12 native map[string]bool
13 marks []string
14 reads int
15}
16
17func newFakeLatch() *fakeLatch {
18 return &fakeLatch{native: map[string]bool{}}
19}
20
21func (f *fakeLatch) IsNative(host string) bool {
22 f.mu.Lock()
23 defer f.mu.Unlock()
24 f.reads++
25 return f.native[host]
26}
27
28func (f *fakeLatch) readCount() int {
29 f.mu.Lock()
30 defer f.mu.Unlock()
31 return f.reads
32}
33
34func (f *fakeLatch) MarkNative(host string) {
35 f.mu.Lock()
36 defer f.mu.Unlock()
37 f.native[host] = true
38 f.marks = append(f.marks, host)
39}
40
41func (f *fakeLatch) markCount() int {
42 f.mu.Lock()
43 defer f.mu.Unlock()
44 return len(f.marks)
45}
46
47func probeReturning(s CapStatus, calls *int) func() CapStatus {
48 return func() CapStatus {
49 *calls++
50 return s
51 }
52}
53
54func isNativeForTest(g *nativeGate, host string, probe func() CapStatus) bool {
55 return g.status(host, probe) == CapPresent
56}
57
58func TestNativeGateMemoSkipsSecondProbe(t *testing.T) {
59 g := &nativeGate{}
60 calls := 0
61 probe := probeReturning(CapPresent, &calls)
62
63 if !isNativeForTest(g, "clam.nel.pet", probe) {
64 t.Fatal("first probe true: want native")
65 }
66 if !isNativeForTest(g, "clam.nel.pet", probe) {
67 t.Fatal("memoized: want native")
68 }
69 if calls != 1 {
70 t.Fatalf("calls = %d, want 1; a memoized native host must skip the probe", calls)
71 }
72}
73
74func TestNativeGateLatchHitSkipsProbe(t *testing.T) {
75 g := &nativeGate{}
76 fl := newFakeLatch()
77 fl.native["whelk.nel.pet"] = true
78 g.use(fl)
79
80 calls := 0
81 if !isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) {
82 t.Fatal("latched native: want native even though the probe would fail")
83 }
84 if calls != 0 {
85 t.Fatalf("calls = %d, want 0; a latched host must never be probed", calls)
86 }
87 if fl.markCount() != 0 {
88 t.Fatalf("marks = %d, want 0; a latch hit must not re-mark", fl.markCount())
89 }
90}
91
92func TestNativeGateProbeMarksLatchOnce(t *testing.T) {
93 g := &nativeGate{}
94 fl := newFakeLatch()
95 g.use(fl)
96
97 calls := 0
98 probe := probeReturning(CapPresent, &calls)
99 if !isNativeForTest(g, "limpet.nel.pet", probe) {
100 t.Fatal("probe true: want native")
101 }
102 if fl.markCount() != 1 {
103 t.Fatalf("marks = %d, want 1; a first successful probe must latch the host", fl.markCount())
104 }
105
106 isNativeForTest(g, "limpet.nel.pet", probe)
107 if fl.markCount() != 1 {
108 t.Fatalf("marks = %d, want 1; the memo must prevent a second mark", fl.markCount())
109 }
110}
111
112func TestNativeGateProbeFalseDoesNotMark(t *testing.T) {
113 g := &nativeGate{}
114 fl := newFakeLatch()
115 g.use(fl)
116
117 calls := 0
118 if isNativeForTest(g, "clam.nel.pet", probeReturning(CapAbsent, &calls)) {
119 t.Fatal("probe false on a fresh host: want not native")
120 }
121 if fl.markCount() != 0 {
122 t.Fatalf("marks = %d, want 0; a failed probe must not latch", fl.markCount())
123 }
124}
125
126func TestNativeGateDurableAcrossMemoReset(t *testing.T) {
127 fl := newFakeLatch()
128
129 warm := &nativeGate{}
130 warm.use(fl)
131 calls := 0
132 if !isNativeForTest(warm, "whelk.nel.pet", probeReturning(CapPresent, &calls)) {
133 t.Fatal("warm gate probe true: want native")
134 }
135
136 restarted := &nativeGate{}
137 restarted.use(fl)
138 cold := 0
139 if !isNativeForTest(restarted, "whelk.nel.pet", probeReturning(CapAbsent, &cold)) {
140 t.Fatal("after restart the durable latch must resolve native without a probe")
141 }
142 if cold != 0 {
143 t.Fatalf("calls = %d, want 0; a durably latched host survives a memo reset without probing", cold)
144 }
145}
146
147func TestNativeGateNegativeMemoThrottlesLatchReads(t *testing.T) {
148 now := time.Now()
149 g := &nativeGate{now: func() time.Time { return now }}
150 fl := newFakeLatch()
151 g.use(fl)
152
153 calls := 0
154 probe := probeReturning(CapAbsent, &calls)
155
156 for range 5 {
157 if isNativeForTest(g, "clam.nel.pet", probe) {
158 t.Fatal("a probe-false host must not be native")
159 }
160 }
161 if fl.readCount() != 1 {
162 t.Fatalf("latch reads = %d, want 1; the negative memo must throttle repeat latch reads within the window", fl.readCount())
163 }
164 if calls != 1 {
165 t.Fatalf("probe calls = %d, want 1; the negative memo must throttle repeat probes within the window", calls)
166 }
167
168 now = now.Add(versionProbeFresh + time.Second)
169 if isNativeForTest(g, "clam.nel.pet", probe) {
170 t.Fatal("still not native after the window")
171 }
172 if fl.readCount() != 2 {
173 t.Fatalf("latch reads = %d, want 2; an expired negative memo must re-read the latch", fl.readCount())
174 }
175 if calls != 2 {
176 t.Fatalf("probe calls = %d, want 2; an expired negative memo must re-probe", calls)
177 }
178}
179
180func TestNativeGateNegativeMemoNeverShadowsLatchedNative(t *testing.T) {
181 now := time.Now()
182 g := &nativeGate{now: func() time.Time { return now }}
183 fl := newFakeLatch()
184 g.use(fl)
185
186 calls := 0
187 if isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) {
188 t.Fatal("probe false on a fresh host: want not native")
189 }
190
191 fl.mu.Lock()
192 fl.native["whelk.nel.pet"] = true
193 fl.mu.Unlock()
194
195 now = now.Add(versionProbeFresh + time.Second)
196 if !isNativeForTest(g, "whelk.nel.pet", probeReturning(CapAbsent, &calls)) {
197 t.Fatal("once the negative memo expires a latched host must resolve native again")
198 }
199}
200
201func TestNativeGateUnknownDistinctFromAbsent(t *testing.T) {
202 now := time.Now()
203 g := &nativeGate{now: func() time.Time { return now }}
204 fl := newFakeLatch()
205 g.use(fl)
206
207 calls := 0
208 probe := probeReturning(CapUnknown, &calls)
209
210 for range 3 {
211 if got := g.status("clam.nel.pet", probe); got != CapUnknown {
212 t.Fatalf("status = %v, want CapUnknown for a failed probe", got)
213 }
214 }
215 if calls != 1 {
216 t.Fatalf("probe calls = %d, want 1; a memoized unknown must throttle re-probes within the window", calls)
217 }
218 if fl.markCount() != 0 {
219 t.Fatalf("marks = %d, want 0; an unknown probe must never latch", fl.markCount())
220 }
221
222 now = now.Add(versionProbeFresh + time.Second)
223 if got := g.status("clam.nel.pet", probeReturning(CapAbsent, &calls)); got != CapAbsent {
224 t.Fatalf("status = %v, want CapAbsent once a fresh probe reaches the knot", got)
225 }
226}
227
228func TestAtLeast(t *testing.T) {
229 cases := []struct {
230 in string
231 minMajor int
232 minMinor int
233 want bool
234 }{
235 {"v1.14.0", 1, 14, true},
236 {"v1.14.0-alpha", 1, 14, true},
237 {"v1.14.5", 1, 14, true},
238 {"v1.13.0", 1, 14, false},
239 {"v1.13.0-alpha", 1, 14, false},
240 {"v1.0.0", 1, 14, false},
241 {"v2.0.0", 1, 14, true},
242 {"1.14.0", 1, 14, true},
243 {"1.13.99", 1, 14, false},
244 {"(devel)", 1, 14, true},
245 {"", 1, 14, false},
246 {"garbagio-furioso", 1, 14, false},
247 {"v1", 1, 14, false},
248 {"vX.Y.Z", 1, 14, false},
249 {"unknown", 1, 14, false},
250 {"unknown-abc1234", 1, 14, false},
251 {"unknown-abc1234-modified", 1, 14, false},
252 {"v1.15.0", 1, 15, true},
253 {"v1.15.2-alpha", 1, 15, true},
254 {"v1.16.0", 1, 15, true},
255 {"v1.14.1-alpha", 1, 15, false},
256 {"v1.14.9", 1, 15, false},
257 {"v2.0.0", 1, 15, true},
258 {"(devel)", 1, 15, true},
259 {"", 1, 15, false},
260 }
261 for _, c := range cases {
262 t.Run(c.in, func(t *testing.T) {
263 if got := atLeast(c.in, c.minMajor, c.minMinor); got != c.want {
264 t.Errorf("atLeast(%q, %d, %d) = %v, want %v", c.in, c.minMajor, c.minMinor, got, c.want)
265 }
266 })
267 }
268}
269
270func newProbeCache() *versionProbeCache {
271 return &versionProbeCache{entries: map[string]versionProbeEntry{}}
272}
273
274func TestVersionProbeCacheFreshSkipsProbe(t *testing.T) {
275 c := newProbeCache()
276 now := time.Unix(1_000_000, 0)
277 calls := 0
278 probe := func() (string, bool) { calls++; return "v1.15.0", true }
279
280 if !c.supports(now, "knot.nel.pet", 1, 15, false, probe) {
281 t.Fatal("cold probe: want supported")
282 }
283 if calls != 1 {
284 t.Fatalf("calls = %d, want 1 after cold probe", calls)
285 }
286 if !c.supports(now.Add(time.Minute), "knot.nel.pet", 1, 15, false, probe) {
287 t.Fatal("cached: want supported")
288 }
289 if calls != 1 {
290 t.Fatalf("calls = %d, want 1; a fresh cache entry must skip the probe", calls)
291 }
292}
293
294func TestVersionProbeCacheServesStaleOnFailure(t *testing.T) {
295 c := newProbeCache()
296 now := time.Unix(1_000_000, 0)
297 if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) {
298 t.Fatal("seed: want supported")
299 }
300
301 failProbe := func() (string, bool) { return "", false }
302 if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, failProbe) {
303 t.Error("a probe failure within the trust window must serve the last-known version, not fail closed")
304 }
305}
306
307func TestVersionProbeCacheFailsClosedWhenUntrusted(t *testing.T) {
308 c := newProbeCache()
309 now := time.Unix(1_000_000, 0)
310 failProbe := func() (string, bool) { return "", false }
311 if c.supports(now, "knot.nel.pet", 1, 15, false, failProbe) {
312 t.Error("a cold probe failure on a fail-closed gate must return false")
313 }
314 if !c.supports(now, "knot.nel.pet", 1, 14, true, failProbe) {
315 t.Error("a cold probe failure on a fail-open gate must return true")
316 }
317}
318
319func TestVersionProbeCacheExpiresTrust(t *testing.T) {
320 c := newProbeCache()
321 now := time.Unix(1_000_000, 0)
322 if !c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) {
323 t.Fatal("seed: want supported")
324 }
325 if c.supports(now.Add(2*time.Hour), "knot.nel.pet", 1, 15, false, func() (string, bool) { return "", false }) {
326 t.Error("a probe failure past the trust window must fail closed")
327 }
328}
329
330func TestVersionProbeCacheRefreshesAfterFresh(t *testing.T) {
331 c := newProbeCache()
332 now := time.Unix(1_000_000, 0)
333 if c.supports(now, "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.14.0", true }) {
334 t.Fatal("seed: 1.14 must not satisfy 1.15")
335 }
336 if !c.supports(now.Add(10*time.Minute), "knot.nel.pet", 1, 15, false, func() (string, bool) { return "v1.15.0", true }) {
337 t.Error("a re-probe past the fresh window must pick up the upgraded version")
338 }
339}
340
341func TestVersionProbeCacheEnforcesHardCap(t *testing.T) {
342 c := newProbeCache()
343 now := time.Unix(1_000_000, 0)
344 probe := func() (string, bool) { return "v1.15.0", true }
345 for i := 0; i < versionProbeCacheMax+200; i++ {
346 c.supports(now, "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe)
347 }
348 if len(c.entries) > versionProbeCacheMax {
349 t.Fatalf("entries = %d, must never exceed cap %d even when every entry is fresh", len(c.entries), versionProbeCacheMax)
350 }
351}
352
353func TestVersionProbeCacheEvictsOldestWhenFull(t *testing.T) {
354 c := newProbeCache()
355 base := time.Unix(1_000_000, 0)
356 probe := func() (string, bool) { return "v1.15.0", true }
357 for i := 0; i < versionProbeCacheMax; i++ {
358 c.supports(base.Add(time.Duration(i)*time.Millisecond), "knot"+strconv.Itoa(i)+".nel.pet", 1, 15, false, probe)
359 }
360 c.supports(base.Add(versionProbeCacheMax*time.Millisecond), "newcomer.nel.pet", 1, 15, false, probe)
361
362 if len(c.entries) != versionProbeCacheMax {
363 t.Fatalf("entries = %d, want exactly cap %d after evicting one for the newcomer", len(c.entries), versionProbeCacheMax)
364 }
365 if _, ok := c.get("knot0.nel.pet"); ok {
366 t.Error("oldest entry must be evicted to make room")
367 }
368 if _, ok := c.get("newcomer.nel.pet"); !ok {
369 t.Error("newcomer must be retained")
370 }
371}