Sfoglia il codice sorgente

Move CoreData stack initialization to async and add retries

Sam King 1 anno fa
parent
commit
923d14525c

+ 69 - 31
Model/CoreDataStack.swift

@@ -11,6 +11,8 @@ class CoreDataStack: ObservableObject {
 
     let persistentContainer: NSPersistentContainer
 
+    private let maxRetries = 3
+
     private init(inMemory: Bool = false) {
         self.inMemory = inMemory
 
@@ -41,29 +43,12 @@ class CoreDataStack: ObservableObject {
         description.shouldMigrateStoreAutomatically = true
         description.shouldInferMappingModelAutomatically = true
 
-        persistentContainer.loadPersistentStores { _, error in
-            if let error = error as NSError? {
-                fatalError("Unresolved Error \(DebuggingIdentifiers.failed) \(error), \(error.userInfo)")
-            }
-        }
-
         persistentContainer.viewContext.automaticallyMergesChangesFromParent = false
         persistentContainer.viewContext.name = "viewContext"
         /// - Tag: viewContextmergePolicy
         persistentContainer.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
         persistentContainer.viewContext.undoManager = nil
         persistentContainer.viewContext.shouldDeleteInaccessibleFaults = true
-
-        // Observe Core Data remote change notifications on the queue where the changes were made
-        notificationToken = Foundation.NotificationCenter.default.addObserver(
-            forName: .NSPersistentStoreRemoteChange,
-            object: nil,
-            queue: nil
-        ) { _ in
-            Task {
-                await self.fetchPersistentHistory()
-            }
-        }
     }
 
     deinit {
@@ -84,19 +69,18 @@ class CoreDataStack: ObservableObject {
     }
 
     // Factory method for tests
-    static func createForTests() -> CoreDataStack {
-        CoreDataStack(inMemory: true)
+    static func createForTests() async throws -> CoreDataStack {
+        let stack = CoreDataStack(inMemory: true)
+        try await stack.initializeStack()
+        return stack
     }
 
     // Used for Canvas Preview
-    static var preview: CoreDataStack = {
+    static func preview() async throws -> CoreDataStack {
         let stack = CoreDataStack(inMemory: true)
-        let context = stack.persistentContainer.viewContext
-
-        let pumpHistory = PumpEventStored.makePreviewEvents(count: 10, provider: stack)
-
+        try await stack.initializeStack()
         return stack
-    }()
+    }
 
     // Shared managed object model
     static var managedObjectModel: NSManagedObjectModel = {
@@ -183,13 +167,67 @@ class CoreDataStack: ObservableObject {
         }
     }
 
-    func initializeStack() throws {
-        // Force initialization of persistent container
-        let container = persistentContainer
+    private func setupPersistentStoreChangeNotifications() {
+        // Observe Core Data remote change notifications on the queue where the changes were made
+        notificationToken = Foundation.NotificationCenter.default.addObserver(
+            forName: .NSPersistentStoreRemoteChange,
+            object: nil,
+            queue: nil
+        ) { _ in
+            Task {
+                await self.fetchPersistentHistory()
+            }
+        }
+
+        debug(.coreData, "Set up persistent store change notifications")
+    }
+
+    private func loadPersistentStores() async throws {
+        try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
+            persistentContainer.loadPersistentStores { storeDescription, error in
+                if let error = error {
+                    warning(.coreData, "Failed to load persistent stores: \(error.localizedDescription)")
+                    continuation.resume(throwing: error)
+                } else {
+                    debug(.coreData, "Successfully loaded persistent store: \(storeDescription.url?.absoluteString ?? "unknown")")
+                    continuation.resume(returning: ())
+                }
+            }
+        }
+    }
+
+    func initializeStack(retryCount: Int = 0) async throws {
+        do {
+            // Load stores asynchronously
+            try await loadPersistentStores()
+
+            // Verify the store is loaded
+            guard persistentContainer.persistentStoreCoordinator.persistentStores.isEmpty == false else {
+                let error = CoreDataError.storeNotInitializedError(function: #function, file: #file)
+                throw error
+            }
+
+            setupPersistentStoreChangeNotifications()
 
-        // Verify the store is loaded
-        guard container.persistentStoreCoordinator.persistentStores.isEmpty == false else {
-            throw CoreDataError.storeNotInitializedError(function: #function, file: #file)
+            debug(.coreData, "Core Data stack initialized successfully")
+
+        } catch {
+            debug(.coreData, "Failed to initialize Core Data stack: \(error.localizedDescription)")
+
+            // If we still have retries left, try again after a delay
+            if retryCount < maxRetries {
+                debug(.coreData, "Retrying initialization (\(retryCount + 1)/\(maxRetries))")
+
+                // Wait before retrying
+                try await Task.sleep(for: .seconds(1))
+
+                // Retry the initialization
+                try await initializeStack(retryCount: retryCount + 1)
+            } else {
+                // We've exhausted our retries
+                debug(.coreData, "Core Data initialization failed after \(maxRetries) attempts")
+                throw error
+            }
         }
     }
 }

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -202,14 +202,13 @@
 		38FEF3FC2737E53800574A46 /* MainStateModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FB2737E53800574A46 /* MainStateModel.swift */; };
 		38FEF3FE2738083E00574A46 /* CGMSettingsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */; };
 		38FEF413273B317A00574A46 /* HKUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FEF412273B317A00574A46 /* HKUnit.swift */; };
+		3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */; };
 		45252C95D220E796FDB3B022 /* ConfigEditorDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F8A87AA037BD079BA3528BA /* ConfigEditorDataFlow.swift */; };
 		45717281F743594AA9D87191 /* ConfigEditorRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920DDB21E5D0EB813197500D /* ConfigEditorRootView.swift */; };
 		491D6FBD2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */; };
 		491D6FBE2D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FB92D56741C00C49F67 /* TempTargetRunStored+CoreDataClass.swift */; };
 		491D6FBF2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */; };
 		491D6FC02D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */; };
-		49249B1C2D46E45E000F4866 /* CurrentTDDSetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */; };
-		49249B382D46E76A000F4866 /* TDD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49249B372D46E76A000F4866 /* TDD.swift */; };
 		49B9B57F2D5768D2009C6B59 /* AdjustmentStored+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */; };
 		5075C1608E6249A51495C422 /* TargetsEditorProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */; };
 		53F2382465BF74DB1A967C8B /* PumpConfigProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = A8630D58BDAD6D9C650B9B39 /* PumpConfigProvider.swift */; };
@@ -921,6 +920,7 @@
 		38FEF3FB2737E53800574A46 /* MainStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainStateModel.swift; sourceTree = "<group>"; };
 		38FEF3FD2738083E00574A46 /* CGMSettingsProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGMSettingsProvider.swift; sourceTree = "<group>"; };
 		38FEF412273B317A00574A46 /* HKUnit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HKUnit.swift; sourceTree = "<group>"; };
+		3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainLoadingView.swift; sourceTree = "<group>"; };
 		3BDEA2DC60EDE0A3CA54DC73 /* TargetsEditorProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorProvider.swift; sourceTree = "<group>"; };
 		3BF768BD6264FF7D71D66767 /* NightscoutConfigProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NightscoutConfigProvider.swift; sourceTree = "<group>"; };
 		3F60E97100041040446F44E7 /* PumpConfigStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PumpConfigStateModel.swift; sourceTree = "<group>"; };
@@ -931,8 +931,6 @@
 		491D6FBA2D56741C00C49F67 /* TempTargetRunStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetRunStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		491D6FBB2D56741C00C49F67 /* TempTargetStored+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataClass.swift"; sourceTree = "<group>"; };
 		491D6FBC2D56741C00C49F67 /* TempTargetStored+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TempTargetStored+CoreDataProperties.swift"; sourceTree = "<group>"; };
-		49249B1B2D46E45E000F4866 /* CurrentTDDSetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentTDDSetup.swift; sourceTree = "<group>"; };
-		49249B372D46E76A000F4866 /* TDD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TDD.swift; sourceTree = "<group>"; };
 		49B9B57E2D5768D2009C6B59 /* AdjustmentStored+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentStored+Helper.swift"; sourceTree = "<group>"; };
 		4DD795BA46B193644D48138C /* TargetsEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TargetsEditorRootView.swift; sourceTree = "<group>"; };
 		505E09DC17A0C3D0AF4B66FE /* ISFEditorStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ISFEditorStateModel.swift; sourceTree = "<group>"; };
@@ -1739,6 +1737,7 @@
 		3811DE1F25C9D48300A708ED /* View */ = {
 			isa = PBXGroup;
 			children = (
+				3BAD36B12D7CDC1400CC298D /* MainLoadingView.swift */,
 				3811DE2025C9D48300A708ED /* MainRootView.swift */,
 			);
 			path = View;
@@ -3746,6 +3745,7 @@
 				3811DEB125C9D88300A708ED /* Keychain.swift in Sources */,
 				DD17453E2C55BFB600211FAC /* AlgorithmAdvancedSettingsStateModel.swift in Sources */,
 				CE95BF572BA5F5FE00DC3DE3 /* PluginManager.swift in Sources */,
+				3BAD36B22D7CDC1A00CC298D /* MainLoadingView.swift in Sources */,
 				382C133725F13A1E00715CE1 /* InsulinSensitivities.swift in Sources */,
 				19D466A529AA2BD4004D5F33 /* MealSettingsProvider.swift in Sources */,
 				DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */,

+ 3 - 3
Trio/Sources/Application/AppDelegate.swift

@@ -4,11 +4,11 @@ import UserNotifications
 
 class AppDelegate: NSObject, UIApplicationDelegate, ObservableObject, UNUserNotificationCenterDelegate {
     func application(
-        _ application: UIApplication,
+        _: UIApplication,
         didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]?
     ) -> Bool {
-        application.registerForRemoteNotifications()
-        return true
+        // application.registerForRemoteNotifications()
+        true
     }
 
     func application(

+ 73 - 24
Trio/Sources/Application/TrioApp.swift

@@ -5,6 +5,11 @@ import Foundation
 import SwiftUI
 import Swinject
 
+extension Notification.Name {
+    static let initializationCompleted = Notification.Name("initializationCompleted")
+    static let initializationError = Notification.Name("initializationError")
+}
+
 @main struct TrioApp: App {
     @Environment(\.scenePhase) var scenePhase
 
@@ -13,9 +18,17 @@ import Swinject
     // Read the color scheme preference from UserDefaults; defaults to system default setting
     @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
 
-    let coreDataStack: CoreDataStack
+    let coreDataStack = CoreDataStack.shared
+    class InitState {
+        var complete = false
+        var error = false
+    }
+
+    let initState = InitState()
 
     @State private var appState = AppState()
+    @State private var showLoadingView = true
+    @State private var showLoadingError = false
 
     // Dependencies Assembler
     // contain all dependencies Assemblies
@@ -72,36 +85,75 @@ import Swinject
             "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.default.buildDate()))] [buildExpires: \(String(describing: BuildDetails.default.calculateExpirationDate()))] [submodules: \(submodulesInfo)]"
         )
 
-        // Setup up the Core Data Stack
-        coreDataStack = CoreDataStack.shared
+        // Fix bug in iOS 18 related to the translucent tab bar
+        configureTabBarAppearance()
 
-        // Explicitly initialize Core Data Stack
-        do {
-            try coreDataStack.initializeStack()
+        deferredInitialization()
+    }
 
-            // Only load services after successful Core Data initialization
-            loadServices()
+    private func deferredInitialization() {
+        Task {
+            do {
+                try await coreDataStack.initializeStack()
 
-            // Fix bug in iOS 18 related to the translucent tab bar
-            configureTabBarAppearance()
+                await MainActor.run {
+                    // Only load services after successful Core Data initialization
+                    loadServices()
 
-            // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
-            cleanupOldData()
-        } catch {
-            debug(.coreData, "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)")
+                    // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
+                    cleanupOldData()
 
-            fatalError("Core Data Stack initialization failed")
+                    self.initState.complete = true
+                    Foundation.NotificationCenter.default.post(name: .initializationCompleted, object: nil)
+                    UIApplication.shared.registerForRemoteNotifications()
+                }
+            } catch {
+                debug(
+                    .coreData,
+                    "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)"
+                )
+
+                await MainActor.run {
+                    self.initState.error = true
+                    Foundation.NotificationCenter.default.post(name: .initializationError, object: nil)
+                }
+            }
         }
     }
 
+    private func retryCoreDataInitialization() {
+        showLoadingError = false
+        initState.error = false
+        deferredInitialization()
+    }
+
     var body: some Scene {
         WindowGroup {
-            Main.RootView(resolver: resolver)
-                .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
-                .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
-                .environment(appState)
-                .environmentObject(Icons())
-                .onOpenURL(perform: handleURL)
+            if self.showLoadingView {
+                Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
+                    .onAppear {
+                        if self.initState.complete {
+                            self.showLoadingView = false
+                        }
+                        if self.initState.error {
+                            self.showLoadingError = true
+                        }
+                    }
+                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
+                        self.showLoadingView = false
+                    }
+                    .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
+                        self.showLoadingError = true
+                    }
+
+            } else {
+                Main.RootView(resolver: resolver)
+                    .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
+                    .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
+                    .environment(appState)
+                    .environmentObject(Icons())
+                    .onOpenURL(perform: handleURL)
+            }
         }
         .onChange(of: scenePhase) { _, newScenePhase in
             debug(.default, "APPLICATION PHASE: \(newScenePhase)")
@@ -117,9 +169,6 @@ import Swinject
                 {
                     AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
                 }
-
-                // Check if we need to perform a database cleaning
-                performCleanupIfNecessary()
             }
         }
     }

+ 6 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -88601,6 +88601,9 @@
         }
       }
     },
+    "Getting everything ready for you..." : {
+
+    },
     "Give Apple Health Write Permissions" : {
       "localizations" : {
         "bg" : {
@@ -157170,6 +157173,9 @@
         }
       }
     },
+    "Something went wrong while loading your data. Please try again in a few moments." : {
+
+    },
     "Source of the glucose reading will be added to the notification." : {
       "localizations" : {
         "bg" : {

+ 41 - 16
Trio/Sources/Modules/Home/View/Chart/ChartElements/GlucoseChartView.swift

@@ -78,23 +78,48 @@ struct GlucoseChartView: ChartContent {
 }
 
 #Preview {
-    let previewStack = CoreDataStack.preview
-    return NavigationView {
-        VStack {
-            Chart {
-                GlucoseChartView(
-                    glucoseData: GlucoseStored.makePreviewGlucose(count: 24, provider: previewStack),
-                    units: .mgdL,
-                    highGlucose: 180,
-                    lowGlucose: 70,
-                    currentGlucoseTarget: 100,
-                    isSmoothingEnabled: false,
-                    glucoseColorScheme: .dynamicColor
-                )
+    struct PreviewWrapper: View {
+        @State private var previewStack: CoreDataStack? = nil
+        @State private var glucoseData: [GlucoseStored] = []
+        @State private var isLoading = true
+
+        var body: some View {
+            NavigationView {
+                Group {
+                    if isLoading {
+                        ProgressView("Loading data...")
+                    } else {
+                        VStack {
+                            Chart {
+                                GlucoseChartView(
+                                    glucoseData: glucoseData,
+                                    units: .mgdL,
+                                    highGlucose: 180,
+                                    lowGlucose: 70,
+                                    currentGlucoseTarget: 100,
+                                    isSmoothingEnabled: false,
+                                    glucoseColorScheme: .dynamicColor
+                                )
+                            }
+                            .frame(height: 200)
+                            .padding()
+                        }
+                    }
+                }
+                .navigationTitle("Glucose Chart")
+                .task {
+                    // Use the preview stack that's initialized asynchronously in CoreDataStack
+                    previewStack = try? await CoreDataStack.preview()
+
+                    // Now you can safely create preview data
+                    if let stack = previewStack {
+                        glucoseData = GlucoseStored.makePreviewGlucose(count: 24, provider: stack)
+                        isLoading = false
+                    }
+                }
             }
-            .frame(height: 200)
-            .padding()
         }
-        .navigationTitle("Glucose Chart")
     }
+
+    return PreviewWrapper()
 }

+ 72 - 0
Trio/Sources/Modules/Main/View/MainLoadingView.swift

@@ -0,0 +1,72 @@
+import SwiftUI
+
+extension Main {
+    struct LoadingView: View {
+        @Binding var showError: Bool
+        let retry: () -> Void
+        var body: some View {
+            VStack {
+                Spacer().frame(maxHeight: 92)
+                Image(.trioWhite)
+                    .resizable()
+                    .scaledToFit()
+                    .frame(width: 92, height: 92)
+                Spacer().frame(maxHeight: 32)
+                ZStack {
+                    // Invisible placeholder with same height as progress view
+                    Color.clear.frame(width: 30, height: 30)
+                    ProgressView()
+                        .scaleEffect(1.5)
+                        .opacity(showError ? 0 : 1)
+                }
+                Spacer().frame(maxHeight: 32)
+                if showError {
+                    Text("Something went wrong while loading your data. Please try again in a few moments.")
+                    Spacer().frame(maxHeight: 32)
+                    RetryButton(action: retry)
+                } else {
+                    Text("Getting everything ready for you...")
+                }
+                Spacer()
+            }
+            .padding()
+        }
+    }
+
+    struct RetryButton: View {
+        var action: () -> Void
+        var label: String = "Retry"
+        var iconName: String = "arrow.clockwise"
+
+        var body: some View {
+            Button(action: action) {
+                HStack(spacing: 8) {
+                    Image(systemName: iconName)
+                        .font(.system(size: 16, weight: .medium))
+                    Text(label)
+                        .font(.system(size: 16, weight: .semibold))
+                }
+                .padding(.horizontal, 20)
+                .padding(.vertical, 12)
+                .background(
+                    Capsule()
+                        .fill(Color.blue)
+                )
+                .foregroundColor(.white)
+                .shadow(color: Color.black.opacity(0.1), radius: 5, x: 0, y: 2)
+            }
+            // .buttonStyle(ScaleButtonStyle())
+        }
+    }
+}
+
+struct LoadingView_Previews: PreviewProvider {
+    static var previews: some View {
+        Group {
+            Main.LoadingView(showError: .constant(false), retry: {})
+                .previewDisplayName("Loading")
+            Main.LoadingView(showError: .constant(true), retry: {})
+                .previewDisplayName("Error")
+        }
+    }
+}

+ 8 - 7
TrioTests/CoreDataTests/CarbsStorageTests.swift

@@ -8,11 +8,12 @@ import Testing
 @Suite("CarbsStorage Tests", .serialized) struct CarbsStorageTests: Injectable {
     @Injected() var storage: CarbsStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
 
-    init() {
+    init() async throws {
         // Create test context
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly
@@ -55,7 +56,7 @@ import Testing
 
         // When
         try await storage.storeCarbs(testEntries, areFetchedFromRemote: false)
-        let recentEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let recentEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "TRUEPREDICATE"),
@@ -95,7 +96,7 @@ import Testing
         try await storage.storeCarbs([testEntry], areFetchedFromRemote: false)
 
         // Get the stored entry's ObjectID
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "carbs == 30"),
@@ -111,7 +112,7 @@ import Testing
         await storage.deleteCarbsEntryStored(objectID)
 
         // Then - verify deletion
-        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "carbs == 30"),
@@ -166,7 +167,7 @@ import Testing
         try await storage.storeCarbs([testEntry], areFetchedFromRemote: false)
 
         // First verify all stored entries
-        let allStoredEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let allStoredEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: CarbEntryStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "fpuID == %@", fpuID),

+ 4 - 3
TrioTests/CoreDataTests/DeterminationStorageTests.swift

@@ -8,12 +8,13 @@ import Testing
 @Suite("Determination Storage Tests", .serialized) struct DeterminationStorageTests: Injectable {
     @Injected() var storage: DeterminationStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
 
-    init() {
+    init() async throws {
         // Create test context
         // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly

+ 10 - 9
TrioTests/CoreDataTests/GlucoseStorageTests.swift

@@ -8,12 +8,13 @@ import Testing
 @Suite("GlucoseStorage Tests", .serialized) struct GlucoseStorageTests: Injectable {
     @Injected() var storage: GlucoseStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
 
-    init() {
+    init() async throws {
         // Create test context
         // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly
@@ -49,7 +50,7 @@ import Testing
         try await storage.storeGlucose(testGlucose)
 
         // Then verify stored entries
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 126"),
@@ -71,7 +72,7 @@ import Testing
         try await storage.storeGlucose(testGlucose)
 
         // Get the stored entry's ObjectID
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 140"),
@@ -89,7 +90,7 @@ import Testing
         await storage.deleteGlucose(objectID)
 
         // Then verify deletion
-        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 140"),
@@ -144,7 +145,7 @@ import Testing
 
         // When - Test low glucose
         try await storage.storeGlucose(lowGlucose)
-        var storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        var storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 55"),
@@ -158,7 +159,7 @@ import Testing
 
         // When - Test high glucose
         try await storage.storeGlucose(highGlucose)
-        storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 271"),
@@ -172,7 +173,7 @@ import Testing
 
         // When - Test normal glucose
         try await storage.storeGlucose(normalGlucose)
-        storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: GlucoseStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "glucose == 100"),

+ 7 - 6
TrioTests/CoreDataTests/OverrideStorageTests.swift

@@ -8,12 +8,13 @@ import Testing
 @Suite("Override Storage Tests", .serialized) struct OverrideStorageTests: Injectable {
     @Injected() var storage: OverrideStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
 
-    init() {
+    init() async throws {
         // Create test context
         // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly
@@ -68,7 +69,7 @@ import Testing
         try await storage.storeOverride(override: testOverride)
 
         // Then verify stored entries
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Test Override"),
@@ -158,7 +159,7 @@ import Testing
         try await storage.storeOverride(override: testPreset)
 
         // Get the stored preset's ObjectID
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Delete Test"),
@@ -174,7 +175,7 @@ import Testing
         await storage.deleteOverridePreset(objectID)
 
         // Then verify deletion
-        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: OverrideStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Delete Test"),

+ 4 - 3
TrioTests/CoreDataTests/PumpHistoryStorageTests.swift

@@ -9,12 +9,13 @@ import Testing
 @Suite("PumpHistoryStorage Tests", .serialized) struct PumpHistoryStorageTests: Injectable {
     @Injected() var storage: PumpHistoryStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
     typealias PumpEvent = PumpEventStored.EventType
 
-    init() {
+    init() async throws {
         // Create test context
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly

+ 7 - 6
TrioTests/CoreDataTests/TempTargetStorageTests.swift

@@ -8,11 +8,12 @@ import Testing
 @Suite("TempTargetStorage Tests", .serialized) struct TempTargetsStorageTests: Injectable {
     @Injected() var storage: TempTargetsStorage!
     let resolver: Resolver
-    let coreDataStack = CoreDataStack.createForTests()
-    let testContext: NSManagedObjectContext
+    var coreDataStack: CoreDataStack!
+    var testContext: NSManagedObjectContext!
 
-    init() {
+    init() async throws {
         // Create test context
+        coreDataStack = try await CoreDataStack.createForTests()
         testContext = coreDataStack.newTaskContext()
 
         // Create assembler with test assembly
@@ -58,7 +59,7 @@ import Testing
         try await storage.storeTempTarget(tempTarget: testTarget)
 
         // Then verify stored entries
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Test Target"),
@@ -91,7 +92,7 @@ import Testing
         try await storage.storeTempTarget(tempTarget: testTarget)
 
         // Get the stored target's ObjectID
-        let storedEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let storedEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Delete Test"),
@@ -107,7 +108,7 @@ import Testing
         await storage.deleteTempTargetPreset(objectID)
 
         // Then verify deletion
-        let remainingEntries = try await CoreDataStack.shared.fetchEntitiesAsync(
+        let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: testContext,
             predicate: NSPredicate(format: "name == %@", "Delete Test"),