From 911902e1e7ed5436374395207cbe3f1309be4a07 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 | 18 ++- Sources/Dependencies/DependencyValues.swift | 135 ++++++++++-------- .../TestObserver.swift | 22 +++ 3 files changed, 116 insertions(+), 59 deletions(-) create mode 100644 Sources/DependenciesTestObserver/TestObserver.swift diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift index dbcd7c1b..bead1b25 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,12 @@ let package = Package( .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), ], targets: [ + .target( + name: "DependenciesTestObserver", + dependencies: [ + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ] + ), .target( name: "Dependencies", dependencies: [ @@ -85,6 +91,16 @@ let package = Package( ) #endif +#if !os(macOS) && !os(WASI) +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 7a99ca88..34e23c15 100644 --- a/Sources/Dependencies/DependencyValues.swift +++ b/Sources/Dependencies/DependencyValues.swift @@ -1,5 +1,38 @@ import Foundation import XCTestDynamicOverlay +#if os(Windows) + import WinSDK +#elseif os(Linux) + import Glibc +#endif +// WASI does not support dynamic linking +#if os(WASI) + import XCTest +#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 {} +#elseif os(WASI) + final class TestObserver: NSObject, XCTestObservation { + private let resetCache: @convention(c) () -> Void + internal init(_ resetCache: @convention(c) () -> Void) { + self.resetCache = resetCache + } + public func testCaseWillStart(_ testCase: XCTestCase) { + self.resetCache() + } + } +#endif /// A collection of dependencies that is globally available. /// @@ -94,8 +127,50 @@ 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()) + } + #elseif os(WASI) + if _XCTIsTesting { + XCTestObservationCenter.shared.addTestObserver(TestObserver(resetCache)) + } + #else + typealias RegisterTestObserver = @convention(thin) (@convention(c) () -> Void) -> Void + var pRegisterTestObserver: RegisterTestObserver? = nil + + #if os(Windows) + let hModule = LoadLibraryA("DependenciesTestObserver.dll") + if let hModule, let pAddress = GetProcAddress(hModule, "$s24DependenciesTestObserver08registerbC0yyyyXCF") { + pRegisterTestObserver = unsafeBitCast(pAddress, to: RegisterTestObserver.self) + } + #else + let hModule: UnsafeMutableRawPointer? = dlopen("libDependenciesTestObserver.so", RTLD_NOW) + if let hModule, let pAddress = dlsym(hModule, "$s24DependenciesTestObserver08registerbC0yyyyXCF") { + pRegisterTestObserver = unsafeBitCast(pAddress, to: RegisterTestObserver.self) + } + #endif + pRegisterTestObserver?({ + DependencyValues._current.cachedValues.cached = [:] + }) #endif } @@ -363,59 +438,3 @@ private final class CachedValues: @unchecked Sendable { } } } - -// 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..cec0c08d --- /dev/null +++ b/Sources/DependenciesTestObserver/TestObserver.swift @@ -0,0 +1,22 @@ + +import XCTest +import XCTestDynamicOverlay + +#if !_runtime(_ObjC) + final class TestObserver: NSObject, XCTestObservation { + private let resetCache: @convention(c) () -> Void + internal init(_ resetCache: @convention(c) () -> Void) { + self.resetCache = resetCache + } + public func testCaseWillStart(_ testCase: XCTestCase) { + self.resetCache() + } + } +#endif + +public func registerTestObserver(_ resetCache: @convention(c) () -> Void) { + guard _XCTIsTesting else { return } + #if !_runtime(_ObjC) + XCTestObservationCenter.shared.addTestObserver(TestObserver(resetCache)) + #endif +}