Skip to content

test(config): use synctest in TestAddFlags for avoiding nondeterminism #3311

Merged
julienrbrt merged 3 commits intoevstack:mainfrom
CaelRowley:cael/fix/flaky-namespace-test
May 7, 2026
Merged

test(config): use synctest in TestAddFlags for avoiding nondeterminism #3311
julienrbrt merged 3 commits intoevstack:mainfrom
CaelRowley:cael/fix/flaky-namespace-test

Conversation

@CaelRowley
Copy link
Copy Markdown
Contributor

@CaelRowley CaelRowley commented May 6, 2026

Overview

Fixes a flaky TestAddFlags caused by non-deterministic defaults.
DA.Namespace used randString seeded with time.Now().Unix(), and DefaultConfig() was called twice in the test (once in AddFlags, once for the expected value). If the timestamp second changed between calls, the seeds diverged and the assertion failed.

This PR extracts the time source into a package-level nowUnix variable in defaults.go, allowing the test to fix the seed and make both calls deterministic.

Reproduced locally via go test ./... -count=1:

  --- FAIL: TestAddFlags (0.00s)                                                                         
      config_test.go:502:
          Error: Not equal:                                                                              
              expected: "5ofJh48irg"
              actual  : "Duu3osBRoN"                                                                     
          Test:    TestAddFlags                                                                          
          Messages: Flag evnode.da.namespace should have default value 5ofJh48irg
  FAIL    github.com/evstack/ev-node/pkg/config   3.396s   

Summary by CodeRabbit

  • Tests
    • Improved test stability for configuration flag assertions by implementing deterministic time handling, ensuring consistent behavior across repeated test runs.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

TestAddFlags is wrapped in a synctest.Test block to freeze the time source during execution, ensuring deterministic DefaultConfig() behavior. The test overrides the nowUnix function to a fixed timestamp, runs assertions with stable values, and restores the original time function after cleanup.

Changes

Test Flakiness Fix via Time Freezing

Layer / File(s) Summary
Test Stabilization
pkg/config/config_test.go
TestAddFlags is wrapped in synctest.Test with frozen time source to ensure deterministic DefaultConfig() outputs and stable DA.Namespace values across repeated test runs.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A test that danced with time,
Now frozen at its prime,
With synctest's gentle hold,
No flakiness takes hold!
Deterministic flags enroll.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description provides a comprehensive overview including the problem (flaky test), root cause (non-deterministic time-based seeding), and solution (extracting time source into a package-level variable).
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly describes the main change: using synctest in TestAddFlags to fix non-determinism issues. It's specific, concise, and directly relates to the primary objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
pkg/config/defaults.go (1)

136-137: 💤 Low value

Package-level mutable nowUnix is unsafe if tests ever run in parallel.

Mutating a bare var from a test goroutine while another goroutine reads it (e.g., via a concurrent DefaultConfig() call) is a data race that go test -race will catch. Currently no tests in this package call t.Parallel(), so there is no active race, but the risk is latent.

A minimal guard would use sync/atomic with a stored int64, or accept the constraint by adding a comment that parallel tests must not stub nowUnix. As an alternative, passing the seed as a parameter to randString (and accepting it from DefaultConfig) avoids the global entirely.

🛡️ Option A – atomic swap (low-footprint)
-// nowUnix returns the current Unix timestamp; package-level so tests can stub it.
-var nowUnix = func() int64 { return time.Now().Unix() }
+import "sync/atomic"
+import "unsafe"
+
+// nowUnixFn is the actual function pointer; swapped atomically in tests.
+var nowUnixPtr atomic.Pointer[func() int64]
+
+func init() {
+    f := func() int64 { return time.Now().Unix() }
+    nowUnixPtr.Store(&f)
+}
+
+func nowUnix() int64 { return (*nowUnixPtr.Load())() }

In tests:

-origNowUnix := nowUnix
-nowUnix = func() int64 { return 2_000_000_000 }
-t.Cleanup(func() { nowUnix = origNowUnix })
+f := func() int64 { return 2_000_000_000 }
+nowUnixPtr.Store(&f)
+t.Cleanup(func() {
+    orig := func() int64 { return time.Now().Unix() }
+    nowUnixPtr.Store(&orig)
+})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/config/defaults.go` around lines 136 - 137, The package-level mutable var
nowUnix must be replaced with an atomic-backed value to avoid races: introduce a
package int64 nowUnixVal and replace the var nowUnix func with a getter
NowUnix() that returns atomic.LoadInt64(&nowUnixVal), add a test helper
SetNowUnixForTest(v int64) that does atomic.StoreInt64(&nowUnixVal), and update
all callers (e.g., DefaultConfig and randString) to call NowUnix() instead of
invoking the former nowUnix variable; this preserves test stubbing without data
races.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@pkg/config/defaults.go`:
- Around line 136-137: The package-level mutable var nowUnix must be replaced
with an atomic-backed value to avoid races: introduce a package int64 nowUnixVal
and replace the var nowUnix func with a getter NowUnix() that returns
atomic.LoadInt64(&nowUnixVal), add a test helper SetNowUnixForTest(v int64) that
does atomic.StoreInt64(&nowUnixVal), and update all callers (e.g., DefaultConfig
and randString) to call NowUnix() instead of invoking the former nowUnix
variable; this preserves test stubbing without data races.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d64c11e7-8bfc-4fc1-b2f6-1932d49513c1

📥 Commits

Reviewing files that changed from the base of the PR and between 264314f and be48b91.

📒 Files selected for processing (2)
  • pkg/config/config_test.go
  • pkg/config/defaults.go

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.85%. Comparing base (495c621) to head (890b784).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3311      +/-   ##
==========================================
- Coverage   60.87%   60.85%   -0.03%     
==========================================
  Files         127      127              
  Lines       13762    13762              
==========================================
- Hits         8378     8375       -3     
- Misses       4473     4476       +3     
  Partials      911      911              
Flag Coverage Δ
combined 60.85% <ø> (-0.03%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Member

@julienrbrt julienrbrt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Let's use the latest libs

Comment thread pkg/config/config_test.go
@@ -41,6 +41,12 @@ func TestDefaultConfig(t *testing.T) {
}

func TestAddFlags(t *testing.T) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use testing/synctest instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah good suggestion, then we dont need to modify config/defaults.go updated 👍

@CaelRowley CaelRowley requested a review from julienrbrt May 7, 2026 07:42
@julienrbrt julienrbrt changed the title fix(config): de-flake TestAddFlags by stubbing time source test(config): de-flake TestAddFlags by stubbing time source May 7, 2026
@julienrbrt julienrbrt changed the title test(config): de-flake TestAddFlags by stubbing time source test(config): use synctest in TestAddFlags for avoiding nondeterminism May 7, 2026
@julienrbrt julienrbrt enabled auto-merge May 7, 2026 07:45
@CaelRowley
Copy link
Copy Markdown
Contributor Author

Hey @julienrbrt The failing CI jobs look unrelated to my changes, Docker push and the Claude review fail with fork-PR permission errors (denied: installation not allowed to Write organization package, id-token: write missing), and the E2E failure is in TestHASequencerRollingRestartE2E which my PR doesn't touch.

@julienrbrt julienrbrt disabled auto-merge May 7, 2026 09:57
@julienrbrt julienrbrt merged commit 01791ca into evstack:main May 7, 2026
25 of 30 checks passed
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.

2 participants