1package wayland
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 wlr "github.com/bnema/go-wayland-bindings/server/wlroutputmanagement"
9 "github.com/bnema/neferwl/internal/adapters/logging"
10 "github.com/bnema/neferwl/internal/ports"
11 "github.com/bnema/wlturbo"
12)
13
14type managementEvents struct {
15 wlturbo.BaseProxy
16 events chan [2]uint32
17 client *wlturbo.Display
18 modes chan uint32
19 headEvents chan [2]uint32
20}
21
22func (p *managementEvents) Dispatch(e *wlturbo.Event) {
23 msg := [2]uint32{uint32(e.Opcode), 0}
24 if e.Opcode == uint16(wlr.ZwlrOutputManagerV1EventHead) || e.Opcode == uint16(wlr.ZwlrOutputManagerV1EventDone) {
25 msg[1] = e.Uint32()
26 }
27 if e.Opcode == uint16(wlr.ZwlrOutputManagerV1EventHead) {
28 h := &managementHeadEvents{client: p.client, modes: p.modes, events: p.headEvents}
29 h.SetID(msg[1])
30 registerWireProxy(p.client, h)
31 }
32 p.events <- msg
33}
34
35type managementHeadEvents struct {
36 wlturbo.BaseProxy
37 client *wlturbo.Display
38 modes chan uint32
39 events chan [2]uint32
40}
41
42func (p *managementHeadEvents) Dispatch(e *wlturbo.Event) {
43 if p.events != nil {
44 value := uint32(0)
45 switch uint32(e.Opcode) {
46 case wlr.ZwlrOutputHeadV1EventEnabled, wlr.ZwlrOutputHeadV1EventCurrentMode, wlr.ZwlrOutputHeadV1EventTransform:
47 value = e.Uint32()
48 }
49 p.events <- [2]uint32{uint32(e.Opcode), value}
50 }
51 if e.Opcode == uint16(wlr.ZwlrOutputHeadV1EventMode) {
52 mode := &protocolProxy{}
53 mode.SetID(e.Uint32())
54 registerWireProxy(p.client, mode)
55 if p.modes != nil {
56 p.modes <- mode.ID()
57 }
58 }
59}
60func outputTestServer(t *testing.T) (*Server, chan ports.OutputHeads, chan ports.OutputApply, chan ports.OutputApplied, string) {
61 t.Helper()
62 dir := t.TempDir()
63 heads := make(chan ports.OutputHeads, 8)
64 applies := make(chan ports.OutputApply, 8)
65 replies := make(chan ports.OutputApplied, 8)
66 s, err := New(Options{RuntimeDir: dir}, Channels{OutputHeads: heads, OutputApply: applies, OutputApplied: replies}, logging.For(context.Background(), "wayland"))
67 if err != nil {
68 t.Fatal(err)
69 }
70 ctx, cancel := context.WithCancel(context.Background())
71 done := make(chan error, 1)
72 go func() { done <- s.Run(ctx) }()
73 t.Cleanup(func() {
74 cancel()
75 if err := <-done; err != nil {
76 t.Error(err)
77 }
78 })
79 return s, heads, applies, replies, dir
80}
81func testHead() ports.OutputHeads {
82 mode := ports.OutputMode{Width: 1920, Height: 1080, RefreshMilli: 60000, Preferred: true}
83 return ports.OutputHeads{Heads: []ports.OutputHead{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080, RefreshMilli: 60000}, Modes: []ports.OutputMode{mode}, Current: &mode, Enabled: true}}}
84}
85func recvManagement(t *testing.T, ch <-chan [2]uint32) [2]uint32 {
86 t.Helper()
87 select {
88 case v := <-ch:
89 return v
90 case <-time.After(2 * time.Second):
91 t.Fatal("management event timeout")
92 return [2]uint32{}
93 }
94}
95func waitManagementEvent(t *testing.T, c *wlturbo.Display, ch <-chan [2]uint32, want uint32) {
96 t.Helper()
97 for i := 0; i < 12; i++ {
98 if err := c.Roundtrip(); err != nil {
99 t.Fatal(err)
100 }
101 select {
102 case ev := <-ch:
103 if ev[0] != want {
104 t.Fatalf("event %v want %d", ev, want)
105 }
106 return
107 default:
108 }
109 }
110 t.Fatal("missing management event")
111}
112func TestOutputManagementStaleSerialAndLayout(t *testing.T) {
113 s, _, _, _, dir := outputTestServer(t)
114 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
115 t.Fatal("display stopped")
116 }
117 c := protocolClient(t, s, dir)
118 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
119 manager := &managementEvents{events: make(chan [2]uint32, 32), client: c}
120 manager.SetID(id)
121 registerWireProxy(c, manager)
122 if err := c.Roundtrip(); err != nil {
123 t.Fatal(err)
124 }
125 var first [2]uint32
126 for first[0] != wlr.ZwlrOutputManagerV1EventDone {
127 first = recvManagement(t, manager.events)
128 }
129 if first[0] != wlr.ZwlrOutputManagerV1EventDone {
130 t.Fatalf("initial: %v", first)
131 }
132 if !s.display.Do(func() {
133 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: ports.OutputInfo{Name: "HEADLESS-1", Width: 1920, Height: 1080}, X: 100, Width: 1920, Height: 1080, Scale: 1}}})
134 }) {
135 t.Fatal("display stopped")
136 }
137 if err := c.Roundtrip(); err != nil {
138 t.Fatal(err)
139 }
140 second := recvManagement(t, manager.events)
141 if second[0] != wlr.ZwlrOutputManagerV1EventDone || second[1] == first[1] {
142 t.Fatalf("layout serial: %v -> %v", first, second)
143 }
144 configID := c.AllocateID()
145 proxy := &managementEvents{events: make(chan [2]uint32, 8), client: c}
146 proxy.SetID(configID)
147 registerWireProxy(c, proxy)
148 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, configID, first[1])
149 waitManagementEvent(t, c, proxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventCancelled))
150 if !s.display.Do(func() { s.setOutputHeads(ports.OutputHeads{}) }) {
151 t.Fatal("display stopped")
152 }
153 if err := c.Roundtrip(); err != nil {
154 t.Fatal(err)
155 }
156 third := recvManagement(t, manager.events)
157 if third[0] != wlr.ZwlrOutputManagerV1EventDone || third[1] == second[1] {
158 t.Fatalf("inventory serial: %v -> %v", second, third)
159 }
160}
161
162func TestOutputManagementHeadsAndApply(t *testing.T) {
163 s, heads, apply, replies, dir := outputTestServer(t)
164 heads <- testHead()
165 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
166 t.Fatal("display stopped")
167 }
168 c := protocolClient(t, s, dir)
169 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
170 manager := &managementEvents{events: make(chan [2]uint32, 32), client: c}
171 manager.SetID(id)
172 registerWireProxy(c, manager)
173 if err := c.Roundtrip(); err != nil {
174 t.Fatal(err)
175 }
176 head := recvManagement(t, manager.events)
177 if head[0] != wlr.ZwlrOutputManagerV1EventHead {
178 t.Fatalf("head event: %v", head)
179 }
180 var serial [2]uint32
181 for serial[0] != wlr.ZwlrOutputManagerV1EventDone {
182 serial = recvManagement(t, manager.events)
183 }
184 if serial[1] == 0 {
185 t.Fatalf("done: %v", serial)
186 }
187 configID := c.AllocateID()
188 configProxy := &managementEvents{events: make(chan [2]uint32, 8), client: c}
189 configProxy.SetID(configID)
190 registerWireProxy(c, configProxy)
191 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, configID, serial[1])
192 chID := c.AllocateID()
193 registerProtocol(t, c, chID)
194 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestEnableHead, chID, head[1])
195 requestProtocol(t, c, chID, wlr.ZwlrOutputConfigurationHeadV1RequestSetPosition, int32(100), int32(0))
196 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestTest)
197 if err := c.Roundtrip(); err != nil {
198 t.Fatal(err)
199 }
200 var req ports.OutputApply
201 select {
202 case req = <-apply:
203 case <-time.After(time.Second):
204 t.Fatal("no test request")
205 }
206 if !req.Test || len(req.Heads) != 1 || req.Heads[0].Pos.X != 100 {
207 t.Fatalf("test: %+v", req)
208 }
209 replies <- ports.OutputApplied{ID: req.ID}
210 waitManagementEvent(t, c, configProxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventSucceeded))
211 configID = c.AllocateID()
212 configProxy = &managementEvents{events: make(chan [2]uint32, 8), client: c}
213 configProxy.SetID(configID)
214 registerWireProxy(c, configProxy)
215 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, configID, serial[1])
216 chID = c.AllocateID()
217 registerProtocol(t, c, chID)
218 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestEnableHead, chID, head[1])
219 requestProtocol(t, c, chID, wlr.ZwlrOutputConfigurationHeadV1RequestSetScale, int32(512))
220 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestApply)
221 if err := c.Roundtrip(); err != nil {
222 t.Fatal(err)
223 }
224 select {
225 case req = <-apply:
226 case <-time.After(time.Second):
227 t.Fatal("no apply request")
228 }
229 if req.Test || req.Heads[0].Scale != 2 {
230 t.Fatalf("apply: %+v", req)
231 }
232 replies <- ports.OutputApplied{ID: req.ID}
233 waitManagementEvent(t, c, configProxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventSucceeded))
234}
235
236// Unsupported but well-formed settings fail the configuration, not the client,
237// and must never be sent across the app port.
238func TestOutputManagementUnsupportedSettings(t *testing.T) {
239 for _, tc := range []struct {
240 name string
241 send func(*testing.T, *wlturbo.Display, uint32)
242 }{
243 {"custom mode", func(t *testing.T, c *wlturbo.Display, ch uint32) {
244 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetCustomMode, int32(800), int32(600), int32(60000))
245 }},
246 {"adaptive sync off", func(t *testing.T, c *wlturbo.Display, ch uint32) {
247 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(0))
248 }},
249 } {
250 t.Run(tc.name, func(t *testing.T) {
251 s, _, apply, _, dir := outputTestServer(t)
252 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
253 t.Fatal("display stopped")
254 }
255 c := protocolClient(t, s, dir)
256 managerID := bindVersion(t, c, "zwlr_output_manager_v1", 4)
257 manager := &managementEvents{events: make(chan [2]uint32, 32), client: c}
258 manager.SetID(managerID)
259 registerWireProxy(c, manager)
260 if err := c.Roundtrip(); err != nil {
261 t.Fatal(err)
262 }
263 head := recvManagement(t, manager.events)
264 var done [2]uint32
265 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
266 done = recvManagement(t, manager.events)
267 }
268 cfg := c.AllocateID()
269 proxy := &managementEvents{events: make(chan [2]uint32, 8), client: c}
270 proxy.SetID(cfg)
271 registerWireProxy(c, proxy)
272 requestProtocol(t, c, managerID, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, done[1])
273 ch := c.AllocateID()
274 registerProtocol(t, c, ch)
275 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head[1])
276 tc.send(t, c, ch)
277 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
278 waitManagementEvent(t, c, proxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventFailed))
279 select {
280 case req := <-apply:
281 t.Fatalf("unsupported setting reached app: %+v", req)
282 default:
283 }
284 })
285 }
286}
287
288// set_transform reaches the backend, and heads report the placement's
289// transform, at bind time and when it changes.
290func TestOutputManagementTransform(t *testing.T) {
291 s, _, apply, replies, dir := outputTestServer(t)
292 heads := testHead()
293 if !s.display.Do(func() {
294 s.setOutputHeads(heads)
295 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: heads.Heads[0].Info, Scale: 1, Transform: 1}}})
296 }) {
297 t.Fatal("display stopped")
298 }
299 c := protocolClient(t, s, dir)
300 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
301 p := &managementEvents{events: make(chan [2]uint32, 32), headEvents: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
302 p.SetID(id)
303 registerWireProxy(c, p)
304 if err := c.Roundtrip(); err != nil {
305 t.Fatal(err)
306 }
307 var head, serial [2]uint32
308 for ev := recvManagement(t, p.events); ; ev = recvManagement(t, p.events) {
309 if ev[0] == wlr.ZwlrOutputManagerV1EventHead {
310 head = ev
311 }
312 if ev[0] == wlr.ZwlrOutputManagerV1EventDone {
313 serial = ev
314 break
315 }
316 }
317 transform := func() (uint32, bool) {
318 v, ok := uint32(0), false
319 for len(p.headEvents) > 0 {
320 if ev := <-p.headEvents; ev[0] == wlr.ZwlrOutputHeadV1EventTransform {
321 v, ok = ev[1], true
322 }
323 }
324 return v, ok
325 }
326 if v, ok := transform(); !ok || v != 1 {
327 t.Fatalf("initial transform: %d, %v", v, ok)
328 }
329 configID := c.AllocateID()
330 configProxy := &managementEvents{events: make(chan [2]uint32, 8), client: c}
331 configProxy.SetID(configID)
332 registerWireProxy(c, configProxy)
333 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, configID, serial[1])
334 chID := c.AllocateID()
335 registerProtocol(t, c, chID)
336 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestEnableHead, chID, head[1])
337 requestProtocol(t, c, chID, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(3))
338 requestProtocol(t, c, configID, wlr.ZwlrOutputConfigurationV1RequestApply)
339 if err := c.Roundtrip(); err != nil {
340 t.Fatal(err)
341 }
342 var req ports.OutputApply
343 select {
344 case req = <-apply:
345 case <-time.After(time.Second):
346 t.Fatal("no apply request")
347 }
348 if len(req.Heads) != 1 || req.Heads[0].Transform == nil || *req.Heads[0].Transform != 3 {
349 t.Fatalf("apply: %+v", req)
350 }
351 replies <- ports.OutputApplied{ID: req.ID}
352 waitManagementEvent(t, c, configProxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventSucceeded))
353 if !s.display.Do(func() {
354 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: heads.Heads[0].Info, Scale: 1, Transform: 3}}})
355 s.setOutputHeads(heads)
356 }) {
357 t.Fatal("display stopped")
358 }
359 if err := c.Roundtrip(); err != nil {
360 t.Fatal(err)
361 }
362 if v, ok := transform(); !ok || v != 3 {
363 t.Fatalf("updated transform: %d, %v", v, ok)
364 }
365}
366
367// Each case uses a new connection because wl_display.error terminates that client.
368func TestOutputManagementProtocolErrors(t *testing.T) {
369 const (
370 enable = wlr.ZwlrOutputConfigurationV1RequestEnableHead
371 disable = wlr.ZwlrOutputConfigurationV1RequestDisableHead
372 apply = wlr.ZwlrOutputConfigurationV1RequestApply
373 custom = wlr.ZwlrOutputConfigurationHeadV1RequestSetCustomMode
374 )
375 cases := []struct {
376 name string
377 object string
378 code uint32
379 send func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32)
380 }{
381 {"already configured (enable)", "configuration", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
382 requestProtocol(t, c, cfg, enable, c.AllocateID(), head)
383 }},
384 {"already configured (disable)", "configuration", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
385 requestProtocol(t, c, cfg, disable, head)
386 }},
387 {"unconfigured head", "configuration", 2, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
388 requestProtocol(t, c, cfg, apply)
389 }},
390 {"already used (configuration)", "configuration", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
391 requestProtocol(t, c, cfg, apply)
392 requestProtocol(t, c, cfg, apply)
393 }},
394 {"already used (head)", "configuration", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
395 requestProtocol(t, c, cfg, apply)
396 requestProtocol(t, c, cfg, enable, c.AllocateID(), head)
397 }},
398 {"already set mode", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
399 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
400 requestProtocol(t, c, ch, custom, int32(800), int32(600), int32(60000))
401 }},
402 {"already set custom mode", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
403 requestProtocol(t, c, ch, custom, int32(800), int32(600), int32(60000))
404 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
405 }},
406 {"already set position", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
407 for range 2 {
408 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetPosition, int32(0), int32(0))
409 }
410 }},
411 {"already set transform", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
412 for range 2 {
413 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(0))
414 }
415 }},
416 {"already set scale", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
417 for range 2 {
418 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetScale, int32(256))
419 }
420 }},
421 {"already set adaptive sync", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
422 for range 2 {
423 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(0))
424 }
425 }},
426 {"invalid mode", "head", 2, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
427 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
428 }},
429 {"invalid custom mode", "head", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
430 requestProtocol(t, c, ch, custom, int32(0), int32(600), int32(60000))
431 }},
432 {"invalid transform", "head", 4, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
433 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(8))
434 }},
435 {"invalid scale", "head", 5, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
436 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetScale, int32(0))
437 }},
438 {"invalid adaptive sync state", "head", 6, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
439 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(2))
440 }},
441 }
442 for _, tc := range cases {
443 t.Run(tc.name, func(t *testing.T) {
444 s, _, _, _, dir := outputTestServer(t)
445 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
446 t.Fatal("display stopped")
447 }
448 c := protocolClient(t, s, dir)
449 managerID := bindVersion(t, c, "zwlr_output_manager_v1", 4)
450 p := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
451 p.SetID(managerID)
452 registerWireProxy(c, p)
453 if err := c.Roundtrip(); err != nil {
454 t.Fatal(err)
455 }
456 head := recvManagement(t, p.events)
457 if head[0] != wlr.ZwlrOutputManagerV1EventHead {
458 t.Fatalf("head: %v", head)
459 }
460 var done [2]uint32
461 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
462 done = recvManagement(t, p.events)
463 }
464 mode := <-p.modes
465 if tc.name == "invalid mode" {
466 other := bindVersion(t, c, "zwlr_output_manager_v1", 4)
467 otherEvents := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
468 otherEvents.SetID(other)
469 registerWireProxy(c, otherEvents)
470 if err := c.Roundtrip(); err != nil {
471 t.Fatal(err)
472 }
473 mode = <-otherEvents.modes
474 }
475 cfg := c.AllocateID()
476 registerProtocol(t, c, cfg)
477 requestProtocol(t, c, managerID, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, done[1])
478 var ch uint32
479 if tc.name != "unconfigured head" {
480 ch = c.AllocateID()
481 registerProtocol(t, c, ch)
482 requestProtocol(t, c, cfg, enable, ch, head[1])
483 }
484 if err := c.Roundtrip(); err != nil {
485 t.Fatal(err)
486 }
487 tc.send(t, c, cfg, ch, head[1], mode)
488 object := cfg
489 if tc.object == "head" {
490 object = ch
491 }
492 expectProtocolError(t, c, object, tc.code)
493 })
494 }
495}
496
497func TestOutputManagementInitialHeadState(t *testing.T) {
498 for _, enabled := range []bool{true, false} {
499 t.Run(map[bool]string{true: "enabled", false: "disabled"}[enabled], func(t *testing.T) {
500 s, _, _, _, dir := outputTestServer(t)
501 heads := testHead()
502 heads.Heads[0].Enabled = enabled
503 if !s.display.Do(func() {
504 s.setOutputHeads(heads)
505 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: heads.Heads[0].Info, X: 100, Scale: 2}}})
506 }) {
507 t.Fatal("display stopped")
508 }
509 c := protocolClient(t, s, dir)
510 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
511 p := &managementEvents{events: make(chan [2]uint32, 32), headEvents: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
512 p.SetID(id)
513 registerWireProxy(c, p)
514 if err := c.Roundtrip(); err != nil {
515 t.Fatal(err)
516 }
517 for ev := recvManagement(t, p.events); ev[0] != wlr.ZwlrOutputManagerV1EventDone; ev = recvManagement(t, p.events) {
518 }
519 mode := <-p.modes
520 found := map[uint32]bool{}
521 for len(p.headEvents) > 0 {
522 ev := <-p.headEvents
523 found[ev[0]] = true
524 if ev[0] == wlr.ZwlrOutputHeadV1EventEnabled && (ev[1] != 0) != enabled {
525 t.Fatalf("enabled event: %v", ev)
526 }
527 if ev[0] == wlr.ZwlrOutputHeadV1EventCurrentMode && ev[1] != mode {
528 t.Fatalf("current mode: %v, want %d", ev, mode)
529 }
530 }
531 if !found[wlr.ZwlrOutputHeadV1EventMode] || !found[wlr.ZwlrOutputHeadV1EventEnabled] || found[wlr.ZwlrOutputHeadV1EventCurrentMode] != enabled || found[wlr.ZwlrOutputHeadV1EventTransform] != enabled || found[wlr.ZwlrOutputHeadV1EventPosition] != enabled || found[wlr.ZwlrOutputHeadV1EventScale] != enabled {
532 t.Fatalf("head state: %v", found)
533 }
534 })
535 }
536}
537
538func TestOutputManagementStopDestroysManager(t *testing.T) {
539 s, _, _, _, dir := outputTestServer(t)
540 c := protocolClient(t, s, dir)
541 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
542 p := &managementEvents{events: make(chan [2]uint32, 8), client: c}
543 p.SetID(id)
544 registerWireProxy(c, p)
545 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestStop)
546 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputManagerV1EventDone)
547 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputManagerV1EventFinished)
548 if !s.display.Do(func() {
549 if len(s.outputManagers) != 0 {
550 t.Errorf("manager retained: %d", len(s.outputManagers))
551 }
552 }) {
553 t.Fatal("display stopped")
554 }
555}
556
557func TestOutputManagementReleasedModeNotReused(t *testing.T) {
558 s, _, _, _, dir := outputTestServer(t)
559 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
560 t.Fatal("display stopped")
561 }
562 c := protocolClient(t, s, dir)
563 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
564 p := &managementEvents{events: make(chan [2]uint32, 32), headEvents: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
565 p.SetID(id)
566 registerWireProxy(c, p)
567 if err := c.Roundtrip(); err != nil {
568 t.Fatal(err)
569 }
570 for ev := recvManagement(t, p.events); ev[0] != wlr.ZwlrOutputManagerV1EventDone; ev = recvManagement(t, p.events) {
571 }
572 mode := <-p.modes
573 for len(p.headEvents) > 0 {
574 <-p.headEvents
575 }
576 requestProtocol(t, c, mode, wlr.ZwlrOutputModeV1RequestRelease)
577 if err := c.Roundtrip(); err != nil {
578 t.Fatal(err)
579 }
580 if !s.display.Do(func() {
581 m := s.outputManagers[0]
582 h := m.heads["HEADLESS-1"]
583 if len(h.modes) != 0 || len(h.modeResources) != 0 {
584 t.Errorf("released mode retained: %d %d", len(h.modes), len(h.modeResources))
585 }
586 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: testHead().Heads[0].Info, X: 100, Scale: 1}}})
587 }) {
588 t.Fatal("display stopped")
589 }
590 if err := c.Roundtrip(); err != nil {
591 t.Fatal(err)
592 }
593 for len(p.headEvents) > 0 {
594 if ev := <-p.headEvents; ev[0] == wlr.ZwlrOutputHeadV1EventCurrentMode {
595 t.Fatalf("dead mode referenced: %v", ev)
596 }
597 }
598}
599
600func managementSetup(t *testing.T) (*Server, *wlturbo.Display, uint32, uint32, uint32, uint32) {
601 t.Helper()
602 s, _, _, _, dir := outputTestServer(t)
603 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
604 t.Fatal("display stopped")
605 }
606 c := protocolClient(t, s, dir)
607 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
608 p := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
609 p.SetID(id)
610 registerWireProxy(c, p)
611 if err := c.Roundtrip(); err != nil {
612 t.Fatal(err)
613 }
614 head := recvManagement(t, p.events)[1]
615 var done [2]uint32
616 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
617 done = recvManagement(t, p.events)
618 }
619 return s, c, id, head, <-p.modes, done[1]
620}
621
622func TestOutputManagementSetModeAfterHotplug(t *testing.T) {
623 s, c, id, head, mode, serial := managementSetup(t)
624 cfg := c.AllocateID()
625 registerProtocol(t, c, cfg)
626 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
627 ch := c.AllocateID()
628 registerProtocol(t, c, ch)
629 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
630 if err := c.Roundtrip(); err != nil {
631 t.Fatal(err)
632 }
633 if !s.display.Do(func() { s.setOutputHeads(ports.OutputHeads{}) }) {
634 t.Fatal("display stopped")
635 }
636 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
637 expectProtocolError(t, c, ch, uint32(wlr.ZwlrOutputConfigurationHeadV1ErrorInvalidMode))
638}
639
640func TestOutputManagementDestroyConfigurationChildren(t *testing.T) {
641 s, c, id, head, _, serial := managementSetup(t)
642 cfg := c.AllocateID()
643 registerProtocol(t, c, cfg)
644 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
645 ch := c.AllocateID()
646 registerProtocol(t, c, ch)
647 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
648 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
649 if err := c.Roundtrip(); err != nil {
650 t.Fatal(err)
651 }
652 var child *wlr.ZwlrOutputConfigurationHeadV1
653 if !s.display.Do(func() {
654 for _, pending := range s.outputReplies {
655 child = pending.heads["HEADLESS-1"].res
656 }
657 }) {
658 t.Fatal("display stopped")
659 }
660 if child == nil {
661 t.Fatal("no child resource")
662 }
663 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestDestroy)
664 retireWireObject(c, ch) // the parent destructor also destroys its configuration heads
665 if err := c.Roundtrip(); err != nil {
666 t.Fatal(err)
667 }
668 if !s.display.Do(func() {
669 if child.Alive() {
670 t.Error("configuration head survived parent destruction")
671 }
672 }) {
673 t.Fatal("display stopped")
674 }
675}
676
677func TestOutputManagementStaleConfigurationSingleTerminal(t *testing.T) {
678 _, c, id, _, _, serial := managementSetup(t)
679 cfg := c.AllocateID()
680 p := &managementEvents{events: make(chan [2]uint32, 8), client: c}
681 p.SetID(cfg)
682 registerWireProxy(c, p)
683 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial-1)
684 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputConfigurationV1EventCancelled)
685 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
686 expectProtocolError(t, c, cfg, uint32(wlr.ZwlrOutputConfigurationV1ErrorAlreadyUsed))
687 select {
688 case ev := <-p.events:
689 t.Fatalf("second terminal event: %v", ev)
690 default:
691 }
692}
693
694func TestOutputManagementDestroyDropsPendingReply(t *testing.T) {
695 s, c, id, head, _, serial := managementSetup(t)
696 cfg := c.AllocateID()
697 registerProtocol(t, c, cfg)
698 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
699 ch := c.AllocateID()
700 registerProtocol(t, c, ch)
701 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
702 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
703 if err := c.Roundtrip(); err != nil {
704 t.Fatal(err)
705 }
706 if !s.display.Do(func() {
707 if len(s.outputReplies) != 1 {
708 t.Errorf("pending replies: %d", len(s.outputReplies))
709 }
710 }) {
711 t.Fatal("display stopped")
712 }
713 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestDestroy)
714 retireWireObject(c, ch) // the parent destructor also destroys its configuration heads
715 if err := c.Roundtrip(); err != nil {
716 t.Fatal(err)
717 }
718 if !s.display.Do(func() {
719 if len(s.outputReplies) != 0 {
720 t.Errorf("orphan replies: %d", len(s.outputReplies))
721 }
722 }) {
723 t.Fatal("display stopped")
724 }
725}