v0.1.0 d9218d6

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/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	if n, err := unix.Read(fd, make([]byte, 1)); n != 0 || err != nil {
84		t.Fatalf("read %d %v, want EOF", n, err)
85	}
86	cancel()
87	select {
88	case err := <-done:
89		if err != nil {
90			t.Fatal(err)
91		}
92	case <-time.After(3 * time.Second):
93		t.Fatal("Run did not stop")
94	}
95}
96
97// satelliteScript writes a real script that records each run and whether it
98// received listening sockets on fds 3 and 4, then exits.
99func satelliteScript(t *testing.T, dir string) (string, string) {
100	t.Helper()
101	runs := filepath.Join(dir, "runs")
102	bin := filepath.Join(dir, "satellite")
103	script := "#!/bin/sh\n[ -S /proc/self/fd/3 ] && [ -S /proc/self/fd/4 ] && echo \"$*\" >> " + runs + "\n"
104	if err := os.WriteFile(bin, []byte(script), 0o755); err != nil {
105		t.Fatal(err)
106	}
107	return bin, runs
108}
109
110func readRuns(t *testing.T, path string, want int) []string {
111	t.Helper()
112	deadline := time.Now().Add(3 * time.Second)
113	for {
114		b, _ := os.ReadFile(path)
115		lines := strings.Fields(strings.ReplaceAll(string(b), " ", "_"))
116		if len(lines) >= want || time.Now().After(deadline) {
117			return lines
118		}
119		time.Sleep(20 * time.Millisecond)
120	}
121}
122
123// Each connection starts the satellite once, with both sockets handed over.
124func TestRunStartsSatelliteOnConnect(t *testing.T) {
125	opts := testOptions(t, "")
126	var runs string
127	opts.Binary, runs = satelliteScript(t, opts.TmpDir)
128	opts.Abstract = true
129	d, err := Open(opts)
130	if err != nil {
131		t.Fatal(err)
132	}
133	defer d.Close()
134	ctx, cancel := context.WithCancel(context.Background())
135	done := make(chan error, 1)
136	go func() { done <- d.Run(ctx) }()
137	time.Sleep(100 * time.Millisecond)
138	if got := readRuns(t, runs, 0); len(got) != 0 {
139		t.Fatalf("started without a client: %v", got)
140	}
141	unix.Close(dial(t, d.socket))
142	want := d.Name() + "_-listenfd_3_-listenfd_4"
143	if got := readRuns(t, runs, 1); len(got) != 1 || got[0] != want {
144		t.Fatalf("runs %v, want [%s]", got, want)
145	}
146	// The next client, after the backoff, starts it again.
147	time.Sleep(1200 * time.Millisecond)
148	if got := readRuns(t, runs, 1); len(got) != 1 {
149		t.Fatalf("respawned without a client: %v", got)
150	}
151	unix.Close(dial(t, "\x00"+d.socket))
152	if got := readRuns(t, runs, 2); len(got) != 2 {
153		t.Fatalf("runs %v after the second client", got)
154	}
155	cancel()
156	if err := <-done; err != nil {
157		t.Fatal(err)
158	}
159}
160
161func TestStaleLockReclaimed(t *testing.T) {
162	opts := testOptions(t, "true")
163	// PID 0x7ffffff0 does not exist.
164	if err := os.WriteFile(filepath.Join(opts.TmpDir, ".X0-lock"), []byte("2147483632\n"), 0o444); err != nil {
165		t.Fatal(err)
166	}
167	// A live process keeps its display.
168	if err := os.WriteFile(filepath.Join(opts.TmpDir, ".X1-lock"), []byte(strconv.Itoa(os.Getpid())), 0o444); err != nil {
169		t.Fatal(err)
170	}
171	d, err := Open(opts)
172	if err != nil {
173		t.Fatal(err)
174	}
175	defer d.Close()
176	if d.Name() != ":0" {
177		t.Fatalf("display %s", d.Name())
178	}
179	if b, _ := os.ReadFile(d.lock); strings.TrimSpace(string(b)) != strconv.Itoa(os.Getpid()) {
180		t.Fatalf("lock holds %q", b)
181	}
182}
183
184func TestDirChecks(t *testing.T) {
185	for name, setup := range map[string]func(dir string) error{
186		"not sticky": func(dir string) error { return os.Mkdir(dir, 0o777) },
187		"symlink": func(dir string) error {
188			target := dir + "-target"
189			if err := os.Mkdir(target, 0o1777); err != nil {
190				return err
191			}
192			return os.Symlink(target, dir)
193		},
194		"file": func(dir string) error { return os.WriteFile(dir, nil, 0o777) },
195	} {
196		t.Run(name, func(t *testing.T) {
197			opts := testOptions(t, "true")
198			if err := setup(opts.Dir); err != nil {
199				t.Fatal(err)
200			}
201			_ = os.Chmod(opts.Dir, 0o777|os.ModeSticky)
202			if name == "not sticky" {
203				_ = os.Chmod(opts.Dir, 0o777)
204			}
205			if _, err := Open(opts); err == nil {
206				t.Fatal("opened an unsafe X11 directory")
207			}
208		})
209	}
210}
211
212func TestSupported(t *testing.T) {
213	if !Supported(context.Background(), "true") || Supported(context.Background(), "false") {
214		t.Fatal("exit status not honoured")
215	}
216	hang := filepath.Join(t.TempDir(), "hang")
217	if err := os.WriteFile(hang, []byte("#!/bin/sh\nexec sleep 10\n"), 0o755); err != nil {
218		t.Fatal(err)
219	}
220	start := time.Now()
221	if Supported(context.Background(), hang) || time.Since(start) > 3*time.Second {
222		t.Fatalf("hanging binary: %v", time.Since(start))
223	}
224}
225
226// Stopping kills the satellite's whole process group, even when a child
227// keeps its stderr open.
228func TestRunKillsSatelliteGroup(t *testing.T) {
229	opts := testOptions(t, "")
230	pidfile := filepath.Join(opts.TmpDir, "pid")
231	opts.Binary = filepath.Join(opts.TmpDir, "satellite")
232	script := "#!/bin/sh\ntrap '' TERM\nsleep 30 &\necho $! > " + pidfile + "\nwait\n"
233	if err := os.WriteFile(opts.Binary, []byte(script), 0o755); err != nil {
234		t.Fatal(err)
235	}
236	d, err := Open(opts)
237	if err != nil {
238		t.Fatal(err)
239	}
240	defer d.Close()
241	ctx, cancel := context.WithCancel(context.Background())
242	done := make(chan error, 1)
243	go func() { done <- d.Run(ctx) }()
244	unix.Close(dial(t, d.socket))
245	var pid int
246	for deadline := time.Now().Add(3 * time.Second); pid == 0 && time.Now().Before(deadline); time.Sleep(20 * time.Millisecond) {
247		b, _ := os.ReadFile(pidfile)
248		pid, _ = strconv.Atoi(strings.TrimSpace(string(b)))
249	}
250	if pid == 0 {
251		t.Fatal("satellite did not start")
252	}
253	cancel()
254	select {
255	case <-done:
256	case <-time.After(5 * time.Second):
257		t.Fatal("Run did not stop")
258	}
259	for deadline := time.Now().Add(2 * time.Second); ; time.Sleep(20 * time.Millisecond) {
260		if errors.Is(unix.Kill(pid, 0), unix.ESRCH) {
261			break
262		}
263		if time.Now().After(deadline) {
264			t.Fatalf("child %d survived", pid)
265		}
266	}
267}

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