PumpOpsSynchronousTests.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. //
  2. // PumpOpsSynchronousTests.swift
  3. // RileyLink
  4. //
  5. // Created by Jaim Zuber on 2/21/17.
  6. // Copyright © 2017 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. @testable import RileyLinkKit
  10. @testable import MinimedKit
  11. @testable import RileyLinkBLEKit
  12. class PumpOpsSynchronousTests: XCTestCase {
  13. var sut: PumpOpsSession!
  14. var pumpSettings: PumpSettings!
  15. var pumpState: PumpState!
  16. var pumpID: String!
  17. var pumpRegion: PumpRegion!
  18. var pumpModel: PumpModel!
  19. var mockMessageSender: MockPumpMessageSender!
  20. let dateComponents2007 = DateComponents(calendar: Calendar.current, year: 2007, month: 1, day: 1)
  21. let dateComponents2017 = DateComponents(calendar: Calendar.current, year: 2017, month: 1, day: 1)
  22. let squareBolusDataLength = 26
  23. lazy var datePast2007: Date = {
  24. return self.dateComponents2017.date!.addingTimeInterval(TimeInterval(minutes:60))
  25. }()
  26. lazy var datePast2017: Date = {
  27. return self.dateComponents2017.date!.addingTimeInterval(TimeInterval(minutes:60))
  28. }()
  29. lazy var dateTimestamp2010: DateComponents = {
  30. self.createSquareBolusEvent2010().timestamp
  31. }()
  32. override func setUp() {
  33. super.setUp()
  34. pumpID = "636781"
  35. pumpRegion = .worldWide
  36. pumpModel = PumpModel.model523
  37. mockMessageSender = MockPumpMessageSender()
  38. setUpSUT()
  39. }
  40. /// Creates the System Under Test. This is needed because our SUT has dependencies injected through the constructor
  41. func setUpSUT() {
  42. pumpSettings = PumpSettings(pumpID: pumpID, pumpRegion: pumpRegion)
  43. pumpState = PumpState()
  44. pumpState.pumpModel = pumpModel
  45. pumpState.awakeUntil = Date(timeIntervalSinceNow: 100) // pump is awake
  46. sut = PumpOpsSession(settings: pumpSettings, pumpState: pumpState, messageSender: mockMessageSender, delegate: mockMessageSender)
  47. }
  48. /// Duplicates logic in setUp with a new PumpModel
  49. ///
  50. /// - Parameter newPumpModel: model of the pump to test
  51. func setUpTestWithPumpModel(_ newPumpModel: PumpModel) {
  52. pumpModel = newPumpModel
  53. setUpSUT()
  54. }
  55. var ack: PumpMessage {
  56. return PumpMessage(pumpID: pumpID, type: .pumpAck)
  57. }
  58. func testSetNormalBolus() {
  59. mockMessageSender.responses = [
  60. .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: false))],
  61. .bolus: [ack, ack],
  62. ]
  63. let result = sut.setNormalBolus(units: 1)
  64. XCTAssertNil(result)
  65. }
  66. func testSetNormalBolusWhileBolusing() {
  67. mockMessageSender.responses = [
  68. .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: true, suspended: false))],
  69. .bolus: [ack, ack],
  70. ]
  71. let result = sut.setNormalBolus(units: 1)
  72. XCTAssertNotNil(result)
  73. if case SetBolusError.certain(PumpOpsError.bolusInProgress) = result! {
  74. // pass
  75. } else {
  76. XCTFail("Expected bolus in progress error, got: \(result!)")
  77. }
  78. }
  79. func testSetNormalBolusWhileSuspended() {
  80. mockMessageSender.responses = [
  81. .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: true))],
  82. .bolus: [ack, ack],
  83. ]
  84. let result = sut.setNormalBolus(units: 1)
  85. XCTAssertNotNil(result)
  86. if case SetBolusError.certain(PumpOpsError.pumpSuspended) = result! {
  87. // pass
  88. } else {
  89. XCTFail("Expected pump suspended error, got: \(result!)")
  90. }
  91. }
  92. func testSetNormalBolusUncertain() {
  93. mockMessageSender.responses = [
  94. .readPumpStatus: [mockMessageSender.makeMockResponse(.readPumpStatus, ReadPumpStatusMessageBody(bolusing: false, suspended: false))],
  95. .bolus: [ack], // Second ack missing will cause PumpOpsError.noReponse during second exchange
  96. ]
  97. let result = sut.setNormalBolus(units: 1)
  98. XCTAssertNotNil(result)
  99. switch result {
  100. case .uncertain:
  101. break
  102. default:
  103. XCTFail("Expected pump suspended error, got: \(result!)")
  104. }
  105. }
  106. func testShouldContinueIfTimestampBeforeStartDateNotEncountered() {
  107. let page = HistoryPage(events: [createBatteryEvent()])
  108. let (_, hasMoreEvents, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  109. XCTAssertTrue(hasMoreEvents)
  110. }
  111. func testShouldFinishIfTimestampBeforeStartDateEncountered() {
  112. let batteryEvent = createBatteryEvent()
  113. let page = HistoryPage(events: [batteryEvent])
  114. let afterBatteryEventDate = batteryEvent.timestamp.date!.addingTimeInterval(TimeInterval(hours: 10))
  115. let (_, hasMoreEvents, _) = page.timestampedEvents(after: afterBatteryEventDate, timeZone: pumpState.timeZone, model: pumpModel)
  116. XCTAssertFalse(hasMoreEvents)
  117. }
  118. func testEventsAfterStartDateAreReturned() {
  119. let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007)
  120. let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017)
  121. let page = HistoryPage(events: [batteryEvent2007, batteryEvent2017])
  122. let (events, _, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  123. XCTAssertEqual(events.count, 2)
  124. }
  125. func testEventBeforeStartDateIsFiltered() {
  126. let datePast2007 = dateComponents2007.date!.addingTimeInterval(TimeInterval(minutes: 60))
  127. let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007)
  128. let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017)
  129. let page = HistoryPage(events: [batteryEvent2007, batteryEvent2017])
  130. let (events, hasMoreEvents, cancelled) = page.timestampedEvents(after: datePast2007, timeZone: pumpState.timeZone, model: pumpModel)
  131. assertArray(events, doesntContainPumpEvent: batteryEvent2007)
  132. XCTAssertEqual(events.count, 1)
  133. XCTAssertFalse(hasMoreEvents)
  134. XCTAssertFalse(cancelled)
  135. }
  136. func testPumpLostTimeCancelsFetchEarly() {
  137. let batteryEvent2007 = createBatteryEvent(withDateComponent: dateComponents2007)
  138. let batteryEvent2017 = createBatteryEvent(withDateComponent: dateComponents2017)
  139. let page = HistoryPage(events: [batteryEvent2017, batteryEvent2007])
  140. let (events, hasMoreEvents, cancelledEarly) = page.timestampedEvents(after: Date.distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  141. XCTAssertTrue(cancelledEarly)
  142. XCTAssertFalse(hasMoreEvents)
  143. XCTAssertEqual(events.count, 1)
  144. assertArray(events, doesntContainPumpEvent: batteryEvent2017)
  145. }
  146. func testEventsWithSameDataArentAddedTwice() {
  147. let page = HistoryPage(events: [createBolusEvent2009(), createBolusEvent2009()])
  148. let (events, _, _) = page.timestampedEvents(after: Date.distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  149. XCTAssertEqual(events.count, 1)
  150. }
  151. func testNonMutableSquareWaveBolusFor522IsReturned() {
  152. // device that can have out of order events
  153. setUpTestWithPumpModel(.model522)
  154. // 2009-07-31 09:00:00 +0000
  155. // 120 minute duration
  156. let squareWaveBolus = BolusNormalPumpEvent(availableData: Data(hexadecimalString: "010080048000240009a24a1510")!, pumpModel: pumpModel)!
  157. let page = HistoryPage(events: [squareWaveBolus])
  158. let (timeStampedEvents, _, _) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  159. // It should be included
  160. XCTAssertTrue(array(timeStampedEvents, containsPumpEvent: squareWaveBolus))
  161. }
  162. // This sets up a square wave bolus that has a timestamp four hours before a temp basal, but is appended to history
  163. // "after" the temp basal. This is an important condition to test, because the temp basal could be filtered out erroneously
  164. // if we were just filtering on startDate, and startDate was after the bolus timestamp, but before the temp basal.
  165. // Previously, convertPumpEventToTimestampedEvents was being called with startDate: Date.distantPast
  166. // Changing it to use a time in that important window to cover
  167. func testDelayedAppendEventDoesNotCauseValidEventsToBeFilteredOut() {
  168. setUpTestWithPumpModel(.model522)
  169. let tempEventBasal = createTempEventBasal2016()
  170. let dateComponents = tempEventBasal.timestamp.addingTimeInterval(TimeInterval(hours:-4))
  171. let squareBolusEventFourHoursBefore = createSquareBolusEvent(dateComponents: dateComponents)
  172. let page = HistoryPage(events: [tempEventBasal, squareBolusEventFourHoursBefore])
  173. let (timeStampedEvents, hasMoreEvents, cancelled) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  174. // Debatable (undefined) whether this should be returned. It is tested to avoid inadvertantly changing behavior
  175. assertArray(timeStampedEvents, containsPumpEvent: squareBolusEventFourHoursBefore)
  176. XCTAssertTrue(hasMoreEvents)
  177. XCTAssertFalse(cancelled)
  178. }
  179. // MARK: Regular Bolus Event before starttime (offset 9 minutes)
  180. func test522RegularBolusEventBeforeStartTimeShouldNotCancel() {
  181. setUpTestWithPumpModel(.model522)
  182. let pumpEvent = createSquareBolusEvent2010()
  183. let page = HistoryPage(events: [pumpEvent])
  184. let startDate = pumpEvent.timestamp.date!.addingTimeInterval(TimeInterval(minutes:9))
  185. let (timestampedEvents, hasMoreEvents, cancelled) = page.timestampedEvents(after: startDate, timeZone: pumpState.timeZone, model: pumpModel)
  186. assertArray(timestampedEvents, containsPumpEvent: pumpEvent)
  187. //We found an event before the start time but we can't verify the timestamp from the Square Bolus so there could be more valid events
  188. XCTAssertTrue(hasMoreEvents)
  189. XCTAssertFalse(cancelled)
  190. }
  191. // The test the border condition sof the pump lost time detection, the main behavior of which is covered
  192. // in testPumpLostTimeCancelsFetchEarly. The precise point at which we decide pump time is lost (the one hour mark) is aribtrary.
  193. func testOutOfOrderEventUnderAnHourDoesntCancel() {
  194. setUpTestWithPumpModel(.model523)
  195. let after2007Date = dateComponents2007.date!.addingTimeInterval(TimeInterval(minutes:59))
  196. let batteryEvent = createBatteryEvent(withDateComponent: dateComponents2007)
  197. let laterBatteryEvent = createBatteryEvent(atTime: after2007Date)
  198. let page = HistoryPage(events: [laterBatteryEvent, batteryEvent])
  199. let (_, _, cancelled) = page.timestampedEvents(after: .distantPast, timeZone: pumpState.timeZone, model: pumpModel)
  200. XCTAssertFalse(cancelled)
  201. }
  202. // MARK: Test Sanity Checks
  203. func test2010EventSanityWith523() {
  204. setUpTestWithPumpModel(.model523)
  205. let bolusEvent = createSquareBolusEvent2010()
  206. XCTAssertEqual(bolusEvent.timestamp.year!, 2010)
  207. XCTAssertEqual(bolusEvent.timestamp.timeZone, pumpState.timeZone)
  208. }
  209. func test2009EventSanityWith523() {
  210. setUpTestWithPumpModel(.model523)
  211. let bolusEvent = createBolusEvent2009()
  212. XCTAssertEqual(bolusEvent.timestamp.year!, 2009)
  213. XCTAssertEqual(bolusEvent.timestamp.timeZone, pumpState.timeZone)
  214. }
  215. func test2009EventSavityWith522() {
  216. setUpTestWithPumpModel(.model522)
  217. XCTAssertEqual(createBolusEvent2009().timestamp.year!, 2009)
  218. }
  219. func test2010EventSanityWith522() {
  220. setUpTestWithPumpModel(.model522)
  221. XCTAssertEqual(createSquareBolusEvent2010().timestamp.year!, 2010)
  222. }
  223. func createBatteryEvent(withDateComponent dateComponents: DateComponents) -> BatteryPumpEvent {
  224. return createBatteryEvent(atTime: dateComponents.date!)
  225. }
  226. func createBatteryEvent(atTime date: Date = Date()) -> BatteryPumpEvent {
  227. let calendar = Calendar.current
  228. let year = calendar.component(.year, from: date) - 2000
  229. let month = calendar.component(.month, from: date)
  230. let day = calendar.component(.day, from: date)
  231. let hour = calendar.component(.hour, from: date)
  232. let minute = calendar.component(.minute, from: date)
  233. let second = calendar.component(.second, from: date)
  234. let secondByte = UInt8(second) & 0b00111111
  235. let minuteByte = UInt8(minute) & 0b00111111
  236. let hourByte = UInt8(hour) & 0b00011111
  237. let dayByte = UInt8(day) & 0b00011111
  238. let monthUpperComponent = (UInt8(month) & 0b00001100) << 4
  239. let monthLowerComponent = (UInt8(month) & 0b00000011) << 6
  240. let secondMonthByte = secondByte | monthUpperComponent
  241. let minuteMonthByte = minuteByte | monthLowerComponent
  242. let yearByte = UInt8(year) & 0b01111111
  243. let batteryData = Data([0,0, secondMonthByte, minuteMonthByte, hourByte, dayByte, yearByte])
  244. let batteryPumpEvent = BatteryPumpEvent(availableData: batteryData, pumpModel: PumpModel.model523)!
  245. return batteryPumpEvent
  246. }
  247. func createSquareBolusEvent2010() -> BolusNormalPumpEvent {
  248. //2010-08-01 05:00:16 +000
  249. let dateComponents = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2010, month: 8, day: 1, hour: 5, minute: 0, second: 16)
  250. let data = Data(hexadecimalString: "01009000900058008a344b1010")!
  251. return BolusNormalPumpEvent(length: BolusNormalPumpEvent.calculateLength(pumpModel.larger), rawData: data, timestamp: dateComponents, unabsorbedInsulinRecord: nil, amount: 0.0, programmed: 0.0, unabsorbedInsulinTotal: 0.0, type: .square, duration: TimeInterval(minutes: 120), wasRemotelyTriggered: false)
  252. }
  253. func createSquareBolusEvent(dateComponents: DateComponents) -> BolusNormalPumpEvent {
  254. let data = Data(hexadecimalString: randomDataString(length: squareBolusDataLength))!
  255. return BolusNormalPumpEvent(length: BolusNormalPumpEvent.calculateLength(pumpModel.larger), rawData: data, timestamp: dateComponents, unabsorbedInsulinRecord: nil, amount: 0.0, programmed: 0.0, unabsorbedInsulinTotal: 0.0, type: .square, duration: TimeInterval(hours: 8), wasRemotelyTriggered: false)
  256. }
  257. func createBolusEvent2011() -> BolusNormalPumpEvent {
  258. //2010-08-01 05:00:11 +000
  259. let dateComponents = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2011, month: 8, day: 1, hour: 5, minute: 0, second: 16)
  260. let data = Data(hexadecimalString: "01009000900058008a344b10FF")!
  261. return BolusNormalPumpEvent(length: BolusNormalPumpEvent.calculateLength(pumpModel.larger), rawData: data, timestamp: dateComponents, unabsorbedInsulinRecord: nil, amount: 0.0, programmed: 0.0, unabsorbedInsulinTotal: 0.0, type: .normal, duration: TimeInterval(minutes: 120), wasRemotelyTriggered: false)
  262. }
  263. func createTempEventBasal2016() -> TempBasalPumpEvent {
  264. // 2016-05-30 01:21:00 +0000
  265. let tempEventBasal = TempBasalPumpEvent(availableData: Data(hexadecimalString:"338c4055145d1000")!, pumpModel: pumpModel)!
  266. return tempEventBasal
  267. }
  268. func createBolusEvent2009() -> BolusNormalPumpEvent {
  269. let dateComponents = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2009, month: 7, day: 31, hour: 9, minute: 0, second: 0)
  270. let timeInterval: TimeInterval = TimeInterval(minutes: 2)
  271. let data = Data(hexadecimalString:"338c4055145d2000")!
  272. return BolusNormalPumpEvent(length: 13, rawData: data, timestamp: dateComponents, unabsorbedInsulinRecord: nil, amount: 2.0, programmed: 1.0, unabsorbedInsulinTotal: 0.0, type: .normal, duration: timeInterval, wasRemotelyTriggered: false)
  273. }
  274. func createNonDelayedEvent2009() -> BolusReminderPumpEvent {
  275. let dateComponents = DateComponents(calendar: Calendar.current, timeZone: pumpState.timeZone, year: 2009, month: 7, day: 31, hour: 9, minute: 0, second: 0)
  276. let data = Data(hexadecimalString:"338c48FFF45d2000")!
  277. let length = 7
  278. return BolusReminderPumpEvent(length: length, rawData: data, timestamp: dateComponents)
  279. }
  280. }
  281. // from comment at https://gist.github.com/szhernovoy/276e69eb90a0de84dd90
  282. func randomDataString(length:Int) -> String {
  283. let charSet = "abcdef0123456789"
  284. let c = charSet.map { String($0) }
  285. var s:String = ""
  286. for _ in 0..<length {
  287. s.append(c[Int(arc4random()) % c.count])
  288. }
  289. return s
  290. }
  291. func array(_ timestampedEvents: [TimestampedHistoryEvent], containsPumpEvent pumpEvent: PumpEvent) -> Bool {
  292. let event = timestampedEvents.first { $0.pumpEvent.rawData == pumpEvent.rawData }
  293. return event != nil
  294. }
  295. func assertArray(_ timestampedEvents: [TimestampedHistoryEvent], containsPumpEvent pumpEvent: PumpEvent) {
  296. XCTAssertNotNil(timestampedEvents.first { $0.pumpEvent.rawData == pumpEvent.rawData})
  297. }
  298. func assertArray(_ timestampedEvents: [TimestampedHistoryEvent], containsPumpEvents pumpEvents: [PumpEvent]) {
  299. pumpEvents.forEach { assertArray(timestampedEvents, containsPumpEvent: $0) }
  300. }
  301. func assertArray(_ timestampedEvents: [TimestampedHistoryEvent], doesntContainPumpEvent pumpEvent: PumpEvent) {
  302. XCTAssertNil(timestampedEvents.first { $0.pumpEvent.rawData == pumpEvent.rawData })
  303. }
  304. // from http://jernejstrasner.com/2015/07/08/testing-throwable-methods-in-swift-2.html - transferred to Swift 3
  305. func assertThrows<T>(_ expression: @autoclosure () throws -> T, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
  306. do {
  307. let _ = try expression()
  308. XCTFail("No error to catch! - \(message)", file: file, line: line)
  309. } catch {
  310. }
  311. }
  312. func assertNoThrow<T>(_ expression: @autoclosure () throws -> T, _ message: String = "", file: StaticString = #file, line: UInt = #line) {
  313. do {
  314. let _ = try expression()
  315. } catch let error {
  316. XCTFail("Caught error: \(error) - \(message)", file: file, line: line)
  317. }
  318. }
  319. extension DateComponents {
  320. func addingTimeInterval(_ timeInterval: TimeInterval) -> DateComponents {
  321. let newDate = self.date!.addingTimeInterval(timeInterval)
  322. let newDateComponents = Calendar.current.dateComponents(in: TimeZone.currentFixed, from: newDate)
  323. return newDateComponents
  324. }
  325. }