test: cover stale release notification, old-generation pending waits and sticky failure
Use cmp.Compare, check Signal errors, assert the unchanged Render path and poll Pending in the allocation guard instead of sleeping blindly.
- commit
- d1933fcb2deafdb3773cf327138ca5d1229539e5
- author
- brice · · 2026-10-01 17:40 UTC
- parent
- 84a151f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +97 -29
M internal/presentation/session/session_test.go +30 -0
| @@ -3,8 +3,10 @@ | |||
| 3 | 3 | package session | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "errors" | |
| 6 | 7 | "testing" | |
| 7 | 8 | ||
| 9 | + | "github.com/bnema/nefergui/internal/presentation/buffers" | |
| 8 | 10 | "github.com/bnema/nefergui/internal/presentation/vkdevice" | |
| 9 | 11 | ) | |
| 10 | 12 | ||
| @@ -24,3 +26,31 @@ func TestInstallReleaseWaitDefersUnreadySubmission(t *testing.T) { | |||
| 24 | 26 | t.Fatalf("failed replacement: installed=%v err=%v slot=%+v", installed, err, slot) | |
| 25 | 27 | } | |
| 26 | 28 | } | |
| 29 | + | ||
| 30 | + | // A resize while a release wait is outstanding leaves old-generation slots to | |
| 31 | + | // retire: finishPending must not touch them, even if their frame is not ready. | |
| 32 | + | func TestTargetFinishPendingSkipsOldGenerations(t *testing.T) { | |
| 33 | + | pool, _ := buffers.New(1) | |
| 34 | + | old := pool.Buffers[0] | |
| 35 | + | if err := pool.Resize(1); err != nil { | |
| 36 | + | t.Fatal(err) | |
| 37 | + | } | |
| 38 | + | // A nil Frame would panic if the old slot were examined. | |
| 39 | + | tg := &Target{Pool: pool, Slots: map[uint64]*Slot{old.ID: {Buffer: old, PendingWait: &vkdevice.BinaryFence{}}}} | |
| 40 | + | pending, err := tg.finishPending() | |
| 41 | + | if err != nil || pending { | |
| 42 | + | t.Fatalf("pending=%v err=%v", pending, err) | |
| 43 | + | } | |
| 44 | + | if old.State != buffers.Available || tg.Slots[old.ID].PendingWait == nil { | |
| 45 | + | t.Fatal("old-generation slot was modified") | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | ||
| 49 | + | func TestTargetBrokenIsSticky(t *testing.T) { | |
| 50 | + | tg := &Target{wantW: 1, wantH: 1} | |
| 51 | + | boom := errors.New("boom") | |
| 52 | + | first := tg.fail(boom) | |
| 53 | + | if _, err := tg.Draw(nil, 1, &Output{}); !errors.Is(err, boom) || err != first { | |
| 54 | + | t.Fatalf("Draw after failure: %v", err) | |
| 55 | + | } | |
| 56 | + | } | |
| @@ -3,8 +3,10 @@ | |||||
| 3 | package session | 3 | package session | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "errors" | |||
| 6 | "testing" | 7 | "testing" | ||
| 7 | 8 | ||||
| 9 | + | "github.com/bnema/nefergui/internal/presentation/buffers" | |||
| 8 | "github.com/bnema/nefergui/internal/presentation/vkdevice" | 10 | "github.com/bnema/nefergui/internal/presentation/vkdevice" | ||
| 9 | ) | 11 | ) | ||
| 10 | 12 | ||||
| @@ -24,3 +26,31 @@ func TestInstallReleaseWaitDefersUnreadySubmission(t *testing.T) { | |||||
| 24 | t.Fatalf("failed replacement: installed=%v err=%v slot=%+v", installed, err, slot) | 26 | t.Fatalf("failed replacement: installed=%v err=%v slot=%+v", installed, err, slot) | ||
| 25 | } | 27 | } | ||
| 26 | } | 28 | } | ||
| 29 | + | ||||
| 30 | + | // A resize while a release wait is outstanding leaves old-generation slots to | |||
| 31 | + | // retire: finishPending must not touch them, even if their frame is not ready. | |||
| 32 | + | func TestTargetFinishPendingSkipsOldGenerations(t *testing.T) { | |||
| 33 | + | pool, _ := buffers.New(1) | |||
| 34 | + | old := pool.Buffers[0] | |||
| 35 | + | if err := pool.Resize(1); err != nil { | |||
| 36 | + | t.Fatal(err) | |||
| 37 | + | } | |||
| 38 | + | // A nil Frame would panic if the old slot were examined. | |||
| 39 | + | tg := &Target{Pool: pool, Slots: map[uint64]*Slot{old.ID: {Buffer: old, PendingWait: &vkdevice.BinaryFence{}}}} | |||
| 40 | + | pending, err := tg.finishPending() | |||
| 41 | + | if err != nil || pending { | |||
| 42 | + | t.Fatalf("pending=%v err=%v", pending, err) | |||
| 43 | + | } | |||
| 44 | + | if old.State != buffers.Available || tg.Slots[old.ID].PendingWait == nil { | |||
| 45 | + | t.Fatal("old-generation slot was modified") | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | ||||
| 49 | + | func TestTargetBrokenIsSticky(t *testing.T) { | |||
| 50 | + | tg := &Target{wantW: 1, wantH: 1} | |||
| 51 | + | boom := errors.New("boom") | |||
| 52 | + | first := tg.fail(boom) | |||
| 53 | + | if _, err := tg.Draw(nil, 1, &Output{}); !errors.Is(err, boom) || err != first { | |||
| 54 | + | t.Fatalf("Draw after failure: %v", err) | |||
| 55 | + | } | |||
| 56 | + | } | |||
M internal/ui/renderer_gpu_test.go +52 -16
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | package ui | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | + | "cmp" | |
| 6 | 7 | "os" | |
| 7 | 8 | "slices" | |
| 8 | 9 | "testing" | |
| @@ -39,7 +40,7 @@ func gpuRenderer(t testing.TB) (*Renderer, *session.Target) { | |||
| 39 | 40 | for m := range mods { | |
| 40 | 41 | formats = append(formats, Format{FourCC: vkdevice.XRGB8888, Modifier: m}) | |
| 41 | 42 | } | |
| 42 | - | slices.SortFunc(formats, func(a, b Format) int { return int(a.Modifier>>1) - int(b.Modifier>>1) }) | |
| 43 | + | slices.SortFunc(formats, func(a, b Format) int { return cmp.Compare(a.Modifier, b.Modifier) }) | |
| 43 | 44 | vf := make([]vkdevice.Format, len(formats)) | |
| 44 | 45 | for i, f := range formats { | |
| 45 | 46 | vf[i] = vkdevice.Format{FourCC: f.FourCC, Modifier: f.Modifier} | |
| @@ -205,24 +206,19 @@ func TestAllocRendererSteadyFrame(t *testing.T) { | |||
| 205 | 206 | step := func() { | |
| 206 | 207 | n++ | |
| 207 | 208 | r.Invalidate() | |
| 208 | - | // A false result can only mean the GPU still works on the previous | |
| 209 | - | // frame of every free buffer; the application retries shortly. | |
| 210 | - | ok := false | |
| 211 | - | for tries := 0; !ok && tries < 1000; tries++ { | |
| 212 | - | var err error | |
| 213 | - | if ok, err = r.Render(&out, &n, view); err != nil { | |
| 214 | - | t.Fatal(err) | |
| 215 | - | } | |
| 216 | - | if !ok { | |
| 217 | - | time.Sleep(time.Millisecond) | |
| 218 | - | } | |
| 209 | + | ok, err := r.Render(&out, &n, view) | |
| 210 | + | for tries := 0; err == nil && !ok && r.Pending() && tries < 5000; tries++ { | |
| 211 | + | time.Sleep(200 * time.Microsecond) | |
| 212 | + | ok, err = r.Render(&out, &n, view) | |
| 219 | 213 | } | |
| 220 | - | if !ok { | |
| 221 | - | t.Fatal("render never found a ready buffer") | |
| 214 | + | if err != nil || !ok { | |
| 215 | + | t.Fatalf("render: ok=%v err=%v pending=%v", ok, err, r.Pending()) | |
| 222 | 216 | } | |
| 223 | 217 | slot := tg.Slots[out.Buffer] | |
| 224 | - | _ = tg.Node.Signal(slot.ReleaseHandle, out.ReleasePoint) | |
| 225 | - | if err := r.Released(out.Buffer); err != nil { | |
| 218 | + | if err = tg.Node.Signal(slot.ReleaseHandle, out.ReleasePoint); err != nil { | |
| 219 | + | t.Fatal(err) | |
| 220 | + | } | |
| 221 | + | if err = r.Released(out.Buffer); err != nil { | |
| 226 | 222 | t.Fatal(err) | |
| 227 | 223 | } | |
| 228 | 224 | } | |
| @@ -235,3 +231,43 @@ func TestAllocRendererSteadyFrame(t *testing.T) { | |||
| 235 | 231 | t.Fatalf("allocs per steady frame = %v, baseline %v", got, allocBaselineRenderer) | |
| 236 | 232 | } | |
| 237 | 233 | } | |
| 234 | + | ||
| 235 | + | // A readable eventfd whose point is not signaled (a stale notification) must be | |
| 236 | + | // drained without spinning, and the live registration must still work. | |
| 237 | + | func TestRendererGPUStaleReleaseNotification(t *testing.T) { | |
| 238 | + | r, tg := gpuRenderer(t) | |
| 239 | + | var out Output | |
| 240 | + | var m struct{} | |
| 241 | + | view := func(f *Frame, _ *struct{}) { f.Root().Box(Inline("width:10px;height:10px;background:#fff")) } | |
| 242 | + | r.Resize(64, 64, 1) | |
| 243 | + | if ok, err := r.Render(&out, &m, view); !ok || err != nil { | |
| 244 | + | t.Fatalf("ok=%v err=%v", ok, err) | |
| 245 | + | } | |
| 246 | + | one := [8]byte{1} | |
| 247 | + | if _, err := unix.Write(out.ReleaseFD, one[:]); err != nil { | |
| 248 | + | t.Fatal(err) | |
| 249 | + | } | |
| 250 | + | if !readable(t, out.ReleaseFD, time.Second) { | |
| 251 | + | t.Fatal("test setup: eventfd not readable") | |
| 252 | + | } | |
| 253 | + | if err := r.Released(out.Buffer); err != nil { | |
| 254 | + | t.Fatal(err) | |
| 255 | + | } | |
| 256 | + | if readable(t, out.ReleaseFD, 0) { | |
| 257 | + | t.Fatal("stale notification left the eventfd readable: the caller would spin") | |
| 258 | + | } | |
| 259 | + | if tg.Slots[out.Buffer].Buffer.State == "available" { | |
| 260 | + | t.Fatal("buffer reused before the compositor released it") | |
| 261 | + | } | |
| 262 | + | signalRelease(t, tg, &out) | |
| 263 | + | if !readable(t, out.ReleaseFD, time.Second) { | |
| 264 | + | t.Fatal("live registration lost after a stale notification") | |
| 265 | + | } | |
| 266 | + | if err := r.Released(out.Buffer); err != nil { | |
| 267 | + | t.Fatal(err) | |
| 268 | + | } | |
| 269 | + | // Released for an unknown or idle buffer is a no-op that still drains. | |
| 270 | + | if err := r.Released(999); err != nil { | |
| 271 | + | t.Fatal(err) | |
| 272 | + | } | |
| 273 | + | } | |
| @@ -3,6 +3,7 @@ | |||||
| 3 | package ui | 3 | package ui | ||
| 4 | 4 | ||||
| 5 | import ( | 5 | import ( | ||
| 6 | + | "cmp" | |||
| 6 | "os" | 7 | "os" | ||
| 7 | "slices" | 8 | "slices" | ||
| 8 | "testing" | 9 | "testing" | ||
| @@ -39,7 +40,7 @@ func gpuRenderer(t testing.TB) (*Renderer, *session.Target) { | |||||
| 39 | for m := range mods { | 40 | for m := range mods { | ||
| 40 | formats = append(formats, Format{FourCC: vkdevice.XRGB8888, Modifier: m}) | 41 | formats = append(formats, Format{FourCC: vkdevice.XRGB8888, Modifier: m}) | ||
| 41 | } | 42 | } | ||
| 42 | - | slices.SortFunc(formats, func(a, b Format) int { return int(a.Modifier>>1) - int(b.Modifier>>1) }) | 43 | + | slices.SortFunc(formats, func(a, b Format) int { return cmp.Compare(a.Modifier, b.Modifier) }) |
| 43 | vf := make([]vkdevice.Format, len(formats)) | 44 | vf := make([]vkdevice.Format, len(formats)) | ||
| 44 | for i, f := range formats { | 45 | for i, f := range formats { | ||
| 45 | vf[i] = vkdevice.Format{FourCC: f.FourCC, Modifier: f.Modifier} | 46 | vf[i] = vkdevice.Format{FourCC: f.FourCC, Modifier: f.Modifier} | ||
| @@ -205,24 +206,19 @@ func TestAllocRendererSteadyFrame(t *testing.T) { | |||||
| 205 | step := func() { | 206 | step := func() { | ||
| 206 | n++ | 207 | n++ | ||
| 207 | r.Invalidate() | 208 | r.Invalidate() | ||
| 208 | - | // A false result can only mean the GPU still works on the previous | 209 | + | ok, err := r.Render(&out, &n, view) |
| 209 | - | // frame of every free buffer; the application retries shortly. | 210 | + | for tries := 0; err == nil && !ok && r.Pending() && tries < 5000; tries++ { |
| 210 | - | ok := false | 211 | + | time.Sleep(200 * time.Microsecond) |
| 211 | - | for tries := 0; !ok && tries < 1000; tries++ { | 212 | + | ok, err = r.Render(&out, &n, view) |
| 212 | - | var err error | |||
| 213 | - | if ok, err = r.Render(&out, &n, view); err != nil { | |||
| 214 | - | t.Fatal(err) | |||
| 215 | - | } | |||
| 216 | - | if !ok { | |||
| 217 | - | time.Sleep(time.Millisecond) | |||
| 218 | - | } | |||
| 219 | } | 213 | } | ||
| 220 | - | if !ok { | 214 | + | if err != nil || !ok { |
| 221 | - | t.Fatal("render never found a ready buffer") | 215 | + | t.Fatalf("render: ok=%v err=%v pending=%v", ok, err, r.Pending()) |
| 222 | } | 216 | } | ||
| 223 | slot := tg.Slots[out.Buffer] | 217 | slot := tg.Slots[out.Buffer] | ||
| 224 | - | _ = tg.Node.Signal(slot.ReleaseHandle, out.ReleasePoint) | 218 | + | if err = tg.Node.Signal(slot.ReleaseHandle, out.ReleasePoint); err != nil { |
| 225 | - | if err := r.Released(out.Buffer); err != nil { | 219 | + | t.Fatal(err) |
| 220 | + | } | |||
| 221 | + | if err = r.Released(out.Buffer); err != nil { | |||
| 226 | t.Fatal(err) | 222 | t.Fatal(err) | ||
| 227 | } | 223 | } | ||
| 228 | } | 224 | } | ||
| @@ -235,3 +231,43 @@ func TestAllocRendererSteadyFrame(t *testing.T) { | |||||
| 235 | t.Fatalf("allocs per steady frame = %v, baseline %v", got, allocBaselineRenderer) | 231 | t.Fatalf("allocs per steady frame = %v, baseline %v", got, allocBaselineRenderer) | ||
| 236 | } | 232 | } | ||
| 237 | } | 233 | } | ||
| 234 | + | ||||
| 235 | + | // A readable eventfd whose point is not signaled (a stale notification) must be | |||
| 236 | + | // drained without spinning, and the live registration must still work. | |||
| 237 | + | func TestRendererGPUStaleReleaseNotification(t *testing.T) { | |||
| 238 | + | r, tg := gpuRenderer(t) | |||
| 239 | + | var out Output | |||
| 240 | + | var m struct{} | |||
| 241 | + | view := func(f *Frame, _ *struct{}) { f.Root().Box(Inline("width:10px;height:10px;background:#fff")) } | |||
| 242 | + | r.Resize(64, 64, 1) | |||
| 243 | + | if ok, err := r.Render(&out, &m, view); !ok || err != nil { | |||
| 244 | + | t.Fatalf("ok=%v err=%v", ok, err) | |||
| 245 | + | } | |||
| 246 | + | one := [8]byte{1} | |||
| 247 | + | if _, err := unix.Write(out.ReleaseFD, one[:]); err != nil { | |||
| 248 | + | t.Fatal(err) | |||
| 249 | + | } | |||
| 250 | + | if !readable(t, out.ReleaseFD, time.Second) { | |||
| 251 | + | t.Fatal("test setup: eventfd not readable") | |||
| 252 | + | } | |||
| 253 | + | if err := r.Released(out.Buffer); err != nil { | |||
| 254 | + | t.Fatal(err) | |||
| 255 | + | } | |||
| 256 | + | if readable(t, out.ReleaseFD, 0) { | |||
| 257 | + | t.Fatal("stale notification left the eventfd readable: the caller would spin") | |||
| 258 | + | } | |||
| 259 | + | if tg.Slots[out.Buffer].Buffer.State == "available" { | |||
| 260 | + | t.Fatal("buffer reused before the compositor released it") | |||
| 261 | + | } | |||
| 262 | + | signalRelease(t, tg, &out) | |||
| 263 | + | if !readable(t, out.ReleaseFD, time.Second) { | |||
| 264 | + | t.Fatal("live registration lost after a stale notification") | |||
| 265 | + | } | |||
| 266 | + | if err := r.Released(out.Buffer); err != nil { | |||
| 267 | + | t.Fatal(err) | |||
| 268 | + | } | |||
| 269 | + | // Released for an unknown or idle buffer is a no-op that still drains. | |||
| 270 | + | if err := r.Released(999); err != nil { | |||
| 271 | + | t.Fatal(err) | |||
| 272 | + | } | |||
| 273 | + | } | |||
M internal/ui/renderer_test.go +15 -13
| @@ -78,19 +78,9 @@ func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) { | |||
| 78 | 78 | if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) { | |
| 79 | 79 | t.Fatalf("output: %+v", out) | |
| 80 | 80 | } | |
| 81 | - | // The follow-up frame a Build requests after settling still draws once; after that nothing changes. | |
| 82 | - | for i := 0; i < 3; i++ { | |
| 83 | - | ok, err := r.Render(&out, &m, testView) | |
| 84 | - | if err != nil { | |
| 85 | - | t.Fatal(err) | |
| 86 | - | } | |
| 87 | - | if ok { | |
| 88 | - | drawOK(tg, nil).Maybe() | |
| 89 | - | } | |
| 90 | - | } | |
| 91 | - | // tg asserts no Draw beyond the allowed ones when unchanged. | |
| 92 | - | if ok, _ := r.Render(&out, &m, testView); ok { | |
| 93 | - | t.Fatal("unchanged view drew") | |
| 81 | + | // Nothing changed: no rebuild result, so no Draw (the mock has no further expectation). | |
| 82 | + | if ok, err := r.Render(&out, &m, testView); ok || err != nil { | |
| 83 | + | t.Fatalf("unchanged view: ok=%v err=%v", ok, err) | |
| 94 | 84 | } | |
| 95 | 85 | } | |
| 96 | 86 | ||
| @@ -101,9 +91,13 @@ func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) { | |||
| 101 | 91 | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | |
| 102 | 92 | r.Resize(100, 50, 1) | |
| 103 | 93 | tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once() | |
| 94 | + | tg.EXPECT().Waiting().Return(true).Once() | |
| 104 | 95 | if ok, err := r.Render(&out, &m, testView); ok || err != nil { | |
| 105 | 96 | t.Fatalf("no buffer: ok=%v err=%v", ok, err) | |
| 106 | 97 | } | |
| 98 | + | if !r.Pending() { | |
| 99 | + | t.Fatal("a built frame blocked on the GPU must report Pending") | |
| 100 | + | } | |
| 107 | 101 | drawOK(tg, nil).Once() | |
| 108 | 102 | if ok, err := r.Render(&out, &m, testView); !ok || err != nil { | |
| 109 | 103 | t.Fatalf("retry: ok=%v err=%v", ok, err) | |
| @@ -249,6 +243,14 @@ func TestRendererForwardsReleasedAndClose(t *testing.T) { | |||
| 249 | 243 | if err := r.Close(); err != nil { | |
| 250 | 244 | t.Fatal(err) | |
| 251 | 245 | } | |
| 246 | + | // After Close nothing reaches the target (the mock has no expectation). | |
| 247 | + | r.Resize(10, 10, 1) | |
| 248 | + | if r.Input(&Input{Kind: InputPointerMotion}) { | |
| 249 | + | t.Fatal("Input after Close requested a redraw") | |
| 250 | + | } | |
| 251 | + | if err := r.Released(3); err == nil { | |
| 252 | + | t.Fatal("Released after Close succeeded") | |
| 253 | + | } | |
| 252 | 254 | if err := r.Close(); err != nil { // second Close is a no-op | |
| 253 | 255 | t.Fatal(err) | |
| 254 | 256 | } | |
| @@ -78,19 +78,9 @@ func TestRendererRenderBeforeResizeAndWhenUnchanged(t *testing.T) { | |||||
| 78 | if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) { | 78 | if out.Buffer != 1 || out.Width != 100 || len(out.Damage) != 1 || out.Damage[0] != (Rect{Width: 100, Height: 50}) { | ||
| 79 | t.Fatalf("output: %+v", out) | 79 | t.Fatalf("output: %+v", out) | ||
| 80 | } | 80 | } | ||
| 81 | - | // The follow-up frame a Build requests after settling still draws once; after that nothing changes. | 81 | + | // Nothing changed: no rebuild result, so no Draw (the mock has no further expectation). |
| 82 | - | for i := 0; i < 3; i++ { | 82 | + | if ok, err := r.Render(&out, &m, testView); ok || err != nil { |
| 83 | - | ok, err := r.Render(&out, &m, testView) | 83 | + | t.Fatalf("unchanged view: ok=%v err=%v", ok, err) |
| 84 | - | if err != nil { | |||
| 85 | - | t.Fatal(err) | |||
| 86 | - | } | |||
| 87 | - | if ok { | |||
| 88 | - | drawOK(tg, nil).Maybe() | |||
| 89 | - | } | |||
| 90 | - | } | |||
| 91 | - | // tg asserts no Draw beyond the allowed ones when unchanged. | |||
| 92 | - | if ok, _ := r.Render(&out, &m, testView); ok { | |||
| 93 | - | t.Fatal("unchanged view drew") | |||
| 94 | } | 84 | } | ||
| 95 | } | 85 | } | ||
| 96 | 86 | ||||
| @@ -101,9 +91,13 @@ func TestRendererKeepsFrameWhenNoBufferFree(t *testing.T) { | |||||
| 101 | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | 91 | tg.EXPECT().Resize(mock.Anything, mock.Anything).Return().Once() | ||
| 102 | r.Resize(100, 50, 1) | 92 | r.Resize(100, 50, 1) | ||
| 103 | tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once() | 93 | tg.EXPECT().Draw(mock.Anything, mock.Anything, mock.Anything).Return(false, nil).Once() | ||
| 94 | + | tg.EXPECT().Waiting().Return(true).Once() | |||
| 104 | if ok, err := r.Render(&out, &m, testView); ok || err != nil { | 95 | if ok, err := r.Render(&out, &m, testView); ok || err != nil { | ||
| 105 | t.Fatalf("no buffer: ok=%v err=%v", ok, err) | 96 | t.Fatalf("no buffer: ok=%v err=%v", ok, err) | ||
| 106 | } | 97 | } | ||
| 98 | + | if !r.Pending() { | |||
| 99 | + | t.Fatal("a built frame blocked on the GPU must report Pending") | |||
| 100 | + | } | |||
| 107 | drawOK(tg, nil).Once() | 101 | drawOK(tg, nil).Once() | ||
| 108 | if ok, err := r.Render(&out, &m, testView); !ok || err != nil { | 102 | if ok, err := r.Render(&out, &m, testView); !ok || err != nil { | ||
| 109 | t.Fatalf("retry: ok=%v err=%v", ok, err) | 103 | t.Fatalf("retry: ok=%v err=%v", ok, err) | ||
| @@ -249,6 +243,14 @@ func TestRendererForwardsReleasedAndClose(t *testing.T) { | |||||
| 249 | if err := r.Close(); err != nil { | 243 | if err := r.Close(); err != nil { | ||
| 250 | t.Fatal(err) | 244 | t.Fatal(err) | ||
| 251 | } | 245 | } | ||
| 246 | + | // After Close nothing reaches the target (the mock has no expectation). | |||
| 247 | + | r.Resize(10, 10, 1) | |||
| 248 | + | if r.Input(&Input{Kind: InputPointerMotion}) { | |||
| 249 | + | t.Fatal("Input after Close requested a redraw") | |||
| 250 | + | } | |||
| 251 | + | if err := r.Released(3); err == nil { | |||
| 252 | + | t.Fatal("Released after Close succeeded") | |||
| 253 | + | } | |||
| 252 | if err := r.Close(); err != nil { // second Close is a no-op | 254 | if err := r.Close(); err != nil { // second Close is a no-op | ||
| 253 | t.Fatal(err) | 255 | t.Fatal(err) | ||
| 254 | } | 256 | } | ||