1package sessionlock
2
3import (
4 "fmt"
5 "testing"
6
7 "github.com/bnema/neferwl/internal/ports"
8)
9
10func lockProof(generation ports.LockGeneration, instance ports.OutputInstance) ports.OutputProtection {
11 return ports.OutputProtection{Generation: generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}
12}
13
14func requireLockAccepted(t *testing.T, accepted bool) {
15 t.Helper()
16 if !accepted {
17 t.Fatal("valid operation rejected")
18 }
19}
20
21func requireLockReady(t *testing.T, r *Readiness, want bool) {
22 t.Helper()
23 if got := r.Ready(); got != want {
24 t.Fatalf("Ready() = %v, want %v; ledger %+v", got, want, r)
25 }
26}
27
28func TestLockReadinessZeroValue(t *testing.T) {
29 var r Readiness
30 requireLockReady(t, &r, false)
31 if r.Begin(0) || r.End(0) || r.Add(0) || r.Remove(0) || r.Remove(99) ||
32 r.Record(ports.OutputProtection{}) || r.Record(lockProof(1, 1)) ||
33 r.BackendBarrier(0) || r.BackendBarrier(1) || r.CaptureBarrier(0) || r.CaptureBarrier(1) {
34 t.Fatal("zero/disengaged ledger accepted invalid input")
35 }
36 requireLockReady(t, &r, false)
37}
38
39func TestLockReadinessEmptyInventoryNeedsBothBarriers(t *testing.T) {
40 for _, backendFirst := range []bool{false, true} {
41 t.Run(fmt.Sprintf("backend-first=%v", backendFirst), func(t *testing.T) {
42 var r Readiness
43 requireLockAccepted(t, r.Begin(7))
44 requireLockReady(t, &r, false)
45 if backendFirst {
46 requireLockAccepted(t, r.BackendBarrier(7))
47 } else {
48 requireLockAccepted(t, r.CaptureBarrier(7))
49 }
50 requireLockReady(t, &r, false)
51 if backendFirst {
52 requireLockAccepted(t, r.CaptureBarrier(7))
53 } else {
54 requireLockAccepted(t, r.BackendBarrier(7))
55 }
56 requireLockReady(t, &r, true)
57 })
58 }
59}
60
61func TestLockReadinessEveryPrerequisiteInAnyOrder(t *testing.T) {
62 // Two output proofs and two independent barriers: all 24 arrival orders.
63 for a := range 4 {
64 for b := range 4 {
65 for c := range 4 {
66 for d := range 4 {
67 order := [4]int{a, b, c, d}
68 if a == b || a == c || a == d || b == c || b == d || c == d {
69 continue
70 }
71 t.Run(fmt.Sprint(order), func(t *testing.T) {
72 var r Readiness
73 requireLockAccepted(t, r.Add(11))
74 requireLockAccepted(t, r.Add(12))
75 requireLockAccepted(t, r.Begin(5))
76 requireLockReady(t, &r, false)
77 for i, step := range order {
78 var accepted bool
79 switch step {
80 case 0:
81 accepted = r.Record(lockProof(5, 11))
82 case 1:
83 accepted = r.Record(ports.OutputProtection{Generation: 5, Instance: 12, Kind: ports.ProtectionInactiveOutput})
84 case 2:
85 accepted = r.BackendBarrier(5)
86 case 3:
87 accepted = r.CaptureBarrier(5)
88 }
89 requireLockAccepted(t, accepted)
90 requireLockReady(t, &r, i == 3)
91 }
92 // Duplicate evidence is idempotent.
93 requireLockAccepted(t, r.Record(lockProof(5, 11)))
94 requireLockAccepted(t, r.BackendBarrier(5))
95 requireLockAccepted(t, r.CaptureBarrier(5))
96 requireLockReady(t, &r, true)
97 })
98 }
99 }
100 }
101 }
102}
103
104func TestLockReadinessGenerationFencing(t *testing.T) {
105 var r Readiness
106 requireLockAccepted(t, r.Add(1))
107 requireLockAccepted(t, r.Begin(10))
108 requireLockAccepted(t, r.Record(lockProof(10, 1)))
109 requireLockAccepted(t, r.BackendBarrier(10))
110 requireLockAccepted(t, r.CaptureBarrier(10))
111 requireLockReady(t, &r, true)
112 for _, generation := range []ports.LockGeneration{0, 9, 10} {
113 if r.Begin(generation) {
114 t.Fatalf("accepted non-increasing generation %d", generation)
115 }
116 requireLockReady(t, &r, true)
117 }
118
119 // A newer Begin while engaged clears both barriers and every proof.
120 requireLockAccepted(t, r.Begin(11))
121 requireLockReady(t, &r, false)
122 for _, generation := range []ports.LockGeneration{0, 9, 10, 12} {
123 if r.Record(lockProof(generation, 1)) || r.BackendBarrier(generation) || r.CaptureBarrier(generation) || r.End(generation) {
124 t.Fatalf("accepted wrong-generation evidence/End for %d", generation)
125 }
126 }
127 requireLockAccepted(t, r.BackendBarrier(11))
128 requireLockAccepted(t, r.CaptureBarrier(11))
129 requireLockReady(t, &r, false) // old proof was cleared
130 requireLockAccepted(t, r.Record(lockProof(11, 1)))
131 requireLockReady(t, &r, true)
132 requireLockAccepted(t, r.End(11))
133 requireLockReady(t, &r, false)
134 if r.Record(lockProof(11, 1)) || r.BackendBarrier(11) || r.CaptureBarrier(11) || r.End(11) || r.Begin(11) || r.Begin(10) {
135 t.Fatal("ended round accepted late evidence or reused generation")
136 }
137
138 requireLockAccepted(t, r.Begin(13)) // intervening gate release may consume 12
139 if r.End(11) || r.Record(lockProof(11, 1)) || r.BackendBarrier(11) || r.CaptureBarrier(11) {
140 t.Fatal("late old round affected newer round")
141 }
142 requireLockAccepted(t, r.Record(lockProof(13, 1)))
143 requireLockReady(t, &r, false) // neither old barrier survived End/Begin
144 requireLockAccepted(t, r.BackendBarrier(13))
145 requireLockReady(t, &r, false)
146 requireLockAccepted(t, r.CaptureBarrier(13))
147 requireLockReady(t, &r, true)
148}
149
150func TestLockReadinessGenerationExhaustionDoesNotWrap(t *testing.T) {
151 var r Readiness
152 maxGeneration := ports.LockGeneration(^uint64(0))
153 requireLockAccepted(t, r.Begin(maxGeneration))
154 requireLockAccepted(t, r.End(maxGeneration))
155 for _, generation := range []ports.LockGeneration{0, 1, maxGeneration} {
156 if r.Begin(generation) {
157 t.Fatalf("accepted wrapped/reused generation %d", generation)
158 }
159 }
160 requireLockReady(t, &r, false)
161}
162
163func TestLockReadinessInvalidProofsDoNotCountOrEraseEvidence(t *testing.T) {
164 invalid := []ports.OutputProtection{
165 {},
166 {Generation: 1, Instance: 1},
167 {Generation: 1, Instance: 1, Kind: "submitted-frame"},
168 {Generation: 1, Instance: 1, Kind: "error-gone"},
169 {Generation: 1, Instance: 0, Kind: ports.ProtectionProtectedFrame},
170 {Generation: 1, Instance: 99, Kind: ports.ProtectionProtectedFrame},
171 {Generation: 0, Instance: 1, Kind: ports.ProtectionProtectedFrame},
172 {Generation: 2, Instance: 1, Kind: ports.ProtectionProtectedFrame},
173 }
174 var r Readiness
175 requireLockAccepted(t, r.Add(1))
176 requireLockAccepted(t, r.Begin(1))
177 requireLockAccepted(t, r.BackendBarrier(1))
178 requireLockAccepted(t, r.CaptureBarrier(1))
179 for _, proof := range invalid {
180 if r.Record(proof) {
181 t.Fatalf("accepted invalid proof %+v", proof)
182 }
183 requireLockReady(t, &r, false)
184 }
185 requireLockAccepted(t, r.Record(lockProof(1, 1)))
186 for _, proof := range invalid {
187 if r.Record(proof) {
188 t.Fatalf("accepted invalid proof %+v", proof)
189 }
190 requireLockReady(t, &r, true)
191 }
192}
193
194func TestLockReadinessOutputInventoryChanges(t *testing.T) {
195 var r Readiness
196 requireLockAccepted(t, r.Add(1))
197 requireLockAccepted(t, r.Add(2))
198 requireLockAccepted(t, r.Remove(2)) // registration and retirement before Begin
199 if r.Add(2) || r.Remove(2) || r.Add(1) {
200 t.Fatal("accepted duplicate or retired lifetime")
201 }
202 requireLockAccepted(t, r.Begin(1))
203 requireLockAccepted(t, r.Add(3)) // new output before Ready
204 requireLockAccepted(t, r.Record(lockProof(1, 1)))
205 requireLockAccepted(t, r.BackendBarrier(1))
206 requireLockAccepted(t, r.CaptureBarrier(1))
207 requireLockReady(t, &r, false)
208 requireLockAccepted(t, r.Record(lockProof(1, 3)))
209 requireLockReady(t, &r, true)
210 if r.Add(0) || r.Add(3) || r.Remove(99) {
211 t.Fatal("accepted invalid inventory operation")
212 }
213 requireLockReady(t, &r, true) // duplicate registration did not erase proof
214 requireLockAccepted(t, r.Add(4)) // hotplug after Ready must revoke it
215 requireLockReady(t, &r, false)
216 requireLockAccepted(t, r.Remove(4)) // caller-confirmed retired lifetime
217 requireLockReady(t, &r, true)
218
219 // The same output name can return, but the new lifetime is instance 5.
220 requireLockAccepted(t, r.Remove(3))
221 requireLockAccepted(t, r.Add(5))
222 if r.Record(lockProof(1, 3)) || r.Add(3) {
223 t.Fatal("replacement accepted old-lifetime evidence")
224 }
225 requireLockReady(t, &r, false)
226 requireLockAccepted(t, r.Record(lockProof(1, 5)))
227 requireLockReady(t, &r, true)
228
229 requireLockAccepted(t, r.End(1))
230 requireLockAccepted(t, r.Add(6)) // changes after End join next snapshot
231 requireLockAccepted(t, r.Remove(1))
232 requireLockAccepted(t, r.Begin(2))
233 requireLockAccepted(t, r.BackendBarrier(2))
234 requireLockAccepted(t, r.CaptureBarrier(2))
235 requireLockAccepted(t, r.Record(lockProof(2, 5)))
236 requireLockReady(t, &r, false)
237 requireLockAccepted(t, r.Record(lockProof(2, 6)))
238 requireLockReady(t, &r, true)
239 if r.Add(1) || r.Add(2) || r.Add(3) || r.Add(4) {
240 t.Fatal("retired lifetime reused across lock rounds")
241 }
242}
243
244func TestLockReadinessInvalidationRequiresFreshProof(t *testing.T) {
245 var r Readiness
246 requireLockAccepted(t, r.Add(1))
247 requireLockAccepted(t, r.Begin(7))
248 requireLockAccepted(t, r.Record(lockProof(7, 1)))
249 requireLockAccepted(t, r.BackendBarrier(7))
250 requireLockAccepted(t, r.CaptureBarrier(7))
251 requireLockReady(t, &r, true)
252 if r.Invalidate(6, 1) || r.Invalidate(7, 2) {
253 t.Fatal("invalid invalidation accepted")
254 }
255 requireLockReady(t, &r, true)
256 requireLockAccepted(t, r.Invalidate(7, 1))
257 requireLockReady(t, &r, false)
258 requireLockAccepted(t, r.Record(lockProof(7, 1)))
259 requireLockReady(t, &r, true)
260 requireLockAccepted(t, r.End(7))
261 if r.Invalidate(7, 1) {
262 t.Fatal("ended invalidation accepted")
263 }
264}
265
266func TestLockReadinessInvalidationIsolatedAndRetired(t *testing.T) {
267 var r Readiness
268 requireLockAccepted(t, r.Add(1))
269 requireLockAccepted(t, r.Add(2))
270 requireLockAccepted(t, r.Add(3))
271 requireLockAccepted(t, r.Remove(3))
272 requireLockAccepted(t, r.Begin(7))
273 requireLockAccepted(t, r.Record(lockProof(7, 1)))
274 requireLockAccepted(t, r.Record(lockProof(7, 2)))
275 requireLockAccepted(t, r.BackendBarrier(7))
276 requireLockAccepted(t, r.CaptureBarrier(7))
277 if r.Invalidate(8, 1) || r.Invalidate(7, 3) {
278 t.Fatal("future or retired invalidation accepted")
279 }
280 requireLockReady(t, &r, true)
281 requireLockAccepted(t, r.Invalidate(7, 1))
282 requireLockReady(t, &r, false)
283 requireLockAccepted(t, r.Record(lockProof(7, 1)))
284 requireLockReady(t, &r, true) // output 2 and both barriers retained
285}
286
287func TestLockReadinessRemovingLastOutputCannotReplaceBackendACK(t *testing.T) {
288 var r Readiness
289 requireLockAccepted(t, r.Add(1))
290 requireLockAccepted(t, r.Begin(1))
291 requireLockAccepted(t, r.CaptureBarrier(1))
292 requireLockAccepted(t, r.Remove(1))
293 requireLockReady(t, &r, false)
294 requireLockAccepted(t, r.BackendBarrier(1))
295 requireLockReady(t, &r, true)
296}