test: 拆分运行时测试类并缩短健康等待窗口

This commit is contained in:
Qiu Yuzhou
2026-10-04 00:06:47 +08:00
parent 210c677c1b
commit 0235e64ad1
12 changed files with 55 additions and 17 deletions
@@ -37,6 +37,9 @@ extension ProxyRuntimeControllerTests {
return String(bytes: data, encoding: .utf8) ?? ""
}
}
final class CustomRuleControllerTests: ProxyRuntimeControllerTests {
/// 规则内容变化重编译 ACL 并按完整重启路径生效。
func testUpdateCustomRulesRecompilesACLAndRestarts() async throws {
let seeded = try makeSeededCatalog()
@@ -2,7 +2,7 @@ import XCTest
@testable import ShadowsocksX_NG2
extension ProxyRuntimeControllerTests {
final class CustomRuleDeletionControllerTests: ProxyRuntimeControllerTests {
func testDeletionWhileOffPersistsBatchAndDisabledOrphansWithoutStarting() async throws {
let (store, _) = try makeCustomRuleStore()
let rules = [
@@ -4,7 +4,7 @@ import XCTest
/// Verify the editor's public commands against the production commit adapter,
/// using the existing hostless runtime and system-proxy fixtures.
extension ProxyRuntimeControllerTests {
final class CustomRuleEditingControllerTests: ProxyRuntimeControllerTests {
func testEditorSaveWhileOffPersistsWithoutStartingRuntime() async throws {
let (store, _) = try makeCustomRuleStore()
let controller = makeControllerWithCustomRules(
@@ -5,7 +5,7 @@ import XCTest
/// Mode restoration and commands share the Domain availability policy without
/// starting a real runtime or touching host proxy settings.
@MainActor
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeControllerModeTests: ProxyRuntimeControllerTests {
func testRuntimeFactsProjectControllerStateAtStableSeam() async {
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
@@ -26,7 +26,7 @@ extension ProxyRuntimeControllerTests {
// 旧收敛复活;restore 先查时效再动持久层)
@MainActor
extension ProxyRuntimeControllerTests {
extension ProxyRuntimeControllerModeTests {
/// agent-off 落在 mode 切换的健康门 await 窗口内:票据查全四事实 →
/// superseded,不触发 restore 重注册,用户的 mode 意图保留在持久层与内存。
func testAgentOffDuringModeTransitionHealthGateSupersedesWithoutRestore() async throws {
@@ -5,7 +5,7 @@ import XCTest
/// Agent 开关与系统代理开关的拆分语义(issue #60):与既有开关用例同文件
/// 追加(Xcode 测试扫描限制),扩展持有独立用例组。
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeAgentAndSystemProxyTests: ProxyRuntimeControllerTests {
func testEnableWithoutActiveTargetDeploysEmptyServerListening() async throws {
_ = try makeSeededCatalog()
let controller = makeController(
@@ -195,6 +195,9 @@ extension ProxyRuntimeControllerTests {
}
/// 关闭 agent 的级联(issue #71):系统代理意图仍开启时一并持久化为关闭,
/// 先请求清理、再停止监听;之后重启 agent 不会恢复系统代理意图。
}
extension ProxyRuntimeAgentAndSystemProxyTests {
func testAgentOffClearsSystemProxyBeforeStoppingListeningAndCascadesIntentOff() async throws {
let seeded = try makeSeededCatalog()
let settingsStore = InMemoryProxySettingsStore()
@@ -450,7 +453,7 @@ extension ProxyRuntimeControllerTests {
}
}
extension ProxyRuntimeControllerTests {
extension ProxyRuntimeAgentAndSystemProxyTests {
/// 意图已开启时的开启命令是失败态的显式重试入口(issue #60):无需先关再开。
func testAgentEnableCommandRetriesConvergenceWhileIntentAlreadyOn() async throws {
let seeded = try makeSeededCatalog()
@@ -4,7 +4,7 @@ import XCTest
/// 目录提交 → 运行时收敛语义(D3/D5 与 CONTEXT.md「有效值未变的提交不做
/// 运行时收敛」):与主测试类同构扩展,按既有模式独立成文件。
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeCatalogConvergenceTests: ProxyRuntimeControllerTests {
// MARK: 目录重展开消费(D3/D5)
/// 模拟 wrapper 在跑(pid 指向本测试进程,kill(pid, 0) 判活通过)。
@@ -3,7 +3,7 @@ import XCTest
@testable import ShadowsocksX_NG2
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeFirewallTests: ProxyRuntimeControllerTests {
func testLocalhostModeNeverQueriesApplicationFirewall() async throws {
let seeded = try makeSeededCatalog()
let firewall = ProxyRuntimeFixture.FakeFirewallChecker(.blocked)
@@ -5,7 +5,7 @@ import XCTest
/// 全局模式以 ACL 实现(issue #62):proxy_all + 固定本地绕过;无活动服务器
/// 时 agent 仍监听、系统代理保持待应用;ACL 变化完整重启并失败回滚。
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeGlobalModeTests: ProxyRuntimeControllerTests {
func testGlobalModeDeploysProxyAllACLAndProjectsSOCKSProxyWithFixedExceptions() async throws {
let seeded = try makeSeededCatalog()
let settingsStore = InMemoryProxySettingsStore()
@@ -3,7 +3,7 @@ import XCTest
@testable import ShadowsocksX_NG2
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeModeCommandTests: ProxyRuntimeControllerTests {
func testSetProxyModePersistsTheChoiceAndARestoreRestoresIt() async throws {
let settingsStore = InMemoryProxySettingsStore()
let controller = makeController(
@@ -3,7 +3,7 @@ import XCTest
@testable import ShadowsocksX_NG2
extension ProxyRuntimeControllerTests {
final class ProxyRuntimeResyncAndSettingsTests: ProxyRuntimeControllerTests {
// MARK: GUI 重启重同步(D5)
func testResyncWithRegisteredAgentAndMatchingContractSkipsRewrite() async throws {
@@ -7,7 +7,7 @@ import XCTest
/// agent 开关与系统代理开关的独立语义、目录重展开消费与 GUI 重同步重合(不
/// 触真实 SMAppService/launchd/SystemConfiguration)。
@MainActor
final class ProxyRuntimeControllerTests: XCTestCase {
class ProxyRuntimeControllerTests: XCTestCase {
var runtime: ProxyRuntimeFixture.TemporaryRuntime!
var catalogFileURL: URL!
var activationFileURL: URL!
@@ -24,6 +24,10 @@ final class ProxyRuntimeControllerTests: XCTestCase {
private(set) var records: [(pid: Int32, signal: Int32)] = []
/// SIGUSR1 送达后模拟 wrapper 热重载刷新回执。
var reloadReceipt: (() -> Void)?
/// wrapper 进程存活位:生产中注销 LaunchAgent 后 wrapper 随 launchd 停止
/// 消亡,pid 文件留存至 deleteRuntimeFiles、死亡靠 kill(_, 0) 探测;
/// 重注册即拉起新进程。不置死则停止协议会空等退出超时上限。
private var wrapperAlive = true
var signalsSent: [(pid: Int32, signal: Int32)] {
lock.lock()
@@ -37,6 +41,20 @@ final class ProxyRuntimeControllerTests: XCTestCase {
lock.unlock()
}
/// 注销 LaunchAgent:对应 wrapper 进程消亡。
func terminateWrapper() {
lock.lock()
wrapperAlive = false
lock.unlock()
}
/// 重新注册:拉起新 wrapper 进程。
func relaunchWrapper() {
lock.lock()
wrapperAlive = true
lock.unlock()
}
func send(_ pid: Int32, _ signal: Int32) -> Int32 {
record(pid, signal)
if signal == SIGUSR1 {
@@ -45,7 +63,10 @@ final class ProxyRuntimeControllerTests: XCTestCase {
// kill(_, 0) 判活语义:只对夹具约定的存活 pid(42 与本测试进程)报告
// 在跑;陈旧残留 pid 返回 ESRCH,停止协议才不必空等退出超时上限。
if signal == 0 {
return pid == 42 || pid == ProcessInfo.processInfo.processIdentifier
lock.lock()
let alive = wrapperAlive
lock.unlock()
return (pid == 42 && alive) || pid == ProcessInfo.processInfo.processIdentifier
? 0 : Int32(ESRCH)
}
return 0
@@ -63,6 +84,8 @@ final class ProxyRuntimeControllerTests: XCTestCase {
systemProxyHelper = ProxyRuntimeFixture.FakeSystemProxyHelperService()
systemProxyNetworkChangeMonitor = ProxyRuntimeFixture.FakeSystemProxyNetworkChangeMonitor()
signals = SignalRecorder()
// 注销即 wrapper 消亡:停止协议的退出等待据 kill(_, 0) 放行,不空等超时。
agent.onUnregister = { [weak self] in self?.signals?.terminateWrapper() }
}
override func tearDown() async throws {
@@ -102,7 +125,9 @@ final class ProxyRuntimeControllerTests: XCTestCase {
) -> ProxyRuntimeController {
agent.setStatus(agentStatus)
let runtimeFileStore = RuntimeFileStore(fileURL: runtime.contract)
agent.onRegister = { [runtimeFileStore] in
agent.onRegister = { [runtimeFileStore, signals] in
// 重注册=拉起新 wrapper 进程(注销时已随 LaunchAgent 置死)。
signals?.relaunchWrapper()
guard let document = runtimeFileStore.loadDocument() else { return }
try? runtimeFileStore.writeRuntimeReceipt(for: document, processID: 42)
}
@@ -145,6 +170,10 @@ final class ProxyRuntimeControllerTests: XCTestCase {
processIsAlive: processIsAlive)
}
}
/// Concrete suite; the shared fixture must not contain inherited test methods.
final class ProxyRuntimeControllerCoreTests: ProxyRuntimeControllerTests {
// MARK: Agent 开关与首次默认
func testActivateWhileAgentOffPublishesActiveTargetIDWithoutDeploying() async throws {
@@ -2,7 +2,7 @@ import XCTest
@testable import ShadowsocksX_NG2
extension ProxyRuntimeControllerTests {
final class RuleEnablementControllerTests: ProxyRuntimeControllerTests {
func testRapidRuleSavesCoalesceIntoOneLatestACLDeployment() async throws {
let seeded = try makeSeededCatalog()
let (store, _) = try makeCustomRuleStore()
@@ -38,7 +38,7 @@ extension ProxyRuntimeControllerTests {
store: store,
settings: ProxySettings(
listen: ActivationFixture.listen, preferredMode: .rule, agentEnabled: true),
proxyMode: .rule, launchHealthTimeoutSeconds: 2)
proxyMode: .rule, launchHealthTimeoutSeconds: 0.3)
try await controller.activate(seeded.server)
let runtimeStore = RuntimeFileStore(fileURL: runtime.contract)
let started = expectation(description: "first deployment awaiting receipt")
@@ -238,6 +238,9 @@ extension ProxyRuntimeControllerTests {
XCTAssertEqual(agent.registerCount, 0)
}
}
extension RuleEnablementControllerTests {
func testAgentOffDuringRecoveryCannotBeOverwrittenByOldRuleTransaction() async throws {
try await interruptRuleRecovery(switchMode: false)
}
@@ -256,7 +259,7 @@ extension ProxyRuntimeControllerTests {
ruleDefaultAction: .proxyWhenUnmatched, agentEnabled: true)
let controller = makeControllerWithCustomRules(
store: store, settings: settings, proxyMode: .rule,
launchHealthTimeoutSeconds: 1)
launchHealthTimeoutSeconds: 0.2)
try await controller.activate(seeded.server)
let runtimeStore = RuntimeFileStore(fileURL: runtime.contract)
let recoveryStarted = expectation(description: "recovery awaiting health")