Skip to content

Commit

Permalink
Use DependencyValues.$_current.withValue(dependencies) to compute `…
Browse files Browse the repository at this point in the history
…wrappedValue` (#71)

* Add failing test

* Compute dependency value using withValue

* Fix test

* update and add tests

* remove #if DEBUG guard

---------

Co-authored-by: Brandon Williams <[email protected]>
  • Loading branch information
iampatbrown and mbrandonw authored Apr 10, 2023
1 parent 0b18a15 commit 0f34848
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 56 deletions.
10 changes: 8 additions & 2 deletions Sources/Dependencies/Dependency.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,16 @@ public struct Dependency<Value>: @unchecked Sendable, _HasInitialValues {
currentDependency.fileID = self.fileID
currentDependency.line = self.line
return DependencyValues.$currentDependency.withValue(currentDependency) {
self.initialValues.merging(DependencyValues._current)[keyPath: self.keyPath]
let dependencies = self.initialValues.merging(DependencyValues._current)
return DependencyValues.$_current.withValue(dependencies) {
DependencyValues._current[keyPath: self.keyPath]
}
}
#else
return self.initialValues.merging(DependencyValues._current)[keyPath: self.keyPath]
let dependencies = self.initialValues.merging(DependencyValues._current)
return DependencyValues.$_current.withValue(dependencies) {
DependencyValues._current[keyPath: self.keyPath]
}
#endif
}
}
Expand Down
104 changes: 50 additions & 54 deletions Sources/Dependencies/DependencyValues.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,7 @@ 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 DEBUG
_ = setUpTestObservers
#endif
_ = setUpTestObservers
}

/// Accesses the dependency value associated with a custom key.
Expand Down Expand Up @@ -351,62 +349,60 @@ private final class CachedValues: @unchecked Sendable {

// NB: We cannot statically link/load XCTest on Apple platforms, so we dynamically load things
// instead and we limit this to debug builds to avoid App Store binary rejection.
#if DEBUG
#if !canImport(ObjectiveC)
import XCTest
#endif
#if !canImport(ObjectiveC)
import XCTest
#endif

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())
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 = [:]
}
#else
XCTestObservationCenter.shared.addTestObserver(TestObserver())
#endif
}
}()

#if canImport(ObjectiveC)
private final class TestObserver: NSObject {}
#else
private final class TestObserver: NSObject, XCTestObservation {
func testCaseWillStart(_ testCase: XCTestCase) {
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 {}
#else
private final class TestObserver: NSObject, XCTestObservation {
func testCaseWillStart(_ testCase: XCTestCase) {
DependencyValues._current.cachedValues.cached = [:]
}
#endif
}
#endif

extension DispatchQueue {
private static let key = DispatchSpecificKey<UInt8>()
private static let value: UInt8 = 0
extension DispatchQueue {
private static let key = DispatchSpecificKey<UInt8>()
private static let value: UInt8 = 0

fileprivate static func mainSync<R>(execute block: @Sendable () -> R) -> R {
Self.main.setSpecific(key: Self.key, value: Self.value)
if getSpecific(key: Self.key) == Self.value {
return block()
} else {
return Self.main.sync(execute: block)
}
fileprivate static func mainSync<R>(execute block: @Sendable () -> R) -> R {
Self.main.setSpecific(key: Self.key, value: Self.value)
if getSpecific(key: Self.key) == Self.value {
return block()
} else {
return Self.main.sync(execute: block)
}
}
#endif
}
117 changes: 117 additions & 0 deletions Tests/DependenciesTests/ResolutionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,102 @@ final class ResolutionTests: XCTestCase {
}
}

func testDependencyDependingOnDependency_Nested() {
struct Model {
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
@Dependency(\.nestedChild) var nestedChild: NestedChildDependency
}

let model = withDependencies {
$0.nestedChild.value = { 1 }
} operation: {
Model()
}

XCTAssertEqual(model.nestedParent.value(), 1)
XCTAssertEqual(model.nestedChild.value(), 1)

withDependencies {
$0.nestedChild.value = { 42 }
} operation: {
XCTAssertEqual(model.nestedParent.value(), 42)
XCTAssertEqual(model.nestedChild.value(), 42)
}
}

func testFirstAccessBehavior() {
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
struct Model {
@Dependency(\.nestedParent) var nestedParent: NestedParentDependency
@Dependency(\.nestedChild) var nestedChild: NestedChildDependency
}

let model = withDependencies {
$0.nestedChild.value = { 1 }
} operation: {
Model()
}

XCTAssertEqual(nestedParent.value(), 1729)
XCTAssertEqual(model.nestedParent.value(), 1729)
XCTAssertEqual(model.nestedChild.value(), 1)

withDependencies {
$0.nestedChild.value = { 42 }
} operation: {
XCTAssertEqual(model.nestedParent.value(), 42)
XCTAssertEqual(model.nestedChild.value(), 42)
}
}

func testParentChildScoping() {
withDependencies {
$0.context = .live
} operation: {
@Dependency(\.date) var date
let _ = date.now

class ParentModel {
@Dependency(\.date) var date
var child1: Child1Model?
var child2: Child2Model?
func goToChild1() {
self.child1 = withDependencies(from: self) { Child1Model() }
}
func goToChild2() {
self.child2 = withDependencies(from: self) { Child2Model() }
}
}
class Child1Model {
@Dependency(\.date) var date
}
class Child2Model {
@Dependency(\.date) var date
}

let model = withDependencies {
$0.date = .constant(Date(timeIntervalSince1970: 1))
} operation: {
ParentModel()
}

withDependencies {
$0.date = .constant(Date(timeIntervalSince1970: 2))
} operation: {
model.goToChild1()
}
withDependencies {
$0.date = .constant(Date(timeIntervalSince1970: 3))
} operation: {
model.goToChild2()
}

XCTAssertEqual(model.date.now.timeIntervalSince1970, 1)
XCTAssertEqual(model.child1?.date.now.timeIntervalSince1970, 2)
XCTAssertEqual(model.child2?.date.now.timeIntervalSince1970, 3)
}
}

func testDependencyDiamond() {
@Dependency(\.diamondA) var diamondA: DiamondDependencyA
@Dependency(\.diamondB1) var diamondB1: DiamondDependencyB1
Expand Down Expand Up @@ -111,6 +207,19 @@ private struct LazyChildDependency: TestDependencyKey {

static let testValue = Self { 1729 }
}
private struct NestedParentDependency: TestDependencyKey {
var value: () -> Int
static var testValue: NestedParentDependency {
@Dependency(\.nestedChild) var child
return Self {
return child.value()
}
}
}
private struct NestedChildDependency: TestDependencyKey {
var value: () -> Int
static let testValue = Self { 1729 }
}
private struct DiamondDependencyA: TestDependencyKey {
var value: @Sendable () -> Int
static let testValue = Self {
Expand Down Expand Up @@ -169,6 +278,14 @@ extension DependencyValues {
get { self[LazyChildDependency.self] }
set { self[LazyChildDependency.self] = newValue }
}
fileprivate var nestedParent: NestedParentDependency {
get { self[NestedParentDependency.self] }
set { self[NestedParentDependency.self] = newValue }
}
fileprivate var nestedChild: NestedChildDependency {
get { self[NestedChildDependency.self] }
set { self[NestedChildDependency.self] = newValue }
}
fileprivate var diamondA: DiamondDependencyA {
get { self[DiamondDependencyA.self] }
set { self[DiamondDependencyA.self] = newValue }
Expand Down

0 comments on commit 0f34848

Please sign in to comment.