diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index dbcd7c1b..697b4b3e 100644 --- a/Package@swift-5.9.swift +++ b/Package@swift-5.9.swift @@ -19,7 +19,7 @@ let package = Package( .library( name: "DependenciesMacros", targets: ["DependenciesMacros"] - ) + ), ], dependencies: [ .package(url: "https://github.com/apple/swift-syntax", from: "509.0.0"), @@ -31,6 +31,13 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], targets: [ + .target( + name: "DependenciesTestObserver", + dependencies: [ + "Dependencies", + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ] + ), .target( name: "Dependencies", dependencies: [ @@ -85,6 +92,16 @@ let package = Package( ) #endif +#if !os(macOS) +package.products.append( + .library( + name: "DependenciesTestObserver", + type: .dynamic, + targets: ["DependenciesTestObserver"] + ) +) +#endif + //for target in package.targets { // target.swiftSettings = target.swiftSettings ?? [] // target.swiftSettings?.append( diff --git a/Sources/Dependencies/DependencyValues.swift b/Sources/Dependencies/DependencyValues.swift index a73374f3..9db8fd3a 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -1,4 +1,23 @@ import Foundation +#if os(Windows) + import WinSDK +#elseif os(Linux) + import Glibc +#endif + +#if _runtime(_ObjC) +extension DispatchQueue { + fileprivate static func mainSync(execute block: @Sendable () -> R) -> R { + if Thread.isMainThread { + return block() + } else { + return Self.main.sync(execute: block) + } + } +} + +final class TestObserver: NSObject {} +#endif /// A collection of dependencies that is globally available. /// @@ -86,7 +105,8 @@ public struct DependencyValues: Sendable { #endif @TaskLocal static var currentDependency = CurrentDependency() - fileprivate var cachedValues = CachedValues() + @_spi(DependenciesTestObserver) + public var cachedValues = CachedValues() private var storage: [ObjectIdentifier: AnySendable] = [:] /// Creates a dependency values instance. @@ -95,8 +115,44 @@ public struct DependencyValues: Sendable { /// provide access only to default values. Instead, you rely on the dependency values' instance /// that the library manages for you when you use the ``Dependency`` property wrapper. public init() { - #if canImport(XCTest) - _ = setUpTestObservers + #if _runtime(_ObjC) + DispatchQueue.mainSync { + guard + let XCTestObservation = objc_getProtocol("XCTestObservation"), + let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter"), + let XCTestObservationCenter = XCTestObservationCenter as Any as? NSObjectProtocol, + let XCTestObservationCenterShared = + XCTestObservationCenter + .perform(Selector(("sharedTestObservationCenter")))? + .takeUnretainedValue() + else { return } + let testCaseWillStartBlock: @convention(block) (AnyObject) -> Void = { _ in + DependencyValues._current.cachedValues.cached = [:] + } + let testCaseWillStartImp = imp_implementationWithBlock(testCaseWillStartBlock) + class_addMethod( + TestObserver.self, Selector(("testCaseWillStart:")), testCaseWillStartImp, nil) + class_addProtocol(TestObserver.self, XCTestObservation) + _ = + XCTestObservationCenterShared + .perform(Selector(("addTestObserver:")), with: TestObserver()) + } + #else + typealias RegisterTestObserver = @convention(c) () -> Void + var pRegisterTestObserver: RegisterTestObserver? = nil + + #if os(Windows) + let hModule = LoadLibraryA("DependenciesTestObserver.dll") + if let hModule, let pAddress = GetProcAddress(hModule, "registerTestObserver") { + pRegisterTestObserver = unsafeBitCast(pAddress, to: RegisterTestObserver.self) + } + #else + let hModule: UnsafeMutableRawPointer? = dlopen("libDependenciesTestObserver.so", RTLD_NOW) + if let hModule, let pAddress = dlsym(hModule, "_registerTestObserver") { + pRegisterTestObserver = unsafeBitCast(pAddress, to: RegisterTestObserver.self) + } + #endif + pRegisterTestObserver?() #endif } @@ -207,8 +263,9 @@ public struct DependencyValues: Sendable { } } -private struct AnySendable: @unchecked Sendable { - let base: Any +@_spi(DependenciesTestObserver) +public struct AnySendable: @unchecked Sendable { + public let base: Any @inlinable init(_ base: Base) { self.base = base @@ -257,14 +314,16 @@ private let defaultContext: DependencyContext = { } }() -private final class CachedValues: @unchecked Sendable { - struct CacheKey: Hashable, Sendable { +@_spi(DependenciesTestObserver) +public final class CachedValues: @unchecked Sendable { + @_spi(DependenciesTestObserver) + public struct CacheKey: Hashable, Sendable { let id: ObjectIdentifier let context: DependencyContext } private let lock = NSRecursiveLock() - fileprivate var cached = [CacheKey: AnySendable]() + public var cached: [CacheKey: AnySendable] = [:] func value( for key: Key.Type, @@ -348,59 +407,3 @@ private final class CachedValues: @unchecked Sendable { return value } } - -// NB: We cannot statically link/load XCTest on Apple platforms, so we dynamically load things -// instead on platforms where XCTest is available. -#if canImport(XCTest) - private let setUpTestObservers: Void = { - if _XCTIsTesting { - #if canImport(ObjectiveC) - DispatchQueue.mainSync { - guard - let XCTestObservation = objc_getProtocol("XCTestObservation"), - let XCTestObservationCenter = NSClassFromString("XCTestObservationCenter"), - let XCTestObservationCenter = XCTestObservationCenter as Any as? NSObjectProtocol, - let XCTestObservationCenterShared = - XCTestObservationCenter - .perform(Selector(("sharedTestObservationCenter")))? - .takeUnretainedValue() - else { return } - let testCaseWillStartBlock: @convention(block) (AnyObject) -> Void = { _ in - DependencyValues._current.cachedValues.cached = [:] - } - let testCaseWillStartImp = imp_implementationWithBlock(testCaseWillStartBlock) - class_addMethod( - TestObserver.self, Selector(("testCaseWillStart:")), testCaseWillStartImp, nil) - class_addProtocol(TestObserver.self, XCTestObservation) - _ = - XCTestObservationCenterShared - .perform(Selector(("addTestObserver:")), with: TestObserver()) - } - #else - XCTestObservationCenter.shared.addTestObserver(TestObserver()) - #endif - } - }() - - #if canImport(ObjectiveC) - private final class TestObserver: NSObject {} - - extension DispatchQueue { - fileprivate static func mainSync(execute block: @Sendable () -> R) -> R { - if Thread.isMainThread { - return block() - } else { - return Self.main.sync(execute: block) - } - } - } - #else - import XCTest - - private final class TestObserver: NSObject, XCTestObservation { - func testCaseWillStart(_ testCase: XCTestCase) { - DependencyValues._current.cachedValues.cached = [:] - } - } - #endif -#endif diff --git a/Sources/DependenciesTestObserver/TestObserver.swift b/Sources/DependenciesTestObserver/TestObserver.swift new file mode 100644 index 00000000..bab49ad9 --- /dev/null +++ b/Sources/DependenciesTestObserver/TestObserver.swift @@ -0,0 +1,21 @@ + +import XCTest +@_spi(DependenciesTestObserver) +import Dependencies +import XCTestDynamicOverlay + +#if !_runtime(_ObjC) + public final class TestObserver: NSObject, XCTestObservation { + public func testCaseWillStart(_ testCase: XCTestCase) { + DependencyValues._current.cachedValues.cached = [:] + } + } +#endif + +@_cdecl("registerTestObserver") +public func registerTestObserver() { + guard _XCTIsTesting else { return } + #if !_runtime(_ObjC) + XCTestObservationCenter.shared.addTestObserver(TestObserver()) + #endif +}