1package wayland
2
3import (
4 "context"
5 "encoding/binary"
6 "io"
7 "net"
8 "os"
9 "slices"
10 "testing"
11 "time"
12
13 "github.com/bnema/neferafk/internal/ports"
14 "github.com/bnema/zerowrap"
15 "golang.org/x/sys/unix"
16)
17
18func sockets(t *testing.T) (net.Conn, *net.UnixConn) {
19 t.Helper()
20 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
21 if err != nil {
22 t.Fatal(err)
23 }
24 files := []*os.File{os.NewFile(uintptr(fds[0]), "client"), os.NewFile(uintptr(fds[1]), "peer")}
25 a, err := net.FileConn(files[0])
26 files[0].Close()
27 if err != nil {
28 t.Fatal(err)
29 }
30 b, err := net.FileConn(files[1])
31 files[1].Close()
32 if err != nil {
33 t.Fatal(err)
34 }
35 t.Cleanup(func() { a.Close(); b.Close() })
36 return a, b.(*net.UnixConn)
37}
38func packet(id uint32, op uint16, body []byte) []byte {
39 b := make([]byte, 8+len(body))
40 binary.NativeEndian.PutUint32(b, id)
41 binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
42 copy(b[8:], body)
43 return b
44}
45func words(v ...uint32) []byte {
46 b := make([]byte, 4*len(v))
47 for i, n := range v {
48 binary.NativeEndian.PutUint32(b[i*4:], n)
49 }
50 return b
51}
52func wireString(s string) []byte {
53 b := make([]byte, 4+(len(s)+4)&^3)
54 binary.NativeEndian.PutUint32(b, uint32(len(s)+1))
55 copy(b[4:], s)
56 return b
57}
58func readWire(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) {
59 t.Helper()
60 p.SetReadDeadline(time.Now().Add(2 * time.Second))
61 h := make([]byte, 8)
62 if _, err := io.ReadFull(p, h); err != nil {
63 t.Fatal(err)
64 }
65 size := binary.NativeEndian.Uint32(h[4:]) >> 16
66 b := make([]byte, size-8)
67 if _, err := io.ReadFull(p, b); err != nil {
68 t.Fatal(err)
69 }
70 return binary.NativeEndian.Uint32(h), uint16(binary.NativeEndian.Uint32(h[4:])), b
71}
72func sendWire(t *testing.T, p *net.UnixConn, id uint32, op uint16, b []byte) {
73 t.Helper()
74 if _, err := p.Write(packet(id, op, b)); err != nil {
75 t.Fatal(err)
76 }
77}
78func global(t *testing.T, p *net.UnixConn, registry, name uint32, iface string, v uint32) {
79 b := append(words(name), wireString(iface)...)
80 b = append(b, words(v)...)
81 sendWire(t, p, registry, 0, b)
82}
83func synchronize(t *testing.T, p *net.UnixConn, body []byte) {
84 id := binary.NativeEndian.Uint32(body)
85 sendWire(t, p, id, 0, words(1))
86 sendWire(t, p, 1, 1, words(id))
87}
88func capability(t *testing.T, out <-chan ports.WaylandEvent) ports.Capabilities {
89 t.Helper()
90 for {
91 select {
92 case e := <-out:
93 if c, ok := e.(ports.WaylandCapabilities); ok {
94 return c.Capabilities
95 }
96 case <-time.After(2 * time.Second):
97 t.Fatal("capability timeout")
98 }
99 }
100}
101func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent {
102 t.Helper()
103 select {
104 case e := <-out:
105 return e
106 case <-time.After(2 * time.Second):
107 t.Fatal("event timeout")
108 return nil
109 }
110}
111
112func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) {
113 conn, peer := sockets(t)
114 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
115 if err != nil {
116 t.Fatal(err)
117 }
118 _, _, body := readWire(t, peer)
119 registry := binary.NativeEndian.Uint32(body)
120 ctx, cancel := context.WithCancel(context.Background())
121 defer cancel()
122 commands := make(chan ports.WaylandCommand, 8)
123 out := make(chan ports.WaylandEvent, 64)
124 done := make(chan error, 1)
125 go func() { done <- c.Run(ctx, commands, out) }()
126 _, _, body = readWire(t, peer)
127 global(t, peer, registry, 10, "wl_seat", 5)
128 global(t, peer, registry, 11, "wl_output", 4)
129 global(t, peer, registry, 12, "ext_idle_notifier_v1", 2)
130 global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1)
131 global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1)
132 synchronize(t, peer, body)
133 ids := map[string]uint32{}
134 notifications := map[uint32]uint32{}
135 var wake, power uint32
136 for {
137 id, op, b := readWire(t, peer)
138 if id == 1 {
139 synchronize(t, peer, b)
140 break
141 }
142 if id == registry {
143 n := int(binary.NativeEndian.Uint32(b[4:]))
144 iface := string(b[8 : 8+n-1])
145 ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:])
146 if iface == "wl_output" {
147 sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1"))
148 }
149 continue
150 }
151 if id == ids["ext_idle_notifier_v1"] {
152 child := binary.NativeEndian.Uint32(b)
153 ms := binary.NativeEndian.Uint32(b[4:])
154 if op == 2 {
155 wake = child
156 } else {
157 notifications[ms] = child
158 }
159 }
160 if id == ids["zwlr_output_power_manager_v1"] {
161 power = binary.NativeEndian.Uint32(b)
162 sendWire(t, peer, power, 0, words(1))
163 }
164 }
165 caps := capability(t, out)
166 if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported {
167 t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps)
168 }
169 if !slices.Contains(caps.Missing, "zwp_linux_dmabuf_v1 >= 4") || slices.Contains(caps.Missing, "ext_session_lock_manager_v1 >= 1") {
170 t.Fatal(caps.Missing)
171 }
172 lifetime := caps.Outputs[0].Lifetime
173 sendWire(t, peer, notifications[1000], 0, nil)
174 idle, ok := nextEvent(t, out).(ports.WaylandIdle)
175 if !ok || idle.Generation != 1 || idle.After != time.Second || idle.Cycle != 1 {
176 t.Fatal(idle, ok)
177 }
178 // Reload retains the one-second proxy and removes only the two-second one.
179 commands <- ports.SetDeadlines{Generation: 2, Deadlines: []time.Duration{time.Second, 3 * time.Second}}
180 var added uint32
181 for i := 0; i < 2; i++ {
182 id, op, b := readWire(t, peer)
183 if id == notifications[2000] && op == 0 {
184 continue
185 }
186 if id == ids["ext_idle_notifier_v1"] && op == 1 && binary.NativeEndian.Uint32(b[4:]) == 3000 {
187 added = binary.NativeEndian.Uint32(b)
188 continue
189 }
190 t.Fatalf("reload request %d/%d %x", id, op, b)
191 }
192 if added == 0 {
193 t.Fatal("new deadline missing")
194 }
195 capability(t, out)
196 // An in-flight event for the deleted notification must never become policy.
197 sendWire(t, peer, notifications[2000], 0, nil)
198 sendWire(t, peer, notifications[1000], 1, nil)
199 sendWire(t, peer, notifications[1000], 0, nil)
200 idle, ok = nextEvent(t, out).(ports.WaylandIdle)
201 if !ok || idle.Generation != 2 || idle.Proxy == 0 || idle.Cycle != 2 {
202 t.Fatal(idle, ok)
203 }
204 commands <- ports.OutputPower{On: false}
205 id, op, b := readWire(t, peer)
206 if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 {
207 t.Fatalf("off %d/%d %x", id, op, b)
208 }
209 sendWire(t, peer, wake, 0, nil)
210 sendWire(t, peer, wake, 1, nil)
211 id, op, b = readWire(t, peer)
212 if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 1 {
213 t.Fatalf("wake %d/%d %x", id, op, b)
214 }
215 if _, ok := nextEvent(t, out).(ports.WaylandActivity); !ok {
216 t.Fatal("missing activity")
217 }
218 // Removing/re-adding the same global number creates a new output lifetime.
219 sendWire(t, peer, registry, 1, words(11))
220 for i := 0; i < 2; i++ {
221 readWire(t, peer)
222 }
223 caps = capability(t, out)
224 if len(caps.Outputs) != 0 {
225 t.Fatal(caps)
226 }
227 global(t, peer, registry, 11, "wl_output", 4)
228 var newPower uint32
229 for i := 0; i < 2; i++ {
230 id, _, b = readWire(t, peer)
231 if id == registry {
232 ids["new-output"] = binary.NativeEndian.Uint32(b[len(b)-4:])
233 } else {
234 newPower = binary.NativeEndian.Uint32(b)
235 }
236 }
237 caps = capability(t, out)
238 if len(caps.Outputs) != 1 || caps.Outputs[0].Lifetime == lifetime {
239 t.Fatal(caps)
240 }
241 // A late failed event from the previous output cannot disable its successor.
242 sendWire(t, peer, power, 1, nil)
243 sendWire(t, peer, newPower, 1, nil)
244 id, op, _ = readWire(t, peer)
245 if id != newPower || op != 1 {
246 t.Fatal("failed control not destroyed", id, op)
247 }
248 if failure, ok := nextEvent(t, out).(ports.WaylandPowerFailed); !ok || failure.Output != caps.Outputs[0].Lifetime {
249 t.Fatal(failure, ok)
250 }
251 caps = capability(t, out)
252 if caps.Outputs[0].PowerSupported || len(caps.Missing) != 4 {
253 t.Fatal(caps)
254 }
255 cancel()
256 select {
257 case err := <-done:
258 if err != nil {
259 t.Fatal(err)
260 }
261 case <-time.After(2 * time.Second):
262 t.Fatal("Run failed to join reader")
263 }
264}
265
266func TestMissingAndDisabledCapabilities(t *testing.T) {
267 conn, peer := sockets(t)
268 c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
269 if err != nil {
270 t.Fatal(err)
271 }
272 _, _, b := readWire(t, peer)
273 registry := binary.NativeEndian.Uint32(b)
274 ctx, cancel := context.WithCancel(context.Background())
275 defer cancel()
276 out := make(chan ports.WaylandEvent, 8)
277 done := make(chan error, 1)
278 go func() { done <- c.Run(ctx, nil, out) }()
279 _, _, b = readWire(t, peer)
280 global(t, peer, registry, 10, "ext_idle_notifier_v1", 1)
281 global(t, peer, registry, 11, "zwlr_output_power_manager_v1", 1)
282 synchronize(t, peer, b)
283 id, _, b := readWire(t, peer)
284 if id != 1 {
285 t.Fatalf("disabled optional global bound: %d", id)
286 }
287 synchronize(t, peer, b)
288 caps := capability(t, out)
289 if len(caps.Missing) != 5 || slices.Contains(caps.Missing, "ext_idle_notifier_v1 >= 2") {
290 t.Fatal(caps)
291 }
292 cancel()
293 if err := <-done; err != nil {
294 t.Fatal(err)
295 }
296}
297
298func TestDeadlineValidation(t *testing.T) {
299 for _, d := range []time.Duration{-time.Second, time.Nanosecond, time.Duration(1<<32) * time.Millisecond} {
300 if _, err := normalize([]time.Duration{d}); err == nil {
301 t.Fatal(d)
302 }
303 }
304 ds, err := normalize([]time.Duration{0, time.Second, time.Second})
305 if err != nil || len(ds) != 1 {
306 t.Fatal(ds, err)
307 }
308}
309
310func TestCancelBlockedStartup(t *testing.T) {
311 conn, peer := sockets(t)
312 c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
313 if err != nil {
314 t.Fatal(err)
315 }
316 readWire(t, peer)
317 ctx, cancel := context.WithCancel(context.Background())
318 done := make(chan error, 1)
319 go func() { done <- c.Run(ctx, nil, nil) }()
320 readWire(t, peer) // sync request deliberately unanswered
321 cancel()
322 select {
323 case err := <-done:
324 if err != nil {
325 t.Fatal(err)
326 }
327 case <-time.After(time.Second):
328 t.Fatal("cancel did not close socket")
329 }
330}
331
332func TestBoundedCallbackQueueFailsClosed(t *testing.T) {
333 conn, peer := sockets(t)
334 c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard}))
335 if err != nil {
336 t.Fatal(err)
337 }
338 readWire(t, peer)
339 for i := 0; i <= queueSize; i++ {
340 c.enqueue(notice{kind: 1})
341 }
342 select {
343 case <-c.overflow:
344 default:
345 t.Fatal("overflow not reported")
346 }
347}