Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Trio.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@
3EF667132FE48509009FB31A /* BasalDeliveryState+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EF667122FE48502009FB31A /* BasalDeliveryState+Extension.swift */; };
3F23E18680094E6DA98628E4 /* QuickPickBolusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A54068ABDAE4898B243DF14 /* QuickPickBolusesView.swift */; };
41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */; };
A15B00312F30AA0100AB0C31 /* SettingsBackupTestFixtures.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15B00412F30AA0100AB0C41 /* SettingsBackupTestFixtures.swift */; };
A15B00322F30AA0100AB0C32 /* SettingsBackupCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15B00422F30AA0100AB0C42 /* SettingsBackupCodableTests.swift */; };
A15B00332F30AA0100AB0C33 /* SettingsImportApplierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15B00432F30AA0100AB0C43 /* SettingsImportApplierTests.swift */; };
A15B00342F30AA0100AB0C34 /* SettingsBackupE2ETests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15B00442F30AA0100AB0C44 /* SettingsBackupE2ETests.swift */; };
A15B00352F30AA0100AB0C35 /* SettingsImportPresetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A15B00452F30AA0100AB0C45 /* SettingsImportPresetTests.swift */; };
45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
49090A8D2E9FE8D200D0F5DB /* GarminWatchSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49090A8C2E9FE8D200D0F5DB /* GarminWatchSettings.swift */; };
Expand Down Expand Up @@ -1428,6 +1433,11 @@
B015AFE12E500000000D7351 /* BolusSafetyValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidator.swift; sourceTree = "<group>"; };
B015AFE42E500000000D7351 /* BolusSafetyValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BolusSafetyValidatorTests.swift; sourceTree = "<group>"; };
B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SettingsSearchTests.swift; sourceTree = "<group>"; };
A15B00412F30AA0100AB0C41 /* SettingsBackupTestFixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupTestFixtures.swift; sourceTree = "<group>"; };
A15B00422F30AA0100AB0C42 /* SettingsBackupCodableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupCodableTests.swift; sourceTree = "<group>"; };
A15B00432F30AA0100AB0C43 /* SettingsImportApplierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsImportApplierTests.swift; sourceTree = "<group>"; };
A15B00442F30AA0100AB0C44 /* SettingsBackupE2ETests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsBackupE2ETests.swift; sourceTree = "<group>"; };
A15B00452F30AA0100AB0C45 /* SettingsImportPresetTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsImportPresetTests.swift; sourceTree = "<group>"; };
B6E925122EB3932A0076D719 /* OmnipodKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OmnipodKit.framework; sourceTree = BUILT_PRODUCTS_DIR; };
B9B5C0607505A38F256BF99A /* CGMSettingsDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CGMSettingsDataFlow.swift; sourceTree = "<group>"; };
BA49538D56989D8DA6FCF538 /* TargetsEditorDataFlow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorDataFlow.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3043,6 +3053,10 @@
CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
3B5CD2C72D4AECD500CE213C /* OpenAPSSwiftTests */,
B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */,
A15B00412F30AA0100AB0C41 /* SettingsBackupTestFixtures.swift */,
A15B00422F30AA0100AB0C42 /* SettingsBackupCodableTests.swift */,
A15B00432F30AA0100AB0C43 /* SettingsImportApplierTests.swift */,
A15B00442F30AA0100AB0C44 /* SettingsBackupE2ETests.swift */,
BD8FC0532D66186000B95AED /* TestError.swift */,
C39FF1EC12CF4B1EBB13BE26 /* NightscoutUploadSerializerTests.swift */,
BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */,
Expand Down Expand Up @@ -3694,6 +3708,7 @@
BD8FC05A2D6618AF00B95AED /* DeterminationStorageTests.swift */,
BD8FC05F2D6619DB00B95AED /* CarbsStorageTests.swift */,
BD8FC0612D6619E600B95AED /* OverrideStorageTests.swift */,
A15B00452F30AA0100AB0C45 /* SettingsImportPresetTests.swift */,
BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */,
BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */,
);
Expand Down Expand Up @@ -5762,6 +5777,11 @@
DD30BA002E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift in Sources */,
BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,
41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */,
A15B00312F30AA0100AB0C31 /* SettingsBackupTestFixtures.swift in Sources */,
A15B00322F30AA0100AB0C32 /* SettingsBackupCodableTests.swift in Sources */,
A15B00332F30AA0100AB0C33 /* SettingsImportApplierTests.swift in Sources */,
A15B00342F30AA0100AB0C34 /* SettingsBackupE2ETests.swift in Sources */,
A15B00352F30AA0100AB0C35 /* SettingsImportPresetTests.swift in Sources */,
B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */,
BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */,
CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */,
Expand Down
51 changes: 44 additions & 7 deletions Trio/Sources/Models/SettingsBackup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,47 @@ extension SettingsBackup {
}
}

extension InsulinSensitivities: Equatable {}
extension InsulinSensitivityEntry: Equatable {}
extension CarbRatios: Equatable {}
extension CarbRatioEntry: Equatable {}
extension BGTargets: Equatable {}
extension BGTargetEntry: Equatable {}
extension PumpSettings: Equatable {}
// Swift cannot synthesize Equatable in extensions outside the declaring file, so == is spelled
// out for the therapy types the backup embeds.
extension InsulinSensitivities: Equatable {
static func == (lhs: InsulinSensitivities, rhs: InsulinSensitivities) -> Bool {
lhs.units == rhs.units && lhs.userPreferredUnits == rhs.userPreferredUnits &&
lhs.sensitivities == rhs.sensitivities
}
}

extension InsulinSensitivityEntry: Equatable {
static func == (lhs: InsulinSensitivityEntry, rhs: InsulinSensitivityEntry) -> Bool {
lhs.sensitivity == rhs.sensitivity && lhs.offset == rhs.offset && lhs.start == rhs.start
}
}

extension CarbRatios: Equatable {
static func == (lhs: CarbRatios, rhs: CarbRatios) -> Bool {
lhs.units == rhs.units && lhs.schedule == rhs.schedule
}
}

extension CarbRatioEntry: Equatable {
static func == (lhs: CarbRatioEntry, rhs: CarbRatioEntry) -> Bool {
lhs.start == rhs.start && lhs.offset == rhs.offset && lhs.ratio == rhs.ratio
}
}

extension BGTargets: Equatable {
static func == (lhs: BGTargets, rhs: BGTargets) -> Bool {
lhs.units == rhs.units && lhs.userPreferredUnits == rhs.userPreferredUnits && lhs.targets == rhs.targets
}
}

extension BGTargetEntry: Equatable {
static func == (lhs: BGTargetEntry, rhs: BGTargetEntry) -> Bool {
lhs.low == rhs.low && lhs.high == rhs.high && lhs.start == rhs.start && lhs.offset == rhs.offset
}
}

extension PumpSettings: Equatable {
static func == (lhs: PumpSettings, rhs: PumpSettings) -> Bool {
lhs.insulinActionCurve == rhs.insulinActionCurve && lhs.maxBolus == rhs.maxBolus && lhs.maxBasal == rhs.maxBasal
}
}
269 changes: 269 additions & 0 deletions TrioTests/CoreDataTests/SettingsImportPresetTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import CoreData
import Foundation
import Swinject
import Testing

@testable import Trio

@Suite("Settings Import Preset Tests", .serialized) struct SettingsImportPresetTests: Injectable {
@Injected() var tempTargetsStorage: TempTargetsStorage!
@Injected() var overrideStorage: OverrideStorage!
let resolver: Resolver
var coreDataStack: CoreDataStack!
var testContext: NSManagedObjectContext!

init() async throws {
coreDataStack = try await CoreDataStack.createForTests()
testContext = coreDataStack.newTaskContext()

let assembler = Assembler([
StorageAssembly(),
ServiceAssembly(),
APSAssembly(),
NetworkAssembly(),
UIAssembly(),
SecurityAssembly(),
TestAssembly(testContext: testContext)
])

resolver = assembler.resolver
injectServices(resolver)
}

private func seedTempTargetPreset(name: String, target: Decimal, enabled: Bool = false) async throws {
try await tempTargetsStorage.storeTempTarget(tempTarget: TempTarget(
name: name,
createdAt: Date(),
targetTop: target,
targetBottom: target,
duration: 60,
enteredBy: TempTarget.local,
reason: nil,
isPreset: true,
enabled: enabled,
halfBasalTarget: nil
))
}

private func loadedPresets() async throws -> SettingsBackupPresetLoader.LoadedPresets {
try await SettingsBackupPresetLoader.load(
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)
}

@Test("Replace same-named replaces duplicates and keeps others") func testReplaceSameNamed() async throws {
try await seedTempTargetPreset(name: "Sport", target: 140)
try await seedTempTargetPreset(name: "Night", target: 110)

let imported = SettingsBackup.Presets(
tempTargets: [
SettingsBackup.TempTargetPreset(name: "Sport", target: 150, duration: 90, halfBasalTarget: nil, orderPosition: nil),
SettingsBackup.TempTargetPreset(name: "New", target: 130, duration: 30, halfBasalTarget: nil, orderPosition: nil)
],
overrides: nil,
meals: nil
)

await SettingsBackupPresetApplier.apply(
imported,
categories: [.tempTargetPresets],
strategy: .replaceSameNamed,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let byName = Dictionary(
(result.presets.tempTargets ?? []).map { ($0.name, $0) },
uniquingKeysWith: { first, _ in first }
)

#expect(byName.count == 3)
#expect(byName["Sport"]?.target == 150)
#expect(byName["Sport"]?.duration == 90)
#expect(byName["Night"]?.target == 110)
#expect(byName["New"]?.target == 130)
}

@Test("Keep existing never touches presets with known names") func testKeepExisting() async throws {
try await seedTempTargetPreset(name: "Sport", target: 140)

let imported = SettingsBackup.Presets(
tempTargets: [
SettingsBackup.TempTargetPreset(name: "Sport", target: 150, duration: 90, halfBasalTarget: nil, orderPosition: nil),
SettingsBackup.TempTargetPreset(name: "New", target: 130, duration: 30, halfBasalTarget: nil, orderPosition: nil)
],
overrides: nil,
meals: nil
)

await SettingsBackupPresetApplier.apply(
imported,
categories: [.tempTargetPresets],
strategy: .keepExisting,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let byName = Dictionary(
(result.presets.tempTargets ?? []).map { ($0.name, $0) },
uniquingKeysWith: { first, _ in first }
)

#expect(byName.count == 2)
#expect(byName["Sport"]?.target == 140)
#expect(byName["New"]?.target == 130)
}

@Test("Replace all removes presets missing from the file") func testReplaceAll() async throws {
try await seedTempTargetPreset(name: "Sport", target: 140)
try await seedTempTargetPreset(name: "Night", target: 110)

let imported = SettingsBackup.Presets(
tempTargets: [
SettingsBackup.TempTargetPreset(name: "Sport", target: 150, duration: 90, halfBasalTarget: nil, orderPosition: nil)
],
overrides: nil,
meals: nil
)

await SettingsBackupPresetApplier.apply(
imported,
categories: [.tempTargetPresets],
strategy: .replaceAll,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let tempTargets = result.presets.tempTargets ?? []

#expect(tempTargets.count == 1)
#expect(tempTargets.first?.name == "Sport")
#expect(tempTargets.first?.target == 150)
}

@Test("A running preset survives replace-all untouched") func testRunningPresetIsProtected() async throws {
try await seedTempTargetPreset(name: "Sport", target: 140, enabled: true)

let imported = SettingsBackup.Presets(
tempTargets: [
SettingsBackup.TempTargetPreset(name: "Sport", target: 150, duration: 90, halfBasalTarget: nil, orderPosition: nil)
],
overrides: nil,
meals: nil
)

let warnings = await SettingsBackupPresetApplier.apply(
imported,
categories: [.tempTargetPresets],
strategy: .replaceAll,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let tempTargets = result.presets.tempTargets ?? []

#expect(tempTargets.count == 1)
#expect(tempTargets.first?.target == 140)
#expect(result.activeTempTargetNames.contains("Sport"))
#expect(warnings.contains { $0.contains("Sport") })
}

@Test("Override presets round-trip all attributes") func testOverridePresetFidelity() async throws {
let importedOverride = SettingsBackup.OverridePreset(
name: "Lazy Sunday",
percentage: 80,
indefinite: false,
duration: 120,
target: 120,
advancedSettings: true,
smbIsOff: false,
smbIsScheduledOff: true,
start: 8,
end: 20,
isfAndCr: true,
isf: true,
cr: true,
smbMinutes: 45,
uamMinutes: 45,
orderPosition: nil
)

await SettingsBackupPresetApplier.apply(
SettingsBackup.Presets(tempTargets: nil, overrides: [importedOverride], meals: nil),
categories: [.overridePresets],
strategy: .replaceSameNamed,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let stored = try #require(result.presets.overrides?.first)

#expect(stored.name == importedOverride.name)
#expect(stored.percentage == importedOverride.percentage)
#expect(stored.indefinite == importedOverride.indefinite)
#expect(stored.duration == importedOverride.duration)
#expect(stored.target == importedOverride.target)
#expect(stored.advancedSettings == importedOverride.advancedSettings)
#expect(stored.smbIsScheduledOff == importedOverride.smbIsScheduledOff)
#expect(stored.start == importedOverride.start)
#expect(stored.end == importedOverride.end)
#expect(stored.isfAndCr == importedOverride.isfAndCr)
#expect(stored.smbMinutes == importedOverride.smbMinutes)
#expect(stored.uamMinutes == importedOverride.uamMinutes)
}

@Test("Meal presets import and replace by dish name") func testMealPresets() async throws {
let firstImport = SettingsBackup.Presets(
tempTargets: nil,
overrides: nil,
meals: [SettingsBackup.MealPreset(dish: "Pizza", carbs: 80, fat: 30, protein: 25)]
)
await SettingsBackupPresetApplier.apply(
firstImport,
categories: [.mealPresets],
strategy: .replaceSameNamed,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let secondImport = SettingsBackup.Presets(
tempTargets: nil,
overrides: nil,
meals: [
SettingsBackup.MealPreset(dish: "Pizza", carbs: 90, fat: 35, protein: 30),
SettingsBackup.MealPreset(dish: "Salad", carbs: 10, fat: 5, protein: 5)
]
)
await SettingsBackupPresetApplier.apply(
secondImport,
categories: [.mealPresets],
strategy: .replaceSameNamed,
tempTargetsStorage: tempTargetsStorage,
overrideStorage: overrideStorage,
context: testContext
)

let result = try await loadedPresets()
let byDish = Dictionary(
(result.presets.meals ?? []).map { ($0.dish, $0) },
uniquingKeysWith: { first, _ in first }
)

#expect(byDish.count == 2)
#expect(byDish["Pizza"]?.carbs == 90)
#expect(byDish["Salad"]?.carbs == 10)
}
}
Loading