Skip to content

test: make timer-dependent tests deterministic with testing/synctest - #63

Merged
kolkov merged 1 commit into
mainfrom
test/deterministic-time
Oct 18, 2026
Merged

kolkov merged 1 commit into
mainfrom
test/deterministic-time

Conversation

@kolkov

@kolkov kolkov commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

TestWithTimeout and TestIsCanceled in internal/util failed intermittently: they set a 1ms timeout, slept 5ms and expected the context to be canceled. context.WithTimeout cancels from a time.AfterFunc goroutine. When the process is descheduled across both deadlines, both timers fire in the same scheduler pass and the test goroutine runs before the cancel goroutine. Under parallel load the old tests failed 57 times in 4800 runs; with a goroutine holding the only P past the deadlines, 151 of 200.

Every unit test that depended on real elapsed time now runs in testing/synctest (Go 1.25, stdlib). The fake clock advances only while all goroutines are blocked, and synctest.Wait lets the cancel goroutine finish, so the checks are exact and take no wall time:

  • internal/util: the context is alive 1ns before the timeout and DeadlineExceeded exactly at it; the deadline value is checked for equality instead of within 100ms.
  • internal/core: TestContextTimeout / TestContextDeadline had the same race with a wider window.
  • internal/util: UUIDv7 ordering relied on the wall clock advancing within a 2ms sleep, which depends on the OS clock tick (15.6ms by default on Windows). It now asserts consecutive millisecond timestamps, so a constant timestamp can no longer pass half the time; the embedded timestamp is checked exactly.
  • internal/core: TestWithHealthCheck slept 150ms and asserted nothing. It now checks that the first tick pings and records the time, and that a closed database is reported unhealthy with the error kept. lastError() lost its //nolint:unused because the test uses it.

No library behaviour changes.

Verification:

  • 0 failures in the same 12-process stress run (-cpu 1,4).
  • Each rewritten test fails when its behaviour is broken. These mutations were all caught: timeout ±1ns, IsCanceled always false, constant UUID timestamp, health checker not pinging, losing the error, wrong timestamp.
  • -race clean on Linux (go1.27.1).
  • gofmt, vet, go test ./... and golangci-lint pass on a clean checkout (go1.25.5), plus linux and darwin/arm64 builds.

TestWithTimeout and TestIsCanceled slept 5ms past a 1ms deadline and
then checked the context. The deadline is enforced by time.AfterFunc,
which cancels from its own goroutine; when the process is descheduled
across both deadlines, both timers fire in one scheduler pass and the
test goroutine runs before the cancel goroutine. Under parallel load the
old tests failed 57 times in 4800 runs.

Move every test that depends on real elapsed time to testing/synctest
(Go 1.25): the fake clock advances only while all goroutines are blocked
and synctest.Wait lets the cancel goroutine finish.

- util: context timeout checked exactly at the boundary (alive 1ns
  before, DeadlineExceeded at the timeout); exact deadline value
- core: TestContextTimeout/TestContextDeadline, same race
- util: UUIDv7 ordering asserted on consecutive millisecond timestamps
  instead of the OS wall clock tick; embedded timestamp checked exactly
- core: TestWithHealthCheck slept 150ms and asserted nothing; it now
  checks the first tick pings and records the time, and that a closed
  database is reported unhealthy with the error kept
- core/health.go: drop the nolint on lastError, now used by the test
@codecov

codecov Bot commented Oct 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@kolkov
kolkov merged commit 7dc13fd into main Oct 18, 2026
10 checks passed
@kolkov
kolkov deleted the test/deterministic-time branch October 18, 2026 00:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant