Files
FinDataHub 73b45d7c0c fix(client): debug.log 加轮转并缓存时间戳格式化
三个问题都在 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,全部通过。
2026-10-26 20:55:42 +08:00

82 lines
3.2 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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, "各级别颜色不得重复")
}
}