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
123func TestAcquirePollTerminalFailureRetiresActiveAndAdded(t *testing.T) {
124 poller := newMockacquirePoller(t)
125 wake := make(chan struct{}, 1)
126 sw, err := newSyncWaiter(func() { wake <- struct{}{} })
127 if err != nil {
128 t.Fatal(err)
129 }
130 sw.poller = poller
131 defer sw.close()
132 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC)
133 if err != nil {
134 t.Fatal(err)
135 }
136 active := sw.add(fd)
137 var added *syncWait
138 poller.EXPECT().poll(mock.Anything, 100).RunAndReturn(func(fds []unix.PollFd, _ int) (int, error) { fds[0].Revents = unix.POLLIN; return 1, nil }).Once()
139 poller.EXPECT().poll(mock.Anything, 100).RunAndReturn(func([]unix.PollFd, int) (int, error) {
140 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC)
141 if err != nil {
142 t.Error(err)
143 return -1, err
144 }
145 added = sw.add(fd)
146 return -1, unix.EIO
147 }).Once()
148 sw.run(context.Background())
149 select {
150 case <-wake:
151 default:
152 t.Fatal("terminal failure did not wake owner")
153 }
154 for _, w := range []*syncWait{active, added} {
155 if w == nil || !sw.fired(w) || !w.failed {
156 t.Fatal("wait stranded after terminal poll failure")
157 }
158 }
159 if !sw.stopped() {
160 t.Fatal("waiter not terminal")
161 }
162 dev := newMocksyncobjDevice(t) // no device access permitted after terminal failure
163 if w, err := sw.watch(syncPoint{tl: &timeline{dev: dev, handle: 7}, point: 1}); err == nil || !w.failed {
164 t.Fatal("new explicit watch accepted")
165 }
166 var waits [4]*syncWait
167 if err := sw.watchImplicit(&ports.DMABuf{}, &waits); err == nil {
168 t.Fatal("new implicit watch accepted")
169 }
170}
171
172func TestFailedRootRetiresEveryChildPredecessorGraph(t *testing.T) {
173 h := newSyncHarness(t)
174 sw := h.s.syncWait
175 root := &surface{server: h.s}
176 child := &surface{server: h.s}
177 a, b := &surface{server: h.s}, &surface{server: h.s}
178 tl := &timeline{dev: h.dev, handle: 7, alive: true, uses: 4}
179 var waits [2]*syncWait
180 var graphs [2]*update
181 h.s.display.Do(func() {
182 for i, surf := range []*surface{a, b} {
183 fd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
184 if err != nil {
185 t.Error(err)
186 return
187 }
188 waits[i] = sw.add(fd)
189 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]}
190 surf.queueUpdate()
191 graphs[i] = surf.queue[len(surf.queue)-1]
192 graphs[i].refs++ // pin retired state for assertions
193 }
194 child.queueUpdate()
195 child.queueUpdate()
196 one, two := child.queue[0], child.queue[1]
197 one.deps = []*update{graphs[0]}
198 graphs[0].refs++
199 two.deps = []*update{graphs[1]}
200 graphs[1].refs++
201 root.queueUpdate()
202 u := root.queue[0]
203 u.deps = []*update{two}
204 two.refs++
205 u.failed = true
206 child.queueUpdate()
207 later := child.queue[2]
208 u.applyGraph()
209 if len(child.queue) != 1 || child.queue[0] != later || len(a.queue) != 0 || len(b.queue) != 0 || tl.uses != 0 {
210 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))
211 }
212 for i, g := range graphs {
213 if !g.retired || g.refs != 1 || !waits[i].cancelled.Load() {
214 t.Error("grandchild not safely retired")
215 }
216 g.releaseRef()
217 }
218 child.dropQueue()
219 })
220 if len(h.signalled()) != 0 {
221 t.Fatal("release signalled before acquire")
222 }
223}
224
225func TestAcquireExportFailurePreservesCurrentContent(t *testing.T) {
226 h := newSyncHarness(t)
227 h.releases = map[uint32]*syncReleaseProxy{}
228 if err := h.c.Roundtrip(); err != nil {
229 t.Fatal(err)
230 }
231 h.s.display.Do(func() { h.dev.ExpectedCalls = nil })
232 h.dev.EXPECT().destroy(mock.Anything).Return(nil).Maybe()
233 h.dev.EXPECT().signalled(uint32(7), uint64(111)).Return(true, nil).Once()
234 h.dev.EXPECT().exportSyncFile(uint32(7), uint64(111)).Return(nil, unix.EIO).Once()
235 h.dev.EXPECT().signal(uint32(7), uint64(112)).Return(nil).Once() // observed acquire: safe discarded release
236 drainContents(h.contents)
237 buffer := h.dmabuf()
238 var before ports.SurfaceContent
239 h.s.display.Do(func() { before = h.s.windows[h.win].xdg.surface.content })
240 h.commit(buffer, 111, 112)
241 if got, ok := h.content(50 * time.Millisecond); ok {
242 t.Fatalf("export failure published %+v", got)
243 }
244 h.s.display.Do(func() {
245 surf := h.s.windows[h.win].xdg.surface
246 if surf.content.Seq != before.Seq || surf.content.DMABuf != before.DMABuf || surf.hold.acquire != nil || len(surf.queue) != 0 {
247 t.Error("export failure changed current content")
248 }
249 for _, tl := range h.s.timelines {
250 if tl.uses != 0 {
251 t.Error("export failure pinned timeline")
252 }
253 }
254 })
255}