detached 706981b

test(synctest): run timing-sensitive tests in synctest bubbles (#147)

Wrap core, drm, app and libinput tests that relied on real sleeps or
short time.After windows in testing/synctest. Negative checks now use
synctest.Wait() instead of sleeping and hoping, and timer-driven tests
(stuck frames, user activity throttle) run on the fake clock.
commit
706981be278334389641dfc5eaf5228de2215ec8
author
bnema · · 2026-10-07 21:21 UTC
committer
GitHub · · 2026-10-07 21:21 UTC
parent
360492d
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
11 files · +105 -44
M internal/adapters/drm/capture_test.go +7 -1
@@ -8,6 +8,7 @@ import (
88 "sync"
99 "sync/atomic"
1010 "testing"
11+ "testing/synctest"
1112 "time"
1213
1314 "github.com/bnema/neferwl/internal/adapters/capture"
@@ -428,6 +429,10 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) {
428429 // that shows the border draws the frame without it, copies that, then draws
429430 // and commits the frame with the border.
430431 func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {
432+ synctest.Test(t, runCaptureWaitsForItsIndicatorScene)
433+}
434+
435+func runCaptureWaitsForItsIndicatorScene(t *testing.T) {
431436 o, k, commits, commitMu := testOutputMu(t)
432437 o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{}
433438 flips := make(chan flipEvent, 4)
@@ -503,10 +508,11 @@ func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {
503508 req := sessionRequest(t, 1, false)
504509 req.Indicate = true
505510 captures <- req
511+ synctest.Wait()
506512 select {
507513 case d := <-captured:
508514 t.Fatalf("served from a scene without the indicator: %+v", d)
509- case <-time.After(60 * time.Millisecond):
515+ default:
510516 }
511517 require.Equal(t, 3, count(), "no frame is drawn for a held request")
512518
@@ -8,6 +8,7 @@ import (
8 "sync"8 "sync"
9 "sync/atomic"9 "sync/atomic"
10 "testing"10 "testing"
11+ "testing/synctest"
11 "time"12 "time"
12 13
13 "github.com/bnema/neferwl/internal/adapters/capture"14 "github.com/bnema/neferwl/internal/adapters/capture"
@@ -428,6 +429,10 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) {
428 // that shows the border draws the frame without it, copies that, then draws429 // that shows the border draws the frame without it, copies that, then draws
429 // and commits the frame with the border.430 // and commits the frame with the border.
430 func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {431 func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {
432+ synctest.Test(t, runCaptureWaitsForItsIndicatorScene)
433+}
434+
435+func runCaptureWaitsForItsIndicatorScene(t *testing.T) {
431 o, k, commits, commitMu := testOutputMu(t)436 o, k, commits, commitMu := testOutputMu(t)
432 o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{}437 o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{}
433 flips := make(chan flipEvent, 4)438 flips := make(chan flipEvent, 4)
@@ -503,10 +508,11 @@ func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) {
503 req := sessionRequest(t, 1, false)508 req := sessionRequest(t, 1, false)
504 req.Indicate = true509 req.Indicate = true
505 captures <- req510 captures <- req
511+ synctest.Wait()
506 select {512 select {
507 case d := <-captured:513 case d := <-captured:
508 t.Fatalf("served from a scene without the indicator: %+v", d)514 t.Fatalf("served from a scene without the indicator: %+v", d)
509- case <-time.After(60 * time.Millisecond):515+ default:
510 }516 }
511 require.Equal(t, 3, count(), "no frame is drawn for a held request")517 require.Equal(t, 3, count(), "no frame is drawn for a held request")
512 518
M internal/adapters/drm/commit_test.go +11 -2
@@ -5,6 +5,7 @@ import (
55 "errors"
66 "os"
77 "testing"
8+ "testing/synctest"
89 "time"
910
1011 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -373,6 +374,10 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
373374 // Enabling an output whose images were not made yet (started switched
374375 // away) tests them and modesets once.
375376 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
377+ synctest.Test(t, enableAfterStartAwayModesetsOnce)
378+}
379+
380+func enableAfterStartAwayModesetsOnce(t *testing.T) {
376381 o, k, commits, commitMu := testOutputMu(t)
377382 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
378383 r := portsmocks.NewMockRenderer(t)
@@ -408,7 +413,7 @@ func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
408413 }
409414 return n
410415 }
411- time.Sleep(20 * time.Millisecond)
416+ synctest.Wait()
412417 if n := count(atomicAllowModes); n != 0 {
413418 t.Fatalf("%d modesets while away", n)
414419 }
@@ -470,6 +475,10 @@ func TestCursorOffSkipsLoads(t *testing.T) {
470475 // Content read while only a cursor commit is in flight is reported when
471476 // that commit's event arrives, not held until a later frame.
472477 func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
478+ synctest.Test(t, runReportsSeenAfterCursorCommit)
479+}
480+
481+func runReportsSeenAfterCursorCommit(t *testing.T) {
473482 o, k, commits, commitMu := testOutputMu(t)
474483 o.tearing, o.cursor.fbs = false, [2]uint32{}
475484 flips := make(chan flipEvent, 1)
@@ -524,7 +533,7 @@ func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
524533 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
525534 // A window this output does not show commits while the cursor flies.
526535 contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
527- time.Sleep(20 * time.Millisecond)
536+ synctest.Wait()
528537 flips <- eventOf(cur)
529538 select {
530539 case p := <-presented:
@@ -5,6 +5,7 @@ import (
5 "errors"5 "errors"
6 "os"6 "os"
7 "testing"7 "testing"
8+ "testing/synctest"
8 "time"9 "time"
9 10
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -373,6 +374,10 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) {
373 // Enabling an output whose images were not made yet (started switched374 // Enabling an output whose images were not made yet (started switched
374 // away) tests them and modesets once.375 // away) tests them and modesets once.
375 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {376 func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
377+ synctest.Test(t, enableAfterStartAwayModesetsOnce)
378+}
379+
380+func enableAfterStartAwayModesetsOnce(t *testing.T) {
376 o, k, commits, commitMu := testOutputMu(t)381 o, k, commits, commitMu := testOutputMu(t)
377 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}382 o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{}
378 r := portsmocks.NewMockRenderer(t)383 r := portsmocks.NewMockRenderer(t)
@@ -408,7 +413,7 @@ func TestEnableAfterStartAwayModesetsOnce(t *testing.T) {
408 }413 }
409 return n414 return n
410 }415 }
411- time.Sleep(20 * time.Millisecond)416+ synctest.Wait()
412 if n := count(atomicAllowModes); n != 0 {417 if n := count(atomicAllowModes); n != 0 {
413 t.Fatalf("%d modesets while away", n)418 t.Fatalf("%d modesets while away", n)
414 }419 }
@@ -470,6 +475,10 @@ func TestCursorOffSkipsLoads(t *testing.T) {
470 // Content read while only a cursor commit is in flight is reported when475 // Content read while only a cursor commit is in flight is reported when
471 // that commit's event arrives, not held until a later frame.476 // that commit's event arrives, not held until a later frame.
472 func TestRunReportsSeenAfterCursorCommit(t *testing.T) {477 func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
478+ synctest.Test(t, runReportsSeenAfterCursorCommit)
479+}
480+
481+func runReportsSeenAfterCursorCommit(t *testing.T) {
473 o, k, commits, commitMu := testOutputMu(t)482 o, k, commits, commitMu := testOutputMu(t)
474 o.tearing, o.cursor.fbs = false, [2]uint32{}483 o.tearing, o.cursor.fbs = false, [2]uint32{}
475 flips := make(chan flipEvent, 1)484 flips := make(chan flipEvent, 1)
@@ -524,7 +533,7 @@ func TestRunReportsSeenAfterCursorCommit(t *testing.T) {
524 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })533 waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok })
525 // A window this output does not show commits while the cursor flies.534 // A window this output does not show commits while the cursor flies.
526 contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}535 contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}}
527- time.Sleep(20 * time.Millisecond)536+ synctest.Wait()
528 flips <- eventOf(cur)537 flips <- eventOf(cur)
529 select {538 select {
530 case p := <-presented:539 case p := <-presented:
M internal/adapters/drm/output_test.go +6 -2
@@ -11,6 +11,7 @@ import (
1111 "sync"
1212 "sync/atomic"
1313 "testing"
14+ "testing/synctest"
1415 "time"
1516
1617 "github.com/bnema/neferwl/internal/adapters/presented"
@@ -552,7 +553,9 @@ func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
552553 // Run: while a frame is in flight its buffers may still be read by the
553554 // GPU, so a newer content is reported only after the flip event; every
554555 // frame fence is closed.
555-func TestRunReportsSeenAfterFlip(t *testing.T) {
556+func TestRunReportsSeenAfterFlip(t *testing.T) { synctest.Test(t, runReportsSeenAfterFlip) }
557+
558+func runReportsSeenAfterFlip(t *testing.T) {
556559 o, k, commits, commitMu := testOutputMu(t)
557560 o.cursor, o.tearing = nil, false
558561 flips := make(chan flipEvent, 1)
@@ -605,10 +608,11 @@ func TestRunReportsSeenAfterFlip(t *testing.T) {
605608 waitFor(t, func() bool { return frameCommits() == 1 })
606609 // Frame 1 is in flight: content 3 replaces what it may be reading.
607610 contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
611+ synctest.Wait()
608612 select {
609613 case p := <-presented:
610614 t.Fatalf("report while a frame is in flight: %+v", p)
611- case <-time.After(30 * time.Millisecond):
615+ default:
612616 }
613617 commitMu.Lock()
614618 last := (*commits)[len(*commits)-1]
@@ -11,6 +11,7 @@ import (
11 "sync"11 "sync"
12 "sync/atomic"12 "sync/atomic"
13 "testing"13 "testing"
14+ "testing/synctest"
14 "time"15 "time"
15 16
16 "github.com/bnema/neferwl/internal/adapters/presented"17 "github.com/bnema/neferwl/internal/adapters/presented"
@@ -552,7 +553,9 @@ func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) {
552 // Run: while a frame is in flight its buffers may still be read by the553 // Run: while a frame is in flight its buffers may still be read by the
553 // GPU, so a newer content is reported only after the flip event; every554 // GPU, so a newer content is reported only after the flip event; every
554 // frame fence is closed.555 // frame fence is closed.
555-func TestRunReportsSeenAfterFlip(t *testing.T) {556+func TestRunReportsSeenAfterFlip(t *testing.T) { synctest.Test(t, runReportsSeenAfterFlip) }
557+
558+func runReportsSeenAfterFlip(t *testing.T) {
556 o, k, commits, commitMu := testOutputMu(t)559 o, k, commits, commitMu := testOutputMu(t)
557 o.cursor, o.tearing = nil, false560 o.cursor, o.tearing = nil, false
558 flips := make(chan flipEvent, 1)561 flips := make(chan flipEvent, 1)
@@ -605,10 +608,11 @@ func TestRunReportsSeenAfterFlip(t *testing.T) {
605 waitFor(t, func() bool { return frameCommits() == 1 })608 waitFor(t, func() bool { return frameCommits() == 1 })
606 // Frame 1 is in flight: content 3 replaces what it may be reading.609 // Frame 1 is in flight: content 3 replaces what it may be reading.
607 contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}610 contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}}
611+ synctest.Wait()
608 select {612 select {
609 case p := <-presented:613 case p := <-presented:
610 t.Fatalf("report while a frame is in flight: %+v", p)614 t.Fatalf("report while a frame is in flight: %+v", p)
611- case <-time.After(30 * time.Millisecond):615+ default:
612 }616 }
613 commitMu.Lock()617 commitMu.Lock()
614 last := (*commits)[len(*commits)-1]618 last := (*commits)[len(*commits)-1]
M internal/adapters/drm/power_test.go +10 -6
@@ -4,7 +4,7 @@ import (
44 "context"
55 "os"
66 "testing"
7- "time"
7+ "testing/synctest"
88
99 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
1010 "github.com/bnema/neferwl/internal/ports"
@@ -13,7 +13,9 @@ import (
1313
1414 // A scene with Off turns the CRTC inactive and draws nothing; the next
1515 // scene without Off modesets (ACTIVE=1) and draws a frame.
16-func TestRunOutputPower(t *testing.T) {
16+func TestRunOutputPower(t *testing.T) { synctest.Test(t, runOutputPower) }
17+
18+func runOutputPower(t *testing.T) {
1719 o, k, commits, commitMu := testOutputMu(t)
1820 o.tearing, o.cursor = false, nil
1921 flips := make(chan flipEvent, 1)
@@ -79,7 +81,7 @@ func TestRunOutputPower(t *testing.T) {
7981 <-renders
8082 }
8183 scenes <- off
82- time.Sleep(20 * time.Millisecond)
84+ synctest.Wait()
8385 select {
8486 case <-renders:
8587 t.Fatal("rendered while off")
@@ -93,7 +95,7 @@ func TestRunOutputPower(t *testing.T) {
9395 vt <- false
9496 vt <- true
9597 waitFor(t, func() bool { return len(snapshot()) > n })
96- time.Sleep(20 * time.Millisecond)
98+ synctest.Wait()
9799 for _, c := range snapshot()[n:] {
98100 if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 {
99101 t.Fatal("VT resume turned the display on")
@@ -120,7 +122,9 @@ func TestRunOutputPower(t *testing.T) {
120122 // An output started off (it reconnected while turned off) never activates
121123 // its CRTC before a scene turns it on; its tearing probe, refused by an
122124 // inactive CRTC, waits until then.
123-func TestRunOutputStartOff(t *testing.T) {
125+func TestRunOutputStartOff(t *testing.T) { synctest.Test(t, runOutputStartOff) }
126+
127+func runOutputStartOff(t *testing.T) {
124128 o, k, commits, commitMu := testOutputMu(t)
125129 o.tearing, o.cursor = true, nil
126130 o.StartOff = true
@@ -180,7 +184,7 @@ func TestRunOutputStartOff(t *testing.T) {
180184 }
181185 off := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1, Off: true}
182186 scenes <- off
183- time.Sleep(20 * time.Millisecond)
187+ synctest.Wait()
184188 if lit(snapshot()) {
185189 t.Fatal("a display started off lit up")
186190 }
@@ -4,7 +4,7 @@ import (
4 "context"4 "context"
5 "os"5 "os"
6 "testing"6 "testing"
7- "time"7+ "testing/synctest"
8 8
9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"9 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
10 "github.com/bnema/neferwl/internal/ports"10 "github.com/bnema/neferwl/internal/ports"
@@ -13,7 +13,9 @@ import (
13 13
14 // A scene with Off turns the CRTC inactive and draws nothing; the next14 // A scene with Off turns the CRTC inactive and draws nothing; the next
15 // scene without Off modesets (ACTIVE=1) and draws a frame.15 // scene without Off modesets (ACTIVE=1) and draws a frame.
16-func TestRunOutputPower(t *testing.T) {16+func TestRunOutputPower(t *testing.T) { synctest.Test(t, runOutputPower) }
17+
18+func runOutputPower(t *testing.T) {
17 o, k, commits, commitMu := testOutputMu(t)19 o, k, commits, commitMu := testOutputMu(t)
18 o.tearing, o.cursor = false, nil20 o.tearing, o.cursor = false, nil
19 flips := make(chan flipEvent, 1)21 flips := make(chan flipEvent, 1)
@@ -79,7 +81,7 @@ func TestRunOutputPower(t *testing.T) {
79 <-renders81 <-renders
80 }82 }
81 scenes <- off83 scenes <- off
82- time.Sleep(20 * time.Millisecond)84+ synctest.Wait()
83 select {85 select {
84 case <-renders:86 case <-renders:
85 t.Fatal("rendered while off")87 t.Fatal("rendered while off")
@@ -93,7 +95,7 @@ func TestRunOutputPower(t *testing.T) {
93 vt <- false95 vt <- false
94 vt <- true96 vt <- true
95 waitFor(t, func() bool { return len(snapshot()) > n })97 waitFor(t, func() bool { return len(snapshot()) > n })
96- time.Sleep(20 * time.Millisecond)98+ synctest.Wait()
97 for _, c := range snapshot()[n:] {99 for _, c := range snapshot()[n:] {
98 if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 {100 if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 {
99 t.Fatal("VT resume turned the display on")101 t.Fatal("VT resume turned the display on")
@@ -120,7 +122,9 @@ func TestRunOutputPower(t *testing.T) {
120 // An output started off (it reconnected while turned off) never activates122 // An output started off (it reconnected while turned off) never activates
121 // its CRTC before a scene turns it on; its tearing probe, refused by an123 // its CRTC before a scene turns it on; its tearing probe, refused by an
122 // inactive CRTC, waits until then.124 // inactive CRTC, waits until then.
123-func TestRunOutputStartOff(t *testing.T) {125+func TestRunOutputStartOff(t *testing.T) { synctest.Test(t, runOutputStartOff) }
126+
127+func runOutputStartOff(t *testing.T) {
124 o, k, commits, commitMu := testOutputMu(t)128 o, k, commits, commitMu := testOutputMu(t)
125 o.tearing, o.cursor = true, nil129 o.tearing, o.cursor = true, nil
126 o.StartOff = true130 o.StartOff = true
@@ -180,7 +184,7 @@ func TestRunOutputStartOff(t *testing.T) {
180 }184 }
181 off := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1, Off: true}185 off := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1, Off: true}
182 scenes <- off186 scenes <- off
183- time.Sleep(20 * time.Millisecond)187+ synctest.Wait()
184 if lit(snapshot()) {188 if lit(snapshot()) {
185 t.Fatal("a display started off lit up")189 t.Fatal("a display started off lit up")
186 }190 }
M internal/adapters/drm/security_run_test.go +9 -2
@@ -5,6 +5,7 @@ import (
55 "os"
66 "sync/atomic"
77 "testing"
8+ "testing/synctest"
89 "time"
910
1011 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -13,6 +14,10 @@ import (
1314 )
1415
1516 func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
17+ synctest.Test(t, securityRunWakePendingResumeAndOff)
18+}
19+
20+func securityRunWakePendingResumeAndOff(t *testing.T) {
1621 o, k, commits, mu := testOutputMu(t)
1722 o.cursor, o.tearing = nil, false
1823 var state atomic.Uint64
@@ -74,10 +79,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
7479 })
7580 state.Store(3)
7681 wake <- ports.SecurityState{} // stale payload: read gate, not payload
82+ synctest.Wait()
7783 select {
7884 case ev := <-events:
7985 t.Fatalf("proof while old frame pending: %+v", ev)
80- case <-time.After(20 * time.Millisecond):
86+ default:
8187 }
8288 flips <- eventOf(frame)
8389 proof := func(kind ports.ProtectionKind) {
@@ -98,10 +104,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
98104 }
99105 proof(ports.ProtectionProtectedFrame)
100106 scenes <- ports.Scene{Seq: 99} // held desktop scene cannot be replayed
107+ synctest.Wait()
101108 select {
102109 case <-desktop:
103110 t.Fatal("stale desktop rendered under protection")
104- case <-time.After(20 * time.Millisecond):
111+ default:
105112 }
106113 active <- false
107114 select {
@@ -5,6 +5,7 @@ import (
5 "os"5 "os"
6 "sync/atomic"6 "sync/atomic"
7 "testing"7 "testing"
8+ "testing/synctest"
8 "time"9 "time"
9 10
10 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -13,6 +14,10 @@ import (
13 )14 )
14 15
15 func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {16 func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
17+ synctest.Test(t, securityRunWakePendingResumeAndOff)
18+}
19+
20+func securityRunWakePendingResumeAndOff(t *testing.T) {
16 o, k, commits, mu := testOutputMu(t)21 o, k, commits, mu := testOutputMu(t)
17 o.cursor, o.tearing = nil, false22 o.cursor, o.tearing = nil, false
18 var state atomic.Uint6423 var state atomic.Uint64
@@ -74,10 +79,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
74 })79 })
75 state.Store(3)80 state.Store(3)
76 wake <- ports.SecurityState{} // stale payload: read gate, not payload81 wake <- ports.SecurityState{} // stale payload: read gate, not payload
82+ synctest.Wait()
77 select {83 select {
78 case ev := <-events:84 case ev := <-events:
79 t.Fatalf("proof while old frame pending: %+v", ev)85 t.Fatalf("proof while old frame pending: %+v", ev)
80- case <-time.After(20 * time.Millisecond):86+ default:
81 }87 }
82 flips <- eventOf(frame)88 flips <- eventOf(frame)
83 proof := func(kind ports.ProtectionKind) {89 proof := func(kind ports.ProtectionKind) {
@@ -98,10 +104,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) {
98 }104 }
99 proof(ports.ProtectionProtectedFrame)105 proof(ports.ProtectionProtectedFrame)
100 scenes <- ports.Scene{Seq: 99} // held desktop scene cannot be replayed106 scenes <- ports.Scene{Seq: 99} // held desktop scene cannot be replayed
107+ synctest.Wait()
101 select {108 select {
102 case <-desktop:109 case <-desktop:
103 t.Fatal("stale desktop rendered under protection")110 t.Fatal("stale desktop rendered under protection")
104- case <-time.After(20 * time.Millisecond):111+ default:
105 }112 }
106 active <- false113 active <- false
107 select {114 select {
M internal/adapters/drm/stuck_test.go +9 -1
@@ -6,6 +6,7 @@ import (
66 "sync"
77 "sync/atomic"
88 "testing"
9+ "testing/synctest"
910 "time"
1011
1112 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -149,6 +150,10 @@ func fencePipe(t *testing.T) (fence, signal *os.File) {
149150 // waiting past the limit; once the fence signalled and still no event
150151 // came, the output modesets.
151152 func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
153+ synctest.Test(t, unsignalledFenceDelaysRecovery)
154+}
155+
156+func unsignalledFenceDelaysRecovery(t *testing.T) {
152157 var mu sync.Mutex
153158 var signals []*os.File
154159 h := startStuck(t, func() *os.File {
@@ -161,6 +166,7 @@ func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
161166 h.scenes <- ports.Scene{Seq: 1}
162167 waitFor(t, func() bool { return h.count() == 3 })
163168 time.Sleep(3 * 200 * time.Millisecond)
169+ synctest.Wait()
164170 if n := h.count(); n != 3 {
165171 t.Fatalf("%d commits while the frame's fence is unsignalled", n)
166172 }
@@ -212,7 +218,9 @@ func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
212218
213219 // An EBUSY run ends when commits stop being refused: a later EBUSY is
214220 // retried, not taken as the same run lasting past the limit.
215-func TestLaterBusyStartsNewRun(t *testing.T) {
221+func TestLaterBusyStartsNewRun(t *testing.T) { synctest.Test(t, laterBusyStartsNewRun) }
222+
223+func laterBusyStartsNewRun(t *testing.T) {
216224 o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)
217225 o.frame.stuckAfter = 100 * time.Millisecond
218226 enabled := true
@@ -6,6 +6,7 @@ import (
6 "sync"6 "sync"
7 "sync/atomic"7 "sync/atomic"
8 "testing"8 "testing"
9+ "testing/synctest"
9 "time"10 "time"
10 11
11 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"12 portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
@@ -149,6 +150,10 @@ func fencePipe(t *testing.T) (fence, signal *os.File) {
149 // waiting past the limit; once the fence signalled and still no event150 // waiting past the limit; once the fence signalled and still no event
150 // came, the output modesets.151 // came, the output modesets.
151 func TestUnsignalledFenceDelaysRecovery(t *testing.T) {152 func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
153+ synctest.Test(t, unsignalledFenceDelaysRecovery)
154+}
155+
156+func unsignalledFenceDelaysRecovery(t *testing.T) {
152 var mu sync.Mutex157 var mu sync.Mutex
153 var signals []*os.File158 var signals []*os.File
154 h := startStuck(t, func() *os.File {159 h := startStuck(t, func() *os.File {
@@ -161,6 +166,7 @@ func TestUnsignalledFenceDelaysRecovery(t *testing.T) {
161 h.scenes <- ports.Scene{Seq: 1}166 h.scenes <- ports.Scene{Seq: 1}
162 waitFor(t, func() bool { return h.count() == 3 })167 waitFor(t, func() bool { return h.count() == 3 })
163 time.Sleep(3 * 200 * time.Millisecond)168 time.Sleep(3 * 200 * time.Millisecond)
169+ synctest.Wait()
164 if n := h.count(); n != 3 {170 if n := h.count(); n != 3 {
165 t.Fatalf("%d commits while the frame's fence is unsignalled", n)171 t.Fatalf("%d commits while the frame's fence is unsignalled", n)
166 }172 }
@@ -212,7 +218,9 @@ func TestSeenWaitsForRefusedFrameFence(t *testing.T) {
212 218
213 // An EBUSY run ends when commits stop being refused: a later EBUSY is219 // An EBUSY run ends when commits stop being refused: a later EBUSY is
214 // retried, not taken as the same run lasting past the limit.220 // retried, not taken as the same run lasting past the limit.
215-func TestLaterBusyStartsNewRun(t *testing.T) {221+func TestLaterBusyStartsNewRun(t *testing.T) { synctest.Test(t, laterBusyStartsNewRun) }
222+
223+func laterBusyStartsNewRun(t *testing.T) {
216 o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)224 o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY)
217 o.frame.stuckAfter = 100 * time.Millisecond225 o.frame.stuckAfter = 100 * time.Millisecond
218 enabled := true226 enabled := true
M internal/adapters/libinput/forward_test.go +8 -5
@@ -6,6 +6,7 @@ import (
66 "strings"
77 "sync"
88 "testing"
9+ "testing/synctest"
910 "time"
1011
1112 "github.com/bnema/neferwl/internal/ports"
@@ -15,19 +16,20 @@ import (
1516
1617 // A slow consumer must not block push, and stale motion must collapse into
1718 // the latest position while keys and buttons keep their order.
19+// In the bubble, a push that blocked would deadlock and fail the test.
1820 func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
21+ synctest.Test(t, forwarderCoalescesMotionWithoutBlocking)
22+}
23+
24+func forwarderCoalescesMotionWithoutBlocking(t *testing.T) {
1925 f := newForwarder(zerowrap.Default())
2026 input := make(chan ports.InputEvent) // unbuffered: core is busy
21- start := time.Now()
2227 for i := range 1000 {
2328 f.push(ports.PointerMotion{X: float64(i)})
2429 }
2530 f.push(ports.PointerButton{Button: 272, Pressed: true})
2631 f.push(ports.PointerMotion{X: 2000})
2732 f.push(ports.PointerMotion{X: 2001})
28- if d := time.Since(start); d > 100*time.Millisecond {
29- t.Fatalf("push blocked for %v", d)
30- }
3133 ctx, cancel := context.WithCancel(context.Background())
3234 defer cancel()
3335 go f.run(ctx, input)
@@ -46,10 +48,11 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
4648 t.Fatalf("event %d not delivered", i)
4749 }
4850 }
51+ synctest.Wait()
4952 select {
5053 case got := <-input:
5154 t.Fatalf("unexpected extra event %#v", got)
52- case <-time.After(20 * time.Millisecond):
55+ default:
5356 }
5457 s := f.take()
5558 if s.Motions != 1002 || s.Coalesced != 1000 || s.Sent != 3 {
@@ -6,6 +6,7 @@ import (
6 "strings"6 "strings"
7 "sync"7 "sync"
8 "testing"8 "testing"
9+ "testing/synctest"
9 "time"10 "time"
10 11
11 "github.com/bnema/neferwl/internal/ports"12 "github.com/bnema/neferwl/internal/ports"
@@ -15,19 +16,20 @@ import (
15 16
16 // A slow consumer must not block push, and stale motion must collapse into17 // A slow consumer must not block push, and stale motion must collapse into
17 // the latest position while keys and buttons keep their order.18 // the latest position while keys and buttons keep their order.
19+// In the bubble, a push that blocked would deadlock and fail the test.
18 func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {20 func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
21+ synctest.Test(t, forwarderCoalescesMotionWithoutBlocking)
22+}
23+
24+func forwarderCoalescesMotionWithoutBlocking(t *testing.T) {
19 f := newForwarder(zerowrap.Default())25 f := newForwarder(zerowrap.Default())
20 input := make(chan ports.InputEvent) // unbuffered: core is busy26 input := make(chan ports.InputEvent) // unbuffered: core is busy
21- start := time.Now()
22 for i := range 1000 {27 for i := range 1000 {
23 f.push(ports.PointerMotion{X: float64(i)})28 f.push(ports.PointerMotion{X: float64(i)})
24 }29 }
25 f.push(ports.PointerButton{Button: 272, Pressed: true})30 f.push(ports.PointerButton{Button: 272, Pressed: true})
26 f.push(ports.PointerMotion{X: 2000})31 f.push(ports.PointerMotion{X: 2000})
27 f.push(ports.PointerMotion{X: 2001})32 f.push(ports.PointerMotion{X: 2001})
28- if d := time.Since(start); d > 100*time.Millisecond {
29- t.Fatalf("push blocked for %v", d)
30- }
31 ctx, cancel := context.WithCancel(context.Background())33 ctx, cancel := context.WithCancel(context.Background())
32 defer cancel()34 defer cancel()
33 go f.run(ctx, input)35 go f.run(ctx, input)
@@ -46,10 +48,11 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) {
46 t.Fatalf("event %d not delivered", i)48 t.Fatalf("event %d not delivered", i)
47 }49 }
48 }50 }
51+ synctest.Wait()
49 select {52 select {
50 case got := <-input:53 case got := <-input:
51 t.Fatalf("unexpected extra event %#v", got)54 t.Fatalf("unexpected extra event %#v", got)
52- case <-time.After(20 * time.Millisecond):55+ default:
53 }56 }
54 s := f.take()57 s := f.take()
55 if s.Motions != 1002 || s.Coalesced != 1000 || s.Sent != 3 {58 if s.Motions != 1002 || s.Coalesced != 1000 || s.Sent != 3 {
M internal/app/security_hotplug_test.go +13 -2
@@ -4,6 +4,7 @@ import (
44 "context"
55 "reflect"
66 "testing"
7+ "testing/synctest"
78 "time"
89
910 "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"
@@ -185,6 +186,10 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) {
185186 }
186187
187188 func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
189+ synctest.Test(t, securityOwnerFIFOSaturationDrainsOnNormalStop)
190+}
191+
192+func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
188193 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
189194 defer cancel()
190195 events := make(chan ports.SecurityBackendEvent) // block display after registration
@@ -235,10 +240,11 @@ func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
235240 t.Fatal("FIFO failed to fill")
236241 }
237242 }
243+ synctest.Wait()
238244 select {
239245 case got := <-accepted:
240246 t.Fatalf("full FIFO did not backpressure owner: %d", got)
241- case <-time.After(20 * time.Millisecond):
247+ default:
242248 }
243249 var ledger sessionlock.Readiness
244250 ledger.Add(registered.Instance)
@@ -330,6 +336,10 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) {
330336 }
331337
332338 func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
339+ synctest.Test(t, securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders)
340+}
341+
342+func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
333343 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
334344 defer cancel()
335345 events := make(chan ports.SecurityBackendEvent)
@@ -364,10 +374,11 @@ func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.
364374 t.Fatal("FIFO never filled")
365375 }
366376 }
377+ synctest.Wait()
367378 select {
368379 case <-accepted:
369380 t.Fatal("producer did not block at finite FIFO")
370- case <-time.After(20 * time.Millisecond):
381+ default:
371382 }
372383 waitDone := make(chan error, 1)
373384 go func() { waitDone <- set.wait() }()
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "reflect"5 "reflect"
6 "testing"6 "testing"
7+ "testing/synctest"
7 "time"8 "time"
8 9
9 "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"10 "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock"
@@ -185,6 +186,10 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) {
185 }186 }
186 187
187 func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {188 func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
189+ synctest.Test(t, securityOwnerFIFOSaturationDrainsOnNormalStop)
190+}
191+
192+func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
188 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)193 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
189 defer cancel()194 defer cancel()
190 events := make(chan ports.SecurityBackendEvent) // block display after registration195 events := make(chan ports.SecurityBackendEvent) // block display after registration
@@ -235,10 +240,11 @@ func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) {
235 t.Fatal("FIFO failed to fill")240 t.Fatal("FIFO failed to fill")
236 }241 }
237 }242 }
243+ synctest.Wait()
238 select {244 select {
239 case got := <-accepted:245 case got := <-accepted:
240 t.Fatalf("full FIFO did not backpressure owner: %d", got)246 t.Fatalf("full FIFO did not backpressure owner: %d", got)
241- case <-time.After(20 * time.Millisecond):247+ default:
242 }248 }
243 var ledger sessionlock.Readiness249 var ledger sessionlock.Readiness
244 ledger.Add(registered.Instance)250 ledger.Add(registered.Instance)
@@ -330,6 +336,10 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) {
330 }336 }
331 337
332 func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {338 func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
339+ synctest.Test(t, securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders)
340+}
341+
342+func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) {
333 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)343 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
334 defer cancel()344 defer cancel()
335 events := make(chan ports.SecurityBackendEvent)345 events := make(chan ports.SecurityBackendEvent)
@@ -364,10 +374,11 @@ func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.
364 t.Fatal("FIFO never filled")374 t.Fatal("FIFO never filled")
365 }375 }
366 }376 }
377+ synctest.Wait()
367 select {378 select {
368 case <-accepted:379 case <-accepted:
369 t.Fatal("producer did not block at finite FIFO")380 t.Fatal("producer did not block at finite FIFO")
370- case <-time.After(20 * time.Millisecond):381+ default:
371 }382 }
372 waitDone := make(chan error, 1)383 waitDone := make(chan error, 1)
373 go func() { waitDone <- set.wait() }()384 go func() { waitDone <- set.wait() }()
M internal/core/core_test.go +10 -4
@@ -6,6 +6,7 @@ import (
66 "reflect"
77 "slices"
88 "testing"
9+ "testing/synctest"
910 "time"
1011
1112 "github.com/bnema/neferwl/internal/adapters/config"
@@ -519,9 +520,13 @@ func workspaceSwitch(t *testing.T, animated bool) {
519520 }
520521 }
521522
522-func TestClickAfterWorkspaceSwitch(t *testing.T) { clickAfterWorkspaceSwitch(t, false) }
523+func TestClickAfterWorkspaceSwitch(t *testing.T) {
524+ synctest.Test(t, func(t *testing.T) { clickAfterWorkspaceSwitch(t, false) })
525+}
523526
524-func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) { clickAfterWorkspaceSwitch(t, true) }
527+func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) {
528+ synctest.Test(t, func(t *testing.T) { clickAfterWorkspaceSwitch(t, true) })
529+}
525530
526531 func clickAfterWorkspaceSwitch(t *testing.T, animated bool) {
527532 cfg := config.Defaults()
@@ -551,8 +556,9 @@ func clickAfterWorkspaceSwitch(t *testing.T, animated bool) {
551556 client <- ports.WindowMapped{ID: 1}
552557 scene(t, scenes)
553558 input <- ports.PointerMotion{X: 50, Y: 40}
554- for len(commands) < 3 {
555- time.Sleep(time.Millisecond)
559+ synctest.Wait()
560+ if len(commands) < 3 {
561+ t.Fatalf("%d commands after pointer motion, want at least 3", len(commands))
556562 }
557563 for len(commands) > 0 {
558564 <-commands
@@ -6,6 +6,7 @@ import (
6 "reflect"6 "reflect"
7 "slices"7 "slices"
8 "testing"8 "testing"
9+ "testing/synctest"
9 "time"10 "time"
10 11
11 "github.com/bnema/neferwl/internal/adapters/config"12 "github.com/bnema/neferwl/internal/adapters/config"
@@ -519,9 +520,13 @@ func workspaceSwitch(t *testing.T, animated bool) {
519 }520 }
520 }521 }
521 522
522-func TestClickAfterWorkspaceSwitch(t *testing.T) { clickAfterWorkspaceSwitch(t, false) }523+func TestClickAfterWorkspaceSwitch(t *testing.T) {
524+ synctest.Test(t, func(t *testing.T) { clickAfterWorkspaceSwitch(t, false) })
525+}
523 526
524-func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) { clickAfterWorkspaceSwitch(t, true) }527+func TestClickAfterWorkspaceSwitchAnimated(t *testing.T) {
528+ synctest.Test(t, func(t *testing.T) { clickAfterWorkspaceSwitch(t, true) })
529+}
525 530
526 func clickAfterWorkspaceSwitch(t *testing.T, animated bool) {531 func clickAfterWorkspaceSwitch(t *testing.T, animated bool) {
527 cfg := config.Defaults()532 cfg := config.Defaults()
@@ -551,8 +556,9 @@ func clickAfterWorkspaceSwitch(t *testing.T, animated bool) {
551 client <- ports.WindowMapped{ID: 1}556 client <- ports.WindowMapped{ID: 1}
552 scene(t, scenes)557 scene(t, scenes)
553 input <- ports.PointerMotion{X: 50, Y: 40}558 input <- ports.PointerMotion{X: 50, Y: 40}
554- for len(commands) < 3 {559+ synctest.Wait()
555- time.Sleep(time.Millisecond)560+ if len(commands) < 3 {
561+ t.Fatalf("%d commands after pointer motion, want at least 3", len(commands))
556 }562 }
557 for len(commands) > 0 {563 for len(commands) > 0 {
558 <-commands564 <-commands
M internal/core/inhibit_test.go +6 -2
@@ -3,6 +3,7 @@ package core_test
33 import (
44 "context"
55 "testing"
6+ "testing/synctest"
67 "time"
78
89 "github.com/bnema/neferwl/internal/adapters/config"
@@ -30,7 +31,9 @@ func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.Client
3031
3132 // A focused window inhibiting shortcuts gets bound keys; it stops when
3233 // it loses focus, and binds run again.
33-func TestShortcutsInhibit(t *testing.T) {
34+func TestShortcutsInhibit(t *testing.T) { synctest.Test(t, shortcutsInhibit) }
35+
36+func shortcutsInhibit(t *testing.T) {
3437 cfg := config.Defaults()
3538 input := make(chan ports.InputEvent, 4)
3639 client := make(chan ports.ClientEvent, 4)
@@ -60,10 +63,11 @@ func TestShortcutsInhibit(t *testing.T) {
6063 if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) {
6164 t.Fatal(v)
6265 }
66+ synctest.Wait()
6367 select {
6468 case r := <-spawn:
6569 t.Fatalf("bind ran: %v", r)
66- case <-time.After(30 * time.Millisecond):
70+ default:
6771 }
6872 // A new window takes focus: the inhibitor goes inactive, binds run.
6973 client <- ports.WindowMapped{ID: 2}
@@ -3,6 +3,7 @@ package core_test
3 import (3 import (
4 "context"4 "context"
5 "testing"5 "testing"
6+ "testing/synctest"
6 "time"7 "time"
7 8
8 "github.com/bnema/neferwl/internal/adapters/config"9 "github.com/bnema/neferwl/internal/adapters/config"
@@ -30,7 +31,9 @@ func commandOf[T ports.ClientCommand](t *testing.T, commands <-chan ports.Client
30 31
31 // A focused window inhibiting shortcuts gets bound keys; it stops when32 // A focused window inhibiting shortcuts gets bound keys; it stops when
32 // it loses focus, and binds run again.33 // it loses focus, and binds run again.
33-func TestShortcutsInhibit(t *testing.T) {34+func TestShortcutsInhibit(t *testing.T) { synctest.Test(t, shortcutsInhibit) }
35+
36+func shortcutsInhibit(t *testing.T) {
34 cfg := config.Defaults()37 cfg := config.Defaults()
35 input := make(chan ports.InputEvent, 4)38 input := make(chan ports.InputEvent, 4)
36 client := make(chan ports.ClientEvent, 4)39 client := make(chan ports.ClientEvent, 4)
@@ -60,10 +63,11 @@ func TestShortcutsInhibit(t *testing.T) {
60 if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) {63 if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) {
61 t.Fatal(v)64 t.Fatal(v)
62 }65 }
66+ synctest.Wait()
63 select {67 select {
64 case r := <-spawn:68 case r := <-spawn:
65 t.Fatalf("bind ran: %v", r)69 t.Fatalf("bind ran: %v", r)
66- case <-time.After(30 * time.Millisecond):70+ default:
67 }71 }
68 // A new window takes focus: the inhibitor goes inactive, binds run.72 // A new window takes focus: the inhibitor goes inactive, binds run.
69 client <- ports.WindowMapped{ID: 2}73 client <- ports.WindowMapped{ID: 2}
M internal/core/power_test.go +16 -17
@@ -4,6 +4,7 @@ import (
44 "context"
55 "slices"
66 "testing"
7+ "testing/synctest"
78 "time"
89
910 "github.com/bnema/neferwl/internal/adapters/config"
@@ -147,27 +148,25 @@ func gone(t *testing.T, scenes <-chan []ports.Scene, name string) {
147148 }
148149
149150 // Input reports user activity to wayland, at most once per interval.
151+// The bubble's fake clock makes the interval exact.
150152 func TestUserActivity(t *testing.T) {
151- input, _, _, commands, _ := powerCore(t)
152- for range 3 {
153- input <- ports.PointerMotion{X: 1, Y: 1}
154- }
155- commandOf[ports.UserActivity](t, commands)
156- deadline := time.After(ports.ActivityInterval / 2)
157- for {
158- select {
159- case c := <-commands:
160- if _, ok := c.(ports.UserActivity); ok {
153+ synctest.Test(t, func(t *testing.T) {
154+ input, _, _, commands, _ := powerCore(t)
155+ for range 3 {
156+ input <- ports.PointerMotion{X: 1, Y: 1}
157+ }
158+ commandOf[ports.UserActivity](t, commands)
159+ time.Sleep(ports.ActivityInterval / 2)
160+ synctest.Wait()
161+ for len(commands) > 0 {
162+ if _, ok := (<-commands).(ports.UserActivity); ok {
161163 t.Fatal("second UserActivity within the interval")
162164 }
163- continue
164- case <-deadline:
165165 }
166- break
167- }
168- time.Sleep(ports.ActivityInterval)
169- input <- ports.PointerMotion{X: 2, Y: 2}
170- commandOf[ports.UserActivity](t, commands)
166+ time.Sleep(ports.ActivityInterval)
167+ input <- ports.PointerMotion{X: 2, Y: 2}
168+ commandOf[ports.UserActivity](t, commands)
169+ })
171170 }
172171
173172 // With an injected clock, the throttle follows it: no second UserActivity
@@ -4,6 +4,7 @@ import (
4 "context"4 "context"
5 "slices"5 "slices"
6 "testing"6 "testing"
7+ "testing/synctest"
7 "time"8 "time"
8 9
9 "github.com/bnema/neferwl/internal/adapters/config"10 "github.com/bnema/neferwl/internal/adapters/config"
@@ -147,27 +148,25 @@ func gone(t *testing.T, scenes <-chan []ports.Scene, name string) {
147 }148 }
148 149
149 // Input reports user activity to wayland, at most once per interval.150 // Input reports user activity to wayland, at most once per interval.
151+// The bubble's fake clock makes the interval exact.
150 func TestUserActivity(t *testing.T) {152 func TestUserActivity(t *testing.T) {
151- input, _, _, commands, _ := powerCore(t)153+ synctest.Test(t, func(t *testing.T) {
152- for range 3 {154+ input, _, _, commands, _ := powerCore(t)
153- input <- ports.PointerMotion{X: 1, Y: 1}155+ for range 3 {
154- }156+ input <- ports.PointerMotion{X: 1, Y: 1}
155- commandOf[ports.UserActivity](t, commands)157+ }
156- deadline := time.After(ports.ActivityInterval / 2)158+ commandOf[ports.UserActivity](t, commands)
157- for {159+ time.Sleep(ports.ActivityInterval / 2)
158- select {160+ synctest.Wait()
159- case c := <-commands:161+ for len(commands) > 0 {
160- if _, ok := c.(ports.UserActivity); ok {162+ if _, ok := (<-commands).(ports.UserActivity); ok {
161 t.Fatal("second UserActivity within the interval")163 t.Fatal("second UserActivity within the interval")
162 }164 }
163- continue
164- case <-deadline:
165 }165 }
166- break166+ time.Sleep(ports.ActivityInterval)
167- }167+ input <- ports.PointerMotion{X: 2, Y: 2}
168- time.Sleep(ports.ActivityInterval)168+ commandOf[ports.UserActivity](t, commands)
169- input <- ports.PointerMotion{X: 2, Y: 2}169+ })
170- commandOf[ports.UserActivity](t, commands)
171 }170 }
172 171
173 // With an injected clock, the throttle follows it: no second UserActivity172 // With an injected clock, the throttle follows it: no second UserActivity

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 23

Tags 10

↑↓ moveenter openesc close