Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -53,7 +52,6 @@ if isDevelop {
name: "StubNetworkKitTests",
dependencies: [
"StubNetworkKit",
.product(name: "Testing", package: "swift-testing"),
"Alamofire",
],
resources: [.copy("_Fixtures")]
Expand All @@ -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
Comment on lines +64 to +71

Copilot AI Oct 27, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build-time conditional compilation directives like #if compiler(<6.0) are evaluated during package manifest compilation, not during target compilation. This means the condition will be checked against the Swift compiler version used to build the Package.swift file itself, not the version used to build the targets. This will not achieve the intended backward compatibility. Consider using target-level conditions or separate package manifests for different Swift versions instead.

Suggested change
#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
// NOTE: Conditional compilation directives like `#if compiler(<6.0)` are evaluated during manifest compilation,
// not during target compilation. To support Swift < 6.0, consider using separate manifests or document this requirement.
// If you need to add the swift-testing dependency only for Swift < 6.0, update the manifest manually as needed.

Copilot uses AI. Check for mistakes.
package.targets.append(testTarget)

if isObjcAvailable {
Expand Down
15 changes: 10 additions & 5 deletions Sources/StubNetworkKit/StubURLProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import FoundationNetworking

// based on https://github.com/417-72KI/MultipartFormDataParser/blob/main/Tests/MultipartFormDataParserTests/StubURLProtocol.swift
final class StubURLProtocol: URLProtocol {

Copilot AI Oct 31, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OSAllocatedUnfairLock is only available on macOS 13.0+, iOS 16.0+, watchOS 9.0+, and tvOS 16.0+. While this aligns with the package's minimum platform versions in Package.swift, the lack of an explicit @available annotation makes the platform requirements less clear and could cause issues if the minimum platform versions are lowered in the future. Consider adding an @available annotation to make the dependency explicit.

Suggested change
final class StubURLProtocol: URLProtocol {
final class StubURLProtocol: URLProtocol {
@available(macOS 13.0, iOS 16.0, watchOS 9.0, tvOS 16.0, *)

Copilot uses AI. Check for mistakes.
nonisolated(unsafe) private(set) static var stubs: [Stub] = []
private static let stubs = Lock([Stub]())

override static func canInit(with request: URLRequest) -> Bool {
true
Expand Down Expand Up @@ -54,17 +54,22 @@ final class StubURLProtocol: URLProtocol {

extension StubURLProtocol {
static func register(_ stub: Stub) {
stubs.append(stub)
stubs.withLock { stubs in
stubs.append(stub)
}
}

static func reset() {
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) })
}
}
}
39 changes: 39 additions & 0 deletions Sources/StubNetworkKit/Util/Lock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import Foundation

#if canImport(os)
import os

final class Lock<State: Sendable>: Sendable {
let lock: OSAllocatedUnfairLock<State>
Comment on lines +3 to +7

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock is only defined when either os or Synchronization can be imported. In the repo’s CI there is a Linux test job (Swift 5.10–6.2), where canImport(os) is false and Synchronization may not be available (e.g., Swift 5.10), leaving no Lock type and breaking the build. Add a cross-platform fallback implementation (e.g., NSLock/pthread_mutex/DispatchSemaphore) under an #else branch so the module compiles on Linux.

Copilot uses AI. Check for mistakes.

init(_ initialState: State) {
lock = .init(initialState: initialState)
}
}

extension Lock {
@inlinable
func withLock<R: Sendable>(_ body: @Sendable (inout State) throws -> R) rethrows -> R {

Copilot AI May 1, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The withLock wrapper constrains the return type to R: Sendable, which is stricter than the underlying lock APIs and can make this utility unusable for non-Sendable intermediate values inside the module. Unless you specifically need this constraint, consider removing it to keep Lock generally usable.

Suggested change
func withLock<R: Sendable>(_ body: @Sendable (inout State) throws -> R) rethrows -> R {
func withLock<R>(_ body: @Sendable (inout State) throws -> R) rethrows -> R {

Copilot uses AI. Check for mistakes.
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<Value: Sendable>: Sendable {
let lock: Mutex<Value>

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<Result: ~Copyable, E: Error>(_ body: (inout sending Value) throws(E) -> sending Result) throws(E) -> sending Result {
try lock.withLock(body)
}
}
#endif
1 change: 0 additions & 1 deletion TestPlan.xctestplan
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"defaultOptions" : {
"maximumTestRepetitions" : 3,
"nsZombieEnabled" : true,
"repeatInNewRunnerProcess" : true,
"testRepetitionMode" : "retryOnFailure",
"testTimeoutsEnabled" : true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading