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/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}

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 23

Tags 10

↑↓ moveenter openesc close