main e82a709

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	for _, opcode := range []uint16{3, 4, 5} {
96		signature := "uint,"
97		if opcode == 3 {
98			signature = "uint,uint,"
99		}
100		d.RegisterEventSignature(7, opcode, signature)
101	}
102	return d, &records
103}
104
105// dispatchUntilError dispatches at most max frames and returns the first error.
106func dispatchUntilError(d *Display, max int) error {
107	for i := 0; i < max; i++ {
108		if err := d.Dispatch(); err != nil {
109			return err
110		}
111	}
112	return nil
113}
114
115func TestDisplayDispatch_Fragmented(t *testing.T) {
116	body := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
117	msg := message(7, 3, body)
118
119	// Split the header across three reads and the body byte by byte.
120	chunks := [][]byte{
121		msg[0:3],
122		msg[3:5],
123		msg[5:HeaderSize],
124	}
125	for i := HeaderSize; i < len(msg); i++ {
126		chunks = append(chunks, msg[i:i+1])
127	}
128
129	d, records := newChunkDisplay(chunks)
130	err := dispatchUntilError(d, 2)
131	if !errors.Is(err, io.ErrUnexpectedEOF) {
132		t.Fatalf("Dispatch after the message = %v, want %v", err, io.ErrUnexpectedEOF)
133	}
134
135	want := []EventRecord{{Object: 7, Opcode: 3, Body: body}}
136	if !reflect.DeepEqual(*records, want) {
137		t.Fatalf("records = %#v, want %#v", *records, want)
138	}
139}
140
141func TestDisplayDispatch_Coalesced(t *testing.T) {
142	first := message(7, 4, []byte{0x11, 0x12, 0x13, 0x14})
143	second := message(7, 4, []byte{0x21, 0x22, 0x23, 0x24})
144
145	d, records := newChunkDisplay([][]byte{append(append([]byte{}, first...), second...)})
146
147	// Two messages arrive in a single read; one Dispatch per message.
148	if err := d.Dispatch(); err != nil {
149		t.Fatalf("first Dispatch: %v", err)
150	}
151	if err := d.Dispatch(); err != nil {
152		t.Fatalf("second Dispatch: %v", err)
153	}
154
155	want := []EventRecord{
156		{Object: 7, Opcode: 4, Body: []byte{0x11, 0x12, 0x13, 0x14}},
157		{Object: 7, Opcode: 4, Body: []byte{0x21, 0x22, 0x23, 0x24}},
158	}
159	if !reflect.DeepEqual(*records, want) {
160		t.Fatalf("records = %#v, want %#v", *records, want)
161	}
162}
163
164func TestDisplayDispatch_HalfCoalesced(t *testing.T) {
165	// A read boundary that ends between the header and the body of the second
166	// message must not be mistaken for a frame boundary.
167	whole := append(message(7, 4, []byte{0x01, 0x02, 0x03, 0x04}),
168		message(7, 5, []byte{0x05, 0x06, 0x07, 0x08})...)
169
170	split := HeaderSize + 4 + 4 // mid-body of the second message
171	d, records := newChunkDisplay([][]byte{whole[:split], whole[split:]})
172
173	if err := d.Dispatch(); err != nil {
174		t.Fatalf("first Dispatch: %v", err)
175	}
176	if err := d.Dispatch(); err != nil {
177		t.Fatalf("second Dispatch: %v", err)
178	}
179
180	want := []EventRecord{
181		{Object: 7, Opcode: 4, Body: []byte{0x01, 0x02, 0x03, 0x04}},
182		{Object: 7, Opcode: 5, Body: []byte{0x05, 0x06, 0x07, 0x08}},
183	}
184	if !reflect.DeepEqual(*records, want) {
185		t.Fatalf("records = %#v, want %#v", *records, want)
186	}
187}
188
189func TestDisplayDispatch_ProtocolError(t *testing.T) {
190	cases := []struct {
191		name     string
192		chunks   [][]byte
193		maxLen   uint32
194		wantErr  error
195		wantKind string
196	}{
197		{
198			name:     "size smaller than header",
199			chunks:   [][]byte{header(7, 3, 0)},
200			wantErr:  ErrMalformedFrame,
201			wantKind: "header_size",
202		},
203		{
204			name:     "misaligned size",
205			chunks:   [][]byte{header(7, 3, 10)},
206			wantErr:  ErrMalformedFrame,
207			wantKind: "misaligned_size",
208		},
209		{
210			name:     "size over configured maximum",
211			chunks:   [][]byte{message(7, 3, make([]byte, 32))},
212			maxLen:   16,
213			wantErr:  ErrMalformedFrame,
214			wantKind: "size_over_limit",
215		},
216		{
217			name:     "unknown object",
218			chunks:   [][]byte{message(9, 1, []byte{0x01, 0x02, 0x03, 0x04})},
219			wantErr:  ErrUnknownObject,
220			wantKind: "unknown_object",
221		},
222		{
223			name:     "unknown opcode on display",
224			chunks:   [][]byte{message(displayObjectID, 9, nil)},
225			wantErr:  ErrUnknownOpcode,
226			wantKind: "unknown_opcode",
227		},
228	}
229
230	for _, tc := range cases {
231		t.Run(tc.name, func(t *testing.T) {
232			d, records := newChunkDisplay(tc.chunks)
233			if tc.maxLen != 0 {
234				d.maxMsgLen = tc.maxLen
235			}
236
237			err := d.Dispatch()
238			if !errors.Is(err, tc.wantErr) {
239				t.Fatalf("Dispatch error = %v, want %v", err, tc.wantErr)
240			}
241
242			var perr *ProtocolError
243			if !errors.As(err, &perr) {
244				t.Fatalf("Dispatch error type = %T, want *ProtocolError", err)
245			}
246			if perr.Kind != tc.wantKind {
247				t.Fatalf("error kind = %q, want %q", perr.Kind, tc.wantKind)
248			}
249			if len(*records) != 0 {
250				t.Fatalf("records = %#v, want none", *records)
251			}
252		})
253	}
254}
255
256func TestDisplayDispatch_DisplayError(t *testing.T) {
257	// wl_display.error: object id, code, message (padded to 4 bytes).
258	msg := []byte("boom")
259	body := make([]byte, 0, 8+4+8)
260	body = binary.LittleEndian.AppendUint32(body, 7)
261	body = binary.LittleEndian.AppendUint32(body, 3)
262	body = binary.LittleEndian.AppendUint32(body, uint32(len(msg)+1))
263	body = append(body, msg...)
264	body = append(body, 0, 0, 0, 0)
265
266	d, _ := newChunkDisplay([][]byte{message(displayObjectID, 0, body)})
267
268	err := d.Dispatch()
269	if !errors.Is(err, ErrDisplayError) {
270		t.Fatalf("Dispatch error = %v, want %v", err, ErrDisplayError)
271	}
272
273	var derr *DisplayError
274	if !errors.As(err, &derr) {
275		t.Fatalf("Dispatch error type = %T, want *DisplayError", err)
276	}
277	if derr.ObjectID != 7 || derr.Code != 3 || derr.Message != "boom" {
278		t.Fatalf("DisplayError = %+v, want object=7 code=3 message=boom", derr)
279	}
280}
281
282func TestDisplayDispatch_DeleteID(t *testing.T) {
283	d, records := newChunkDisplay([][]byte{message(displayObjectID, 1, []byte{7, 0, 0, 0})})
284	for d.nextID <= 7 {
285		d.allocateID()
286	}
287	d.objects.Delete(uint32(7))
288
289	if err := d.Dispatch(); err != nil {
290		t.Fatalf("Dispatch: %v", err)
291	}
292	if _, ok := d.objects.Load(uint32(7)); ok {
293		t.Fatal("delete_id did not remove object 7")
294	}
295	if len(*records) != 0 {
296		t.Fatalf("records = %#v, want none", *records)
297	}
298}
299
300func TestDisplayDispatch_PeerClose(t *testing.T) {
301	cases := []struct {
302		name   string
303		chunks [][]byte
304	}{
305		{name: "closed before any byte"},
306		{name: "closed mid header", chunks: [][]byte{{0x01, 0x02, 0x03}}},
307		{
308			name:   "closed mid body",
309			chunks: [][]byte{append(header(7, 3, 16), 0x01, 0x02, 0x03, 0x04)},
310		},
311		{
312			name:   "closed after a complete message",
313			chunks: [][]byte{message(7, 4, []byte{0x01, 0x02, 0x03, 0x04})},
314		},
315	}
316
317	for _, tc := range cases {
318		t.Run(tc.name, func(t *testing.T) {
319			d, _ := newChunkDisplay(tc.chunks)
320
321			var err error
322			for i := 0; i < 4; i++ {
323				if err = d.Dispatch(); err != nil {
324					break
325				}
326			}
327			if !errors.Is(err, io.ErrUnexpectedEOF) {
328				t.Fatalf("Dispatch error = %v, want %v", err, io.ErrUnexpectedEOF)
329			}
330		})
331	}
332}
333
334func TestDisplayDispatch_ClosedReturnsErrClosed(t *testing.T) {
335	d, _ := newChunkDisplay([][]byte{message(7, 3, nil)})
336	if err := d.Close(); err != nil {
337		t.Fatalf("Close: %v", err)
338	}
339	if err := d.Dispatch(); !errors.Is(err, net.ErrClosed) {
340		t.Fatalf("Dispatch after Close = %v, want %v", err, net.ErrClosed)
341	}
342}
343
344func TestReadFrame(t *testing.T) {
345	body := []byte{0x0a, 0x0b, 0x0c, 0x0d}
346	object, opcode, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, body)}}, DefaultMaxMessageSize)
347	if err != nil {
348		t.Fatalf("readFrame: %v", err)
349	}
350	if object != 7 || opcode != 3 || !reflect.DeepEqual(got, body) {
351		t.Fatalf("readFrame = (%d, %d, %x), want (7, 3, %x)", object, opcode, got, body)
352	}
353
354	// A header-only message has an empty body.
355	if _, _, got, err := readFrame(&chunkConn{chunks: [][]byte{message(7, 3, nil)}}, 0); err != nil || len(got) != 0 {
356		t.Fatalf("readFrame(header only) = (%x, %v), want (empty, nil)", got, err)
357	}
358
359	// Truncated bodies are reported, never returned partially.
360	if _, _, _, err := readFrame(&chunkConn{chunks: [][]byte{append(header(7, 3, 16), 0x01)}}, 0); !errors.Is(err, io.ErrUnexpectedEOF) {
361		t.Fatalf("readFrame(truncated) error = %v, want %v", err, io.ErrUnexpectedEOF)
362	}
363}
364
365func TestDeleteIDReuseAndInvalid(t *testing.T) {
366	d := newDisplay(&chunkConn{})
367	id := d.AllocateID()
368	body := make([]byte, 4)
369	binary.LittleEndian.PutUint32(body, id)
370	if err := d.handleDisplayEvent(1, body); err != nil {
371		t.Fatal(err)
372	}
373	if err := d.handleDisplayEvent(1, body); err == nil {
374		t.Fatal("duplicate delete_id accepted")
375	}
376	d.nextID = 0xff000000 // simulate exhaustion
377	if got := d.AllocateID(); got != id {
378		t.Fatalf("reused %d, want %d", got, id)
379	}
380	if got := d.AllocateID(); got != 0 {
381		t.Fatalf("allocated server-range ID %x", got)
382	}
383	d.objects.Store(id, d)
384	if err := d.handleDisplayEvent(1, body); err == nil {
385		t.Fatal("live ID deleted")
386	}
387	for _, bad := range []uint32{0, 1, 0xff000001} {
388		binary.LittleEndian.PutUint32(body, bad)
389		if err := d.handleDisplayEvent(1, body); err == nil {
390			t.Fatalf("accepted reserved ID %x", bad)
391		}
392	}
393}

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