1//go:build linux
2
3package wayland
4
5import (
6 "context"
7 "encoding/binary"
8 "errors"
9 "fmt"
10 "io"
11 "net"
12 "os"
13 "path/filepath"
14 "strings"
15 "testing"
16 "time"
17
18 "github.com/bnema/nefergui/internal/harness"
19 "github.com/bnema/wlturbo"
20 "github.com/bnema/wlturbo/protocol/core"
21 "github.com/bnema/wlturbo/wl"
22)
23
24func TestLayerConstantsMatchProtocol(t *testing.T) {
25 if LayerBackground != 0 || LayerBottom != 1 || LayerTop != 2 || LayerOverlay != 3 {
26 t.Fatal("layer values")
27 }
28 if AnchorTop != 1 || AnchorBottom != 2 || AnchorLeft != 4 || AnchorRight != 8 {
29 t.Fatal("anchor bits")
30 }
31 if KeyboardNone != 0 || KeyboardExclusive != 1 || KeyboardOnDemand != 2 {
32 t.Fatal("keyboard modes")
33 }
34}
35
36func TestSurfaceOptionsValidate(t *testing.T) {
37 ok := LayerOptions{Layer: LayerOverlay, Anchor: AnchorTop | AnchorLeft | AnchorRight | AnchorBottom, Keyboard: KeyboardOnDemand}
38 for name, tc := range map[string]struct {
39 opts SurfaceOptions
40 bad bool
41 }{
42 "zero": {},
43 "valid layer": {opts: SurfaceOptions{Layer: &ok}},
44 "empty passthru": {opts: SurfaceOptions{InputRects: []Rect{}}},
45 "layer 4": {opts: SurfaceOptions{Layer: &LayerOptions{Layer: 4}}, bad: true},
46 "anchor 16": {opts: SurfaceOptions{Layer: &LayerOptions{Anchor: 16}}, bad: true},
47 "zone -2": {opts: SurfaceOptions{Layer: &LayerOptions{ExclusiveZone: -2}}, bad: true},
48 "zone -1": {opts: SurfaceOptions{Layer: &LayerOptions{ExclusiveZone: -1}}},
49 "keyboard 3": {opts: SurfaceOptions{Layer: &LayerOptions{Keyboard: 3}}, bad: true},
50 "namespace NUL": {opts: SurfaceOptions{Layer: &LayerOptions{Namespace: "a\x00b"}}, bad: true},
51 "zero-size rect": {opts: SurfaceOptions{InputRects: []Rect{{0, 0, 0, 5}}}, bad: true},
52 "overflow rect": {opts: SurfaceOptions{InputRects: []Rect{{1<<31 - 2, 0, 5, 5}}}, bad: true},
53 "negative origin": {opts: SurfaceOptions{InputRects: []Rect{{-5, -5, 10, 10}}}},
54 } {
55 if err := tc.opts.Validate(); (err != nil) != tc.bad {
56 t.Errorf("%s: err=%v", name, err)
57 }
58 }
59 if _, err := ConnectWithOptions(context.Background(), "", 10, 10, false, SurfaceOptions{Layer: &LayerOptions{Layer: 9}}); err == nil || !strings.Contains(err.Error(), "invalid layer") {
60 t.Fatalf("connect must validate before dialing: %v", err)
61 }
62}
63
64func TestLayerConfigureSizeBounds(t *testing.T) {
65 if w, h, err := layerConfigureSize(0, 16384); err != nil || w != 0 || h != 16384 {
66 t.Fatal(w, h, err)
67 }
68 if _, _, err := layerConfigureSize(16385, 1); err == nil {
69 t.Fatal("oversize accepted")
70 }
71 w := &Window{Width: 100, Height: 50}
72 w.applyLayerSize(0, 40)
73 if w.Width != 100 || w.Height != 40 || !w.Dirty {
74 t.Fatalf("%+v", w)
75 }
76}
77
78// A post-startup configure must carry size and serial together, even when the
79// previous frame is ready. Applying it must ack before the owner can commit.
80func TestLayerConfigureAfterReaderStart(t *testing.T) {
81 srv, accept := newWireServer(t)
82 type dialed struct {
83 d *wlturbo.Display
84 err error
85 }
86 ch := make(chan dialed, 1)
87 go func() {
88 d, err := wlturbo.Connect(srv.path)
89 ch <- dialed{d, err}
90 }()
91 conn := accept()
92 r := <-ch
93 if r.err != nil {
94 t.Fatal(r.err)
95 }
96 d := r.d
97 defer d.Close()
98 comp := core.NewCompositor(d.Context())
99 if err := d.Registry().Bind(1, core.CompositorInterface, 6, comp); err != nil {
100 t.Fatal(err)
101 }
102 surface, err := comp.CreateSurface()
103 if err != nil {
104 t.Fatal(err)
105 }
106 w := &Window{Display: d, Compositor: comp, Surface: surface, Width: 100, Height: 40,
107 Configured: true, FrameReady: true, events: make(chan Event, 8), readerStop: make(chan struct{})}
108 if err := w.bindLayerShell(func(iface string, max, min uint32, p wl.Proxy) error {
109 return d.Registry().Bind(2, iface, max, p)
110 }); err != nil {
111 t.Fatal(err)
112 }
113 if err := w.createLayerSurface(LayerOptions{Layer: LayerTop}); err != nil {
114 t.Fatal(err)
115 }
116 w.StartReader()
117 defer func() {
118 close(w.readerStop)
119 _ = d.Close()
120 <-w.readerDone
121 }()
122 for _, tc := range []struct {
123 serial, width, height uint32
124 wantW, wantH int32
125 ready bool
126 }{{7, 200, 50, 200, 50, true}, {8, 0, 60, 200, 60, false}} {
127 w.FrameReady, w.Dirty = tc.ready, false
128 if _, err := conn.Write(frame(w.LayerSurface.ID(), 0, tc.serial, tc.width, tc.height)); err != nil {
129 t.Fatal(err)
130 }
131 var ev Event
132 select {
133 case ev = <-w.Events():
134 case <-time.After(5 * time.Second):
135 t.Fatal("no configure event")
136 }
137 if ev.Kind != ConfigureLayer || ev.Width != int32(tc.width) || ev.Height != int32(tc.height) || ev.Serial != tc.serial {
138 t.Fatalf("configure must be one size+serial event: %+v", ev)
139 }
140 if err := w.Apply(ev); err != nil {
141 t.Fatal(err)
142 }
143 if w.Width != tc.wantW || w.Height != tc.wantH || !w.Dirty || !w.Configured || !w.FrameReady {
144 t.Fatalf("configure state: size=%dx%d dirty=%v configured=%v ready=%v", w.Width, w.Height, w.Dirty, w.Configured, w.FrameReady)
145 }
146 if err := surface.Commit(); err != nil {
147 t.Fatal(err)
148 }
149 // Ignore setup requests, but require exactly one ack of this configure
150 // before the first subsequent surface commit.
151 _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
152 acks := 0
153 for {
154 var hdr [8]byte
155 if _, err := io.ReadFull(conn, hdr[:]); err != nil {
156 t.Fatal(err)
157 }
158 obj, word := binary.NativeEndian.Uint32(hdr[:]), binary.NativeEndian.Uint32(hdr[4:])
159 size, op := int(word>>16), uint16(word)
160 if size < 8 {
161 t.Fatalf("bad frame size %d", size)
162 }
163 args := make([]byte, size-8)
164 if _, err := io.ReadFull(conn, args); err != nil {
165 t.Fatal(err)
166 }
167 if obj == w.LayerSurface.ID() && op == 6 {
168 if len(args) != 4 || binary.NativeEndian.Uint32(args) != tc.serial {
169 t.Fatalf("wrong ack payload %x, want serial %d", args, tc.serial)
170 }
171 acks++
172 }
173 if obj == surface.ID() && op == 6 {
174 if acks != 1 {
175 t.Fatalf("commit preceded by %d configure acks, want 1", acks)
176 }
177 break
178 }
179 }
180 }
181}
182
183// Harness acceptance: a real NeferWL compositor and the real client code.
184func runLayerHarness(t *testing.T, child string) (string, string) {
185 t.Helper()
186 if os.Getenv("NEFERGUI_HARNESS") != "1" {
187 t.Skip("set NEFERGUI_HARNESS=1 and NEFERGUI_NEFERWL for the layer-shell harness")
188 }
189 root := filepath.Join(t.TempDir(), "artifacts")
190 result, code := harness.Run(context.Background(), harness.Options{Size: "400x300", Scale: "1", Layout: "us", Background: "#111111", Out: root, AllowUnpinned: os.Getenv("NEFERGUI_NEFERWL") != "", Timeout: 30 * time.Second, ReadyTimeout: 10 * time.Second, Client: []string{os.Args[0], "-test.run=^" + child + "$", "-test.v"}})
191 out, _ := os.ReadFile(result.Artifacts["client_stdout"])
192 errOut, _ := os.ReadFile(result.Artifacts["client_stderr"])
193 if code != harness.Pass {
194 t.Fatalf("code=%d status=%s error=%s\nstdout:\n%s\nstderr:\n%s", code, result.Status, result.Error, out, errOut)
195 }
196 return string(out), string(errOut)
197}
198
199func TestHarnessLayerSurface(t *testing.T) {
200 out, _ := runLayerHarness(t, "TestHarnessLayerSurfaceClient")
201 for _, want := range []string{"LAYER configured", "LAYER height=40", "LAYER input-passthrough", "LAYER input-default", "LAYER closed"} {
202 if !strings.Contains(out, want) {
203 t.Fatalf("missing %q in:\n%s", want, out)
204 }
205 }
206}
207
208func TestHarnessLayerOutputSelection(t *testing.T) {
209 out, _ := runLayerHarness(t, "TestHarnessLayerOutputClient")
210 for _, want := range []string{"OUTPUT exact ok", "OUTPUT missing rejected"} {
211 if !strings.Contains(out, want) {
212 t.Fatalf("missing %q in:\n%s", want, out)
213 }
214 }
215}
216
217func TestHarnessXDGDefaultUnchanged(t *testing.T) {
218 out, _ := runLayerHarness(t, "TestHarnessXDGClient")
219 if !strings.Contains(out, "XDG configured toplevel") {
220 t.Fatal(out)
221 }
222}
223
224func harnessChild(t *testing.T) {
225 t.Helper()
226 if os.Getenv("NEFERGUI_DEBUG_DIR") == "" {
227 t.Skip("harness child only")
228 }
229}
230
231func TestHarnessLayerSurfaceClient(t *testing.T) {
232 harnessChild(t)
233 w, err := ConnectWithOptions(context.Background(), "", 100, 40, true, SurfaceOptions{
234 InputRects: []Rect{{0, 0, 10, 10}},
235 Layer: &LayerOptions{
236 Layer: LayerTop, Anchor: AnchorTop | AnchorLeft | AnchorRight, Keyboard: KeyboardOnDemand,
237 ExclusiveZone: 40, Margin: [4]int32{2, 3, 4, 5}, Namespace: "nefergui-test",
238 },
239 })
240 if err != nil {
241 t.Fatal(err)
242 }
243 if !w.IsLayer() || w.Toplevel != nil || w.XdgSurface != nil || w.Shell != nil {
244 t.Fatal("layer window must not create xdg objects")
245 }
246 if !w.Configured || w.Width < 100 {
247 t.Fatalf("not configured: %+v", w)
248 }
249 fmt.Println("LAYER configured", w.Width, w.Height)
250 if w.Height == 40 {
251 fmt.Println("LAYER height=40")
252 }
253 if err = w.SetTitle("ignored"); err != nil {
254 t.Fatal(err)
255 }
256 // Empty non-nil region: pass-through; nil: default full surface.
257 if err = w.SetInputRects([]Rect{}); err != nil {
258 t.Fatal(err)
259 }
260 if err = w.SetInputRects([]Rect{}); err != nil {
261 t.Fatal(err)
262 }
263 fmt.Println("LAYER input-passthrough")
264 if err = w.SetInputRects(nil); err != nil {
265 t.Fatal(err)
266 }
267 fmt.Println("LAYER input-default")
268 // A roundtrip proves the compositor processed and accepted every request
269 // (a protocol error would fail it); only then does the reader take over.
270 if err = w.Display.Roundtrip(); err != nil {
271 t.Fatal(err)
272 }
273 // Hand dispatch to the reader and prove it works: a close from the
274 // owner side must still tear down cleanly.
275 events := w.StartReader()
276 for drained := false; !drained; {
277 select {
278 case ev := <-events:
279 if ev.Kind == TransportError || ev.Kind == CloseEvent {
280 t.Fatalf("unexpected %+v", ev)
281 }
282 if err = w.Apply(ev); err != nil {
283 t.Fatal(err)
284 }
285 default:
286 drained = true
287 }
288 }
289 if err = w.Close(); err != nil {
290 t.Fatal(err)
291 }
292 fmt.Println("LAYER closed")
293}
294
295func TestHarnessLayerOutputClient(t *testing.T) {
296 harnessChild(t)
297 d, err := wl.Connect("")
298 if err != nil {
299 t.Fatal(err)
300 }
301 if err = d.Roundtrip(); err != nil {
302 t.Fatal(err)
303 }
304 var name string
305 for _, g := range d.Registry().GetGlobals() {
306 if g.Interface != core.OutputInterface {
307 continue
308 }
309 if g.Version < outputNameVersion {
310 t.Fatalf("wl_output v%d lacks names", g.Version)
311 }
312 o := core.NewOutput(d.Context())
313 o.OnName(func(n string) { name = n })
314 if err = d.Registry().Bind(g.Name, core.OutputInterface, outputNameVersion, o); err != nil {
315 t.Fatal(err)
316 }
317 break
318 }
319 if err = d.Roundtrip(); err != nil {
320 t.Fatal(err)
321 }
322 _ = d.Close()
323 if name == "" {
324 t.Fatal("no output name")
325 }
326 w, err := ConnectWithOptions(context.Background(), "", 100, 40, false, SurfaceOptions{Layer: &LayerOptions{Output: name, Layer: LayerBottom, Anchor: AnchorBottom | AnchorLeft | AnchorRight}})
327 if err != nil {
328 t.Fatalf("output %q: %v", name, err)
329 }
330 if err = w.Close(); err != nil {
331 t.Fatal(err)
332 }
333 fmt.Println("OUTPUT exact ok", name)
334 _, err = ConnectWithOptions(context.Background(), "", 100, 40, false, SurfaceOptions{Layer: &LayerOptions{Output: name + "-missing"}})
335 var ce *CapabilityError
336 if !errors.As(err, &ce) {
337 t.Fatalf("missing output error: %v", err)
338 }
339 fmt.Println("OUTPUT missing rejected")
340}
341
342func TestHarnessXDGClient(t *testing.T) {
343 harnessChild(t)
344 w, err := Connect("", 100, 50, false)
345 if err != nil {
346 t.Fatal(err)
347 }
348 if w.IsLayer() || w.Toplevel == nil || w.XdgSurface == nil || !w.Configured {
349 t.Fatal("default must be xdg toplevel")
350 }
351 if err = w.SetTitle("x"); err != nil {
352 t.Fatal(err)
353 }
354 fmt.Println("XDG configured toplevel")
355 if err = w.Close(); err != nil {
356 t.Fatal(err)
357 }
358}
359
360// wireServer is a real unix-socket Wayland peer; tests speak raw frames.
361type wireServer struct {
362 conn net.Conn
363 path string
364}
365
366func newWireServer(t *testing.T) (*wireServer, func() net.Conn) {
367 t.Helper()
368 dir := t.TempDir()
369 path := filepath.Join(dir, "wl-test")
370 ln, err := net.Listen("unix", path)
371 if err != nil {
372 t.Fatal(err)
373 }
374 t.Cleanup(func() { _ = ln.Close() })
375 srv := &wireServer{path: path}
376 accepted := make(chan net.Conn, 1)
377 go func() {
378 c, e := ln.Accept()
379 if e == nil {
380 accepted <- c
381 }
382 }()
383 return srv, func() net.Conn {
384 select {
385 case c := <-accepted:
386 srv.conn = c
387 t.Cleanup(func() { _ = c.Close() })
388 return c
389 case <-time.After(5 * time.Second):
390 t.Fatal("client never connected")
391 return nil
392 }
393 }
394}
395
396func frame(object uint32, opcode uint16, args ...uint32) []byte {
397 b := make([]byte, 8+4*len(args))
398 binary.NativeEndian.PutUint32(b, object)
399 binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(opcode))
400 for i, a := range args {
401 binary.NativeEndian.PutUint32(b[8+4*i:], a)
402 }
403 return b
404}
405
406// A compositor that never answers must not block startup past ctx; the
407// connection is closed so the server sees EOF.
408func TestConnectWithOptionsCancelsHangingServer(t *testing.T) {
409 srv, accept := newWireServer(t)
410 ctx, cancel := context.WithCancel(context.Background())
411 type result struct {
412 w *Window
413 err error
414 }
415 done := make(chan result, 1)
416 go func() {
417 w, err := ConnectWithOptions(ctx, srv.path, 100, 40, false, SurfaceOptions{})
418 done <- result{w, err}
419 }()
420 conn := accept()
421 buf := make([]byte, 64)
422 if _, err := conn.Read(buf); err != nil { // get_registry / sync requests: client is now blocked
423 t.Fatal(err)
424 }
425 select {
426 case <-done:
427 t.Fatal("returned before cancel")
428 case <-time.After(100 * time.Millisecond):
429 }
430 cancel()
431 select {
432 case r := <-done:
433 if r.w != nil || !errors.Is(r.err, context.Canceled) {
434 t.Fatalf("window=%v err=%v", r.w, r.err)
435 }
436 case <-time.After(5 * time.Second):
437 t.Fatal("ConnectWithOptions still blocked after cancel")
438 }
439 _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
440 for {
441 if _, err := conn.Read(buf); err != nil {
442 if !errors.Is(err, io.EOF) {
443 t.Fatalf("server expected EOF (client closed), got %v", err)
444 }
445 break
446 }
447 }
448}
449
450func TestConnectWithOptionsPrecancelledAndNil(t *testing.T) {
451 ctx, cancel := context.WithCancel(context.Background())
452 cancel()
453 if _, err := ConnectWithOptions(ctx, "/nonexistent", 1, 1, false, SurfaceOptions{}); !errors.Is(err, context.Canceled) {
454 t.Fatal(err)
455 }
456 //lint:ignore SA1012 nil context is an explicit error case
457 if _, err := ConnectWithOptions(nil, "/nonexistent", 1, 1, false, SurfaceOptions{}); err == nil {
458 t.Fatal("nil context accepted")
459 }
460}
461
462// Removal of the selected output's registry global closes the window, before
463// and after the reader starts; other globals are ignored.
464func TestSelectedOutputRemovalClosesWindow(t *testing.T) {
465 for _, reader := range []bool{false, true} {
466 t.Run(fmt.Sprintf("reader=%v", reader), func(t *testing.T) {
467 srv, accept := newWireServer(t)
468 type dialed struct {
469 d *wlturbo.Display
470 err error
471 }
472 ch := make(chan dialed, 1)
473 go func() {
474 d, err := wlturbo.Connect(srv.path)
475 ch <- dialed{d, err}
476 }()
477 conn := accept()
478 r := <-ch
479 if r.err != nil {
480 t.Fatal(r.err)
481 }
482 d := r.d
483 defer d.Close()
484 w := &Window{Display: d, events: make(chan Event, 8), readerStop: make(chan struct{}), layerOutputSelected: true, layerOutputGlobal: 7}
485 d.Registry().AddGlobalRemoveHandler(outputRemoved{w})
486 if reader {
487 w.StartReader()
488 }
489 regID := d.Registry().ID()
490 if _, err := conn.Write(frame(regID, 1, 9)); err != nil { // other global
491 t.Fatal(err)
492 }
493 if !reader {
494 if err := d.Dispatch(); err != nil {
495 t.Fatal(err)
496 }
497 if w.Closed {
498 t.Fatal("unrelated removal closed the window")
499 }
500 } else {
501 select {
502 case ev := <-w.Events():
503 t.Fatalf("unrelated removal posted %+v", ev)
504 case <-time.After(100 * time.Millisecond):
505 }
506 }
507 if _, err := conn.Write(frame(regID, 1, 7)); err != nil {
508 t.Fatal(err)
509 }
510 if !reader {
511 if err := d.Dispatch(); err != nil {
512 t.Fatal(err)
513 }
514 if !w.Closed {
515 t.Fatal("selected output removal did not close")
516 }
517 return
518 }
519 select {
520 case ev := <-w.Events():
521 if ev.Kind != CloseEvent {
522 t.Fatalf("event %+v", ev)
523 }
524 case <-time.After(5 * time.Second):
525 t.Fatal("no CloseEvent")
526 }
527 close(w.readerStop)
528 _ = d.Close()
529 <-w.readerDone
530 })
531 }
532}
533
534// surfaceRequestCounts reads raw client frames until the sentinel (wl_surface.damage on
535// surface) and returns "object/opcode" counts for the surface.
536func surfaceRequestCounts(t *testing.T, conn net.Conn, surface uint32) map[uint16]int {
537 t.Helper()
538 counts := map[uint16]int{}
539 _ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))
540 hdr := make([]byte, 8)
541 for {
542 if _, err := io.ReadFull(conn, hdr); err != nil {
543 t.Fatal(err)
544 }
545 obj := binary.NativeEndian.Uint32(hdr)
546 word := binary.NativeEndian.Uint32(hdr[4:])
547 size, op := int(word>>16), uint16(word)
548 if size < 8 {
549 t.Fatalf("bad frame size %d", size)
550 }
551 if _, err := io.CopyN(io.Discard, conn, int64(size-8)); err != nil {
552 t.Fatal(err)
553 }
554 if obj != surface {
555 continue
556 }
557 if op == 2 { // wl_surface.damage sentinel
558 return counts
559 }
560 counts[op]++
561 }
562}
563
564// StageInputRects never commits; SetInputRects commits only a real change on a
565// configured window; an unchanged request sends nothing. Counted on the wire.
566func TestInputRectRequestsOnWire(t *testing.T) {
567 const opSetInputRegion, opCommit = 5, 6
568 srv, accept := newWireServer(t)
569 type dialed struct {
570 d *wlturbo.Display
571 err error
572 }
573 ch := make(chan dialed, 1)
574 go func() {
575 d, err := wlturbo.Connect(srv.path)
576 ch <- dialed{d, err}
577 }()
578 conn := accept()
579 r := <-ch
580 if r.err != nil {
581 t.Fatal(r.err)
582 }
583 d := r.d
584 defer d.Close()
585 comp := core.NewCompositor(d.Context())
586 if err := d.Registry().Bind(1, core.CompositorInterface, 6, comp); err != nil {
587 t.Fatal(err)
588 }
589 surface, err := comp.CreateSurface()
590 if err != nil {
591 t.Fatal(err)
592 }
593 w := &Window{Display: d, Compositor: comp, Surface: surface}
594 step := func(name string, run func() error, wantSet, wantCommit int) {
595 t.Helper()
596 if err := run(); err != nil {
597 t.Fatal(name, err)
598 }
599 if err := surface.Damage(0, 0, 1, 1); err != nil {
600 t.Fatal(err)
601 }
602 got := surfaceRequestCounts(t, conn, surface.ID())
603 if got[opSetInputRegion] != wantSet || got[opCommit] != wantCommit {
604 t.Fatalf("%s: set_input_region=%d commit=%d, want %d/%d", name, got[opSetInputRegion], got[opCommit], wantSet, wantCommit)
605 }
606 }
607 rects := []Rect{{0, 0, 10, 10}, {20, 0, 5, 5}}
608 step("stage", func() error { return w.StageInputRects(rects) }, 1, 0)
609 step("stage identical", func() error { return w.StageInputRects(append([]Rect(nil), rects...)) }, 0, 0)
610 step("set identical while configured", func() error { w.Configured = true; return w.SetInputRects(rects) }, 0, 0)
611 step("set changed", func() error { return w.SetInputRects([]Rect{{1, 1, 2, 2}}) }, 1, 1)
612 step("set passthrough", func() error { return w.SetInputRects([]Rect{}) }, 1, 1)
613 step("set passthrough again", func() error { return w.SetInputRects([]Rect{}) }, 0, 0)
614 step("set default", func() error { return w.SetInputRects(nil) }, 1, 1)
615 step("set default again", func() error { return w.SetInputRects(nil) }, 0, 0)
616 w.Configured = false
617 step("unconfigured change stages only", func() error { return w.SetInputRects(rects) }, 1, 0)
618 before := len(w.inputRects)
619 step("invalid stays unsent", func() error {
620 if err := w.StageInputRects([]Rect{{0, 0, 0, 1}}); err == nil {
621 t.Fatal("invalid rect accepted")
622 }
623 return nil
624 }, 0, 0)
625 if len(w.inputRects) != before {
626 t.Fatal("invalid request changed cached state")
627 }
628 if n := testing.AllocsPerRun(100, func() { _ = w.StageInputRects(rects) }); n != 0 {
629 t.Fatalf("unchanged stage allocates %v", n)
630 }
631}