Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

at icy/sntnrt 9.7 kB View raw
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}