mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-11 19:25:06 +00:00
Tests/ 根层原有 138 个平铺测试文件,无法从目录读出测试覆盖的功能领域。 按功能领域重组为 17 个目录(Catalog/Workflow 与 Rules/Custom 两处嵌套,分别 对应 App/Application/CatalogWorkflow 与 Domain/Rules/Custom):Architecture / Support / Agent / Activation / Catalog / Subscription / Rules / ProxyRuntime / ProxyControl / Runtime / Settings / SystemProxy / Plugin / Diagnostics / Credentials / Workspace / Servers;Smoke/ 保持独立 target 不动。138 个移动中 132 个是 100% 相似度的纯重命名,其余 6 个在重命名之上带路径反推改写(见下)。 本次不是纯搬移:测试代码比生产代码多一层位置耦合,有 7 处用固定层数的 deletingLastPathComponent() 从 #filePath 反推仓库路径(都假定文件就在 Tests/ 根层)。这 7 处改走新增的 Tests/Support/TestSourceTree.swift——从调用点沿父 目录上溯找到含 project.yml 的目录,因此不再依赖父目录层数,以后移动测试文件 也不会静默指错。 配套改动: - 新增 Tests 布局守卫 TestsLayoutTests,断言 Tests/ 根层无平铺 .swift。比 Domain 那条弱一档——根层的 Info.plist(info.path 生成物)与 Fixtures/RuleSnapshots 下的 JSON(folder resource)都必须保留,不能用 「树下只有 .swift」覆盖。 - project.yml:Smoke target 按文件引用的 Tests/AppArtifact.swift 改指 Tests/Support/;两处注释里写死的 Tests/SkeletonTests.swift 随移动更新。 excludes 的裸名 "Smoke" / "Fixtures" 仍匹配 Tests/ 直属子目录,无需改动。 - README 的 Tests 条目展开为目录说明,并修掉 Domain 条目里写死的 Tests/SkeletonTests.swift。 - 顺带删去 DomainLayoutTests 文档注释里「整理前 62 个文件」这类易漂移的数字。 验证:xcodegen generate 后单测 target 为 139 个源文件(全部非 Smoke 文件,0 个 Smoke 文件混入)且仍以 Fixtures/RuleSnapshots 为资源,Smoke target 为 8 个 Smoke 文件加 AppArtifact;依赖路径反推的 6 个测试类全绿(路径指错即失败); task format 未改写任何被移动文件;task lint 与整理前同为 21 条内容型 warning; build 0 error;task test 1025 用例全通过(整理前 1024,+1 为新守卫); TestsLayoutTests 经变异验证(根层投放探针即失败);packaging-gate ALL CHECKS PASSED。
155 lines
6.9 KiB
Swift
155 lines
6.9 KiB
Swift
import XCTest
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
extension ProxyControlWorkflowIntegrationTests {
|
||
func testPassiveNetworkChangesNeverReapplySystemProxy() async throws {
|
||
let server = try makeSeededCatalog()
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||
_ = try await composition.catalog.activate(server)
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
_ = await composition.control.setSystemProxyEnabled(true)
|
||
let writes = systemProxy.applied.count
|
||
XCTAssertEqual(writes, 1)
|
||
await emitAndWaitForInspection(
|
||
[.networkConfiguration, .proxyConfiguration, .networkPath], in: composition)
|
||
XCTAssertEqual(systemProxy.applied.count, writes)
|
||
XCTAssertEqual(systemProxy.clearCount, 0)
|
||
}
|
||
|
||
// MARK: - 系统代理开关(issue #60)
|
||
|
||
func testSystemProxyIntentAppliesThroughSnapshot() async throws {
|
||
let server = try makeSeededCatalog()
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||
|
||
_ = try await composition.catalog.activate(server)
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
let snapshot = await composition.control.setSystemProxyEnabled(true)
|
||
|
||
XCTAssertTrue(snapshot.systemProxyIntentEnabled)
|
||
XCTAssertEqual(snapshot.systemProxyApplication, .applied)
|
||
XCTAssertEqual(snapshot.runtime.status, .running, "agent 运行状态不受系统代理影响")
|
||
XCTAssertEqual(systemProxy.applied.count, 1)
|
||
}
|
||
|
||
/// 健康门禁:端点不健康时系统代理意图保持待应用(快照呈现 pending)。
|
||
func testSystemProxyIntentStaysPendingWhenEndpointsUnhealthy() async throws {
|
||
let server = try makeSeededCatalog()
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.refusing())
|
||
|
||
_ = try await composition.catalog.activate(server)
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
let snapshot = await composition.control.setSystemProxyEnabled(true)
|
||
|
||
XCTAssertTrue(snapshot.systemProxyIntentEnabled)
|
||
XCTAssertEqual(snapshot.systemProxyApplication, .paused, "待应用进入 snapshot")
|
||
XCTAssertEqual(snapshot.runtime.status, .launchFailed)
|
||
XCTAssertTrue(systemProxy.applied.isEmpty, "健康门未过不写系统设置")
|
||
}
|
||
|
||
/// 系统代理写入失败:typed fact 进入 snapshot,agent 继续运行。
|
||
func testSystemProxyFailureSurfacesTypedFactInSnapshot() async throws {
|
||
let server = try makeSeededCatalog()
|
||
systemProxy.applyError = SystemProxyError.applyFailed("denied")
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||
|
||
_ = try await composition.catalog.activate(server)
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
let snapshot = await composition.control.setSystemProxyEnabled(true)
|
||
|
||
XCTAssertTrue(snapshot.systemProxyIntentEnabled)
|
||
XCTAssertEqual(
|
||
snapshot.systemProxyApplication, .failed(.operation(.applyFailed)))
|
||
XCTAssertEqual(snapshot.runtime, ProxyRuntimeFacts(status: .running, isOn: true))
|
||
}
|
||
|
||
/// 关闭系统代理:清理匹配端点的设置;本地监听不停止。
|
||
func testSystemProxyOffKeepsAgentRunningAndReflectsIdleInSnapshot() async throws {
|
||
let server = try makeSeededCatalog()
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||
_ = try await composition.catalog.activate(server)
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
_ = await composition.control.setSystemProxyEnabled(true)
|
||
|
||
let snapshot = await composition.control.setSystemProxyEnabled(false)
|
||
|
||
XCTAssertFalse(snapshot.systemProxyIntentEnabled)
|
||
XCTAssertEqual(snapshot.systemProxyApplication, .idle)
|
||
XCTAssertEqual(snapshot.runtime, ProxyRuntimeFacts(status: .running, isOn: true))
|
||
XCTAssertEqual(systemProxy.clearCount, 1)
|
||
XCTAssertEqual(agent.unregisterCount, 0, "不注销 agent")
|
||
}
|
||
|
||
/// 无活动目标:agent 监听,但需要出口的系统代理意图保持待应用。
|
||
func testSystemProxyIntentStaysPendingWithoutActiveTarget() async throws {
|
||
_ = try makeSeededCatalog()
|
||
let composition = makeProxies(
|
||
probe: ProxyRuntimeFixture.FakeProbe.reachable(),
|
||
settings: ProxySettings(listen: ActivationFixture.listen, agentEnabled: false))
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
|
||
let snapshot = await composition.control.setSystemProxyEnabled(true)
|
||
|
||
XCTAssertEqual(snapshot.runtime.status, .running, "agent 以空列表监听")
|
||
XCTAssertEqual(snapshot.systemProxyApplication, .paused, "无可用出口保持待应用")
|
||
XCTAssertTrue(systemProxy.applied.isEmpty)
|
||
XCTAssertNil(snapshot.activeTarget)
|
||
}
|
||
|
||
/// 待应用意图在目标激活后自动收敛(issue #60)。目录命令后的 snapshot 更新
|
||
/// 经主队列 hop 到达,有界等待收敛。
|
||
func testPendingSystemProxyIntentConvergesAfterTargetActivation() async throws {
|
||
let server = try makeSeededCatalog()
|
||
let composition = makeProxies(probe: ProxyRuntimeFixture.FakeProbe.reachable())
|
||
_ = await composition.control.setAgentEnabled(true)
|
||
let pending = await composition.control.setSystemProxyEnabled(true)
|
||
XCTAssertEqual(pending.systemProxyApplication, .paused)
|
||
|
||
_ = try await composition.catalog.activate(server)
|
||
let deadline = Date().addingTimeInterval(2)
|
||
while composition.control.snapshot.systemProxyApplication != .applied && Date() < deadline {
|
||
try await Task.sleep(nanoseconds: 10_000_000)
|
||
}
|
||
|
||
XCTAssertEqual(composition.control.snapshot.systemProxyApplication, .applied, "激活后自动收敛")
|
||
XCTAssertEqual(systemProxy.applied.count, 1)
|
||
}
|
||
|
||
// MARK: - HTTP 导出能力派生(生产 adapter 的唯一点)
|
||
|
||
func testHTTPExportCapabilityDerivesSafeCopyableLineForLoopback() {
|
||
XCTAssertEqual(
|
||
HTTPExportCapability(listen: SslocalListenSettings()).copyableLine,
|
||
"export http_proxy=http://127.0.0.1:11087;export https_proxy=http://127.0.0.1:11087;")
|
||
}
|
||
|
||
func testHTTPExportCapabilityUsesLoopbackAddressForAllIPv4Interfaces() {
|
||
var listen = SslocalListenSettings()
|
||
listen.listenerMode = .allIPv4Interfaces
|
||
listen.httpPort = 8080
|
||
XCTAssertEqual(
|
||
HTTPExportCapability(listen: listen).copyableLine,
|
||
"export http_proxy=http://127.0.0.1:8080;export https_proxy=http://127.0.0.1:8080;")
|
||
}
|
||
|
||
func testHTTPExportCapabilityUsesIPv4LoopbackForDualStackMode() {
|
||
var listen = SslocalListenSettings()
|
||
listen.listenerMode = .allIPv4AndIPv6Interfaces
|
||
listen.httpPort = 8080
|
||
|
||
XCTAssertEqual(
|
||
HTTPExportCapability(listen: listen).copyableLine,
|
||
"export http_proxy=http://127.0.0.1:8080;export https_proxy=http://127.0.0.1:8080;")
|
||
}
|
||
|
||
func testHTTPExportCapabilityBracketsIPv6LoopbackOnlyForIPv6OnlyMode() {
|
||
var listen = SslocalListenSettings()
|
||
listen.listenerMode = .allIPv6Interfaces
|
||
listen.httpPort = 8080
|
||
XCTAssertEqual(
|
||
HTTPExportCapability(listen: listen).copyableLine,
|
||
"export http_proxy=http://[::1]:8080;export https_proxy=http://[::1]:8080;")
|
||
}
|
||
}
|