mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
单测不再以 app 可执行为宿主:新增 ShadowsocksX-NG2Core 静态库 target 同源 编译 App/(除 @main)与 Domain/,模块名沿用 ShadowsocksX_NG2 使全部测试的 @testable import 零改动;app target 模块改名 ShadowsocksX_NG2App 避免共享 Products 目录 swiftmodule 撞名。两个测试 target 改链 Core 后 xcodegen 不再 生成 TEST_HOST,xcodebuild test 只构建不启动 app——与安装版运行实例零接触 (pgrep 全程验证无 app 进程)。同源双编译沿用 Agent/Helper 共享 Domain 文件 的既定模式,Core 只被测试 target 依赖,发布链不编译它。 随之删除 MainApp.swift 的整套测试宿主脚手架(isUnitTesting/makeTesting/ Noop 服务/Ephemeral 存储,-142 行),app 在测试下不再启动,守卫失去存在 理由。宿主移除暴露三处 Bundle.main 依赖,改为显式缝:控制器新增 appBundle 注入(内置规则快照 BuiltinRuleCatalog 与 LaunchDaemon 清单指纹 launchDaemonPlistData(in:) 均按 bundle 读取,生产默认 .main 不变);XPC listener delegate 期望客户端身份改为可注入(测试按本进程 SecCode 自查)。 测试经 AppArtifact.bundle(沿测试包落盘位置向上查找 app 产物,兼容 TEST_HOST 内嵌与平级两种布局)统一取产物。 665 单测 + 11 冒烟全绿,task build 生产构建不受影响。
147 lines
5.9 KiB
Swift
147 lines
5.9 KiB
Swift
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// Mode restoration and commands share the Domain availability policy without
|
|
/// starting a real runtime or touching host proxy settings.
|
|
@MainActor
|
|
extension ProxyRuntimeControllerTests {
|
|
func testRuntimeFactsProjectControllerStateAtStableSeam() async {
|
|
let controller = makeController(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
|
|
|
XCTAssertEqual(
|
|
controller.runtimeFacts,
|
|
ProxyRuntimeFacts(status: .off, isOn: false))
|
|
|
|
await controller.resyncOnLaunch()
|
|
|
|
XCTAssertEqual(
|
|
controller.runtimeFacts,
|
|
ProxyRuntimeFacts(status: .running, isOn: true),
|
|
"默认 agent 意图开启:重同步后无目标也进入空列表监听")
|
|
}
|
|
|
|
}
|
|
|
|
/// Coordinator/controller integration for the shared committed catalog source.
|
|
@MainActor
|
|
final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase {
|
|
private var runtime: ProxyRuntimeFixture.TemporaryRuntime!
|
|
private var catalogFileURL: URL!
|
|
private var activationFileURL: URL!
|
|
private var credentials: InMemoryCredentialStore!
|
|
private var agent: ProxyRuntimeFixture.FakeLaunchAgent!
|
|
private var systemProxy: ProxyRuntimeFixture.FakeSystemProxy!
|
|
|
|
override func setUp() async throws {
|
|
try await super.setUp()
|
|
runtime = ProxyRuntimeFixture.makeTemporaryRuntime()
|
|
catalogFileURL = runtime.directory.appendingPathComponent("catalog.json")
|
|
activationFileURL = runtime.directory.appendingPathComponent("activation.json")
|
|
credentials = InMemoryCredentialStore()
|
|
agent = ProxyRuntimeFixture.FakeLaunchAgent()
|
|
systemProxy = ProxyRuntimeFixture.FakeSystemProxy()
|
|
}
|
|
|
|
override func tearDown() async throws {
|
|
try? FileManager.default.removeItem(at: runtime.directory)
|
|
try await super.tearDown()
|
|
}
|
|
|
|
func testCommitWithoutActiveTargetFeedsLaterActivationThroughSharedSnapshot() async throws {
|
|
let fileStore = CatalogFileStore(fileURL: catalogFileURL)
|
|
let bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
|
|
let controller = makeController(
|
|
catalogSnapshotReader: bootstrap.catalogSnapshotReader,
|
|
settings: ProxySettings(listen: ActivationFixture.listen, agentEnabled: false))
|
|
let coordinator = CatalogCommitCoordinator(
|
|
fileStore: fileStore,
|
|
runtime: ProxyRuntimeSyncAdapter(controller: controller),
|
|
bootstrap: bootstrap)
|
|
|
|
let server = try coordinator.commit { catalog, _ in
|
|
try ActivationFixture.addPlainServer(
|
|
"提交后激活", in: &catalog, credentials: credentials)
|
|
}
|
|
|
|
XCTAssertEqual(coordinator.syncStatus, .idle, "无目标时目录只发布,不触发运行时")
|
|
XCTAssertNil(controller.activeTargetID)
|
|
try await controller.activate(server)
|
|
|
|
XCTAssertEqual(controller.activeTargetID, server, "激活读取协调器刚发布的唯一快照")
|
|
XCTAssertEqual(controller.state, .off, "agent 意图关闭时该命令仅选择目标,不部署")
|
|
}
|
|
|
|
func testCoordinatorUsesProductionAdapterToClearRemovedActiveTarget() async throws {
|
|
let server = try makeSeededCatalog()
|
|
try ActivationStateFileStore(fileURL: activationFileURL).save(activeTargetID: server)
|
|
let fileStore = CatalogFileStore(fileURL: catalogFileURL)
|
|
let bootstrap = CatalogCommitCoordinator.bootstrap(fileStore: fileStore)
|
|
let controller = makeController(
|
|
catalogSnapshotReader: bootstrap.catalogSnapshotReader,
|
|
probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
|
let coordinator = CatalogCommitCoordinator(
|
|
fileStore: fileStore,
|
|
runtime: ProxyRuntimeSyncAdapter(controller: controller),
|
|
bootstrap: bootstrap)
|
|
|
|
_ = try coordinator.commit { catalog, _ in try catalog.remove(server) }
|
|
|
|
await waitUntilRuntimeSettles(
|
|
{
|
|
if case .finished = coordinator.syncStatus { return true }
|
|
return false
|
|
}())
|
|
|
|
guard case .finished(_, .clearedAndStopped) = coordinator.syncStatus else {
|
|
return XCTFail("生产 adapter 应以刚提交快照清理无效目标")
|
|
}
|
|
XCTAssertNil(controller.activeTargetID)
|
|
XCTAssertNil(
|
|
try ActivationStateFileStore(fileURL: activationFileURL).loadActiveTargetID(),
|
|
"adapter 收敛后持久化清除失效目标")
|
|
}
|
|
|
|
private func makeController(
|
|
catalogSnapshotReader: RuntimeCatalogSnapshotReading,
|
|
settings: ProxySettings? = nil,
|
|
probe: EndpointProbing = SystemEndpointProbe()
|
|
) -> ProxyRuntimeController {
|
|
let runtimeFileStore = RuntimeFileStore(fileURL: runtime.contract)
|
|
agent.onRegister = { [runtimeFileStore] in
|
|
guard let document = runtimeFileStore.loadDocument() else { return }
|
|
try? runtimeFileStore.writeRuntimeReceipt(for: document, processID: 42)
|
|
}
|
|
return ProxyRuntimeController(
|
|
catalogSnapshotReader: catalogSnapshotReader,
|
|
activationFileStore: ActivationStateFileStore(fileURL: activationFileURL),
|
|
runtimeFileStore: runtimeFileStore,
|
|
credentials: credentials,
|
|
plugins: ActivationFixture.plugins,
|
|
listenRestore: RestoredListenSettings(
|
|
settings: ActivationFixture.listen, unreadableError: nil),
|
|
settingsStore: InMemoryProxySettingsStore(),
|
|
appBundle: AppArtifact.bundle,
|
|
settingsRestore: RestoredProxySettings(
|
|
settings: settings ?? ProxySettings(listen: ActivationFixture.listen),
|
|
unreadableError: nil),
|
|
agent: agent,
|
|
probe: probe,
|
|
systemProxy: systemProxy,
|
|
systemProxyHelper: ProxyRuntimeFixture.FakeSystemProxyHelperService(),
|
|
firewallExecutableURLs: [URL(fileURLWithPath: "/bundle/Helpers/sslocal")],
|
|
firewallPollIntervalNanoseconds: 1_000_000,
|
|
launchHealthTimeoutSeconds: 0.05,
|
|
sendSignal: { _, _ in 0 },
|
|
processIsAlive: { $0 == 42 })
|
|
}
|
|
|
|
private func makeSeededCatalog() throws -> NodeID {
|
|
var catalog = ConfigurationCatalog()
|
|
let server = try ActivationFixture.addPlainServer(
|
|
"待删除", in: &catalog, credentials: credentials)
|
|
try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog))
|
|
return server
|
|
}
|
|
}
|