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 Unverified
B5690EEEBB952194The key is unknown
11 files · +105 -44
M internal/adapters/drm/capture_test.go +7 -1
| @@ -8,6 +8,7 @@ import ( | |||
| 8 | 8 | "sync" | |
| 9 | 9 | "sync/atomic" | |
| 10 | 10 | "testing" | |
| 11 | + | "testing/synctest" | |
| 11 | 12 | "time" | |
| 12 | 13 | ||
| 13 | 14 | "github.com/bnema/neferwl/internal/adapters/capture" | |
| @@ -428,6 +429,10 @@ func TestRunChildHoldLiftedWhileDisplayCommitStalls(t *testing.T) { | |||
| 428 | 429 | // that shows the border draws the frame without it, copies that, then draws | |
| 429 | 430 | // and commits the frame with the border. | |
| 430 | 431 | func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) { | |
| 432 | + | synctest.Test(t, runCaptureWaitsForItsIndicatorScene) | |
| 433 | + | } | |
| 434 | + | ||
| 435 | + | func runCaptureWaitsForItsIndicatorScene(t *testing.T) { | |
| 431 | 436 | o, k, commits, commitMu := testOutputMu(t) | |
| 432 | 437 | o.tearing, o.cursor, o.fbs = false, nil, [2]uint32{} | |
| 433 | 438 | flips := make(chan flipEvent, 4) | |
| @@ -503,10 +508,11 @@ func TestRunCaptureWaitsForItsIndicatorScene(t *testing.T) { | |||
| 503 | 508 | req := sessionRequest(t, 1, false) | |
| 504 | 509 | req.Indicate = true | |
| 505 | 510 | captures <- req | |
| 511 | + | synctest.Wait() | |
| 506 | 512 | select { | |
| 507 | 513 | case d := <-captured: | |
| 508 | 514 | t.Fatalf("served from a scene without the indicator: %+v", d) | |
| 509 | - | case <-time.After(60 * time.Millisecond): | |
| 515 | + | default: | |
| 510 | 516 | } | |
| 511 | 517 | require.Equal(t, 3, count(), "no frame is drawn for a held request") | |
| 512 | 518 | ||
| @@ -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 draws | 429 | // 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 = true | 509 | req.Indicate = true | ||
| 505 | captures <- req | 510 | 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 ( | |||
| 5 | 5 | "errors" | |
| 6 | 6 | "os" | |
| 7 | 7 | "testing" | |
| 8 | + | "testing/synctest" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| @@ -373,6 +374,10 @@ func TestShowImagesSkipsSinglePlaneWhenDriverImagesHadOnePlane(t *testing.T) { | |||
| 373 | 374 | // Enabling an output whose images were not made yet (started switched | |
| 374 | 375 | // away) tests them and modesets once. | |
| 375 | 376 | func TestEnableAfterStartAwayModesetsOnce(t *testing.T) { | |
| 377 | + | synctest.Test(t, enableAfterStartAwayModesetsOnce) | |
| 378 | + | } | |
| 379 | + | ||
| 380 | + | func enableAfterStartAwayModesetsOnce(t *testing.T) { | |
| 376 | 381 | o, k, commits, commitMu := testOutputMu(t) | |
| 377 | 382 | o.cursor, o.tearing, o.fbs = nil, false, [2]uint32{} | |
| 378 | 383 | r := portsmocks.NewMockRenderer(t) | |
| @@ -408,7 +413,7 @@ func TestEnableAfterStartAwayModesetsOnce(t *testing.T) { | |||
| 408 | 413 | } | |
| 409 | 414 | return n | |
| 410 | 415 | } | |
| 411 | - | time.Sleep(20 * time.Millisecond) | |
| 416 | + | synctest.Wait() | |
| 412 | 417 | if n := count(atomicAllowModes); n != 0 { | |
| 413 | 418 | t.Fatalf("%d modesets while away", n) | |
| 414 | 419 | } | |
| @@ -470,6 +475,10 @@ func TestCursorOffSkipsLoads(t *testing.T) { | |||
| 470 | 475 | // Content read while only a cursor commit is in flight is reported when | |
| 471 | 476 | // that commit's event arrives, not held until a later frame. | |
| 472 | 477 | func TestRunReportsSeenAfterCursorCommit(t *testing.T) { | |
| 478 | + | synctest.Test(t, runReportsSeenAfterCursorCommit) | |
| 479 | + | } | |
| 480 | + | ||
| 481 | + | func runReportsSeenAfterCursorCommit(t *testing.T) { | |
| 473 | 482 | o, k, commits, commitMu := testOutputMu(t) | |
| 474 | 483 | o.tearing, o.cursor.fbs = false, [2]uint32{} | |
| 475 | 484 | flips := make(chan flipEvent, 1) | |
| @@ -524,7 +533,7 @@ func TestRunReportsSeenAfterCursorCommit(t *testing.T) { | |||
| 524 | 533 | waitFor(t, func() bool { var ok bool; cur, ok = last(userState); return ok }) | |
| 525 | 534 | // A window this output does not show commits while the cursor flies. | |
| 526 | 535 | contents <- ports.SurfaceContent{ID: 7, Seq: 2, SHM: &ports.SHMBuffer{}} | |
| 527 | - | time.Sleep(20 * time.Millisecond) | |
| 536 | + | synctest.Wait() | |
| 528 | 537 | flips <- eventOf(cur) | |
| 529 | 538 | select { | |
| 530 | 539 | 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 switched | 374 | // 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 n | 414 | 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 when | 475 | // 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 ( | |||
| 11 | 11 | "sync" | |
| 12 | 12 | "sync/atomic" | |
| 13 | 13 | "testing" | |
| 14 | + | "testing/synctest" | |
| 14 | 15 | "time" | |
| 15 | 16 | ||
| 16 | 17 | "github.com/bnema/neferwl/internal/adapters/presented" | |
| @@ -552,7 +553,9 @@ func TestBufferRefusedIsNotBlamedOnVRR(t *testing.T) { | |||
| 552 | 553 | // Run: while a frame is in flight its buffers may still be read by the | |
| 553 | 554 | // GPU, so a newer content is reported only after the flip event; every | |
| 554 | 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 | 559 | o, k, commits, commitMu := testOutputMu(t) | |
| 557 | 560 | o.cursor, o.tearing = nil, false | |
| 558 | 561 | flips := make(chan flipEvent, 1) | |
| @@ -605,10 +608,11 @@ func TestRunReportsSeenAfterFlip(t *testing.T) { | |||
| 605 | 608 | waitFor(t, func() bool { return frameCommits() == 1 }) | |
| 606 | 609 | // Frame 1 is in flight: content 3 replaces what it may be reading. | |
| 607 | 610 | contents <- ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{}} | |
| 611 | + | synctest.Wait() | |
| 608 | 612 | select { | |
| 609 | 613 | case p := <-presented: | |
| 610 | 614 | t.Fatalf("report while a frame is in flight: %+v", p) | |
| 611 | - | case <-time.After(30 * time.Millisecond): | |
| 615 | + | default: | |
| 612 | 616 | } | |
| 613 | 617 | commitMu.Lock() | |
| 614 | 618 | 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 the | 553 | // 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; every | 554 | // 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, false | 560 | 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "os" | |
| 6 | 6 | "testing" | |
| 7 | - | "time" | |
| 7 | + | "testing/synctest" | |
| 8 | 8 | ||
| 9 | 9 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| 10 | 10 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -13,7 +13,9 @@ import ( | |||
| 13 | 13 | ||
| 14 | 14 | // A scene with Off turns the CRTC inactive and draws nothing; the next | |
| 15 | 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 | 19 | o, k, commits, commitMu := testOutputMu(t) | |
| 18 | 20 | o.tearing, o.cursor = false, nil | |
| 19 | 21 | flips := make(chan flipEvent, 1) | |
| @@ -79,7 +81,7 @@ func TestRunOutputPower(t *testing.T) { | |||
| 79 | 81 | <-renders | |
| 80 | 82 | } | |
| 81 | 83 | scenes <- off | |
| 82 | - | time.Sleep(20 * time.Millisecond) | |
| 84 | + | synctest.Wait() | |
| 83 | 85 | select { | |
| 84 | 86 | case <-renders: | |
| 85 | 87 | t.Fatal("rendered while off") | |
| @@ -93,7 +95,7 @@ func TestRunOutputPower(t *testing.T) { | |||
| 93 | 95 | vt <- false | |
| 94 | 96 | vt <- true | |
| 95 | 97 | waitFor(t, func() bool { return len(snapshot()) > n }) | |
| 96 | - | time.Sleep(20 * time.Millisecond) | |
| 98 | + | synctest.Wait() | |
| 97 | 99 | for _, c := range snapshot()[n:] { | |
| 98 | 100 | if v, ok := active(c); ok && v != 0 && c.flags&atomicTestOnly == 0 { | |
| 99 | 101 | t.Fatal("VT resume turned the display on") | |
| @@ -120,7 +122,9 @@ func TestRunOutputPower(t *testing.T) { | |||
| 120 | 122 | // An output started off (it reconnected while turned off) never activates | |
| 121 | 123 | // its CRTC before a scene turns it on; its tearing probe, refused by an | |
| 122 | 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 | 128 | o, k, commits, commitMu := testOutputMu(t) | |
| 125 | 129 | o.tearing, o.cursor = true, nil | |
| 126 | 130 | o.StartOff = true | |
| @@ -180,7 +184,7 @@ func TestRunOutputStartOff(t *testing.T) { | |||
| 180 | 184 | } | |
| 181 | 185 | off := ports.Scene{OutputWidth: 200, OutputHeight: 100, Scale: 1, Off: true} | |
| 182 | 186 | scenes <- off | |
| 183 | - | time.Sleep(20 * time.Millisecond) | |
| 187 | + | synctest.Wait() | |
| 184 | 188 | if lit(snapshot()) { | |
| 185 | 189 | t.Fatal("a display started off lit up") | |
| 186 | 190 | } | |
| @@ -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 next | 14 | // 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, nil | 20 | 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 | <-renders | 81 | <-renders | ||
| 80 | } | 82 | } | ||
| 81 | scenes <- off | 83 | 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 <- false | 95 | vt <- false | ||
| 94 | vt <- true | 96 | 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 activates | 122 | // 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 an | 123 | // 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, nil | 129 | o.tearing, o.cursor = true, nil | ||
| 126 | o.StartOff = true | 130 | 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 <- off | 186 | 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 ( | |||
| 5 | 5 | "os" | |
| 6 | 6 | "sync/atomic" | |
| 7 | 7 | "testing" | |
| 8 | + | "testing/synctest" | |
| 8 | 9 | "time" | |
| 9 | 10 | ||
| 10 | 11 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| @@ -13,6 +14,10 @@ import ( | |||
| 13 | 14 | ) | |
| 14 | 15 | ||
| 15 | 16 | func TestSecurityRunWakePendingResumeAndOff(t *testing.T) { | |
| 17 | + | synctest.Test(t, securityRunWakePendingResumeAndOff) | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func securityRunWakePendingResumeAndOff(t *testing.T) { | |
| 16 | 21 | o, k, commits, mu := testOutputMu(t) | |
| 17 | 22 | o.cursor, o.tearing = nil, false | |
| 18 | 23 | var state atomic.Uint64 | |
| @@ -74,10 +79,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) { | |||
| 74 | 79 | }) | |
| 75 | 80 | state.Store(3) | |
| 76 | 81 | wake <- ports.SecurityState{} // stale payload: read gate, not payload | |
| 82 | + | synctest.Wait() | |
| 77 | 83 | select { | |
| 78 | 84 | case ev := <-events: | |
| 79 | 85 | t.Fatalf("proof while old frame pending: %+v", ev) | |
| 80 | - | case <-time.After(20 * time.Millisecond): | |
| 86 | + | default: | |
| 81 | 87 | } | |
| 82 | 88 | flips <- eventOf(frame) | |
| 83 | 89 | proof := func(kind ports.ProtectionKind) { | |
| @@ -98,10 +104,11 @@ func TestSecurityRunWakePendingResumeAndOff(t *testing.T) { | |||
| 98 | 104 | } | |
| 99 | 105 | proof(ports.ProtectionProtectedFrame) | |
| 100 | 106 | scenes <- ports.Scene{Seq: 99} // held desktop scene cannot be replayed | |
| 107 | + | synctest.Wait() | |
| 101 | 108 | select { | |
| 102 | 109 | case <-desktop: | |
| 103 | 110 | t.Fatal("stale desktop rendered under protection") | |
| 104 | - | case <-time.After(20 * time.Millisecond): | |
| 111 | + | default: | |
| 105 | 112 | } | |
| 106 | 113 | active <- false | |
| 107 | 114 | 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, false | 22 | o.cursor, o.tearing = nil, false | ||
| 18 | var state atomic.Uint64 | 23 | 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 payload | 81 | 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 replayed | 106 | 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 <- false | 113 | active <- false | ||
| 107 | select { | 114 | select { | ||
M internal/adapters/drm/stuck_test.go +9 -1
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "sync" | |
| 7 | 7 | "sync/atomic" | |
| 8 | 8 | "testing" | |
| 9 | + | "testing/synctest" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| 11 | 12 | portsmocks "github.com/bnema/neferwl/internal/mocks/ports" | |
| @@ -149,6 +150,10 @@ func fencePipe(t *testing.T) (fence, signal *os.File) { | |||
| 149 | 150 | // waiting past the limit; once the fence signalled and still no event | |
| 150 | 151 | // came, the output modesets. | |
| 151 | 152 | func TestUnsignalledFenceDelaysRecovery(t *testing.T) { | |
| 153 | + | synctest.Test(t, unsignalledFenceDelaysRecovery) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func unsignalledFenceDelaysRecovery(t *testing.T) { | |
| 152 | 157 | var mu sync.Mutex | |
| 153 | 158 | var signals []*os.File | |
| 154 | 159 | h := startStuck(t, func() *os.File { | |
| @@ -161,6 +166,7 @@ func TestUnsignalledFenceDelaysRecovery(t *testing.T) { | |||
| 161 | 166 | h.scenes <- ports.Scene{Seq: 1} | |
| 162 | 167 | waitFor(t, func() bool { return h.count() == 3 }) | |
| 163 | 168 | time.Sleep(3 * 200 * time.Millisecond) | |
| 169 | + | synctest.Wait() | |
| 164 | 170 | if n := h.count(); n != 3 { | |
| 165 | 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 | 219 | // An EBUSY run ends when commits stop being refused: a later EBUSY is | |
| 214 | 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 | 224 | o, _, _ := testOutput(t, unix.EBUSY, unix.EBUSY) | |
| 217 | 225 | o.frame.stuckAfter = 100 * time.Millisecond | |
| 218 | 226 | 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 event | 150 | // 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.Mutex | 157 | var mu sync.Mutex | ||
| 153 | var signals []*os.File | 158 | 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 is | 219 | // 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.Millisecond | 225 | o.frame.stuckAfter = 100 * time.Millisecond | ||
| 218 | enabled := true | 226 | enabled := true | ||
M internal/adapters/libinput/forward_test.go +8 -5
| @@ -6,6 +6,7 @@ import ( | |||
| 6 | 6 | "strings" | |
| 7 | 7 | "sync" | |
| 8 | 8 | "testing" | |
| 9 | + | "testing/synctest" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| 11 | 12 | "github.com/bnema/neferwl/internal/ports" | |
| @@ -15,19 +16,20 @@ import ( | |||
| 15 | 16 | ||
| 16 | 17 | // A slow consumer must not block push, and stale motion must collapse into | |
| 17 | 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 | 20 | func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |
| 21 | + | synctest.Test(t, forwarderCoalescesMotionWithoutBlocking) | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func forwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |
| 19 | 25 | f := newForwarder(zerowrap.Default()) | |
| 20 | 26 | input := make(chan ports.InputEvent) // unbuffered: core is busy | |
| 21 | - | start := time.Now() | |
| 22 | 27 | for i := range 1000 { | |
| 23 | 28 | f.push(ports.PointerMotion{X: float64(i)}) | |
| 24 | 29 | } | |
| 25 | 30 | f.push(ports.PointerButton{Button: 272, Pressed: true}) | |
| 26 | 31 | f.push(ports.PointerMotion{X: 2000}) | |
| 27 | 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 | 33 | ctx, cancel := context.WithCancel(context.Background()) | |
| 32 | 34 | defer cancel() | |
| 33 | 35 | go f.run(ctx, input) | |
| @@ -46,10 +48,11 @@ func TestForwarderCoalescesMotionWithoutBlocking(t *testing.T) { | |||
| 46 | 48 | t.Fatalf("event %d not delivered", i) | |
| 47 | 49 | } | |
| 48 | 50 | } | |
| 51 | + | synctest.Wait() | |
| 49 | 52 | select { | |
| 50 | 53 | case got := <-input: | |
| 51 | 54 | t.Fatalf("unexpected extra event %#v", got) | |
| 52 | - | case <-time.After(20 * time.Millisecond): | |
| 55 | + | default: | |
| 53 | 56 | } | |
| 54 | 57 | s := f.take() | |
| 55 | 58 | 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 into | 17 | // 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 busy | 26 | 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "reflect" | |
| 6 | 6 | "testing" | |
| 7 | + | "testing/synctest" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| 9 | 10 | "github.com/bnema/neferwl/internal/adapters/wayland/sessionlock" | |
| @@ -185,6 +186,10 @@ func TestSecurityHotplugLifetimeAndProofOrdering(t *testing.T) { | |||
| 185 | 186 | } | |
| 186 | 187 | ||
| 187 | 188 | func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) { | |
| 189 | + | synctest.Test(t, securityOwnerFIFOSaturationDrainsOnNormalStop) | |
| 190 | + | } | |
| 191 | + | ||
| 192 | + | func securityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) { | |
| 188 | 193 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 189 | 194 | defer cancel() | |
| 190 | 195 | events := make(chan ports.SecurityBackendEvent) // block display after registration | |
| @@ -235,10 +240,11 @@ func TestSecurityOwnerFIFOSaturationDrainsOnNormalStop(t *testing.T) { | |||
| 235 | 240 | t.Fatal("FIFO failed to fill") | |
| 236 | 241 | } | |
| 237 | 242 | } | |
| 243 | + | synctest.Wait() | |
| 238 | 244 | select { | |
| 239 | 245 | case got := <-accepted: | |
| 240 | 246 | t.Fatalf("full FIFO did not backpressure owner: %d", got) | |
| 241 | - | case <-time.After(20 * time.Millisecond): | |
| 247 | + | default: | |
| 242 | 248 | } | |
| 243 | 249 | var ledger sessionlock.Readiness | |
| 244 | 250 | ledger.Add(registered.Instance) | |
| @@ -330,6 +336,10 @@ func TestSecurityOwnerBufferedEvidenceDrainsAfterFinish(t *testing.T) { | |||
| 330 | 336 | } | |
| 331 | 337 | ||
| 332 | 338 | func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) { | |
| 339 | + | synctest.Test(t, securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders) | |
| 340 | + | } | |
| 341 | + | ||
| 342 | + | func securityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing.T) { | |
| 333 | 343 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) | |
| 334 | 344 | defer cancel() | |
| 335 | 345 | events := make(chan ports.SecurityBackendEvent) | |
| @@ -364,10 +374,11 @@ func TestSecurityOwnerFIFOSaturationCancelReleasesOwnerAndForwarders(t *testing. | |||
| 364 | 374 | t.Fatal("FIFO never filled") | |
| 365 | 375 | } | |
| 366 | 376 | } | |
| 377 | + | synctest.Wait() | |
| 367 | 378 | select { | |
| 368 | 379 | case <-accepted: | |
| 369 | 380 | t.Fatal("producer did not block at finite FIFO") | |
| 370 | - | case <-time.After(20 * time.Millisecond): | |
| 381 | + | default: | |
| 371 | 382 | } | |
| 372 | 383 | waitDone := make(chan error, 1) | |
| 373 | 384 | 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 registration | 195 | 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.Readiness | 249 | 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 ( | |||
| 6 | 6 | "reflect" | |
| 7 | 7 | "slices" | |
| 8 | 8 | "testing" | |
| 9 | + | "testing/synctest" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| 11 | 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 | 531 | func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |
| 527 | 532 | cfg := config.Defaults() | |
| @@ -551,8 +556,9 @@ func clickAfterWorkspaceSwitch(t *testing.T, animated bool) { | |||
| 551 | 556 | client <- ports.WindowMapped{ID: 1} | |
| 552 | 557 | scene(t, scenes) | |
| 553 | 558 | 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)) | |
| 556 | 562 | } | |
| 557 | 563 | for len(commands) > 0 { | |
| 558 | 564 | <-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 | <-commands | 564 | <-commands | ||
M internal/core/inhibit_test.go +6 -2
| @@ -3,6 +3,7 @@ package core_test | |||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | 5 | "testing" | |
| 6 | + | "testing/synctest" | |
| 6 | 7 | "time" | |
| 7 | 8 | ||
| 8 | 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 | 32 | // A focused window inhibiting shortcuts gets bound keys; it stops when | |
| 32 | 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 | 37 | cfg := config.Defaults() | |
| 35 | 38 | input := make(chan ports.InputEvent, 4) | |
| 36 | 39 | client := make(chan ports.ClientEvent, 4) | |
| @@ -60,10 +63,11 @@ func TestShortcutsInhibit(t *testing.T) { | |||
| 60 | 63 | if v := commandOf[ports.ForwardKey](t, commands); v != (ports.ForwardKey{ID: 1, Key: bound}) { | |
| 61 | 64 | t.Fatal(v) | |
| 62 | 65 | } | |
| 66 | + | synctest.Wait() | |
| 63 | 67 | select { | |
| 64 | 68 | case r := <-spawn: | |
| 65 | 69 | t.Fatalf("bind ran: %v", r) | |
| 66 | - | case <-time.After(30 * time.Millisecond): | |
| 70 | + | default: | |
| 67 | 71 | } | |
| 68 | 72 | // A new window takes focus: the inhibitor goes inactive, binds run. | |
| 69 | 73 | 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 when | 32 | // 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 ( | |||
| 4 | 4 | "context" | |
| 5 | 5 | "slices" | |
| 6 | 6 | "testing" | |
| 7 | + | "testing/synctest" | |
| 7 | 8 | "time" | |
| 8 | 9 | ||
| 9 | 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 | 150 | // Input reports user activity to wayland, at most once per interval. | |
| 151 | + | // The bubble's fake clock makes the interval exact. | |
| 150 | 152 | 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 { | |
| 161 | 163 | t.Fatal("second UserActivity within the interval") | |
| 162 | 164 | } | |
| 163 | - | continue | |
| 164 | - | case <-deadline: | |
| 165 | 165 | } | |
| 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 | + | }) | |
| 171 | 170 | } | |
| 172 | 171 | ||
| 173 | 172 | // 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 | - | break | 166 | + | 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 UserActivity | 172 | // With an injected clock, the throttle follows it: no second UserActivity | ||