Skip to content

Commit 5780971

Browse files
author
Rick Pasetto
authored
LOOP-1938: Acknowledge all Alerts given an identifier (#259)
* LOOP-1938: Acknowledge all given an identifier I thought it would be sufficient to only acknowledge the latest Alert, but LOOP-1938 points out that if an alert repeats without (prior issued Alerts) getting acknowledged, those prior Alerts get "stuck" and will replay on restart until they all are acknowledged. Becky convinced me that, conceptually, if you get multiple issues of the _same_ alert, recording an acknowledgement date on all of them is appropriate. * checkpoint * Unit tests & refactoring
1 parent 51d43ae commit 5780971

File tree

2 files changed

+102
-32
lines changed

2 files changed

+102
-32
lines changed

Loop/Managers/Alerts/AlertStore.swift

Lines changed: 73 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import LoopKit
1111

1212
public class AlertStore {
1313

14+
static let totalFetchLimit = 500
15+
1416
public enum AlertStoreError: Error {
1517
case notFound
1618
}
@@ -87,18 +89,18 @@ public class AlertStore {
8789

8890
public func recordAcknowledgement(of identifier: Alert.Identifier, at date: Date = Date(),
8991
completion: ((Result<Void, Error>) -> Void)? = nil) {
90-
recordUpdateOfLatest(of: identifier,
91-
addingPredicate: NSPredicate(format: "acknowledgedDate == nil"),
92-
with: {
93-
$0.acknowledgedDate = date
94-
return .save
95-
},
96-
completion: completion)
92+
recordUpdateOfAll(identifier: identifier,
93+
addingPredicate: NSPredicate(format: "acknowledgedDate == nil"),
94+
with: {
95+
$0.acknowledgedDate = date
96+
return .save
97+
},
98+
completion: completion)
9799
}
98100

99101
public func recordRetraction(of identifier: Alert.Identifier, at date: Date = Date(),
100102
completion: ((Result<Void, Error>) -> Void)? = nil) {
101-
recordUpdateOfLatest(of: identifier,
103+
recordUpdateOfLatest(identifier: identifier,
102104
addingPredicate: NSPredicate(format: "retractedDate == nil"),
103105
with: {
104106
// if the alert was retracted before it was ever shown, delete it.
@@ -135,29 +137,40 @@ public class AlertStore {
135137
// MARK: Private functions
136138

137139
extension AlertStore {
138-
139-
private func recordUpdateOfLatest(of identifier: Alert.Identifier,
140+
141+
private func recordUpdateOfAll(identifier: Alert.Identifier,
142+
addingPredicate predicate: NSPredicate,
143+
with updateBlock: @escaping ManagedObjectUpdateBlock,
144+
completion: ((Result<Void, Error>) -> Void)?) {
145+
managedObjectContext.perform {
146+
self.lookupAll(identifier: identifier, predicate: predicate) {
147+
switch $0 {
148+
case .success(let objects):
149+
if objects.count > 0 {
150+
let result = self.update(objects: objects, with: updateBlock)
151+
completion?(result)
152+
} else {
153+
self.log.error("Alert not found for update: %{public}@", identifier.value)
154+
completion?(.failure(AlertStoreError.notFound))
155+
}
156+
case .failure(let error):
157+
completion?(.failure(error))
158+
}
159+
}
160+
}
161+
}
162+
163+
private func recordUpdateOfLatest(identifier: Alert.Identifier,
140164
addingPredicate predicate: NSPredicate,
141-
with block: @escaping ManagedObjectUpdateBlock,
165+
with updateBlock: @escaping ManagedObjectUpdateBlock,
142166
completion: ((Result<Void, Error>) -> Void)?) {
143-
self.managedObjectContext.perform {
167+
managedObjectContext.perform {
144168
self.lookupLatest(identifier: identifier, predicate: predicate) {
145169
switch $0 {
146170
case .success(let object):
147171
if let object = object {
148-
let shouldDelete = block(object) == .delete
149-
do {
150-
if shouldDelete {
151-
self.managedObjectContext.delete(object)
152-
}
153-
try self.managedObjectContext.save()
154-
self.log.default("%{public}@ alert: %{public}@", shouldDelete ? "Deleted" : "Recorded", identifier.value)
155-
self.purgeExpired()
156-
completion?(.success)
157-
} catch {
158-
self.log.error("Could not store alert: %{public}@, %{public}@", identifier.value, String(describing: error))
159-
completion?(.failure(error))
160-
}
172+
let result = self.update(objects: [object], with: updateBlock)
173+
completion?(result)
161174
} else {
162175
self.log.error("Alert not found for update: %{public}@", identifier.value)
163176
completion?(.failure(AlertStoreError.notFound))
@@ -168,6 +181,41 @@ extension AlertStore {
168181
}
169182
}
170183
}
184+
185+
private func update(objects: [StoredAlert], with updateBlock: @escaping ManagedObjectUpdateBlock) -> Result<Void, Error> {
186+
objects.forEach { alert in
187+
let shouldDelete = updateBlock(alert) == .delete
188+
if shouldDelete {
189+
self.managedObjectContext.delete(alert)
190+
}
191+
self.log.default("%{public}@ alert: %{public}@", shouldDelete ? "Deleted" : "Recorded", alert.identifier.value)
192+
}
193+
do {
194+
try self.managedObjectContext.save()
195+
} catch {
196+
return .failure(error)
197+
}
198+
self.purgeExpired()
199+
return .success
200+
}
201+
202+
203+
private func lookupAll(identifier: Alert.Identifier, predicate: NSPredicate, completion: @escaping (Result<[StoredAlert], Error>) -> Void) {
204+
managedObjectContext.perform {
205+
do {
206+
let fetchRequest: NSFetchRequest<StoredAlert> = StoredAlert.fetchRequest()
207+
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [
208+
identifier.equalsPredicate,
209+
predicate
210+
])
211+
fetchRequest.fetchLimit = Self.totalFetchLimit
212+
let result = try self.managedObjectContext.fetch(fetchRequest)
213+
completion(.success(result))
214+
} catch {
215+
completion(.failure(error))
216+
}
217+
}
218+
}
171219

172220
private func lookupLatest(identifier: Alert.Identifier, predicate: NSPredicate, completion: @escaping (Result<StoredAlert?, Error>) -> Void) {
173221
managedObjectContext.perform {

LoopTests/Managers/Alerts/AlertStoreTests.swift

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ class AlertStoreTests: XCTestCase {
135135

136136
func testRecordAcknowledgedOfInvalid() {
137137
let expect = self.expectation(description: #function)
138-
self.alertStore.recordAcknowledgement(of: Self.identifier1, at: Self.historicDate) {
138+
self.alertStore.recordAcknowledgement(of: Self.identifier1, at: Self.historicDate) {
139139
switch $0 {
140140
case .failure: break
141141
case .success: XCTFail("Unexpected success")
@@ -504,9 +504,9 @@ class AlertStoreTests: XCTestCase {
504504
let expect = self.expectation(description: #function)
505505
let now = Date()
506506
fillWith(startDate: Self.historicDate, data: [
507-
(alert1, false, false),
507+
(alert1, true, false),
508508
(alert2, false, false),
509-
(alert1, true, false)
509+
(alert1, false, false)
510510
]) {
511511
self.alertStore.recordAcknowledgement(of: self.alert1.identifier, at: now, completion: self.expectSuccess {
512512
self.alertStore.fetch(completion: self.expectSuccess { storedAlerts in
@@ -515,7 +515,7 @@ class AlertStoreTests: XCTestCase {
515515
XCTAssertNotNil(storedAlerts.last)
516516
if let last = storedAlerts.last {
517517
XCTAssertEqual(Self.identifier1, last.identifier)
518-
XCTAssertEqual(Self.historicDate, last.issuedDate)
518+
XCTAssertEqual(Self.historicDate + 4, last.issuedDate)
519519
XCTAssertEqual(now, last.acknowledgedDate)
520520
XCTAssertNil(last.retractedDate)
521521
}
@@ -526,6 +526,28 @@ class AlertStoreTests: XCTestCase {
526526
wait(for: [expect], timeout: 1)
527527
}
528528

529+
func testAcknowledgeMultiple() {
530+
let expect = self.expectation(description: #function)
531+
let now = Date()
532+
fillWith(startDate: Self.historicDate, data: [
533+
(alert1, false, false),
534+
(alert2, false, false),
535+
(alert1, false, false)
536+
]) {
537+
self.alertStore.recordAcknowledgement(of: self.alert1.identifier, at: now, completion: self.expectSuccess {
538+
self.alertStore.fetch(completion: self.expectSuccess { storedAlerts in
539+
XCTAssertEqual(3, storedAlerts.count)
540+
for alert in storedAlerts where alert.identifier == Self.identifier1 {
541+
XCTAssertEqual(now, alert.acknowledgedDate)
542+
XCTAssertNil(alert.retractedDate)
543+
}
544+
expect.fulfill()
545+
})
546+
})
547+
}
548+
wait(for: [expect], timeout: 1)
549+
}
550+
529551
func testLookupAllUnacknowledgedEmpty() {
530552
let expect = self.expectation(description: #function)
531553
alertStore.lookupAllUnacknowledged(completion: expectSuccess { alerts in
@@ -561,12 +583,12 @@ class AlertStoreTests: XCTestCase {
561583
func testLookupAllUnacknowledgedSomeNot() {
562584
let expect = self.expectation(description: #function)
563585
fillWith(startDate: Self.historicDate, data: [
564-
(alert1, false, false),
586+
(alert1, true, false),
565587
(alert2, false, false),
566-
(alert1, true, false)
588+
(alert1, false, false),
567589
]) {
568590
self.alertStore.lookupAllUnacknowledged(completion: self.expectSuccess { alerts in
569-
self.assertEqual([self.alert1, self.alert2], alerts)
591+
self.assertEqual([self.alert2, self.alert1], alerts)
570592
expect.fulfill()
571593
})
572594
}

0 commit comments

Comments
 (0)