v0.4.0 b6be3a8

protection_test.go RawHistoryFolder

1package drm
2
3import (
4	"context"
5	"errors"
6	"os"
7	"testing"
8	"time"
9
10	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
11	"github.com/bnema/neferwl/internal/ports"
12	"github.com/stretchr/testify/mock"
13	"golang.org/x/sys/unix"
14)
15
16func protectionFence(t *testing.T, signalled bool) (*os.File, *os.File) {
17	t.Helper()
18	f, w, err := os.Pipe()
19	if err != nil {
20		t.Fatal(err)
21	}
22	t.Cleanup(func() { f.Close(); w.Close() })
23	if signalled {
24		if _, err := w.Write([]byte{1}); err != nil {
25			t.Fatal(err)
26		}
27	}
28	return f, w
29}
30
31func checkProtectedDisable(t *testing.T, c commitRec, planes ...uint32) {
32	t.Helper()
33	if c.flags != atomicAllowModes || c.user != 0 {
34		t.Fatalf("not blocking modeset: flags=%#x user=%d", c.flags, c.user)
35	}
36	if active, ok := c.req.value(tCrtc, pActive); !ok || active != 0 {
37		t.Fatalf("CRTC still active: %d %v", active, ok)
38	}
39	// Full disable: no CRTC enabled without its primary plane (amdgpu rule).
40	if v, ok := c.req.value(tCrtc, pMode); !ok || v != 0 {
41		t.Fatalf("CRTC still enabled: MODE_ID %d %v", v, ok)
42	}
43	if v, ok := c.req.value(tConn, pConnCrtc); !ok || v != 0 {
44		t.Fatalf("connector still routed: CRTC_ID %d %v", v, ok)
45	}
46	for _, p := range planes {
47		for _, prop := range []uint32{pFB, pCrtcID} {
48			if v, ok := c.req.value(p, prop); !ok || v != 0 {
49				t.Fatalf("plane %d prop %d still attached: %d %v", p, prop, v, ok)
50			}
51		}
52	}
53}
54
55// Adapted from the parent's draft: detach, clear, wait, then activate only
56// the cleared target, with no nonblocking commit or desktop cursor test.
57func TestProtectedModesetClearsBeforeActivation(t *testing.T) {
58	o, k, commits := testOutput(t)
59	o.back = 1 // exercise the other target, not just startup's back=0
60	o.overlay = &plane{id: tOverlay, props: planeProps}
61	o.stray = []*plane{o.overlay, {id: 53, props: planeProps}, {id: 54, props: planeProps}}
62	k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil)
63	k.EXPECT().objProps(uint32(54), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, 99}}, nil)
64	r := portsmocks.NewMockRenderer(t)
65	r.EXPECT().UseTarget(1).Return().Once()
66	f, _ := protectionFence(t, true)
67	// Blob creation is part of activation and must occur after fence close.
68	k.EXPECT().createBlob(mock.Anything).RunAndReturn(func([]byte) (uint32, error) {
69		if _, err := f.Stat(); !errors.Is(err, os.ErrClosed) {
70			t.Fatalf("activation before clear fence close: %v", err)
71		}
72		return 99, nil
73	}).Once()
74	r.EXPECT().Render(ports.Scene{Seq: 0, Background: "#000000"}, map[ports.WindowID]ports.SurfaceContent(nil)).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
75		if len(*commits) != 1 {
76			t.Fatalf("clear before disable: %d commits", len(*commits))
77		}
78		checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor, tOverlay, 53)
79		if _, ok := (*commits)[0].req.value(54, pFB); ok {
80			t.Fatal("detached another output's plane")
81		}
82		return f, nil
83	}).Once()
84	if err := o.protectedModeset(context.Background(), r); err != nil {
85		t.Fatal(err)
86	}
87	if len(*commits) != 2 {
88		t.Fatalf("commits %d", len(*commits))
89	}
90	last := (*commits)[1]
91	if last.flags != atomicAllowModes || last.user != 0 {
92		t.Fatal("activation was not blocking")
93	}
94	if fb, _ := last.req.value(tPrimary, pFB); fb != 71 {
95		t.Fatalf("activated fb %d, want cleared 71", fb)
96	}
97	if active, _ := last.req.value(tCrtc, pActive); active != 1 {
98		t.Fatal("not activated")
99	}
100	if o.back != 0 || !o.protected || o.off {
101		t.Fatalf("back=%d protected=%v off=%v", o.back, o.protected, o.off)
102	}
103	if _, err := f.Stat(); !errors.Is(err, os.ErrClosed) {
104		t.Fatalf("fence not closed: %v", err)
105	}
106}
107
108func TestProtectedModesetFailureNeverActivates(t *testing.T) {
109	for _, stage := range []string{"disable", "render", "fence", "blob", "activate"} {
110		t.Run(stage, func(t *testing.T) {
111			boom := errors.New("refused")
112			var failures []error
113			if stage == "disable" {
114				failures = []error{unix.EACCES}
115			}
116			if stage == "activate" {
117				failures = []error{nil, unix.EPERM}
118			}
119			o, k, commits := testOutput(t, failures...)
120			r := portsmocks.NewMockRenderer(t)
121			var fence *os.File
122			if stage != "disable" {
123				r.EXPECT().UseTarget(0).Return().Once()
124				f, w := protectionFence(t, stage != "fence")
125				fence = f
126				if stage == "fence" {
127					w.Close() // HUP without POLLIN is not fence completion.
128				}
129				var renderErr error
130				if stage == "render" {
131					renderErr = boom
132				}
133				r.EXPECT().Render(mock.Anything, mock.Anything).Return(fence, renderErr).Once()
134			}
135			if stage == "activate" {
136				k.EXPECT().createBlob(mock.Anything).Return(99, nil).Once()
137				k.EXPECT().destroyBlob(uint32(99)).Return(nil).Once()
138			}
139			if stage == "blob" {
140				k.EXPECT().createBlob(mock.Anything).Return(0, boom).Once()
141			}
142			if err := o.protectedModeset(context.Background(), r); err == nil {
143				t.Fatal("failure reported success")
144			}
145			if !o.protected || o.back != 0 {
146				t.Fatal("failure dropped protection or flipped target")
147			}
148			if stage != "activate" && len(*commits) != 1 {
149				t.Fatalf("activated after %s failure", stage)
150			}
151			if stage != "disable" && !o.off {
152				t.Fatal("disabled output considered active")
153			}
154			if fence != nil {
155				if _, err := fence.Stat(); !errors.Is(err, os.ErrClosed) {
156					t.Fatalf("fence not closed: %v", err)
157				}
158			}
159		})
160	}
161}
162
163func TestProtectedResumeWithoutClearStaysInactive(t *testing.T) {
164	o, _, commits := testOutput(t)
165	o.protected = true
166	if err := o.modeset(); err != nil {
167		t.Fatal(err)
168	}
169	checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor)
170	if !o.off {
171		t.Fatal("resume exposed stale target")
172	}
173}
174
175func TestProtectedCloseNeverRestoresSavedDesktop(t *testing.T) {
176	o, k, commits := testOutput(t)
177	o.protected = true
178	o.saved = modeCrtc{crtcID: tCrtc, fbID: 123, modeValid: 1, mode: o.mode}
179	o.overlay = &plane{id: tOverlay, props: planeProps}
180	o.stray = []*plane{o.overlay, {id: 53, props: planeProps}, {id: 54, props: planeProps}}
181	k.EXPECT().objProps(uint32(53), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, tCrtc}}, nil).Once()
182	k.EXPECT().objProps(uint32(54), uint32(objPlane)).Return(map[string][2]uint64{"CRTC_ID": {pCrtcID, 99}}, nil).Once()
183	k.EXPECT().rmFB(mock.Anything).Return(nil)
184	o.Close()
185	if len(*commits) != 1 {
186		t.Fatalf("commits %d", len(*commits))
187	}
188	checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor, tOverlay, 53)
189	if _, ok := (*commits)[0].req.value(54, pFB); ok {
190		t.Fatal("close detached another output's plane")
191	}
192}
193
194func TestProtectedModesetPendingDoesNotWaitForClientFence(t *testing.T) {
195	o, _, commits := testOutput(t)
196	f, _ := protectionFence(t, false)
197	o.frame.begin(pendingFrame{fences: []*os.File{f}}, time.Now())
198	r := portsmocks.NewMockRenderer(t)
199	for _, call := range []func() error{
200		func() error { return o.protectedModeset(context.Background(), r) },
201		o.modeset,
202	} {
203		if err := call(); !errors.Is(err, errProtectionPending) {
204			t.Fatalf("pending result: %v", err)
205		}
206	}
207	if !o.protected || len(*commits) != 0 || !o.frame.pendingCommit() {
208		t.Fatal("pending protection touched KMS or dropped pending state")
209	}
210	if _, err := f.Stat(); err != nil {
211		t.Fatalf("client fence ownership dropped: %v", err)
212	}
213}
214
215func TestProtectedModesetCanceled(t *testing.T) {
216	for _, before := range []bool{true, false} {
217		t.Run(map[bool]string{true: "before-disable", false: "during-wait"}[before], func(t *testing.T) {
218			o, _, commits := testOutput(t)
219			r := portsmocks.NewMockRenderer(t)
220			ctx, cancel := context.WithCancel(context.Background())
221			defer cancel()
222			var fence *os.File
223			if before {
224				cancel()
225			} else {
226				fence, _ = protectionFence(t, false)
227				r.EXPECT().UseTarget(0).Return().Once()
228				r.EXPECT().Render(mock.Anything, mock.Anything).RunAndReturn(func(ports.Scene, map[ports.WindowID]ports.SurfaceContent) (*os.File, error) {
229					cancel()
230					return fence, nil
231				}).Once()
232			}
233			if err := o.protectedModeset(ctx, r); !errors.Is(err, context.Canceled) {
234				t.Fatalf("cancel result: %v", err)
235			}
236			want := 1
237			if before {
238				want = 0
239			}
240			if len(*commits) != want || !o.protected || o.back != 0 {
241				t.Fatal("cancel activated or dropped latch")
242			}
243			if fence != nil {
244				if !o.off {
245					t.Fatal("canceled clear left output marked active")
246				}
247				if _, err := fence.Stat(); !errors.Is(err, os.ErrClosed) {
248					t.Fatalf("cancel did not close fence: %v", err)
249				}
250			}
251		})
252	}
253}
254
255func TestProtectedModesetPreservesPowerOff(t *testing.T) {
256	o, _, commits := testOutput(t)
257	o.wantOff = true
258	r := portsmocks.NewMockRenderer(t)
259	r.EXPECT().UseTarget(0).Return().Once()
260	f, _ := protectionFence(t, true)
261	r.EXPECT().Render(ports.Scene{Seq: 0, Background: "#000000"}, map[ports.WindowID]ports.SurfaceContent(nil)).Return(f, nil).Once()
262	if err := o.protectedModeset(context.Background(), r); err != nil {
263		t.Fatal(err)
264	}
265	if len(*commits) != 1 || !o.wantOff || !o.off || !o.protected || o.back != 0 {
266		t.Fatal("off proof activated output, flipped target or changed power intent")
267	}
268	checkProtectedDisable(t, (*commits)[0], tPrimary, tCursor)
269}
270
271func TestProtectedModesetUnreadableStrayFailsClosed(t *testing.T) {
272	for _, missing := range []bool{false, true} {
273		t.Run(map[bool]string{false: "read-error", true: "missing-property"}[missing], func(t *testing.T) {
274			o, k, commits := testOutput(t)
275			o.stray = []*plane{{id: tOverlay, props: planeProps}}
276			var err error
277			if !missing {
278				err = unix.EACCES
279			}
280			k.EXPECT().objProps(uint32(tOverlay), uint32(objPlane)).Return(map[string][2]uint64{}, err).Once()
281			if err := o.protectedModeset(context.Background(), portsmocks.NewMockRenderer(t)); err == nil {
282				t.Fatal("unverified stray plane reported success")
283			}
284			if !o.protected || len(*commits) != 0 {
285				t.Fatal("unverified detachment touched KMS or dropped latch")
286			}
287		})
288	}
289}

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