Skip to content

Commit a1dbd10

Browse files
authored
Add assert and precondition (#213)
* Add `assert` and `precondition` Lightweight ways of writing assertions and preconditions in a testable fashion. * wip * wip * wip * Update Sources/Dependencies/DependencyValues/Assert.swift * wip * wip * wip * wip
1 parent ccf31d0 commit a1dbd10

File tree

6 files changed

+299
-26
lines changed

6 files changed

+299
-26
lines changed

Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 24 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
extension DependencyValues {
2+
/// A dependency for handling assertions.
3+
///
4+
/// Useful as a controllable and testable substitute for Swift's `assert` function that calls
5+
/// `XCTFail` in tests instead of terminating the executable.
6+
///
7+
/// ```swift
8+
/// func operate(_ n: Int) {
9+
/// @Dependency(\.assert) var assert
10+
/// assert(n > 0, "Number must be greater than zero")
11+
/// // ...
12+
/// }
13+
/// ```
14+
///
15+
/// Tests can assert against this precondition using `XCTExpectFailure`:
16+
///
17+
/// ```swift
18+
/// XCTExpectFailure {
19+
/// operate(n)
20+
/// } issueMatcher: {
21+
/// $0.compactDescription = "Number must be greater than zero"
22+
/// }
23+
/// ```
24+
public var assert: any AssertionEffect {
25+
get { self[AssertKey.self] }
26+
set { self[AssertKey.self] = newValue }
27+
}
28+
29+
/// A dependency for failing an assertion.
30+
///
31+
/// Equivalent to passing a `false` condition to ``DependencyValues/assert``.
32+
public var assertionFailure: any AssertionFailureEffect {
33+
AssertionFailure(base: self.assert)
34+
}
35+
36+
/// A dependency for handling preconditions.
37+
///
38+
/// Useful as a controllable and testable substitute for Swift's `precondition` function that
39+
/// calls `XCTFail` in tests instead of terminating the executable.
40+
///
41+
/// ```swift
42+
/// func operate(_ n: Int) {
43+
/// @Dependency(\.precondition) var precondition
44+
/// precondition(n > 0, "Number must be greater than zero")
45+
/// // ...
46+
/// }
47+
/// ```
48+
///
49+
/// Tests can assert against this precondition using `XCTExpectFailure`:
50+
///
51+
/// ```swift
52+
/// XCTExpectFailure {
53+
/// operate(n)
54+
/// } issueMatcher: {
55+
/// $0.compactDescription = "Number must be greater than zero"
56+
/// }
57+
/// ```
58+
public var precondition: any AssertionEffect {
59+
get { self[PreconditionKey.self] }
60+
set { self[PreconditionKey.self] = newValue }
61+
}
62+
}
63+
64+
/// A type for creating an assertion or precondition.
65+
///
66+
/// See ``DependencyValues/assert`` or ``DependencyValues/precondition`` for more information.
67+
public protocol AssertionEffect: Sendable {
68+
func callAsFunction(
69+
_ condition: @autoclosure () -> Bool,
70+
_ message: @autoclosure () -> String,
71+
file: StaticString,
72+
line: UInt
73+
)
74+
}
75+
76+
extension AssertionEffect {
77+
@_disfavoredOverload
78+
@_transparent
79+
public func callAsFunction(
80+
_ condition: @autoclosure () -> Bool,
81+
_ message: @autoclosure () -> String = "",
82+
file: StaticString = #file,
83+
line: UInt = #line
84+
) {
85+
self.callAsFunction(condition(), message(), file: file, line: line)
86+
}
87+
}
88+
89+
private struct LiveAssertionEffect: AssertionEffect {
90+
@_transparent
91+
func callAsFunction(
92+
_ condition: @autoclosure () -> Bool,
93+
_ message: @autoclosure () -> String,
94+
file: StaticString,
95+
line: UInt
96+
) {
97+
Swift.assert(condition(), message(), file: file, line: line)
98+
}
99+
}
100+
101+
private struct LivePreconditionEffect: AssertionEffect {
102+
@_transparent
103+
func callAsFunction(
104+
_ condition: @autoclosure () -> Bool,
105+
_ message: @autoclosure () -> String,
106+
file: StaticString,
107+
line: UInt
108+
) {
109+
Swift.precondition(condition(), message(), file: file, line: line)
110+
}
111+
}
112+
113+
private struct TestAssertionEffect: AssertionEffect {
114+
@_transparent
115+
func callAsFunction(
116+
_ condition: @autoclosure () -> Bool,
117+
_ message: @autoclosure () -> String,
118+
file: StaticString,
119+
line: UInt
120+
) {
121+
guard condition() else { return XCTFail(message(), file: file, line: line) }
122+
}
123+
}
124+
125+
public protocol AssertionFailureEffect: Sendable {
126+
func callAsFunction(
127+
_ message: @autoclosure () -> String,
128+
file: StaticString,
129+
line: UInt
130+
)
131+
}
132+
133+
extension AssertionFailureEffect {
134+
@_disfavoredOverload
135+
@_transparent
136+
public func callAsFunction(
137+
_ message: @autoclosure () -> String = "",
138+
file: StaticString = #file,
139+
line: UInt = #line
140+
) {
141+
self.callAsFunction(message(), file: file, line: line)
142+
}
143+
}
144+
145+
private struct AssertionFailure: AssertionFailureEffect {
146+
let base: any AssertionEffect
147+
148+
@_transparent
149+
func callAsFunction(
150+
_ message: @autoclosure () -> String,
151+
file: StaticString,
152+
line: UInt
153+
) {
154+
self.base(false, message(), file: file, line: line)
155+
}
156+
}
157+
158+
private enum AssertKey: DependencyKey {
159+
public static let liveValue: any AssertionEffect = LiveAssertionEffect()
160+
public static let testValue: any AssertionEffect = TestAssertionEffect()
161+
}
162+
163+
private enum PreconditionKey: DependencyKey {
164+
public static let liveValue: any AssertionEffect = LivePreconditionEffect()
165+
public static let testValue: any AssertionEffect = TestAssertionEffect()
166+
}
167+
168+
/// An ``AssertionEffect`` that invokes the given closure.
169+
public struct AnyAssertionEffect: AssertionEffect {
170+
private let assert: @Sendable (
171+
_ condition: @autoclosure () -> Bool,
172+
_ message: @autoclosure () -> String,
173+
_ file: StaticString,
174+
_ line: UInt
175+
) -> Void
176+
177+
public init(
178+
_ assert: @escaping @Sendable (
179+
_ condition: @autoclosure () -> Bool,
180+
_ message: @autoclosure () -> String,
181+
_ file: StaticString,
182+
_ line: UInt
183+
) -> Void
184+
) {
185+
self.assert = assert
186+
}
187+
188+
public func callAsFunction(
189+
_ condition: @autoclosure () -> Bool,
190+
_ message: @autoclosure () -> String,
191+
file: StaticString,
192+
line: UInt
193+
) {
194+
self.assert(condition(), message(), file, line)
195+
}
196+
}

Sources/Dependencies/Documentation.docc/Extensions/DependencyValues.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
### Creating and accessing values
66

77
- ``init()``
8-
- ``subscript(_:_:_:_:)``
8+
- ``subscript(key:file:function:line:)
99

1010
### Overriding values
1111

@@ -18,6 +18,8 @@
1818

1919
### Dependency values
2020

21+
- ``assert``
22+
- ``assertionFailure``
2123
- ``calendar``
2224
- ``context``
2325
- ``continuousClock``
@@ -27,6 +29,7 @@
2729
- ``mainQueue``
2830
- ``mainRunLoop``
2931
- ``openURL``
32+
- ``precondition``
3033
- ``suspendingClock``
3134
- ``timeZone``
3235
- ``urlSession``
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# ``Dependencies/DependencyValues/assert``
2+
3+
## Topics
4+
5+
### Dependency values
6+
7+
- ``AssertionEffect``
8+
- ``AssertionFailureEffect``
9+
10+
### Custom assertions
11+
12+
- ``AnyAssertionEffect``

0 commit comments

Comments
 (0)