Skip to content

Commit

Permalink
Add assert and precondition (#213)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
stephencelis committed May 8, 2024
1 parent ccf31d0 commit a1dbd10
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 26 deletions.
39 changes: 24 additions & 15 deletions Dependencies.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "fddd1c00396eed152c45a46bea9f47b98e59301d",
"version" : "1.2.0"
"revision" : "46989693916f56d1186bd59ac15124caef896560",
"version" : "1.3.1"
}
},
{
Expand All @@ -32,25 +32,34 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-clocks",
"state" : {
"revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb",
"version" : "1.0.0"
"revision" : "a8421d68068d8f45fbceb418fbf22c5dad4afd33",
"version" : "1.0.2"
}
},
{
"identity" : "swift-concurrency-extras",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "ea631ce892687f5432a833312292b80db238186a",
"version" : "1.0.0"
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
}
},
{
"identity" : "swift-docc-plugin",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-plugin",
"state" : {
"revision" : "3303b164430d9a7055ba484c8ead67a52f7b74f6",
"revision" : "26ac5758409154cc448d7ab82389c520fa8a8247",
"version" : "1.3.0"
}
},
{
"identity" : "swift-docc-symbolkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-docc-symbolkit",
"state" : {
"revision" : "b45d1f2ed151d057b54504d653e0da5552844e34",
"version" : "1.0.0"
}
},
Expand All @@ -59,35 +68,35 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing",
"state" : {
"revision" : "35acd9468d40ae87e75991a18af6271e8124c261",
"version" : "0.2.1"
"revision" : "5c4a1b9d7c23cd5c08ea50677d8e89080365cb00",
"version" : "0.4.0"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "bb0ea08db8e73324fe6c3727f755ca41a23ff2f4",
"version" : "1.14.2"
"revision" : "625ccca8570773dd84a34ee51a81aa2bc5a4f97a",
"version" : "1.16.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax",
"state" : {
"revision" : "ffa3cd6fc2aa62adbedd31d3efaf7c0d86a9f029",
"version" : "509.0.1"
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
"version" : "510.0.2"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2",
"version" : "1.1.1"
"revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2",
"version" : "1.1.2"
}
}
],
Expand Down
20 changes: 10 additions & 10 deletions Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41",
"version" : "1.3.0"
"revision" : "46989693916f56d1186bd59ac15124caef896560",
"version" : "1.3.1"
}
},
{
Expand Down Expand Up @@ -68,35 +68,35 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-macro-testing",
"state" : {
"revision" : "90e38eec4bf661ec0da1bbfd3ec507d0f0c05310",
"version" : "0.3.0"
"revision" : "5c4a1b9d7c23cd5c08ea50677d8e89080365cb00",
"version" : "0.4.0"
}
},
{
"identity" : "swift-snapshot-testing",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
"state" : {
"revision" : "5b0c434778f2c1a4c9b5ebdb8682b28e84dd69bd",
"version" : "1.15.4"
"revision" : "625ccca8570773dd84a34ee51a81aa2bc5a4f97a",
"version" : "1.16.0"
}
},
{
"identity" : "swift-syntax",
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-syntax",
"state" : {
"revision" : "08a2f0a9a30e0f705f79c9cfaca1f68b71bdc775",
"version" : "510.0.0"
"revision" : "303e5c5c36d6a558407d364878df131c3546fad8",
"version" : "510.0.2"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
"state" : {
"revision" : "b13b1d1a8e787a5ffc71ac19dcaf52183ab27ba2",
"version" : "1.1.1"
"revision" : "6f30bdba373bbd7fbfe241dddd732651f2fbd1e2",
"version" : "1.1.2"
}
}
],
Expand Down
196 changes: 196 additions & 0 deletions Sources/Dependencies/DependencyValues/Assert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
extension DependencyValues {
/// A dependency for handling assertions.
///
/// Useful as a controllable and testable substitute for Swift's `assert` function that calls
/// `XCTFail` in tests instead of terminating the executable.
///
/// ```swift
/// func operate(_ n: Int) {
/// @Dependency(\.assert) var assert
/// assert(n > 0, "Number must be greater than zero")
/// // ...
/// }
/// ```
///
/// Tests can assert against this precondition using `XCTExpectFailure`:
///
/// ```swift
/// XCTExpectFailure {
/// operate(n)
/// } issueMatcher: {
/// $0.compactDescription = "Number must be greater than zero"
/// }
/// ```
public var assert: any AssertionEffect {
get { self[AssertKey.self] }
set { self[AssertKey.self] = newValue }
}

/// A dependency for failing an assertion.
///
/// Equivalent to passing a `false` condition to ``DependencyValues/assert``.
public var assertionFailure: any AssertionFailureEffect {
AssertionFailure(base: self.assert)
}

/// A dependency for handling preconditions.
///
/// Useful as a controllable and testable substitute for Swift's `precondition` function that
/// calls `XCTFail` in tests instead of terminating the executable.
///
/// ```swift
/// func operate(_ n: Int) {
/// @Dependency(\.precondition) var precondition
/// precondition(n > 0, "Number must be greater than zero")
/// // ...
/// }
/// ```
///
/// Tests can assert against this precondition using `XCTExpectFailure`:
///
/// ```swift
/// XCTExpectFailure {
/// operate(n)
/// } issueMatcher: {
/// $0.compactDescription = "Number must be greater than zero"
/// }
/// ```
public var precondition: any AssertionEffect {
get { self[PreconditionKey.self] }
set { self[PreconditionKey.self] = newValue }
}
}

/// A type for creating an assertion or precondition.
///
/// See ``DependencyValues/assert`` or ``DependencyValues/precondition`` for more information.
public protocol AssertionEffect: Sendable {
func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
)
}

extension AssertionEffect {
@_disfavoredOverload
@_transparent
public func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) {
self.callAsFunction(condition(), message(), file: file, line: line)
}
}

private struct LiveAssertionEffect: AssertionEffect {
@_transparent
func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
) {
Swift.assert(condition(), message(), file: file, line: line)
}
}

private struct LivePreconditionEffect: AssertionEffect {
@_transparent
func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
) {
Swift.precondition(condition(), message(), file: file, line: line)
}
}

private struct TestAssertionEffect: AssertionEffect {
@_transparent
func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
) {
guard condition() else { return XCTFail(message(), file: file, line: line) }
}
}

public protocol AssertionFailureEffect: Sendable {
func callAsFunction(
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
)
}

extension AssertionFailureEffect {
@_disfavoredOverload
@_transparent
public func callAsFunction(
_ message: @autoclosure () -> String = "",
file: StaticString = #file,
line: UInt = #line
) {
self.callAsFunction(message(), file: file, line: line)
}
}

private struct AssertionFailure: AssertionFailureEffect {
let base: any AssertionEffect

@_transparent
func callAsFunction(
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
) {
self.base(false, message(), file: file, line: line)
}
}

private enum AssertKey: DependencyKey {
public static let liveValue: any AssertionEffect = LiveAssertionEffect()
public static let testValue: any AssertionEffect = TestAssertionEffect()
}

private enum PreconditionKey: DependencyKey {
public static let liveValue: any AssertionEffect = LivePreconditionEffect()
public static let testValue: any AssertionEffect = TestAssertionEffect()
}

/// An ``AssertionEffect`` that invokes the given closure.
public struct AnyAssertionEffect: AssertionEffect {
private let assert: @Sendable (
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
_ file: StaticString,
_ line: UInt
) -> Void

public init(
_ assert: @escaping @Sendable (
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
_ file: StaticString,
_ line: UInt
) -> Void
) {
self.assert = assert
}

public func callAsFunction(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String,
file: StaticString,
line: UInt
) {
self.assert(condition(), message(), file, line)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
### Creating and accessing values

- ``init()``
- ``subscript(_:_:_:_:)``
- ``subscript(key:file:function:line:)

### Overriding values

Expand All @@ -18,6 +18,8 @@

### Dependency values

- ``assert``
- ``assertionFailure``
- ``calendar``
- ``context``
- ``continuousClock``
Expand All @@ -27,6 +29,7 @@
- ``mainQueue``
- ``mainRunLoop``
- ``openURL``
- ``precondition``
- ``suspendingClock``
- ``timeZone``
- ``urlSession``
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# ``Dependencies/DependencyValues/assert``

## Topics

### Dependency values

- ``AssertionEffect``
- ``AssertionFailureEffect``

### Custom assertions

- ``AnyAssertionEffect``
Loading

0 comments on commit a1dbd10

Please sign in to comment.