Files
Qiu Yuzhou 1d85e86b8e feat(diagnostics): unify log lines into one filtered view
Collapse the four definitions of "one log line" into LogLine (timestamp,
level, source, text) with a single renderer and parser. agent.log lines
now carry the uniform prefix, absorbing the ssxng: event marker into the
source column; legacy lines degrade to raw display without rewriting.
Level detection moves into the row model with a classic three-level
taxonomy whose unified-log projection stays unchanged.

The diagnostics log view merges GUI events and the agent.log tail through
DiagnosticLogIntake into one time-ordered list with level and source
filters; the tail window logic moves into the intake and blank lines are
kept for raw fidelity. The report whitelist projection and unified-log
routing are unchanged.
2026-10-20 14:42:51 +08:00

106 lines
3.9 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
/// 记录 intake 分发行的替身去向。
private final class RecordingDestination: SslocalLineDestination, @unchecked Sendable {
private let lock = NSLock()
private var rows: [LogLine] = []
func receive(_ row: LogLine) {
lock.lock()
defer { lock.unlock() }
rows.append(row)
}
var received: [LogLine] {
lock.lock()
defer { lock.unlock() }
return rows
}
}
struct SslocalOutputIntakeTests {
/// 固定时钟:行时间戳可精确断言。
private static let stamp = Date(timeIntervalSince1970: 1_758_000_000)
private func makeIntake(_ destinations: [any SslocalLineDestination]) -> SslocalOutputIntake {
SslocalOutputIntake(destinations: destinations, now: { Self.stamp })
}
@Test func framesLinesAcrossChunkBoundaries() {
let destination = RecordingDestination()
let intake = makeIntake([destination])
intake.write(Data("hel".utf8))
intake.write(Data("lo wo".utf8))
intake.write(Data("rld\nsecond\n".utf8))
#expect(destination.received.map(\.text) == ["hello world", "second"])
#expect(
destination.received.allSatisfy {
$0.timestamp == Self.stamp && $0.level == .info && $0.source == .sslocal
}, "intake 产出行:时间戳来自注入时钟,来源固定 sslocal")
}
@Test func hidesPasswordBeforeDeliveringToEveryDestination() {
let first = RecordingDestination()
let second = RecordingDestination()
let intake = makeIntake([first, second])
intake.write(Data("authentication rejected, password: secret-one\n".utf8))
intake.write(Data("password: sec".utf8))
intake.write(Data("ret-two\n".utf8))
let expected = [
"authentication rejected, password: [redacted]",
"password: [redacted]",
]
#expect(first.received.map(\.text) == expected)
#expect(second.received.map(\.text) == expected)
}
@Test func discardsOversizedLineAndKeepsFollowingLines() {
let destination = RecordingDestination()
let intake = makeIntake([destination])
var oversized = Data("password: ".utf8)
oversized.append(Data(String(repeating: "x", count: 70_000).utf8))
intake.write(oversized)
intake.write(Data("secret-tail\nordinary event\n".utf8))
#expect(destination.received.map(\.text) == ["log line truncated", "ordinary event"])
// 截断标记是 info:agent.log 才是持久面,统一日志侧不落盘。
#expect(destination.received.first?.level == .info)
}
@Test func flushesTrailingPartialLineAtEndOfStream() {
let destination = RecordingDestination()
let intake = makeIntake([destination])
intake.write(Data("no trailing newline".utf8))
intake.write(Data())
#expect(destination.received.map(\.text) == ["no trailing newline"])
intake.write(Data())
#expect(destination.received.map(\.text) == ["no trailing newline"])
}
@Test func detectsUpstreamLevelIntoRow() {
let destination = RecordingDestination()
let intake = makeIntake([destination])
intake.write(
Data(
"2026-10-20T03:04:12Z ERROR sslocal] boom\n2026-10-20T03:04:12Z WARN retry\r\nok\n".utf8))
#expect(destination.received.map(\.level) == [.error, .warning, .info])
}
@Test func stderrDestinationRendersUniformRows() throws {
let pipe = Pipe()
let destination = SslocalStandardErrorDestination(handle: pipe.fileHandleForWriting)
destination.receive(
LogLine(timestamp: Self.stamp, level: .warning, source: .sslocal, text: "hello"))
destination.receive(LogLine(timestamp: Self.stamp, level: .info, source: .sslocal, text: ""))
try pipe.fileHandleForWriting.close()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
let stamp = LogLine.timestampFormatter.string(from: Self.stamp)
#expect(
data
== Data(
"\(stamp) [warning] [sslocal] hello\n\(stamp) [info] [sslocal] \n".utf8))
}
}