三个问题都在 DebugLogger: 1. 无轮转:minLevel 默认 .debug 且应用常驻,实测 debug.log 已达 1.5 MB 且只在手动清理时下降,长期运行会到 GB 级。 修:5MB × 3 份轮转(debug.log → .1 → .2 → .3),磁盘上限约 20 MB。 2. timestampStr 是计算属性,每次读取新建 DateFormatter(构造约 100µs), 而它被日志列表逐条渲染读取——刷新一次列表就新建 N 个。 修:创建条目时格式化一次并存储。 3. 每条日志 open/close 一次 FileHandle(3 次系统调用)。 修:复用句柄。 实现约束(已写入注释): - 共享 DateFormatter 非线程安全,故标注仅主线程调用 (DebugLogger 是 @MainActor,条目只在其方法内创建) - nonisolated(unsafe) 的两个状态只在 logQueue 串行队列上访问 - 阈值常量放文件作用域:@MainActor 类型的 static let 会被 actor 隔离, nonisolated 方法引用会触发 Swift 6 错误 测试:DebugLogEntryTests.swift 6 条(格式、稳定性、级别顺序与配色唯一性)。 Swift 测试 12 → 18,全部通过。
82 lines
3.2 KiB
Swift
82 lines
3.2 KiB
Swift
import XCTest
|
||
@testable import FinDataHub
|
||
|
||
/// DebugLogEntry 时间戳格式化测试。
|
||
///
|
||
/// 背景:`timestampStr` 原为计算属性,每次读取都新建一个 `DateFormatter`。
|
||
/// 该属性被日志列表逐条渲染读取,等于刷新一次列表就新建 N 个重量级对象
|
||
/// (`DateFormatter` 构造约 100µs 量级)。改为创建条目时格式化一次并存储。
|
||
///
|
||
/// 这些用例锁定两件事:格式不变(UI 与日志文件解析都依赖 `HH:mm:ss.SSS`),
|
||
/// 以及同一个 Date 生成的字符串稳定可复现。
|
||
final class DebugLogEntryTests: XCTestCase {
|
||
|
||
/// 构造固定时刻:UTC 2026-10-25 14:30:05.123
|
||
private func makeDate() -> Date {
|
||
var comps = DateComponents()
|
||
comps.year = 2026
|
||
comps.month = 10
|
||
comps.day = 25
|
||
comps.hour = 14
|
||
comps.minute = 30
|
||
comps.second = 5
|
||
comps.nanosecond = 123_000_000
|
||
comps.timeZone = TimeZone(identifier: "UTC")
|
||
var calendar = Calendar(identifier: .gregorian)
|
||
calendar.timeZone = TimeZone(identifier: "UTC")!
|
||
return calendar.date(from: comps)!
|
||
}
|
||
|
||
func testTimestampFormatIsHHmmssWithMillis() {
|
||
let entry = DebugLogEntry(
|
||
timestamp: makeDate(), level: .info, category: "Test", message: "hello"
|
||
)
|
||
// 只校验形状,不绑定具体时区偏移(运行环境时区不同则小时数不同)
|
||
let pattern = #"^\d{2}:\d{2}:\d{2}\.\d{3}$"#
|
||
XCTAssertNotNil(
|
||
entry.timestampStr.range(of: pattern, options: .regularExpression),
|
||
"时间戳格式必须为 HH:mm:ss.SSS,实际:\(entry.timestampStr)"
|
||
)
|
||
}
|
||
|
||
func testTimestampIsStableAcrossReads() {
|
||
let entry = DebugLogEntry(
|
||
timestamp: makeDate(), level: .debug, category: "Test", message: "x"
|
||
)
|
||
// 预格式化后多次读取必须完全一致(计算属性版本也一致,但会重复格式化)
|
||
XCTAssertEqual(entry.timestampStr, entry.timestampStr)
|
||
}
|
||
|
||
func testDistinctDatesProduceDistinctStrings() {
|
||
let base = makeDate()
|
||
let a = DebugLogEntry(timestamp: base, level: .info, category: "T", message: "a")
|
||
let b = DebugLogEntry(
|
||
timestamp: base.addingTimeInterval(1), level: .info, category: "T", message: "b"
|
||
)
|
||
XCTAssertNotEqual(a.timestampStr, b.timestampStr)
|
||
}
|
||
|
||
func testEntryCarriesLevelAndCategory() {
|
||
let entry = DebugLogEntry(
|
||
timestamp: Date(), level: .error, category: "Sync", message: "boom"
|
||
)
|
||
XCTAssertEqual(entry.level, .error)
|
||
XCTAssertEqual(entry.level.rawValue, "ERROR")
|
||
XCTAssertEqual(entry.category, "Sync")
|
||
XCTAssertEqual(entry.message, "boom")
|
||
}
|
||
|
||
func testLevelOrderingUsedByMinLevelFilter() {
|
||
// shouldLog 依赖 allCases 的顺序(debug < info < warning < error)
|
||
let levels = DebugLogLevel.allCases
|
||
XCTAssertEqual(levels.first, .debug)
|
||
XCTAssertEqual(levels.last, .error)
|
||
XCTAssertEqual(levels.count, 4)
|
||
}
|
||
|
||
func testLevelColorsAreDistinct() {
|
||
let hexes = Set(DebugLogLevel.allCases.map { $0.colorHex })
|
||
XCTAssertEqual(hexes.count, DebugLogLevel.allCases.count, "各级别颜色不得重复")
|
||
}
|
||
}
|