Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dependencies: dynamically link XCTest #169

Merged
merged 1 commit into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}
Loading