v0.2.0 ab733e5

transport_test.go RawHistoryFolder

1package wlturbo
2
3import (
4	"encoding/binary"
5	"errors"
6	"io"
7	"net"
8	"reflect"
9	"testing"
10	"time"
11)
12
13// EventRecord is what a test observes for one dispatched event.
14type EventRecord struct {
15	Object uint32
16	Opcode uint16
17	Body   []byte
18}
19
20// recordingProxy records every dispatched event. It is a stand-in for a real
21// protocol proxy so framing tests need no compositor.
22type recordingProxy struct {
23	BaseProxy
24	records *[]EventRecord
25}
26
27func (p *recordingProxy) Dispatch(event *Event) {
28	*p.records = append(*p.records, EventRecord{
29		Object: event.ProxyID,
30		Opcode: event.Opcode,
31		Body:   append([]byte(nil), event.Data()...),
32	})
33}
34
35// chunkConn returns one chunk per Read, so a test controls exactly how bytes
36// are split across read boundaries.
37type chunkConn struct {
38	chunks [][]byte
39	idx    int
40}
41
42func (c *chunkConn) Read(p []byte) (int, error) {
43	if c.idx >= len(c.chunks) {
44		return 0, io.EOF
45	}
46	chunk := c.chunks[c.idx]
47	n := copy(p, chunk)
48	switch {
49	case n == len(chunk):
50		c.idx++
51	case n < len(chunk):
52		// Caller's buffer was smaller than the chunk: keep the remainder.
53		c.chunks[c.idx] = chunk[n:]
54	}
55	return n, nil
56}
57
58func (c *chunkConn) Write(p []byte) (int, error)      { return len(p), nil }
59func (c *chunkConn) Close() error                     { return nil }
60func (c *chunkConn) LocalAddr() net.Addr              { return testAddr{} }
61func (c *chunkConn) RemoteAddr() net.Addr             { return testAddr{} }
62func (c *chunkConn) SetDeadline(time.Time) error      { return nil }
63func (c *chunkConn) SetReadDeadline(time.Time) error  { return nil }
64func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
65
66type testAddr struct{}
67
68func (testAddr) Network() string { return "test" }
69func (testAddr) String() string  { return "test" }
70
71// header builds a Wayland message header for the given object, opcode and
72// total message size.
73func header(object uint32, opcode uint16, size uint32) []byte {
74	buf := make([]byte, HeaderSize)
75	binary.LittleEndian.PutUint32(buf[0:4], object)
76	binary.LittleEndian.PutUint32(buf[4:8], (size<<16)|uint32(opcode))
77	return buf
78}
79
80// message builds one complete 4-byte-aligned Wayland message.
81func message(object uint32, opcode uint16, body []byte) []byte {
82	msg := header(object, opcode, uint32(HeaderSize+len(body)))
83	return append(msg, body...)
84}
85
86// newChunkDisplay builds a Display over a connection that yields the given
87// chunks, with a recording proxy registered at object ID 7.
88func newChunkDisplay(chunks [][]byte) (*Display, *[]EventRecord) {
89	d := newDisplay(&chunkConn{chunks: chunks})
90	records := []EventRecord{}
91	d.objects.Store(uint32(7), &recordingProxy{
92		BaseProxy: BaseProxy{id: 7, context: d.context},
93		records:   &records,
94	})
95	return d, &records
96}
97
98// dispatchUntilError dispatches at most max frames and returns the first error.
99func dispatchUntilError(d *Display, max int) error {
100	for i := 0; i < max; i++ {
101		if err := d.Dispatch(); err != nil {
102			return err
103		}
104	}
105	return nil
106}
107
108func TestDisplayDispatch_Fragmented(t *testing.T) {
109	body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
110	msg := message(7, 3, body)
111
112	// Split the header across three reads and the body byte by byte.
113	chunks := [][]byte{
114		msg[0:3],
115		msg[3:5],
116		msg[5:HeaderSize],
117	}
118	for i := HeaderSize; i < len(msg); i++ {
119		chunks = append(chunks, msg[i:i+1])
120	}
121
122	d, records := newChunkDisplay(chunks)
123	err := dispatchUntilError(d, 2)
124	if !errors.Is(err, io.ErrUnexpectedEOF) {
125		t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
126	}
127
128	want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
129	if !reflect.DeepEqual(*records, want) {
130		t.Fatalf("records = %#v, want %#v", *records, want)
131	}
132}
133
134func TestDisplayDispatch_Coalesced(t *testing.T) {
135	first := message(7, 3, []byte{0x11, 0x12, 0x13, 0x14})
136	second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
137
138	d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
139
140	// Two messages arrive in a single read; one Dispatch per message.
141	if err := d.Dispatch(); err != nil {
142		t.Fatalf("first Dispatch: %v", err)
143	}
144	if err := d.Dispatch(); err != nil {
145		t.Fatalf("second Dispatch: %v", err)
146	}
147
148	want := []EventRecord{
149		{Object: 7, Opcode: 3, Body: []byte{0x11, 0x12, 0x13, 0x14}},
150		{Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
151	}
152	if !reflect.DeepEqual(*records, want) {
153		t.Fatalf("records = %#v, want %#v", *records, want)
154	}
155}
156
157func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
158	// A read boundary that ends between the header and the body of the second
159	// message must not be mistaken for a frame boundary.
160	whole := append(message(7, 3, []byte{0x01, 0x02, 0x03, 0x04}),
161		message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
162
163	split := HeaderSize + 4 + 4 // mid-body of the second message
164	d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
165
166	if err := d.Dispatch(); err != nil {
167		t.Fatalf("first Dispatch: %v", err)
168	}
169	if err := d.Dispatch(); err != nil {
170		t.Fatalf("second Dispatch: %v", err)
171	}
172
173	want := []EventRecord{
174		{Object: 7, Opcode: 3, Body: []byte{0x01, 0x02, 0x03, 0x04}},
175		{Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
176	}
177	if !reflect.DeepEqual(*records, want) {
178		t.Fatalf("records = %#v, want %#v", *records, want)
179	}
180}
181
182func TestDisplayDispatch_ProtocolError(t *testing.T) {
183	cases := []struct {
184		name     string
185		chunks   [][]byte
186		maxLen   uint32
187		wantErr  error
188		wantKind string
189	}{
190		{
191			name:     "size smaller than header",
192			chunks:   [][]byte{header(7, 3, 0)},
193			wantErr:  ErrMalformedFrame,
194			wantKind: "header_size",
195		},
196		{
197			name:     "misaligned size",
198			chunks:   [][]byte{header(7, 3, 10)},
199			wantErr:  ErrMalformedFrame,
200			wantKind: "misaligned_size",
201		},
202		{
203			name:     "size over configured maximum",
204			chunks:   [][]byte{message(7, 3, make([]byte, 32))},
205			maxLen:   16,
206			wantErr:  ErrMalformedFrame,
207			wantKind: "size_over_limit",
208		},
209		{
210			name:     "unknown object",
211			chunks:   [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
212			wantErr:  ErrUnknownObject,
213			wantKind: "unknown_object",
214		},
215		{
216			name:     "unknown opcode on display",
217			chunks:   [][]byte{message(displayObjectID, 9, nil)},
218			wantErr:  ErrUnknownOpcode,
219			wantKind: "unknown_opcode",
220		},
221	}
222
223	for _, tc := range cases {
224		t.Run(tc.name, func(t *testing.T) {
225			d, records := newChunkDisplay(tc.chunks)
226			if tc.maxLen != 0 {
227				d.maxMsgLen = tc.maxLen
228			}
229
230			err := d.Dispatch()
231			if !errors.Is(err, tc.wantErr) {
232				t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
233			}
234
235			var perr *ProtocolError
236			if !errors.As(err, &perr) {
237				t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
238			}
239			if perr.Kind != tc.wantKind {
240				t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
241			}
242			if len(*records) != 0 {
243				t.Fatalf("records = %#v, want none", *records)
244			}
245		})
246	}
247}
248
249func TestDisplayDispatch_DisplayError(t *testing.T) {
250	// wl_display.error: object id, code, message (padded to 4 bytes).
251	msg := []byte("boom")
252	body := make([]byte, 0, 8+4+8)
253	body = binary.LittleEndian.AppendUint32(body, 7)
254	body = binary.LittleEndian.AppendUint32(body, 3)
255	body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
256	body = append(body, msg...)
257	body = append(body, 0, 0, 0, 0)
258
259	d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
260
261	err := d.Dispatch()
262	if !errors.Is(err, ErrDisplayError) {
263		t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
264	}
265
266	var derr *DisplayError
267	if !errors.As(err, &derr) {
268		t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
269	}
270	if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
271		t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
272	}
273}
274
275func TestDisplayDispatch_DeleteID(t *testing.T) {
276	d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
277
278	if err := d.Dispatch(); err != nil {
279		t.Fatalf("Dispatch: %v", err)
280	}
281	if _, ok := d.objects.Load(uint32(7)); ok {
282		t.Fatal("delete_id did not remove object 7")
283	}
284	if len(*records) != 0 {
285		t.Fatalf("records = %#v, want none", *records)
286	}
287}
288
289func TestDisplayDispatch_PeerClose(t *testing.T) {
290	cases := []struct {
291		name   string
292		chunks [][]byte
293	}{
294		{name: "closed before any byte"},
295		{name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
296		{
297			name:   "closed mid body",
298			chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
299		},
300		{
301			name:   "closed after a complete message",
302			chunks: [][]byte{message(7, 3, []byte{0x01, 0x02, 0x03, 0x04})},
303		},
304	}
305
306	for _, tc := range cases {
307		t.Run(tc.name, func(t *testing.T) {
308			d, _ := newChunkDisplay(tc.chunks)
309
310			var err error
311			for i := 0; i < 4; i++ {
312				if err = d.Dispatch(); err != nil {
313					break
314				}
315			}
316			if !errors.Is(err, io.ErrUnexpectedEOF) {
317				t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
318			}
319		})
320	}
321}
322
323func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
324	d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
325	if err := d.Close(); err != nil {
326		t.Fatalf("Close: %v", err)
327	}
328	if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
329		t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
330	}
331}
332
333func TestReadFrame(t *testing.T) {
334	body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
335	object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
336	if err != nil {
337		t.Fatalf("readFrame: %v", err)
338	}
339	if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
340		t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
341	}
342
343	// A header-only message has an empty body.
344	if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
345		t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
346	}
347
348	// Truncated bodies are reported, never returned partially.
349	if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
350		t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
351	}
352}

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 8

↑↓ moveenter openesc close