mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-28 13:13:47 +00:00
refactor(tests): drop TEST_HOST by linking tests against app-source static lib
单测不再以 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 生产构建不受影响。
This commit is contained in:
@@ -63,11 +63,10 @@ struct ShadowsocksXNG2App: App {
|
||||
}
|
||||
}
|
||||
|
||||
/// 随窗激活策略的真实 NSApp 落点(hermetic:单测 host 不触碰激活策略)。
|
||||
/// 随窗激活策略的真实 NSApp 落点。
|
||||
@MainActor
|
||||
private final class NSAppWindowActivationApplier: WindowActivationPolicyApplying {
|
||||
func apply(_ policy: NSApplication.ActivationPolicy) {
|
||||
guard !ApplicationDependencies.isUnitTesting else { return }
|
||||
NSApp.setActivationPolicy(policy)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +82,6 @@ private struct StatusMenuLabel: View {
|
||||
Image(systemName: "network")
|
||||
.accessibilityLabel("ShadowsocksX-NG2")
|
||||
.task {
|
||||
guard !ApplicationDependencies.isUnitTesting else { return }
|
||||
await control.resyncOnLaunch()
|
||||
}
|
||||
}
|
||||
@@ -196,10 +194,8 @@ private struct AppComposition {
|
||||
}
|
||||
}
|
||||
|
||||
/// The test host still loads the app executable because this target is a macOS
|
||||
/// SwiftUI application. Its bootstrap must nevertheless be hermetic: starting
|
||||
/// XCTest must not read the user's catalog, settings, activation state, Legacy
|
||||
/// defaults, login-item registration, runtime files, or production Keychain.
|
||||
/// 组合根依赖集(仅生产形态):单测不再以本 app 为宿主(去宿主化,ADR 0021),
|
||||
/// 装配永远面向真实存储、系统服务与用户默认域。
|
||||
@MainActor
|
||||
private struct ApplicationDependencies {
|
||||
let credentials: CredentialStoring
|
||||
@@ -217,18 +213,7 @@ private struct ApplicationDependencies {
|
||||
let diagnosticReportExporter: any DiagnosticReportExporter
|
||||
let configurationGroupFileExporter: any ConfigurationGroupFileExporter
|
||||
|
||||
static var isUnitTesting: Bool {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
return environment["XCTestConfigurationFilePath"] != nil
|
||||
|| environment["XCTestSessionIdentifier"] != nil
|
||||
}
|
||||
|
||||
static func make() -> ApplicationDependencies {
|
||||
guard isUnitTesting else { return makeProduction() }
|
||||
return makeTesting()
|
||||
}
|
||||
|
||||
private static func makeProduction() -> ApplicationDependencies {
|
||||
// ADR 0017 初版文件形态的迁移(发布链从未包含,仅存量开发机):文件不存在
|
||||
// 即空操作,随每次启动调用无害。
|
||||
SilentLaunchStore.migrateLegacyFileIfPresent(
|
||||
@@ -257,97 +242,4 @@ private struct ApplicationDependencies {
|
||||
configurationGroupFileExporter: AppKitConfigurationGroupFileExporter())
|
||||
}
|
||||
|
||||
private static func makeTesting() -> ApplicationDependencies {
|
||||
let directory = FileManager.default.temporaryDirectory
|
||||
.appendingPathComponent("ShadowsocksX-NG2-test-host-\(UUID().uuidString)", isDirectory: true)
|
||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
|
||||
// 测试宿主不得触碰生产 defaults 域:走独立套件并在装配时清空(封闭性公约)。
|
||||
let silentLaunchSuite = "ShadowsocksX-NG2-test-host-silent-launch"
|
||||
UserDefaults.standard.removePersistentDomain(forName: silentLaunchSuite)
|
||||
let silentLaunchStore = SilentLaunchStore(
|
||||
defaults: UserDefaults(suiteName: silentLaunchSuite)!)
|
||||
|
||||
let credentials = EphemeralCredentialStore()
|
||||
let catalogFileStore = CatalogFileStore(
|
||||
fileURL: directory.appendingPathComponent("catalog.json"))
|
||||
let settingsStore = ProxySettingsFileStore(
|
||||
fileURL: directory.appendingPathComponent("settings.json"),
|
||||
legacyListenFileURL: directory.appendingPathComponent("listen-settings.json"))
|
||||
let restoredSettings = ProxySettingsFileStore.restored(store: settingsStore)
|
||||
let marker = EphemeralLegacyImportMarkerStore()
|
||||
let legacyImportService = LegacyImportService(
|
||||
source: EmptyLegacySnapshotProvider(),
|
||||
catalogStore: catalogFileStore,
|
||||
credentials: credentials,
|
||||
marker: marker)
|
||||
|
||||
return ApplicationDependencies(
|
||||
credentials: credentials,
|
||||
catalogFileStore: catalogFileStore,
|
||||
activationFileStore: ActivationStateFileStore(
|
||||
fileURL: directory.appendingPathComponent("activation.json")),
|
||||
runtimeFileStore: RuntimeFileStore(
|
||||
fileURL: directory.appendingPathComponent("sslocal-active.json")),
|
||||
settingsStore: settingsStore,
|
||||
settingsRestore: restoredSettings,
|
||||
listenRestore: RestoredListenSettings(
|
||||
settings: restoredSettings.settings.listen, unreadableError: nil),
|
||||
legacyImportService: legacyImportService,
|
||||
launchAgent: NoopLaunchAgentService(),
|
||||
loginService: NoopLaunchAtLoginService(),
|
||||
silentLaunchStore: silentLaunchStore,
|
||||
textClipboard: InMemoryTextClipboard(),
|
||||
diagnosticReportExporter: InMemoryDiagnosticReportExporter(),
|
||||
configurationGroupFileExporter: InMemoryConfigurationGroupFileExporter())
|
||||
}
|
||||
}
|
||||
|
||||
private final class EphemeralCredentialStore: CredentialStoring {
|
||||
private var values: [CredentialReference: String] = [:]
|
||||
private let lock = NSLock()
|
||||
|
||||
func save(_ secret: String, for reference: CredentialReference) throws {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
values[reference] = secret
|
||||
}
|
||||
|
||||
func secret(for reference: CredentialReference) throws -> String? {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return values[reference]
|
||||
}
|
||||
|
||||
func delete(_ reference: CredentialReference) throws {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
values.removeValue(forKey: reference)
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptyLegacySnapshotProvider: LegacySnapshotProviding {
|
||||
func readSnapshot() throws -> LegacySnapshot? { nil }
|
||||
}
|
||||
|
||||
private final class EphemeralLegacyImportMarkerStore: LegacyImportMarkerStoring {
|
||||
private var completed = false
|
||||
|
||||
func isCompleted() throws -> Bool { completed }
|
||||
|
||||
func setCompleted(_ completed: Bool) throws {
|
||||
self.completed = completed
|
||||
}
|
||||
}
|
||||
|
||||
private struct NoopLaunchAgentService: LaunchAgentControlling {
|
||||
var status: LaunchAgentStatus { .notRegistered }
|
||||
func register() throws {}
|
||||
func unregister() throws {}
|
||||
}
|
||||
|
||||
private struct NoopLaunchAtLoginService: LaunchAtLoginControlling {
|
||||
var status: LoginItemStatus { .notRegistered }
|
||||
func register() throws {}
|
||||
func unregister() throws {}
|
||||
}
|
||||
|
||||
@@ -133,15 +133,16 @@ extension ProxyRuntimeController {
|
||||
/// (issue #63/#64)。快照缺失/损坏/版本不匹配时抛错,调用方必须失败
|
||||
/// 并保留旧 ACL,不得静默退化成全局(issue #63 AC4)。
|
||||
func chinaDirectRules() throws -> [ProxyRule] {
|
||||
let geolocation = try BuiltinRuleCatalog.loadGeolocationCN()
|
||||
let chinaIPv4 = try BuiltinRuleCatalog.loadChinaIPv4()
|
||||
let geolocation = try BuiltinRuleCatalog.loadGeolocationCN(from: appBundle)
|
||||
let chinaIPv4 = try BuiltinRuleCatalog.loadChinaIPv4(from: appBundle)
|
||||
return BuiltinRuleCatalog.chinaDirectRules(from: [geolocation, chinaIPv4])
|
||||
}
|
||||
|
||||
/// GFWList 候选(issue #65):可准确表达且未被更宽代理规则遮蔽的规则
|
||||
/// (含未遮蔽例外),参与 `bypass_all` ACL 编译。
|
||||
func gfwlistRules() throws -> [ProxyRule] {
|
||||
BuiltinRuleCatalog.gfwlistRules(from: try BuiltinRuleCatalog.loadGFWList())
|
||||
BuiltinRuleCatalog.gfwlistRules(
|
||||
from: try BuiltinRuleCatalog.loadGFWList(from: appBundle))
|
||||
}
|
||||
|
||||
private func deployModeTransition(
|
||||
|
||||
@@ -149,7 +149,7 @@ extension ProxyRuntimeController {
|
||||
private func ensureHelperAvailableForApply() async -> Bool {
|
||||
switch systemProxyHelper.status {
|
||||
case .approved:
|
||||
if Self.helperRegistrationDrifted() {
|
||||
if helperRegistrationDrifted() {
|
||||
do {
|
||||
try systemProxyHelper.unregister()
|
||||
// launchd 对节流中(spawn scheduled)job 的移除是异步的:立即重注
|
||||
@@ -157,7 +157,7 @@ extension ProxyRuntimeController {
|
||||
// runtime(10s)节流窗再重注;间隔内收敛以旧定义失败呈超时。
|
||||
try? await Task.sleep(nanoseconds: helperRefreshDelayNanoseconds)
|
||||
try systemProxyHelper.register()
|
||||
Self.storeHelperRegistrationStamp()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
// 刷新失败不记指纹,下次启动重试;状态退回后由下方分支接手。
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error)))
|
||||
@@ -170,7 +170,7 @@ extension ProxyRuntimeController {
|
||||
case .notRegistered:
|
||||
do {
|
||||
try systemProxyHelper.register()
|
||||
Self.storeHelperRegistrationStamp()
|
||||
storeHelperRegistrationStamp()
|
||||
} catch {
|
||||
RuntimeLog.emit(.systemProxyHelperRegisterFailed(detail: describe(error)))
|
||||
}
|
||||
@@ -185,14 +185,16 @@ extension ProxyRuntimeController {
|
||||
return false
|
||||
}
|
||||
|
||||
private static func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool {
|
||||
private func helperRegistrationDrifted(defaults: UserDefaults = .standard) -> Bool {
|
||||
SystemProxyHelperRegistrationStamp.drifted(
|
||||
plistData: SystemProxyHelperIdentity.launchDaemonPlistData,
|
||||
plistData: SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle),
|
||||
storedStamp: defaults.string(forKey: SystemProxyHelperRegistrationStamp.defaultsKey))
|
||||
}
|
||||
|
||||
private static func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) {
|
||||
guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData else { return }
|
||||
private func storeHelperRegistrationStamp(defaults: UserDefaults = .standard) {
|
||||
guard let plistData = SystemProxyHelperIdentity.launchDaemonPlistData(in: appBundle) else {
|
||||
return
|
||||
}
|
||||
defaults.set(
|
||||
SystemProxyHelperRegistrationStamp.stamp(plistData),
|
||||
forKey: SystemProxyHelperRegistrationStamp.defaultsKey)
|
||||
|
||||
@@ -94,6 +94,10 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
let systemProxyNetworkChangeMonitor: SystemProxyNetworkChangeMonitoring
|
||||
let firewallChecker: FirewallStatusChecking
|
||||
let firewallExecutableURLs: [URL]
|
||||
/// 宿主 app bundle 缝(生产为 Bundle.main):内置规则快照与 LaunchDaemon
|
||||
/// 清单等打包资源的定位基准。去宿主化(ADR 0021)后单测运行于 xctest
|
||||
/// runner,`Bundle.main` 不再是 app,经此缝注入构建产物 bundle。
|
||||
let appBundle: Bundle
|
||||
let firewallPollIntervalNanoseconds: UInt64
|
||||
let launchHealthTimeoutSeconds: TimeInterval
|
||||
/// 注册清单漂移重注时,注销与重注的间隔(launchd 对节流中 job 的移除异步)。
|
||||
@@ -126,6 +130,7 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
listenRestore: RestoredListenSettings = ListenSettingsFileStore.restored(),
|
||||
settingsStore: ProxySettingsStoring = ProxySettingsFileStore(),
|
||||
customRuleStore: CustomRuleStore = CustomRuleStore(),
|
||||
appBundle: Bundle = .main,
|
||||
settingsRestore: RestoredProxySettings? = nil,
|
||||
agent: LaunchAgentControlling = SMAppLaunchAgentService(),
|
||||
probe: EndpointProbing = SystemEndpointProbe(),
|
||||
@@ -149,6 +154,7 @@ final class ProxyRuntimeController: ObservableObject {
|
||||
self.plugins = plugins
|
||||
self.settingsStore = settingsStore
|
||||
self.customRuleStore = customRuleStore
|
||||
self.appBundle = appBundle
|
||||
let restoredSettings =
|
||||
settingsRestore
|
||||
?? RestoredProxySettings(
|
||||
|
||||
@@ -20,9 +20,11 @@ enum SystemProxyHelperIdentity {
|
||||
|
||||
/// App bundle 内 LaunchDaemon 清单内容(注册漂移指纹的基准)。helper 进程
|
||||
/// 内没有 bundle,返回 nil,调用方按「无基准不动作」处理。Contents/Library
|
||||
/// 不是资源目录,Bundle 资源 API 搜不到,按固定路径直读。
|
||||
static var launchDaemonPlistData: Data? {
|
||||
let url = Bundle.main.bundleURL
|
||||
/// 不是资源目录,Bundle 资源 API 搜不到,按固定路径直读。bundle 参数生产
|
||||
/// 默认 Bundle.main;去宿主化(ADR 0021)后单测经控制器的 appBundle 缝注入
|
||||
/// 构建产物 bundle。
|
||||
static func launchDaemonPlistData(in bundle: Bundle = .main) -> Data? {
|
||||
let url = bundle.bundleURL
|
||||
.appendingPathComponent("Contents/Library/LaunchDaemons")
|
||||
.appendingPathComponent(plistName)
|
||||
return try? Data(contentsOf: url)
|
||||
|
||||
@@ -6,11 +6,18 @@ import Security
|
||||
/// 不校验 Team ID 或 UID。校验基于连接对端 pid 的 SecCode 查询。
|
||||
final class SystemProxyHelperListenerDelegate: NSObject, NSXPCListenerDelegate {
|
||||
private let engine: SystemProxyHelperEngine
|
||||
private let expectedClientIdentifier: String
|
||||
|
||||
/// 引擎由进程入口显式注入:特权写入器 SystemProxyWriter 属 helper 专属
|
||||
/// (不进共享编译单元),测试注入假写入器。
|
||||
init(engine: SystemProxyHelperEngine) {
|
||||
/// (不进共享编译单元),测试注入假写入器。期望客户端身份生产默认为 2.0
|
||||
/// bundle identifier;去宿主化(ADR 0021)后单测运行于 xctest runner,
|
||||
/// 连接方身份随宿主改变,测试注入本进程实际签名 identifier。
|
||||
init(
|
||||
engine: SystemProxyHelperEngine,
|
||||
expectedClientIdentifier: String = SystemProxyHelperIdentity.clientCodeSigningIdentifier
|
||||
) {
|
||||
self.engine = engine
|
||||
self.expectedClientIdentifier = expectedClientIdentifier
|
||||
super.init()
|
||||
}
|
||||
|
||||
@@ -20,7 +27,7 @@ final class SystemProxyHelperListenerDelegate: NSObject, NSXPCListenerDelegate {
|
||||
) -> Bool {
|
||||
guard
|
||||
SystemProxyClientValidator.clientSigningIdentifier(newConnection)
|
||||
== SystemProxyHelperIdentity.clientCodeSigningIdentifier
|
||||
== expectedClientIdentifier
|
||||
else { return false }
|
||||
newConnection.exportedInterface = NSXPCInterface(with: SystemProxyHelperControlling.self)
|
||||
newConnection.exportedObject = engine
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
import Foundation
|
||||
|
||||
/// 构建产物定位:沿测试 bundle 落盘位置向上查找 app 产物。覆盖两种布局——
|
||||
/// TEST_HOST 过渡期 `.xctest` 嵌在 app bundle 的 `Contents/PlugIns/` 内,
|
||||
/// 去宿主化后 `.xctest` 与 `.app` 平级同在 `Build/Products/<config>/`。
|
||||
/// 测试需要 app 产物(嵌入二进制、Info.plist 打包面断言)时一律经此定位,
|
||||
/// 不得用 `Bundle.main`(去宿主化后它是 xctest runner 的 bundle)。
|
||||
/// 构建产物定位:从测试 bundle 落盘位置向上逐层查找 app 产物。覆盖两种布局
|
||||
/// ——TEST_HOST 过渡期 `.xctest` 嵌于 app bundle 的 `Contents/PlugIns/`(app
|
||||
/// 是祖先),去宿主化后 `.xctest` 与 `.app` 平级同在 `Build/Products/<config>/`
|
||||
/// (app 是某层祖先的子项)。测试需要 app 产物(嵌入二进制、Info.plist 打包面
|
||||
/// 断言)时一律经此定位,不得用 `Bundle.main`(去宿主化后它是 xctest runner
|
||||
/// 的 bundle)。
|
||||
enum AppArtifact {
|
||||
static let bundleURL: URL = {
|
||||
var directory = Bundle(for: Marker.self).bundleURL
|
||||
let testBundleURL = Bundle(for: Marker.self).bundleURL
|
||||
var directory = testBundleURL
|
||||
while directory.path != "/" {
|
||||
directory.deleteLastPathComponent()
|
||||
if directory.lastPathComponent == "ShadowsocksX-NG2.app" {
|
||||
return directory
|
||||
}
|
||||
directory.deleteLastPathComponent()
|
||||
let candidate = directory.appendingPathComponent(
|
||||
"ShadowsocksX-NG2.app", isDirectory: true)
|
||||
if FileManager.default.fileExists(atPath: candidate.path) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
// 定位失败时返回原目录让用例以「产物缺失」失败并暴露实际路径,不炸进程。
|
||||
return Bundle(for: Marker.self).bundleURL
|
||||
return testBundleURL
|
||||
}()
|
||||
|
||||
/// `bundleURL` 的 Bundle 形态:注入按 Bundle 取资源的生产缝(如
|
||||
/// ProxyRuntimeController 的内置规则快照 bundle)。定位失败时退回测试
|
||||
/// bundle,让资源缺失以用例失败呈现。
|
||||
static let bundle: Bundle = Bundle(url: bundleURL) ?? Bundle(for: Marker.self)
|
||||
|
||||
/// `Bundle(for:)` 需要类对象;marker 只为取得测试 bundle 的落盘位置。
|
||||
private final class Marker {}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ final class ProxyControlWorkflowIntegrationTests: XCTestCase {
|
||||
listenRestore: RestoredListenSettings(
|
||||
settings: ActivationFixture.listen, unreadableError: nil),
|
||||
settingsStore: settingsStore ?? InMemoryProxySettingsStore(),
|
||||
appBundle: AppArtifact.bundle,
|
||||
settingsRestore: RestoredProxySettings(
|
||||
settings: settings ?? ProxySettings(listen: ActivationFixture.listen),
|
||||
unreadableError: nil),
|
||||
|
||||
@@ -121,6 +121,7 @@ final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase {
|
||||
listenRestore: RestoredListenSettings(
|
||||
settings: ActivationFixture.listen, unreadableError: nil),
|
||||
settingsStore: InMemoryProxySettingsStore(),
|
||||
appBundle: AppArtifact.bundle,
|
||||
settingsRestore: RestoredProxySettings(
|
||||
settings: settings ?? ProxySettings(listen: ActivationFixture.listen),
|
||||
unreadableError: nil),
|
||||
|
||||
@@ -126,6 +126,7 @@ final class ProxyRuntimeControllerTests: XCTestCase {
|
||||
settingsStore: settingsStore ?? InMemoryProxySettingsStore(),
|
||||
customRuleStore: customRuleStore
|
||||
?? CustomRuleStore(fileURL: runtime.directory.appendingPathComponent("custom-rules.json")),
|
||||
appBundle: AppArtifact.bundle,
|
||||
settingsRestore: restored,
|
||||
agent: agent,
|
||||
probe: probe,
|
||||
|
||||
@@ -59,6 +59,7 @@ final class SettingsWorkflowTests: XCTestCase {
|
||||
listenRestore: RestoredListenSettings(
|
||||
settings: ActivationFixture.listen, unreadableError: nil),
|
||||
settingsStore: settingsStore,
|
||||
appBundle: AppArtifact.bundle,
|
||||
agent: agent,
|
||||
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
|
||||
systemProxy: systemProxy,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Security
|
||||
import XCTest
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
@@ -6,8 +7,9 @@ import XCTest
|
||||
/// 真实 XPC 装配层的契约 round-trip(issue #71):匿名监听器挂真实 listener
|
||||
/// delegate(注入假特权写入器,不触真实系统设置)+ listenerEndpoint 连接,覆盖
|
||||
/// fakes 与纯逻辑单测覆盖不到的层:NSXPCInterface 方法签名、客户端签名
|
||||
/// identifier 校验、Data payload 传输与 XPC 内部队列应答路径。测试宿主是
|
||||
/// app bundle,客户端 identifier 与 delegate 期望一致,无需 launchd。
|
||||
/// identifier 校验、Data payload 传输与 XPC 内部队列应答路径。去宿主化
|
||||
/// (ADR 0021)后连接方是 xctest runner 本进程,delegate 期望身份按本进程
|
||||
/// 实际签名 identifier 注入,无需 launchd。
|
||||
/// 注意:NSXPCListener.delegate 是弱引用,delegate 必须由测试强持有。
|
||||
final class SystemProxyHelperXPCRoundTripTests: XCTestCase {
|
||||
func testTypedApplyAndClearRoundTripThroughRealXPCConnection() async throws {
|
||||
@@ -17,7 +19,9 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase {
|
||||
case .clear: return .cleared
|
||||
}
|
||||
})
|
||||
let delegate = SystemProxyHelperListenerDelegate(engine: engine)
|
||||
let delegate = SystemProxyHelperListenerDelegate(
|
||||
engine: engine,
|
||||
expectedClientIdentifier: try XCTUnwrap(Self.currentProcessSigningIdentifier()))
|
||||
let listener = NSXPCListener.anonymous()
|
||||
listener.delegate = delegate
|
||||
listener.resume()
|
||||
@@ -35,8 +39,7 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase {
|
||||
|
||||
let configuration = SystemProxyConfiguration(
|
||||
socks: .init(host: "127.0.0.1", port: 1086),
|
||||
http: .init(host: "127.0.0.1", port: 1087),
|
||||
https: .init(host: "127.0.0.1", port: 1087),
|
||||
http: .init(host: "127.0.0.1", port: 1087), https: .init(host: "127.0.0.1", port: 1087),
|
||||
exceptions: ["localhost"])
|
||||
let payload = try SystemProxyHelperWire.encodeConfiguration(configuration)
|
||||
|
||||
@@ -66,4 +69,19 @@ final class SystemProxyHelperXPCRoundTripTests: XCTestCase {
|
||||
await fulfillment(of: [clearReplied], timeout: 10)
|
||||
XCTAssertEqual(clearedResponse, .cleared)
|
||||
}
|
||||
|
||||
/// 本进程的代码签名 identifier(与生产 SystemProxyClientValidator 同一
|
||||
/// SecCode 查询路径,对自身用 SecCodeCopySelf 取 code)。
|
||||
private static func currentProcessSigningIdentifier() -> String? {
|
||||
var code: SecCode?
|
||||
guard SecCodeCopySelf([], &code) == errSecSuccess, let code else { return nil }
|
||||
var staticCode: SecStaticCode?
|
||||
guard SecCodeCopyStaticCode(code, [], &staticCode) == errSecSuccess, let staticCode
|
||||
else { return nil }
|
||||
var information: CFDictionary?
|
||||
guard SecCodeCopySigningInformation(staticCode, SecCSFlags(), &information) == errSecSuccess,
|
||||
let information
|
||||
else { return nil }
|
||||
return (information as NSDictionary)[kSecCodeInfoIdentifier] as? String
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ final class UnreadableListenSettingsTests: XCTestCase {
|
||||
plugins: ActivationFixture.plugins,
|
||||
listenRestore: RestoredListenSettings(
|
||||
settings: SslocalListenSettings(), unreadableError: listenUnreadable),
|
||||
appBundle: AppArtifact.bundle,
|
||||
settingsRestore: settingsRestore,
|
||||
agent: agent,
|
||||
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
|
||||
|
||||
@@ -85,6 +85,10 @@ targets:
|
||||
base:
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2
|
||||
PRODUCT_NAME: ShadowsocksX-NG2
|
||||
# 模块名让位给 Core 镜像库(ShadowsocksX_NG2):两者 swiftmodule 都落
|
||||
# 共享 Products 目录,重名会互相覆盖;app 可执行自身不 import 任何
|
||||
# 消费方,改名零影响。
|
||||
PRODUCT_MODULE_NAME: ShadowsocksX_NG2App
|
||||
CODE_SIGN_IDENTITY: "Developer ID Application"
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
MARKETING_VERSION: "2.0.0"
|
||||
@@ -118,6 +122,29 @@ targets:
|
||||
copy:
|
||||
destination: wrapper
|
||||
subpath: Contents/MacOS
|
||||
ShadowsocksX-NG2Core:
|
||||
type: library.static
|
||||
platform: macOS
|
||||
# 测试专用 app 源码镜像(去宿主化,ADR 0021):与 app target 同源编译
|
||||
# (除 @main 组合根 MainApp.swift),模块名沿用 ShadowsocksX_NG2,全部
|
||||
# 测试的 @testable import 零改动。测试 bundle 改链本库后不再生成
|
||||
# TEST_HOST:xcodebuild test 只构建 app、不再启动 app 进程,与安装版
|
||||
# 运行实例零接触。同源双编译沿用 Agent/Helper 共享 Domain 文件的既定
|
||||
# 模式;Core 只被测试 target 依赖,发布链与普通构建不编译它。
|
||||
sources:
|
||||
- path: App
|
||||
excludes:
|
||||
- "MainApp.swift"
|
||||
- "Info.plist"
|
||||
- "ShadowsocksX-NG2.entitlements"
|
||||
- "Assets.xcassets"
|
||||
- path: Domain
|
||||
settings:
|
||||
base:
|
||||
PRODUCT_NAME: ShadowsocksX-NG2Core
|
||||
PRODUCT_MODULE_NAME: ShadowsocksX_NG2
|
||||
# 静态库产物不签名(app 的 DevID 身份不适用)。
|
||||
CODE_SIGNING_ALLOWED: NO
|
||||
ShadowsocksX-NG2Agent:
|
||||
type: tool
|
||||
platform: macOS
|
||||
@@ -177,7 +204,9 @@ targets:
|
||||
path: Tests/Info.plist
|
||||
properties: {}
|
||||
dependencies:
|
||||
- target: ShadowsocksX-NG2
|
||||
# 去宿主化(ADR 0021):单测链 Core 静态库镜像而非 app 可执行,测试在
|
||||
# 独立 xctest runner 中运行,宿主 app 不再被启动。
|
||||
- target: ShadowsocksX-NG2Core
|
||||
ShadowsocksX-NG2SmokeTests:
|
||||
type: bundle.unit-test
|
||||
platform: macOS
|
||||
@@ -194,7 +223,9 @@ targets:
|
||||
path: Tests/Smoke/Info.plist
|
||||
properties: {}
|
||||
dependencies:
|
||||
- target: ShadowsocksX-NG2
|
||||
# 去宿主化(ADR 0021):smoke 同样链 Core 镜像库;真实 sslocal/agent 从
|
||||
# scheme 构建的 app 产物取(AppArtifact),宿主 app 不再被启动。
|
||||
- target: ShadowsocksX-NG2Core
|
||||
schemes:
|
||||
# 真实进程冒烟独立 scheme(task test:smoke):拉起真实 sslocal,按需执行;
|
||||
# 发布门槛要求与 packaging-gate 一起全绿。
|
||||
|
||||
Reference in New Issue
Block a user