Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/Connectivity/ConnectivityMeasurementTests.swift
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

141 lines
5.5 KiB
Swift

import Foundation
import Testing
@testable import ShadowsocksX_NG2
@MainActor
struct ConnectivityMeasurementTests {
@Test func selectsFastestSuccessfulIPAndSamplesItsMedian() async throws {
var addresses: [String] = []
var limits: [Double] = []
let values = [90, 20, 200, 40, 60]
let module = ConnectivityMeasurement(
resolve: { _, _, _ in ["192.0.2.1", "192.0.2.2"] },
request: { _, address, _, timeout in
let index = addresses.count
addresses.append(address)
limits.append(timeout)
return ConnectivityHTTPObservation(
outcome: .success(milliseconds: values[index]),
totalMilliseconds: Double(values[index]))
})
let result = await module.measure(
url: URL(string: "http://target.invalid/check?token=private")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(30)))
#expect(result.outcome == .success(milliseconds: 60))
#expect(result.selectedIP == "192.0.2.2")
#expect(addresses == ["192.0.2.1", "192.0.2.2", "192.0.2.2", "192.0.2.2", "192.0.2.2"])
#expect(limits == [3, 3, 5, 5, 5])
#expect(result.samples.count == 3)
}
@Test(arguments: [true, false])
func requiresTwoSuccessesAndAveragesTwoSamples(twoSuccesses: Bool) async {
var calls = 0
let module = ConnectivityMeasurement(
resolve: { _, _, _ in ["192.0.2.1"] },
request: { _, _, _, _ in
defer { calls += 1 }
if calls == 2 || (!twoSuccesses && calls == 3) {
return ConnectivityHTTPObservation(outcome: .requestTimeout, failureStage: "first_byte")
}
return ConnectivityHTTPObservation(
outcome: .success(milliseconds: 10), totalMilliseconds: calls == 1 ? 10 : 30)
})
let result = await module.measure(
url: URL(string: "http://target.invalid/")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(30)))
#expect(calls == 4)
#expect(result.outcome == (twoSuccesses ? .success(milliseconds: 20) : .requestTimeout))
#expect(result.trace.filter { $0.phase == .sample }.count == 3)
#expect(result.trace.last?.status == (twoSuccesses ? .success : .insufficientSamples))
}
@Test func boundsCandidatesPerFamilyAndDoesNotSelectFailedResponses() async {
var addresses: [String] = []
let module = ConnectivityMeasurement(
resolve: { _, _, _ in
[
"not-an-ip", "192.0.2.1", "192.0.2.1", "192.0.2.2", "192.0.2.3",
"2001:db8::1", "2001:db8::2", "2001:db8::3",
]
},
request: { _, address, _, _ in
addresses.append(address)
return ConnectivityHTTPObservation(
outcome: address == "2001:db8::2" ? .success(milliseconds: 10) : .http(302),
totalMilliseconds: 10)
})
let result = await module.measure(
url: URL(string: "http://target.invalid/")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(30)))
#expect(Array(addresses.prefix(4)) == ["192.0.2.1", "192.0.2.2", "2001:db8::1", "2001:db8::2"])
#expect(Array(addresses.suffix(3)) == Array(repeating: "2001:db8::2", count: 3))
#expect(result.outcome == .success(milliseconds: 10))
}
@Test func expiredPreparationDoesNotRequestAndRecordsItsFailureStage() async {
var calls = 0
let module = ConnectivityMeasurement(
resolve: { _, _, _ in ["192.0.2.1"] },
request: { _, _, _, _ in
calls += 1
return ConnectivityHTTPObservation()
})
let result = await module.measure(
url: URL(string: "http://target.invalid/")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(-1)))
#expect(result.outcome == .preparationTimeout)
#expect(calls == 0)
#expect(result.trace.last?.status == .timeout)
#expect(result.trace.last?.failureStage == .selection)
}
@Test func literalTargetSkipsDNSAndLogsNeverContainURLSecrets() async {
var resolutions = 0
let module = ConnectivityMeasurement(
resolve: { _, _, _ in
resolutions += 1
return []
},
request: { _, _, _, _ in
ConnectivityHTTPObservation(outcome: .success(milliseconds: 15), totalMilliseconds: 15)
})
let result = await module.measure(
url: URL(string: "http://192.0.2.1/private?token=SECRET")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(30)))
#expect(resolutions == 0)
#expect(result.outcome == .success(milliseconds: 15))
let lines = result.trace.map { RuntimeLogEvent.connectivityMeasurement(id: UUID(), entry: $0) }
#expect(lines.allSatisfy { $0.diagnosticReportLine == nil })
#expect(lines.allSatisfy { $0.standardErrorLine() == nil })
#expect(!lines.map(\.description).joined().contains("SECRET"))
#expect(!lines.map(\.description).joined().contains("private"))
}
@Test func cancellationDuringCandidateDoesNotStartMoreRequests() async {
let gate = AsyncGate()
var calls = 0
let module = ConnectivityMeasurement(
resolve: { _, _, _ in ["192.0.2.1", "192.0.2.2"] },
request: { _, _, _, _ in
calls += 1
await gate.wait()
return ConnectivityHTTPObservation(
outcome: .success(milliseconds: 10), totalMilliseconds: 10)
})
let task = Task {
await module.measure(
url: URL(string: "http://target.invalid/")!, proxyPort: 1234,
preparationDeadline: .now.advanced(by: .seconds(30)))
}
while calls == 0 { await Task.yield() }
task.cancel()
gate.release()
let result = await task.value
#expect(calls == 1)
#expect(result.samples.isEmpty)
#expect(result.trace.last?.status == .cancelled)
}
}