refactor/kvconf 3462c76

xwayland_test.go RawHistoryFolder

1package xwayland
2
3import (
4	"context"
5	"errors"
6	"os"
7	"path/filepath"
8	"strconv"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/bnema/neferwl/internal/adapters/logging"
14	"golang.org/x/sys/unix"
15)
16
17func testOptions(t *testing.T, binary string) Options {
18	t.Helper()
19	tmp := t.TempDir()
20	if err := os.Chmod(tmp, 0o1777); err != nil {
21		t.Fatal(err)
22	}
23	return Options{Binary: binary, Dir: filepath.Join(tmp, ".X11-unix"), TmpDir: tmp, Log: logging.For(context.Background(), "xwayland")}
24}
25
26func dial(t *testing.T, path string) int {
27	t.Helper()
28	fd, err := unix.Socket(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
29	if err != nil {
30		t.Fatal(err)
31	}
32	if err := unix.Connect(fd, &unix.SockaddrUnix{Name: path}); err != nil {
33		t.Fatal(err)
34	}
35	return fd
36}
37
38func TestOpenPicksFreeDisplay(t *testing.T) {
39	opts := testOptions(t, "true")
40	if err := os.WriteFile(filepath.Join(opts.TmpDir, ".X0-lock"), nil, 0o444); err != nil {
41		t.Fatal(err)
42	}
43	d, err := Open(opts)
44	if err != nil {
45		t.Fatal(err)
46	}
47	if d.Name() != ":1" {
48		t.Fatalf("display %s", d.Name())
49	}
50	lock, err := os.ReadFile(filepath.Join(opts.TmpDir, ".X1-lock"))
51	if err != nil || strings.TrimSpace(string(lock)) != strconv.Itoa(os.Getpid()) {
52		t.Fatalf("lock %q %v", lock, err)
53	}
54	st, err := os.Stat(opts.Dir)
55	if err != nil || st.Mode()&os.ModeSticky == 0 || st.Mode().Perm() != 0o777 {
56		t.Fatalf("dir %v %v", st.Mode(), err)
57	}
58	d.Close()
59	for _, p := range []string{filepath.Join(opts.Dir, "X1"), filepath.Join(opts.TmpDir, ".X1-lock")} {
60		if _, err := os.Stat(p); !os.IsNotExist(err) {
61			t.Fatalf("%s left behind: %v", p, err)
62		}
63	}
64}
65
66// A satellite that exits without accepting drops the waiting client and is
67// not respawned until another client connects.
68func TestRunDropsClientsOfFailedSatellite(t *testing.T) {
69	d, err := Open(testOptions(t, "false"))
70	if err != nil {
71		t.Fatal(err)
72	}
73	defer d.Close()
74	ctx, cancel := context.WithCancel(context.Background())
75	done := make(chan error, 1)
76	go func() { done <- d.Run(ctx) }()
77	fd := dial(t, d.socket)
78	defer unix.Close(fd)
79	// The dropped connection reads EOF.
80	if err := unix.SetsockoptTimeval(fd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
81		t.Fatal(err)
82	}
83	// Go runtime signals (preemption) can interrupt the raw read: retry.
84	n, err := unix.Read(fd, make([]byte, 1))
85	for errors.Is(err, unix.EINTR) {
86		n, err = unix.Read(fd, make([]byte, 1))
87	}
88	if n != 0 || err != nil {
89		t.Fatalf("read %d %v, want EOF", n, err)
90	}
91	cancel()
92	select {
93	case err := <-done:
94		if err != nil {
95			t.Fatal(err)
96		}
97	case <-time.After(3 * time.Second):
98		t.Fatal("Run did not stop")
99	}
100}
101
102// satelliteScript writes a real script that records each run and whether it
103// received listening sockets on fds 3 and 4, then exits.
104func satelliteScript(t *testing.T, dir string) (string, string) {
105	t.Helper()
106	runs := filepath.Join(dir, "runs")
107	bin := filepath.Join(dir, "satellite")
108	script := "#!/bin/sh\n[ -S /proc/self/fd/3 ] && [ -S /proc/self/fd/4 ] && echo \"$*\" >> " + runs + "\n"
109	if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
110		t.Fatal(err)
111	}
112	return bin, runs
113}
114
115func readRuns(t *testing.T, path string, want int) []string {
116	t.Helper()
117	deadline := time.Now().Add(3 * time.Second)
118	for {
119		b, _ := os.ReadFile(path)
120		lines := strings.Fields(strings.ReplaceAll(string(b), " ", "_"))
121		if len(lines) >= want || time.Now().After(deadline) {
122			return lines
123		}
124		time.Sleep(20 * time.Millisecond)
125	}
126}
127
128// Each connection starts the satellite once, with both sockets handed over.
129func TestRunStartsSatelliteOnConnect(t *testing.T) {
130	opts := testOptions(t, "")
131	var runs string
132	opts.Binary, runs = satelliteScript(t, opts.TmpDir)
133	opts.Abstract = true
134	d, err := Open(opts)
135	if err != nil {
136		t.Fatal(err)
137	}
138	defer d.Close()
139	ctx, cancel := context.WithCancel(context.Background())
140	done := make(chan error, 1)
141	go func() { done <- d.Run(ctx) }()
142	time.Sleep(100 * time.Millisecond)
143	if got := readRuns(t, runs, 0); len(got) != 0 {
144		t.Fatalf("started without a client: %v", got)
145	}
146	unix.Close(dial(t, d.socket))
147	want := d.Name() + "_-listenfd_3_-listenfd_4"
148	if got := readRuns(t, runs, 1); len(got) != 1 || got[0] != want {
149		t.Fatalf("runs %v, want [%s]", got, want)
150	}
151	// The next client, after the backoff, starts it again.
152	time.Sleep(1200 * time.Millisecond)
153	if got := readRuns(t, runs, 1); len(got) != 1 {
154		t.Fatalf("respawned without a client: %v", got)
155	}
156	unix.Close(dial(t, "\x00"+d.socket))
157	if got := readRuns(t, runs, 2); len(got) != 2 {
158		t.Fatalf("runs %v after the second client", got)
159	}
160	cancel()
161	if err := <-done; err != nil {
162		t.Fatal(err)
163	}
164}
165
166func TestStaleLockReclaimed(t *testing.T) {
167	opts := testOptions(t, "true")
168	// PID 0x7ffffff0 does not exist.
169	if err := os.WriteFile(filepath.Join(opts.TmpDir, ".X0-lock"), []byte("2147483632\n"), 0o444); err != nil {
170		t.Fatal(err)
171	}
172	// A live process keeps its display.
173	if err := os.WriteFile(filepath.Join(opts.TmpDir, ".X1-lock"), []byte(strconv.Itoa(os.Getpid())), 0o444); err != nil {
174		t.Fatal(err)
175	}
176	d, err := Open(opts)
177	if err != nil {
178		t.Fatal(err)
179	}
180	defer d.Close()
181	if d.Name() != ":0" {
182		t.Fatalf("display %s", d.Name())
183	}
184	if b, _ := os.ReadFile(d.lock); strings.TrimSpace(string(b)) != strconv.Itoa(os.Getpid()) {
185		t.Fatalf("lock holds %q", b)
186	}
187}
188
189func TestDirChecks(t *testing.T) {
190	for name, setup := range map[string]func(dir string) error{
191		"not sticky": func(dir string) error { return os.Mkdir(dir, 0o777) },
192		"symlink": func(dir string) error {
193			target := dir + "-target"
194			if err := os.Mkdir(target, 0o1777); err != nil {
195				return err
196			}
197			return os.Symlink(target, dir)
198		},
199		"file": func(dir string) error { return os.WriteFile(dir, nil, 0o777) },
200	} {
201		t.Run(name, func(t *testing.T) {
202			opts := testOptions(t, "true")
203			if err := setup(opts.Dir); err != nil {
204				t.Fatal(err)
205			}
206			_ = os.Chmod(opts.Dir, 0o777|os.ModeSticky)
207			if name == "not sticky" {
208				_ = os.Chmod(opts.Dir, 0o777)
209			}
210			if _, err := Open(opts); err == nil {
211				t.Fatal("opened an unsafe X11 directory")
212			}
213		})
214	}
215}
216
217func TestSupported(t *testing.T) {
218	if !Supported(context.Background(), "true") || Supported(context.Background(), "false") {
219		t.Fatal("exit status not honoured")
220	}
221	hang := filepath.Join(t.TempDir(), "hang")
222	if err := os.WriteFile(hang, []byte("#!/bin/sh\nexec sleep 10\n"), 0o755); err != nil {
223		t.Fatal(err)
224	}
225	start := time.Now()
226	if Supported(context.Background(), hang) || time.Since(start) > 3*time.Second {
227		t.Fatalf("hanging binary: %v", time.Since(start))
228	}
229}
230
231// Stopping kills the satellite's whole process group, even when a child
232// keeps its stderr open.
233func TestRunKillsSatelliteGroup(t *testing.T) {
234	opts := testOptions(t, "")
235	pidfile := filepath.Join(opts.TmpDir, "pid")
236	opts.Binary = filepath.Join(opts.TmpDir, "satellite")
237	script := "#!/bin/sh\ntrap '' TERM\nsleep 30 &\necho $! > " + pidfile + "\nwait\n"
238	if err := os.WriteFile(opts.Binary, []byte(script), 0o755); err != nil {
239		t.Fatal(err)
240	}
241	d, err := Open(opts)
242	if err != nil {
243		t.Fatal(err)
244	}
245	defer d.Close()
246	ctx, cancel := context.WithCancel(context.Background())
247	done := make(chan error, 1)
248	go func() { done <- d.Run(ctx) }()
249	unix.Close(dial(t, d.socket))
250	var pid int
251	for deadline := time.Now().Add(3 * time.Second); pid == 0 && time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
252		b, _ := os.ReadFile(pidfile)
253		pid, _ = strconv.Atoi(strings.TrimSpace(string(b)))
254	}
255	if pid == 0 {
256		t.Fatal("satellite did not start")
257	}
258	cancel()
259	select {
260	case <-done:
261	case <-time.After(5 * time.Second):
262		t.Fatal("Run did not stop")
263	}
264	for deadline := time.Now().Add(2 * time.Second); ; time.Sleep(20 * time.Millisecond) {
265		if errors.Is(unix.Kill(pid, 0), unix.ESRCH) {
266			break
267		}
268		if time.Now().After(deadline) {
269			t.Fatalf("child %d survived", pid)
270		}
271	}
272}

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