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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | + | } | |||