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