detached 61a84c3

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 VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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 (
1212 "github.com/bnema/dumber/internal/application/port"
1313 )
1414
15+// gpuRecoveryHost records render-host calls. pulses counts Invalidate calls
16+// (two per pulse); ops records the full call sequence.
1517 type gpuRecoveryHost struct {
1618 purecef.BrowserHost
1719 pulses int
20+ ops []string
1821 }
1922
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") }
2441
2542 type gpuRecoveryBrowser struct {
2643 purecef.Browser
@@ -194,7 +211,10 @@ func TestGPURecovery_ViewThatPaintsIsNotReloaded(t *testing.T) {
194211 h.setPaints(wv, 10)
195212
196213 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")
198218 require.Len(t, h.pending(), 1)
199219 require.Equal(t, gpuRecoveryVerifyDelay, h.pending()[0].delay)
200220
@@ -365,23 +385,6 @@ func TestGPURecovery_LostWakeup_ShowThenDeferDoesNotLeavePending(t *testing.T) {
365385 require.Empty(t, h.pending())
366386 }
367387
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-
385388 func TestGPURecovery_ConcurrentShowAndDeferNeverStrandsPending(t *testing.T) {
386389 newGPURecoveryHarness(t)
387390 wv, host, _ := newGPURecoveryView(1)
@@ -613,9 +616,22 @@ func startVerifyBlockedOnGTK(t *testing.T, h *gpuRecoveryHarness, q *queuedGTK)
613616 pending[0].fn()
614617 }()
615618 // 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+ }
619635 }
620636
621637 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.BrowserHost18 purecef.BrowserHost
17 pulses int19 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.Browser43 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.ops619+ // Fail fast if verify returns without queuing one, instead of hanging.
617- q.ops <- op620+ 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) {

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 8

Tags 42

↑↓ moveenter openesc close