TemporaryScheduleOverrideTests.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. //
  2. // TemporaryScheduleOverrideTests.swift
  3. // LoopKitTests
  4. //
  5. // Created by Michael Pangburn on 1/2/19.
  6. // Copyright © 2019 LoopKit Authors. All rights reserved.
  7. //
  8. import XCTest
  9. @testable import LoopKit
  10. extension TimeZone {
  11. static var fixtureTimeZone: TimeZone {
  12. return TimeZone(secondsFromGMT: 25200)! // -0700
  13. }
  14. static var utcTimeZone: TimeZone {
  15. return TimeZone(secondsFromGMT: 0)!
  16. }
  17. }
  18. extension ISO8601DateFormatter {
  19. static func fixtureFormatter(timeZone: TimeZone = .fixtureTimeZone) -> Self {
  20. let formatter = self.init()
  21. formatter.formatOptions = .withInternetDateTime
  22. formatter.formatOptions.subtract(.withTimeZone)
  23. formatter.timeZone = timeZone
  24. return formatter
  25. }
  26. }
  27. class TemporaryScheduleOverrideTests: XCTestCase {
  28. let dateFormatter = ISO8601DateFormatter.localTimeDate()
  29. let epsilon = 1e-6
  30. let basalRateSchedule = BasalRateSchedule(dailyItems: [
  31. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  32. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  33. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  34. ])!
  35. private func date(at time: String) -> Date {
  36. return dateFormatter.date(from: "2019-01-01T\(time):00")!
  37. }
  38. private func basalUpOverride(start: String, end: String) -> TemporaryScheduleOverride {
  39. return TemporaryScheduleOverride(
  40. context: .custom,
  41. settings: TemporaryScheduleOverrideSettings(
  42. unit: .milligramsPerDeciliter,
  43. targetRange: nil,
  44. insulinNeedsScaleFactor: 1.5
  45. ),
  46. startDate: date(at: start),
  47. duration: .finite(date(at: end).timeIntervalSince(date(at: start))),
  48. enactTrigger: .local,
  49. syncIdentifier: UUID()
  50. )
  51. }
  52. private func applyingActiveBasalOverride(from start: String, to end: String, on schedule: BasalRateSchedule, referenceDate: Date? = nil) -> BasalRateSchedule {
  53. let override = basalUpOverride(start: start, end: end)
  54. let referenceDate = referenceDate ?? override.startDate
  55. return schedule.applyingBasalRateMultiplier(from: override, relativeTo: referenceDate)
  56. }
  57. // Override start aligns with schedule item start
  58. func testBasalRateScheduleOverrideStartTimeMatch() {
  59. let overrideBasalSchedule = applyingActiveBasalOverride(from: "00:00", to: "01:00", on: basalRateSchedule)
  60. let expected = BasalRateSchedule(dailyItems: [
  61. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  62. RepeatingScheduleValue(startTime: .hours(1), value: 1.2),
  63. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  64. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  65. ])!
  66. XCTAssert(overrideBasalSchedule.equals(expected, accuracy: epsilon))
  67. }
  68. // Override contained fully within a schedule item
  69. func testBasalRateScheduleOverrideContained() {
  70. let overridden = applyingActiveBasalOverride(from: "02:00", to: "04:00", on: basalRateSchedule)
  71. let expected = BasalRateSchedule(dailyItems: [
  72. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  73. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  74. RepeatingScheduleValue(startTime: .hours(4), value: 1.2),
  75. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  76. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  77. ])!
  78. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  79. }
  80. // Override end aligns with schedule item start
  81. func testBasalRateScheduleOverrideEndTimeMatch() {
  82. let overridden = applyingActiveBasalOverride(from: "02:00", to: "06:00", on: basalRateSchedule)
  83. let expected = BasalRateSchedule(dailyItems: [
  84. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  85. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  86. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  87. RepeatingScheduleValue(startTime: .hours(20), value: 1.0)
  88. ])!
  89. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  90. }
  91. // Override completely encapsulates schedule item
  92. func testBasalRateScheduleOverrideEncapsulate() {
  93. let overridden = applyingActiveBasalOverride(from: "02:00", to: "22:00", on: basalRateSchedule)
  94. let expected = BasalRateSchedule(dailyItems: [
  95. RepeatingScheduleValue(startTime: .hours(0), value: 1.2),
  96. RepeatingScheduleValue(startTime: .hours(2), value: 1.8),
  97. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  98. RepeatingScheduleValue(startTime: .hours(20), value: 1.5),
  99. RepeatingScheduleValue(startTime: .hours(22), value: 1.0),
  100. ])!
  101. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  102. }
  103. func testSingleBasalRateSchedule() {
  104. let basalRateSchedule = BasalRateSchedule(dailyItems: [
  105. RepeatingScheduleValue(startTime: .hours(0), value: 1.0)
  106. ])!
  107. let overridden = applyingActiveBasalOverride(from: "08:00", to: "12:00", on: basalRateSchedule)
  108. let expected = BasalRateSchedule(dailyItems: [
  109. RepeatingScheduleValue(startTime: .hours(0), value: 1.0),
  110. RepeatingScheduleValue(startTime: .hours(8), value: 1.5),
  111. RepeatingScheduleValue(startTime: .hours(12), value: 1.0)
  112. ])!
  113. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  114. }
  115. func testOverrideCrossingMidnight() {
  116. var override = basalUpOverride(start: "22:00", end: "23:00")
  117. override.duration += .hours(5) // override goes from 10pm to 4am of the next day
  118. let overridden = basalRateSchedule.applyingBasalRateMultiplier(from: override, relativeTo: date(at: "22:00"))
  119. let expected = BasalRateSchedule(dailyItems: [
  120. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  121. RepeatingScheduleValue(startTime: .hours(4), value: 1.2),
  122. RepeatingScheduleValue(startTime: .hours(6), value: 1.4),
  123. RepeatingScheduleValue(startTime: .hours(20), value: 1.0),
  124. RepeatingScheduleValue(startTime: .hours(22), value: 1.5)
  125. ])!
  126. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  127. }
  128. func testMultiDayOverride() {
  129. var override = basalUpOverride(start: "02:00", end: "22:00")
  130. override.duration += .hours(48) // override goes from 2am until 10pm two days later
  131. let overridden = basalRateSchedule.applyingBasalRateMultiplier(
  132. from: override,
  133. relativeTo: date(at: "02:00") + .hours(24)
  134. )
  135. // expect full schedule override; start/end dates are too distant to have an effect
  136. let expected = BasalRateSchedule(dailyItems: [
  137. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  138. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  139. RepeatingScheduleValue(startTime: .hours(20), value: 1.5)
  140. ])!
  141. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  142. }
  143. func testOutdatedOverride() {
  144. let overridden = applyingActiveBasalOverride(from: "02:00", to: "04:00", on: basalRateSchedule,
  145. referenceDate: date(at: "12:00").addingTimeInterval(.hours(24)))
  146. let expected = basalRateSchedule
  147. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  148. }
  149. func testFarFutureOverride() {
  150. let overridden = applyingActiveBasalOverride(from: "10:00", to: "12:00", on: basalRateSchedule,
  151. referenceDate: date(at: "02:00").addingTimeInterval(-.hours(24)))
  152. let expected = basalRateSchedule
  153. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  154. }
  155. func testIndefiniteOverride() {
  156. var override = basalUpOverride(start: "02:00", end: "22:00")
  157. override.duration = .indefinite
  158. let overridden = basalRateSchedule.applyingBasalRateMultiplier(from: override, relativeTo: date(at: "02:00"))
  159. // expect full schedule overridden
  160. let expected = BasalRateSchedule(dailyItems: [
  161. RepeatingScheduleValue(startTime: .hours(0), value: 1.8),
  162. RepeatingScheduleValue(startTime: .hours(6), value: 2.1),
  163. RepeatingScheduleValue(startTime: .hours(20), value: 1.5)
  164. ])!
  165. XCTAssert(overridden.equals(expected, accuracy: epsilon))
  166. }
  167. func testDurationIsInfinite() {
  168. let tempOverride = TemporaryScheduleOverride(context: .legacyWorkout,
  169. settings: .init(unit: .milligramsPerDeciliter, targetRange: DoubleRange(minValue: 120, maxValue: 150)),
  170. startDate: Date(),
  171. duration: .indefinite,
  172. enactTrigger: .local,
  173. syncIdentifier: UUID())
  174. XCTAssertTrue(tempOverride.duration.isInfinite)
  175. }
  176. func testOverrideScheduleAnnotatingReservoirSplitsDose() {
  177. let schedule = BasalRateSchedule(dailyItems: [
  178. RepeatingScheduleValue(startTime: 0, value: 0.225),
  179. RepeatingScheduleValue(startTime: 3600.0, value: 0.18000000000000002),
  180. RepeatingScheduleValue(startTime: 10800.0, value: 0.135),
  181. RepeatingScheduleValue(startTime: 12689.855275034904, value: 0.15),
  182. RepeatingScheduleValue(startTime: 21600.0, value: 0.2),
  183. RepeatingScheduleValue(startTime: 32400.0, value: 0.2),
  184. RepeatingScheduleValue(startTime: 50400.0, value: 0.2),
  185. RepeatingScheduleValue(startTime: 52403.79680299759, value: 0.16000000000000003),
  186. RepeatingScheduleValue(startTime: 63743.58014559746, value: 0.2),
  187. RepeatingScheduleValue(startTime: 63743.58014583588, value: 0.16000000000000003),
  188. RepeatingScheduleValue(startTime: 69968.05249071121, value: 0.2),
  189. RepeatingScheduleValue(startTime: 69968.05249094963, value: 0.18000000000000002),
  190. RepeatingScheduleValue(startTime: 79200.0, value: 0.225),
  191. ])!
  192. let dose = DoseEntry(
  193. type: .tempBasal,
  194. startDate: date(at: "19:25"),
  195. endDate: date(at: "19:30"),
  196. value: 0.8,
  197. unit: .units
  198. )
  199. let annotated = [dose].annotated(with: schedule)
  200. XCTAssertEqual(3, annotated.count)
  201. XCTAssertEqual(dose.programmedUnits, annotated.map { $0.unitsInDeliverableIncrements }.reduce(0, +))
  202. }
  203. // MARK: - Target range tests
  204. func testActiveTargetRangeOverride() {
  205. let overrideRange = DoubleRange(minValue: 120, maxValue: 140)
  206. let overrideStart = Date()
  207. let overrideDuration = TimeInterval(hours: 4)
  208. let settings = TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, targetRange: overrideRange)
  209. let override = TemporaryScheduleOverride(context: .custom, settings: settings, startDate: overrideStart, duration: .finite(overrideDuration), enactTrigger: .local, syncIdentifier: UUID())
  210. let normalRange = DoubleRange(minValue: 95, maxValue: 105)
  211. let rangeSchedule = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: normalRange)])!.applyingOverride(override)
  212. XCTAssertEqual(rangeSchedule.value(at: overrideStart), overrideRange)
  213. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration / 2), overrideRange)
  214. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration), overrideRange)
  215. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration + .hours(2)), overrideRange)
  216. }
  217. func testFutureTargetRangeOverride() {
  218. let overrideRange = DoubleRange(minValue: 120, maxValue: 140)
  219. let overrideStart = Date() + .hours(2)
  220. let overrideDuration = TimeInterval(hours: 4)
  221. let settings = TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter, targetRange: overrideRange)
  222. let futureOverride = TemporaryScheduleOverride(context: .custom, settings: settings, startDate: overrideStart, duration: .finite(overrideDuration), enactTrigger: .local, syncIdentifier: UUID())
  223. let normalRange = DoubleRange(minValue: 95, maxValue: 105)
  224. let rangeSchedule = GlucoseRangeSchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue(startTime: 0, value: normalRange)])!.applyingOverride(futureOverride)
  225. XCTAssertEqual(rangeSchedule.value(at: overrideStart + .minutes(-5)), normalRange)
  226. XCTAssertEqual(rangeSchedule.value(at: overrideStart), overrideRange)
  227. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration), overrideRange)
  228. XCTAssertEqual(rangeSchedule.value(at: overrideStart + overrideDuration + .hours(2)), overrideRange)
  229. }
  230. }
  231. class TemporaryScheduleOverrideContextCodableTests: XCTestCase {
  232. func testCodablePreMeal() throws {
  233. try assertTemporaryScheduleOverrideContextCodable(.preMeal, encodesJSON: """
  234. {
  235. "context" : "preMeal"
  236. }
  237. """
  238. )
  239. }
  240. func testCodableLegacyWorkout() throws {
  241. try assertTemporaryScheduleOverrideContextCodable(.legacyWorkout, encodesJSON: """
  242. {
  243. "context" : "legacyWorkout"
  244. }
  245. """
  246. )
  247. }
  248. func testCodablePreset() throws {
  249. let preset = TemporaryScheduleOverridePreset(id: UUID(uuidString: "238E41EA-9576-4981-A1A4-51E10228584F")!,
  250. symbol: "🚀",
  251. name: "Rocket",
  252. settings: TemporaryScheduleOverrideSettings(unit: .milligramsPerDeciliter,
  253. targetRange: DoubleRange(minValue: 90, maxValue: 100)),
  254. duration: .indefinite)
  255. try assertTemporaryScheduleOverrideContextCodable(.preset(preset), encodesJSON: """
  256. {
  257. "context" : {
  258. "preset" : {
  259. "preset" : {
  260. "duration" : "indefinite",
  261. "id" : "238E41EA-9576-4981-A1A4-51E10228584F",
  262. "name" : "Rocket",
  263. "settings" : {
  264. "targetRangeInMgdl" : {
  265. "maxValue" : 100,
  266. "minValue" : 90
  267. }
  268. },
  269. "symbol" : "🚀"
  270. }
  271. }
  272. }
  273. }
  274. """
  275. )
  276. }
  277. func testCodableCustom() throws {
  278. try assertTemporaryScheduleOverrideContextCodable(.custom, encodesJSON: """
  279. {
  280. "context" : "custom"
  281. }
  282. """
  283. )
  284. }
  285. private func assertTemporaryScheduleOverrideContextCodable(_ original: TemporaryScheduleOverride.Context, encodesJSON string: String) throws {
  286. let data = try encoder.encode(TestContainer(context: original))
  287. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  288. let decoded = try decoder.decode(TestContainer.self, from: data)
  289. XCTAssertEqual(decoded.context, original)
  290. }
  291. private let encoder: JSONEncoder = {
  292. let encoder = JSONEncoder()
  293. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  294. return encoder
  295. }()
  296. private let decoder = JSONDecoder()
  297. private struct TestContainer: Codable, Equatable {
  298. let context: TemporaryScheduleOverride.Context
  299. }
  300. }
  301. class TemporaryScheduleOverrideEnactTriggerCodableTests: XCTestCase {
  302. func testCodableLocal() throws {
  303. try assertTemporaryScheduleOverrideEnactTriggerCodable(.local, encodesJSON: """
  304. {
  305. "enactTrigger" : "local"
  306. }
  307. """
  308. )
  309. }
  310. func testCodableRemote() throws {
  311. try assertTemporaryScheduleOverrideEnactTriggerCodable(.remote("address"), encodesJSON: """
  312. {
  313. "enactTrigger" : {
  314. "remote" : {
  315. "address" : "address"
  316. }
  317. }
  318. }
  319. """
  320. )
  321. }
  322. private func assertTemporaryScheduleOverrideEnactTriggerCodable(_ original: TemporaryScheduleOverride.EnactTrigger, encodesJSON string: String) throws {
  323. let data = try encoder.encode(TestContainer(enactTrigger: original))
  324. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  325. let decoded = try decoder.decode(TestContainer.self, from: data)
  326. XCTAssertEqual(decoded.enactTrigger, original)
  327. }
  328. private let encoder: JSONEncoder = {
  329. let encoder = JSONEncoder()
  330. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  331. return encoder
  332. }()
  333. private let decoder = JSONDecoder()
  334. private struct TestContainer: Codable, Equatable {
  335. let enactTrigger: TemporaryScheduleOverride.EnactTrigger
  336. }
  337. }
  338. class TemporaryScheduleOverrideDurationCodableTests: XCTestCase {
  339. func testCodableFinite() throws {
  340. try assertTemporaryScheduleOverrideDurationCodable(.finite(.hours(2.5)), encodesJSON: """
  341. {
  342. "duration" : {
  343. "finite" : {
  344. "duration" : 9000
  345. }
  346. }
  347. }
  348. """
  349. )
  350. }
  351. func testCodableIndefinite() throws {
  352. try assertTemporaryScheduleOverrideDurationCodable(.indefinite, encodesJSON: """
  353. {
  354. "duration" : "indefinite"
  355. }
  356. """
  357. )
  358. }
  359. private func assertTemporaryScheduleOverrideDurationCodable(_ original: TemporaryScheduleOverride.Duration, encodesJSON string: String) throws {
  360. let data = try encoder.encode(TestContainer(duration: original))
  361. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  362. let decoded = try decoder.decode(TestContainer.self, from: data)
  363. XCTAssertEqual(decoded.duration, original)
  364. }
  365. private let encoder: JSONEncoder = {
  366. let encoder = JSONEncoder()
  367. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  368. return encoder
  369. }()
  370. private let decoder = JSONDecoder()
  371. private struct TestContainer: Codable, Equatable {
  372. let duration: TemporaryScheduleOverride.Duration
  373. }
  374. }
  375. private extension TemporaryScheduleOverride.Duration {
  376. static func += (lhs: inout TemporaryScheduleOverride.Duration, rhs: TimeInterval) {
  377. switch lhs {
  378. case .finite(let interval):
  379. lhs = .finite(interval + rhs)
  380. case .indefinite:
  381. return
  382. }
  383. }
  384. }
  385. class TemporaryOverrideEndCodableTests: XCTestCase {
  386. var dateFormatter = ISO8601DateFormatter.fixtureFormatter()
  387. private func date(at time: String) -> Date {
  388. return dateFormatter.date(from: "2019-01-01T\(time):00")!
  389. }
  390. func testCodableOverrideEarlyEnd() throws {
  391. let end = End.early(date(at: "02:00"))
  392. try assertEndCodable(end, encodesJSON: """
  393. {
  394. "end" : {
  395. "date" : 567975600,
  396. "type" : "early"
  397. }
  398. }
  399. """
  400. )
  401. }
  402. func testCodableOverrideNaturalEnd() throws {
  403. let end = End.natural
  404. try assertEndCodable(end, encodesJSON: """
  405. {
  406. "end" : {
  407. "type" : "natural"
  408. }
  409. }
  410. """
  411. )
  412. }
  413. func testCodableOverrideDeleted() throws {
  414. let end = End.deleted
  415. try assertEndCodable(end, encodesJSON: """
  416. {
  417. "end" : {
  418. "type" : "deleted"
  419. }
  420. }
  421. """
  422. )
  423. }
  424. private func assertEndCodable(_ original: End, encodesJSON string: String) throws {
  425. let data = try encoder.encode(TestContainer(end: original))
  426. XCTAssertEqual(String(data: data, encoding: .utf8), string)
  427. let decoded = try decoder.decode(TestContainer.self, from: data)
  428. XCTAssertEqual(decoded.end, original)
  429. }
  430. private let encoder: JSONEncoder = {
  431. let encoder = JSONEncoder()
  432. encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
  433. return encoder
  434. }()
  435. private let decoder = JSONDecoder()
  436. private struct TestContainer: Codable, Equatable {
  437. let end: End
  438. }
  439. }