v0.4.0 b6be3a8

outputmanagement_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 25

Tags 10

↑↓ moveenter openesc close