2024/8/9
laprasdrum.icon 先々の旅行計画を立ててるときがワクワクする
/icons/hr.icon
Swift Testingの実装を読む
2024/8/4に Swift TestingにaddTearDownBlockがないことが気になり、実装を読んで調査しようとコードリーディングを進めた。 コードを読む前にこちらのコードを用意し、
code:Test.swift
@Test func expectTrue() {
let isTrue = true
}
これをXcode上でExpand Macroを実行する。
code:Expanded.swift
@available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.")
@Sendable private static func $s18SampleProject07SampleB9TestC10expectTrue0F0fMp_16funcexpectTrue__fMu0_() async throws -> Void {
try await Testing.__ifMainActorIsolationEnforced { [] in
let $s18SampleProject07SampleB9TestC10expectTrue0F0fMp_16funcexpectTrue__fMu_ = try await (SampleTest(), Testing.__requiringTry, Testing.__requiringAwait).0
_ = try await ($s18SampleProject07SampleB9TestC10expectTrue0F0fMp_16funcexpectTrue__fMu_.expectTrue(), Testing.__requiringTry, Testing.__requiringAwait).0
} else: { [] in
let $s18SampleProject07SampleB9TestC10expectTrue0F0fMp_16funcexpectTrue__fMu_ = try await (SampleTest(), Testing.__requiringTry, Testing.__requiringAwait).0
_ = try await ($s18SampleProject07SampleB9TestC10expectTrue0F0fMp_16funcexpectTrue__fMu_.expectTrue(), Testing.__requiringTry, Testing.__requiringAwait).0
}
}
さらに @Test にコードジャンプすると以下の定義を確認できる。
code:Testing.swift
/// This macro declaration is necessary to help the compiler disambiguate
/// display names from traits, but it does not need to be documented separately.
///
/// ## See Also
///
/// - Test(_:_:)
@attached(peer) public macro Test(_ traits: any Testing.TestTrait...) = #externalMacro(module: "TestingMacros", type: "TestDeclarationMacro") ざっくりだが TestDeclarationMacro というのが関係してそうということがわかる。
ざっと読んで気になったのが XCTestCase という文字列があるこのコード。
code:TestDeclarationMacro.swift
if let selectorExpr {
// Provide XCTest the source location of the test function. Use the
// start of the function's name when determining the location (instead
// of the start of the @Test attribute as used elsewhere.) This
// matches the indexer's heuristic when discovering XCTest functions.
let sourceLocationExpr = createSourceLocationExpr(of: functionDecl.name, context: context)
thunkBody = """
if try await Testing.__invokeXCTestCaseMethod(\(selectorExpr), onInstanceOf: \(typeName).self, sourceLocation: \(sourceLocationExpr)) {
return
}
\(thunkBody)
"""
}
__invokeXCTestCaseMethod が気になる。
code:TestDeclarationMacro.Swift
// If this function is synchronous and is not explicitly isolated to the
// main actor, it may still need to run main-actor-isolated depending on the
// runtime configuration in the test process.
if functionDecl.signature.effectSpecifiers?.asyncSpecifier == nil && !isMainActorIsolated {
thunkBody = """
try await Testing.__ifMainActorIsolationEnforced { \(captureListExpr) in
\(thunkBody)
} else: { \(captureListExpr) in
\(thunkBody)
}
"""
}
Macro展開時に出てきた __ifMainActorIsolationEnforced を発見。
__invokeXCTestCaseMethod に話を戻し、こちらの定義ファイルを探す。
code:Test+Macro.swift
/// Run a test function as an XCTestCase-compatible method.
///
/// This overload is used for types that are classes. If the type is not a
/// subclass of XCTestCase, or if XCTest is not loaded in the current process,
/// this function returns immediately.
///
/// - Warning: This function is used to implement the @Test macro. Do not call
/// it directly.
public func __invokeXCTestCaseMethod<T>(
_ selector: __XCTestCompatibleSelector?,
onInstanceOf xcTestCaseSubclass: T.Type,
sourceLocation: SourceLocation
) async throws -> Bool where T: AnyObject {
// All classes will end up on this code path, so only record an issue if it is
// really an XCTestCase subclass.
guard let xcTestCaseClass, isClass(xcTestCaseSubclass, subclassOf: xcTestCaseClass) else {
return false
}
Issue.record(
.apiMisused,
backtrace: nil,
sourceLocation: sourceLocation
)
return true
}
XCTestCaseを継承していなければfalseを返すだけとなっている。Swift Testingを利用する場合基本継承しないので、ここではfalseを返される。
Issue.recordに指定しているcommentsもそういう意図に読める。
ちなみに度々出てくる thunk とはサブルーチンに別の演算(関数実行)を埋め込むサブルーチンのことらしい。
先程から見ていた関数の定義は以下で、引数に渡した functionDeclに挿入する関数を定義するsyntax nodeを返り値とする。
@Sendable private ... func ... async throws -> Void { ... } が挿入される関数となる。
code:TestDeclarationMacro.swift
/// Create a thunk function with a normalized signature that calls a
/// developer-supplied test function.
///
/// - Parameters:
/// - functionDecl: The function declaration to write a thunk for.
/// - typeName: The name of the type of which functionDecl is a member, if
/// any.
/// - selectorExpr: The XCTest-compatible selector corresponding to
/// functionDecl, if any.
/// - context: The macro context in which the expression is being parsed.
///
/// - Returns: A syntax node that declares a function thunking functionDecl.
private static func _createThunkDecl(
calling functionDecl: FunctionDeclSyntax,
on typeName: TypeSyntax?,
xcTestCompatibleSelector selectorExpr: ExprSyntax?,
in context: some MacroExpansionContext
) -> FunctionDeclSyntax {
...
let thunkDecl: DeclSyntax = """
@available(*, deprecated, message: "This function is an implementation detail of the testing library. Do not use it directly.")
@Sendable private \(_staticKeyword(for: typeName)) func \(thunkName)\(thunkParamsExpr) async throws -> Void {
\(thunkBody)
}
"""
return thunkDecl.cast(FunctionDeclSyntax.self)
}
code:SyntaxNodesEF.swift
// MARK: - FunctionDeclSyntax
/// A Swift func declaration.
///
/// ### Example
///
/// A func declaration may be declared without any parameter.
///
/// `swift
/// func foo() {
///
/// }
/// `
///
/// A func declaration with multiple parameters.
///
/// `swift
/// func bar(_ arg1: Int, _ arg2: Int) {
///
/// }
/// `
///
/// ### Children
///
/// - attributes: AttributeListSyntax
/// - modifiers: DeclModifierListSyntax
/// - funcKeyword: func
/// - name: (<identifier> | <binaryOperator> | <prefixOperator> | <postfixOperator>)
/// - genericParameterClause: GenericParameterClauseSyntax?
/// - signature: FunctionSignatureSyntax
/// - genericWhereClause: GenericWhereClauseSyntax?
/// - body: CodeBlockSyntax?
public struct FunctionDeclSyntax: DeclSyntaxProtocol, SyntaxHashable, _LeafDeclSyntaxNodeProtocol {
今日はここでおしまい。
始めて見るキーワードを探索する時間が多く、結局addTearDownBlockを独自に追加する上で必要なライフサイクルの話が掴めなかったが、Macro展開後の実装の追い方やSwiftSyntaxとの関係が少し理解できた。
次回はライフサイクル関連のコードを追う。