refactor: make ThemeService's settings path redirectable for tests - #1928
Merged
Conversation
…1741) ThemeService held its settings path in a `static readonly SettingsPath` built from Environment.SpecialFolder.ApplicationData. That API resolves through the Win32 known-folder function and ignores the APPDATA environment variable, so no test could point it at a temp directory — any test that constructed the service and saved would overwrite the developer's real theme.json. It is the same untestable-static-path class that already caused data loss in SpeedTestHistoryService (#1734) and the last instance-backed offender on the ArchitectureTests.Services_DoNotHoldUserDataPathsInStaticFields ratchet. The path moves to an instance field set from the shared `string? configDir = null` constructor seam the other persistence services already use — production still reaches it through the Instance singleton (null -> the real %AppData% path, ROAMING, unchanged), the ctor is internal because the app has exactly one theme service. ThemeService.SettingsPath comes off the ratchet's known-list, leaving only LogService (a static partial class by Serilog-sink design, which needs a real design decision, not a mechanical edit). One enabling change: Apply() now no-ops when Application.Current is null instead of NPEing. It is the service's single WPF touch-point, reached from every public entry, so guarding it is what lets the persistence path run in a headless unit test — and it is harmless in production, where Application.Current is always set. New ThemeServiceTests exercise the seam headlessly: SetPreset writes to the configured directory and leaves the real profile untouched; a second instance reads the choice back; an empty directory keeps the default; a shade survives a reload. Red proof, 3 mutations: hard-coding the real path reddens the "not the real profile" test; re-adding a static user-data path anywhere in Services reddens the ratchet; removing the Apply guard reddens all the headless tests. Behaviour in the running app is unchanged — no version bump, no release.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
ThemeServiceheld its settings path in astatic readonly SettingsPathbuilt fromEnvironment.SpecialFolder.ApplicationData. That known-folder API ignores theAPPDATAenvironmentvariable (verified in #1741), so no test could point the service at a temp directory — any test that
constructed it and saved would overwrite the developer's real
theme.json. It is the sameuntestable-static-path class that already caused data loss in
SpeedTestHistoryService(#1734), andthe last instance-backed offender on the
ArchitectureTests.Services_DoNotHoldUserDataPathsInStaticFieldsratchet.What
string? configDir = nullconstructor seamthe other persistence services already use. Production is unchanged: it reaches the service through
the
Instancesingleton, which passes null → the real%AppData%\SysManager\theme.json(ROAMING, asbefore). The ctor is
internalbecause the app has exactly one theme service.ThemeService.SettingsPathcomes off the ratchet's known-list, leaving onlyLogService— astatic partial classby Serilog-sink design, which needs a genuine design decision (drop thestatic-sink model), not a mechanical edit, and which no test constructs (lowest risk).
Apply()now no-ops whenApplication.Current is nullinstead of throwing.It is the service's single WPF touch-point, reached from every public entry, so guarding it is what
lets the persistence path run in a headless unit test. Harmless in production, where
Application.Currentis always set; a no-op is the correct behaviour when there is no resourcedictionary to repaint.
Verification
New
ThemeServiceTests, headless:SetPresetwrites to the configured directory and leaves the real profile byte-for-byte untouched(the actual data-loss guard);
Red proof: 3 mutations, both touched source files restored byte-for-byte.
configDir, hard-code the real%AppData%path (the shipped defect)SetPreset_WritesToTheConfiguredDirectory_NotTheRealProfileServicesApplynull-guard (throws again headless)The read-back and shade tests deliberately do not discriminate the path mutation — they round-trip
over whatever single path both instances share — which is why the "…NotTheRealProfile" test is the
load-bearing one for #1741.
Behaviour in the running app is identical, so this is
refactor:— all four projects build 0/0,dotnet format --verify-no-changesexit 0 on both, author headers present, leak scan over all 32 termsgives 0 hits. No version bump, no release.