refactor: own daily currency calculation lifetime
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
import ConjureKit
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@MainActor
|
||||
private final class RateRequests {
|
||||
var pending: [CheckedContinuation<CurrencyRates, any Error>] = []
|
||||
func fetch() async throws -> CurrencyRates {
|
||||
try await withCheckedThrowingContinuation { pending.append($0) }
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func waitForRates(_ condition: () -> Bool) async throws {
|
||||
// Scheduling deadline only; calculation freshness uses the injected clock.
|
||||
for _ in 0..<1_000 {
|
||||
if condition() { return }
|
||||
try await Task.sleep(for: .milliseconds(1))
|
||||
}
|
||||
try #require(condition(), "Currency calculation did not reach the expected state")
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func currencyLifetimeSharesRefreshAndPreservesStaleCopyUntilFailure() async throws {
|
||||
var calendar = Calendar(identifier: .gregorian)
|
||||
calendar.timeZone = TimeZone(secondsFromGMT: 0)!
|
||||
var now = calendar.date(from: DateComponents(year: 2026, month: 1, day: 2, hour: 12))!
|
||||
let requests = RateRequests()
|
||||
let calculation = CurrencyCalculation(now: { now }, calendar: calendar, fetch: { try await requests.fetch() })
|
||||
var completions = 0
|
||||
calculation.changed = { completions += 1 }
|
||||
let snapshot = CurrencyRates(date: "2026-01-02", rates: ["eur": 1, "usd": 2, "dkk": 8])
|
||||
|
||||
calculation.update("100 USD to DKK")
|
||||
#expect(calculation.isCurrency)
|
||||
#expect(calculation.result?.copyValue == nil)
|
||||
try await waitForRates { requests.pending.count == 1 }
|
||||
calculation.update("50 USD to DKK")
|
||||
requests.pending[0].resume(returning: snapshot)
|
||||
try await waitForRates { completions == 1 }
|
||||
#expect(calculation.result?.copyValue == "200 DKK")
|
||||
calculation.update("100 USD to DKK")
|
||||
#expect(calculation.result?.copyValue == "400 DKK")
|
||||
#expect(requests.pending.count == 1)
|
||||
|
||||
now = calendar.date(byAdding: .day, value: 1, to: now)!
|
||||
calculation.update("100 USD to DKK")
|
||||
#expect(calculation.result?.copyValue == "400 DKK")
|
||||
#expect(calculation.result?.detail.contains("Stale cache; refreshing") == true)
|
||||
try await waitForRates { requests.pending.count == 2 }
|
||||
calculation.update("50 USD to DKK")
|
||||
requests.pending[1].resume(throwing: URLError(.notConnectedToInternet))
|
||||
try await waitForRates { completions == 2 }
|
||||
#expect(calculation.result?.copyValue == nil)
|
||||
#expect(calculation.result?.displayValue == "Currency rates unavailable")
|
||||
calculation.update("50 USD to DKK")
|
||||
#expect(calculation.result?.copyValue == nil)
|
||||
#expect(requests.pending.count == 2)
|
||||
|
||||
calculation.update("25 USD to DKK")
|
||||
try await waitForRates { requests.pending.count == 3 }
|
||||
calculation.update("Safari") // Leaving currency does not cancel shared work.
|
||||
#expect(!calculation.isCurrency)
|
||||
#expect(calculation.result == nil)
|
||||
requests.pending[2].resume(returning: snapshot)
|
||||
try await waitForRates { completions == 3 }
|
||||
#expect(calculation.result == nil)
|
||||
calculation.update("25 USD to DKK")
|
||||
#expect(calculation.result?.copyValue == "100 DKK")
|
||||
#expect(requests.pending.count == 3) // Fetch day, not snapshot metadata, controls freshness.
|
||||
calculation.update("1 km to mi")
|
||||
#expect(!calculation.isCurrency)
|
||||
#expect(calculation.result?.copyValue == "0.621371192237 mi")
|
||||
}
|
||||
Reference in New Issue
Block a user