From 0a7b61ed13d46e3115c4b14796e868b19a71ffc8 Mon Sep 17 00:00:00 2001 From: Saleem Abdulrasool Date: Tue, 16 Jan 2024 11:00:57 -0800 Subject: [PATCH] Dependencies: dynamically link XCTest Use delayed dynamic linking for registering the test observer. This is particularly important for Windows which does not support weak linking and weak symbols. Even in the case that `Dependencies` is imported due to use, we will not pull in the test observer until runtime, as a best effort. If the DependenciesTestObserver library is found in the search path, it will be loaded and the observer initialised. If we can't find the library, we will simply continue without the test observer. --- Package@swift-5.9.swift | 19 ++- Sources/Dependencies/DependencyValues.swift | 131 +++++++++--------- .../TestObserver.swift | 21 +++ 3 files changed, 106 insertions(+), 65 deletions(-) create mode 100644 Sources/DependenciesTestObserver/TestObserver.swift 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..d7d0dd8c 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 +}