main 602baa7

xserver_script_test.go RawHistoryFolder

1package xturbo
2
3import (
4	"encoding/binary"
5	"io"
6	"os"
7	"sync"
8	"sync/atomic"
9	"testing"
10)
11
12// scriptExt is an extension the scripted server advertises.
13type scriptExt struct{ major, firstEvent, firstError uint8 }
14
15// script is the behaviour of the scripted X server behind a socketpair. It
16// answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and
17// XC-MISC GetXIDRange like an X server; other requests get no reply unless
18// hook handles them.
19type script struct {
20	exts      map[string]scriptExt
21	bigreqMax uint32            // Enable reply maximum_request_length
22	atoms     map[string]uint32 // known atoms; new names get nextAtom++
23	nextAtom  uint32
24	badAtoms  map[string]bool // InternAtom answers BadAlloc
25	xidRanges [][2]uint32     // successive GetXIDRange replies {start, count}
26	// hook sees every request first; returning true skips the default.
27	hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
28	// silent, once set, makes the server read requests and never answer
29	// (alive but hung).
30	silent atomic.Bool
31}
32
33// scriptConn is the server side of one scripted connection.
34type scriptConn struct {
35	f   *os.File
36	sc  *script
37	seq uint16
38
39	mu   sync.Mutex
40	reqs []scriptReq // every request after setup, in order
41}
42
43type scriptReq struct {
44	seq   uint16
45	major uint8
46	minor uint8
47	raw   []byte // whole request, header included
48}
49
50func (s *scriptConn) requests() []scriptReq {
51	s.mu.Lock()
52	defer s.mu.Unlock()
53	return append([]scriptReq(nil), s.reqs...)
54}
55
56func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) }
57
58func (s *scriptConn) reply(data uint8, body []byte) {
59	words := (max(len(body), 24) - 24 + 3) / 4
60	p := make([]byte, 32+4*words)
61	p[0], p[1] = 1, data
62	binary.LittleEndian.PutUint16(p[2:], s.seq)
63	binary.LittleEndian.PutUint32(p[4:], uint32(words))
64	copy(p[8:], body)
65	s.write(p)
66}
67
68func (s *scriptConn) xerror(code, major uint8, bad uint32) {
69	p := make([]byte, 32)
70	p[1] = code
71	binary.LittleEndian.PutUint16(p[2:], s.seq)
72	binary.LittleEndian.PutUint32(p[4:], bad)
73	p[10] = major
74	s.write(p)
75}
76
77// event writes a 32-byte core event carrying the current sequence number.
78func (s *scriptConn) event(code uint8, fill byte) {
79	p := make([]byte, 32)
80	for i := range p {
81		p[i] = fill
82	}
83	p[0] = code
84	binary.LittleEndian.PutUint16(p[2:], s.seq)
85	s.write(p)
86}
87
88func (sc *script) extByMajor(major uint8) string {
89	for name, e := range sc.exts {
90		if e.major == major {
91			return name
92		}
93	}
94	return ""
95}
96
97// serve reads requests until EOF and answers them.
98func (s *scriptConn) serve() {
99	sc := s.sc
100	for {
101		var hdr [4]byte
102		if _, err := io.ReadFull(s.f, hdr[:]); err != nil {
103			return
104		}
105		raw := hdr[:]
106		n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4
107		if n == 0 { // BIG-REQUESTS header
108			var l [4]byte
109			if _, err := io.ReadFull(s.f, l[:]); err != nil {
110				return
111			}
112			raw = append(raw, l[:]...)
113			n = int(binary.LittleEndian.Uint32(l[:])) * 4
114		}
115		rest := make([]byte, n-len(raw))
116		if _, err := io.ReadFull(s.f, rest); err != nil {
117			return
118		}
119		raw = append(append([]byte(nil), raw...), rest...)
120		body := raw[len(raw)-len(rest):]
121		s.seq++
122		major, minor := hdr[0], hdr[1]
123		s.mu.Lock()
124		s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
125		s.mu.Unlock()
126		if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
127			continue
128		}
129		s.answer(major, minor, body)
130	}
131}
132
133// answer runs the default behaviour for one request, holding s.mu so tests
134// can read sc (atom) concurrently.
135func (s *scriptConn) answer(major, minor uint8, body []byte) {
136	sc := s.sc
137	s.mu.Lock()
138	defer s.mu.Unlock()
139	switch {
140	case major == opQueryExtension:
141		e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
142		var present uint8
143		if ok {
144			present = 1
145		}
146		s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
147	case major == opInternAtom:
148		name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
149		if sc.badAtoms[name] {
150			s.xerror(11, opInternAtom, 0) // BadAlloc
151			return
152		}
153		a, ok := sc.atoms[name]
154		if !ok && minor == 0 {
155			if sc.atoms == nil {
156				sc.atoms = map[string]uint32{}
157			}
158			sc.nextAtom++
159			a = sc.nextAtom + 1000
160			sc.atoms[name] = a
161		}
162		s.reply(0, binary.LittleEndian.AppendUint32(nil, a))
163	case major == opGetInputFocus:
164		s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234))
165	case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0:
166		s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax))
167	case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange:
168		r := [2]uint32{0, 1}
169		if len(sc.xidRanges) > 0 {
170			r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:]
171		}
172		s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1]))
173	}
174}
175
176// fullExts is what a typical Xvfb advertises, at its usual opcodes.
177func fullExts() map[string]scriptExt {
178	return map[string]scriptExt{
179		"BIG-REQUESTS":            {133, 0, 0},
180		"Generic Event Extension": {128, 0, 0},
181		"XC-MISC":                 {136, 0, 0},
182		"Composite":               {142, 0, 0},
183		"XFIXES":                  {138, 87, 140},
184		"X-Resource":              {144, 0, 0},
185		"RENDER":                  {139, 0, 141},
186		"SHAPE":                   {129, 64, 0},
187	}
188}
189
190// startScript serves sc behind a socketpair after answering the setup with
191// the recorded Success reply, and returns the client fd (not yet set up) and
192// the server connection.
193func startScript(t *testing.T, sc *script) (int, *scriptConn) {
194	t.Helper()
195	fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc)
196	return fd, srv
197}
198
199// dialScript runs NewConn against sc.
200func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) {
201	t.Helper()
202	fd, srv := startScript(t, sc)
203	c, err := NewConn(fd)
204	if err != nil {
205		t.Fatal(err)
206	}
207	t.Cleanup(func() { _ = c.Close() })
208	return c, srv
209}
210
211// atom returns the atom the server assigned to name.
212func (s *scriptConn) atom(name string) uint32 {
213	s.mu.Lock()
214	defer s.mu.Unlock()
215	return s.sc.atoms[name]
216}

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