mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
191 lines
6.3 KiB
Swift
191 lines
6.3 KiB
Swift
import Foundation
|
|
import Testing
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
@MainActor
|
|
struct ServerConnectivityTests {
|
|
@Test func repeatedRequestsShareWorkAndCoolingDoesNotAddHistory() async {
|
|
let id = NodeID(rawValue: UUID().uuidString)
|
|
var calls = 0
|
|
var now = Date(timeIntervalSince1970: 100)
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in .fixture }, now: { now },
|
|
measure: { _, _, _ in
|
|
calls += 1
|
|
await Task.yield()
|
|
return .success(milliseconds: 42)
|
|
})
|
|
let batch = module.test([id, id])
|
|
_ = module.test([id])
|
|
await module.wait(for: batch)
|
|
#expect(calls == 1)
|
|
#expect(module.history(for: id).count == 1)
|
|
let skipped = module.test([id])
|
|
#expect(module.batch(skipped)?.cooling == [id])
|
|
#expect(module.history(for: id).count == 1)
|
|
now.addTimeInterval(10)
|
|
await module.wait(for: module.test([id]))
|
|
#expect(calls == 2)
|
|
}
|
|
|
|
@Test func overlappingBatchesShareOneSlotUntilCleanupCompletes() async {
|
|
let ids = (0..<7).map { _ in NodeID.fresh() }
|
|
let gate = AsyncGate()
|
|
var active = 0
|
|
var peak = 0
|
|
var calls = 0
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in .fixture },
|
|
measure: { _, _, _ in
|
|
active += 1
|
|
calls += 1
|
|
peak = max(peak, active)
|
|
await gate.wait()
|
|
active -= 1
|
|
return .success(milliseconds: 12)
|
|
})
|
|
let firstTarget = module.test(Array(ids.prefix(5)))
|
|
let secondTarget = module.test(Array(ids.suffix(5)))
|
|
for _ in 0..<100 { await Task.yield() }
|
|
#expect(calls == 1)
|
|
#expect(module.testing.count == 7)
|
|
gate.release()
|
|
await module.wait(for: firstTarget)
|
|
await module.wait(for: secondTarget)
|
|
#expect(peak == 1)
|
|
#expect(calls == 7)
|
|
#expect(ids.allSatisfy { module.history(for: $0).count == 1 })
|
|
}
|
|
|
|
@Test func credentialValueChangesSuppressLateResultsAndPreserveCooldown() async {
|
|
let id = NodeID.fresh()
|
|
var server = SslocalServerDocument.fixture
|
|
let gate = AsyncGate()
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in server },
|
|
measure: { _, _, _ in
|
|
await gate.wait()
|
|
return .success(milliseconds: 9)
|
|
})
|
|
let batch = module.test([id])
|
|
await Task.yield()
|
|
server = SslocalServerDocument(
|
|
id: "test", server: "example.com", serverPort: 443,
|
|
password: "changed", method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
|
|
module.configurationsChanged()
|
|
gate.release()
|
|
await module.wait(for: batch)
|
|
#expect(module.history(for: id).isEmpty)
|
|
#expect(module.isCooling(id))
|
|
module.setURL(URL(string: "https://example.com/check")!)
|
|
#expect(module.isCooling(id))
|
|
}
|
|
|
|
@Test func latestFailureReplacesSuccessAndHistoryIsBounded() async {
|
|
let id = NodeID.fresh()
|
|
var now = Date(timeIntervalSince1970: 100)
|
|
var outcome = ServerConnectivity.Outcome.success(milliseconds: 11)
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in .fixture }, now: { now },
|
|
measure: { _, _, _ in outcome })
|
|
await module.wait(for: module.test([id]))
|
|
outcome = .http(302)
|
|
for _ in 0..<12 {
|
|
now.addTimeInterval(10)
|
|
await module.wait(for: module.test([id]))
|
|
}
|
|
#expect(module.history(for: id).count == 10)
|
|
#expect(module.history(for: id).first?.outcome == .http(302))
|
|
let summary = module.summary(for: [id], at: now)
|
|
#expect(summary.hasResults)
|
|
#expect(summary.record == nil)
|
|
#expect(!summary.isTesting)
|
|
}
|
|
|
|
@Test func freshResultsBeatStaleAndTiesUseCatalogOrder() async {
|
|
let ids = (0..<3).map { _ in NodeID.fresh() }
|
|
var now = Date(timeIntervalSince1970: 100)
|
|
var latency = 1
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in .fixture }, now: { now },
|
|
measure: { _, _, _ in .success(milliseconds: latency) })
|
|
await module.wait(for: module.test([ids[0]]))
|
|
now.addTimeInterval(300)
|
|
latency = 20
|
|
await module.wait(for: module.test(Array(ids.suffix(2))))
|
|
#expect(module.summary(for: ids, at: now).serverID == ids[1])
|
|
module.networkChanged()
|
|
#expect(module.summary(for: ids, at: now).serverID == ids[0])
|
|
#expect(module.summary(for: ids, at: now).record?.isExpired(at: now) == true)
|
|
module.setURL(URL(string: "https://example.com/new")!)
|
|
#expect(!module.summary(for: ids, at: now).hasResults)
|
|
}
|
|
|
|
@Test func urlAndNetworkChangesSuppressInFlightAndShutdownDrainsQueuedWork() async {
|
|
let ids = (0..<5).map { _ in NodeID.fresh() }
|
|
let gate = AsyncGate()
|
|
let module = ServerConnectivity(
|
|
configuration: { _ in .fixture },
|
|
measure: { _, _, _ in
|
|
await gate.wait()
|
|
return .success(milliseconds: 9)
|
|
})
|
|
let batch = module.test(ids)
|
|
await Task.yield()
|
|
module.networkChanged()
|
|
let shutdown = Task { await module.shutdown() }
|
|
await Task.yield()
|
|
#expect(!module.testing.isEmpty)
|
|
gate.release()
|
|
await shutdown.value
|
|
await module.wait(for: batch)
|
|
#expect(module.testing.isEmpty)
|
|
#expect(ids.allSatisfy { module.history(for: $0).isEmpty && module.isCooling($0) })
|
|
}
|
|
|
|
@Test func queuedConfigurationIsRecheckedBeforeLaunching() async {
|
|
let ids = (0..<4).map { _ in NodeID.fresh() }
|
|
let gate = AsyncGate()
|
|
var changed = false
|
|
var calls = 0
|
|
let module = ServerConnectivity(
|
|
configuration: { id in
|
|
if changed && id == ids[3] { throw ServerFormLoadError.notFound }
|
|
return .fixture
|
|
},
|
|
measure: { _, _, _ in
|
|
calls += 1
|
|
await gate.wait()
|
|
return .success(milliseconds: 5)
|
|
})
|
|
let batch = module.test(ids)
|
|
for _ in 0..<100 { await Task.yield() }
|
|
changed = true
|
|
gate.release()
|
|
await module.wait(for: batch)
|
|
#expect(calls == 3)
|
|
#expect(module.history(for: ids[3]).isEmpty)
|
|
#expect(module.isCooling(ids[3]))
|
|
}
|
|
|
|
@Test func urlValidationAllowsHTTPOnly() {
|
|
#expect(ServerConnectivity.validURL("http://example.com/check") != nil)
|
|
#expect(ServerConnectivity.validURL("https://example.com/check?q=secret") == nil)
|
|
for value in [
|
|
"file:///tmp/check", "ftp://example.com", "https:///", "example.com", "http://x/#fragment",
|
|
] {
|
|
#expect(ServerConnectivity.validURL(value) == nil)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension SslocalServerDocument {
|
|
fileprivate static var fixture: Self {
|
|
Self(
|
|
id: "test", server: "example.com", serverPort: 443, password: "secret",
|
|
method: "aes-256-gcm", plugin: nil, pluginOpts: nil)
|
|
}
|
|
}
|