Add Worth the Drive fuel savings with road-routed estimates
Optional Fuel Savings feature (off by default): vehicle MPG in UK imperial gallons, fill amount in pounds or litres, persisted through the existing keychain/app-group store. Ranks the WHOLE station pool by estimated net saving (fuel-price saving at each pump minus the extra return-trip driving cost) using MapKit road distances with a straight-line fallback marked (est.). The closest station with a valid price for the selected fuel is the comparison baseline and is labelled "Closest station"; other rows read "Save £X.XX after travel" / "£X.XX worse off after travel" / "Break even". Labels live in the shared model so they are unit-tested. The TOP badge still marks the cheapest pump price and reads "CHEAPEST" when the list is ranked by net saving. Road routing now prioritises the Worth-the-Drive candidates plus the baseline and merges cache entries instead of replacing them, so far-out stations get real routes rather than permanent straight-line estimates. Numeric fields gain a Done button above the keypad (decimalPad has no Return key); the fill-up control is hidden unless the feature is configured; Favourites shares one session fill amount with the station list. 131 tests.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import XCTest
|
||||
@testable import FuelBoardShared
|
||||
|
||||
/// Shared payload storage must survive an install whose provisioning profile
|
||||
/// has NO `com.apple.security.application-groups` entitlement (free/personal
|
||||
/// team and SideStore installs) — the exact situation this test runner is in.
|
||||
///
|
||||
/// Regression guard: `appGroupFileURL` used to return nil without a group
|
||||
/// container, so every save silently failed and every load returned empty. That
|
||||
/// left `loadStations()` permanently empty (letting the freshness gate skip the
|
||||
/// fetch for ever, i.e. a blank station list) and re-downloaded the ~2.8 MB
|
||||
/// dump on every refresh. Storage now falls back to the app's own container.
|
||||
final class FuelStoreFileCacheTests: XCTestCase {
|
||||
|
||||
func testSharedFileURLExistsWithoutAppGroupContainer() {
|
||||
XCTAssertNotNil(
|
||||
FuelStore.appGroupFileURL("fuelboard.test.probe.json"),
|
||||
"storage must resolve to a writable location even when the App Group container is unprovisioned"
|
||||
)
|
||||
}
|
||||
|
||||
func testSharedFileRoundTripsWithoutAppGroupContainer() throws {
|
||||
let name = "fuelboard.test.roundtrip.json"
|
||||
let payload = Data("hello-cache".utf8)
|
||||
|
||||
XCTAssertTrue(
|
||||
FuelStore.saveAppGroupFile(payload, name: name),
|
||||
"save must succeed without an App Group container"
|
||||
)
|
||||
XCTAssertEqual(FuelStore.loadAppGroupFile(name), payload)
|
||||
|
||||
if let url = FuelStore.appGroupFileURL(name) {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// The dump cache is what stops a ~2.8 MB re-download on every refresh, so
|
||||
/// it must round-trip too — not just the station list.
|
||||
func testDumpCacheRoundTripsWithoutAppGroupContainer() {
|
||||
let day = "2026-01-01"
|
||||
let data = Data(repeating: 7, count: 4096)
|
||||
|
||||
MirrorFuelProvider.saveDumpCache(day: day, data: data)
|
||||
let cache = MirrorFuelProvider.loadDumpCache()
|
||||
|
||||
XCTAssertEqual(cache?.day, day, "cached dump day must survive a round-trip")
|
||||
XCTAssertEqual(cache?.data, data, "cached dump payload must survive a round-trip")
|
||||
|
||||
if let url = FuelStore.appGroupFileURL(MirrorFuelProvider.liveDumpCacheFileName) {
|
||||
try? FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// The road-distance cache is memoized in memory because re-reading it per
|
||||
/// station (it lives in the keychain) stalled the main thread past the iOS
|
||||
/// launch watchdog. A save must therefore invalidate the memo — a stale
|
||||
/// in-memory cache would show wrong distances after a route refresh.
|
||||
func testRoadDistanceMemoRefreshesAfterSave() {
|
||||
FuelStore.saveRoadDistances(sourceLat: 51.0, sourceLng: -1.0, entries: [:])
|
||||
XCTAssertEqual(FuelStore.loadRoadDistances()?.sourceLat, 51.0)
|
||||
|
||||
FuelStore.saveRoadDistances(sourceLat: 52.0, sourceLng: -2.0, entries: [:])
|
||||
|
||||
XCTAssertEqual(
|
||||
FuelStore.loadRoadDistances()?.sourceLat, 52.0,
|
||||
"load must reflect the newest save, not a stale memoized cache"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user