mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:44:50 +00:00
168 lines
7.2 KiB
Swift
168 lines
7.2 KiB
Swift
import XCTest
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
/// 系统代理观察机 interface 驱动测试(ADR-0022 策略机抽取后):不经控制器
|
||
/// 夹具,直接以三条缝的替身驱动 cleanup-until-quiet 循环与操作互斥。
|
||
@MainActor
|
||
final class SystemProxyObserverTests: XCTestCase {
|
||
func testCleanupRescansWhileNetworkKeepsChangingThenGoesQuiet() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
|
||
let observer = makeObserver(
|
||
proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000)
|
||
// 第一次 clear 期间到达的网络变化触发整轮重扫;随后安静,循环收敛。
|
||
var emittedChange = false
|
||
proxy.onClear = {
|
||
guard !emittedChange else { return }
|
||
emittedChange = true
|
||
monitor.emit([.networkConfiguration, .proxyConfiguration])
|
||
}
|
||
|
||
let outcome = await observer.clearAndStopSystemProxyObservation()
|
||
|
||
XCTAssertEqual(outcome, .idle)
|
||
XCTAssertEqual(proxy.clearCount, 2, "清理期内的网络变化必须触发整轮重扫")
|
||
XCTAssertTrue(emittedChange)
|
||
XCTAssertEqual(monitor.startCount, 1)
|
||
XCTAssertEqual(monitor.stopCount, 1)
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .stopped)
|
||
XCTAssertEqual(observer.systemProxyState, .idle)
|
||
XCTAssertFalse(observer.systemProxyInspection.isBusy)
|
||
}
|
||
|
||
func testConvergeWaitsForInFlightOperationInsteadOfSkipping() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
|
||
// 拉长首次读服务的窗口,制造进行中的操作。
|
||
proxy.beforeRead = { _ = try? await Task.sleep(nanoseconds: 30_000_000) }
|
||
|
||
let first = Task { await observer.convergeSystemProxy(forceApply: true) }
|
||
try await Task.sleep(nanoseconds: 10_000_000)
|
||
await observer.convergeSystemProxy(forceApply: true)
|
||
await first.value
|
||
|
||
XCTAssertEqual(proxy.applied.count, 2, "并发收敛必须等操作收尾后执行而非被跳过")
|
||
}
|
||
|
||
func testEnableIntentAppliesFreshAndStartsObservation() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
|
||
let observer = makeObserver(proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000)
|
||
|
||
await observer.enableSystemProxyIntent()
|
||
|
||
XCTAssertEqual(proxy.applied.count, 1, "开启意图必须真实应用一次(清理期可能已清空系统设置)")
|
||
XCTAssertEqual(observer.systemProxyState, .applied)
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
|
||
XCTAssertEqual(monitor.startCount, 1)
|
||
}
|
||
|
||
func testDisableIntentClearsStopsObservationAndPublishesOutcome() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
|
||
let observer = makeObserver(proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000)
|
||
await observer.enableSystemProxyIntent()
|
||
|
||
await observer.disableSystemProxyIntent()
|
||
|
||
XCTAssertEqual(proxy.clearCount, 1)
|
||
XCTAssertEqual(observer.systemProxyState, .idle)
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .stopped)
|
||
XCTAssertEqual(monitor.stopCount, 1)
|
||
XCTAssertFalse(observer.systemProxyApprovalRequired)
|
||
}
|
||
|
||
func testAgentStopPresentationFollowsProxyIntent() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let monitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
|
||
let intent = IntentState(enabled: true)
|
||
let observer = makeObserver(
|
||
proxy: proxy, monitor: monitor, cleanupSettleNanoseconds: 1_000_000,
|
||
isIntentEnabled: { intent.enabled })
|
||
|
||
observer.agentDidStop()
|
||
XCTAssertEqual(observer.systemProxyState, .pending, "代理意图仍在开时,agent 停机保持待应用(issue #71)")
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .stopped)
|
||
XCTAssertEqual(monitor.stopCount, 1)
|
||
|
||
intent.enabled = false
|
||
observer.agentDidStop()
|
||
XCTAssertEqual(observer.systemProxyState, .idle, "代理意图已关时,agent 停机呈现空闲")
|
||
}
|
||
|
||
func testModeTransitionAwaitingRuntimeStampsPendingOnlyUnderIntent() async throws {
|
||
let intent = IntentState(enabled: false)
|
||
let observer = makeObserver(
|
||
proxy: ProxyRuntimeFixture.FakeSystemProxy(), cleanupSettleNanoseconds: 1_000_000,
|
||
isIntentEnabled: { intent.enabled })
|
||
|
||
observer.modeTransitionAwaitingRuntime()
|
||
XCTAssertEqual(observer.systemProxyState, .idle, "代理意图关闭时不打待应用图章")
|
||
|
||
intent.enabled = true
|
||
observer.modeTransitionAwaitingRuntime()
|
||
XCTAssertEqual(observer.systemProxyState, .pending)
|
||
}
|
||
|
||
func testSettleLaunchAfterConvergedInspectsWithoutReapplying() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
|
||
|
||
await observer.settleLaunchAfterAgentConverged()
|
||
|
||
XCTAssertEqual(proxy.applied.count, 0, "健康的 GUI 启动只巡检,不强推应用")
|
||
XCTAssertGreaterThanOrEqual(proxy.readCount, 1)
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
|
||
XCTAssertFalse(observer.systemProxyStartupInProgress)
|
||
// 播种 lastDesired 后,后续收敛(如网络变化回调)不得因相等而重新应用。
|
||
await observer.convergeSystemProxy()
|
||
XCTAssertEqual(proxy.applied.count, 0, "播种 lastDesired 后收敛不得重新应用")
|
||
}
|
||
|
||
func testStartupSuppressionBlocksConvergeUntilSettled() async throws {
|
||
let proxy = ProxyRuntimeFixture.FakeSystemProxy()
|
||
let observer = makeObserver(proxy: proxy, cleanupSettleNanoseconds: 1_000_000)
|
||
observer.beginStartupResync()
|
||
|
||
await observer.convergeSystemProxy(forceApply: true)
|
||
XCTAssertEqual(proxy.applied.count, 0, "launch 重同步抑制期 converge 必须被拒绝")
|
||
|
||
await observer.settleLaunchWithAgentOff()
|
||
XCTAssertFalse(observer.systemProxyStartupInProgress)
|
||
XCTAssertEqual(observer.systemProxyState, .paused, "抑制解除后按意图挂起残留配置")
|
||
XCTAssertEqual(observer.systemProxyObservationMode, .enabled)
|
||
}
|
||
|
||
@MainActor
|
||
private final class IntentState {
|
||
var enabled: Bool
|
||
|
||
init(enabled: Bool) { self.enabled = enabled }
|
||
}
|
||
|
||
private func makeObserver(
|
||
proxy: ProxyRuntimeFixture.FakeSystemProxy,
|
||
monitor: SystemProxyNetworkChangeMonitoring =
|
||
ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor(),
|
||
cleanupSettleNanoseconds: UInt64,
|
||
isIntentEnabled: @escaping @MainActor () -> Bool = { true }
|
||
) -> SystemProxyObserver {
|
||
SystemProxyObserver(
|
||
systemProxy: proxy,
|
||
systemProxyHelper: ProxyRuntimeFixture.FakeSystemProxyHelperService(),
|
||
systemProxyNetworkChangeMonitor: monitor,
|
||
appBundle: AppArtifact.bundle,
|
||
helperRefreshDelayNanoseconds: 1_000_000,
|
||
cleanupSettleNanoseconds: cleanupSettleNanoseconds,
|
||
isIntentEnabled: isIntentEnabled,
|
||
isExitAvailable: { true },
|
||
desiredConfiguration: {
|
||
ProxyMode.direct.systemProxyConfiguration(
|
||
for: SslocalRuntimeDocument(servers: [], listen: ProxyListeningConfiguration()),
|
||
exceptions: [])
|
||
},
|
||
systemProxyHealthObservationLoop: {})
|
||
}
|
||
}
|