Skip to content

Commit

Permalink
Dependencies: dynamically link XCTest (#169)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
compnerd committed Jan 22, 2024
1 parent a9c9b58 commit 4ce7fd2
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 59 deletions.
18 changes: 17 additions & 1 deletion [email protected]
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -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: [
Expand Down Expand Up @@ -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(
Expand Down
137 changes: 79 additions & 58 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
@@ -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<R>(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.
///
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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<R>(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
22 changes: 22 additions & 0 deletions Sources/DependenciesTestObserver/TestObserver.swift
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 4ce7fd2

Please sign in to comment.