main 602baa7

conn_test.go RawHistoryFolder

1package xturbo
2
3import (
4	"bytes"
5	"encoding/binary"
6	"errors"
7	"io"
8	"os"
9	"path/filepath"
10	"testing"
11
12	"bnema.dev/xturbo/internal/xtest"
13	"golang.org/x/sys/unix"
14)
15
16func loadSetup(t testing.TB, name string) []byte {
17	t.Helper()
18	b, err := xtest.LoadHex(filepath.Join("testdata", "setup", name+".setup.hex"))
19	if err != nil {
20		t.Fatal(err)
21	}
22	return b
23}
24
25// scriptedServer runs the server side of a socketpair: it reads one setup
26// request, reports it on the returned channel, writes reply in chunks of
27// chunk bytes (0 = all at once) and, if hangup, closes its end. Otherwise it
28// then answers requests as a server without extensions (script{}).
29func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) {
30	t.Helper()
31	fd, _, ch := serveSetup(t, reply, chunk, hangup, &script{})
32	return fd, ch
33}
34
35// serveSetup is scriptedServer serving sc after the setup.
36func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script) (int, *scriptConn, <-chan []byte) {
37	t.Helper()
38	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
39	if err != nil {
40		t.Fatal(err)
41	}
42	// A blocking fd: NewConn switches it to O_NONBLOCK itself.
43	if err := unix.SetNonblock(fds[0], false); err != nil {
44		t.Fatal(err)
45	}
46	server := os.NewFile(uintptr(fds[1]), "server")
47	srv := &scriptConn{f: server, sc: sc}
48	ch := make(chan []byte, 1)
49	done := make(chan struct{})
50	t.Cleanup(func() { <-done; server.Close() })
51	go func() {
52		defer close(done)
53		var hdr [12]byte
54		if _, err := io.ReadFull(server, hdr[:]); err != nil {
55			ch <- nil
56			return
57		}
58		n := pad4(int(binary.LittleEndian.Uint16(hdr[6:]))) + pad4(int(binary.LittleEndian.Uint16(hdr[8:])))
59		rest := make([]byte, n)
60		if _, err := io.ReadFull(server, rest); err != nil {
61			ch <- nil
62			return
63		}
64		ch <- append(hdr[:], rest...)
65		for p := reply; len(p) > 0; {
66			k := len(p)
67			if chunk > 0 {
68				k = min(k, chunk)
69			}
70			if _, err := server.Write(p[:k]); err != nil {
71				return
72			}
73			p = p[k:]
74		}
75		if hangup {
76			server.Close()
77			return
78		}
79		srv.serve()
80	}()
81	return fds[0], srv, ch
82}
83
84func TestNewConnSuccess(t *testing.T) {
85	setup := loadSetup(t, "Success")
86	fd, req := scriptedServer(t, setup, 7, false)
87	c, err := NewConn(fd)
88	if err != nil {
89		t.Fatal(err)
90	}
91	defer c.Close()
92	got := <-req
93	want := []byte{'l', 0, 11, 0, 0, 0, 0, 0, 0, 0, 0, 0}
94	if !bytes.Equal(got, want) {
95		t.Fatalf("setup request %x, want %x", got, want)
96	}
97	if !bytes.Equal(c.SetupBytes(), setup) {
98		t.Fatal("SetupBytes differs from the reply")
99	}
100	// Expected values are read from the fixture header comment
101	// (Success.setup.hex), i.e. what that recorded Xvfb assigned.
102	if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff {
103		t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask)
104	}
105	// The bootstrap issued one QueryExtension per extension and a fence.
106	if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != Seq(extCount)+1 {
107		t.Fatal("accessors", c.Buf().Next())
108	}
109	if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present {
110		t.Fatal("ExtTable not empty")
111	}
112	flags, err := unix.FcntlInt(uintptr(fd), unix.F_GETFL, 0)
113	if err != nil || flags&unix.O_NONBLOCK == 0 {
114		t.Fatalf("fd not O_NONBLOCK after setup: %v", err)
115	}
116}
117
118func TestNewConnAuthRequest(t *testing.T) {
119	fd, req := scriptedServer(t, loadSetup(t, "Success"), 0, false)
120	cookie := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}
121	c, err := newConn(fd, authMagicCookie, cookie)
122	if err != nil {
123		t.Fatal(err)
124	}
125	defer c.Close()
126	want := append([]byte{'l', 0, 11, 0, 0, 0, 18, 0, 16, 0, 0, 0}, authMagicCookie+"\x00\x00"...)
127	want = append(want, cookie...)
128	if got := <-req; !bytes.Equal(got, want) {
129		t.Fatalf("setup request %x, want %x", got, want)
130	}
131}
132
133func TestNewConnRefused(t *testing.T) {
134	authenticate := []byte{2, 0, 0, 0, 0, 0, 2, 0, 'n', 'o', 'p', 'e', '!', 0, 0, 0}
135	for _, tc := range []struct {
136		name   string
137		reply  []byte
138		status uint8
139		reason string
140	}{
141		{"failed", loadSetup(t, "Failed"), 0, "Authorization required, but no authorization protocol specified\n"},
142		{"authenticate", authenticate, 2, "nope!"},
143	} {
144		t.Run(tc.name, func(t *testing.T) {
145			fd, _ := scriptedServer(t, tc.reply, 5, true)
146			c, err := NewConn(fd)
147			var se *SetupError
148			if c != nil || !errors.As(err, &se) || !errors.Is(err, ErrSetupFailed) {
149				t.Fatalf("conn %t, err %v", c != nil, err)
150			}
151			if se.Status != tc.status || se.Reason != tc.reason {
152				t.Fatalf("status %d reason %q", se.Status, se.Reason)
153			}
154			if _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); err != unix.EBADF {
155				t.Fatalf("fd not closed on failure: %v", err)
156			}
157		})
158	}
159}
160
161func TestNewConnMalformed(t *testing.T) {
162	setup := loadSetup(t, "Success")
163	noScreens := bytes.Clone(setup)
164	noScreens[28] = 0
165	badMask := bytes.Clone(setup)
166	binary.LittleEndian.PutUint32(badMask[16:], 0)
167	badVersion := bytes.Clone(setup)
168	badVersion[2] = 10
169	badStatus := bytes.Clone(setup)
170	badStatus[0] = 3
171	// Fixed Setup only (length 8 words): one root declared, no SCREEN bytes.
172	screenPast := bytes.Clone(setup[:40])
173	binary.LittleEndian.PutUint16(screenPast[6:], 8)
174	binary.LittleEndian.PutUint16(screenPast[24:], 0) // vendor_len
175	screenPast[29] = 0                                // pixmap_formats_len
176	// Walk the recorded reply to find the first screen and its first depth
177	// that carries visuals; every extent is a multiple of 4 bytes.
178	screen := setupFixedSize + pad4(int(binary.LittleEndian.Uint16(setup[24:]))) + formatSize*int(setup[29])
179	visualDepth := screen + screenFixedSize
180	for binary.LittleEndian.Uint16(setup[visualDepth+2:]) == 0 {
181		visualDepth += depthFixedSize
182	}
183	// Screen header present, its allowed depths cut off.
184	missingDepths := truncateSetup(setup, screen+screenFixedSize)
185	// A depth header announcing visuals, the visuals cut off.
186	missingVisuals := truncateSetup(setup, visualDepth+depthFixedSize)
187	// One visual of the depth present, the rest cut off.
188	partialVisuals := truncateSetup(setup, visualDepth+depthFixedSize+visualSize)
189	// Complete first screen, a second screen declared but absent.
190	missingScreen := bytes.Clone(setup)
191	missingScreen[28] = 2
192	// Header claims 8 bytes of body, failure reason claims 20.
193	reasonPast := []byte{0, 20, 11, 0, 0, 0, 2, 0, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'}
194	// Success header whose length is shorter than the fixed Setup.
195	shortSuccess := []byte{1, 0, 11, 0, 0, 0, 1, 0, 0, 0, 0, 0}
196	for _, tc := range []struct {
197		name  string
198		reply []byte
199	}{
200		{"empty", nil},
201		{"truncated header", setup[:5]},
202		{"truncated body", setup[:len(setup)-3]},
203		{"no screens", noScreens},
204		{"bad xid mask", badMask},
205		{"protocol major", badVersion},
206		{"unknown status", badStatus},
207		{"screen past end", screenPast},
208		{"missing depths", missingDepths},
209		{"missing visuals", missingVisuals},
210		{"partial visuals", partialVisuals},
211		{"missing subsequent screen", missingScreen},
212		{"reason past end", reasonPast},
213		{"short success", shortSuccess},
214	} {
215		t.Run(tc.name, func(t *testing.T) {
216			fd, _ := scriptedServer(t, tc.reply, 3, true)
217			c, err := NewConn(fd)
218			if c != nil || !errors.Is(err, ErrMalformed) || errors.Is(err, ErrSetupFailed) {
219				t.Fatalf("conn %t, err %v", c != nil, err)
220			}
221		})
222	}
223}
224
225// truncateSetup keeps the first n bytes (n ≡ 0 mod 4) of a setup reply and
226// rewrites its additional-data length to match.
227func truncateSetup(setup []byte, n int) []byte {
228	p := bytes.Clone(setup[:n])
229	binary.LittleEndian.PutUint16(p[6:], uint16((n-setupHeaderSize)/4))
230	return p
231}
232
233func TestConnFlushClose(t *testing.T) {
234	c, srv := dialScript(t, &script{})
235	first := c.Buf().Next()
236	p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
237	if p == nil || seq != first {
238		t.Fatal("request")
239	}
240	if err := c.Flush(); err != nil {
241		t.Fatal(err)
242	}
243	waitRequests(t, srv, int(seq))
244	if got := srv.requests()[seq-1].raw; !bytes.Equal(got, []byte{43, 0, 1, 0}) {
245		t.Fatalf("%x", got)
246	}
247	if err := c.Close(); err != nil {
248		t.Fatal(err)
249	}
250	if c.Fd() != -1 || !errors.Is(c.Close(), ErrClosed) || !errors.Is(c.Flush(), ErrClosed) {
251		t.Fatal("after Close")
252	}
253	if p, _ := c.Buf().Request(43, 0, 0); p != nil {
254		t.Fatal("Buf usable after Close")
255	}
256}
257
258func TestSetupErrorMessage(t *testing.T) {
259	for _, tc := range []struct {
260		e    SetupError
261		want string
262	}{
263		{SetupError{0, "no"}, "xturbo: connection setup failed: no"},
264		{SetupError{2, "why"}, "xturbo: connection setup authenticate: why"},
265	} {
266		if got := tc.e.Error(); got != tc.want {
267			t.Errorf("%q, want %q", got, tc.want)
268		}
269	}
270}

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