条件付きで添付物を保存する
Conditionally saving attachments
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
ST-0009 で導入された Swift Testing の attachment 機能では、Attachment.record(_:named:sourceLocation:) を呼び出した時点でその添付物が常に保存対象になります。テストの結果に関わらずデータが保存されてしまうため、テストが成功した場合や、特定の条件を満たすときだけ添付物を残したい、というニーズには対応できませんでした。
XCTest では XCTAttachment の lifetime プロパティを設定することで、たとえば「テストが失敗したときだけ添付物を保存する」といった制御が可能です。CI 環境のように永続ストレージにコストがかかったり、容量が限られていたりする環境では、サイズの大きな添付物(数百 MB 以上にもなり得る画像やログなど)が常に保存されると、保存先の容量を圧迫したり料金がかさんだりします。Swift Testing にも同等の制御手段が求められていました。
この機能は ST-0009 でも future direction として挙げられていたものです。
02 どのように解決されるのか
添付物を保存するかどうかの条件を表す新しい trait AttachmentSavingTrait を導入します。この trait をテスト関数やテストスイートに付与すると、Attachment.record(_:named:sourceLocation:) で記録された添付物はいったんメモリなどの一時領域に保持され、テスト終了時に trait の条件が評価されて、条件を満たす場合のみ永続ストレージ(ディスク上のファイルや Xcode のテストレポートなど)へ保存されます。条件を満たさない場合、添付物は破棄されます。
trait は Trait/savingAttachments(if:) というファクトリ関数から生成します。複数の AttachmentSavingTrait が(直接または間接に)適用されている場合、添付物が保存されるためにはそれらの条件が すべて 満たされる必要があります。これは既存の ConditionTrait の挙動と一貫しています。
なお、この trait をまったく付与していないテストでは、これまでと同様に実行環境側の設定に従って添付物が扱われます。Xcode ではテストプランの設定、VS Code では既定で ./.build 配下の一時ディレクトリ、swift test では --attachments-path オプションの指定によって保存先が決まります。
条件の指定
条件として、よく使われるパターンは AttachmentSavingTrait.Condition の static プロパティ/メソッドとして提供されます。
extension AttachmentSavingTrait {
public struct Condition: Sendable {
/// テストが成功した場合に保存します。
public static var testPasses: Self { get }
/// テストが失敗した場合に保存します。
public static var testFails: Self { get }
/// 記録された issue のいずれかが条件に合致した場合に保存します。
public static func testRecordsIssue(
matching issueMatcher: @escaping @Sendable (_ issue: Issue) async throws -> Bool
) -> Self
}
}
たとえば、テストが失敗したときだけ添付物を残したい場合は次のように書きます。
@Test(.savingAttachments(if: .testFails))
func `All vowels are green`() {
var vowels = "AEIOU"
if Bool.random() {
vowels += "Y"
}
Attachment.record(vowels)
#expect(vowels.allSatisfy { $0.color == .green })
}
特定のファイル中で issue が記録された場合だけ保存したい、といった用途には testRecordsIssue(matching:) を使います。
extension Issue {
var inCriticalFile: Bool {
guard let sourceLocation else { return false }
return sourceLocation.fileID.hasSuffix("Critical.swift")
}
}
@Test(.savingAttachments(if: .testRecordsIssue { $0.inCriticalFile }))
func `Ideas taste tremendous`() { ... }
テスト結果以外の外部状態に基づいて条件を組み立てたい場合のために、Bool を返す autoclosure や、エラーを throw し得るクロージャ、async クロージャを受け取るオーバーロードも用意されます。複数の trait を並べると AND 条件として評価されます。
@Test(
.savingAttachments(if: CommandLine.arguments.contains("--save-attachments")),
.savingAttachments { try await CI.current.storage.available >= 500.MB }
)
func `The fandango is especially grim today`() { ... }
クロージャがエラーを throw した場合は、そのエラーが issue として記録されたうえで、添付物は破棄されます。
JSON event stream への影響
JSON event stream を消費するツールは、.valueAttached イベントを観測しているのであれば基本的に変更は不要です。trait が適用されたテストでは、このイベントの配信タイミングが従来より遅くなりますが、それでも対応する .testCaseEnded よりは前に届きます。
スキーマバージョン "6.3" 以降では、添付物を表す JSON 構造に次の 2 つのプロパティが追加されます。
preferredName: テスト作者が指定した、添付物の優先ファイル名(文字列)bytes: 添付物のシリアライズ表現(Base64 エンコードされた文字列、または 1 バイトずつの整数配列)。既存のpathプロパティが設定されている場合は省略可能ですが、添付物の保存ディレクトリを指定せずに event stream だけを消費するツールにとっては、このプロパティが添付物を復元する唯一の手段になります
03 今後の見通し
将来の発展としては次のような方向が挙げられています。
- 添付物を「現在のテスト」ではなく「個別の
Issue」に紐付けられるようにする方向。Issue.record()への引数追加といった形が想定されています。 - テスト単位ではなく個々の添付物単位で保存可否を制御する方向。
Attachment.record()にAttachmentSavingTrait.Condition型の引数を追加する案や、AttachmentSavingTraitをローカルスコープに適用できるようにする案(これは「ローカルスコープを持つ trait」という別の検討対象とも関係します)が議論されています。多くのテスト作者にとってはテスト単位の粒度で十分という想定のもとでの将来案です。 ConditionTraitに対して boolean 演算(AND / OR / NOT など)を行えるようにする拡張。同様の機能をAttachmentSavingTraitにも持たせることが検討されています。
これらは将来の構想であり、実現を約束するものではありません。