mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-30 14:13:50 +00:00
build: migrate NG2 to Swift 6 and clear warnings
This commit is contained in:
@@ -3,6 +3,7 @@ import AppKit
|
||||
/// 混合应用形态的 AppKit 生命周期家:LSUIElement 让进程默认纯后台
|
||||
/// (accessory),这里负责前台形态的进入与退出——任意路径使主窗口可见
|
||||
/// 时切成普通应用(regular),最后一个窗口关闭后切回纯后台。
|
||||
@MainActor
|
||||
final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
/// 混合形态的硬性要求:关窗不退进程,代理 GUI 仍驻留菜单栏。
|
||||
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||
|
||||
@@ -185,6 +185,7 @@ enum ActivationCommandOutcome: Equatable, Sendable {
|
||||
|
||||
/// 激活缝(issue #41):目录命令面经此发出激活意图。生产 adapter 为
|
||||
/// `ProxyRuntimeController`;测试注入假 adapter 观察目标传递。
|
||||
@MainActor
|
||||
protocol Activating: AnyObject {
|
||||
func activate(_ target: NodeID) async throws -> ActivationCommandOutcome
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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「重新校验同步」)。
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+15
-13
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/// 控制器、目录工作流与代理控制工作流的生产组合(同一控制器经两条缝)。
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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, "点名原因独立呈现")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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-<summary>.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: 显式停止清理
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import XCTest
|
||||
@testable import ShadowsocksX_NG2
|
||||
|
||||
/// 轮询等待异步投影落定(设置工作流测试共享)。
|
||||
@MainActor
|
||||
func waitUntil(
|
||||
_ condition: @autoclosure () -> Bool,
|
||||
timeout: TimeInterval = 2,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user