Last week, I wrote an article titled “Swift Testing: Introduce Confirmation”.
This article explores the confirmation
specification.
Let’s read Confirmation.swift in Swift Testing.
Interface
Here’s the confirmation
implementation.
There are five arguments:
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: Int = 1,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: (Confirmation) async throws -> sending R
) async rethrows -> R {
try await confirmation(
comment,
expectedCount: expectedCount ... expectedCount,
isolation: isolation,
sourceLocation: sourceLocation,
body
)
}
comment
is a comment provided when the test fails.
It’s a Comment
type and inherits from ExpressibleByStringLiteral
in this code:
// MARK: - ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible
extension Comment: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
public init(stringLiteral: String) {
self.init(rawValue: stringLiteral, kind: .stringLiteral)
}
public var description: String {
rawValue
}
}
That’s why comment
accepts the String
variable.
It seems that comment
is used to record an issue`, so let’s check the implementation later.
expectedCount
is, as explained in the previous post, the number of times that event occurs.
The callee function in this code has almost similar arguments except expectedCount
.
expectedCount
is defined as an Int
type in this function while described as a ClosedRange
type in the callee function.
However, upon closer inspection, it is enclosed by expectedCount
, so the range of values seems limited to expectedCount
.
isolation
is the actor to which body
is isolated.
This argument was introduced so that tests requiring @MainActor
can be compiled in Swift6 language mode.
- Swift Forums: Where to order #isolation and #_sourceLocation arguments
- swiftlang/swift-testing #624: Add isolation argument to functions taking non-sendable async closures.
sourceLocation
refers to the file information recorded when the test fails.
It’s like the #file
and #line
of XCTAssert
arguments.
You can check the SourceLocation
implementation here.
body
is a function that takes a Confirmation
typed argument and invoked to check the test content.
Implementation
Now let’s read the callee function here. It’s implemented in only 13 lines.
In this function, a Confirmation
type instance named confirmation
is created and body
is invoked with confirmation
.
Finally, an issue will be recorded to fail the test if expectedCount
doesn’t match actualCount
obtained from confirmation
:
@_spi(Experimental)
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: some RangeExpression<Int> & Sendable,
isolation: isolated (any Actor)? = #isolation,
sourceLocation: SourceLocation = #_sourceLocation,
_ body: (Confirmation) async throws -> sending R
) async rethrows -> R {
let confirmation = Confirmation()
defer {
let actualCount = confirmation.count.rawValue
if !expectedCount.contains(actualCount) {
let issue = Issue(
kind: expectedCount.issueKind(forActualCount: actualCount),
comments: Array(comment),
sourceContext: .init(backtrace: .current(), sourceLocation: sourceLocation)
)
issue.record()
}
}
return try await body(confirmation)
}
The Confirmation
is defined here.
It has a count
that holds the number of function calls.
A confirm
function increments the count
, and we can call it by callAsFunction
:
/// A type that can be used to confirm that an event occurs zero or more times.
public struct Confirmation: Sendable {
/// The number of times ``confirm(count:)`` has been called.
///
/// This property is fileprivate because it may be mutated asynchronously and
/// callers may be tempted to use it in ways that result in data races.
fileprivate var count = Locked(rawValue: 0)
/// Confirm this confirmation.
///
/// - Parameters:
/// - count: The number of times to confirm this instance.
///
/// As a convenience, this method can be called by calling the confirmation
/// directly.
public func confirm(count: Int = 1) {
precondition(count > 0)
self.count.add(count)
}
}
extension Confirmation {
/// Confirm this confirmation.
///
/// - Parameters:
/// - count: The number of times to confirm this instance.
///
/// Calling a confirmation as a function is shorthand for calling its
/// ``confirm(count:)`` method.
public func callAsFunction(count: Int = 1) {
confirm(count: count)
}
}
The type of count
is Locked<Int>
.
Locked<T>
leverages ManagedBuffer, which seems to add values based on a locking mechanism tailored to its platform, such as iOS, macOS, or Linux.
You can check the details here.
Conclusion
I traced the confirmation
specification from the Swift Testing source code.
It is a more straightforward implementation than I had imagined.
It can be applied as an Extension to XCTestCase, which summarizes the closure of asynchronous processing and the expected number of times it is executed.
Thanks!