v0.2.0 13c6a90

outputapply_e2e_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"path/filepath"
6	"strings"
7	"testing"
8	"time"
9
10	wlr "github.com/bnema/go-wayland-bindings/server/wlroutputmanagement"
11	"github.com/bnema/neferwl/internal/adapters/config"
12	"github.com/bnema/neferwl/internal/ports"
13	"github.com/bnema/wlturbo"
14	client "github.com/bnema/wlturbo/protocol/outputmanagement"
15)
16
17// A wlr-output-management client goes through Wayland, the headless owner
18// loop and core: an accepted scale reaches the scene, a disable is refused.
19func TestHeadlessOutputManagementEndToEnd(t *testing.T) {
20	runtime := t.TempDir()
21	t.Setenv("XDG_RUNTIME_DIR", runtime)
22	scenes := make(chan []ports.Scene, 64)
23	ctx, cancel := context.WithCancel(context.Background())
24	done := make(chan error, 1)
25	go func() {
26		done <- Run(ctx, Options{Backend: "headless", NoXwayland: true, Config: config.Defaults(), NoTerminal: true, Sizes: [][2]int{{640, 480}}, testScenes: scenes})
27	}()
28	// A stuck loop never answers the client: the watchdog stops the app,
29	// which closes the socket, so a blocked Roundtrip fails instead of hanging.
30	watchdog := time.AfterFunc(10*time.Second, cancel)
31	exited := false
32	t.Cleanup(func() {
33		watchdog.Stop()
34		cancel()
35		if exited {
36			return
37		}
38		select {
39		case err := <-done:
40			if err != nil {
41				t.Errorf("Run: %v", err)
42			}
43		case <-time.After(3 * time.Second):
44			t.Error("Run did not stop")
45		}
46	})
47	// The first scene means the Wayland socket exists.
48	scene := func(want func(ports.Scene) bool) {
49		t.Helper()
50		deadline := time.After(5 * time.Second)
51		for {
52			select {
53			case set := <-scenes:
54				if len(set) == 1 && want(set[0]) {
55					return
56				}
57			case err := <-done:
58				exited = true
59				if err != nil && strings.Contains(strings.ToLower(err.Error()), "vulkan") {
60					t.Skipf("Vulkan unavailable: %v", err)
61				}
62				t.Fatalf("app exited early: %v", err)
63			case <-deadline:
64				t.Fatal("scene missing")
65			}
66		}
67	}
68	scene(func(s ports.Scene) bool { return s.Scale == 1 })
69	sockets, _ := filepath.Glob(filepath.Join(runtime, "wayland-*[0-9]"))
70	if len(sockets) != 1 {
71		t.Fatalf("sockets: %v", sockets)
72	}
73	c, err := wlturbo.Connect(sockets[0])
74	if err != nil {
75		t.Fatal(err)
76	}
77	t.Cleanup(func() { _ = c.Close() })
78	if err := c.Roundtrip(); err != nil {
79		t.Fatal(err)
80	}
81	manager := client.NewOutputManager(c.Context())
82	var heads []*client.OutputHead
83	var serial uint32
84	manager.OnHead(func(head *client.OutputHead) { heads = append(heads, head) })
85	manager.OnDone(func(value uint32) { serial = value })
86	bindTestClient(t, c, client.OutputManagerInterface, 4, manager)
87	roundtrip := func(until func() bool) {
88		t.Helper()
89		// The owner loops answer asynchronously: roundtrip until they do,
90		// or until the watchdog stops the app.
91		for !until() {
92			if err := c.Roundtrip(); err != nil {
93				t.Fatalf("no answer: %v", err)
94			}
95		}
96	}
97	// The inventory reaches Wayland through the owner loop's outbox.
98	roundtrip(func() bool { return serial != 0 && len(heads) == 1 })
99	head, initialSerial := heads[0], serial
100	configure := func(set func(*client.OutputConfiguration) *client.OutputConfigurationHead) int {
101		t.Helper()
102		// Inventory and core layout are independent startup publications. A
103		// done serial can become stale before create_configuration reaches the
104		// server. Only protocol cancellation is retryable, never a failed apply.
105		for attempt := 0; attempt < 3; attempt++ {
106			requested := serial
107			cfg, err := manager.CreateConfiguration(requested)
108			if err != nil {
109				t.Fatal(err)
110			}
111			result := -1
112			cfg.OnSucceeded(func() { result = int(wlr.ZwlrOutputConfigurationV1EventSucceeded) })
113			cfg.OnFailed(func() { result = int(wlr.ZwlrOutputConfigurationV1EventFailed) })
114			cfg.OnCancelled(func() { result = int(wlr.ZwlrOutputConfigurationV1EventCancelled) })
115			// A configuration cancelled on creation accepts only its destructor.
116			if err := c.Roundtrip(); err != nil {
117				t.Fatal(err)
118			}
119			var child *client.OutputConfigurationHead
120			if result < 0 {
121				child = set(cfg)
122				if err := cfg.Apply(); err != nil {
123					t.Fatal(err)
124				}
125				roundtrip(func() bool { return result >= 0 })
126			}
127			// Configuration heads have no destructor request of their own.
128			if err := cfg.Destroy(); err != nil {
129				t.Fatal(err)
130			}
131			if child != nil {
132				c.Context().Unregister(child)
133			}
134			if result != int(wlr.ZwlrOutputConfigurationV1EventCancelled) {
135				return result
136			}
137			if serial == requested {
138				t.Fatalf("configuration cancelled without a newer inventory serial: %d", requested)
139			}
140		}
141		t.Fatal("output inventory kept changing across three configurations")
142		return -1
143	}
144	got := configure(func(cfg *client.OutputConfiguration) *client.OutputConfigurationHead {
145		ch, err := cfg.EnableHead(head)
146		if err != nil {
147			t.Fatal(err)
148		}
149		if err := ch.SetScale(wlturbo.NewFixed(2)); err != nil {
150			t.Fatal(err)
151		}
152		return ch
153	})
154	if got != int(wlr.ZwlrOutputConfigurationV1EventSucceeded) {
155		t.Fatalf("scale 2: event %d", got)
156	}
157	scene(func(s ports.Scene) bool { return s.Scale == 2 })
158	roundtrip(func() bool { return serial != initialSerial })
159	got = configure(func(cfg *client.OutputConfiguration) *client.OutputConfigurationHead {
160		if err := cfg.DisableHead(head); err != nil {
161			t.Fatal(err)
162		}
163		return nil
164	})
165	if got != int(wlr.ZwlrOutputConfigurationV1EventFailed) {
166		t.Fatalf("disable: event %d", got)
167	}
168}

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