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..b6ed31f8 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,52 @@ 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({ + DependencyValues._current.cachedValues.cached = [:] + })) + } + #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 +440,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 +}