mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-09 19:29:41 +00:00
152 lines
5.7 KiB
Swift
152 lines
5.7 KiB
Swift
import Foundation
|
|
import HapiClient
|
|
import HapiProtocol
|
|
import Testing
|
|
|
|
/// Tile/format derivations — thresholds mirror the web reference
|
|
/// (`formatTokens` in `usage.tsx`, `formatFileSize` in `file-metadata.ts`).
|
|
/// Transcribed from the Android `UsageMathTest`.
|
|
@Suite("UsageMath")
|
|
struct UsageMathTests {
|
|
|
|
// MARK: - formatTokens
|
|
|
|
@Test func tokenFormattingMirrorsTheWebThresholds() {
|
|
#expect(UsageMath.formatTokens(0) == "0")
|
|
#expect(UsageMath.formatTokens(999) == "999")
|
|
#expect(UsageMath.formatTokens(1_000) == "1.0K")
|
|
#expect(UsageMath.formatTokens(1_234) == "1.2K")
|
|
#expect(UsageMath.formatTokens(9_999) == "10.0K")
|
|
#expect(UsageMath.formatTokens(10_000) == "10K")
|
|
#expect(UsageMath.formatTokens(999_999) == "1000K")
|
|
#expect(UsageMath.formatTokens(1_500_000) == "1.5M")
|
|
#expect(UsageMath.formatTokens(9_999_999) == "10.0M")
|
|
#expect(UsageMath.formatTokens(12_000_000) == "12M")
|
|
#expect(UsageMath.formatTokens(2_300_000_000) == "2.3B")
|
|
}
|
|
|
|
// MARK: - formatBytes
|
|
|
|
@Test func byteFormattingMirrorsTheWebFormatFileSize() {
|
|
#expect(UsageMath.formatBytes(0) == "0 B")
|
|
#expect(UsageMath.formatBytes(512) == "512 B")
|
|
#expect(UsageMath.formatBytes(1_024) == "1 KB")
|
|
#expect(UsageMath.formatBytes(1_536) == "1.5 KB")
|
|
// < 10 keeps one decimal, trailing .0 stripped; >= 10 rounds whole.
|
|
#expect(UsageMath.formatBytes(10_186) == "9.9 KB")
|
|
#expect(UsageMath.formatBytes(10_240) == "10 KB")
|
|
#expect(UsageMath.formatBytes(4_194_304) == "4 MB")
|
|
#expect(UsageMath.formatBytes(84_930_560) == "81 MB")
|
|
#expect(UsageMath.formatBytes(2_147_483_648) == "2 GB")
|
|
#expect(UsageMath.formatBytes(-5) == "0 B")
|
|
}
|
|
|
|
// MARK: - cacheHitRate
|
|
|
|
private func totals(input: Int, cacheRead: Int) -> UsageSummaryTotals {
|
|
UsageSummaryTotals(
|
|
inputTokens: input,
|
|
outputTokens: 0,
|
|
cacheReadTokens: cacheRead,
|
|
cacheCreationTokens: 0,
|
|
totalTokens: input,
|
|
uncachedTokens: input - cacheRead,
|
|
requests: 1,
|
|
sessions: 1
|
|
)
|
|
}
|
|
|
|
@Test func cacheHitRateIsCacheReadOverInputWithOneDecimal() {
|
|
#expect(UsageMath.cacheHitRate(totals(input: 12_400_000, cacheRead: 9_800_000)) == "79.0%")
|
|
#expect(UsageMath.cacheHitRate(totals(input: 5, cacheRead: 5)) == "100.0%")
|
|
#expect(UsageMath.cacheHitRate(totals(input: 3, cacheRead: 1)) == "33.3%")
|
|
}
|
|
|
|
@Test func cacheHitRateDegradesToZeroPercentWithoutInputTokens() {
|
|
#expect(UsageMath.cacheHitRate(totals(input: 0, cacheRead: 0)) == "0%")
|
|
}
|
|
|
|
// MARK: - dailyBars
|
|
|
|
private func bucket(_ key: String, total: Int) -> UsageSummaryBucket {
|
|
UsageSummaryBucket(
|
|
key: key,
|
|
inputTokens: total,
|
|
outputTokens: 0,
|
|
cacheReadTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
totalTokens: total,
|
|
uncachedTokens: total,
|
|
requests: 1
|
|
)
|
|
}
|
|
|
|
@Test func boundedRangesFillTheCalendarWindowEndingToday() {
|
|
let sparse = [bucket("2026-08-13", total: 5), bucket("2026-08-18", total: 9)]
|
|
|
|
let bars = UsageMath.dailyBars(daily: sparse, days: 7, todayKey: "2026-08-18")
|
|
|
|
#expect(bars.count == 7)
|
|
#expect(bars.first?.key == "2026-08-12")
|
|
#expect(bars.last?.key == "2026-08-18")
|
|
#expect(bars.first?.totalTokens == 0)
|
|
#expect(bars.first?.bucket == nil)
|
|
#expect(bars[1].totalTokens == 5)
|
|
#expect(bars.last?.totalTokens == 9)
|
|
#expect(bars.last?.bucket == sparse[1])
|
|
}
|
|
|
|
@Test func rangeAllKeepsTheSparseBucketsAsIs() {
|
|
let sparse = [bucket("2025-01-01", total: 3), bucket("2026-08-18", total: 4)]
|
|
let bars = UsageMath.dailyBars(daily: sparse, days: nil, todayKey: "2026-08-18")
|
|
#expect(bars.map(\.key) == ["2025-01-01", "2026-08-18"])
|
|
#expect(bars.map(\.totalTokens) == [3, 4])
|
|
}
|
|
|
|
@Test func calendarFillSpansMonthBoundaries() {
|
|
let bars = UsageMath.dailyBars(daily: [], days: 30, todayKey: "2026-03-05")
|
|
#expect(bars.count == 30)
|
|
#expect(bars.first?.key == "2026-02-04")
|
|
#expect(bars.last?.key == "2026-03-05")
|
|
}
|
|
|
|
@Test func calendarFillHandlesLeapFebruary() {
|
|
let bars = UsageMath.dailyBars(daily: [], days: 7, todayKey: "2024-03-01")
|
|
#expect(bars.map(\.key) == [
|
|
"2024-02-24", "2024-02-25", "2024-02-26", "2024-02-27",
|
|
"2024-02-28", "2024-02-29", "2024-03-01",
|
|
])
|
|
}
|
|
|
|
@Test func calendarFillSpansYearBoundaries() {
|
|
let bars = UsageMath.dailyBars(daily: [], days: 7, todayKey: "2026-01-03")
|
|
#expect(bars.first?.key == "2025-12-28")
|
|
#expect(bars.last?.key == "2026-01-03")
|
|
}
|
|
|
|
@Test func corruptTodayKeyDegradesToTheSparseMapping() {
|
|
let sparse = [bucket("2026-08-18", total: 4)]
|
|
let bars = UsageMath.dailyBars(daily: sparse, days: 7, todayKey: "not-a-day")
|
|
#expect(bars.map(\.key) == ["2026-08-18"])
|
|
}
|
|
|
|
// MARK: - todayKey
|
|
|
|
@Test func todayKeyUsesTheGivenZonesCalendarDay() throws {
|
|
// 2026-08-17T20:00:00Z.
|
|
let now = Date(timeIntervalSince1970: 1_786_996_800)
|
|
let utc = try #require(TimeZone(identifier: "UTC"))
|
|
let shanghai = try #require(TimeZone(identifier: "Asia/Shanghai"))
|
|
#expect(UsageMath.todayKey(now: now, timeZone: utc) == "2026-08-17")
|
|
// UTC+8 has already rolled to the next day.
|
|
#expect(UsageMath.todayKey(now: now, timeZone: shanghai) == "2026-08-18")
|
|
}
|
|
|
|
// MARK: - shortDayLabel
|
|
|
|
@Test func shortDayLabelsDropTheYear() {
|
|
#expect(UsageMath.shortDayLabel("2026-08-18") == "08-18")
|
|
#expect(UsageMath.shortDayLabel("odd-key") == "odd-key")
|
|
}
|
|
}
|