detached 4ced4ef

test(server): cover runtime argument marshalling, errors and lifecycle without e2e

commit
4ced4ef9f6caf5ca97f16e69816ddc4c9094bde0
author
brice · · 2026-10-01 05:55 UTC
parent
b432003
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +343 -0
A server/runtime_test.go +343 -0
@@ -0,0 +1,343 @@
1+package server
2+
3+import (
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+
16+func i32(v int32) []byte { return u32(uint32(v)) }
17+
18+func 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.
28+type wireEvent struct {
29+ id uint32
30+ opcode uint16
31+ body []byte
32+}
33+
34+func 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.
57+func 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.
72+type rtEnv struct {
73+ d *Display
74+ client Client
75+ peer int
76+}
77+
78+func 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+
89+func (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+
96+func (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+
103+func (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+
114+func 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+
134+type 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+
148+func 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+
221+func 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+
257+func 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+
297+func 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+}
@@ -0,0 +1,343 @@
1+package server
2+
3+import (
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+
16+func i32(v int32) []byte { return u32(uint32(v)) }
17+
18+func 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.
28+type wireEvent struct {
29+ id uint32
30+ opcode uint16
31+ body []byte
32+}
33+
34+func 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.
57+func 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.
72+type rtEnv struct {
73+ d *Display
74+ client Client
75+ peer int
76+}
77+
78+func 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+
89+func (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+
96+func (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+
103+func (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+
114+func 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+
134+type 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+
148+func 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+
221+func 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+
257+func 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+
297+func 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