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 0a7b61e
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 65 deletions.
19 changes: 18 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,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: [
Expand Down Expand Up @@ -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(
Expand Down
131 changes: 67 additions & 64 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 @@ -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.
Expand All @@ -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
}

Expand Down Expand Up @@ -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: Sendable>(_ base: Base) {
self.base = base
Expand Down Expand Up @@ -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<Key: TestDependencyKey>(
for key: Key.Type,
Expand Down Expand Up @@ -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<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
21 changes: 21 additions & 0 deletions Sources/DependenciesTestObserver/TestObserver.swift
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 0a7b61e

Please sign in to comment.