v0.4.0 b6be3a8

security_hotplug_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"reflect"
6	"testing"
7	"time"
8
9	"github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"
10	"github.com/bnema/neferwl/internal/ports"
11)
12
13func hotplugEvent(t *testing.T, events <-chan ports.SecurityBackendEvent) ports.SecurityBackendEvent {
14	t.Helper()
15	select {
16	case event := <-events:
17		return event
18	case <-time.After(2 * time.Second):
19		t.Fatal("missing ordered security event")
20		return nil
21	}
22}
23
24// This switch represents the single display owner's ledger consumption, not
25// an alternate backend implementation. Events come through real outputSet FIFO.
26func hotplugApply(ledger *sessionlock.Readiness, event ports.SecurityBackendEvent) bool {
27	switch v := event.(type) {
28	case ports.SecurityOutputAdded:
29		return ledger.Add(v.Instance)
30	case ports.SecurityOutputRemoved:
31		return ledger.Remove(v.Instance)
32	case ports.SecurityOutputInvalidated:
33		return ledger.Invalidate(v.Generation, v.Instance)
34	case ports.SecurityOutputProof:
35		return ledger.Record(v.Proof)
36	case ports.SecurityBackendBarrier:
37		return v.Err == nil && ledger.BackendBarrier(v.Generation)
38	default:
39		return false
40	}
41}
42
43func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) {
44	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
45	defer cancel()
46	events := make(chan ports.SecurityBackendEvent, 8)
47	set := newOutputSet(ctx, nil)
48	set.wireSecurity(outputChannels{securityEvents: events})
49	defer func() {
50		if err := set.wait(); err != nil {
51			t.Error(err)
52		}
53	}()
54	const generation = ports.LockGeneration(41)
55	set.setSecurity(ports.SecurityState{Generation: generation, Protected: true})
56	var ledger sessionlock.Readiness
57	if !ledger.Begin(generation) || !ledger.BackendBarrier(generation) || !ledger.CaptureBarrier(generation) {
58		t.Fatal("could not begin display readiness round")
59	}
60	// Native Close success is independently tested in DRM. Here the real
61	// owner callback synchronizes that affirmative result before the backend
62	// sends removal; mere callback exit never emits removal from outputSet.
63	start := func(closeSuccess bool) (ports.OutputInstance, chan []ports.SecurityBackendEvent, <-chan bool) {
64		t.Helper()
65		commands := make(chan []ports.SecurityBackendEvent, 1)
66		closed := make(chan bool, 1)
67		var ownerEvents chan<- ports.SecurityBackendEvent
68		if !set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, security <-chan ports.SecurityState, instance ports.OutputInstance) error {
69			defer func() { closed <- closeSuccess }()
70			state := <-security
71			proof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: state.Generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}}
72			select {
73			case ownerEvents <- proof:
74			case <-octx.Done():
75				return nil
76			}
77			for {
78				select {
79				case batch := <-commands:
80					for _, event := range batch {
81						select {
82						case ownerEvents <- event:
83						case <-octx.Done():
84							return nil
85						}
86					}
87				case <-octx.Done():
88					return nil
89				}
90			}
91		}, &ownerEvents) {
92			t.Fatal("start refused")
93		}
94		registered := hotplugEvent(t, events)
95		added, ok := registered.(ports.SecurityOutputAdded)
96		if !ok || added.Output != "DP-1" || added.Instance == 0 || !hotplugApply(&ledger, registered) {
97			t.Fatalf("owner evidence preceded lifetime registration: %+v", registered)
98		}
99		if ledger.Ready() {
100			t.Fatal("new lifetime reused previous physical readiness")
101		}
102		proof := hotplugEvent(t, events)
103		if !hotplugApply(&ledger, proof) || !ledger.Ready() {
104			t.Fatalf("missing exact lifetime proof: %+v", proof)
105		}
106		return added.Instance, commands, closed
107	}
108	old, commands, closed := start(true)
109	// A proof already queued by this owner precedes invalidation, even if the
110	// display reads later. No separate forwarder can replay it after revoke.
111	oldProof := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: generation, Instance: old, Kind: ports.ProtectionProtectedFrame}}
112	invalid := ports.SecurityOutputInvalidated{Generation: generation, Instance: old}
113	commands <- []ports.SecurityBackendEvent{oldProof, invalid}
114	if got := hotplugEvent(t, events); got != oldProof || !hotplugApply(&ledger, got) {
115		t.Fatalf("queued proof lost: %+v", got)
116	}
117	if got := hotplugEvent(t, events); got != invalid || !hotplugApply(&ledger, got) || ledger.Ready() {
118		t.Fatalf("invalidation reordered or ineffective: %+v", got)
119	}
120	commands <- []ports.SecurityBackendEvent{oldProof}
121	if got := hotplugEvent(t, events); got != oldProof || !hotplugApply(&ledger, got) || !ledger.Ready() {
122		t.Fatal("fresh resume proof did not restore readiness")
123	}
124	set.outs["DP-1"].stop()
125	select {
126	case stopped := <-set.stopped:
127		if stopped.instance != old || !set.stoppedCurrent(stopped) {
128			t.Fatalf("wrong stopped lifetime: %+v", stopped)
129		}
130	case <-ctx.Done():
131		t.Fatal("owner did not stop")
132	}
133	if err := set.finish("DP-1"); err != nil {
134		t.Fatal(err)
135	}
136	if !<-closed {
137		t.Fatal("shutdown not affirmative")
138	}
139	select {
140	case got := <-events:
141		t.Fatalf("owner exit forged removal: %+v", got)
142	default:
143	}
144	if !set.securityEvent(ports.SecurityOutputRemoved{Instance: old}) {
145		t.Fatal("confirmed removal refused")
146	}
147	removal := hotplugEvent(t, events)
148	if removal != (ports.SecurityOutputRemoved{Instance: old}) || !hotplugApply(&ledger, removal) {
149		t.Fatalf("wrong removal: %+v", removal)
150	}
151	replacement, _, replacementClosed := start(false)
152	if replacement <= old || set.stoppedCurrent(outputStopped{name: "DP-1", instance: old}) {
153		t.Fatal("same-name replacement reused retired lifetime")
154	}
155	if hotplugApply(&ledger, oldProof) || hotplugApply(&ledger, invalid) {
156		t.Fatal("retired lifetime affected replacement")
157	}
158	stale := ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: generation - 1, Instance: replacement, Kind: ports.ProtectionInactiveOutput}}
159	if hotplugApply(&ledger, stale) || !ledger.Ready() {
160		t.Fatal("stale generation overwrote valid replacement proof")
161	}
162	if !ledger.Invalidate(generation, replacement) || ledger.Ready() {
163		t.Fatal("replacement invalidation ineffective")
164	}
165	set.outs["DP-1"].stop()
166	select {
167	case <-set.stopped:
168	case <-ctx.Done():
169		t.Fatal("replacement stop blocked")
170	}
171	if err := set.finish("DP-1"); err != nil {
172		t.Fatal(err)
173	}
174	if <-replacementClosed {
175		t.Fatal("expected failed shutdown")
176	}
177	select {
178	case got := <-events:
179		t.Fatalf("failed shutdown forged removal: %+v", got)
180	default:
181	}
182	if ledger.Ready() {
183		t.Fatal("failed replacement exit erased required unproved lifetime")
184	}
185}
186
187func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
188	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
189	defer cancel()
190	events := make(chan ports.SecurityBackendEvent) // block display after registration
191	set := newOutputSet(ctx, nil)
192	set.wireSecurity(outputChannels{securityEvents: events})
193	defer func() {
194		if err := set.wait(); err != nil {
195			t.Error(err)
196		}
197	}()
198	const generation = ports.LockGeneration(7)
199	accepted := make(chan int, 40)
200	startDone := make(chan bool, 1)
201	var ownerEvents chan<- ports.SecurityBackendEvent
202	go func() {
203		startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error {
204			for i := range 40 {
205				var event ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: instance}
206				if i%2 == 0 {
207					event = ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: generation, Instance: instance, Kind: ports.ProtectionProtectedFrame}}
208				}
209				select {
210				case ownerEvents <- event:
211					accepted <- i
212				case <-octx.Done():
213					return nil
214				}
215			}
216			return nil
217		}, &ownerEvents)
218	}()
219	registered := hotplugEvent(t, events).(ports.SecurityOutputAdded)
220	if !<-startDone {
221		t.Fatal("start failed")
222	}
223	if cap(set.outs["DP-1"].securityEvents) != 16 {
224		t.Fatal("owner evidence FIFO is not bounded to 16")
225	}
226	// Forwarder holds one event plus its 16-slot input. The eighteenth send
227	// must remain blocked until the display starts draining.
228	for i := range 17 {
229		select {
230		case got := <-accepted:
231			if got != i {
232				t.Fatalf("acceptance order %d want %d", got, i)
233			}
234		case <-ctx.Done():
235			t.Fatal("FIFO failed to fill")
236		}
237	}
238	select {
239	case got := <-accepted:
240		t.Fatalf("full FIFO did not backpressure owner: %d", got)
241	case <-time.After(20 * time.Millisecond):
242	}
243	var ledger sessionlock.Readiness
244	ledger.Add(registered.Instance)
245	ledger.Begin(generation)
246	ledger.BackendBarrier(generation)
247	ledger.CaptureBarrier(generation)
248	for i := range 40 {
249		got := hotplugEvent(t, events)
250		var want ports.SecurityBackendEvent = ports.SecurityOutputInvalidated{Generation: generation, Instance: registered.Instance}
251		if i%2 == 0 {
252			want = ports.SecurityOutputProof{Proof: ports.OutputProtection{Generation: generation, Instance: registered.Instance, Kind: ports.ProtectionProtectedFrame}}
253		}
254		if !reflect.DeepEqual(got, want) || !hotplugApply(&ledger, got) || ledger.Ready() != (i%2 == 0) {
255			t.Fatalf("evidence lost/duplicated/reordered at %d: %+v", i, got)
256		}
257	}
258	select {
259	case stopped := <-set.stopped:
260		if !set.stoppedCurrent(stopped) {
261			t.Fatal("stop not current")
262		}
263	case <-ctx.Done():
264		t.Fatal("normal owner stop blocked")
265	}
266	if err := set.finish("DP-1"); err != nil {
267		t.Fatal(err)
268	}
269	select {
270	case got := <-events:
271		t.Fatalf("duplicate evidence or removal after normal drain: %+v", got)
272	default:
273	}
274}
275
276// All producer evidence is already buffered when the owner stops normally.
277// finish must not cancel that owner's forwarder or lose its outstanding FIFO.
278func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) {
279	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
280	defer cancel()
281	events := make(chan ports.SecurityBackendEvent)
282	set := newOutputSet(ctx, nil)
283	set.wireSecurity(outputChannels{securityEvents: events})
284	defer func() {
285		if err := set.wait(); err != nil {
286			t.Error(err)
287		}
288	}()
289	startDone := make(chan bool, 1)
290	var ownerEvents chan<- ports.SecurityBackendEvent
291	go func() {
292		startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error {
293			for i := range 16 {
294				event := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: instance}
295				select {
296				case ownerEvents <- event:
297				case <-octx.Done():
298					return nil
299				}
300			}
301			return nil
302		}, &ownerEvents)
303	}()
304	registered := hotplugEvent(t, events).(ports.SecurityOutputAdded)
305	if !<-startDone {
306		t.Fatal("start refused")
307	}
308	select {
309	case stopped := <-set.stopped:
310		if stopped.instance != registered.Instance {
311			t.Fatal("wrong stopped lifetime")
312		}
313	case <-ctx.Done():
314		t.Fatal("buffered owner did not finish")
315	}
316	if err := set.finish("DP-1"); err != nil {
317		t.Fatal(err)
318	}
319	for i := range 16 {
320		want := ports.SecurityOutputInvalidated{Generation: ports.LockGeneration(i + 1), Instance: registered.Instance}
321		if got := hotplugEvent(t, events); got != want {
322			t.Fatalf("buffered stopped-owner evidence %d: %+v", i, got)
323		}
324	}
325	select {
326	case got := <-events:
327		t.Fatalf("duplicate buffered evidence: %+v", got)
328	default:
329	}
330}
331
332func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
333	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
334	defer cancel()
335	events := make(chan ports.SecurityBackendEvent)
336	set := newOutputSet(ctx, nil)
337	set.wireSecurity(outputChannels{securityEvents: events})
338	accepted := make(chan int, 40)
339	ownerDone := make(chan struct{})
340	startDone := make(chan bool, 1)
341	var ownerEvents chan<- ports.SecurityBackendEvent
342	go func() {
343		startDone <- set.start(ctx, "DP-1", func(octx context.Context, _ <-chan ports.Scene, _ <-chan ports.SurfaceContent, _ <-chan ports.CursorChange, _ <-chan ports.CaptureRequest, _ <-chan ports.SecurityState, instance ports.OutputInstance) error {
344			defer close(ownerDone)
345			for i := range 40 {
346				select {
347				case ownerEvents <- ports.SecurityOutputInvalidated{Generation: 7, Instance: instance}:
348					accepted <- i
349				case <-octx.Done():
350					return nil
351				}
352			}
353			return nil
354		}, &ownerEvents)
355	}()
356	hotplugEvent(t, events) // registered before producer starts
357	if !<-startDone {
358		t.Fatal("start failed")
359	}
360	for range 17 {
361		select {
362		case <-accepted:
363		case <-ctx.Done():
364			t.Fatal("FIFO never filled")
365		}
366	}
367	select {
368	case <-accepted:
369		t.Fatal("producer did not block at finite FIFO")
370	case <-time.After(20 * time.Millisecond):
371	}
372	waitDone := make(chan error, 1)
373	go func() { waitDone <- set.wait() }()
374	select {
375	case err := <-waitDone:
376		if err != nil {
377			t.Fatal(err)
378		}
379	case <-ctx.Done():
380		t.Fatal("saturated evidence leaked owner/forwarder on cancellation")
381	}
382	select {
383	case <-ownerDone:
384	default:
385		t.Fatal("wait returned before producer exited")
386	}
387	if len(set.outs) != 0 {
388		t.Fatal("stopped lifetime retained in running set")
389	}
390	// wait includes forwarders.Wait: termination is directly synchronized,
391	// rather than guessed from goroutine counts or arbitrary sleeps.
392}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close