Files

168 lines
7.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 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: {})
}
}