Skip to content

Commit

Permalink
Dependencies: dynamically link XCTest
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 17, 2024
1 parent e726c87 commit 3508642
Show file tree
Hide file tree
Showing 3 changed files with 98 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)
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
117 changes: 59 additions & 58 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
@@ -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<R>(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.
///
Expand Down Expand Up @@ -95,8 +114,46 @@ 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(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 @@ -348,59 +405,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<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 3508642

Please sign in to comment.