From 72078f0df9668111ebf5f9a6c3720449f3760e4c Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Fri, 11 Oct 2024 08:05:51 -0500 Subject: [PATCH] LOOP-5088 Update Loop for LoopKit api changes for avoiding thread blocking (#712) * Update Loop for LoopKit api changes for avoiding thread blocking * Fix non-deterministic test behavior * Updates to use latest LoopAlgorithm package --- .../StatusWidgetTimelineProvider.swift | 39 ++++++------ .../GlucoseStore+SimulatedCoreData.swift | 4 +- Loop/Managers/CGMStalenessMonitor.swift | 44 ++++++------- .../CriticalEventLogExportManager.swift | 18 +----- Loop/Managers/DeviceDataManager.swift | 59 ++++++++++-------- Loop/Managers/LoopAppManager.swift | 35 +++++++---- Loop/Managers/LoopDataManager.swift | 3 +- Loop/Managers/RemoteDataServicesManager.swift | 61 +++++++++---------- Loop/Managers/WatchDataManager.swift | 14 +++-- Loop/Models/StoredDataAlgorithmInput.swift | 2 + .../Managers/CGMStalenessMonitorTests.swift | 36 ++++++----- .../Managers/DeviceDataManagerTests.swift | 7 +-- .../Managers/MealDetectionManagerTests.swift | 3 +- .../ViewModels/BolusEntryViewModelTests.swift | 3 +- .../Managers/LoopDataManager.swift | 33 +++++----- 15 files changed, 185 insertions(+), 176 deletions(-) diff --git a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift index 314ea4542b..b48bb1f7bf 100644 --- a/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift +++ b/Loop Widget Extension/Timeline/StatusWidgetTimelineProvider.swift @@ -30,10 +30,16 @@ class StatusWidgetTimelineProvider: TimelineProvider { store: cacheStore, expireAfter: localCacheDuration) - lazy var glucoseStore = GlucoseStore( - cacheStore: cacheStore, - provenanceIdentifier: HKSource.default().bundleIdentifier - ) + var glucoseStore: GlucoseStore! + + init() { + Task { + glucoseStore = await GlucoseStore( + cacheStore: cacheStore, + provenanceIdentifier: HKSource.default().bundleIdentifier + ) + } + } func placeholder(in context: Context) -> StatusWidgetTimelimeEntry { log.default("%{public}@: context=%{public}@", #function, String(describing: context)) @@ -90,29 +96,22 @@ class StatusWidgetTimelineProvider: TimelineProvider { } func update(completion: @escaping (StatusWidgetTimelimeEntry) -> Void) { - let group = DispatchGroup() - - var glucose: [StoredGlucoseSample] = [] let startDate = Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval) - group.enter() - glucoseStore.getGlucoseSamples(start: startDate) { (result) in - switch result { - case .failure: + Task { + + var glucose: [StoredGlucoseSample] = [] + + do { + glucose = try await glucoseStore.getGlucoseSamples(start: startDate) + self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: glucose.last?.startDate), String(describing: glucose.last?.quantity)) + } catch { self.log.error("Failed to fetch glucose after %{public}@", String(describing: startDate)) - glucose = [] - case .success(let samples): - self.log.default("Fetched glucose: last = %{public}@, %{public}@", String(describing: samples.last?.startDate), String(describing: samples.last?.quantity)) - glucose = samples } - group.leave() - } - group.wait() - let finalGlucose = glucose + let finalGlucose = glucose - Task { @MainActor in guard let defaults = self.defaults, let context = defaults.statusExtensionContext, let contextUpdatedAt = context.createdAt, diff --git a/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift b/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift index e5cc830a70..e30a548a4a 100644 --- a/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift +++ b/Loop/Extensions/GlucoseStore+SimulatedCoreData.swift @@ -82,8 +82,8 @@ extension GlucoseStore { return addError } - func purgeHistoricalGlucoseObjects(completion: @escaping (Error?) -> Void) { - purgeCachedGlucoseObjects(before: historicalEndDate, completion: completion) + func purgeHistoricalGlucoseObjects() async throws { + try await purgeCachedGlucoseObjects(before: historicalEndDate) } } diff --git a/Loop/Managers/CGMStalenessMonitor.swift b/Loop/Managers/CGMStalenessMonitor.swift index 60fe0d06b2..25c0365e1e 100644 --- a/Loop/Managers/CGMStalenessMonitor.swift +++ b/Loop/Managers/CGMStalenessMonitor.swift @@ -12,7 +12,7 @@ import LoopCore import LoopAlgorithm protocol CGMStalenessMonitorDelegate: AnyObject { - func getLatestCGMGlucose(since: Date, completion: @escaping (_ result: Swift.Result) -> Void) + func getLatestCGMGlucose(since: Date) async throws -> StoredGlucoseSample? } class CGMStalenessMonitor { @@ -21,13 +21,7 @@ class CGMStalenessMonitor { private var cgmStalenessTimer: Timer? - weak var delegate: CGMStalenessMonitorDelegate? = nil { - didSet { - if delegate != nil { - checkCGMStaleness() - } - } - } + weak var delegate: CGMStalenessMonitorDelegate? @Published var cgmDataIsStale: Bool = true { didSet { @@ -57,29 +51,27 @@ class CGMStalenessMonitor { cgmStalenessTimer?.invalidate() cgmStalenessTimer = Timer.scheduledTimer(withTimeInterval: expiration.timeIntervalSinceNow, repeats: false) { [weak self] _ in self?.log.debug("cgmStalenessTimer fired") - self?.checkCGMStaleness() + Task { + await self?.checkCGMStaleness() + } } cgmStalenessTimer?.tolerance = CGMStalenessMonitor.cgmStalenessTimerTolerance } - private func checkCGMStaleness() { - delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) { (result) in - DispatchQueue.main.async { - self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: result)) - switch result { - case .success(let sample): - if let sample = sample { - self.cgmDataIsStale = false - self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance)) - } else { - self.cgmDataIsStale = true - } - case .failure(let error): - self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error)) - // Some kind of system error; check again in 5 minutes - self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5))) - } + func checkCGMStaleness() async { + do { + let sample = try await delegate?.getLatestCGMGlucose(since: Date(timeIntervalSinceNow: -LoopAlgorithm.inputDataRecencyInterval)) + self.log.debug("Fetched latest CGM Glucose for checkCGMStaleness: %{public}@", String(describing: sample)) + if let sample = sample { + self.cgmDataIsStale = false + self.updateCGMStalenessTimer(expiration: sample.startDate.addingTimeInterval(LoopAlgorithm.inputDataRecencyInterval + CGMStalenessMonitor.cgmStalenessTimerTolerance)) + } else { + self.cgmDataIsStale = true } + } catch { + self.log.error("Unable to get latest CGM clucose: %{public}@ ", String(describing: error)) + // Some kind of system error; check again in 5 minutes + self.updateCGMStalenessTimer(expiration: Date(timeIntervalSinceNow: .minutes(5))) } } } diff --git a/Loop/Managers/CriticalEventLogExportManager.swift b/Loop/Managers/CriticalEventLogExportManager.swift index 546c7986fe..50489ff1a7 100644 --- a/Loop/Managers/CriticalEventLogExportManager.swift +++ b/Loop/Managers/CriticalEventLogExportManager.swift @@ -199,16 +199,6 @@ public class CriticalEventLogExportManager { calendar.timeZone = TimeZone(identifier: "UTC")! return calendar }() - - // MARK: - Background Tasks - - func registerBackgroundTasks() { - if Self.registerCriticalEventLogHistoricalExportBackgroundTask({ self.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) { - log.debug("Critical event log export background task registered") - } else { - log.error("Critical event log export background task not registered") - } - } } // MARK: - CriticalEventLogBaseExporter @@ -567,11 +557,7 @@ fileprivate extension FileManager { // MARK: - Critical Event Log Export extension CriticalEventLogExportManager { - private static var criticalEventLogHistoricalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" } - - public static func registerCriticalEventLogHistoricalExportBackgroundTask(_ handler: @escaping (BGProcessingTask) -> Void) -> Bool { - return BGTaskScheduler.shared.register(forTaskWithIdentifier: criticalEventLogHistoricalExportBackgroundTaskIdentifier, using: nil) { handler($0 as! BGProcessingTask) } - } + static var historicalExportBackgroundTaskIdentifier: String { "com.loopkit.background-task.critical-event-log.historical-export" } public func handleCriticalEventLogHistoricalExportBackgroundTask(_ task: BGProcessingTask) { dispatchPrecondition(condition: .notOnQueue(.main)) @@ -602,7 +588,7 @@ extension CriticalEventLogExportManager { public func scheduleCriticalEventLogHistoricalExportBackgroundTask(isRetry: Bool = false) { do { let earliestBeginDate = isRetry ? retryExportHistoricalDate() : nextExportHistoricalDate() - let request = BGProcessingTaskRequest(identifier: Self.criticalEventLogHistoricalExportBackgroundTaskIdentifier) + let request = BGProcessingTaskRequest(identifier: Self.historicalExportBackgroundTaskIdentifier) request.earliestBeginDate = earliestBeginDate request.requiresExternalPower = true diff --git a/Loop/Managers/DeviceDataManager.swift b/Loop/Managers/DeviceDataManager.swift index 4719b5a677..99403a37a4 100644 --- a/Loop/Managers/DeviceDataManager.swift +++ b/Loop/Managers/DeviceDataManager.swift @@ -288,7 +288,11 @@ final class DeviceDataManager { glucoseStore.delegate = self cgmEventStore.delegate = self doseStore.insulinDeliveryStore.delegate = self - + + Task { + await cgmStalenessMonitor.checkCGMStaleness() + } + setupPump() setupCGM() @@ -1179,28 +1183,25 @@ extension DeviceDataManager { return } - let devicePredicate = HKQuery.predicateForObjects(from: [testingPumpManager.testingDevice]) let insulinDeliveryStore = doseStore.insulinDeliveryStore Task { do { try await doseStore.resetPumpData() - } catch { - completion?(error) - return - } - let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied - guard !insulinSharingDenied else { - // only clear cache since access to health kit is denied - insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() { error in - completion?(error) + let insulinSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.insulinQuantityType) == .sharingDenied + guard !insulinSharingDenied else { + // only clear cache since access to health kit is denied + await insulinDeliveryStore.purgeCachedInsulinDeliveryObjects() + completion?(nil) + return } - return - } - - insulinDeliveryStore.purgeAllDoseEntries(healthKitPredicate: devicePredicate) { error in + + try await insulinDeliveryStore.purgeDoseEntriesForDevice(testingPumpManager.testingDevice) + completion?(nil) + } catch { completion?(error) + return } } } @@ -1210,19 +1211,25 @@ extension DeviceDataManager { completion?(nil) return } - - let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied - guard !glucoseSharingDenied else { - // only clear cache since access to health kit is denied - glucoseStore.purgeCachedGlucoseObjects() { error in - completion?(error) + + Task { + let glucoseSharingDenied = self.healthStore.authorizationStatus(for: HealthKitSampleStore.glucoseType) == .sharingDenied + guard !glucoseSharingDenied else { + // only clear cache since access to health kit is denied + do { + try await glucoseStore.purgeCachedGlucoseObjects() + } catch { + completion?(error) + } + return } - return - } - let predicate = HKQuery.predicateForObjects(from: [testingCGMManager.testingDevice]) - glucoseStore.purgeAllGlucoseSamples(healthKitPredicate: predicate) { error in - completion?(error) + do { + try await glucoseStore.purgeAllGlucose(for: testingCGMManager.testingDevice) + completion?(nil) + } catch { + completion?(error) + } } } } diff --git a/Loop/Managers/LoopAppManager.swift b/Loop/Managers/LoopAppManager.swift index 0c3e9e24a6..23d064f509 100644 --- a/Loop/Managers/LoopAppManager.swift +++ b/Loop/Managers/LoopAppManager.swift @@ -8,6 +8,7 @@ import UIKit import Intents +import BackgroundTasks import Combine import LoopKit import LoopKitUI @@ -133,9 +134,27 @@ class LoopAppManager: NSObject { self.state = state.next } + func registerBackgroundTasks() { + let taskIdentifier = CriticalEventLogExportManager.historicalExportBackgroundTaskIdentifier + let registered = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in + guard let criticalEventLogExportManager = self.criticalEventLogExportManager else { + self.log.error("Critical event log export launch handler called before initialization complete!") + return + } + criticalEventLogExportManager.handleCriticalEventLogHistoricalExportBackgroundTask(task as! BGProcessingTask) + } + if registered { + log.debug("Critical event log export background task registered") + } else { + log.error("Critical event log export background task not registered") + } + } + func launch() { precondition(isLaunchPending) + registerBackgroundTasks() + Task { await resumeLaunch() } @@ -248,7 +267,7 @@ class LoopAppManager: NSObject { observationStart: Date().addingTimeInterval(-CarbMath.maximumAbsorptionTimeInterval) ) - self.doseStore = DoseStore( + self.doseStore = await DoseStore( healthKitSampleStore: insulinHealthStore, cacheStore: cacheStore, cacheLength: localCacheDuration, @@ -263,7 +282,7 @@ class LoopAppManager: NSObject { observationStart: Date().addingTimeInterval(-.hours(24)) ) - self.glucoseStore = GlucoseStore( + self.glucoseStore = await GlucoseStore( healthKitSampleStore: glucoseHealthStore, cacheStore: cacheStore, cacheLength: localCacheDuration, @@ -390,9 +409,6 @@ class LoopAppManager: NSObject { directory: FileManager.default.exportsDirectoryURL, historicalDuration: localCacheDuration) - criticalEventLogExportManager.registerBackgroundTasks() - - statusExtensionManager = ExtensionDataManager( deviceDataManager: deviceDataManager, loopDataManager: loopDataManager, @@ -1045,6 +1061,7 @@ extension LoopAppManager: SimulatedData { Task { @MainActor in do { try await self.doseStore.purgeHistoricalPumpEvents() + try await self.glucoseStore.purgeHistoricalGlucoseObjects() } catch { completion(error) return @@ -1059,13 +1076,7 @@ extension LoopAppManager: SimulatedData { completion(error) return } - self.glucoseStore.purgeHistoricalGlucoseObjects() { error in - guard error == nil else { - completion(error) - return - } - self.settingsManager.purgeHistoricalSettingsObjects(completion: completion) - } + self.settingsManager.purgeHistoricalSettingsObjects(completion: completion) } } } diff --git a/Loop/Managers/LoopDataManager.swift b/Loop/Managers/LoopDataManager.swift index 519c92518b..e11363a446 100644 --- a/Loop/Managers/LoopDataManager.swift +++ b/Loop/Managers/LoopDataManager.swift @@ -435,7 +435,8 @@ final class LoopDataManager: ObservableObject { carbAbsorptionModel: carbAbsorptionModel, recommendationInsulinModel: insulinModel(for: deliveryDelegate?.pumpInsulinType ?? .novolog), recommendationType: .manualBolus, - automaticBolusApplicationFactor: effectiveBolusApplicationFactor) + automaticBolusApplicationFactor: effectiveBolusApplicationFactor, + useMidAbsorptionISF: false) } func loopingReEnabled() async { diff --git a/Loop/Managers/RemoteDataServicesManager.swift b/Loop/Managers/RemoteDataServicesManager.swift index a14710b69c..153dd008a7 100644 --- a/Loop/Managers/RemoteDataServicesManager.swift +++ b/Loop/Managers/RemoteDataServicesManager.swift @@ -431,24 +431,22 @@ extension RemoteDataServicesManager { let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose) ?? GlucoseStore.QueryAnchor() var continueUpload = false - self.glucoseStore.executeGlucoseQuery(fromQueryAnchor: previousQueryAnchor, limit: remoteDataService.glucoseDataLimit ?? Int.max) { result in - switch result { - case .failure(let error): + Task { + do { + let (queryAnchor, data) = try await self.glucoseStore.executeGlucoseQuery(fromQueryAnchor: previousQueryAnchor, limit: remoteDataService.glucoseDataLimit ?? Int.max) + do { + try await remoteDataService.uploadGlucoseData(data) + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + await self.uploadSucceeded(key) + } catch { + self.log.error("Error synchronizing glucose data: %{public}@", String(describing: error)) + await self.uploadFailed(key) + } + semaphore.signal() + } catch { self.log.error("Error querying glucose data: %{public}@", String(describing: error)) semaphore.signal() - case .success(let queryAnchor, let data): - Task { - do { - try await remoteDataService.uploadGlucoseData(data) - UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .glucose, queryAnchor) - continueUpload = queryAnchor != previousQueryAnchor - self.uploadSucceeded(key) - } catch { - self.log.error("Error synchronizing glucose data: %{public}@", String(describing: error)) - self.uploadFailed(key) - } - semaphore.signal() - } } } @@ -472,25 +470,22 @@ extension RemoteDataServicesManager { let semaphore = DispatchSemaphore(value: 0) let previousQueryAnchor = UserDefaults.appGroup?.getQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent) ?? DoseStore.QueryAnchor() var continueUpload = false - - self.doseStore.executePumpEventQuery(fromQueryAnchor: previousQueryAnchor, limit: remoteDataService.pumpEventDataLimit ?? Int.max) { result in - switch result { - case .failure(let error): + Task { + do { + let (queryAnchor, data) = try await self.doseStore.executePumpEventQuery(fromQueryAnchor: previousQueryAnchor, limit: remoteDataService.pumpEventDataLimit ?? Int.max) + do { + try await remoteDataService.uploadPumpEventData(data) + UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent, queryAnchor) + continueUpload = queryAnchor != previousQueryAnchor + self.uploadSucceeded(key) + } catch { + self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) + self.uploadFailed(key) + } + semaphore.signal() + } catch { self.log.error("Error querying pump event data: %{public}@", String(describing: error)) semaphore.signal() - case .success(let queryAnchor, let data): - Task { - do { - try await remoteDataService.uploadPumpEventData(data) - UserDefaults.appGroup?.setQueryAnchor(for: remoteDataService, withRemoteDataType: .pumpEvent, queryAnchor) - continueUpload = queryAnchor != previousQueryAnchor - self.uploadSucceeded(key) - } catch { - self.log.error("Error synchronizing pump event data: %{public}@", String(describing: error)) - self.uploadFailed(key) - } - semaphore.signal() - } } } diff --git a/Loop/Managers/WatchDataManager.swift b/Loop/Managers/WatchDataManager.swift index dc0997b791..c73af7aeea 100644 --- a/Loop/Managers/WatchDataManager.swift +++ b/Loop/Managers/WatchDataManager.swift @@ -205,12 +205,14 @@ final class WatchDataManager: NSObject { return } + log.default("*** sendWatchContextIfNeeded") + guard case .activated = session.activationState else { session.activate() return } - Task { @MainActor in + Task { let context = await createWatchContext() self.sendWatchContext(context) } @@ -464,13 +466,13 @@ extension WatchDataManager: WCSessionDelegate { } case GlucoseBackfillRequestUserInfo.name?: if let userInfo = GlucoseBackfillRequestUserInfo(rawValue: message) { - glucoseStore.getSyncGlucoseSamples(start: userInfo.startDate.addingTimeInterval(1)) { (result) in - switch result { - case .failure(let error): + Task { + do { + let samples = try await glucoseStore.getSyncGlucoseSamples(start: userInfo.startDate.addingTimeInterval(1)) + replyHandler(WatchHistoricalGlucose(samples: samples).rawValue) + } catch { self.log.error("Failure getting sync glucose objects: %{public}@", String(describing: error)) replyHandler([:]) - case .success(let samples): - replyHandler(WatchHistoricalGlucose(samples: samples).rawValue) } } } else { diff --git a/Loop/Models/StoredDataAlgorithmInput.swift b/Loop/Models/StoredDataAlgorithmInput.swift index 321614a99c..84151fb995 100644 --- a/Loop/Models/StoredDataAlgorithmInput.swift +++ b/Loop/Models/StoredDataAlgorithmInput.swift @@ -51,4 +51,6 @@ struct StoredDataAlgorithmInput: AlgorithmInput { var recommendationType: DoseRecommendationType var automaticBolusApplicationFactor: Double? + + var useMidAbsorptionISF: Bool } diff --git a/LoopTests/Managers/CGMStalenessMonitorTests.swift b/LoopTests/Managers/CGMStalenessMonitorTests.swift index 89afce784b..9da44f7f00 100644 --- a/LoopTests/Managers/CGMStalenessMonitorTests.swift +++ b/LoopTests/Managers/CGMStalenessMonitorTests.swift @@ -30,7 +30,7 @@ class CGMStalenessMonitorTests: XCTestCase { XCTAssert(monitor.cgmDataIsStale) } - func testStalenessWithRecentCMGSample() { + func testStalenessWithRecentCMGSample() async throws { let monitor = CGMStalenessMonitor() fetchExpectation = expectation(description: "Fetch latest cgm glucose") latestCGMGlucose = storedGlucoseSample @@ -46,13 +46,16 @@ class CGMStalenessMonitorTests: XCTestCase { } monitor.delegate = self - waitForExpectations(timeout: 2) - + + await monitor.checkCGMStaleness() + + await fulfillment(of: [fetchExpectation!, exp], timeout: 2) + XCTAssertNotNil(cancelable) XCTAssertEqual(receivedValues, [true, false]) } - func testStalenessWithNoRecentCGMData() { + func testStalenessWithNoRecentCGMData() async throws { let monitor = CGMStalenessMonitor() fetchExpectation = expectation(description: "Fetch latest cgm glucose") latestCGMGlucose = nil @@ -68,13 +71,16 @@ class CGMStalenessMonitorTests: XCTestCase { } monitor.delegate = self - waitForExpectations(timeout: 2) - + + await monitor.checkCGMStaleness() + + await fulfillment(of: [fetchExpectation!, exp], timeout: 2) + XCTAssertNotNil(cancelable) XCTAssertEqual(receivedValues, [true, true]) } - func testStalenessNewReadingsArriving() { + func testStalenessNewReadingsArriving() async throws { let monitor = CGMStalenessMonitor() fetchExpectation = expectation(description: "Fetch latest cgm glucose") latestCGMGlucose = nil @@ -90,19 +96,21 @@ class CGMStalenessMonitorTests: XCTestCase { } monitor.delegate = self - + + await monitor.checkCGMStaleness() + monitor.cgmGlucoseSamplesAvailable([newGlucoseSample]) - - waitForExpectations(timeout: 2) - + + await fulfillment(of: [fetchExpectation!, exp], timeout: 2) + XCTAssertNotNil(cancelable) - XCTAssertEqual(receivedValues, [true, false]) + XCTAssertEqual(receivedValues, [true, true, false]) } } extension CGMStalenessMonitorTests: CGMStalenessMonitorDelegate { - func getLatestCGMGlucose(since: Date, completion: @escaping (Result) -> Void) { - completion(.success(latestCGMGlucose)) + public func getLatestCGMGlucose(since: Date) async throws -> StoredGlucoseSample? { fetchExpectation?.fulfill() + return latestCGMGlucose } } diff --git a/LoopTests/Managers/DeviceDataManagerTests.swift b/LoopTests/Managers/DeviceDataManagerTests.swift index 6c5c09cf5c..c72a955cab 100644 --- a/LoopTests/Managers/DeviceDataManagerTests.swift +++ b/LoopTests/Managers/DeviceDataManagerTests.swift @@ -34,7 +34,7 @@ final class DeviceDataManagerTests: XCTestCase { } } - override func setUpWithError() throws { + override func setUp() async throws { let mockUserNotificationCenter = MockUserNotificationCenter() let mockBluetoothProvider = MockBluetoothProvider() let alertPresenter = MockPresenter() @@ -56,7 +56,7 @@ final class DeviceDataManagerTests: XCTestCase { cacheLength: .days(1) ) - let doseStore = DoseStore( + let doseStore = await DoseStore( cacheStore: persistenceController ) @@ -72,8 +72,7 @@ final class DeviceDataManagerTests: XCTestCase { } let deviceLog = PersistentDeviceLog(storageFile: deviceLogDirectory.appendingPathComponent("Storage.sqlite")) - - let glucoseStore = GlucoseStore(cacheStore: persistenceController) + let glucoseStore = await GlucoseStore(cacheStore: persistenceController) let cgmEventStore = CgmEventStore(cacheStore: persistenceController) diff --git a/LoopTests/Managers/MealDetectionManagerTests.swift b/LoopTests/Managers/MealDetectionManagerTests.swift index 5b97629de5..dae4f15129 100644 --- a/LoopTests/Managers/MealDetectionManagerTests.swift +++ b/LoopTests/Managers/MealDetectionManagerTests.swift @@ -234,7 +234,8 @@ class MealDetectionManagerTests: XCTestCase { includePositiveVelocityAndRC: true, carbAbsorptionModel: .piecewiseLinear, recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult.model, - recommendationType: .automaticBolus + recommendationType: .automaticBolus, + useMidAbsorptionISF: false ) // These tests don't actually run the loop algorithm directly; they were written to take ICE from fixtures, compute carb effects, and subtract them. diff --git a/LoopTests/ViewModels/BolusEntryViewModelTests.swift b/LoopTests/ViewModels/BolusEntryViewModelTests.swift index 275b4c3743..4606f1e8a2 100644 --- a/LoopTests/ViewModels/BolusEntryViewModelTests.swift +++ b/LoopTests/ViewModels/BolusEntryViewModelTests.swift @@ -867,7 +867,8 @@ fileprivate class MockBolusEntryViewModelDelegate: BolusEntryViewModelDelegate { carbAbsorptionModel: .piecewiseLinear, recommendationInsulinModel: ExponentialInsulinModelPreset.rapidActingAdult, recommendationType: .manualBolus, - automaticBolusApplicationFactor: 0.4 + automaticBolusApplicationFactor: 0.4, + useMidAbsorptionISF: false ) func fetchData(for baseTime: Date, disablingPreMeal: Bool, ensureDosingCoverageStart: Date?) async throws -> StoredDataAlgorithmInput { diff --git a/WatchApp Extension/Managers/LoopDataManager.swift b/WatchApp Extension/Managers/LoopDataManager.swift index 1a0be226f2..b8b2d4a50f 100644 --- a/WatchApp Extension/Managers/LoopDataManager.swift +++ b/WatchApp Extension/Managers/LoopDataManager.swift @@ -18,7 +18,7 @@ import LoopAlgorithm class LoopDataManager { let carbStore: CarbStore - let glucoseStore: GlucoseStore + var glucoseStore: GlucoseStore! @PersistedProperty(key: "Settings") private var rawWatchInfo: LoopSettingsUserInfo.RawValue? @@ -69,16 +69,19 @@ class LoopDataManager { cacheLength: .hours(24), // Require 24 hours to store recent carbs "since midnight" for CarbEntryListController syncVersion: 0 ) - glucoseStore = GlucoseStore( - cacheStore: cacheStore, - cacheLength: .hours(4) - ) self.watchInfo = LoopSettingsUserInfo( loopSettings: LoopSettings(), scheduleOverride: nil, preMealOverride: nil ) + + Task { + glucoseStore = await GlucoseStore( + cacheStore: cacheStore, + cacheLength: .hours(4) + ) + } if let rawWatchInfo = rawWatchInfo, let watchInfo = LoopSettingsUserInfo(rawValue: rawWatchInfo) { self.watchInfo = watchInfo @@ -96,7 +99,9 @@ extension LoopDataManager { if activeContext == nil || context.shouldReplace(activeContext!) { if let newGlucoseSample = context.newGlucoseSample { - self.glucoseStore.addGlucoseSamples([newGlucoseSample]) { (_) in } + Task { + try? await self.glucoseStore.addGlucoseSamples([newGlucoseSample]) + } } activeContext = context } @@ -153,8 +158,10 @@ extension LoopDataManager { WCSession.default.sendGlucoseBackfillRequestMessage(userInfo) { (result) in switch result { case .success(let context): - self.glucoseStore.setSyncGlucoseSamples(context.samples) { (error) in - if let error = error { + Task { + do { + try await self.glucoseStore.setSyncGlucoseSamples(context.samples) + } catch { self.log.error("Failure setting sync glucose samples: %{public}@", String(describing: error)) } } @@ -198,14 +205,12 @@ extension LoopDataManager { return } - glucoseStore.getGlucoseSamples(start: .earliestGlucoseCutoff) { result in + Task { var historicalGlucose: [StoredGlucoseSample]? - switch result { - case .failure(let error): + do { + historicalGlucose = try await glucoseStore.getGlucoseSamples(start: .earliestGlucoseCutoff) + } catch { self.log.error("Failure getting glucose samples: %{public}@", String(describing: error)) - historicalGlucose = nil - case .success(let samples): - historicalGlucose = samples } let chartData = GlucoseChartData( unit: activeContext.displayGlucoseUnit,