mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-31 19:40:54 +00:00
test(mac): fixed names for scratch settings suites (#1576)
* test(mac): clean up scratch UserDefaults suites, guard against new leaks * test(mac): fixed suite names so late cfprefsd write-backs reuse one file per test * test(mac): start every scratch suite clean
This commit is contained in:
parent
c17e7b05aa
commit
a7ca8bdb94
22 changed files with 156 additions and 79 deletions
|
|
@ -145,11 +145,10 @@ struct AppStoreCopilotDisconnectTests {
|
|||
@MainActor
|
||||
private func withIsolatedCopilotStore(
|
||||
hasCredential: Bool,
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (AppStore, CopilotFetchStub, UserDefaults) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.copilot.disconnect.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.copilot.disconnect.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let fetch = CopilotFetchStub()
|
||||
|
|
|
|||
|
|
@ -171,9 +171,8 @@ struct CapacityDockGlanceWindowTests {
|
|||
|
||||
// MARK: - Persistence
|
||||
|
||||
private func defaults() -> (UserDefaults, String) {
|
||||
let suiteName = "CodeBurnMenubarTests.CapacityDockGlance.\(UUID().uuidString)"
|
||||
return (UserDefaults(suiteName: suiteName)!, suiteName)
|
||||
private func defaults(_ testName: String = #function) -> (UserDefaults, String) {
|
||||
TestDefaults.make("CodeBurnMenubarTests.CapacityDockGlance.\(testName)")
|
||||
}
|
||||
|
||||
@Test("the glance window defaults to the billing horizon for every provider")
|
||||
|
|
|
|||
|
|
@ -104,8 +104,7 @@ struct CapacityDockDragSettlingTests {
|
|||
@MainActor
|
||||
@Test("settling when no drag is in flight changes nothing")
|
||||
func settlingIdleDockIsANoOp() {
|
||||
let name = "CodeBurnMenubarTests.DragSettle.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: name)!
|
||||
let (defaults, name) = TestDefaults.make("CodeBurnMenubarTests.DragSettle.\(#function)")
|
||||
defer { TestDefaults.forget(name) }
|
||||
|
||||
let controller = CapacityDockController(store: AppStore(), defaults: defaults)
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ import Testing
|
|||
|
||||
@Suite("Capacity Dock preferences")
|
||||
struct CapacityDockPreferencesTests {
|
||||
private func defaults() -> (UserDefaults, String) {
|
||||
let suiteName = "CodeBurnMenubarTests.CapacityDock.\(UUID().uuidString)"
|
||||
return (UserDefaults(suiteName: suiteName)!, suiteName)
|
||||
private func defaults(_ testName: String = #function) -> (UserDefaults, String) {
|
||||
TestDefaults.make("CodeBurnMenubarTests.CapacityDock.\(testName)")
|
||||
}
|
||||
|
||||
@Test("fresh installs keep the dock off with Codex as the only resting provider")
|
||||
|
|
|
|||
|
|
@ -25,8 +25,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("rail body length follows presentation progress instead of snapping to interaction state")
|
||||
func presentationLengthInterpolates() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.Presentation.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.Presentation")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
let model = CapacityDockViewModel(preferences: CapacityDockPreferences.load(defaults: defaults))
|
||||
|
|
@ -43,8 +42,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("Resting provider stays at the reveal anchor")
|
||||
func restingProviderFollowsExpansionAnchor() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.AnchorOrder.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.AnchorOrder")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
let model = CapacityDockViewModel(preferences: CapacityDockPreferences.load(defaults: defaults))
|
||||
|
|
@ -59,8 +57,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("Attachment morphs inside the body without changing the panel size")
|
||||
func attachmentKeepsPanelSizeStable() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.EdgeSpread.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.EdgeSpread")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setScale(1.2, defaults: defaults)
|
||||
let model = CapacityDockViewModel(preferences: CapacityDockPreferences.load(defaults: defaults))
|
||||
|
|
@ -79,8 +76,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("Supported settled scales and edges compose integral panel dimensions")
|
||||
func supportedSettledGeometryIsIntegral() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.IntegralGeometry.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.IntegralGeometry")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
|
||||
|
|
@ -228,8 +224,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("Selected provider identity remains stable throughout reveal and retraction")
|
||||
func selectedProviderIdentityStaysStable() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.Identity.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.Identity")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
CapacityDockPreferences.setPreferredProvider(.codex, defaults: defaults)
|
||||
|
|
@ -252,8 +247,7 @@ struct CapacityDockPresentationTests {
|
|||
@MainActor
|
||||
@Test("Resting provider stays visible when the rail expands toward its start edge")
|
||||
func restingProviderNeverFlashesToAnotherIcon() {
|
||||
let suite = "CodeBurnMenubarTests.CapacityDock.NoIconFlash.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
let (defaults, suite) = TestDefaults.make("CodeBurnMenubarTests.CapacityDock.NoIconFlash")
|
||||
defer { TestDefaults.forget(suite) }
|
||||
CapacityDockPreferences.setSelectedProviders([.codex, .claude, .gemini], defaults: defaults)
|
||||
CapacityDockPreferences.setPreferredProvider(.codex, defaults: defaults)
|
||||
|
|
|
|||
|
|
@ -436,8 +436,7 @@ struct CapacityDockProviderQuotaServiceTests {
|
|||
|
||||
@Test("removeProvider drops only the target and keeps manual selection unlatched")
|
||||
func removeProviderPersistence() throws {
|
||||
let suiteName = "CodeBurnMenubarTests.CapacityDockRemove.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.CapacityDockRemove")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
let cursor = try #require(CapacityDockProvider(rawValue: "cursor"))
|
||||
defaults.set(["codex", "cursor"], forKey: CapacityDockPreferences.selectedProvidersKey)
|
||||
|
|
|
|||
|
|
@ -351,6 +351,7 @@ struct CodexBankedResetAnnouncerTests {
|
|||
|
||||
@MainActor
|
||||
private func withAnnouncer(
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (
|
||||
CodexBankedResetAnnouncer,
|
||||
RecordingBankedResetNotifier,
|
||||
|
|
@ -358,9 +359,7 @@ private func withAnnouncer(
|
|||
MemoryBankedResetStore
|
||||
) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.codex.bankedResets.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.codex.bankedResets.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let notifier = RecordingBankedResetNotifier()
|
||||
|
|
|
|||
|
|
@ -50,9 +50,7 @@ struct CopilotQuotaPresentationTests {
|
|||
|
||||
@Test("explicit disconnect copy differs from first-use; clearing the flag restores first-use")
|
||||
func explicitDisconnectDiffersFromFirstUseAndClears() throws {
|
||||
let suiteName = "codeburn.copilot.presentation.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.copilot.presentation.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
#expect(!CopilotExplicitDisconnect.isSet(defaults: defaults))
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ struct CredentialKeychainCacheRedTests {
|
|||
private let accountSentinel = "cb-red-account-sentinel"
|
||||
|
||||
private func withIsolatedSeams(
|
||||
testName: String = #function,
|
||||
_ body: (URL, InMemoryKeychainCredentialCache) throws -> Void
|
||||
) throws {
|
||||
CredentialStoreTestIsolation.lock.lock()
|
||||
|
|
@ -21,9 +22,7 @@ struct CredentialKeychainCacheRedTests {
|
|||
.appendingPathComponent("codeburn-0b-red-\(UUID().uuidString)", isDirectory: true)
|
||||
let support = root.appendingPathComponent("Application Support", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: support, withIntermediateDirectories: true)
|
||||
let suiteName = "codeburn.0b.red.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.0b.red.\(testName)")
|
||||
|
||||
let fakeKeychain = InMemoryKeychainCredentialCache()
|
||||
ClaudeCredentialStore.resetTestSeams()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ struct CredentialKeychainContinuityTests {
|
|||
}
|
||||
|
||||
private func withHarness(
|
||||
testName: String = #function,
|
||||
_ body: (Harness) throws -> Void
|
||||
) throws {
|
||||
CredentialStoreTestIsolation.lock.lock()
|
||||
|
|
@ -38,9 +39,7 @@ struct CredentialKeychainContinuityTests {
|
|||
.appendingPathComponent("codeburn-0b-cont-\(UUID().uuidString)", isDirectory: true)
|
||||
let support = root.appendingPathComponent("Application Support", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: support, withIntermediateDirectories: true)
|
||||
let suiteName = "codeburn.0b.cont.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.0b.cont.\(testName)")
|
||||
let fake = InMemoryKeychainCredentialCache()
|
||||
|
||||
ClaudeCredentialStore.resetTestSeams()
|
||||
|
|
@ -166,10 +165,8 @@ struct CredentialKeychainContinuityTests {
|
|||
|
||||
@Test("concurrent provider-presence updates do not lose identifiers")
|
||||
func providerPresenceUpdatesAreAtomic() async {
|
||||
let suiteName = "codeburn.capacity-dock-presence.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.capacity-dock-presence.\(#function)")
|
||||
let defaultsBox = SendableUserDefaults(defaults)
|
||||
TestDefaults.forget(suiteName)
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
let providerIDs = Array(CapacityDockPreferences.supportedProviders.prefix(24).map(\.id))
|
||||
|
||||
|
|
|
|||
|
|
@ -7,9 +7,8 @@ import Testing
|
|||
/// external write (verified out of process), KVO does, so the observer is what is tested.
|
||||
@Suite("Defaults key observer")
|
||||
struct DefaultsKeyObserverTests {
|
||||
private func suite() -> (UserDefaults, String) {
|
||||
let name = "CodeBurnMenubarTests.DefaultsKeyObserver.\(UUID().uuidString)"
|
||||
return (UserDefaults(suiteName: name)!, name)
|
||||
private func suite(_ testName: String = #function) -> (UserDefaults, String) {
|
||||
TestDefaults.make("CodeBurnMenubarTests.DefaultsKeyObserver.\(testName)")
|
||||
}
|
||||
|
||||
@Test("a change to the watched key calls back")
|
||||
|
|
|
|||
|
|
@ -608,11 +608,10 @@ private func seedBaseline(_ monitor: EarlyQuotaResetMonitor) async {
|
|||
|
||||
@MainActor
|
||||
private func withIsolatedMonitor(
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (EarlyQuotaResetMonitor, RecordingEarlyResetNotifier, UserDefaults) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.quota.earlyReset.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.quota.earlyReset.monitor.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let notifier = RecordingEarlyResetNotifier()
|
||||
|
|
@ -952,11 +951,10 @@ struct EarlyQuotaResetCodexWiringTests {
|
|||
}
|
||||
|
||||
private func withCodexStore(
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (AppStore, RecordingEarlyResetNotifier) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.quota.earlyReset.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.quota.earlyReset.codexWiring.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let notifier = RecordingEarlyResetNotifier()
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ struct LanguagePreferenceTests {
|
|||
func applyWritesAppleLanguages() {
|
||||
let domain = domain("apply")
|
||||
defer { TestDefaults.forget(domain) }
|
||||
let scratch = UserDefaults(suiteName: domain)!
|
||||
let scratch = TestDefaults.open(domain)
|
||||
LanguagePreference.apply(.chineseSimplified, defaults: scratch)
|
||||
#expect(read(domain) == .chineseSimplified)
|
||||
LanguagePreference.apply(.system, defaults: scratch)
|
||||
|
|
|
|||
|
|
@ -42,8 +42,7 @@ struct MenubarPeriodSettingsTests {
|
|||
|
||||
@Test("periods persist canonical defaults values")
|
||||
func periodsPersistCanonicalDefaultsValues() {
|
||||
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
Period.sevenDays.persistAsMenubarDefault(defaults: defaults)
|
||||
|
|
@ -61,8 +60,7 @@ struct MenubarPeriodSettingsTests {
|
|||
|
||||
@Test("menubar scope persistence defaults to local and round-trips")
|
||||
func menubarScopePersistenceDefaultsToLocalAndRoundTrips() {
|
||||
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
#expect(MenubarScope.savedMenubarScope(defaults: defaults) == .local)
|
||||
|
|
|
|||
|
|
@ -467,8 +467,7 @@ struct MenubarSecondRowTests {
|
|||
|
||||
@Test("settings default to off and round-trip through UserDefaults")
|
||||
func preferencesRoundTrip() {
|
||||
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
#expect(MenubarRowPreferences.load(defaults: defaults) == MenubarRowSettings.default)
|
||||
|
|
|
|||
|
|
@ -115,8 +115,7 @@ struct ProviderSettingsEditorStateTests {
|
|||
|
||||
@Test("presence writes allow a synchronous UserDefaults callback to read presence")
|
||||
func presenceWritesDoNotDeadlockSwiftUICallbacks() async throws {
|
||||
let suiteName = "CodeBurnMenubarTests.PresenceReentry.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.PresenceReentry")
|
||||
let defaultsBox = SendableUserDefaults(defaults)
|
||||
let callbackFinished = LockedCompletionFlag()
|
||||
let writeFinished = LockedCompletionFlag()
|
||||
|
|
|
|||
|
|
@ -150,11 +150,10 @@ struct QuotaCrossingMonitorTests {
|
|||
|
||||
@MainActor
|
||||
private func withMonitor(
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (QuotaCrossingMonitor, RecordingCrossingNotifier, UserDefaults) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.quota.crossing.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.quota.crossing.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let notifier = RecordingCrossingNotifier()
|
||||
|
|
|
|||
|
|
@ -64,13 +64,16 @@ struct TelemetryTests {
|
|||
|
||||
/// A defaults suite and a directory that exist only for one test.
|
||||
final class Scratch {
|
||||
let suiteName = "CodeBurnMenubarTests.Telemetry.\(UUID().uuidString)"
|
||||
let suiteName: String
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("codeburn-telemetry-tests-\(UUID().uuidString)")
|
||||
let defaults: UserDefaults
|
||||
|
||||
init() {
|
||||
defaults = UserDefaults(suiteName: suiteName)!
|
||||
/// `label` distinguishes several `Scratch`es created by one test
|
||||
/// (a loop, or a few named cases); it must be unique per call site.
|
||||
init(_ label: String = "", testName: String = #function) {
|
||||
let name = label.isEmpty ? testName : "\(testName).\(label)"
|
||||
(defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.Telemetry.\(name)")
|
||||
try? FileManager.default.createDirectory(
|
||||
at: directory, withIntermediateDirectories: true)
|
||||
}
|
||||
|
|
@ -206,17 +209,17 @@ struct TelemetryTests {
|
|||
|
||||
@Test("standalone defaults on outside the default-off region, off inside it and off when unknown")
|
||||
func standaloneRegionDefaults() {
|
||||
let us = Scratch()
|
||||
let us = Scratch("us")
|
||||
#expect(Self.client(us, region: "US").status().enabled)
|
||||
|
||||
let de = Scratch()
|
||||
let de = Scratch("de")
|
||||
#expect(Self.client(de, region: "DE").status().enabled == false)
|
||||
|
||||
let unknown = Scratch()
|
||||
let unknown = Scratch("unknown")
|
||||
#expect(Self.client(unknown, region: nil).status().enabled == false)
|
||||
|
||||
// A UN M.49 region is nobody's country code, so it reads as unknown.
|
||||
let m49 = Scratch()
|
||||
let m49 = Scratch("m49")
|
||||
#expect(Self.client(m49, region: "419").status().enabled == false)
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +299,7 @@ struct TelemetryTests {
|
|||
func toggleStateForEveryCombination() {
|
||||
for desktopEnabled in [true, false] {
|
||||
for vetoed in [true, false] {
|
||||
let scratch = Scratch()
|
||||
let scratch = Scratch("desktop\(desktopEnabled)-veto\(vetoed)")
|
||||
scratch.defaults.set(vetoed, forKey: Telemetry.localOptOutKey)
|
||||
let state = scratch.writeDesktopState(
|
||||
"""
|
||||
|
|
@ -312,7 +315,7 @@ struct TelemetryTests {
|
|||
}
|
||||
}
|
||||
|
||||
let scratch = Scratch()
|
||||
let scratch = Scratch("standalone")
|
||||
let standalone = Self.client(scratch, region: "US")
|
||||
#expect(standalone.status().source == .app)
|
||||
#expect(standalone.status().isLocked == false, "standalone the toggle is never a readout")
|
||||
|
|
@ -376,12 +379,12 @@ struct TelemetryTests {
|
|||
func standaloneExplicitDecisionSwitch() {
|
||||
// Both settings of Telemetry.standaloneRequiresExplicitDecision, so the
|
||||
// product call is a one-line change with tests already standing.
|
||||
let regionDecides = Scratch()
|
||||
let regionDecides = Scratch("regionDecides")
|
||||
let deciding = Self.client(regionDecides, region: "US", requiresExplicitDecision: false)
|
||||
deciding.track("app_open")
|
||||
#expect(deciding.queuedEvents.count == 1, "the region default is itself the answer")
|
||||
|
||||
let mustBeAsked = Scratch()
|
||||
let mustBeAsked = Scratch("mustBeAsked")
|
||||
let waiting = Self.client(mustBeAsked, region: "US", requiresExplicitDecision: true)
|
||||
waiting.track("app_open")
|
||||
#expect(waiting.queuedEvents.isEmpty, "nothing before the question has been answered")
|
||||
|
|
|
|||
|
|
@ -265,8 +265,7 @@ struct TerminalLauncherTests {
|
|||
|
||||
@Test("preference defaults to Terminal.app when unset, preserving pre-#877 behaviour")
|
||||
func defaultsToTerminalWhenUnset() {
|
||||
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
#expect(PreferredTerminal.saved(defaults: defaults) == .terminal)
|
||||
|
|
@ -275,8 +274,7 @@ struct TerminalLauncherTests {
|
|||
|
||||
@Test("preference round-trips and unknown values collapse to the default")
|
||||
func preferenceRoundTripsAndRejectsGarbage() {
|
||||
let suiteName = "CodeBurnMenubarTests.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suiteName)!
|
||||
let (defaults, suiteName) = TestDefaults.make("CodeBurnMenubarTests.\(#function)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
PreferredTerminal.iTerm2.persist(defaults: defaults)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,26 @@ import Foundation
|
|||
/// right after creating it -- so a full `swift test` run does not grow the
|
||||
/// file count under Preferences.
|
||||
enum TestDefaults {
|
||||
/// A `UserDefaults` suite named `name`, paired with that name for
|
||||
/// `forget(_:)`. `name` must be fixed, not a uuid: cfprefsd can still
|
||||
/// write a cleared domain's plist back to disk after `forget(_:)`
|
||||
/// deletes it, and a fixed name lands that write on the same file every
|
||||
/// run instead of a new one. A shared helper, a loop, or a parameterized
|
||||
/// test must fold something distinguishing -- the caller's `#function`,
|
||||
/// an index, an argument -- into `name` so no two tests share one.
|
||||
static func make(_ name: String) -> (UserDefaults, String) {
|
||||
(open(name), name)
|
||||
}
|
||||
|
||||
/// Opens a suite/domain name as `UserDefaults`, for call sites that
|
||||
/// already have their own name instead of calling `make(_:)`. Forgets it
|
||||
/// first, so a fixed name starts clean even after a run that got killed
|
||||
/// before its own `forget(_:)` ran.
|
||||
static func open(_ suiteName: String) -> UserDefaults {
|
||||
forget(suiteName)
|
||||
return UserDefaults(suiteName: suiteName)!
|
||||
}
|
||||
|
||||
static func forget(_ suiteName: String) {
|
||||
// A `UserDefaults` instance scoped to the suite -- not `.standard` --
|
||||
// so `synchronize()` below flushes *this* domain's own dirty state.
|
||||
|
|
|
|||
|
|
@ -98,11 +98,10 @@ struct UpdateNotificationTests {
|
|||
|
||||
@MainActor
|
||||
private func withIsolatedChecker(
|
||||
testName: String = #function,
|
||||
_ body: @MainActor (UpdateChecker, RecordingUpdateNotifier, UserDefaults) async throws -> Void
|
||||
) async throws {
|
||||
let suiteName = "codeburn.update.notifications.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
TestDefaults.forget(suiteName)
|
||||
let (defaults, suiteName) = TestDefaults.make("codeburn.update.notifications.\(testName)")
|
||||
defer { TestDefaults.forget(suiteName) }
|
||||
|
||||
let notifier = RecordingUpdateNotifier()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import Foundation
|
||||
import Testing
|
||||
|
||||
/// Guards the fix for the test-only `UserDefaults(suiteName:` leak: every
|
||||
/// scratch suite must go through `TestDefaults.make`/`open`, whose only
|
||||
/// direct call sites live in `TestDefaults.swift`, so its plist actually gets
|
||||
/// cleaned up via `TestDefaults.forget`.
|
||||
@Suite("UserDefaults suite leak guard")
|
||||
struct UserDefaultsSuiteLeakGuardTests {
|
||||
private static var testsDirectory: URL {
|
||||
URL(fileURLWithPath: #filePath)
|
||||
.deletingLastPathComponent() // CodeBurnMenubarTests
|
||||
.deletingLastPathComponent() // Tests
|
||||
}
|
||||
|
||||
@Test("the guard can actually see the files it is meant to check")
|
||||
func testsAreReachable() throws {
|
||||
let files = try swiftFiles(in: Self.testsDirectory)
|
||||
#expect(
|
||||
files.count > 10,
|
||||
"found \(files.count) Swift files under \(Self.testsDirectory.path); if the tree moved, this guard checks nothing"
|
||||
)
|
||||
}
|
||||
|
||||
@Test("no test file creates a UserDefaults suite outside TestDefaults.swift")
|
||||
func noDirectSuiteCreation() throws {
|
||||
let needle = "UserDefaults(" + "suiteName"
|
||||
let exempt: Set<String> = ["TestDefaults.swift", "UserDefaultsSuiteLeakGuardTests.swift"]
|
||||
var offenders: [String] = []
|
||||
for file in try swiftFiles(in: Self.testsDirectory) where !exempt.contains(file.lastPathComponent) {
|
||||
let contents = try String(contentsOf: file, encoding: .utf8)
|
||||
for (index, line) in contents.components(separatedBy: "\n").enumerated() where line.contains(needle) {
|
||||
offenders.append("\(file.lastPathComponent):\(index + 1)")
|
||||
}
|
||||
}
|
||||
#expect(
|
||||
offenders.isEmpty,
|
||||
"""
|
||||
\(offenders.count) test call site(s) create a UserDefaults suite directly, \
|
||||
bypassing TestDefaults.forget and leaking its plist: \(offenders). \
|
||||
Use TestDefaults.make(_:) or TestDefaults.open(_:) instead.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
@Test("no two TestDefaults.make/open call sites use the same static suite name")
|
||||
func noDuplicateSuiteNames() throws {
|
||||
let pattern = try NSRegularExpression(pattern: #"TestDefaults\.(?:make|open)\(\s*"([^"]*)""#)
|
||||
var locations: [String: [String]] = [:]
|
||||
for file in try swiftFiles(in: Self.testsDirectory) where file.lastPathComponent != "TestDefaults.swift" {
|
||||
let contents = try String(contentsOf: file, encoding: .utf8)
|
||||
for (index, line) in contents.components(separatedBy: "\n").enumerated() {
|
||||
let range = NSRange(line.startIndex..., in: line)
|
||||
guard let match = pattern.firstMatch(in: line, range: range),
|
||||
let nameRange = Range(match.range(at: 1), in: line) else { continue }
|
||||
let name = String(line[nameRange])
|
||||
// A name built from the caller's own `#function` is unique per call
|
||||
// site by construction (a different function, a different name);
|
||||
// only a name with no such per-caller piece can silently collide.
|
||||
guard !name.contains("#function") else { continue }
|
||||
locations[name, default: []].append("\(file.lastPathComponent):\(index + 1)")
|
||||
}
|
||||
}
|
||||
let duplicates = locations.filter { $0.value.count > 1 }
|
||||
#expect(
|
||||
duplicates.isEmpty,
|
||||
"""
|
||||
\(duplicates.count) suite name(s) are reused across call sites, so two tests \
|
||||
can write and forget the same plist concurrently: \
|
||||
\(duplicates.map { "\"\($0.key)\" at \($0.value)" }.joined(separator: "; ")). \
|
||||
Fold something distinguishing into the name -- the caller's #function, a loop \
|
||||
index, a parameterized test's argument.
|
||||
"""
|
||||
)
|
||||
}
|
||||
|
||||
private func swiftFiles(in directory: URL) throws -> [URL] {
|
||||
guard let enumerator = FileManager.default.enumerator(
|
||||
at: directory, includingPropertiesForKeys: nil
|
||||
) else { return [] }
|
||||
return enumerator.compactMap { $0 as? URL }.filter { $0.pathExtension == "swift" }
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue