From 5abb6c603399e9b3d194d607fbf4373e16cab033 Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Mon, 28 Sep 2026 08:39:15 +0800 Subject: [PATCH] build: migrate NG2 to Swift 6 and clear warnings --- ShadowsocksX-NG2/App/AppDelegate.swift | 1 + .../CatalogWorkflowModels.swift | 1 + .../DiagnosticsWorkflow.swift | 9 ++--- .../Domain/CustomRuleValidator.swift | 2 +- .../Domain/GFWListConverter.swift | 2 +- .../Domain/GeolocationCNConverter.swift | 33 ++++++++++++------- ShadowsocksX-NG2/Domain/ProxyRule.swift | 14 ++++++-- .../Tests/ActivationStateFileStoreTests.swift | 2 +- .../Tests/CatalogCommitCoordinatorTests.swift | 12 +++---- .../Tests/CatalogFileStoreTests.swift | 2 +- ShadowsocksX-NG2/Tests/CatalogFixtures.swift | 5 +-- ...lowSubscriptionTests+RefreshFailures.swift | 2 ++ ...bscriptionTests+RefreshOrchestration.swift | 28 ++++++++-------- .../CatalogWorkflowTests+RenameAndMove.swift | 3 +- .../Tests/CatalogWorkflowTests.swift | 2 +- .../Tests/DiagnosticsWorkflowFixture.swift | 12 +++---- .../DiagnosticsWorkflowLifecycleTests.swift | 2 +- .../Tests/EndpointHealthProbeTests.swift | 2 +- ...ProxyControlWorkflowIntegrationTests.swift | 8 ++--- .../Tests/ProxyControlWorkflowTests.swift | 4 +-- .../ProxyRuntimeControllerModeTests.swift | 10 +++--- ...eControllerTests+AgentAndSystemProxy.swift | 2 +- ...oxyRuntimeControllerTests+GlobalMode.swift | 2 +- .../ProxyRuntimeControllerTests+Mode.swift | 2 +- .../Tests/ProxyRuntimeControllerTests.swift | 10 +++--- .../Tests/RuntimeFileStoreTests.swift | 16 ++++----- .../SettingsWorkflowInterfaceTests.swift | 4 +-- .../Tests/SettingsWorkflowTestSupport.swift | 1 + .../Tests/SettingsWorkflowTests.swift | 8 ++--- ShadowsocksX-NG2/Tests/SkeletonTests.swift | 4 ++- .../RealSslocalSmokeTests+ACLRouting.swift | 9 +++-- .../RealSslocalSmokeTests+CustomRules.swift | 12 +++---- .../RealSslocalSmokeTests+GFWListACL.swift | 14 ++++---- .../Tests/SubscriptionFetcherTests.swift | 8 ++--- .../Tests/UnreadableListenSettingsTests.swift | 8 ++--- ShadowsocksX-NG2/project.yml | 4 +-- 36 files changed, 148 insertions(+), 112 deletions(-) diff --git a/ShadowsocksX-NG2/App/AppDelegate.swift b/ShadowsocksX-NG2/App/AppDelegate.swift index c3176e80..d3c930a5 100644 --- a/ShadowsocksX-NG2/App/AppDelegate.swift +++ b/ShadowsocksX-NG2/App/AppDelegate.swift @@ -3,6 +3,7 @@ import AppKit /// 混合应用形态的 AppKit 生命周期家:LSUIElement 让进程默认纯后台 /// (accessory),这里负责前台形态的进入与退出——任意路径使主窗口可见 /// 时切成普通应用(regular),最后一个窗口关闭后切回纯后台。 +@MainActor final class AppDelegate: NSObject, NSApplicationDelegate { /// 混合形态的硬性要求:关窗不退进程,代理 GUI 仍驻留菜单栏。 func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { diff --git a/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflowModels.swift b/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflowModels.swift index 5aa300c9..45218657 100644 --- a/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflowModels.swift +++ b/ShadowsocksX-NG2/App/CatalogWorkflow/CatalogWorkflowModels.swift @@ -185,6 +185,7 @@ enum ActivationCommandOutcome: Equatable, Sendable { /// 激活缝(issue #41):目录命令面经此发出激活意图。生产 adapter 为 /// `ProxyRuntimeController`;测试注入假 adapter 观察目标传递。 +@MainActor protocol Activating: AnyObject { func activate(_ target: NodeID) async throws -> ActivationCommandOutcome } diff --git a/ShadowsocksX-NG2/App/DiagnosticsWorkflow/DiagnosticsWorkflow.swift b/ShadowsocksX-NG2/App/DiagnosticsWorkflow/DiagnosticsWorkflow.swift index 3d870206..d99de322 100644 --- a/ShadowsocksX-NG2/App/DiagnosticsWorkflow/DiagnosticsWorkflow.swift +++ b/ShadowsocksX-NG2/App/DiagnosticsWorkflow/DiagnosticsWorkflow.swift @@ -128,8 +128,8 @@ final class DiagnosticsWorkflow: ObservableObject { private let events: RuntimeEventStore private let catalogFacts: @MainActor () -> DiagnosticCatalogFacts? private let agentLogTail: () -> String? - private let fileFacts: () -> [DiagnosticFileFacts] - private let managedPlugins: () -> [DiagnosticPluginFacts] + private let fileFacts: @MainActor () -> [DiagnosticFileFacts] + private let managedPlugins: @MainActor () -> [DiagnosticPluginFacts] private let homePath: () -> String? private let clock: () -> Date private let reportRendering: (DiagnosticSnapshot) -> String? @@ -145,8 +145,9 @@ final class DiagnosticsWorkflow: ObservableObject { agentLogTail: @escaping () -> String? = { AgentLogTail.readLastLines(of: RuntimePaths.agentLogURL()) }, - fileFacts: @escaping () -> [DiagnosticFileFacts] = DiagnosticsWorkflow.productionFileFacts, - managedPlugins: @escaping () -> [DiagnosticPluginFacts] = + fileFacts: @escaping @MainActor () -> [DiagnosticFileFacts] = + DiagnosticsWorkflow.productionFileFacts, + managedPlugins: @escaping @MainActor () -> [DiagnosticPluginFacts] = DiagnosticsWorkflow.productionManagedPlugins, homePath: @escaping () -> String? = { NSHomeDirectory() }, clock: @escaping () -> Date = { Date() }, diff --git a/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift b/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift index d9c5c397..09d5a2f0 100644 --- a/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift +++ b/ShadowsocksX-NG2/Domain/CustomRuleValidator.swift @@ -141,7 +141,7 @@ enum RuleCoverage { /// 代理动作的 IP 规则是否与固定本地 IP 范围冲突。 static func ipConflictsWithFixedLocal(_ match: RuleMatch) -> Bool { for range in FixedLocalProxyRanges.ipRanges { - if let fixed = try? parseFixedLocalCIDR(range), ipIntersects(match, fixed) { + if let fixed = parseFixedLocalCIDR(range), ipIntersects(match, fixed) { return true } } diff --git a/ShadowsocksX-NG2/Domain/GFWListConverter.swift b/ShadowsocksX-NG2/Domain/GFWListConverter.swift index 9060ec90..625a3bc3 100644 --- a/ShadowsocksX-NG2/Domain/GFWListConverter.swift +++ b/ShadowsocksX-NG2/Domain/GFWListConverter.swift @@ -218,7 +218,7 @@ struct GFWListConverter { ) } - var rules = RuleSet(rules: shadowing.keptProxy + shadowing.keptExceptions).rules + let rules = RuleSet(rules: shadowing.keptProxy + shadowing.keptExceptions).rules report.convertedCount = rules.count report.absorbedCount = shadowing.shadowed.count try validateRuleCount(rules.count, previousRuleCount: previousRuleCount) diff --git a/ShadowsocksX-NG2/Domain/GeolocationCNConverter.swift b/ShadowsocksX-NG2/Domain/GeolocationCNConverter.swift index 2f3eeb13..f4512ff8 100644 --- a/ShadowsocksX-NG2/Domain/GeolocationCNConverter.swift +++ b/ShadowsocksX-NG2/Domain/GeolocationCNConverter.swift @@ -40,14 +40,25 @@ enum GeolocationParser { } /// typed 前缀 → 条目构造的对照表;裸域名(无前缀)按 bareDomain 处理。 - private static let typedPrefixes: - [(prefix: String, make: (String, [String]) -> GeolocationEntry)] = [ - ("include:", { .include(name: $0, attributes: $1) }), - ("domain:", { .domain($0, attributes: $1) }), - ("full:", { .full($0, attributes: $1) }), - ("keyword:", { .keyword($0, attributes: $1) }), - ("regexp:", { .regexp($0, attributes: $1) }), - ] + private enum TypedPrefix: String, CaseIterable, Sendable { + case include = "include:" + case domain = "domain:" + case full = "full:" + case keyword = "keyword:" + case regexp = "regexp:" + + func make(value: String, attributes: [String]) -> GeolocationEntry { + switch self { + case .include: .include(name: value, attributes: attributes) + case .domain: .domain(value, attributes: attributes) + case .full: .full(value, attributes: attributes) + case .keyword: .keyword(value, attributes: attributes) + case .regexp: .regexp(value, attributes: attributes) + } + } + } + + private static let typedPrefixes = TypedPrefix.allCases /// 去掉整行 `#` 注释与行内 ` #` 注释并修剪空白;空行返回 nil。 private static func codeLine(from rawLine: String) -> String? { @@ -81,11 +92,11 @@ enum GeolocationParser { let (content, attributes) = extractingAttributes(from: line) guard !content.isEmpty else { return nil } for typed in Self.typedPrefixes { - guard content.hasPrefix(typed.prefix) else { continue } - let value = String(content.dropFirst(typed.prefix.count)) + guard content.hasPrefix(typed.rawValue) else { continue } + let value = String(content.dropFirst(typed.rawValue.count)) .trimmingCharacters(in: .whitespacesAndNewlines) guard !value.isEmpty else { return nil } - return typed.make(value, attributes) + return typed.make(value: value, attributes: attributes) } return .bareDomain(content, attributes: attributes) } diff --git a/ShadowsocksX-NG2/Domain/ProxyRule.swift b/ShadowsocksX-NG2/Domain/ProxyRule.swift index 4254f6bf..f2f16ba3 100644 --- a/ShadowsocksX-NG2/Domain/ProxyRule.swift +++ b/ShadowsocksX-NG2/Domain/ProxyRule.swift @@ -173,7 +173,9 @@ enum RuleMatch: Equatable, Hashable, Codable, Sendable { guard inet_ntop(AF_INET, &networkAddress, &buffer, socklen_t(INET_ADDRSTRLEN)) != nil else { throw RuleMatchError.invalidCIDR(raw) } - let dotted = String(cString: buffer) + guard let dotted = Self.string(from: buffer) else { + throw RuleMatchError.invalidCIDR(raw) + } return "\(dotted)/\(prefixLength)" } @@ -220,7 +222,15 @@ enum RuleMatch: Equatable, Hashable, Codable, Sendable { guard inet_ntop(AF_INET6, &networkAddress, &buffer, socklen_t(INET6_ADDRSTRLEN)) != nil else { throw RuleMatchError.invalidCIDR(raw) } - return "\(String(cString: buffer))/\(prefixLength)" + guard let address = Self.string(from: buffer) else { + throw RuleMatchError.invalidCIDR(raw) + } + return "\(address)/\(prefixLength)" + } + + private static func string(from buffer: [CChar]) -> String? { + let bytes = buffer.prefix(while: { $0 != 0 }).map { UInt8(bitPattern: $0) } + return String(bytes: bytes, encoding: .utf8) } } diff --git a/ShadowsocksX-NG2/Tests/ActivationStateFileStoreTests.swift b/ShadowsocksX-NG2/Tests/ActivationStateFileStoreTests.swift index cd1cdc58..95a68e59 100644 --- a/ShadowsocksX-NG2/Tests/ActivationStateFileStoreTests.swift +++ b/ShadowsocksX-NG2/Tests/ActivationStateFileStoreTests.swift @@ -84,7 +84,7 @@ final class ActivationStateFileStoreTests: XCTestCase { let credentials = InMemoryCredentialStore() let leaf = try ActivationFixture.addPlainServer("a", in: &catalog, credentials: credentials) var machine = ActivationStateMachine() - try ActivationFixture.activate(&machine, leaf, in: catalog, credentials: credentials) + _ = try ActivationFixture.activate(&machine, leaf, in: catalog, credentials: credentials) try store.save(activeTargetID: machine.activeTargetID) // 下次启动:从文件恢复目标,载入目录后立即重校验(D5「重新校验同步」)。 diff --git a/ShadowsocksX-NG2/Tests/CatalogCommitCoordinatorTests.swift b/ShadowsocksX-NG2/Tests/CatalogCommitCoordinatorTests.swift index 80f55892..87488192 100644 --- a/ShadowsocksX-NG2/Tests/CatalogCommitCoordinatorTests.swift +++ b/ShadowsocksX-NG2/Tests/CatalogCommitCoordinatorTests.swift @@ -178,7 +178,7 @@ final class CatalogCommitCoordinatorTests: XCTestCase { await waitUntilRuntimeSettles(runtime.convergeCount == 1) try coordinator.commit { catalog, _ in try catalog.renameGroup(groupID, to: "改名") } await waitUntilRuntimeSettles(runtime.convergeCount == 2) - try coordinator.commit { catalog, _ in try catalog.remove(serverID) } + _ = try coordinator.commit { catalog, _ in try catalog.remove(serverID) } await waitUntilRuntimeSettles(runtime.convergeCount == 3) XCTAssertEqual(runtime.convergeCount, 3, "移动、改名、删除各触发一次同步") @@ -196,7 +196,7 @@ final class CatalogCommitCoordinatorTests: XCTestCase { runtime.hasActiveTarget = true runtime.enqueueOutcomes([.clearedAndStopped(.targetNotFound(targetID))]) - try coordinator.commit { catalog, _ in try catalog.remove(targetID) } + _ = try coordinator.commit { catalog, _ in try catalog.remove(targetID) } await waitUntilRuntimeSettles(coordinator.syncStatus != .syncing(generation: 1)) XCTAssertEqual(runtime.convergeCount, 1) @@ -230,8 +230,8 @@ final class CatalogCommitCoordinatorTests: XCTestCase { func testSupersededSyncBeforeStartNeverCallsRuntime() async throws { runtime.hasActiveTarget = true - try coordinator.commit { catalog, _ in try catalog.addTestServer("第一台") } - try coordinator.commit { catalog, _ in try catalog.addTestServer("第二台") } + _ = try coordinator.commit { catalog, _ in try catalog.addTestServer("第一台") } + _ = try coordinator.commit { catalog, _ in try catalog.addTestServer("第二台") } // 同一 MainActor 回合内的两次提交:旧代次同步尚未启动即被取代。 await waitUntilRuntimeSettles(runtime.convergeCount == 1) @@ -251,10 +251,10 @@ final class CatalogCommitCoordinatorTests: XCTestCase { ]) runtime.armGate() - try coordinator.commit { catalog, _ in try catalog.addTestServer("第一台") } + _ = try coordinator.commit { catalog, _ in try catalog.addTestServer("第一台") } await waitUntilRuntimeSettles(runtime.convergeCount == 1) - try coordinator.commit { catalog, _ in try catalog.addTestServer("第二台") } + _ = try coordinator.commit { catalog, _ in try catalog.addTestServer("第二台") } runtime.openGate() await waitUntilRuntimeSettles(runtime.convergeCount == 2) await waitUntilRuntimeSettles(coordinator.syncStatus != .syncing(generation: 2)) diff --git a/ShadowsocksX-NG2/Tests/CatalogFileStoreTests.swift b/ShadowsocksX-NG2/Tests/CatalogFileStoreTests.swift index 41f4a1da..ae22344f 100644 --- a/ShadowsocksX-NG2/Tests/CatalogFileStoreTests.swift +++ b/ShadowsocksX-NG2/Tests/CatalogFileStoreTests.swift @@ -45,7 +45,7 @@ final class CatalogFileStoreTests: XCTestCase { try store.save(CatalogDocument(catalog: catalog)) try catalog.remove(first) - let second = try catalog.addTestServer("second") + _ = try catalog.addTestServer("second") try store.save(CatalogDocument(catalog: catalog)) XCTAssertEqual(try store.load().catalog, catalog) diff --git a/ShadowsocksX-NG2/Tests/CatalogFixtures.swift b/ShadowsocksX-NG2/Tests/CatalogFixtures.swift index ad77fc43..57547b90 100644 --- a/ShadowsocksX-NG2/Tests/CatalogFixtures.swift +++ b/ShadowsocksX-NG2/Tests/CatalogFixtures.swift @@ -87,9 +87,10 @@ extension ConfigurationCatalog { } /// `XCTAssertThrowsError` 的 async 版本(表达式在 await 之后才能检查)。 +@MainActor func expectThrowsAsync( - _ expression: () async throws -> Void, - onThrow errorHandler: (Error) -> Void = { _ in }, + _ expression: @MainActor () async throws -> Void, + onThrow errorHandler: @MainActor (Error) -> Void = { _ in }, file: StaticString = #filePath, line: UInt = #line ) async { diff --git a/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshFailures.swift b/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshFailures.swift index 99fe5ade..07291781 100644 --- a/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshFailures.swift +++ b/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshFailures.swift @@ -3,6 +3,7 @@ import XCTest @testable import ShadowsocksX_NG2 +@MainActor extension CatalogWorkflowSubscriptionTests { // MARK: 失败保留语义 @@ -88,6 +89,7 @@ extension CatalogWorkflowSubscriptionTests { } } +@MainActor extension CatalogWorkflowSubscriptionTests { // MARK: Typed failure projection diff --git a/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshOrchestration.swift b/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshOrchestration.swift index ddc962c3..c8c8dc48 100644 --- a/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshOrchestration.swift +++ b/ShadowsocksX-NG2/Tests/CatalogWorkflowSubscriptionTests+RefreshOrchestration.swift @@ -4,6 +4,7 @@ import XCTest @testable import ShadowsocksX_NG2 +@MainActor extension CatalogWorkflowSubscriptionTests { // MARK: 批量刷新与持久化 @@ -46,27 +47,28 @@ extension CatalogWorkflowSubscriptionTests { let record = try await seedSuccessfulSubscription() // 用门控获取器重建工作流:刷新停在 fetch 内部,验证并发守卫。 let gated = GatedFetcher() - workflow = makeWorkflow(fetcher: gated) + let refreshWorkflow = makeWorkflow(fetcher: gated) + workflow = refreshWorkflow - async let first: Void = workflow.refreshSubscription(record.id) + async let first: Void = refreshWorkflow.refreshSubscription(record.id) // 轮询而非阻塞等待:主线程阻塞会饿死继承 MainActor 的 async let 子任务。 while !gated.didEnterFetch { try await Task.sleep(nanoseconds: 10_000_000) } - await workflow.refreshSubscription(record.id) + await refreshWorkflow.refreshSubscription(record.id) XCTAssertEqual(gated.requestCount, 1, "同一订阅刷新进行中不重入") gated.releaseAll() await first - XCTAssertTrue(workflow.refreshingSubscriptionIDs.isEmpty, "完成后守卫释放") + XCTAssertTrue(refreshWorkflow.refreshingSubscriptionIDs.isEmpty, "完成后守卫释放") } } -/// 可门控获取缝:让第一次刷新停在 fetch 内部,验证并发守卫。fetch 在协作 -/// 线程池上阻塞等待放行信号,不占用 MainActor。 +/// 可门控获取缝:让第一次刷新停在 fetch 内部,验证并发守卫。fetch 挂起等待 +/// 异步门闩,不阻塞 MainActor 或协作线程池。 final class GatedFetcher: SubscriptionFetching, @unchecked Sendable { private let lock = NSLock() - private let release = DispatchSemaphore(value: 0) + private let release = AsyncGate() private var count = 0 private var entered = false @@ -83,15 +85,15 @@ final class GatedFetcher: SubscriptionFetching, @unchecked Sendable { } func fetch(_ url: URL) async throws -> Data { - lock.lock() - count += 1 - entered = true - lock.unlock() - _ = release.wait(timeout: .now() + 5) + lock.withLock { + count += 1 + entered = true + } + await release.wait() return SubscriptionDocs.flat(serverCount: 0) } func releaseAll() { - release.signal() + release.release() } } diff --git a/ShadowsocksX-NG2/Tests/CatalogWorkflowTests+RenameAndMove.swift b/ShadowsocksX-NG2/Tests/CatalogWorkflowTests+RenameAndMove.swift index 6e5aeee3..2b17d8bd 100644 --- a/ShadowsocksX-NG2/Tests/CatalogWorkflowTests+RenameAndMove.swift +++ b/ShadowsocksX-NG2/Tests/CatalogWorkflowTests+RenameAndMove.swift @@ -2,6 +2,7 @@ import XCTest @testable import ShadowsocksX_NG2 +@MainActor extension CatalogWorkflowTests { // MARK: - 重命名与移动 @@ -24,7 +25,7 @@ extension CatalogWorkflowTests { let manualGroupID = try await workflow.createGroup(named: "手动组", into: nil) await expectThrowsAsync { try await workflow.move(fixture.serverIDs[0], to: manualGroupID) } - await expectThrowsAsync { try await workflow.remove(fixture.serverIDs[0]) } + await expectThrowsAsync { _ = try await workflow.remove(fixture.serverIDs[0]) } await expectThrowsAsync { try await workflow.renameGroup(fixture.groupID, to: "改名") } await expectThrowsAsync { try await workflow.updateServer( diff --git a/ShadowsocksX-NG2/Tests/CatalogWorkflowTests.swift b/ShadowsocksX-NG2/Tests/CatalogWorkflowTests.swift index 7fe6e6e7..a22c42f7 100644 --- a/ShadowsocksX-NG2/Tests/CatalogWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/CatalogWorkflowTests.swift @@ -220,7 +220,7 @@ final class CatalogWorkflowTests: XCTestCase { func testRemoveServerCleansCredentials() async throws { // 已知凭据引用直建目录(模块投影不含引用;身份经文件夹具固定)。 var catalog = ConfigurationCatalog() - var fields = ServerFields( + let fields = ServerFields( address: "203.0.113.7", port: 8388, encryptionMethod: "aes-256-gcm", passwordRef: CredentialReference(rawValue: "ref-pw"), remark: "删除我", pluginProgram: "v2ray-plugin", diff --git a/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowFixture.swift b/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowFixture.swift index 6909189c..da83908b 100644 --- a/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowFixture.swift +++ b/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowFixture.swift @@ -26,8 +26,8 @@ class DiagnosticsWorkflowTestCase: XCTestCase { var facts: FakeRuntimeFacts! var events: RuntimeEventStore! - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() workDir = FileManager.default.temporaryDirectory .appendingPathComponent("ssxng-diagwf-\(UUID().uuidString)", isDirectory: true) try? FileManager.default.createDirectory(at: workDir, withIntermediateDirectories: true) @@ -37,18 +37,18 @@ class DiagnosticsWorkflowTestCase: XCTestCase { RuntimeLog.setSink(events) } - override func tearDownWithError() throws { + override func tearDown() async throws { RuntimeLog.setSink(RuntimeEventStore.shared) try? FileManager.default.removeItem(at: workDir) - try super.tearDownWithError() + try await super.tearDown() } /// 标准 workflow:事实源注入替身、固定时钟(报告生成时间可精确断言)。 func makeWorkflow( catalog: @escaping @MainActor () -> DiagnosticCatalogFacts? = { nil }, agentLog: @escaping () -> String? = { nil }, - fileFacts: @escaping () -> [DiagnosticFileFacts] = { [] }, - managedPlugins: @escaping () -> [DiagnosticPluginFacts] = { [] }, + fileFacts: @escaping @MainActor () -> [DiagnosticFileFacts] = { [] }, + managedPlugins: @escaping @MainActor () -> [DiagnosticPluginFacts] = { [] }, homePath: @escaping () -> String? = { nil }, render: @escaping (DiagnosticSnapshot) -> String? = { DiagnosticReportBuilder.markdown(from: $0) diff --git a/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowLifecycleTests.swift b/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowLifecycleTests.swift index 527b3411..665aea18 100644 --- a/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowLifecycleTests.swift +++ b/ShadowsocksX-NG2/Tests/DiagnosticsWorkflowLifecycleTests.swift @@ -47,7 +47,7 @@ final class DiagnosticsWorkflowLifecycleTests: DiagnosticsWorkflowTestCase { try Data("{\"servers\":1}".utf8).write(to: fileURL) try FileManager.default.setAttributes( [.posixPermissions: 0o600], ofItemAtPath: fileURL.path) - let fileFacts: () -> [DiagnosticFileFacts] = { + let fileFacts: @MainActor @Sendable () -> [DiagnosticFileFacts] = { [DiagnosticFileCollector.collect(label: "catalog.json", url: fileURL)] } let beforeFacts = fileFacts() diff --git a/ShadowsocksX-NG2/Tests/EndpointHealthProbeTests.swift b/ShadowsocksX-NG2/Tests/EndpointHealthProbeTests.swift index d72dda9b..3ec399fb 100644 --- a/ShadowsocksX-NG2/Tests/EndpointHealthProbeTests.swift +++ b/ShadowsocksX-NG2/Tests/EndpointHealthProbeTests.swift @@ -8,7 +8,7 @@ final class EndpointHealthProbeTests: XCTestCase { /// 绑定回环临时端口;调用方负责 close 返回的描述符。 private func bindLoopbackListener() throws -> (descriptor: Int32, port: Int) { let socketFD = socket(AF_INET, SOCK_STREAM, 0) - try XCTUnwrap(socketFD >= 0 ? socketFD : nil, "创建探测用 socket 失败") + _ = try XCTUnwrap(socketFD >= 0 ? socketFD : nil, "创建探测用 socket 失败") var address = sockaddr_in() address.sin_family = sa_family_t(AF_INET) diff --git a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift index 9fb89730..838ae3f9 100644 --- a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowIntegrationTests.swift @@ -15,8 +15,8 @@ final class ProxyControlWorkflowIntegrationTests: XCTestCase { var agent: ProxyRuntimeFixture.FakeLaunchAgent! var systemProxy: ProxyRuntimeFixture.FakeSystemProxy! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() runtime = ProxyRuntimeFixture.makeTemporaryRuntime() catalogFileURL = runtime.directory.appendingPathComponent("catalog.json") activationFileURL = runtime.directory.appendingPathComponent("activation.json") @@ -25,9 +25,9 @@ final class ProxyControlWorkflowIntegrationTests: XCTestCase { systemProxy = ProxyRuntimeFixture.FakeSystemProxy() } - override func tearDownWithError() throws { + override func tearDown() async throws { try? FileManager.default.removeItem(at: runtime.directory) - try super.tearDownWithError() + try await super.tearDown() } /// 控制器、目录工作流与代理控制工作流的生产组合(同一控制器经两条缝)。 diff --git a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowTests.swift b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowTests.swift index 696bee5b..07d635cd 100644 --- a/ShadowsocksX-NG2/Tests/ProxyControlWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyControlWorkflowTests.swift @@ -81,8 +81,8 @@ final class ProxyControlWorkflowTests: XCTestCase { private var targetFacts: FakeTargetFacts! private var workflow: ProxyControlWorkflow! - override func setUp() { - super.setUp() + override func setUp() async throws { + try await super.setUp() runtime = FakeProxyRuntime() targetFacts = FakeTargetFacts() workflow = ProxyControlWorkflow(runtime: runtime, targetFacts: targetFacts) diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift index 4e35cdbf..72beee8a 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerModeTests.swift @@ -33,8 +33,8 @@ final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase { private var agent: ProxyRuntimeFixture.FakeLaunchAgent! private var systemProxy: ProxyRuntimeFixture.FakeSystemProxy! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() runtime = ProxyRuntimeFixture.makeTemporaryRuntime() catalogFileURL = runtime.directory.appendingPathComponent("catalog.json") activationFileURL = runtime.directory.appendingPathComponent("activation.json") @@ -43,9 +43,9 @@ final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase { systemProxy = ProxyRuntimeFixture.FakeSystemProxy() } - override func tearDownWithError() throws { + override func tearDown() async throws { try? FileManager.default.removeItem(at: runtime.directory) - try super.tearDownWithError() + try await super.tearDown() } func testCommitWithoutActiveTargetFeedsLaterActivationThroughSharedSnapshot() async throws { @@ -85,7 +85,7 @@ final class CatalogRuntimeSnapshotIntegrationTests: XCTestCase { runtime: ProxyRuntimeSyncAdapter(controller: controller), bootstrap: bootstrap) - try coordinator.commit { catalog, _ in try catalog.remove(server) } + _ = try coordinator.commit { catalog, _ in try catalog.remove(server) } await waitUntilRuntimeSettles( { diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift index 17350ca2..457eab60 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+AgentAndSystemProxy.swift @@ -212,7 +212,7 @@ extension ProxyRuntimeControllerTests { try CatalogFileStore(fileURL: catalogFileURL).save(CatalogDocument(catalog: catalog)) let committed = try CatalogFileStore(fileURL: catalogFileURL).load().catalog - await controller.catalogDidCommit(snapshot: committed) + _ = await controller.catalogDidCommit(snapshot: committed) XCTAssertNil(controller.activeTargetID) XCTAssertNotNil(controller.lastActivationFailure, "点名原因独立呈现") diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift index a06c9c6d..2ee9d1a4 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+GlobalMode.swift @@ -119,7 +119,7 @@ extension ProxyRuntimeControllerTests { XCTAssertEqual(controller.proxyMode, .rule, "新实例未验证时恢复旧模式") XCTAssertEqual(controller.settings.preferredMode, .rule) XCTAssertEqual(settingsStore.saved?.preferredMode, .rule) - XCTAssertEqual(try runtimeStore.loadDocument(), previousDocument) + XCTAssertEqual(runtimeStore.loadDocument(), previousDocument) XCTAssertEqual(controller.state, .running, "旧运行时恢复后重新呈现健康") XCTAssertEqual(controller.systemProxyState, .applied) XCTAssertEqual(systemProxy.applied.count, previousApplicationCount) diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift index a779439b..529da150 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests+Mode.swift @@ -124,7 +124,7 @@ extension ProxyRuntimeControllerTests { XCTAssertEqual(controller.proxyMode, .rule, "新实例未验证时恢复旧模式") XCTAssertEqual(controller.settings.preferredMode, .rule) XCTAssertEqual(settingsStore.saved?.preferredMode, .rule) - XCTAssertEqual(try runtimeStore.loadDocument(), previousDocument) + XCTAssertEqual(runtimeStore.loadDocument(), previousDocument) XCTAssertEqual(controller.state, .running, "旧运行时恢复后重新呈现健康") XCTAssertEqual(controller.systemProxyState, .applied) XCTAssertEqual(systemProxy.applied.count, previousApplicationCount) diff --git a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift index e8068efa..7a96559b 100644 --- a/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift +++ b/ShadowsocksX-NG2/Tests/ProxyRuntimeControllerTests.swift @@ -50,8 +50,8 @@ final class ProxyRuntimeControllerTests: XCTestCase { } } - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() runtime = ProxyRuntimeFixture.makeTemporaryRuntime() catalogFileURL = runtime.directory.appendingPathComponent("catalog.json") activationFileURL = runtime.directory.appendingPathComponent("activation.json") @@ -61,9 +61,9 @@ final class ProxyRuntimeControllerTests: XCTestCase { signals = SignalRecorder() } - override func tearDownWithError() throws { + override func tearDown() async throws { try? FileManager.default.removeItem(at: runtime.directory) - try super.tearDownWithError() + try await super.tearDown() } /// 建一个含单台服务器的目录并落盘(服务器密码进内存凭据存储)。 @@ -293,7 +293,7 @@ final class ProxyRuntimeControllerTests: XCTestCase { // 协调器生产适配入口(issue #40):以刚提交的内存快照重展开。 let committed = try CatalogFileStore(fileURL: catalogFileURL).load().catalog - await controller.catalogDidCommit(snapshot: committed) + _ = await controller.catalogDidCommit(snapshot: committed) XCTAssertEqual(controller.state, .running) let expectedRemark = "新加坡 01" diff --git a/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift b/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift index 596d9060..91124a07 100644 --- a/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift +++ b/ShadowsocksX-NG2/Tests/RuntimeFileStoreTests.swift @@ -76,7 +76,7 @@ final class RuntimeFileStoreTests: XCTestCase { XCTAssertEqual( try permissions(of: store.aclVariantFileURL(summary: "direct")), 0o600, "变体文件 0600;链接权限由文件系统决定,不作断言") - XCTAssertEqual(try store.loadDocument(), document) + XCTAssertEqual(store.loadDocument(), document) } /// ADR-0011:变体落在 `acl-.ini`,契约路径是稳定的 `acl-active.ini` @@ -162,7 +162,7 @@ final class RuntimeFileStoreTests: XCTestCase { failingStore.aclVariantFileURL(summary: "direct").lastPathComponent, "契约写失败后链接回到此前指向") XCTAssertEqual( - try failingStore.loadDocument(), previous, + failingStore.loadDocument(), previous, "契约未写成功,读取侧仍看到此前文档") } @@ -201,19 +201,19 @@ final class RuntimeFileStoreTests: XCTestCase { let document = ProxyRuntimeFixture.makeDocument() try store.write(document) - XCTAssertEqual(try store.loadDocument(), document) - XCTAssertEqual(try store.readData(), try document.jsonData()) + XCTAssertEqual(store.loadDocument(), document) + XCTAssertEqual(store.readData(), try document.jsonData()) } func testLoadDocumentReturnsNilForMissingFile() throws { - XCTAssertNil(try store.loadDocument()) - XCTAssertNil(try store.readData()) + XCTAssertNil(store.loadDocument()) + XCTAssertNil(store.readData()) } func testLoadDocumentReturnsNilForCorruptJSON() throws { try Data("not json {".utf8).write(to: runtime.contract) - XCTAssertNil(try store.loadDocument()) + XCTAssertNil(store.loadDocument()) } func testLoadDocumentReturnsNilForStructurallyInvalidDocument() throws { @@ -221,7 +221,7 @@ final class RuntimeFileStoreTests: XCTestCase { SslocalRuntimeDocument( servers: [], listen: SslocalListenSettings(socksPort: 0))) - XCTAssertNil(try store.loadDocument(), "本地端口无效属结构性无效(读取侧防御)") + XCTAssertNil(store.loadDocument(), "本地端口无效属结构性无效(读取侧防御)") } // MARK: 显式停止清理 diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift index b354bf12..efcfdd50 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowInterfaceTests.swift @@ -9,8 +9,8 @@ final class SettingsWorkflowInterfaceTests: XCTestCase { var committing: FakeSettingsCommitter! var probe: FakeOccupancyProbe! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() committing = FakeSettingsCommitter() probe = FakeOccupancyProbe() } diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowTestSupport.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowTestSupport.swift index 60a06c6b..5846e52b 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowTestSupport.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowTestSupport.swift @@ -4,6 +4,7 @@ import XCTest @testable import ShadowsocksX_NG2 /// 轮询等待异步投影落定(设置工作流测试共享)。 +@MainActor func waitUntil( _ condition: @autoclosure () -> Bool, timeout: TimeInterval = 2, diff --git a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift index 4e6a30e6..f2e8bdda 100644 --- a/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift +++ b/ShadowsocksX-NG2/Tests/SettingsWorkflowTests.swift @@ -13,8 +13,8 @@ final class SettingsWorkflowTests: XCTestCase { private var systemProxy: ProxyRuntimeFixture.FakeSystemProxy! private var settingsStore: InMemorySettingsStore! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() runtime = ProxyRuntimeFixture.makeTemporaryRuntime() catalogFileURL = runtime.directory.appendingPathComponent("catalog.json") activationFileURL = runtime.directory.appendingPathComponent("activation.json") @@ -24,9 +24,9 @@ final class SettingsWorkflowTests: XCTestCase { settingsStore = InMemorySettingsStore() } - override func tearDownWithError() throws { + override func tearDown() async throws { try? FileManager.default.removeItem(at: runtime.directory) - try super.tearDownWithError() + try await super.tearDown() } func testListenerModeSavePersistsWithoutStartingAnOffAgent() async throws { diff --git a/ShadowsocksX-NG2/Tests/SkeletonTests.swift b/ShadowsocksX-NG2/Tests/SkeletonTests.swift index c21a879e..0a7fb320 100644 --- a/ShadowsocksX-NG2/Tests/SkeletonTests.swift +++ b/ShadowsocksX-NG2/Tests/SkeletonTests.swift @@ -2,13 +2,15 @@ import XCTest @testable import ShadowsocksX_NG2 +@MainActor final class SkeletonTests: XCTestCase { // 以下两个 Bundle.main 断言依赖测试包以 app 为 TEST_HOST 注入(target dependency); // 若移除该依赖,Bundle.main 不再是 app bundle,断言会失效。 func testMenuBarAppDoesNotTerminateAfterLastWindowClosed() { let delegate = AppDelegate() - XCTAssertFalse(delegate.applicationShouldTerminateAfterLastWindowClosed(.shared)) + let shouldTerminate = delegate.applicationShouldTerminateAfterLastWindowClosed(.shared) + XCTAssertFalse(shouldTerminate) } func testAppBundleIDIsDistinctFromLegacy() { diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift index 0d6c6019..e9899eee 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+ACLRouting.swift @@ -198,13 +198,15 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeHit = fakeSS.connectionCount - performSocksConnectReply(socksPort: listen.socksPort, targetHost: "8.8.8.8", targetPort: 53) + _ = performSocksConnectReply( + socksPort: listen.socksPort, targetHost: "8.8.8.8", targetPort: 53) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( fakeSS.connectionCount, beforeHit, "命中中国 IPv4 CIDR 的目标应直连,不得触达 Shadowsocks 出口") - performSocksConnectReply(socksPort: listen.socksPort, targetHost: "1.1.1.1", targetPort: 443) + _ = performSocksConnectReply( + socksPort: listen.socksPort, targetHost: "1.1.1.1", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > beforeHit }, "未命中 CIDR 的公网目标应连接 Shadowsocks 出口") @@ -242,7 +244,8 @@ extension RealSslocalSmokeTests { ) throws { // .cn 域名候选直连:SOCKS CONNECT example.cn 不应触达 SS 出口。 let beforeCN = fakeSS.connectionCount - performSocksConnectReply(socksPort: listen.socksPort, targetHost: "example.cn", targetPort: 443) + _ = performSocksConnectReply( + socksPort: listen.socksPort, targetHost: "example.cn", targetPort: 443) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( fakeSS.connectionCount, beforeCN, diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift index 0bfb613b..26f6d8b8 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+CustomRules.swift @@ -60,21 +60,21 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeDirect = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "sub.direct.example", targetPort: 443) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( fakeSS.connectionCount, beforeDirect, "自定义域名后缀直连规则不得触达 Shadowsocks 出口") - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "exact.direct.example", targetPort: 443) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( fakeSS.connectionCount, beforeDirect, "自定义完整域名直连规则不得触达 Shadowsocks 出口") - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, request: socksIPv4ConnectRequest([203, 0, 113, 10], port: 443)) Thread.sleep(forTimeInterval: 0.5) @@ -88,7 +88,7 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeHit = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "unmatched.example.org", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > beforeHit }, @@ -157,14 +157,14 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeHit = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "sub.blocked.example", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > beforeHit }, "自定义代理候选应连接 Shadowsocks 出口") let afterHit = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "unmatched.example.org", targetPort: 443) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( diff --git a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift index d750d92f..9f270158 100644 --- a/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift +++ b/ShadowsocksX-NG2/Tests/Smoke/RealSslocalSmokeTests+GFWListACL.swift @@ -129,7 +129,7 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeDomain = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "sub.example.com", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > beforeDomain }, @@ -142,7 +142,7 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeIP = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, request: socksIPv4ConnectRequest([8, 8, 8, 8], port: 53)) Thread.sleep(forTimeInterval: 0.5) @@ -150,7 +150,7 @@ extension RealSslocalSmokeTests { fakeSS.connectionCount, beforeIP, "IP 优先级:bypass_list 应优先于 proxy_list") - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, request: socksIPv4ConnectRequest([8, 8, 8, 1], port: 53)) XCTAssertTrue( @@ -164,28 +164,28 @@ extension RealSslocalSmokeTests { listen: SslocalListenSettings, fakeSS: ConnectionCountingServer ) throws { let beforeHit = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "blocked.example", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > beforeHit }, "GFWList 代理候选应连接 Shadowsocks 出口") let afterHit = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "cdn.blocked.example", targetPort: 443) XCTAssertTrue( try waitForCondition(timeout: 5) { fakeSS.connectionCount > afterHit }, "GFWList 子域代理候选应连接 Shadowsocks 出口") let afterMatched = fakeSS.connectionCount - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, targetHost: "unmatched.example.org", targetPort: 443) Thread.sleep(forTimeInterval: 0.5) XCTAssertEqual( fakeSS.connectionCount, afterMatched, "未匹配域名在 bypass_all 下应直连,不得触达 Shadowsocks 出口") - performSocksConnectReply( + _ = performSocksConnectReply( socksPort: listen.socksPort, request: socksIPv4ConnectRequest([1, 1, 1, 1], port: 443)) Thread.sleep(forTimeInterval: 0.5) diff --git a/ShadowsocksX-NG2/Tests/SubscriptionFetcherTests.swift b/ShadowsocksX-NG2/Tests/SubscriptionFetcherTests.swift index 2275cdc9..a411b956 100644 --- a/ShadowsocksX-NG2/Tests/SubscriptionFetcherTests.swift +++ b/ShadowsocksX-NG2/Tests/SubscriptionFetcherTests.swift @@ -169,10 +169,10 @@ final class FakeSubscriptionFetcher: SubscriptionFetching, @unchecked Sendable { } func fetch(_ url: URL) async throws -> Data { - lock.lock() - requests.append(url) - let behavior = self.behavior - lock.unlock() + let behavior = lock.withLock { + requests.append(url) + return self.behavior + } switch behavior { case .success(let data): return data diff --git a/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift b/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift index acf54ca5..1477d79b 100644 --- a/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift +++ b/ShadowsocksX-NG2/Tests/UnreadableListenSettingsTests.swift @@ -13,8 +13,8 @@ final class UnreadableListenSettingsTests: XCTestCase { private var agent: ProxyRuntimeFixture.FakeLaunchAgent! private var systemProxy: ProxyRuntimeFixture.FakeSystemProxy! - override func setUpWithError() throws { - try super.setUpWithError() + override func setUp() async throws { + try await super.setUp() runtime = ProxyRuntimeFixture.makeTemporaryRuntime() catalogFileURL = runtime.directory.appendingPathComponent("catalog.json") activationFileURL = runtime.directory.appendingPathComponent("activation.json") @@ -23,9 +23,9 @@ final class UnreadableListenSettingsTests: XCTestCase { systemProxy = ProxyRuntimeFixture.FakeSystemProxy() } - override func tearDownWithError() throws { + override func tearDown() async throws { try? FileManager.default.removeItem(at: runtime.directory) - try super.tearDownWithError() + try await super.tearDown() } private func makeController( diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml index 1020a799..a6fb92e3 100644 --- a/ShadowsocksX-NG2/project.yml +++ b/ShadowsocksX-NG2/project.yml @@ -7,7 +7,7 @@ options: settings: base: ARCHS: arm64 - SWIFT_VERSION: "5.0" + SWIFT_VERSION: "6.0" CODE_SIGN_STYLE: Manual DEVELOPMENT_TEAM: S878RH3PA8 ENABLE_HARDENED_RUNTIME: YES @@ -164,7 +164,7 @@ schemes: ShadowsocksX-NG2Smoke: build: targets: - ShadowsocksX-NG2: [build] + ShadowsocksX-NG2: [run, test] ShadowsocksX-NG2SmokeTests: [test] test: targets: