test(cef): assert the GPU recovery pulse sequence and fail fast on GTK waits
Record every render-host call so a regression in the pulse sequence fails the tests, drop a test that did not exercise the lost-wakeup race it was named after, and bound the GTK-wait helper so a verify that never queues a reload fails instead of hanging.
- commit
- 61a84c31014292c5da80960278610f28487f2252
- author
- brice · · 2026-10-15 07:52 UTC
- parent
- e2a53aa
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +41 -25
M internal/infrastructure/cef/gpu_recovery_test.go +41 -25
| @@ -12,15 +12,32 @@ import ( | |||
| 12 | 12 | "github.com/bnema/dumber/internal/application/port" | |
| 13 | 13 | ) | |
| 14 | 14 | ||
| 15 | + | // gpuRecoveryHost records render-host calls. pulses counts Invalidate calls | |
| 16 | + | // (two per pulse); ops records the full call sequence. | |
| 15 | 17 | type gpuRecoveryHost struct { | |
| 16 | 18 | purecef.BrowserHost | |
| 17 | 19 | pulses int | |
| 20 | + | ops []string | |
| 18 | 21 | } | |
| 19 | 22 | ||
| 20 | - | func (h *gpuRecoveryHost) Invalidate(purecef.PaintElementType) { h.pulses++ } | |
| 21 | - | func (h *gpuRecoveryHost) WasHidden(int32) {} | |
| 22 | - | func (h *gpuRecoveryHost) NotifyScreenInfoChanged() {} | |
| 23 | - | func (h *gpuRecoveryHost) WasResized() {} | |
| 23 | + | func (h *gpuRecoveryHost) Invalidate(purecef.PaintElementType) { | |
| 24 | + | h.pulses++ | |
| 25 | + | h.ops = append(h.ops, "Invalidate") | |
| 26 | + | } | |
| 27 | + | ||
| 28 | + | func (h *gpuRecoveryHost) WasHidden(hidden int32) { | |
| 29 | + | if hidden != 0 { | |
| 30 | + | h.ops = append(h.ops, "WasHidden(1)") | |
| 31 | + | return | |
| 32 | + | } | |
| 33 | + | h.ops = append(h.ops, "WasHidden(0)") | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | func (h *gpuRecoveryHost) NotifyScreenInfoChanged() { | |
| 37 | + | h.ops = append(h.ops, "NotifyScreenInfoChanged") | |
| 38 | + | } | |
| 39 | + | ||
| 40 | + | func (h *gpuRecoveryHost) WasResized() { h.ops = append(h.ops, "WasResized") } | |
| 24 | 41 | ||
| 25 | 42 | type gpuRecoveryBrowser struct { | |
| 26 | 43 | purecef.Browser | |
| @@ -194,7 +211,10 @@ func TestGPURecovery_ViewThatPaintsIsNotReloaded(t *testing.T) { | |||
| 194 | 211 | h.setPaints(wv, 10) | |
| 195 | 212 | ||
| 196 | 213 | e.runGPURecoveryPass() | |
| 197 | - | require.Equal(t, 2, host.pulses) | |
| 214 | + | require.Equal(t, []string{ | |
| 215 | + | "Invalidate", "WasHidden(1)", "WasHidden(0)", | |
| 216 | + | "NotifyScreenInfoChanged", "WasResized", "Invalidate", | |
| 217 | + | }, host.ops, "pulse must reset visibility and screen info, then repaint") | |
| 198 | 218 | require.Len(t, h.pending(), 1) | |
| 199 | 219 | require.Equal(t, gpuRecoveryVerifyDelay, h.pending()[0].delay) | |
| 200 | 220 | ||
| @@ -365,23 +385,6 @@ func TestGPURecovery_LostWakeup_ShowThenDeferDoesNotLeavePending(t *testing.T) { | |||
| 365 | 385 | require.Empty(t, h.pending()) | |
| 366 | 386 | } | |
| 367 | 387 | ||
| 368 | - | func TestGPURecovery_VerifyHiddenThenShownBetweenCheckAndStore(t *testing.T) { | |
| 369 | - | h := newGPURecoveryHarness(t) | |
| 370 | - | wv, host, browser := newGPURecoveryView(1) | |
| 371 | - | e := newGPURecoveryEngine(wv) | |
| 372 | - | e.runGPURecoveryPass() // visible pulse, verify scheduled | |
| 373 | - | ||
| 374 | - | // The view is hidden and then shown again before verify runs. The show | |
| 375 | - | // happens while nothing is pending, so it must not strand anything and the | |
| 376 | - | // verify must see a visible, still-frozen view and reload it. | |
| 377 | - | wv.applyEffectiveVisibility(host, false) | |
| 378 | - | wv.applyEffectiveVisibility(host, true) | |
| 379 | - | h.flush() | |
| 380 | - | ||
| 381 | - | require.False(t, pendingOf(wv)) | |
| 382 | - | require.Equal(t, 1, browser.reloads) | |
| 383 | - | } | |
| 384 | - | ||
| 385 | 388 | func TestGPURecovery_ConcurrentShowAndDeferNeverStrandsPending(t *testing.T) { | |
| 386 | 389 | newGPURecoveryHarness(t) | |
| 387 | 390 | wv, host, _ := newGPURecoveryView(1) | |
| @@ -613,9 +616,22 @@ func startVerifyBlockedOnGTK(t *testing.T, h *gpuRecoveryHarness, q *queuedGTK) | |||
| 613 | 616 | pending[0].fn() | |
| 614 | 617 | }() | |
| 615 | 618 | // Wait (without sleeping) until the reload operation is queued on GTK. | |
| 616 | - | op := <-q.ops | |
| 617 | - | q.ops <- op | |
| 618 | - | return func() { <-done } | |
| 619 | + | // Fail fast if verify returns without queuing one, instead of hanging. | |
| 620 | + | select { | |
| 621 | + | case op := <-q.ops: | |
| 622 | + | q.ops <- op | |
| 623 | + | case <-done: | |
| 624 | + | t.Fatal("verify finished without queuing a GTK reload operation") | |
| 625 | + | case <-time.After(5 * time.Second): | |
| 626 | + | t.Fatal("timed out waiting for the GTK reload operation") | |
| 627 | + | } | |
| 628 | + | return func() { | |
| 629 | + | select { | |
| 630 | + | case <-done: | |
| 631 | + | case <-time.After(5 * time.Second): | |
| 632 | + | t.Fatal("timed out waiting for verify to finish") | |
| 633 | + | } | |
| 634 | + | } | |
| 619 | 635 | } | |
| 620 | 636 | ||
| 621 | 637 | func TestGPURecovery_ReloadSkippedWhenSupersededWhileWaitingOnGTK(t *testing.T) { | |
| @@ -12,15 +12,32 @@ import ( | |||||
| 12 | "github.com/bnema/dumber/internal/application/port" | 12 | "github.com/bnema/dumber/internal/application/port" | ||
| 13 | ) | 13 | ) | ||
| 14 | 14 | ||||
| 15 | + | // gpuRecoveryHost records render-host calls. pulses counts Invalidate calls | |||
| 16 | + | // (two per pulse); ops records the full call sequence. | |||
| 15 | type gpuRecoveryHost struct { | 17 | type gpuRecoveryHost struct { | ||
| 16 | purecef.BrowserHost | 18 | purecef.BrowserHost | ||
| 17 | pulses int | 19 | pulses int | ||
| 20 | + | ops []string | |||
| 18 | } | 21 | } | ||
| 19 | 22 | ||||
| 20 | - | func (h *gpuRecoveryHost) Invalidate(purecef.PaintElementType) { h.pulses++ } | 23 | + | func (h *gpuRecoveryHost) Invalidate(purecef.PaintElementType) { |
| 21 | - | func (h *gpuRecoveryHost) WasHidden(int32) {} | 24 | + | h.pulses++ |
| 22 | - | func (h *gpuRecoveryHost) NotifyScreenInfoChanged() {} | 25 | + | h.ops = append(h.ops, "Invalidate") |
| 23 | - | func (h *gpuRecoveryHost) WasResized() {} | 26 | + | } |
| 27 | + | ||||
| 28 | + | func (h *gpuRecoveryHost) WasHidden(hidden int32) { | |||
| 29 | + | if hidden != 0 { | |||
| 30 | + | h.ops = append(h.ops, "WasHidden(1)") | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | h.ops = append(h.ops, "WasHidden(0)") | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | func (h *gpuRecoveryHost) NotifyScreenInfoChanged() { | |||
| 37 | + | h.ops = append(h.ops, "NotifyScreenInfoChanged") | |||
| 38 | + | } | |||
| 39 | + | ||||
| 40 | + | func (h *gpuRecoveryHost) WasResized() { h.ops = append(h.ops, "WasResized") } | |||
| 24 | 41 | ||||
| 25 | type gpuRecoveryBrowser struct { | 42 | type gpuRecoveryBrowser struct { | ||
| 26 | purecef.Browser | 43 | purecef.Browser | ||
| @@ -194,7 +211,10 @@ func TestGPURecovery_ViewThatPaintsIsNotReloaded(t *testing.T) { | |||||
| 194 | h.setPaints(wv, 10) | 211 | h.setPaints(wv, 10) | ||
| 195 | 212 | ||||
| 196 | e.runGPURecoveryPass() | 213 | e.runGPURecoveryPass() | ||
| 197 | - | require.Equal(t, 2, host.pulses) | 214 | + | require.Equal(t, []string{ |
| 215 | + | "Invalidate", "WasHidden(1)", "WasHidden(0)", | |||
| 216 | + | "NotifyScreenInfoChanged", "WasResized", "Invalidate", | |||
| 217 | + | }, host.ops, "pulse must reset visibility and screen info, then repaint") | |||
| 198 | require.Len(t, h.pending(), 1) | 218 | require.Len(t, h.pending(), 1) | ||
| 199 | require.Equal(t, gpuRecoveryVerifyDelay, h.pending()[0].delay) | 219 | require.Equal(t, gpuRecoveryVerifyDelay, h.pending()[0].delay) | ||
| 200 | 220 | ||||
| @@ -365,23 +385,6 @@ func TestGPURecovery_LostWakeup_ShowThenDeferDoesNotLeavePending(t *testing.T) { | |||||
| 365 | require.Empty(t, h.pending()) | 385 | require.Empty(t, h.pending()) | ||
| 366 | } | 386 | } | ||
| 367 | 387 | ||||
| 368 | - | func TestGPURecovery_VerifyHiddenThenShownBetweenCheckAndStore(t *testing.T) { | |||
| 369 | - | h := newGPURecoveryHarness(t) | |||
| 370 | - | wv, host, browser := newGPURecoveryView(1) | |||
| 371 | - | e := newGPURecoveryEngine(wv) | |||
| 372 | - | e.runGPURecoveryPass() // visible pulse, verify scheduled | |||
| 373 | - | ||||
| 374 | - | // The view is hidden and then shown again before verify runs. The show | |||
| 375 | - | // happens while nothing is pending, so it must not strand anything and the | |||
| 376 | - | // verify must see a visible, still-frozen view and reload it. | |||
| 377 | - | wv.applyEffectiveVisibility(host, false) | |||
| 378 | - | wv.applyEffectiveVisibility(host, true) | |||
| 379 | - | h.flush() | |||
| 380 | - | ||||
| 381 | - | require.False(t, pendingOf(wv)) | |||
| 382 | - | require.Equal(t, 1, browser.reloads) | |||
| 383 | - | } | |||
| 384 | - | ||||
| 385 | func TestGPURecovery_ConcurrentShowAndDeferNeverStrandsPending(t *testing.T) { | 388 | func TestGPURecovery_ConcurrentShowAndDeferNeverStrandsPending(t *testing.T) { | ||
| 386 | newGPURecoveryHarness(t) | 389 | newGPURecoveryHarness(t) | ||
| 387 | wv, host, _ := newGPURecoveryView(1) | 390 | wv, host, _ := newGPURecoveryView(1) | ||
| @@ -613,9 +616,22 @@ func startVerifyBlockedOnGTK(t *testing.T, h *gpuRecoveryHarness, q *queuedGTK) | |||||
| 613 | pending[0].fn() | 616 | pending[0].fn() | ||
| 614 | }() | 617 | }() | ||
| 615 | // Wait (without sleeping) until the reload operation is queued on GTK. | 618 | // Wait (without sleeping) until the reload operation is queued on GTK. | ||
| 616 | - | op := <-q.ops | 619 | + | // Fail fast if verify returns without queuing one, instead of hanging. |
| 617 | - | q.ops <- op | 620 | + | select { |
| 618 | - | return func() { <-done } | 621 | + | case op := <-q.ops: |
| 622 | + | q.ops <- op | |||
| 623 | + | case <-done: | |||
| 624 | + | t.Fatal("verify finished without queuing a GTK reload operation") | |||
| 625 | + | case <-time.After(5 * time.Second): | |||
| 626 | + | t.Fatal("timed out waiting for the GTK reload operation") | |||
| 627 | + | } | |||
| 628 | + | return func() { | |||
| 629 | + | select { | |||
| 630 | + | case <-done: | |||
| 631 | + | case <-time.After(5 * time.Second): | |||
| 632 | + | t.Fatal("timed out waiting for verify to finish") | |||
| 633 | + | } | |||
| 634 | + | } | |||
| 619 | } | 635 | } | ||
| 620 | 636 | ||||
| 621 | func TestGPURecovery_ReloadSkippedWhenSupersededWhileWaitingOnGTK(t *testing.T) { | 637 | func TestGPURecovery_ReloadSkippedWhenSupersededWhileWaitingOnGTK(t *testing.T) { | ||