2024/8/24
laprasdrum.icon 調べ物したくて深夜に目が醒めてしまった
/icons/hr.icon
confirmationの仕様を探る
まずは定義から。
code:Definition.swift
/// comment
/// An optional comment to apply to any issues generated by this function.
///
/// expectedCount
/// The number of times the expected event should occur when body is invoked. The default value of this argument is 1, indicating that the event should occur exactly once. Pass 0 if the event should never occur when body is invoked.
///
/// sourceLocation
/// The source location to which any recorded issues should be attributed.
///
/// body
/// The function to invoke.
func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: Int = 1,
_ body: (Confirmation) async throws -> R
) async rethrows -> R
body closure内で Confirmation instanceのcallAsFunction()がexpectedCount回呼び出されるのを期待値とする。
例外が発生した場合、Issue.record()をcommentとともに発行する。
Use confirmations to check that an event occurs while a test is running in complex scenarios where #expect() and #require() are insufficient.
For example, a confirmation may be useful when an expected event occurs:
In a context that cannot be awaited by the calling function such as an event handler or delegate callback;
More than once, or never; or
As a callback that is invoked as part of a larger operation.
非同期処理の待機が主な用途になる。
A type that can be used to confirm that an event occurs zero or more times.
続いて関連ドキュメント。
In more complex situations you can use Confirmation to discover whether an expected event happens.
Call confirmation(_:expectedCount:sourceLocation:_:) in your asynchronous test function to create a Confirmation for the expected event. In the trailing closure parameter, call the code under test. Swift Testing passes a Confirmation as the parameter to the closure, which you call as a function in the event handler for the code under test when the event you’re testing for occurs:
To validate that a particular event doesn’t occur during a test, create a Confirmation with an expected count of 0:
code:NeverOccures.swift
@Test func orderCalculatorEncountersNoErrors() async {
let calculator = OrderCalculator()
await confirmation(expectedCount: 0) { confirmation in
calculator.errorHandler = { _ in confirmation() }
calculator.subtotal(for: PizzaToppings(bases: []))
}
}
呼び出されなかったことをテストする方法もある。
このドキュメントはXCTestとの比較としても役立つし、XCTestでできること一覧にも使える。
XCTest has a class, XCTestExpectation, that represents some asynchronous condition. You create an instance of this class (or a subclass like XCTKeyPathExpectation) using an initializer or a convenience method on XCTestCase. When the condition represented by an expectation occurs, the developer fulfills the expectation. Concurrently, the developer waits for the expectation to be fulfilled using an instance of XCTWaiter or using a convenience method on XCTestCase.
Wherever possible, prefer to use Swift concurrency to validate asynchronous conditions. For example, if it’s necessary to determine the result of an asynchronous Swift function, it can be awaited with await. For a function that takes a completion handler but which doesn’t use await, a Swift continuation can be used to convert the call into an async-compatible one.
Some tests, especially those that test asynchronously-delivered events, cannot be readily converted to use Swift concurrency. The testing library offers functionality called confirmations which can be used to implement these tests. Instances of Confirmation are created and used within the scope of the function confirmation(_:expectedCount:sourceLocation:_:).
Confirmations function similarly to the expectations API of XCTest, however, they don’t block or suspend the caller while waiting for a condition to be fulfilled. Instead, the requirement is expected to be confirmed (the equivalent of fulfilling an expectation) before confirmation() returns, and records an issue otherwise:
code:Example.swift
// Before
class FoodTruckTests: XCTestCase {
func testTruckEvents() async {
let soldFood = expectation(description: "…")
FoodTruck.shared.eventHandler = { event in
if case .soldFood = event {
soldFood.fulfill()
}
}
await Customer().buy(.soup)
...
}
...
}
// After
struct FoodTruckTests {
@Test func truckEvents() async {
await confirmation("…") { soldFood in
FoodTruck.shared.eventHandler = { event in
if case .soldFood = event {
soldFood()
}
}
await Customer().buy(.soup)
}
...
}
...
}
いよいよ定義を読み進める。
code:Confirmation.swift
/// 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)
}
}
confirm(count:)を呼び出してcount: Locked<Int>に呼び出し回数を書き込む。
Locked<T>の定義はこちら。
この型のインスタンスは、同期呼び出し元からの共有データへのアクセスを同期するために使用することができます。可能な限り、アクターの分離または他の Swift の同時実行ツールを使用してください。
とあるがrdar://83888717としてレポートされてるように以下のバグがある。
この型によって保護される状態は、代わりにアクター分離を使用して保護されるべきであるが、アクター分離された関数は同期関数から呼び出すことはできない。
とはいえ、自分の言葉で噛み砕いた説明がまだできない状態なので、Actor Isolationと同期関数との関係は改めて整理する。
confirm内のself.count.add(count)で呼び出してる add(_ addend: T)の実装を読むと、nonmutating func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R のbody closure内で値を加算してる。
code:Locked.swift
/// A type that wraps a value requiring access from a synchronous caller during
/// concurrent execution.
///
/// Instances of this type use a lock to synchronize access to their raw values.
/// The lock is not recursive.
///
/// Instances of this type can be used to synchronize access to shared data from
/// a synchronous caller. Wherever possible, use actor isolation or other Swift
/// concurrency tools.
///
/// This type is not part of the public interface of the testing library.
///
/// - Bug: The state protected by this type should instead be protected using
/// actor isolation, but actor-isolated functions cannot be called from
/// synchronous functions. (83888717(rdar://83888717)) struct Locked<T>: RawRepresentable, Sendable where T: Sendable {
/// The platform-specific type to use for locking.
///
/// It would be preferable to implement this lock in Swift, however there is
/// no standard lock or mutex type available across all platforms that is
/// visible in Swift. C11 has a standard mtx_t type, but it is not widely
/// supported and so cannot be relied upon.
///
/// To keep the implementation of this type as simple as possible,
/// pthread_mutex_t is used on Apple platforms instead of os_unfair_lock
/// or OSAllocatedUnfairLock.
#if SWT_TARGET_OS_APPLE || os(Linux) || (os(WASI) && compiler(>=6.1) && _runtime(_multithreaded)) private typealias _Lock = pthread_mutex_t
private typealias _Lock = SRWLOCK
// No locks on WASI without multithreaded runtime.
private typealias _Lock = Void
private typealias _Lock = Void
/// A type providing heap-allocated storage for an instance of Locked.
private final class _Storage: ManagedBuffer<T, _Lock> { ... }
/// Storage for the underlying lock and wrapped value.
private nonisolated(unsafe) var _storage: ManagedBuffer<T, _Lock>
init(rawValue: T) { ... }
var rawValue: T {
withLock { $0 }
}
/// Acquire the lock and invoke a function while it is held.
///
/// - Parameters:
/// - body: A closure to invoke while the lock is held.
///
/// - Returns: Whatever is returned by body.
///
/// - Throws: Whatever is thrown by body.
///
/// This function can be used to synchronize access to shared data from a
/// synchronous caller. Wherever possible, use actor isolation or other Swift
/// concurrency tools.
nonmutating func withLock<R>(_ body: (inout T) throws -> R) rethrows -> R {
try _storage.withUnsafeMutablePointers { rawValue, lock in
...
return try body(&rawValue.pointee)
}
}
...
}
extension Locked where T: AdditiveArithmetic {
/// Add something to the current wrapped value of this instance.
///
/// - Parameters:
/// - addend: The value to add.
///
/// - Returns: The sum of rawValue and addend.
@discardableResult func add(_ addend: T) -> T {
withLock { rawValue in
let result = rawValue + addend
rawValue = result
return result
}
}
}
extension Locked where T: Numeric {
/// Increment the current wrapped value of this instance.
///
/// - Returns: The sum of rawValue and 1.
///
/// This function is exactly equivalent to add(1).
@discardableResult func increment() -> T {
add(1)
}
}
extension Locked {
/// Initialize an instance of this type with a raw value of nil.
init<V>() where T == V? {
self.init(rawValue: nil)
}
/// Initialize an instance of this type with a raw value of [:].
init<K, V>() where T == Dictionary<K, V> {
}
}
ManagedBuffer<T, _Lock>定義となっており、_LockはSWT_TARGET_OS_APPLEだとpthread_mutex_tと同義。
SWT_TARGET_OS_APPLEはPackage.swiftで定義されてる。
code:Package.swift
extension Array where Element == PackageDescription.SwiftSetting {
/// Settings intended to be applied to every Swift target in this package.
/// Analogous to project-level build settings in an Xcode project.
static var packageSettings: Self {
availabilityMacroSettings + [
.enableUpcomingFeature("ExistentialAny"),
.enableExperimentalFeature("AccessLevelOnImport"),
.enableUpcomingFeature("InternalImportsByDefault"),
.define("SWT_NO_DYNAMIC_LINKING", .when(platforms: .wasi)), ]
}
Lockedがどういうものか分かったのでConfirmationに戻り、confirmation()の実装を読む。
code:Confirmation.swift
/// Confirm that some event occurs during the invocation of a function.
///
/// - Parameters:
/// - comment: An optional comment to apply to any issues generated by this
/// function.
/// - expectedCount: The number of times the expected event should occur when
/// body is invoked. The default value of this argument is 1, indicating
/// that the event should occur exactly once. Pass 0 if the event should
/// _never_ occur when body is invoked.
/// - sourceLocation: The source location to which any recorded issues should
/// be attributed.
/// - body: The function to invoke.
///
/// - Returns: Whatever is returned by body.
///
/// - Throws: Whatever is thrown by body.
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: Int = 1,
_ body: (Confirmation) async throws -> R
) async rethrows -> R {
try await confirmation(
comment,
expectedCount: expectedCount ... expectedCount,
sourceLocation: sourceLocation,
body
)
}
sourceLocationはXCTestCaseにおけるfileやlineの代用になっている。
code:SourceLocation.swift
/// A type representing a location in source code.
public struct SourceLocation: Sendable {
/// The file ID of the source file.
///
/// ## See Also
///
/// - moduleName
/// - fileName
public var fileID: String {
didSet {
precondition(!fileID.isEmpty)
precondition(fileID.contains("/"))
}
}
/// The name of the source file.
///
/// The name of the source file is derived from this instance's fileID
/// property. It consists of the substring of the file ID after the last
/// forward-slash character ("/".) For example, if the value of this
/// instance's fileID property is "FoodTruck/WheelTests.swift", the
/// file name is "WheelTests.swift".
///
/// The structure of file IDs is described in the documentation for
/// in the Swift standard library.
///
/// ## See Also
///
/// - fileID
/// - moduleName
public var fileName: String {
let lastSlash = fileID.lastIndex(of: "/")!
}
/// The name of the module containing the source file.
///
/// The name of the module is derived from this instance's fileID
/// property. It consists of the substring of the file ID up to the first
/// forward-slash character ("/".) For example, if the value of this
/// instance's fileID property is "FoodTruck/WheelTests.swift", the
/// module name is "FoodTruck".
///
/// The structure of file IDs is described in the documentation for the
/// macro in the Swift standard library.
///
/// ## See Also
///
/// - fileID
/// - fileName
public var moduleName: String {
let firstSlash = fileID.firstIndex(of: "/")!
}
/// The path to the source file.
///
/// - Warning: This property is provided temporarily to aid in integrating the
/// testing library with existing tools such as Swift Package Manager. It
/// will be removed in a future release.
public var _filePath: String
/// The line in the source file.
public var line: Int {
didSet {
precondition(line > 0)
}
}
/// The column in the source file.
public var column: Int {
didSet {
precondition(column > 0)
}
}
public init(fileID: String, filePath: String, line: Int, column: Int) {
self.fileID = fileID
self._filePath = filePath
self.line = line
self.column = column
}
}
confirmation内で呼び出している @_spi定義のconfirmationを見る。
Swift TestingのSPI group定義は以下。
@_spi(ForToolsIntegrationOnly)
For interfaces used to integrate with external tools
@_spi(Experimental) : こちら
For interfaces that are experimental or under active development
code:Confirmation.swift
@_spi(Experimental)
public func confirmation<R>(
_ comment: Comment? = nil,
expectedCount: some Confirmation.ExpectedCount,
_ body: (Confirmation) async throws -> R
) async rethrows -> R {
let confirmation = Confirmation()
defer {
let actualCount = confirmation.count.rawValue
if !expectedCount.contains(actualCount) {
Issue.record(
expectedCount.issueKind(forActualCount: actualCount),
comments: Array(comment),
backtrace: .current(),
sourceLocation: sourceLocation
)
}
}
return try await body(confirmation)
}
Confirmation instanceを生成し、body closureを await 実行後 defer 内で confirmationの呼び出し数 count が expectedCountに含まれていなかったら Issue.record()を呼び出す。
ここで、expectedCountが Int から some Confirmation.ExpectedCount に変化している。
code:Confirmation.swift
@_spi(Experimental)
extension Confirmation {
/// A protocol that describes a range expression that can be used with
/// confirmation(_:expectedCount:sourceLocation:_:)-41gmd.
///
/// This protocol represents any expression that describes a range of
/// confirmation counts. For example, the expression 1 ..< 10 automatically
/// conforms to it.
///
/// You do not generally need to add conformances to this type yourself. It is
/// used by the testing library to abstract away the different range types
/// provided by the Swift standard library.
public protocol ExpectedCount: Sendable, RangeExpression<Int> {}
}
RangeExpression<Int>とはあるものの、expectedCount ... expectedCountを渡しているので expectedCountのみとの同値比較になる。将来、実行回数チェックに揺らぎをあたえられるのだろうか。
実装は以上。XCTestCaseのexpectationとfulfillの役割ををconfirmation内でどう置き換えられてるのか分かった。
新設サイトのDNS設定を終えた
新規domain購入してNetlifyへの設定を済ませた。
こうしていちからネームサーバー設定したり証明書が反映されてるかチェックするのは久々なので以下のサイト見ながら確認してた。