1//go:build linux
2
3package wayland
4
5import (
6 "encoding/binary"
7 "net"
8 "os"
9 "testing"
10 "time"
11
12 "github.com/bnema/wlturbo"
13 "github.com/bnema/wlturbo/protocol/core"
14 "golang.org/x/sys/unix"
15)
16
17func inputPair(t *testing.T) (*net.UnixConn, *net.UnixConn) {
18 t.Helper()
19 f, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
20 if err != nil {
21 t.Fatal(err)
22 }
23 a := os.NewFile(uintptr(f[0]), "client")
24 b := os.NewFile(uintptr(f[1]), "server")
25 ca, e := net.FileConn(a)
26 _ = a.Close()
27 if e != nil {
28 t.Fatal(e)
29 }
30 cb, e := net.FileConn(b)
31 _ = b.Close()
32 if e != nil {
33 t.Fatal(e)
34 }
35 t.Cleanup(func() { _ = ca.Close(); _ = cb.Close() })
36 return ca.(*net.UnixConn), cb.(*net.UnixConn)
37}
38func inputFrame(id uint32, op uint16, body ...uint32) []byte {
39 b := make([]byte, 8+len(body)*4)
40 binary.LittleEndian.PutUint32(b, id)
41 binary.LittleEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op))
42 for i, v := range body {
43 binary.LittleEndian.PutUint32(b[8+i*4:], v)
44 }
45 return b
46}
47func inputSend(t *testing.T, p *net.UnixConn, msg []byte, fd ...int) {
48 t.Helper()
49 var oob []byte
50 if len(fd) > 0 {
51 oob = unix.UnixRights(fd...)
52 }
53 if _, _, err := p.WriteMsgUnix(msg, oob, nil); err != nil {
54 t.Fatal(err)
55 }
56}
57func inputRequest(t *testing.T, p *net.UnixConn) {
58 t.Helper()
59 _ = p.SetReadDeadline(time.Now().Add(time.Second))
60 b := make([]byte, 256)
61 if _, err := p.Read(b); err != nil {
62 t.Fatal(err)
63 }
64}
65func TestPointerFrameOverSocketpair(t *testing.T) {
66 ca, server := inputPair(t)
67 d, err := wlturbo.ConnectFromConn(ca)
68 if err != nil {
69 t.Fatal(err)
70 }
71 defer d.Close()
72 inputRequest(t, server)
73 ctx := d.Context()
74 seat := core.NewSeat(ctx)
75 if err = d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
76 t.Fatal(err)
77 }
78 inputRequest(t, server)
79 p, err := seat.GetPointer()
80 if err != nil {
81 t.Fatal(err)
82 }
83 inputRequest(t, server)
84 surface := core.NewSurface(ctx)
85 surface.SetID(30)
86 ctx.Register(surface)
87 w := &Window{Surface: surface, seatVersion: 9, events: make(chan Event, 10), readerStop: make(chan struct{})}
88 w.watchPointer(p)
89 for _, msg := range [][]byte{
90 inputFrame(p.ID(), 0, 1, 30, 256*12, 256*7), // enter: surface-local logical pixels
91 inputFrame(p.ID(), 2, 2, 256*14, 256*8),
92 inputFrame(p.ID(), 6, 1), // wheel source
93 inputFrame(p.ID(), 4, 3, 0, 256*5),
94 inputFrame(p.ID(), 9, 0, 120),
95 inputFrame(p.ID(), 3, 4, 4, 0x110, 1),
96 inputFrame(p.ID(), 5),
97 } {
98 inputSend(t, server, msg)
99 if err = d.Dispatch(); err != nil {
100 t.Fatal(err)
101 }
102 }
103 select {
104 case ev := <-w.events:
105 if ev.Kind != InputFrame || len(ev.Inputs) != 5 {
106 t.Fatalf("frame: %+v", ev)
107 }
108 if !ev.Inputs[0].Enter || ev.Inputs[0].Serial != 1 || ev.Inputs[0].PointerGen != w.pointerGen || ev.Inputs[0].X != 12 || ev.Inputs[1].X != 14 || ev.Inputs[2].DY != 5 || ev.Inputs[2].AxisSource != 1 || ev.Inputs[3].Value120 != 120 || ev.Inputs[4].Kind != "press" {
109 t.Fatalf("inputs: %+v", ev.Inputs)
110 }
111 default:
112 t.Fatal("missing pointer frame")
113 }
114}
115func TestKeyboardFDAndRepeatOverSocketpair(t *testing.T) {
116 ca, server := inputPair(t)
117 d, err := wlturbo.ConnectFromConn(ca)
118 if err != nil {
119 t.Fatal(err)
120 }
121 defer d.Close()
122 inputRequest(t, server)
123 ctx := d.Context()
124 seat := core.NewSeat(ctx)
125 if err = d.Registry().Bind(1, core.SeatInterface, 9, seat); err != nil {
126 t.Fatal(err)
127 }
128 inputRequest(t, server)
129 k, err := seat.GetKeyboard()
130 if err != nil {
131 t.Fatal(err)
132 }
133 inputRequest(t, server)
134 surface := core.NewSurface(ctx)
135 surface.SetID(30)
136 ctx.Register(surface)
137 w := &Window{Surface: surface, events: make(chan Event, 10), readerStop: make(chan struct{})}
138 w.watchKeyboard(k)
139 f, err := os.CreateTemp(t.TempDir(), "keymap")
140 if err != nil {
141 t.Fatal(err)
142 }
143 defer f.Close()
144 inputSend(t, server, inputFrame(k.ID(), 0, 0, 5), int(f.Fd()))
145 if err = d.Dispatch(); err != nil {
146 t.Fatal(err)
147 }
148 ev := <-w.events
149 if ev.Kind != InputKeymap || ev.FD < 0 {
150 t.Fatalf("keymap: %+v", ev)
151 }
152 adapter, err := NewInputAdapter("")
153 if err != nil {
154 t.Skipf("xkb unavailable: %v", err)
155 }
156 defer adapter.Close()
157 if _, err = adapter.ApplyInput(ev, time.Now()); err == nil {
158 t.Fatal("unsupported keymap accepted")
159 }
160 if err = unix.Fstat(ev.FD, &unix.Stat_t{}); err != unix.EBADF {
161 t.Fatalf("fd not closed: %v", err)
162 }
163 inputSend(t, server, inputFrame(k.ID(), 5, 0, 200))
164 if err = d.Dispatch(); err != nil {
165 t.Fatal(err)
166 }
167 if _, err = adapter.ApplyInput(<-w.events, time.Now()); err != nil {
168 t.Fatal(err)
169 }
170 if _, ok := adapter.NextRepeat(); ok {
171 t.Fatal("repeat should be disabled")
172 }
173}
174
175// Each keymap path transfers ownership to the adapter or the discarded-event
176// cleanup, including a valid XKB keymap and malformed compositor payloads.
177func TestKeymapOwnershipAndFocusRepeat(t *testing.T) {
178 adapter, err := NewInputAdapter("")
179 if err != nil {
180 t.Fatal(err)
181 }
182 defer adapter.Close()
183 data, err := os.ReadFile("../../keyboard/testdata/fr.xkb")
184 if err != nil {
185 t.Fatal(err)
186 }
187 for _, tc := range []struct {
188 name string
189 data []byte
190 format uint32
191 discard bool
192 good bool
193 }{
194 {"valid", append(data, 0), 1, false, true},
195 {"invalid", []byte("bad keymap\x00"), 1, false, false},
196 {"unsupported", append(data, 0), 2, false, false},
197 {"disconnect", append(data, 0), 1, true, false},
198 } {
199 t.Run(tc.name, func(t *testing.T) {
200 f, err := os.CreateTemp(t.TempDir(), "keymap")
201 if err != nil {
202 t.Fatal(err)
203 }
204 if _, err = f.Write(tc.data); err != nil {
205 t.Fatal(err)
206 }
207 // ApplyInput owns a duplicate, not the os.File descriptor.
208 fd, err := unix.Dup(int(f.Fd()))
209 if err != nil {
210 t.Fatal(err)
211 }
212 defer f.Close()
213 ev := Event{Kind: InputKeymap, FD: fd, Format: tc.format, Size: uint32(len(tc.data))}
214 if tc.discard {
215 CloseEventFD(ev)
216 } else {
217 _, err = adapter.ApplyInput(ev, time.Now())
218 if (err == nil) != tc.good {
219 t.Fatalf("apply: %v", err)
220 }
221 }
222 if err = unix.Fstat(ev.FD, &unix.Stat_t{}); err != unix.EBADF {
223 t.Fatalf("keymap FD still open: %v", err)
224 }
225 })
226 }
227 now := time.Now()
228 for _, ev := range []Event{{Kind: InputFocusIn, Serial: 42}, {Kind: InputRepeatInfo, Rate: 20, Delay: 100}, {Kind: InputKey, Code: 16, Pressed: true, Serial: 43}} {
229 if _, err := adapter.ApplyInput(ev, now); err != nil {
230 t.Fatal(err)
231 }
232 }
233 if _, ok := adapter.NextRepeat(); !ok {
234 t.Fatal("missing repeat")
235 }
236 if _, err := adapter.ApplyInput(Event{Kind: InputRepeatInfo, Rate: 0}, now); err != nil {
237 t.Fatal(err)
238 }
239 if _, ok := adapter.NextRepeat(); ok {
240 t.Fatal("rate zero retained repeat")
241 }
242 if _, err := adapter.ApplyInput(Event{Kind: InputRepeatInfo, Rate: 20, Delay: 100}, now); err != nil {
243 t.Fatal(err)
244 }
245 if _, err := adapter.ApplyInput(Event{Kind: InputKey, Code: 16, Pressed: true}, now); err != nil {
246 t.Fatal(err)
247 }
248 if _, ok := adapter.NextRepeat(); !ok {
249 t.Fatal("repeat not rearmed")
250 }
251 if _, err := adapter.ApplyInput(Event{Kind: InputFocusOut}, now); err != nil {
252 t.Fatal(err)
253 }
254 if _, ok := adapter.NextRepeat(); ok {
255 t.Fatal("keyboard leave retained repeat")
256 }
257
258 // Overflow reset cancels repeat from a discarded press but keeps focus.
259 w := &Window{inputAdapter: adapter}
260 for _, ev := range []Event{{Kind: InputFocusIn, Serial: 44}, {Kind: InputKey, Code: 16, Pressed: true, Serial: 45}} {
261 if _, err := adapter.ApplyInput(ev, now); err != nil {
262 t.Fatal(err)
263 }
264 }
265 for i := 0; i <= 4096; i++ {
266 w.queueInput([]Input{{Kind: "press"}})
267 }
268 if _, ok := adapter.NextRepeat(); ok {
269 t.Fatal("overflow reset retained repeat")
270 }
271 out, err := adapter.ApplyInput(Event{Kind: InputKey, Code: 16, Pressed: true, Serial: 46}, now)
272 if err != nil || len(out) != 1 {
273 t.Fatalf("key after reset: %v %+v", err, out)
274 }
275 if _, ok := adapter.NextRepeat(); !ok {
276 t.Fatal("focus lost by overflow reset")
277 }
278}
279
280func TestInputQueueOverflowResetAndCoalescing(t *testing.T) {
281 w := &Window{}
282 for i := 0; i < 4096; i++ {
283 w.queueInput([]Input{{Kind: "press", Button: uint32(i)}})
284 }
285 w.queueInput([]Input{{Kind: "release", Button: 7}})
286 got := w.DrainInput(nil)
287 if len(got) != 2 || got[0].Kind != InputReset || got[1].Kind != "release" || w.InputOverflow != 4096 {
288 t.Fatalf("reset: len=%d events=%+v overflows=%d", len(got), got, w.InputOverflow)
289 }
290 w.queueInput([]Input{{Kind: "motion", X: 1}})
291 for i := 1; i < 4096; i++ {
292 w.queueInput([]Input{{Kind: "press"}})
293 }
294 w.queueInput([]Input{{Kind: "focus-out"}})
295 got = w.DrainInput(nil)
296 if len(got) != 4096 || got[0].Kind != "press" || got[4095].Kind != "focus-out" || w.InputOverflow != 4097 {
297 t.Fatalf("eviction: len=%d first=%+v last=%+v overflows=%d", len(got), got[0], got[len(got)-1], w.InputOverflow)
298 }
299 w.queueInput([]Input{{Kind: "axis", Axis: 1, DY: 2}})
300 w.queueInput([]Input{{Kind: "axis", Axis: 1, DY: 3}})
301 for i := 2; i < 4096; i++ {
302 w.queueInput([]Input{{Kind: "press"}})
303 }
304 w.queueInput([]Input{{Kind: "release"}})
305 got = w.DrainInput(nil)
306 if len(got) != 4096 || got[0].Kind != "axis" || got[0].DY != 5 || got[4095].Kind != "release" || w.InputOverflow != 4097 {
307 t.Fatalf("axis coalescing: len=%d first=%+v last=%+v overflows=%d", len(got), got[0], got[len(got)-1], w.InputOverflow)
308 }
309 // Eviction skips enter motions; a reset keeps the latest one.
310 w.queueInput([]Input{{Kind: "motion", Enter: true, Serial: 5}})
311 for i := 1; i < 4096; i++ {
312 w.queueInput([]Input{{Kind: "press"}})
313 }
314 w.queueInput([]Input{{Kind: "release"}})
315 got = w.DrainInput(nil)
316 if len(got) != 3 || got[0].Kind != InputReset || !got[1].Enter || got[1].Serial != 5 || got[2].Kind != "release" {
317 t.Fatalf("enter across reset: len=%d first=%+v", len(got), got[:min(3, len(got))])
318 }
319}
320
321func TestApplySeatPointerKeyAndDrain(t *testing.T) {
322 w := &Window{}
323 data, err := os.ReadFile("../../keyboard/testdata/fr.xkb")
324 if err != nil {
325 t.Fatal(err)
326 }
327 f, err := os.CreateTemp(t.TempDir(), "keymap")
328 if err != nil {
329 t.Fatal(err)
330 }
331 if _, err = f.Write(append(data, 0)); err != nil {
332 t.Fatal(err)
333 }
334 // ApplyInput consumes the event FD; keep the os.File descriptor separate.
335 fd, err := unix.Dup(int(f.Fd()))
336 if err != nil {
337 t.Fatal(err)
338 }
339 defer f.Close()
340 for _, ev := range []Event{
341 {Kind: InputKeymap, FD: fd, Format: 1, Size: uint32(len(data) + 1)},
342 {Kind: InputFocusIn, Serial: 3},
343 {Kind: SeatCapabilities, Capabilities: 3},
344 {Kind: InputFrame, Inputs: []Input{{Kind: "motion", X: 4}, {Kind: "press", Button: 1}}},
345 {Kind: InputKey, Code: 16, Pressed: true, Serial: 4},
346 } {
347 if err := w.Apply(ev); err != nil {
348 t.Fatal(err)
349 }
350 }
351 defer w.inputAdapter.Close()
352 got := w.DrainInput(nil)
353 if len(got) != 4 || got[0].Kind != "focus-in" || got[1].Kind != "motion" || got[2].Kind != "press" || got[3].Kind != "key" {
354 t.Fatalf("drained: %+v", got)
355 }
356 if len(w.DrainInput(nil)) != 0 {
357 t.Fatal("queue not drained")
358 }
359}