detached 15e9011

runtime_test.go RawHistoryFolder

1package server
2
3import (
4	"bytes"
5	"encoding/binary"
6	"runtime"
7	"testing"
8	"time"
9
10	"golang.org/x/sys/unix"
11)
12
13// Tests in this file drive the runtime with hand-built interface tables and a
14// raw wire-protocol peer: no client library and no generated bindings.
15
16func i32(v int32) []byte { return u32(uint32(v)) }
17
18func wireArray(b []byte) []byte {
19	out := u32(uint32(len(b)))
20	out = append(out, b...)
21	for len(out)%4 != 0 {
22		out = append(out, 0)
23	}
24	return out
25}
26
27// wireEvent is one message read from the server.
28type wireEvent struct {
29	id     uint32
30	opcode uint16
31	body   []byte
32}
33
34func readFull(t *testing.T, fd, n int) ([]byte, bool) {
35	t.Helper()
36	buf := make([]byte, n)
37	for got := 0; got < n; {
38		m, err := unix.Read(fd, buf[got:])
39		if err == unix.EINTR {
40			continue
41		}
42		if err != nil {
43			t.Fatalf("read: %v", err)
44		}
45		if m == 0 {
46			if got != 0 {
47				t.Fatalf("EOF after %d of %d bytes", got, n)
48			}
49			return nil, false
50		}
51		got += m
52	}
53	return buf, true
54}
55
56// readEvent reads one message; ok is false on a clean EOF.
57func readEvent(t *testing.T, fd int) (ev wireEvent, ok bool) {
58	t.Helper()
59	hdr, ok := readFull(t, fd, 8)
60	if !ok {
61		return ev, false
62	}
63	size := int(binary.LittleEndian.Uint32(hdr[4:]) >> 16)
64	ev = wireEvent{id: binary.LittleEndian.Uint32(hdr), opcode: uint16(binary.LittleEndian.Uint32(hdr[4:]))}
65	if size > 8 {
66		ev.body, _ = readFull(t, fd, size-8)
67	}
68	return ev, true
69}
70
71// rtEnv is a running display with one adopted client and the raw peer end.
72type rtEnv struct {
73	d      *Display
74	client Client
75	peer   int
76}
77
78func newRTEnv(t *testing.T) *rtEnv {
79	t.Helper()
80	d, _ := clientDisplay(t)
81	fds := socketPair(t)
82	c := adopt(t, d, fds.srv)
83	if err := unix.SetsockoptTimeval(fds.peer, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 3}); err != nil {
84		t.Fatal(err)
85	}
86	return &rtEnv{d: d, client: c, peer: fds.peer}
87}
88
89func (e *rtEnv) do(t *testing.T, fn func()) {
90	t.Helper()
91	if !e.d.Do(fn) {
92		t.Fatal("display stopped")
93	}
94}
95
96func (e *rtEnv) send(t *testing.T, msg []byte) {
97	t.Helper()
98	if _, err := unix.Write(e.peer, msg); err != nil {
99		t.Fatal(err)
100	}
101}
102
103func (e *rtEnv) resource(t *testing.T, iface *Interface, id uint32, h Handler) *Resource {
104	t.Helper()
105	var r *Resource
106	var err error
107	e.do(t, func() { r, err = e.client.CreateResource(iface, 3, id, h) })
108	if err != nil {
109		t.Fatal(err)
110	}
111	return r
112}
113
114func rtInterface(t *testing.T, name string) *Interface {
115	t.Helper()
116	iface := &Interface{
117		Name:    name,
118		Version: 3,
119		Requests: []Message{
120			{Name: "all", Signature: "iufsaon"},
121			{Name: "nulls", Signature: "?s?o"},
122		},
123		Events: []Message{
124			{Name: "all", Signature: "iufsaon"},
125			{Name: "nulls", Signature: "?s?oa"},
126		},
127	}
128	if err := NewInterfaces(iface); err != nil {
129		t.Fatal(err)
130	}
131	return iface
132}
133
134type rtRequest struct {
135	i      int32
136	u      uint32
137	f      Fixed
138	s      string
139	a      []byte
140	obj    *Resource
141	newID  uint32
142	nullS  string
143	nullO  *Resource
144	objID  uint32
145	opcode uint32
146}
147
148func TestRuntimeArgumentRoundTrip(t *testing.T) {
149	iface := rtInterface(t, "runtime_round_trip_test")
150	e := newRTEnv(t)
151	// The handler runs on the display goroutine: it reports what it decoded
152	// over a channel, which also orders it before the test's reads.
153	reqs := make(chan rtRequest, 2)
154	var newRes *Resource // written by the handler, read via e.do after the event
155	handler := func(r *Resource, opcode uint32, args []Arg) {
156		got := rtRequest{opcode: opcode}
157		defer func() { reqs <- got }()
158		switch opcode {
159		case 0:
160			got.i, got.u, got.f = args[0].Int(), args[1].Uint(), args[2].Fixed()
161			got.s, got.a, got.obj, got.newID = args[3].String(), args[4].Array(), args[5].Resource(), args[6].NewID()
162			got.objID = got.obj.ID()
163			// Echo everything back as an event, with a server-created resource
164			// for the new_id argument.
165			var err error
166			if newRes, err = r.Client().CreateResource(iface, 1, 0, nil); err != nil {
167				t.Error(err)
168				return
169			}
170			var p runtime.Pinner
171			defer p.Unpin()
172			r.PostEvent(0, Int(got.i), Uint(got.u), FixedArg(FixedFromFloat(got.f.Float())),
173				String(&p, got.s, false), Array(&p, got.a), Object(got.obj), NewID(newRes))
174		case 1:
175			got.nullS, got.nullO = args[0].String(), args[1].Resource()
176			var p runtime.Pinner
177			defer p.Unpin()
178			r.PostEvent(1, String(&p, "", true), Object(nil), Array(&p, nil))
179		}
180	}
181	e.resource(t, iface, 2, handler)
182
183	payload := []byte{1, 2, 3, 4, 5}
184	e.send(t, wireMsg(2, 0, i32(-7), u32(0xdeadbeef), i32(-384), wireString("héllo"), wireArray(payload), u32(2), u32(3)))
185	ev, ok := readEvent(t, e.peer)
186	if !ok {
187		t.Fatal("EOF before event")
188	}
189	got := <-reqs
190	if got.i != -7 || got.u != 0xdeadbeef || got.f.Float() != -1.5 || got.s != "héllo" ||
191		!bytes.Equal(got.a, payload) || got.obj == nil || got.objID != 2 || got.newID != 3 {
192		t.Fatalf("request decoded as %+v", got)
193	}
194	var newID uint32
195	e.do(t, func() { newID = newRes.ID() })
196	if newID < 0xff000000 {
197		t.Fatalf("server-allocated id %#x", newID)
198	}
199	want := wireMsg(2, 0, i32(-7), u32(0xdeadbeef), i32(-384), wireString("héllo"), wireArray(payload), u32(2), u32(newID))
200	have := wireMsg(ev.id, ev.opcode, ev.body)
201	if !bytes.Equal(have, want) {
202		t.Fatalf("event bytes\n got %x\nwant %x", have, want)
203	}
204
205	// Null string and object in, null string and empty array out.
206	e.send(t, wireMsg(2, 1, u32(0), u32(0)))
207	ev, ok = readEvent(t, e.peer)
208	if !ok {
209		t.Fatal("EOF before nulls event")
210	}
211	got = <-reqs
212	if got.opcode != 1 || got.nullS != "" || got.nullO != nil {
213		t.Fatalf("null request decoded as %+v", got)
214	}
215	want = wireMsg(2, 1, u32(0), u32(0), wireArray(nil))
216	if have := wireMsg(ev.id, ev.opcode, ev.body); !bytes.Equal(have, want) {
217		t.Fatalf("nulls event\n got %x\nwant %x", have, want)
218	}
219}
220
221func TestResourcePostError(t *testing.T) {
222	iface := rtInterface(t, "runtime_post_error_test")
223	e := newRTEnv(t)
224	r := e.resource(t, iface, 2, nil)
225	gone := make(chan struct{})
226	e.do(t, func() {
227		e.client.OnDestroy(func() { close(gone) })
228		r.PostError(7, "100% bad")
229	})
230	ev, ok := readEvent(t, e.peer)
231	if !ok {
232		t.Fatal("EOF before wl_display.error")
233	}
234	want := wireMsg(1, 0, u32(2), u32(7), wireString("100% bad"))
235	if have := wireMsg(ev.id, ev.opcode, ev.body); !bytes.Equal(have, want) {
236		t.Fatalf("error event\n got %x\nwant %x", have, want)
237	}
238	// libwayland disconnects the client after a protocol error, once the
239	// connection is next serviced: send a wl_display.sync to trigger that.
240	e.send(t, wireMsg(1, 0, u32(3)))
241	select {
242	case <-gone:
243	case <-time.After(3 * time.Second):
244		t.Fatal("client not disconnected after the protocol error")
245	}
246	if _, ok := readEvent(t, e.peer); ok {
247		t.Fatal("unexpected data after the error")
248	}
249	e.do(t, func() {
250		if r.Alive() {
251			t.Error("resource alive after the client was disconnected")
252		}
253		r.PostError(1, "ignored") // no-op once gone
254	})
255}
256
257func TestGlobalRemoveEmitsGlobalRemove(t *testing.T) {
258	iface := rtInterface(t, "runtime_global_remove_test")
259	e := newRTEnv(t)
260	var g *Global
261	var err error
262	e.do(t, func() {
263		g, err = e.d.AddGlobal(iface, 3, func(Client, uint32, uint32) {})
264	})
265	if err != nil {
266		t.Fatal(err)
267	}
268	e.send(t, wireMsg(1, 1, u32(2))) // wl_display.get_registry(new_id 2)
269	var name uint32
270	for name == 0 {
271		ev, ok := readEvent(t, e.peer)
272		if !ok {
273			t.Fatal("EOF before the global was advertised")
274		}
275		if ev.id != 2 || ev.opcode != 0 {
276			continue
277		}
278		strLen := int(binary.LittleEndian.Uint32(ev.body[4:]))
279		if string(ev.body[8:8+strLen-1]) == iface.Name {
280			name = binary.LittleEndian.Uint32(ev.body)
281		}
282	}
283	e.do(t, func() {
284		g.Remove()
285		g.Remove() // idempotent
286		(*Global)(nil).Remove()
287	})
288	ev, ok := readEvent(t, e.peer)
289	if !ok {
290		t.Fatal("EOF before global_remove")
291	}
292	if ev.id != 2 || ev.opcode != 1 || !bytes.Equal(ev.body, u32(name)) {
293		t.Fatalf("got %+v, want wl_registry.global_remove(%d)", ev, name)
294	}
295}
296
297func TestResourceDestroyLifecycle(t *testing.T) {
298	iface := rtInterface(t, "runtime_destroy_test")
299	e := newRTEnv(t)
300	r := e.resource(t, iface, 2, nil)
301	destroyed := 0
302	e.do(t, func() {
303		r.OnDestroy = func() { destroyed++ }
304		before := LiveResources()
305		if v := r.Version(); v != 3 {
306			t.Errorf("Version = %d, want 3", v)
307		}
308		if r.Iface() != iface || r.ID() != 2 || r.Client() != e.client || !r.Alive() {
309			t.Errorf("accessors before destroy: %v %d %v %v", r.Iface(), r.ID(), r.Client(), r.Alive())
310		}
311		r.Destroy()
312		r.Destroy() // no-op once gone
313		if n := LiveResources(); n != before-1 {
314			t.Errorf("LiveResources %d -> %d, want one fewer", before, n)
315		}
316		if destroyed != 1 || r.Alive() {
317			t.Errorf("destroy callback ran %d times, alive %v", destroyed, r.Alive())
318		}
319		if r.Version() != 0 || r.ID() != 0 || r.Client() != (Client{}) {
320			t.Errorf("accessors after destroy: %d %d %v", r.Version(), r.ID(), r.Client())
321		}
322		r.PostEvent(0, Int(0), Uint(0), FixedArg(0), Arg(0), Arg(0), Arg(0), Arg(0)) // no-op
323	})
324	// Destroying a client-allocated id tells the client it can reuse it. A
325	// wl_display.sync round trip (callback id 3) ensures the events are flushed.
326	e.send(t, wireMsg(1, 0, u32(3)))
327	var sawDelete bool
328	for done := false; !done; {
329		ev, ok := readEvent(t, e.peer)
330		if !ok {
331			t.Fatal("EOF before the sync callback")
332		}
333		switch {
334		case ev.id == 1 && ev.opcode == 1 && bytes.Equal(ev.body, u32(2)): // wl_display.delete_id(2)
335			sawDelete = true
336		case ev.id == 3: // wl_callback.done
337			done = true
338		}
339	}
340	if !sawDelete {
341		t.Fatal("no wl_display.delete_id(2) after Destroy")
342	}
343}

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 14

↑↓ moveenter openesc close