|
| 1 | +package com.evsuite.hardware.telemetry |
| 2 | + |
| 3 | +import org.junit.Assert.assertEquals |
| 4 | +import org.junit.Assert.assertTrue |
| 5 | +import org.junit.Test |
| 6 | + |
| 7 | +class PlanDriftTest { |
| 8 | + |
| 9 | + /** 300 km at 0,20 %/km ± 0,02: leaves at 90 %, plans to arrive at 30 %, reserve 10 %. */ |
| 10 | + private val plan = PlanDrift.Followed( |
| 11 | + legKm = 300.0, |
| 12 | + socAtDeparturePercent = 90.0, |
| 13 | + plannedRatePercentPerKm = 0.20, |
| 14 | + plannedUncertaintyPercentPerKm = 0.02, |
| 15 | + reservePercent = 10.0, |
| 16 | + ) |
| 17 | + |
| 18 | + /** Charge left after [km] at [ratePerKm], as the gauge would read it. */ |
| 19 | + private fun socAfter(km: Double, ratePerKm: Double) = |
| 20 | + plan.socAtDeparturePercent - km * ratePerKm |
| 21 | + |
| 22 | + @Test |
| 23 | + fun `a drive spending what the plan assumed is holding`() { |
| 24 | + val verdict = PlanDrift.check(plan, drivenKm = 100.0, socNowPercent = socAfter(100.0, 0.20)) |
| 25 | + val holding = verdict as PlanDrift.Verdict.Holding |
| 26 | + assertEquals(0.20, holding.reading.observedPercentPerKm, 1e-9) |
| 27 | + assertEquals(200.0, holding.reading.remainingKm, 1e-9) |
| 28 | + assertEquals(30.0, holding.reading.arrivalPercent, 1e-9) |
| 29 | + } |
| 30 | + |
| 31 | + @Test |
| 32 | + fun `spending inside the plan's own band raises nothing`() { |
| 33 | + // 0,215 %/km against a plan of 0,20 ± 0,02, with the measurement's own band on top: |
| 34 | + // the two overlap, so there is no drift to report however the arrival lands. |
| 35 | + val verdict = PlanDrift.check(plan, drivenKm = 100.0, socNowPercent = socAfter(100.0, 0.215)) |
| 36 | + assertTrue(verdict.toString(), verdict is PlanDrift.Verdict.Holding) |
| 37 | + } |
| 38 | + |
| 39 | + @Test |
| 40 | + fun `spending twice the band past the plan, and short of the reserve, drifts`() { |
| 41 | + // 0,26 %/km: 0,06 past the plan, about twice the two bands added. Arrival lands at 4 %. |
| 42 | + val verdict = PlanDrift.check(plan, drivenKm = 100.0, socNowPercent = socAfter(100.0, 0.26)) |
| 43 | + val short = verdict as PlanDrift.Verdict.Short |
| 44 | + assertEquals(0.26, short.reading.observedPercentPerKm, 1e-9) |
| 45 | + assertEquals(12.0, short.reading.arrivalPercent, 1e-9) |
| 46 | + assertTrue(short.shortfallPercent > 0.0) |
| 47 | + // The shortfall is measured at the pessimistic edge, so it is wider than 10 − 12. |
| 48 | + assertEquals(plan.reservePercent - (12.0 - short.reading.bandPercent), short.shortfallPercent, 1e-9) |
| 49 | + } |
| 50 | + |
| 51 | + @Test |
| 52 | + fun `spending past the plan but still arriving well clear of the reserve is not reported`() { |
| 53 | + // Same 0,26 %/km, but only 60 km of leg left to spend it on: the arrival still clears |
| 54 | + // the reserve, and a companion that speaks here is one nobody reads when it matters. |
| 55 | + val near = plan.copy(legKm = 100.0) |
| 56 | + val verdict = PlanDrift.check(near, drivenKm = 40.0, socNowPercent = socAfter(40.0, 0.26)) |
| 57 | + assertTrue(verdict.toString(), verdict is PlanDrift.Verdict.Holding) |
| 58 | + } |
| 59 | + |
| 60 | + @Test |
| 61 | + fun `the car's own remaining distance wins over the leg's arithmetic`() { |
| 62 | + val verdict = PlanDrift.check( |
| 63 | + plan, |
| 64 | + drivenKm = 100.0, |
| 65 | + socNowPercent = socAfter(100.0, 0.20), |
| 66 | + remainingKm = 240.0, |
| 67 | + ) |
| 68 | + val holding = verdict as PlanDrift.Verdict.Holding |
| 69 | + assertEquals(240.0, holding.reading.remainingKm, 1e-9) |
| 70 | + assertEquals(70.0 - 240.0 * 0.20, holding.reading.arrivalPercent, 1e-9) |
| 71 | + } |
| 72 | + |
| 73 | + @Test |
| 74 | + fun `too little road behind the car says so rather than measuring`() { |
| 75 | + val verdict = PlanDrift.check(plan, drivenKm = 5.0, socNowPercent = 88.0) |
| 76 | + assertEquals( |
| 77 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.TOO_SOON), |
| 78 | + verdict, |
| 79 | + ) |
| 80 | + } |
| 81 | + |
| 82 | + @Test |
| 83 | + fun `a charge en route is refused, a descent of one point is not`() { |
| 84 | + assertEquals( |
| 85 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.CHARGED_EN_ROUTE), |
| 86 | + PlanDrift.check(plan, drivenKm = 100.0, socNowPercent = 95.0), |
| 87 | + ) |
| 88 | + // 91 % after 100 km: one point above departure, which is the gauge's own step. |
| 89 | + val recovered = PlanDrift.check(plan, drivenKm = 100.0, socNowPercent = 91.0) |
| 90 | + val holding = recovered as PlanDrift.Verdict.Holding |
| 91 | + assertEquals(0.0, holding.reading.observedPercentPerKm, 1e-9) |
| 92 | + } |
| 93 | + |
| 94 | + @Test |
| 95 | + fun `no plan, no charge, no distance and an arrival each say which`() { |
| 96 | + assertEquals( |
| 97 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.NO_PLAN), |
| 98 | + PlanDrift.check(null, 100.0, 60.0), |
| 99 | + ) |
| 100 | + assertEquals( |
| 101 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.NO_CHARGE), |
| 102 | + PlanDrift.check(plan, 100.0, null), |
| 103 | + ) |
| 104 | + assertEquals( |
| 105 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.NO_DISTANCE), |
| 106 | + PlanDrift.check(plan, null, 60.0), |
| 107 | + ) |
| 108 | + assertEquals( |
| 109 | + PlanDrift.Verdict.Unavailable(PlanDrift.Reason.ARRIVED), |
| 110 | + PlanDrift.check(plan, 320.0, 20.0), |
| 111 | + ) |
| 112 | + } |
| 113 | + |
| 114 | + @Test |
| 115 | + fun `the measurement's band shrinks as the road behind it grows`() { |
| 116 | + val early = PlanDrift.check(plan, 20.0, socAfter(20.0, 0.20)) as PlanDrift.Verdict.Holding |
| 117 | + val late = PlanDrift.check(plan, 200.0, socAfter(200.0, 0.20)) as PlanDrift.Verdict.Holding |
| 118 | + // Same rate, same route: the only thing that changed is how much evidence there is. |
| 119 | + assertTrue( |
| 120 | + "${early.reading.bandPercent} vs ${late.reading.bandPercent}", |
| 121 | + early.reading.bandPercent > late.reading.bandPercent, |
| 122 | + ) |
| 123 | + } |
| 124 | + |
| 125 | + @Test |
| 126 | + fun `the rate the plan was made with is the one with the climb already in it`() { |
| 127 | + // What the app hands over: the planner's own fold, not a second copy of it. |
| 128 | + val rate = SocRate(0.20, 0.02, SocRate.Source.TRIP_HISTORY, sampleCount = 8) |
| 129 | + val effective = ChargeStopPlan.effectiveRate(rate, 300.0, RouteGrade.Cost(6.0, 1.5)) |
| 130 | + assertEquals(0.22, effective.percentPerKm, 1e-9) |
| 131 | + assertEquals(0.025, effective.uncertaintyPercentPerKm, 1e-9) |
| 132 | + } |
| 133 | +} |
0 commit comments