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}