From 299cdfd55ee7fe2673d34001c874e42a30b790cd Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Sat, 26 Oct 2024 14:22:32 +0900 Subject: [PATCH 1/6] use swift-testing only in macOS --- Package.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index e1ebd75..c80fee6 100644 --- a/Package.swift +++ b/Package.swift @@ -39,7 +39,6 @@ let package = Package( if isDevelop { package.dependencies.append(contentsOf: [ - .package(url: "https://github.com/apple/swift-testing", exact: "0.3.0"), .package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.10.0"), ]) if isObjcAvailable { @@ -53,7 +52,6 @@ if isDevelop { name: "StubNetworkKitTests", dependencies: [ "StubNetworkKit", - .product(name: "Testing", package: "swift-testing"), "Alamofire", ], resources: [.copy("_Fixtures")] @@ -63,6 +61,14 @@ if isDevelop { "APIKit", ]) } + #if compiler(<6.0) + package.dependencies.append(contentsOf: [ + .package(url: "https://github.com/apple/swift-testing", exact: "0.3.0"), + ]) + testTarget.dependencies.append(contentsOf: [ + .product(name: "Testing", package: "swift-testing"), + ]) + #endif package.targets.append(testTarget) if isObjcAvailable { From 27a50733c4a71c41ec41d3fdfc6526e307a3b9fd Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Sat, 26 Oct 2024 14:22:53 +0900 Subject: [PATCH 2/6] apply testplan --- TestPlan.xctestplan | 1 - 1 file changed, 1 deletion(-) diff --git a/TestPlan.xctestplan b/TestPlan.xctestplan index b39a742..1b0ae2f 100644 --- a/TestPlan.xctestplan +++ b/TestPlan.xctestplan @@ -11,7 +11,6 @@ "defaultOptions" : { "maximumTestRepetitions" : 3, "nsZombieEnabled" : true, - "repeatInNewRunnerProcess" : true, "testRepetitionMode" : "retryOnFailure", "testTimeoutsEnabled" : true }, From c27e4def24c58c08cf0e03330d260b8fdfce5cf9 Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Sat, 26 Oct 2024 14:23:11 +0900 Subject: [PATCH 3/6] prevent data races with stubs --- Sources/StubNetworkKit/StubURLProtocol.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/StubNetworkKit/StubURLProtocol.swift b/Sources/StubNetworkKit/StubURLProtocol.swift index 0d56ebb..1780f8c 100644 --- a/Sources/StubNetworkKit/StubURLProtocol.swift +++ b/Sources/StubNetworkKit/StubURLProtocol.swift @@ -7,6 +7,8 @@ import FoundationNetworking final class StubURLProtocol: URLProtocol { nonisolated(unsafe) private(set) static var stubs: [Stub] = [] + private static let lock = NSLock() + override static func canInit(with request: URLRequest) -> Bool { true } @@ -54,10 +56,14 @@ final class StubURLProtocol: URLProtocol { extension StubURLProtocol { static func register(_ stub: Stub) { + lock.lock() + defer { lock.unlock() } stubs.append(stub) } static func reset() { + lock.lock() + defer { lock.unlock() } stubs = [] } } From d8f631076c88e9704049cdb6cdfd54e83fcb8271 Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Sat, 16 Nov 2024 01:20:34 +0900 Subject: [PATCH 4/6] add compiler condition to run serial --- .../Examples/StubNetworkKitTests_SwiftTesting.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Tests/StubNetworkKitTests/Examples/StubNetworkKitTests_SwiftTesting.swift b/Tests/StubNetworkKitTests/Examples/StubNetworkKitTests_SwiftTesting.swift index e0f40f1..5fb7952 100644 --- a/Tests/StubNetworkKitTests/Examples/StubNetworkKitTests_SwiftTesting.swift +++ b/Tests/StubNetworkKitTests/Examples/StubNetworkKitTests_SwiftTesting.swift @@ -5,7 +5,11 @@ import FoundationNetworking import Testing import StubNetworkKit +#if compiler(<6.0) @Suite +#else +@Suite(.serialized) +#endif final class StubNetworkKitTests_SwiftTesting { init() { StubNetworking.option(printDebugLog: true, From 6f0955826254f85998fc5012be24659dad11f5d0 Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Fri, 31 Oct 2025 11:32:21 +0900 Subject: [PATCH 5/6] use `OSAllocatedUnfairLock` instead of NSLock ref: https://zenn.dev/treastrain/articles/6ef73efccb33ff --- Sources/StubNetworkKit/StubURLProtocol.swift | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/Sources/StubNetworkKit/StubURLProtocol.swift b/Sources/StubNetworkKit/StubURLProtocol.swift index 1780f8c..2b33f27 100644 --- a/Sources/StubNetworkKit/StubURLProtocol.swift +++ b/Sources/StubNetworkKit/StubURLProtocol.swift @@ -2,12 +2,11 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif +import os // based on https://github.com/417-72KI/MultipartFormDataParser/blob/main/Tests/MultipartFormDataParserTests/StubURLProtocol.swift final class StubURLProtocol: URLProtocol { - nonisolated(unsafe) private(set) static var stubs: [Stub] = [] - - private static let lock = NSLock() + private static let stubs = OSAllocatedUnfairLock(initialState: [Stub]()) override static func canInit(with request: URLRequest) -> Bool { true @@ -56,21 +55,22 @@ final class StubURLProtocol: URLProtocol { extension StubURLProtocol { static func register(_ stub: Stub) { - lock.lock() - defer { lock.unlock() } - stubs.append(stub) + stubs.withLock { stubs in + stubs.append(stub) + } } static func reset() { - lock.lock() - defer { lock.unlock() } - stubs = [] + stubs.withLock { + $0 = [] + } } } private extension StubURLProtocol { func stub(with request: URLRequest) -> Stub? { - Self.stubs - .last(where: { $0.matcher(request) }) + Self.stubs.withLock { + $0.last(where: { $0.matcher(request) }) + } } } From 5d992ca32c6e8966f7ccd2403f69bd06f3fcf940 Mon Sep 17 00:00:00 2001 From: 417-72KI <417.72ki@gmail.com> Date: Sat, 2 May 2026 01:42:58 +0900 Subject: [PATCH 6/6] wrap lock for multiple platforms --- Sources/StubNetworkKit/StubURLProtocol.swift | 3 +- Sources/StubNetworkKit/Util/Lock.swift | 39 ++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 Sources/StubNetworkKit/Util/Lock.swift diff --git a/Sources/StubNetworkKit/StubURLProtocol.swift b/Sources/StubNetworkKit/StubURLProtocol.swift index 2b33f27..b232877 100644 --- a/Sources/StubNetworkKit/StubURLProtocol.swift +++ b/Sources/StubNetworkKit/StubURLProtocol.swift @@ -2,11 +2,10 @@ import Foundation #if canImport(FoundationNetworking) import FoundationNetworking #endif -import os // based on https://github.com/417-72KI/MultipartFormDataParser/blob/main/Tests/MultipartFormDataParserTests/StubURLProtocol.swift final class StubURLProtocol: URLProtocol { - private static let stubs = OSAllocatedUnfairLock(initialState: [Stub]()) + private static let stubs = Lock([Stub]()) override static func canInit(with request: URLRequest) -> Bool { true diff --git a/Sources/StubNetworkKit/Util/Lock.swift b/Sources/StubNetworkKit/Util/Lock.swift new file mode 100644 index 0000000..78165a8 --- /dev/null +++ b/Sources/StubNetworkKit/Util/Lock.swift @@ -0,0 +1,39 @@ +import Foundation + +#if canImport(os) +import os + +final class Lock: Sendable { + let lock: OSAllocatedUnfairLock + + init(_ initialState: State) { + lock = .init(initialState: initialState) + } +} + +extension Lock { + @inlinable + func withLock(_ body: @Sendable (inout State) throws -> R) rethrows -> R { + try lock.withLock(body) + } +} +#elseif canImport(Synchronization) +import Synchronization + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +final class Lock: Sendable { + let lock: Mutex + + init(_ initialValue: Value) { + lock = .init(initialValue) + } +} + +@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *) +extension Lock { + @inlinable + func withLock(_ body: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result { + try lock.withLock(body) + } +} +#endif