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/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 {"transform", func(t *testing.T, c *wlturbo.Display, ch uint32) {
247 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(1))
248 }},
249 {"adaptive sync off", func(t *testing.T, c *wlturbo.Display, ch uint32) {
250 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(0))
251 }},
252 } {
253 t.Run(tc.name, func(t *testing.T) {
254 s, _, apply, _, dir := outputTestServer(t)
255 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
256 t.Fatal("display stopped")
257 }
258 c := protocolClient(t, s, dir)
259 managerID := bindVersion(t, c, "zwlr_output_manager_v1", 4)
260 manager := &managementEvents{events: make(chan [2]uint32, 32), client: c}
261 manager.SetID(managerID)
262 registerWireProxy(c, manager)
263 if err := c.Roundtrip(); err != nil {
264 t.Fatal(err)
265 }
266 head := recvManagement(t, manager.events)
267 var done [2]uint32
268 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
269 done = recvManagement(t, manager.events)
270 }
271 cfg := c.AllocateID()
272 proxy := &managementEvents{events: make(chan [2]uint32, 8), client: c}
273 proxy.SetID(cfg)
274 registerWireProxy(c, proxy)
275 requestProtocol(t, c, managerID, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, done[1])
276 ch := c.AllocateID()
277 registerProtocol(t, c, ch)
278 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head[1])
279 tc.send(t, c, ch)
280 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
281 waitManagementEvent(t, c, proxy.events, uint32(wlr.ZwlrOutputConfigurationV1EventFailed))
282 select {
283 case req := <-apply:
284 t.Fatalf("unsupported setting reached app: %+v", req)
285 default:
286 }
287 })
288 }
289}
290
291// Each case uses a new connection because wl_display.error terminates that client.
292func TestOutputManagementProtocolErrors(t *testing.T) {
293 const (
294 enable = wlr.ZwlrOutputConfigurationV1RequestEnableHead
295 disable = wlr.ZwlrOutputConfigurationV1RequestDisableHead
296 apply = wlr.ZwlrOutputConfigurationV1RequestApply
297 custom = wlr.ZwlrOutputConfigurationHeadV1RequestSetCustomMode
298 )
299 cases := []struct {
300 name string
301 object string
302 code uint32
303 send func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32)
304 }{
305 {"already configured (enable)", "configuration", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
306 requestProtocol(t, c, cfg, enable, c.AllocateID(), head)
307 }},
308 {"already configured (disable)", "configuration", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
309 requestProtocol(t, c, cfg, disable, head)
310 }},
311 {"unconfigured head", "configuration", 2, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
312 requestProtocol(t, c, cfg, apply)
313 }},
314 {"already used (configuration)", "configuration", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
315 requestProtocol(t, c, cfg, apply)
316 requestProtocol(t, c, cfg, apply)
317 }},
318 {"already used (head)", "configuration", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
319 requestProtocol(t, c, cfg, apply)
320 requestProtocol(t, c, cfg, enable, c.AllocateID(), head)
321 }},
322 {"already set mode", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
323 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
324 requestProtocol(t, c, ch, custom, int32(800), int32(600), int32(60000))
325 }},
326 {"already set custom mode", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
327 requestProtocol(t, c, ch, custom, int32(800), int32(600), int32(60000))
328 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
329 }},
330 {"already set position", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
331 for range 2 {
332 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetPosition, int32(0), int32(0))
333 }
334 }},
335 {"already set transform", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
336 for range 2 {
337 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(0))
338 }
339 }},
340 {"already set scale", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
341 for range 2 {
342 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetScale, int32(256))
343 }
344 }},
345 {"already set adaptive sync", "head", 1, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
346 for range 2 {
347 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(0))
348 }
349 }},
350 {"invalid mode", "head", 2, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
351 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
352 }},
353 {"invalid custom mode", "head", 3, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
354 requestProtocol(t, c, ch, custom, int32(0), int32(600), int32(60000))
355 }},
356 {"invalid transform", "head", 4, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
357 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetTransform, int32(8))
358 }},
359 {"invalid scale", "head", 5, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
360 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetScale, int32(0))
361 }},
362 {"invalid adaptive sync state", "head", 6, func(t *testing.T, c *wlturbo.Display, cfg, ch, head, mode uint32) {
363 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetAdaptiveSync, uint32(2))
364 }},
365 }
366 for _, tc := range cases {
367 t.Run(tc.name, func(t *testing.T) {
368 s, _, _, _, dir := outputTestServer(t)
369 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
370 t.Fatal("display stopped")
371 }
372 c := protocolClient(t, s, dir)
373 managerID := bindVersion(t, c, "zwlr_output_manager_v1", 4)
374 p := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
375 p.SetID(managerID)
376 registerWireProxy(c, p)
377 if err := c.Roundtrip(); err != nil {
378 t.Fatal(err)
379 }
380 head := recvManagement(t, p.events)
381 if head[0] != wlr.ZwlrOutputManagerV1EventHead {
382 t.Fatalf("head: %v", head)
383 }
384 var done [2]uint32
385 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
386 done = recvManagement(t, p.events)
387 }
388 mode := <-p.modes
389 if tc.name == "invalid mode" {
390 other := bindVersion(t, c, "zwlr_output_manager_v1", 4)
391 otherEvents := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
392 otherEvents.SetID(other)
393 registerWireProxy(c, otherEvents)
394 if err := c.Roundtrip(); err != nil {
395 t.Fatal(err)
396 }
397 mode = <-otherEvents.modes
398 }
399 cfg := c.AllocateID()
400 registerProtocol(t, c, cfg)
401 requestProtocol(t, c, managerID, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, done[1])
402 var ch uint32
403 if tc.name != "unconfigured head" {
404 ch = c.AllocateID()
405 registerProtocol(t, c, ch)
406 requestProtocol(t, c, cfg, enable, ch, head[1])
407 }
408 if err := c.Roundtrip(); err != nil {
409 t.Fatal(err)
410 }
411 tc.send(t, c, cfg, ch, head[1], mode)
412 object := cfg
413 if tc.object == "head" {
414 object = ch
415 }
416 expectProtocolError(t, c, object, tc.code)
417 })
418 }
419}
420
421func TestOutputManagementInitialHeadState(t *testing.T) {
422 for _, enabled := range []bool{true, false} {
423 t.Run(map[bool]string{true: "enabled", false: "disabled"}[enabled], func(t *testing.T) {
424 s, _, _, _, dir := outputTestServer(t)
425 heads := testHead()
426 heads.Heads[0].Enabled = enabled
427 if !s.display.Do(func() {
428 s.setOutputHeads(heads)
429 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: heads.Heads[0].Info, X: 100, Scale: 2}}})
430 }) {
431 t.Fatal("display stopped")
432 }
433 c := protocolClient(t, s, dir)
434 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
435 p := &managementEvents{events: make(chan [2]uint32, 32), headEvents: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
436 p.SetID(id)
437 registerWireProxy(c, p)
438 if err := c.Roundtrip(); err != nil {
439 t.Fatal(err)
440 }
441 for ev := recvManagement(t, p.events); ev[0] != wlr.ZwlrOutputManagerV1EventDone; ev = recvManagement(t, p.events) {
442 }
443 mode := <-p.modes
444 found := map[uint32]bool{}
445 for len(p.headEvents) > 0 {
446 ev := <-p.headEvents
447 found[ev[0]] = true
448 if ev[0] == wlr.ZwlrOutputHeadV1EventEnabled && (ev[1] != 0) != enabled {
449 t.Fatalf("enabled event: %v", ev)
450 }
451 if ev[0] == wlr.ZwlrOutputHeadV1EventCurrentMode && ev[1] != mode {
452 t.Fatalf("current mode: %v, want %d", ev, mode)
453 }
454 }
455 if !found[wlr.ZwlrOutputHeadV1EventMode] || !found[wlr.ZwlrOutputHeadV1EventEnabled] || found[wlr.ZwlrOutputHeadV1EventCurrentMode] != enabled || found[wlr.ZwlrOutputHeadV1EventTransform] != enabled || found[wlr.ZwlrOutputHeadV1EventPosition] != enabled || found[wlr.ZwlrOutputHeadV1EventScale] != enabled {
456 t.Fatalf("head state: %v", found)
457 }
458 })
459 }
460}
461
462func TestOutputManagementStopDestroysManager(t *testing.T) {
463 s, _, _, _, dir := outputTestServer(t)
464 c := protocolClient(t, s, dir)
465 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
466 p := &managementEvents{events: make(chan [2]uint32, 8), client: c}
467 p.SetID(id)
468 registerWireProxy(c, p)
469 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestStop)
470 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputManagerV1EventDone)
471 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputManagerV1EventFinished)
472 if !s.display.Do(func() {
473 if len(s.outputManagers) != 0 {
474 t.Errorf("manager retained: %d", len(s.outputManagers))
475 }
476 }) {
477 t.Fatal("display stopped")
478 }
479}
480
481func TestOutputManagementReleasedModeNotReused(t *testing.T) {
482 s, _, _, _, dir := outputTestServer(t)
483 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
484 t.Fatal("display stopped")
485 }
486 c := protocolClient(t, s, dir)
487 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
488 p := &managementEvents{events: make(chan [2]uint32, 32), headEvents: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
489 p.SetID(id)
490 registerWireProxy(c, p)
491 if err := c.Roundtrip(); err != nil {
492 t.Fatal(err)
493 }
494 for ev := recvManagement(t, p.events); ev[0] != wlr.ZwlrOutputManagerV1EventDone; ev = recvManagement(t, p.events) {
495 }
496 mode := <-p.modes
497 for len(p.headEvents) > 0 {
498 <-p.headEvents
499 }
500 requestProtocol(t, c, mode, wlr.ZwlrOutputModeV1RequestRelease)
501 if err := c.Roundtrip(); err != nil {
502 t.Fatal(err)
503 }
504 if !s.display.Do(func() {
505 m := s.outputManagers[0]
506 h := m.heads["HEADLESS-1"]
507 if len(h.modes) != 0 || len(h.modeResources) != 0 {
508 t.Errorf("released mode retained: %d %d", len(h.modes), len(h.modeResources))
509 }
510 s.setOutputs(ports.SetOutputs{Outputs: ports.Layout{{Info: testHead().Heads[0].Info, X: 100, Scale: 1}}})
511 }) {
512 t.Fatal("display stopped")
513 }
514 if err := c.Roundtrip(); err != nil {
515 t.Fatal(err)
516 }
517 for len(p.headEvents) > 0 {
518 if ev := <-p.headEvents; ev[0] == wlr.ZwlrOutputHeadV1EventCurrentMode {
519 t.Fatalf("dead mode referenced: %v", ev)
520 }
521 }
522}
523
524func managementSetup(t *testing.T) (*Server, *wlturbo.Display, uint32, uint32, uint32, uint32) {
525 t.Helper()
526 s, _, _, _, dir := outputTestServer(t)
527 if !s.display.Do(func() { s.setOutputHeads(testHead()) }) {
528 t.Fatal("display stopped")
529 }
530 c := protocolClient(t, s, dir)
531 id := bindVersion(t, c, "zwlr_output_manager_v1", 4)
532 p := &managementEvents{events: make(chan [2]uint32, 32), modes: make(chan uint32, 4), client: c}
533 p.SetID(id)
534 registerWireProxy(c, p)
535 if err := c.Roundtrip(); err != nil {
536 t.Fatal(err)
537 }
538 head := recvManagement(t, p.events)[1]
539 var done [2]uint32
540 for done[0] != wlr.ZwlrOutputManagerV1EventDone {
541 done = recvManagement(t, p.events)
542 }
543 return s, c, id, head, <-p.modes, done[1]
544}
545
546func TestOutputManagementSetModeAfterHotplug(t *testing.T) {
547 s, c, id, head, mode, serial := managementSetup(t)
548 cfg := c.AllocateID()
549 registerProtocol(t, c, cfg)
550 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
551 ch := c.AllocateID()
552 registerProtocol(t, c, ch)
553 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
554 if err := c.Roundtrip(); err != nil {
555 t.Fatal(err)
556 }
557 if !s.display.Do(func() { s.setOutputHeads(ports.OutputHeads{}) }) {
558 t.Fatal("display stopped")
559 }
560 requestProtocol(t, c, ch, wlr.ZwlrOutputConfigurationHeadV1RequestSetMode, mode)
561 expectProtocolError(t, c, ch, uint32(wlr.ZwlrOutputConfigurationHeadV1ErrorInvalidMode))
562}
563
564func TestOutputManagementDestroyConfigurationChildren(t *testing.T) {
565 s, c, id, head, _, serial := managementSetup(t)
566 cfg := c.AllocateID()
567 registerProtocol(t, c, cfg)
568 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
569 ch := c.AllocateID()
570 registerProtocol(t, c, ch)
571 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
572 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
573 if err := c.Roundtrip(); err != nil {
574 t.Fatal(err)
575 }
576 var child *wlr.ZwlrOutputConfigurationHeadV1
577 if !s.display.Do(func() {
578 for _, pending := range s.outputReplies {
579 child = pending.heads["HEADLESS-1"].res
580 }
581 }) {
582 t.Fatal("display stopped")
583 }
584 if child == nil {
585 t.Fatal("no child resource")
586 }
587 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestDestroy)
588 retireWireObject(c, ch) // the parent destructor also destroys its configuration heads
589 if err := c.Roundtrip(); err != nil {
590 t.Fatal(err)
591 }
592 if !s.display.Do(func() {
593 if child.Alive() {
594 t.Error("configuration head survived parent destruction")
595 }
596 }) {
597 t.Fatal("display stopped")
598 }
599}
600
601func TestOutputManagementStaleConfigurationSingleTerminal(t *testing.T) {
602 _, c, id, _, _, serial := managementSetup(t)
603 cfg := c.AllocateID()
604 p := &managementEvents{events: make(chan [2]uint32, 8), client: c}
605 p.SetID(cfg)
606 registerWireProxy(c, p)
607 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial-1)
608 waitManagementEvent(t, c, p.events, wlr.ZwlrOutputConfigurationV1EventCancelled)
609 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
610 expectProtocolError(t, c, cfg, uint32(wlr.ZwlrOutputConfigurationV1ErrorAlreadyUsed))
611 select {
612 case ev := <-p.events:
613 t.Fatalf("second terminal event: %v", ev)
614 default:
615 }
616}
617
618func TestOutputManagementDestroyDropsPendingReply(t *testing.T) {
619 s, c, id, head, _, serial := managementSetup(t)
620 cfg := c.AllocateID()
621 registerProtocol(t, c, cfg)
622 requestProtocol(t, c, id, wlr.ZwlrOutputManagerV1RequestCreateConfiguration, cfg, serial)
623 ch := c.AllocateID()
624 registerProtocol(t, c, ch)
625 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestEnableHead, ch, head)
626 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestApply)
627 if err := c.Roundtrip(); err != nil {
628 t.Fatal(err)
629 }
630 if !s.display.Do(func() {
631 if len(s.outputReplies) != 1 {
632 t.Errorf("pending replies: %d", len(s.outputReplies))
633 }
634 }) {
635 t.Fatal("display stopped")
636 }
637 requestProtocol(t, c, cfg, wlr.ZwlrOutputConfigurationV1RequestDestroy)
638 retireWireObject(c, ch) // the parent destructor also destroys its configuration heads
639 if err := c.Roundtrip(); err != nil {
640 t.Fatal(err)
641 }
642 if !s.display.Do(func() {
643 if len(s.outputReplies) != 0 {
644 t.Errorf("orphan replies: %d", len(s.outputReplies))
645 }
646 }) {
647 t.Fatal("display stopped")
648 }
649}