diff --git a/Sources/DevConfiguration/Core/ConfigVariableReader.swift b/Sources/DevConfiguration/Core/ConfigVariableReader.swift index 10dd9f9..cafe333 100644 --- a/Sources/DevConfiguration/Core/ConfigVariableReader.swift +++ b/Sources/DevConfiguration/Core/ConfigVariableReader.swift @@ -116,8 +116,9 @@ public final class ConfigVariableReader: Sendable { var namedProviders = namedProviders if isEditorEnabled { - let provider = EditorOverrideProvider() - provider.load(from: UserDefaults(suiteName: EditorOverrideProvider.suiteName)!) + let provider = EditorOverrideProvider( + userDefaults: UserDefaults(suiteName: "devkit.DevConfiguration") ?? .standard + ) editorOverrideProvider = provider namedProviders.insert(.init(provider, displayName: localizedString("editorOverrideProvider.name")), at: 0) } diff --git a/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift b/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift index c687fb0..add75ee 100644 --- a/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift +++ b/Sources/DevConfiguration/Editor/ConfigVariableEditor.swift @@ -46,7 +46,6 @@ public struct ConfigVariableEditor: View { workingCopyDisplayName: localizedString("editorOverrideProvider.name"), namedProviders: namedProviders, registeredVariables: Array(reader.registeredVariables.values), - userDefaults: .standard, undoManager: UndoManager() ) diff --git a/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift b/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift index 7cdfd5f..58c7a3b 100644 --- a/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift +++ b/Sources/DevConfiguration/Editor/Data Models/EditorDocument.swift @@ -46,9 +46,6 @@ final class EditorDocument { /// The editor override provider. private let editorOverrideProvider: EditorOverrideProvider - /// The UserDefaults instance used for persisting overrides. - private let userDefaults: UserDefaults - /// The undo manager used for working copy changes. let undoManager: UndoManager @@ -75,19 +72,16 @@ final class EditorDocument { /// - workingCopyDisplayName: The display name for the working copy in the UI. /// - namedProviders: The reader's named providers, excluding the editor override provider. /// - registeredVariables: The registered variables to display in the editor. - /// - userDefaults: The UserDefaults instance used for persisting overrides. /// - undoManager: The undo manager for working copy changes. init( editorOverrideProvider: EditorOverrideProvider, workingCopyDisplayName: String, namedProviders: [NamedConfigProvider], registeredVariables: [RegisteredConfigVariable], - userDefaults: UserDefaults, undoManager: UndoManager ) { self.editorOverrideProvider = editorOverrideProvider self.workingCopyDisplayName = workingCopyDisplayName - self.userDefaults = userDefaults self.undoManager = undoManager // Build registered variables dictionary @@ -416,7 +410,7 @@ extension EditorDocument { } // Persist - editorOverrideProvider.persist(to: userDefaults) + editorOverrideProvider.persist() // Update baseline baseline = workingCopy diff --git a/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift b/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift index dee519e..b04a2ac 100644 --- a/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift +++ b/Sources/DevConfiguration/Editor/Data Models/EditorOverrideProvider.swift @@ -49,17 +49,28 @@ final class EditorOverrideProvider: Sendable { /// The name used to identify this provider. static let providerName = "EditorOverrideProvider" - /// The UserDefaults suite name used for persistence. - static let suiteName = "devkit.DevConfiguration" - /// The UserDefaults key under which overrides are stored. private static let persistenceKey = "editorOverrides" /// The logger used for persistence diagnostics. private static let logger = Logger(subsystem: "DevConfiguration", category: "EditorOverrideProvider") + /// The UserDefaults instance used for persistence. + nonisolated(unsafe) private let userDefaults: UserDefaults + /// The mutable state protected by a mutex. private let mutableState: Mutex = .init(MutableState()) + + + /// Creates a new editor override provider. + /// + /// Loads any previously persisted overrides from the given UserDefaults instance. + /// + /// - Parameter userDefaults: The UserDefaults instance used for persistence. + init(userDefaults: UserDefaults) { + self.userDefaults = userDefaults + load() + } } @@ -206,13 +217,10 @@ extension EditorOverrideProvider { // MARK: - Persistence extension EditorOverrideProvider { - /// Loads persisted overrides from the given UserDefaults into memory. - /// - /// Any entries that fail to decode are silently skipped. This method is intended to be called once during setup, - /// before the provider is shared with other components. + /// Loads persisted overrides from UserDefaults into memory. /// - /// - Parameter userDefaults: The UserDefaults instance to load from. - func load(from userDefaults: UserDefaults) { + /// Any entries that fail to decode are silently skipped. + private func load() { guard let stored = userDefaults.dictionary(forKey: Self.persistenceKey) as? [String: Data] else { return } @@ -234,12 +242,10 @@ extension EditorOverrideProvider { } - /// Persists the current overrides to the given UserDefaults. + /// Persists the current overrides to UserDefaults. /// /// Each override is JSON-encoded individually. The resulting dictionary is stored under the persistence key. - /// - /// - Parameter userDefaults: The UserDefaults instance to persist to. - func persist(to userDefaults: UserDefaults) { + func persist() { let currentOverrides = overrides let encoder = JSONEncoder() encoder.outputFormatting = .sortedKeys @@ -258,12 +264,10 @@ extension EditorOverrideProvider { } - /// Removes all persisted overrides from the given UserDefaults. + /// Removes all persisted overrides from UserDefaults. /// /// This does not affect the in-memory overrides. - /// - /// - Parameter userDefaults: The UserDefaults instance to clear. - func clearPersistence(from userDefaults: UserDefaults) { + func clearPersistence() { userDefaults.removeObject(forKey: Self.persistenceKey) } } diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift index d895458..9867905 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable Detail/ConfigVariableDetailViewModelTests.swift @@ -16,15 +16,16 @@ import Testing struct ConfigVariableDetailViewModelTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() - let editorOverrideProvider = EditorOverrideProvider() - var userDefaults: UserDefaults! + let editorOverrideProvider: EditorOverrideProvider var workingCopyDisplayName: String! let undoManager = UndoManager() init() { + let userDefaults = UserDefaults(suiteName: "devkit.DevConfiguration.test.\(UUID())")! + userDefaults.removeObject(forKey: "editorOverrides") + editorOverrideProvider = EditorOverrideProvider(userDefaults: userDefaults) workingCopyDisplayName = randomAlphanumericString() - userDefaults = UserDefaults(suiteName: randomAlphanumericString())! } @@ -39,7 +40,6 @@ struct ConfigVariableDetailViewModelTests: RandomValueGenerating { workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables, - userDefaults: userDefaults, undoManager: undoManager ) } diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift index 5796422..ac74310 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Config Variable List/ConfigVariableListViewModelTests.swift @@ -16,8 +16,7 @@ import Testing struct ConfigVariableListViewModelTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() - let editorOverrideProvider = EditorOverrideProvider() - var userDefaults: UserDefaults! + let editorOverrideProvider: EditorOverrideProvider var workingCopyDisplayName: String! let undoManager = UndoManager() @@ -25,8 +24,10 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { init() { + let userDefaults = UserDefaults(suiteName: "devkit.DevConfiguration.test.\(UUID())")! + userDefaults.removeObject(forKey: "editorOverrides") + editorOverrideProvider = EditorOverrideProvider(userDefaults: userDefaults) workingCopyDisplayName = randomAlphanumericString() - userDefaults = UserDefaults(suiteName: randomAlphanumericString())! onSaveStub = Stub() } @@ -42,7 +43,6 @@ struct ConfigVariableListViewModelTests: RandomValueGenerating { workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables ?? [randomRegisteredVariable()], - userDefaults: userDefaults, undoManager: undoManager ) } diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift index 58b9d70..84de741 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorDocumentTests.swift @@ -16,15 +16,16 @@ import Testing struct EditorDocumentTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() - let editorOverrideProvider = EditorOverrideProvider() - var userDefaults: UserDefaults! + let editorOverrideProvider: EditorOverrideProvider var workingCopyDisplayName: String! let undoManager = UndoManager() init() { + let userDefaults = UserDefaults(suiteName: "devkit.DevConfiguration.test.\(UUID())")! + userDefaults.removeObject(forKey: "editorOverrides") + editorOverrideProvider = EditorOverrideProvider(userDefaults: userDefaults) workingCopyDisplayName = randomAlphanumericString() - userDefaults = UserDefaults(suiteName: randomAlphanumericString())! } @@ -40,7 +41,6 @@ struct EditorDocumentTests: RandomValueGenerating { workingCopyDisplayName: workingCopyDisplayName, namedProviders: namedProviders, registeredVariables: registeredVariables ?? [randomRegisteredVariable()], - userDefaults: userDefaults, undoManager: undoManager ) } diff --git a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorOverrideProviderTests.swift b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorOverrideProviderTests.swift index 7a77435..4bb6d77 100644 --- a/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorOverrideProviderTests.swift +++ b/Tests/DevConfigurationTests/Unit Tests/Editor/Data Models/EditorOverrideProviderTests.swift @@ -16,10 +16,19 @@ struct EditorOverrideProviderTests: RandomValueGenerating { var randomNumberGenerator = makeRandomNumberGenerator() + /// Creates an editor override provider backed by a test-specific UserDefaults suite. + private func makeTestProvider() -> EditorOverrideProvider { + let suiteName = "devkit.DevConfiguration.test.\(UUID())" + let userDefaults = UserDefaults(suiteName: suiteName)! + userDefaults.removeObject(forKey: "editorOverrides") + return EditorOverrideProvider(userDefaults: userDefaults) + } + + @Test func providerNameIsEditor() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() // expect #expect(provider.providerName != "editorOverrideProvider.name") @@ -29,7 +38,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func setOverrideThenRetrieve() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let content = ConfigContent.string(randomAlphanumericString()) @@ -44,7 +53,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeOverrideClearsStoredValue() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() provider.setOverride(.bool(true), forKey: key) @@ -59,7 +68,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeOverrideForNonexistentKeyIsNoOp() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() // exercise @@ -73,7 +82,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test func removeAllOverridesWhenEmptyIsNoOp() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() // exercise provider.removeAllOverrides() @@ -86,7 +95,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeAllOverridesClearsEverything() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key1 = randomConfigKey() let key2 = randomConfigKey() provider.setOverride(.int(1), forKey: key1) @@ -103,7 +112,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func hasOverrideReturnsTrueWhenSet() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() provider.setOverride(.bool(true), forKey: key) @@ -115,7 +124,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func hasOverrideReturnsFalseWhenNotSet() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() // expect @@ -126,7 +135,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func overridesReturnsFullDictionary() { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key1 = randomConfigKey() let key2 = randomConfigKey() let content1 = ConfigContent.string("a") @@ -142,7 +151,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func valueForKeyReturnsValueWhenTypeMatches() throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let content = ConfigContent.int(42) provider.setOverride(content, forKey: key) @@ -158,7 +167,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func valueForKeyReturnsNilValueWhenTypeMismatches() throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() provider.setOverride(.int(42), forKey: key) @@ -173,7 +182,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func valueForKeyReturnsNilValueWhenKeyNotFound() throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() // exercise @@ -187,7 +196,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func fetchValueDelegatesToValue() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let content = ConfigContent.bool(true) provider.setOverride(content, forKey: key) @@ -203,7 +212,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func snapshotReturnsCurrentState() throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let content = ConfigContent.double(3.14) provider.setOverride(content, forKey: key) @@ -221,7 +230,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func setOverrideDoesNotNotifyWhenValueUnchanged() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let absoluteKey = AbsoluteConfigKey(key) provider.setOverride(.int(1), forKey: key) @@ -249,7 +258,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeOverrideNotifiesValueWatchers() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let absoluteKey = AbsoluteConfigKey(key) provider.setOverride(.string("hello"), forKey: key) @@ -275,7 +284,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeOverrideNotifiesSnapshotWatchers() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() provider.setOverride(.bool(true), forKey: key) @@ -303,7 +312,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeAllOverridesNotifiesValueWatchers() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let absoluteKey = AbsoluteConfigKey(key) provider.setOverride(.int(42), forKey: key) @@ -329,7 +338,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func removeAllOverridesNotifiesSnapshotWatchers() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key1 = randomConfigKey() let key2 = randomConfigKey() provider.setOverride(.int(1), forKey: key1) @@ -358,7 +367,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func watchValueReturnsNilValueWhenTypeMismatches() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let absoluteKey = AbsoluteConfigKey(key) provider.setOverride(.int(42), forKey: key) @@ -383,7 +392,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func watchValueEmitsInitialAndSubsequentChanges() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() let absoluteKey = AbsoluteConfigKey(key) provider.setOverride(.int(1), forKey: key) @@ -409,7 +418,7 @@ struct EditorOverrideProviderTests: RandomValueGenerating { @Test mutating func watchSnapshotEmitsInitialAndSubsequentChanges() async throws { // set up - let provider = EditorOverrideProvider() + let provider = makeTestProvider() let key = randomConfigKey() // exercise @@ -448,6 +457,12 @@ struct EditorOverrideProviderPersistenceTests: RandomValueGenerating { } + /// Creates an editor override provider backed by a test-specific UserDefaults suite. + private func makeTestProvider() -> EditorOverrideProvider { + EditorOverrideProvider(userDefaults: makeTestUserDefaults()) + } + + @Test mutating func persistThenLoadRoundTripsOverrides() { // set up @@ -457,14 +472,13 @@ struct EditorOverrideProviderPersistenceTests: RandomValueGenerating { let content1 = ConfigContent.string(randomAlphanumericString()) let content2 = ConfigContent.int(randomInt(in: .min ... .max)) - let provider1 = EditorOverrideProvider() + let provider1 = EditorOverrideProvider(userDefaults: userDefaults) provider1.setOverride(content1, forKey: key1) provider1.setOverride(content2, forKey: key2) - provider1.persist(to: userDefaults) + provider1.persist() // exercise - let provider2 = EditorOverrideProvider() - provider2.load(from: userDefaults) + let provider2 = EditorOverrideProvider(userDefaults: userDefaults) // expect #expect(provider2.overrides[key1] == content1) @@ -476,12 +490,11 @@ struct EditorOverrideProviderPersistenceTests: RandomValueGenerating { func persistEmptyOverrides() { // set up let userDefaults = makeTestUserDefaults() - let provider1 = EditorOverrideProvider() - provider1.persist(to: userDefaults) + let provider1 = EditorOverrideProvider(userDefaults: userDefaults) + provider1.persist() // exercise - let provider2 = EditorOverrideProvider() - provider2.load(from: userDefaults) + let provider2 = EditorOverrideProvider(userDefaults: userDefaults) // expect #expect(provider2.overrides.isEmpty) @@ -492,28 +505,23 @@ struct EditorOverrideProviderPersistenceTests: RandomValueGenerating { mutating func clearPersistenceRemovesStoredData() { // set up let userDefaults = makeTestUserDefaults() - let provider = EditorOverrideProvider() + let provider = EditorOverrideProvider(userDefaults: userDefaults) provider.setOverride(.bool(true), forKey: randomConfigKey()) - provider.persist(to: userDefaults) + provider.persist() // exercise - provider.clearPersistence(from: userDefaults) + provider.clearPersistence() // expect - let reloaded = EditorOverrideProvider() - reloaded.load(from: userDefaults) + let reloaded = EditorOverrideProvider(userDefaults: userDefaults) #expect(reloaded.overrides.isEmpty) } @Test func loadWithNoStoredDataResultsInEmptyOverrides() { - // set up - let userDefaults = makeTestUserDefaults() - // exercise - let provider = EditorOverrideProvider() - provider.load(from: userDefaults) + let provider = makeTestProvider() // expect #expect(provider.overrides.isEmpty) @@ -530,8 +538,7 @@ struct EditorOverrideProviderPersistenceTests: RandomValueGenerating { userDefaults.set(corruptData, forKey: "editorOverrides") // exercise - let provider = EditorOverrideProvider() - provider.load(from: userDefaults) + let provider = EditorOverrideProvider(userDefaults: userDefaults) // expect #expect(provider.overrides.isEmpty)