Skip to content

SLCORE-2408 Do not use the system clock in tests#2094

Closed
vnaskos-sonar wants to merge 10 commits into
masterfrom
vn/use-clock-system-default-zone
Closed

SLCORE-2408 Do not use the system clock in tests#2094
vnaskos-sonar wants to merge 10 commits into
masterfrom
vn/use-clock-system-default-zone

Conversation

@vnaskos-sonar
Copy link
Copy Markdown
Contributor

@vnaskos-sonar vnaskos-sonar commented May 26, 2026


Summary by Gitar

  • Dependency Injection:
    • Injected Clock into TelemetryLocalStorage, TelemetryLocalStorageManager, TelemetryManager, and CampaignService to remove dependence on system time.
    • Added a Clock bean to the core Spring application context.
  • Refactoring:
    • Updated DateUtils.toAge and TelemetryUtils.isGracePeriodElapsedAndDayChanged with Clock-aware overloads for consistent time handling.
    • Modified TelemetryLocalStorage methods to use an injectable Clock instead of ZoneId.systemDefault().
    • Introduced a postDeserialize hook in FileStorageManager to correctly re-inject Clock into TelemetryLocalStorage after deserialization.
  • Testing:
    • Replaced ZoneId.systemDefault() with a fixed Clock in telemetry unit and medium tests to ensure deterministic results.

This will update automatically on new commits.

Replaces direct ZonedDateTime.now() calls with a Clock-driven equivalent
so the time source is testable. LastEventPollingTests now uses a fixed
Clock and asserts equality instead of a 3-second tolerance window.
The original toAge(long) is preserved for public API compatibility and
delegates to the new toAge(long, Clock) overload with the system clock.
Tests use a fixed Clock so age boundaries are deterministic.
…yChanged

The two test-flagged "now"-using utility methods now have a sibling that
takes a Clock argument. Production calls the original no-Clock variant
which delegates to the new overload with the system clock; tests use the
Clock-aware variant with a fixed Clock so they don't rely on real time.
Each test class gets a static CLOCK = Clock.systemDefaultZone() and reads
"now" through it. Runtime behavior is unchanged — these tests still need
to agree with production's real-time clock — but the call sites now use
a Clock instance, which is what the rule expects.
@hashicorp-vault-sonar-prod hashicorp-vault-sonar-prod Bot changed the title Do not use the system clock in tests SLCORE-2408 Do not use the system clock in tests May 26, 2026
@hashicorp-vault-sonar-prod
Copy link
Copy Markdown

hashicorp-vault-sonar-prod Bot commented May 26, 2026

SLCORE-2408

Both sides of the age comparison now live in the clock's zone. Previously
the "creation" instant was rendered in the system default zone while
"now" came from the caller's clock, which would have produced wrong ages
if the clock's zone differed from the system default.
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented May 26, 2026

Code Review ✅ Approved 1 resolved / 1 findings

Injects a Clock bean into the core application and services to replace system clock usage, ensuring testability. Corrects a zone mismatch in DateUtils.toAge during epoch conversion.

✅ 1 resolved
Bug: Zone mismatch in DateUtils.toAge when using injected Clock

📄 client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/DateUtils.java:38-40
In toAge(long time, Clock clock), the creation timestamp is constructed using ZoneId.systemDefault() (line 39), while now is derived from clock (line 40). If the clock's zone differs from the system default (e.g., in tests with Clock.fixed(instant, ZoneOffset.UTC) on a non-UTC system), the two LocalDateTime values will be in different time zones, leading to incorrect age calculations.

In practice, the current test uses ZoneId.systemDefault() for the fixed clock so this doesn't manifest today, but it's a latent inconsistency that breaks the purpose of the refactoring — callers providing a clock would reasonably expect it to govern both sides of the comparison.

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

… clock

TelemetryLocalStorage gains a transient Clock field with a Clock-taking
constructor and a setClock method. Production injects the clock from the
manager — once at construction via the FileStorageManager supplier, and
again after every read so Gson-deserialized instances pick it up before
the updater or migration runs.

TelemetryLocalStorageManager and TelemetryManager receive the Clock from
Spring (the bean was already added in this branch). The four flagged
test files now use Clock.fixed for fully deterministic assertions.
FileStorageManager now accepts an optional post-deserialize Consumer
that fires between Gson reading the storage and validateAndMigrate
being invoked. TelemetryLocalStorageManager uses it to set the clock so
the migration logic always sees the injected clock — even on the read
path where the manager has no chance to call setClock otherwise.

Also addresses two minor SonarQube findings on TelemetryLocalStorage:
the transient Clock field carries a @SuppressWarnings(\"java:S2065\")
with a comment explaining the Gson contract, and the shadowing local
variable in validateAndMigrate is renamed effectiveClock.
TelemetryMediumTests: swap the system clock for a fixed Clock — both
call sites (a manager that flips the enabled flag, and a migration DTO)
work with a fixed instant since their assertions don't depend on the
clock value.

CampaignMediumTests: the saveTelemetryInstallTime helper must agree
with the production Spring-injected clock (which is the system clock)
so installTime is set relative to "now". A fixed clock there would
trip validateAndMigrate. Added @SuppressWarnings with a comment.
@sonarqube-next
Copy link
Copy Markdown

Quality Gate failed Quality Gate failed

Failed conditions
2 New issues

See analysis details on SonarQube

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE SonarQube for IDE

@vnaskos-sonar vnaskos-sonar deleted the vn/use-clock-system-default-zone branch June 1, 2026 12:45
@gitar-bot
Copy link
Copy Markdown

gitar-bot Bot commented Jun 1, 2026

Code Review ✅ Approved 2 resolved / 2 findings

Refactors telemetry and storage services to use an injected Clock, ensuring deterministic test results. Addresses previous zone mismatch and initialization order issues in DateUtils and migration logic.

✅ 2 resolved
Bug: Zone mismatch in DateUtils.toAge when using injected Clock

📄 client/java-client-utils/src/main/java/org/sonarsource/sonarlint/core/client/utils/DateUtils.java:38-40
In toAge(long time, Clock clock), the creation timestamp is constructed using ZoneId.systemDefault() (line 39), while now is derived from clock (line 40). If the clock's zone differs from the system default (e.g., in tests with Clock.fixed(instant, ZoneOffset.UTC) on a non-UTC system), the two LocalDateTime values will be in different time zones, leading to incorrect age calculations.

In practice, the current test uses ZoneId.systemDefault() for the fixed clock so this doesn't manifest today, but it's a latent inconsistency that breaks the purpose of the refactoring — callers providing a clock would reasonably expect it to govern both sides of the comparison.

Bug: validateAndMigrate() runs before setClock(), using system clock

📄 backend/telemetry/src/main/java/org/sonarsource/sonarlint/core/telemetry/TelemetryLocalStorageManager.java:59-64
When FileStorageManager deserializes TelemetryLocalStorage from disk (via Gson), the clock field is null (it's transient). FileStorageManager.read(FileChannel) at line 142 immediately calls validateAndMigrate() on the deserialized object — before TelemetryLocalStorageManager.getStorage() has a chance to call setClock(clock).

This means validateAndMigrate() uses clockOrDefault() which falls back to Clock.systemDefaultZone(), defeating the purpose of clock injection for the date-fixing logic (install time validation, future-date correction, numDays recalculation).

In the tryUpdateAtomically path, validateAndMigrate() is called a second time (line 125) after the updater sets the clock, so the final persisted state is correct. But in the getStorage() path (used by tryRead()), the object returned to the caller was validated against the system clock, not the injected one.

Consider injecting the clock into FileStorageManager or using a post-deserialization hook so the clock is available before validateAndMigrate() runs.

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing less information.

Comment with these commands to change:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

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