FastDropConditionTests.swift 3.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. // LoopFollow
  2. // FastDropConditionTests.swift
  3. import Foundation
  4. @testable import LoopFollow
  5. import Testing
  6. /// Fast drop fires only when **each** of the last `monitoringWindow` consecutive
  7. /// intervals dropped by at least `delta` mg/dL. It is per-reading, not a single
  8. /// cumulative drop — these tests pin that behaviour down.
  9. struct FastDropConditionTests {
  10. let cond = FastDropCondition()
  11. // MARK: - Fires
  12. @Test("fires when every one of N readings drops by at least delta")
  13. func firesOnConsecutiveDrops() {
  14. // 3 intervals, each falling exactly 15 → about 45 over ~15 minutes
  15. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  16. let data = AlarmData.withReadings([150, 135, 120, 105])
  17. #expect(cond.evaluate(alarm: alarm, data: data, now: .init()))
  18. }
  19. @Test("a drop exactly equal to delta still counts")
  20. func firesAtExactThreshold() {
  21. let alarm = Alarm.fastDrop(delta: 15, window: 1)
  22. let data = AlarmData.withReadings([120, 105])
  23. #expect(cond.evaluate(alarm: alarm, data: data, now: .init()))
  24. }
  25. @Test("only the most recent window+1 readings matter")
  26. func ignoresOlderReadings() {
  27. // Earlier readings rise sharply; the last 4 fall 15 each → fires
  28. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  29. let data = AlarmData.withReadings([100, 200, 165, 150, 135, 120])
  30. #expect(cond.evaluate(alarm: alarm, data: data, now: .init()))
  31. }
  32. // MARK: - Does not fire
  33. @Test("does NOT fire when one interval falls short of delta")
  34. func doesNotFireWhenOneIntervalTooSmall() {
  35. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  36. // middle interval is only 10
  37. let data = AlarmData.withReadings([150, 135, 125, 110])
  38. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  39. }
  40. @Test("per-reading, not cumulative: 40+5+5 = 50 total but does NOT fire")
  41. func doesNotFireOnCumulativeOnly() {
  42. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  43. // total fall is 50 (> 45) but two intervals are only 5 each
  44. let data = AlarmData.withReadings([150, 110, 105, 100])
  45. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  46. }
  47. @Test("does NOT fire when glucose is rising")
  48. func doesNotFireWhenRising() {
  49. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  50. let data = AlarmData.withReadings([100, 115, 130, 145])
  51. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  52. }
  53. @Test("does NOT fire without enough readings")
  54. func doesNotFireWithTooFewReadings() {
  55. // window 3 needs 4 readings; only 3 supplied
  56. let alarm = Alarm.fastDrop(delta: 15, window: 3)
  57. let data = AlarmData.withReadings([135, 120, 105])
  58. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  59. }
  60. // MARK: - Guard cases
  61. @Test("does NOT fire when delta is nil")
  62. func ignoresNilDelta() {
  63. let alarm = Alarm.fastDrop(delta: nil, window: 3)
  64. let data = AlarmData.withReadings([150, 135, 120, 105])
  65. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  66. }
  67. @Test("does NOT fire when window is nil")
  68. func ignoresNilWindow() {
  69. let alarm = Alarm.fastDrop(delta: 15, window: nil)
  70. let data = AlarmData.withReadings([150, 135, 120, 105])
  71. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  72. }
  73. @Test("does NOT fire when window is zero")
  74. func ignoresZeroWindow() {
  75. let alarm = Alarm.fastDrop(delta: 15, window: 0)
  76. let data = AlarmData.withReadings([150, 135, 120, 105])
  77. #expect(!cond.evaluate(alarm: alarm, data: data, now: .init()))
  78. }
  79. }