refactor(tests): locate app product via bundle ancestor walk, not Bundle.main

测试需要 app 产物(嵌入二进制、打包面断言)时改经 AppArtifact 沿测试包
落盘位置向上查找 ShadowsocksX-NG2.app,覆盖两种布局:TEST_HOST 过渡期
.xctest 嵌于 Contents/PlugIns/,去宿主化后与 .app 平级。接触点:
AgentLifecycleTests wrapper、BundleManagedPluginProviderTests 组合根事实
断言、smoke 三处 sslocal/wrapper/plugin 定位、SkeletonTests 改读产物
Info.plist。行为等价(665 单测 + 11 冒烟全绿),为去 TEST_HOST 铺路。
This commit is contained in:
Qiu Yuzhou
2026-09-30 20:05:37 +08:00
parent ba20e42f11
commit ac60d14099
7 changed files with 44 additions and 15 deletions
@@ -32,7 +32,7 @@ final class AgentLifecycleTests: XCTestCase {
private var wrapperURL: URL {
get throws {
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/MacOS/ShadowsocksX-NG2Agent")
let url = AppArtifact.bundleURL.appendingPathComponent("Contents/MacOS/ShadowsocksX-NG2Agent")
return try XCTUnwrap(
FileManager.default.fileExists(atPath: url.path) ? url : nil,
"wrapper 未嵌入 app bundle(Contents/MacOS/ShadowsocksX-NG2Agent)")
+23
View File
@@ -0,0 +1,23 @@
import Foundation
/// 构建产物定位:沿测试 bundle 落盘位置向上查找 app 产物。覆盖两种布局——
/// TEST_HOST 过渡期 `.xctest` 嵌在 app bundle 的 `Contents/PlugIns/` 内,
/// 去宿主化后 `.xctest` 与 `.app` 平级同在 `Build/Products/<config>/`。
/// 测试需要 app 产物(嵌入二进制、Info.plist 打包面断言)时一律经此定位,
/// 不得用 `Bundle.main`(去宿主化后它是 xctest runner 的 bundle)。
enum AppArtifact {
static let bundleURL: URL = {
var directory = Bundle(for: Marker.self).bundleURL
while directory.path != "/" {
directory.deleteLastPathComponent()
if directory.lastPathComponent == "ShadowsocksX-NG2.app" {
return directory
}
}
// 定位失败时返回原目录让用例以「产物缺失」失败并暴露实际路径,不炸进程。
return Bundle(for: Marker.self).bundleURL
}()
/// `Bundle(for:)` 需要类对象;marker 只为取得测试 bundle 的落盘位置。
private final class Marker {}
}
@@ -66,12 +66,14 @@ final class BundleManagedPluginProviderTests: XCTestCase {
XCTAssertNil(provider.executablePath(forProgram: ""))
}
/// 组合根事实:宿主 app bundle(Bundle.main,生产默认 provider 的取值来源)
/// 内受管插件在位——防止打包回归静默断开激活链路(编辑器显示提供而激活拒绝)。
/// 组合根事实:app bundle(生产默认 provider 的取值来源,经 AppArtifact 按
/// 构建产物定位、不依赖测试宿主)内受管插件在位——防止打包回归静默断开激
/// 活链路(编辑器显示提供而激活拒绝)。
func testHostAppBundleProvidesManagedPlugin() throws {
let path = try XCTUnwrap(
BundleManagedPluginProvider().executablePath(forProgram: pluginBinaryName),
"宿主 app bundle 未嵌入受管插件(先跑 fetch-external-binaries.sh)")
BundleManagedPluginProvider(bundleURL: AppArtifact.bundleURL)
.executablePath(forProgram: pluginBinaryName),
"app bundle 未嵌入受管插件(先跑 fetch-external-binaries.sh)")
XCTAssertTrue(path.hasSuffix("Contents/Helpers/Plugins/\(pluginBinaryName)"))
}
+9 -7
View File
@@ -4,16 +4,18 @@ import XCTest
@MainActor
final class SkeletonTests: XCTestCase {
// 以下两个 Bundle.main 断言依赖测试包以 app 为 TEST_HOST 注入(target dependency);
// 若移除该依赖,Bundle.main 不再是 app bundle,断言会失效。
// app bundle 断言按构建产物定位(AppArtifact),不依赖测试宿主进程;守住
// 的是打包面:bundle id 与旧版区分、菜单栏形态(LSUIElement)。
func testAppBundleIDIsDistinctFromLegacy() {
XCTAssertEqual(Bundle.main.bundleIdentifier, "com.qiuyuzhou.ShadowsocksX-NG2")
XCTAssertNotEqual(Bundle.main.bundleIdentifier, "com.qiuyuzhou.ShadowsocksX-NG")
func testAppBundleIDIsDistinctFromLegacy() throws {
let bundle = try XCTUnwrap(Bundle(url: AppArtifact.bundleURL))
XCTAssertEqual(bundle.bundleIdentifier, "com.qiuyuzhou.ShadowsocksX-NG2")
XCTAssertNotEqual(bundle.bundleIdentifier, "com.qiuyuzhou.ShadowsocksX-NG")
}
func testAppIsMenuBarAgent() {
XCTAssertEqual(Bundle.main.object(forInfoDictionaryKey: "LSUIElement") as? Bool, true)
func testAppIsMenuBarAgent() throws {
let bundle = try XCTUnwrap(Bundle(url: AppArtifact.bundleURL))
XCTAssertEqual(bundle.object(forInfoDictionaryKey: "LSUIElement") as? Bool, true)
}
}
@@ -11,7 +11,7 @@ extension RealSslocalSmokeTests {
/// 连带 sslocal 退出(D10 以 sslocal 信号为准);真实服务器数据面验证属发布
/// 门槛人工项(spec #21 Further Notes #6),在此以进程边界验证。
func testRealSslocalLaunchesManagedV2rayPluginProcess() throws {
let pluginURL = Bundle.main.bundleURL.appendingPathComponent(
let pluginURL = AppArtifact.bundleURL.appendingPathComponent(
"Contents/Helpers/Plugins/v2ray-plugin")
_ = try XCTUnwrap(
FileManager.default.isExecutableFile(atPath: pluginURL.path) ? pluginURL : nil,
@@ -32,7 +32,7 @@ final class RealSslocalSmokeTests: XCTestCase {
private var sslocalURL: URL {
get throws {
let url = Bundle.main.bundleURL.appendingPathComponent("Contents/Helpers/sslocal")
let url = AppArtifact.bundleURL.appendingPathComponent("Contents/Helpers/sslocal")
return try XCTUnwrap(
FileManager.default.fileExists(atPath: url.path) ? url : nil,
"sslocal 未嵌入 app bundle(先跑 fetch-external-binaries.sh)")
@@ -72,7 +72,7 @@ final class RealSslocalSmokeTests: XCTestCase {
func launchWrapper(_ document: SslocalRuntimeDocument) throws -> Process {
try RuntimeFileStore(fileURL: contractURL).write(document)
let wrapperURL = Bundle.main.bundleURL.appendingPathComponent(
let wrapperURL = AppArtifact.bundleURL.appendingPathComponent(
"Contents/MacOS/ShadowsocksX-NG2Agent")
let process = Process()
process.executableURL = wrapperURL
+2
View File
@@ -183,6 +183,8 @@ targets:
platform: macOS
sources:
- path: Tests/Smoke
# app 产物定位 helper(与单测 target 共享,见 Tests/AppArtifact.swift)。
- path: Tests/AppArtifact.swift
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.qiuyuzhou.ShadowsocksX-NG2.SmokeTests