v0.3.0 2df0761

surface_internal_test.go RawHistoryFolder

1package neferclient
2
3import (
4	"encoding/binary"
5	"strings"
6	"testing"
7
8	"github.com/bnema/go-wayland-bindings/client/linuxdrmsyncobj"
9	"github.com/bnema/go-wayland-bindings/client/viewporter"
10	"github.com/stretchr/testify/mock"
11	"github.com/stretchr/testify/require"
12	"golang.org/x/sys/unix"
13)
14
15// presentable returns a configured surface that sends through req.
16func presentable(req surfaceRequests, scale float64) *Surface {
17	return &Surface{id: 1, req: req, width: 100, height: 50, scale: scale, sentBufScale: 1, role: roleToplevel,
18		configured: true, frameReady: true, fbDone: true,
19		buffers:   map[uint64]importedBuffer{1: {w: 100, h: 50}},
20		timelines: map[uint64]*linuxdrmsyncobj.WpLinuxDrmSyncobjTimeline{1: nil, 2: nil},
21		viewport:  &viewporter.WpViewport{}}
22}
23
24func TestPresentOrdering(t *testing.T) {
25	m := newMocksurfaceRequests(t)
26	s := presentable(m, 1.5)
27	// NotBefore makes testify fail a call that arrives before its predecessor.
28	dest := m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
29	opaque := m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once().NotBefore(dest)
30	acquire := m.EXPECT().setAcquire(uint64(1), uint64(7)).Return(nil).Once().NotBefore(opaque)
31	release := m.EXPECT().setRelease(uint64(2), uint64(8)).Return(nil).Once().NotBefore(acquire)
32	attach := m.EXPECT().attach(uint64(1)).Return(nil).Once().NotBefore(release)
33	damage := m.EXPECT().damageBuffer(Rect{1, 2, 3, 4}).Return(nil).Once().NotBefore(attach)
34	frame := m.EXPECT().frame().Return(nil).Once().NotBefore(damage)
35	m.EXPECT().commit().Return(nil).Once().NotBefore(frame)
36
37	p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, AcquirePoint: 7, ReleasePoint: 8,
38		Damage: []Rect{{1, 2, 3, 4}}, Opaque: true}
39	require.NoError(t, s.Present(p))
40	require.False(t, s.frameReady)
41	require.ErrorContains(t, s.Present(p), "frame callback pending") // no further request: strict mock
42}
43
44func TestPresentSkipsUnchangedState(t *testing.T) {
45	m := newMocksurfaceRequests(t)
46	s := presentable(m, 2)
47	m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
48	m.EXPECT().setOpaqueRegion(int32(100), int32(50), true).Return(nil).Once()
49	m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
50	m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
51	m.EXPECT().attach(uint64(1)).Return(nil).Twice()
52	m.EXPECT().damageBuffer(Rect{0, 0, 200, 100}).Return(nil).Twice() // no damage: whole physical buffer
53	m.EXPECT().frame().Return(nil).Twice()
54	m.EXPECT().commit().Return(nil).Twice()
55	p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2, Opaque: true}
56	require.NoError(t, s.Present(p))
57	s.frameReady = true
58	require.NoError(t, s.Present(p)) // destination and opaque region are not resent
59}
60
61func TestPresentRefusals(t *testing.T) {
62	m := newMocksurfaceRequests(t) // strict: a refused Present sends nothing
63	p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
64	for name, mutate := range map[string]func(*Surface){
65		"before configure": func(s *Surface) { s.configured = false },
66		"before feedback":  func(s *Surface) { s.fbDone = false },
67		"unknown buffer":   func(s *Surface) { delete(s.buffers, 1) },
68		"unknown timeline": func(s *Surface) { delete(s.timelines, 2) },
69		"closed":           func(s *Surface) { s.closed = true },
70		"bad geometry":     func(s *Surface) { s.scale = 0 },
71		"fractional without viewporter": func(s *Surface) {
72			s.scale, s.viewport = 1.5, nil
73		},
74	} {
75		s := presentable(m, 1)
76		mutate(s)
77		require.Error(t, s.Present(p), name)
78	}
79}
80
81func TestPhysicalSize(t *testing.T) {
82	s := presentable(nil, 1.25)
83	w, h, err := s.PhysicalSize()
84	require.NoError(t, err)
85	require.Equal(t, [2]int32{125, 63}, [2]int32{w, h}) // 62.5 rounds up
86}
87
88func tableFD(t *testing.T, entries []Format) int {
89	t.Helper()
90	fd, err := unix.MemfdCreate("table", unix.MFD_CLOEXEC)
91	require.NoError(t, err)
92	var b []byte
93	for _, e := range entries {
94		b = binary.NativeEndian.AppendUint32(b, e.FourCC)
95		b = binary.NativeEndian.AppendUint32(b, 0)
96		b = binary.NativeEndian.AppendUint64(b, e.Modifier)
97	}
98	_, err = unix.Write(fd, b)
99	require.NoError(t, err)
100	return fd
101}
102
103func postRound(c *Conn, s *Surface, fd int, size uint32, device uint64, indices ...uint16) {
104	id := uint32(s.id)
105	c.q.post(&event{kind: evFeedbackTable, id: id, fd: int32(fd), flags: size})
106	c.q.post(&event{kind: evFeedbackMainDevice, id: id, dev: device})
107	e := event{kind: evFeedbackFormats, id: id}
108	for _, i := range indices {
109		binary.NativeEndian.PutUint16(e.name[e.nameLen:], i)
110		e.nameLen += 2
111	}
112	c.q.post(&e)
113	c.q.post(&event{kind: evFeedbackDone, id: id})
114}
115
116func newOwnerConn() (*Conn, *Surface) {
117	c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}, watched: map[int32]uint64{}, efd: -1, epfd: -1}
118	s := presentable(nil, 1)
119	c.surfaces[s.id], s.c, s.fbDone = s, c, false
120	return c, s
121}
122
123func TestFeedbackParsedOnOwner(t *testing.T) {
124	c, s := newOwnerConn()
125	table := []Format{{0x34325258, 0}, {0x34325241, 7}, {0x34325258, 1 << 40}}
126	postRound(c, s, tableFD(t, table), 16*3, 0xe280, 2, 0)
127	h := NewMockHandler(t)
128	h.EXPECT().FeedbackDone(s.id).Return().Once()
129	require.NoError(t, c.Dispatch(h))
130	fb := s.Feedback()
131	require.NotNil(t, fb)
132	require.Equal(t, uint64(0xe280), fb.MainDevice)
133	require.Equal(t, []Format{table[2], table[0]}, fb.Formats, "tranche order")
134
135	// A second round replaces the first without leaking the first one's formats.
136	h.EXPECT().FeedbackDone(s.id).Return().Once()
137	postRound(c, s, tableFD(t, table), 16*3, 0xe281, 1)
138	require.NoError(t, c.Dispatch(h))
139	require.Equal(t, &Feedback{MainDevice: 0xe281, Formats: []Format{table[1]}}, s.Feedback())
140}
141
142func TestFeedbackBadIndexAndSize(t *testing.T) {
143	c, s := newOwnerConn()
144	postRound(c, s, tableFD(t, []Format{{1, 1}}), 16, 5, 3)
145	postRound(c, s, tableFD(t, nil), 15, 5)
146	h := NewMockHandler(t)
147	h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "out of range") })).Return().Once()
148	h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "invalid dmabuf format table size") })).Return().Once()
149	h.EXPECT().FeedbackDone(s.id).Return().Twice()
150	require.NoError(t, c.Dispatch(h))
151}
152
153func fdOpen(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
154
155func TestCloseClosesUndrainedFeedbackFDs(t *testing.T) {
156	c, s := newOwnerConn()
157	ringed := tableFD(t, nil)
158	c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(ringed)})
159	held := tableFD(t, nil)
160	c.q.pausing = true
161	c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(held)}) // parked in held
162	require.True(t, fdOpen(ringed))
163	require.True(t, fdOpen(held))
164	c.q.closeFDs()
165	require.False(t, fdOpen(ringed), "ring event's descriptor")
166	require.False(t, fdOpen(held), "parked event's descriptor")
167
168	// An event that is drained owns its descriptor no more: closeFDs must not
169	// touch a number the process may have reused.
170	drained := tableFD(t, nil)
171	c.q.post(&event{kind: evFeedbackTable, id: 99, fd: int32(drained)}) // pausing: held
172	c.q.pausing = false
173	c.q.endPause()
174	var ev event
175	for c.q.pop(&ev) { // drain everything: the earlier events and this one
176	}
177	c.q.closeFDs()
178	require.True(t, fdOpen(drained))
179	_ = unix.Close(drained)
180}
181
182func TestEventForClosedSurfaceDropsFD(t *testing.T) {
183	c, s := newOwnerConn()
184	fd := tableFD(t, nil)
185	s.closed = true
186	c.q.post(&event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd)})
187	require.NoError(t, c.Dispatch(nil))
188	require.False(t, fdOpen(fd))
189}
190
191// GO-001: the descriptor is consumed by readTable; the event must not close
192// the number a second time, which by then may belong to someone else.
193func TestFeedbackTableFDNotClosedTwice(t *testing.T) {
194	c, s := newOwnerConn()
195	fd := tableFD(t, []Format{{1, 1}})
196	ev := event{kind: evFeedbackTable, id: uint32(s.id), fd: int32(fd), flags: 16}
197	c.applySurface(&ev, NewMockHandler(t))
198	require.False(t, fdOpen(fd), "readTable consumed it")
199	require.Equal(t, int32(-1), ev.fd, "ownership left the event")
200	other := tableFD(t, nil) // the kernel hands out the freed number again
201	require.Equal(t, fd, other)
202	ev.closeFD() // what a second close of the event would do
203	require.True(t, fdOpen(other), "an unrelated descriptor that reuses the number stays open")
204	_ = unix.Close(other)
205}
206
207func TestLockRules(t *testing.T) {
208	c := &Conn{q: newQueue(false), surfaces: map[SurfaceID]*Surface{}}
209	l := &Lock{c: c, gen: 1}
210	c.curLock = l
211	require.ErrorContains(t, l.Unlock(), "before the locked event")
212	c.q.post(&event{kind: evLocked, id: 2}) // another generation: ignored
213	require.NoError(t, c.Dispatch(nil))
214	require.False(t, l.Locked())
215	c.q.post(&event{kind: evLocked, id: 1})
216	require.NoError(t, c.Dispatch(nil))
217	require.True(t, l.Locked())
218	require.ErrorContains(t, l.Close(), "use Unlock")
219	c.q.post(&event{kind: evLockFinished, id: 1})
220	require.NoError(t, c.Dispatch(nil))
221	require.False(t, l.Locked())
222	require.ErrorContains(t, l.Unlock(), "before the locked event")
223}
224
225// GO-004: the buffer scale returns to 1 when the output scale drops, with no
226// viewport to carry the size.
227func TestPresentResetsBufferScale(t *testing.T) {
228	m := newMocksurfaceRequests(t)
229	s := presentable(m, 2)
230	s.viewport = nil
231	s.buffers[1] = importedBuffer{w: 200, h: 100}
232	m.EXPECT().setBufferScale(int32(2)).Return(nil).Once()
233	m.EXPECT().setBufferScale(int32(1)).Return(nil).Once()
234	m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Twice()
235	m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Twice()
236	m.EXPECT().attach(uint64(1)).Return(nil).Twice()
237	m.EXPECT().damageBuffer(mock.Anything).Return(nil).Twice()
238	m.EXPECT().frame().Return(nil).Twice()
239	m.EXPECT().commit().Return(nil).Twice()
240	p := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
241	require.NoError(t, s.Present(p))
242	s.scale, s.frameReady = 1, true
243	s.buffers[1] = importedBuffer{w: 100, h: 50}
244	require.NoError(t, s.Present(p))
245}
246
247// GO-005: size mismatches, timeline point order and a nil request are refused
248// before anything is sent (the mock is strict).
249func TestPresentRefusesInconsistentRequests(t *testing.T) {
250	m := newMocksurfaceRequests(t)
251	good := &Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 2}
252	for name, tc := range map[string]struct {
253		mutate func(*Surface)
254		p      *Present
255	}{
256		"nil":                  {func(*Surface) {}, nil},
257		"mismatch no viewport": {func(s *Surface) { s.viewport = nil; s.buffers[1] = importedBuffer{w: 99, h: 50} }, good},
258		"mismatch lock role":   {func(s *Surface) { s.role = roleLock; s.buffers[1] = importedBuffer{w: 100, h: 49} }, good},
259		"same timeline acquire not before release": {func(*Surface) {},
260			&Present{Buffer: 1, AcquireTimeline: 1, ReleaseTimeline: 1, AcquirePoint: 5, ReleasePoint: 5}},
261	} {
262		s := presentable(m, 1)
263		tc.mutate(s)
264		require.Error(t, s.Present(tc.p), name)
265	}
266	// With a viewport a different buffer size is legal (scaled by the compositor).
267	s := presentable(m, 1)
268	s.buffers[1] = importedBuffer{w: 50, h: 25}
269	m.EXPECT().setDestination(int32(100), int32(50)).Return(nil).Once()
270	m.EXPECT().setAcquire(mock.Anything, mock.Anything).Return(nil).Once()
271	m.EXPECT().setRelease(mock.Anything, mock.Anything).Return(nil).Once()
272	m.EXPECT().attach(uint64(1)).Return(nil).Once()
273	m.EXPECT().damageBuffer(mock.Anything).Return(nil).Once()
274	m.EXPECT().frame().Return(nil).Once()
275	m.EXPECT().commit().Return(nil).Once()
276	require.NoError(t, s.Present(good))
277}
278
279// GO-008: lock and layer configure sizes share the bounds check.
280func TestConfigureEventBounds(t *testing.T) {
281	for _, kind := range []eventKind{evLayerConfigure, evLockConfigure} {
282		ev := configureEvent(kind, 3, 9, maxLayerDimension, 1)
283		require.Equal(t, event{kind: kind, id: 3, serial: 9, a: maxLayerDimension, b: 1}, ev)
284		require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, maxLayerDimension+1, 1).kind)
285		require.Equal(t, evBadEvent, configureEvent(kind, 3, 9, 1, 1<<31).kind, "no wrap to a negative int32")
286	}
287	c, _ := newOwnerConn()
288	c.q.post(&event{kind: evBadEvent, id: 1, a: badConfigure})
289	h := NewMockHandler(t)
290	h.EXPECT().Error(mock.MatchedBy(func(err error) bool { return strings.Contains(err.Error(), "configure size") })).Return().Once()
291	require.NoError(t, c.Dispatch(h))
292}

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 1

Tags 4

↑↓ moveenter openesc close