v0.1.0 602baa7

conn_xserver_test.go RawHistoryFolder

1//go:build xserver
2
3package xturbo_test
4
5import (
6	"bytes"
7	"crypto/rand"
8	"encoding/binary"
9	"errors"
10	"os"
11	"os/exec"
12	"path/filepath"
13	"strconv"
14	"strings"
15	"testing"
16	"time"
17
18	"bnema.dev/xturbo"
19	"bnema.dev/xturbo/xproto"
20	"golang.org/x/sys/unix"
21)
22
23// startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the
24// MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string.
25func startXvfb(t *testing.T, authFile string) string {
26	t.Helper()
27	if _, err := exec.LookPath("Xvfb"); err != nil {
28		t.Skip("Xvfb not installed")
29	}
30	r, w, err := os.Pipe()
31	if err != nil {
32		t.Fatal(err)
33	}
34	// Xvfb may write to -displayfd again (e.g. after a server reset); closing
35	// the read end early makes that write fail and kills the server.
36	t.Cleanup(func() { _ = r.Close() })
37	cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
38	cmd.ExtraFiles = []*os.File{w}
39	var log bytes.Buffer
40	cmd.Stderr = &log
41	if err := cmd.Start(); err != nil {
42		w.Close()
43		t.Fatal(err)
44	}
45	w.Close()
46	t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
47	_ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
48	var buf [32]byte
49	n, err := r.Read(buf[:])
50	if err != nil {
51		t.Fatalf("Xvfb: %v: %s", err, log.String())
52	}
53	num := strings.TrimSpace(string(buf[:n]))
54	if _, err := strconv.Atoi(num); err != nil {
55		t.Fatalf("Xvfb display %q", num)
56	}
57	return ":" + num
58}
59
60// writeXauthority writes one FamilyLocal entry for this host, any display.
61func writeXauthority(t *testing.T, path string, cookie []byte) {
62	t.Helper()
63	host, err := os.Hostname()
64	if err != nil {
65		t.Fatal(err)
66	}
67	var b []byte
68	field := func(s []byte) {
69		b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
70		b = append(b, s...)
71	}
72	b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
73	field([]byte(host))
74	field(nil) // any display number
75	field([]byte("MIT-MAGIC-COOKIE-1"))
76	field(cookie)
77	if err := os.WriteFile(path, b, 0o600); err != nil {
78		t.Fatal(err)
79	}
80}
81
82func TestXserverDial(t *testing.T) {
83	dir := t.TempDir()
84	auth := filepath.Join(dir, "Xauthority")
85	cookie := make([]byte, 16)
86	_, _ = rand.Read(cookie)
87	writeXauthority(t, auth, cookie)
88	display := startXvfb(t, auth)
89
90	// Without a cookie the server refuses the setup: some builds answer Failed,
91	// others close the connection without a reply.
92	t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
93	if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) {
94		t.Fatalf("unauthenticated Dial: %v", err)
95	}
96
97	t.Setenv("XAUTHORITY", auth)
98	c, err := xturbo.Dial(display + ".0")
99	if err != nil {
100		t.Fatal(err)
101	}
102	defer c.Close()
103
104	// Compare root's own parsing against the generated Setup view instead of
105	// server-assigned constants (they differ between X server builds).
106	setup := c.SetupBytes()
107	if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
108		t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
109	}
110	it := xproto.NewSetupIter(setup, 1)
111	if !it.Next() {
112		t.Fatal("setup does not parse")
113	}
114	sv := it.Value()
115	if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
116		t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
117	}
118	screens := sv.Roots()
119	if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
120		t.Fatalf("Root %#x, setup view says otherwise", c.Root())
121	}
122
123	// Round trips through Dispatch: GetInputFocus's reply, then the error
124	// of GetGeometry on window None, both attributed through Expect.
125	const kindFocus, kindGeom = 16, 17
126	seq := xproto.GetInputFocus(c.Buf())
127	bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone))
128	if seq == 0 || bad != seq+1 {
129		t.Fatalf("seqs %d %d", seq, bad)
130	}
131	if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil {
132		t.Fatal(err)
133	}
134	if err := c.Expect(bad, kindGeom, 5, 0, 0, 0); err != nil {
135		t.Fatal(err)
136	}
137	if err := c.Flush(); err != nil {
138		t.Fatal(err)
139	}
140	var reply xturbo.Reply
141	var xerr xturbo.XError
142	h := xturbo.Handler{
143		OnReply: func(r *xturbo.Reply) { reply = *r; reply.Raw = nil },
144		OnError: func(e *xturbo.XError) { xerr = *e },
145	}
146	dispatchUntil(t, c, &h, func() bool { return xerr.Seq != 0 })
147	if reply.Seq != seq || reply.Kind != kindFocus || reply.Args != [4]uint32{1, 2, 3, 4} {
148		t.Fatalf("reply %+v", reply)
149	}
150	if xerr.Seq != bad || xerr.Kind != kindGeom || xerr.Args[0] != 5 || xerr.Code != xproto.BadDrawable ||
151		xerr.Major != 14 || xerr.Ext != xturbo.ExtCore || xerr.Bad != 0 {
152		t.Fatalf("error %+v", xerr)
153	}
154	if st := c.Stats(); st.DroppedReplies != 0 || st.UnhandledErrors != 0 {
155		t.Fatalf("%+v", st)
156	}
157}
158
159// dispatchUntil polls the connection and dispatches until done reports true.
160func dispatchUntil(t *testing.T, c *xturbo.Conn, h *xturbo.Handler, done func() bool) {
161	t.Helper()
162	deadline := time.Now().Add(5 * time.Second)
163	for !done() {
164		if time.Now().After(deadline) {
165			t.Fatal("timeout waiting for the server")
166		}
167		fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: unix.POLLIN}}
168		if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
169			t.Fatal(err)
170		}
171		if err := c.Dispatch(h); err != nil {
172			t.Fatal(err)
173		}
174	}
175}

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 1

Tags 1

↑↓ moveenter openesc close