1package wayland
2
3import (
4 "context"
5 "math"
6 "os"
7 "testing"
8 "time"
9
10 "github.com/bnema/go-wayland-bindings/server/wayland"
11 "github.com/bnema/neferwl/internal/ports"
12 "github.com/stretchr/testify/mock"
13 "golang.org/x/sys/unix"
14)
15
16func TestExplicitAcquireWatchFailureDiscardsCommit(t *testing.T) {
17 for _, failure := range []string{"signalled", "eventfd"} {
18 t.Run(failure, func(t *testing.T) {
19 h := newSyncHarness(t)
20 h.releases = map[uint32]*syncReleaseProxy{}
21 // Order setup after import and reset on the display owner so no
22 // concurrent device call can read the generated expectations.
23 if err := h.c.Roundtrip(); err != nil {
24 t.Fatal(err)
25 }
26 h.s.display.Do(func() { h.dev.ExpectedCalls = nil })
27 h.dev.EXPECT().fdToHandle(mock.Anything).Return(7, nil).Maybe()
28 h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
29 if failure == "signalled" {
30 h.dev.EXPECT().signalled(uint32(7), uint64(101)).Return(false, unix.EIO).Once()
31 } else {
32 h.dev.EXPECT().signalled(uint32(7), uint64(101)).Return(false, nil).Once()
33 h.dev.EXPECT().eventfd(uint32(7), uint64(101), mock.Anything).Return(unix.EIO).Once()
34 }
35 drainContents(h.contents)
36 buffer := h.dmabuf()
37 h.commit(buffer, 101, 102)
38 if got, ok := h.content(50 * time.Millisecond); ok {
39 t.Fatalf("untracked acquire published %+v", got)
40 }
41 h.s.display.Do(func() {
42 surf := h.s.windows[h.win].xdg.surface
43 if len(surf.queue) != 0 || surf.hold.acquire != nil {
44 t.Error("failed acquire retained queued or rendered fence")
45 }
46 for _, tl := range h.s.timelines {
47 if tl.uses != 0 {
48 t.Errorf("timeline uses=%d", tl.uses)
49 }
50 }
51 })
52 if len(h.signalled()) != 0 {
53 t.Fatal("unsafe release point signalled")
54 }
55 select {
56 case <-h.releases[buffer].released:
57 t.Fatal("explicit buffer release event")
58 default:
59 }
60 })
61 }
62}
63
64func TestImplicitAcquireWatchFailureDiscardsCommit(t *testing.T) {
65 for _, failure := range []string{"poll", "hangup"} {
66 t.Run(failure, func(t *testing.T) {
67 h := newSyncHarness(t)
68 h.releases = map[uint32]*syncReleaseProxy{}
69 drainContents(h.contents)
70 buffer := h.dmabuf()
71 var plane *os.File
72 var writer *os.File
73 if failure == "poll" {
74 plane = os.NewFile(uintptr(math.MaxInt32), "invalid")
75 } else {
76 var err error
77 plane, writer, err = os.Pipe()
78 if err != nil {
79 t.Fatal(err)
80 }
81 defer plane.Close()
82 }
83 // Real pollable descriptors exercise observation errors without replacing
84 // watch/poll functions or constructing a handwritten device double.
85 if err := h.c.Roundtrip(); err != nil {
86 t.Fatal(err)
87 }
88 h.s.display.Do(func() {
89 var buf *dmabufBuffer
90 var resource *wayland.Buffer
91 for r, b := range h.s.buffers {
92 if r.ID() == buffer {
93 buf = b.(*dmabufBuffer)
94 resource = wayland.WrapBuffer(r)
95 }
96 }
97 if buf == nil {
98 t.Error("buffer missing")
99 return
100 }
101 original := buf.buf.Planes[0].File
102 defer func() { buf.buf.Planes[0].File = original }()
103 buf.buf.Planes[0].File = plane
104 surf := h.s.windows[h.win].xdg.surface
105 surf.Attach(surf.wl, resource, 0, 0)
106 surf.Commit(surf.wl)
107 })
108 if writer != nil {
109 writer.Close()
110 }
111 if got, ok := h.content(100 * time.Millisecond); ok {
112 t.Fatalf("failed implicit acquire published %+v", got)
113 }
114 h.s.display.Do(func() {
115 if len(h.s.windows[h.win].xdg.surface.queue) != 0 {
116 t.Error("failed implicit acquire left queue blocked")
117 }
118 })
119 })
120 }
121}
122
123// An idle waiter sleeps in one poll with no timeout; cancelling ctx ends it.
124func TestSyncWaiterIdleNeverTicks(t *testing.T) {
125 sw, err := newSyncWaiter(func() {})
126 if err != nil {
127 t.Fatal(err)
128 }
129 defer sw.close()
130 poller := newMockacquirePoller(t)
131 real := sw.poller
132 sw.poller = poller
133 polls := make(chan int, 16)
134 poller.EXPECT().poll(mock.Anything, mock.Anything).RunAndReturn(func(fds []unix.PollFd, timeout int) (int, error) {
135 polls <- timeout
136 return real.poll(fds, timeout)
137 })
138 ctx, cancel := context.WithCancel(context.Background())
139 defer cancel()
140 done := make(chan struct{})
141 go func() { sw.run(ctx); close(done) }()
142 select {
143 case timeout := <-polls:
144 if timeout != -1 {
145 t.Fatalf("idle poll timeout %d, want -1", timeout)
146 }
147 case <-time.After(5 * time.Second):
148 t.Fatal("the waiter never polled")
149 }
150 cancel()
151 select {
152 case <-done:
153 case <-time.After(5 * time.Second):
154 t.Fatal("cancel did not stop the waiter")
155 }
156 if n := len(polls); n != 0 {
157 t.Fatalf("%d extra polls while idle", n)
158 }
159}
160
161func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) {
162 poller := newMockacquirePoller(t)
163 wake := make(chan struct{}, 1)
164 sw, err := newSyncWaiter(func() { wake <- struct{}{} })
165 if err != nil {
166 t.Fatal(err)
167 }
168 sw.poller = poller
169 defer sw.close()
170 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC)
171 if err != nil {
172 t.Fatal(err)
173 }
174 active := sw.add(fd)
175 var added *syncWait
176 poller.EXPECT().poll(mock.Anything, -1).RunAndReturn(func(fds []unix.PollFd, _ int) (int, error) { fds[0].Revents = unix.POLLIN; return 1, nil }).Once()
177 poller.EXPECT().poll(mock.Anything, -1).RunAndReturn(func([]unix.PollFd, int) (int, error) {
178 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC)
179 if err != nil {
180 t.Error(err)
181 return -1, err
182 }
183 added = sw.add(fd)
184 return -1, unix.EIO
185 }).Once()
186 sw.run(context.Background())
187 select {
188 case <-wake:
189 default:
190 t.Fatal("terminal failure did not wake owner")
191 }
192 for _, w := range []*syncWait{active, added} {
193 if w == nil || !sw.fired(w) || !w.failed {
194 t.Fatal("wait stranded after terminal poll failure")
195 }
196 }
197 if !sw.stopped() {
198 t.Fatal("waiter not terminal")
199 }
200 dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure
201 if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed {
202 t.Fatal("new explicit watch accepted")
203 }
204 var waits [4]*syncWait
205 if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil {
206 t.Fatal("new implicit watch accepted")
207 }
208}
209
210// A waiter whose pipe cannot be pinned fails its waits and still closes
211// their descriptors.
212func TestSyncWaiterPinFailureClosesWaits(t *testing.T) {
213 sw, err := newSyncWaiter(func() {})
214 if err != nil {
215 t.Fatal(err)
216 }
217 defer sw.close()
218 // Pinned already: run's own Pin fails with EEXIST.
219 if err := sw.waiter.Pin(int32(sw.pipeR.Fd())); err != nil {
220 t.Fatal(err)
221 }
222 // A pipe stands in for the eventfd: once run closes the read end, a
223 // write fails with EPIPE, whatever reuses the descriptor number.
224 var p [2]int
225 if err := unix.Pipe2(p[:], unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
226 t.Fatal(err)
227 }
228 defer unix.Close(p[1])
229 w := sw.add(p[0])
230 sw.run(context.Background())
231 if !sw.fired(w) || !w.failed {
232 t.Fatal("wait not failed after pin failure")
233 }
234 if _, err := unix.Write(p[1], []byte{0}); err != unix.EPIPE {
235 t.Fatalf("wait descriptor still open: write %v", err)
236 }
237}
238
239func TestFailedRootRetiresEveryChildPredecessorGraph(t *testing.T) {
240 h := newSyncHarness(t)
241 sw := h.s.syncWait
242 root := &surface{server: h.s}
243 child := &surface{server: h.s}
244 a, b := &surface{server: h.s}, &surface{server: h.s}
245 tl := &timeline{dev: h.dev, handle: 7, alive: true, uses: 4}
246 var waits [2]*syncWait
247 var graphs [2]*update
248 h.s.display.Do(func() {
249 for i, surf := range []*surface{a, b} {
250 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
251 if err != nil {
252 t.Error(err)
253 return
254 }
255 waits[i] = sw.add(fd)
256 surf.next.sync = &commitSync{acquire: syncPoint{tl: tl, point: uint64(21 + i*2)}, release: syncPoint{tl: tl, point: uint64(22 + i*2)}, wait: waits[i]}
257 surf.queueUpdate()
258 graphs[i] = surf.queue[len(surf.queue)-1]
259 graphs[i].refs++ // pin retired state for assertions
260 }
261 child.queueUpdate()
262 child.queueUpdate()
263 one, two := child.queue[0], child.queue[1]
264 one.deps = []*update{graphs[0]}
265 graphs[0].refs++
266 two.deps = []*update{graphs[1]}
267 graphs[1].refs++
268 root.queueUpdate()
269 u := root.queue[0]
270 u.deps = []*update{two}
271 two.refs++
272 u.failed = true
273 child.queueUpdate()
274 later := child.queue[2]
275 u.applyGraph()
276 if len(child.queue) != 1 || child.queue[0] != later || len(a.queue) != 0 || len(b.queue) != 0 || tl.uses != 0 {
277 t.Errorf("retirement stranded prefix graphs: uses=%d child=%d a=%d b=%d", tl.uses, len(child.queue), len(a.queue), len(b.queue))
278 }
279 for i, g := range graphs {
280 if !g.retired || g.refs != 1 || !waits[i].cancelled.Load() {
281 t.Error("grandchild not safely retired")
282 }
283 g.releaseRef()
284 }
285 child.dropQueue()
286 })
287 if len(h.signalled()) != 0 {
288 t.Fatal("release signalled before acquire")
289 }
290}
291
292func TestAcquireExportFailurePreservesCurrentContent(t *testing.T) {
293 h := newSyncHarness(t)
294 h.releases = map[uint32]*syncReleaseProxy{}
295 if err := h.c.Roundtrip(); err != nil {
296 t.Fatal(err)
297 }
298 h.s.display.Do(func() { h.dev.ExpectedCalls = nil })
299 h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
300 h.dev.EXPECT().signalled(uint32(7), uint64(111)).Return(true, nil).Once()
301 h.dev.EXPECT().exportSyncFile(uint32(7), uint64(111)).Return(nil, unix.EIO).Once()
302 h.dev.EXPECT().signal(uint32(7), uint64(112)).Return(nil).Once() // observed acquire: safe discarded release
303 drainContents(h.contents)
304 buffer := h.dmabuf()
305 var before ports.SurfaceContent
306 h.s.display.Do(func() { before = h.s.windows[h.win].xdg.surface.content })
307 h.commit(buffer, 111, 112)
308 if got, ok := h.content(50 * time.Millisecond); ok {
309 t.Fatalf("export failure published %+v", got)
310 }
311 h.s.display.Do(func() {
312 surf := h.s.windows[h.win].xdg.surface
313 if surf.content.Seq != before.Seq || surf.content.DMABuf != before.DMABuf || surf.hold.acquire != nil || len(surf.queue) != 0 {
314 t.Error("export failure changed current content")
315 }
316 for _, tl := range h.s.timelines {
317 if tl.uses != 0 {
318 t.Error("export failure pinned timeline")
319 }
320 }
321 })
322}