この記事ではSwiftコンパイラ開発におけるテストについて解説します。
この記事は以下の記事の既読者を対象にしています。
説明の際にディレクトリ名などの環境は、これに従っていると想定しています。
文書
テストについて書かれた文書はdocs/Testing.mdがあります。わからない事がある場合、まずはこれを読むと良いです。
テストの種類
Swiftコンパイラプロジェクトでは、主に4種類のテストがあります。
- ユニットテスト
- 通常テスト
- validationテスト
- ベンチマーク
以下では、個別に説明していきます。
ユニットテスト
ユニットテストはC++で書かれたテストです。テストフレームワークとして、googletestが使われています。C++ではかなりメジャーですね。
コンパイラ内部における様々なライブラリやモジュールがこの仕組みでテストされています。テストコードはswift-source/swift/unittests
に入っていて、テストコードのビルドはCMakeLists.txt
で設定されています。モジュールごとにグループ分けされていて、swift-source/swift/unittests/<module>
が1つの実行ファイルになります。
ユニットテストは通常テストの一部として一緒に実行されるので、build-script
でテストを実行すれば一緒に実行されます。以下に例を示します。
$ cd swift-source/swift
$ utils/build-script --release-debuginfo --test
ユニットテストのグループごとに実行ファイルになっているので、これを単体で実行する事もできます。プロジェクトをビルドすると、ビルドディレクトリの中のunittests
ディレクトリの中に実行ファイルが生成されています。実行ファイルの名前はSwift<module>Tests
という名前になっています。例えば、以下はParseモジュールのユニットテストを実行します。
$ cd swift-source/build/Ninja-RelWithDebugInfoAssert/swift-macosx-x86_64
$ unittests/Parse/SwiftParseTests
通常テスト
通常テストはlitというテストフレームワーク向けに書かれたテストです。litはLLVMプロジェクトの成果物で、swiftコンパイラプロジェクトでもこれを利用しています。文書は以下にあります。
LLVM Testing Infrastructure Guide
litはテストコードとしてlit向けのテキストファイルを読み込むようになっています。そのテストコードの中では任意のコマンドを実行する命令を書く事ができるので、これを使ってビルドして生成されたswiftコンパイラを実行して、その出力をテストするようにしています。ただ、コマンドはなんでも実行できるので、swiftコンパイラ以外の雑多なテスト用の実行ファイルを実行していたり、ただのdiffを実行していたり、様々なテストが行われます。
このlitは面白い仕組みを持っています。swift-source/swift/test/Syntax/tokens_escaped_identifier.swift
を例に仕組みを説明します。テストコードは以下のようになっています。
$ cat tokens_escaped_identifier.swift
// RUN: %swift-syntax-test -input-source-filename %s -dump-full-tokens | %FileCheck %s
let /*leading trivia*/ `if` = 3
print(/*leading trivia*/ `if` )
// CHECK-LABEL: 2:25
// CHECK-NEXT:(Token identifier
// CHECK-NEXT: (trivia block_comment /*leading trivia*/)
// CHECK-NEXT: (trivia space 1)
// CHECK-NEXT: (trivia backtick 1)
// CHECK-NEXT: (text="if")
// CHECK-NEXT: (trivia backtick 1)
// CHECK-NEXT: (trivia space 1))
// CHECK-LABEL: 3:27
// CHECK-NEXT:(Token identifier
// CHECK-NEXT: (trivia block_comment /*leading trivia*/)
// CHECK-NEXT: (trivia space 1)
// CHECK-NEXT: (trivia backtick 1)
// CHECK-NEXT: (text="if")
// CHECK-NEXT: (trivia backtick 1)
// CHECK-NEXT: (trivia space 1))
このテストコードでは、litがRUN:
の行を解釈してコマンドを実行します。%swift-syntax-test
, %s
, %FileCheck
はlitが展開する変数で、それぞれ、ビルドされたswift-syntax-test
コマンドのパス、このテストコードファイル自体のパス、FileCheck
コマンドのパスが入っています。パイプ(|
)はシェルのパイプです。litにとってはCHECK-LABEL:
やCHECK-NEXT:
は命令では無いため特に解釈されません、これは後で説明します。
swift-syntax-test
はSyntaxモジュールのテスト用の実行ファイルで、それの-dump-full-tokens
モードを実行しています。すると、このコマンドはこのテストコードをswiftソースとして解析します。そして、let
の行とprint
の行を解析してダンプします。他にもRUN:
やCHECK-LABEL:
の行も解析されますが、これは//
で始まっているのでswiftソースとしてはコメントとして解釈されます。
その処理結果がFileCheck
に渡されます。FileCheck
はLLVMプロジェクトで作られたツールで、文書はこちらにあります。
FileCheck - Flexible pattern matching file verifier
FileCheck
は標準入力で流し込まれたチェック対象テキストを、引数で渡されたテストコードを使ってチェックします。ここでは%s
が渡されているため、テストコードはまたもやこのテストコード自体です。さて、今度はCHECK-LABEL:
とCHECK-NEXT:
がFileCheck
に対する命令になっています。これは、右側のオペランドの文字列がチェック対象テキストに含まれるかどうかを検査しています。つまり、以下の2つの文字列を探しています。
3:27
(Token identifier
(trivia block_comment /*leading trivia*/)
(trivia space 1)
(trivia backtick 1)
(text="if")
(trivia backtick 1)
(trivia space 1))
3:27
(Token identifier
(trivia block_comment /*leading trivia*/)
(trivia space 1)
(trivia backtick 1)
(text="if")
(trivia backtick 1)
(trivia space 1))
このようにlitでは、テストコードそれ自体がなんらかのコマンドへの入力でありながら、それ自身がチェック内容を記述している、というパターンが多用されています。様々なコマンドやツール、パターンがあるので、自分でテストを書くときにはまずは似たようなテストを探して、それを真似して書くのが良いです。
テストコードはswift-source/swift/test
に入っていて、その中でディレクトリ分けされています。テストの定義はswift-source/swift/test/lit.cfg
などのファイルで設定されていますが、その設定によってこのディレクトリツリーの中にある.swift
ファイルなどがlitの処理にかけられるようになっているので、自分で追加するときはファイルを置くだけで済みます。
前述したユニットテストもこのlitのタスクとして実行されるようになっています。その定義はswift-source/swift/test/Unit
にあります。
テストを実行するにはbuild-script
の--test
オプションを使います。以下は例です。
$ cd swift-source/swift
$ utils/build-script --release-debuginfo --test
テストはかなり時間がかかるので、部分実行すると便利です。部分実行するときは、testディレクトリ配下のパスをディレクトリやファイルのパスを、ビルドされたテストディレクトリのパスに結合して、litに指定して実行します。ビルドされたテストディレクトリはターゲットごとに作られているので、それも適切に選択します。ターゲットについては、だいたいはmacosx-x86_64
で良いでしょう。
litはswift-source/llvm/utils/lit/lit.py
に入っています。引数にはテストのパスを渡します。実行オプションは-sv
が良いです。例えば、全てのテストを実行するときは以下のようなコマンドを実行します。
$ cd swift-source
$ llvm/utils/lit/lit.py -sv build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64
Syntaxモジュールについてのテストだけを実行するときは以下のようになります。
$ cd swift-source
$ llvm/utils/lit/lit.py -sv build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64/Syntax
先程紹介したファイルだけを実行するときは以下のようになります。
$ cd swift-source
$ llvm/utils/lit/lit.py -sv build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64/Syntax/tokens_escaped_identifier.swift
注意点として、ファイルとしてbuild/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64/Syntax/tokens_escaped_identifier.swift
は存在しない点に注意が必要です。build/Ninja-RelWithDebInfoAssert/swift-macosx-x86_64/test-macosx-x86_64
は実在するディレクトリですが、その続きのSyntax/tokens_escaped_identifier.swift
の部分は、swift-source/swift/test
からの相対パスです。このように、litが認識している仮想的なパスとして、テストコードを指定するようになっています。
実行時オプションの-sv
については、-s
と-v
の同時指定です。-s
は進捗率のプログレスバーが出る指定です。-v
は失敗したテストについての詳細を出力する指定です。その他に、-a
はテストの出力を全て出すので便利です。慣れてきたらいろいろ活用するとはかどります。
lit.py
のパス指定が面倒なので、僕はlit.py
が直接コマンドとして使えるように、ホスト環境の$HOME/bin
からシンボリックリンクを張っています。
validationテスト
validationテストは通常テストよりも入念なテストです。コンパイラに対する重要な変更をした際に実行するとのことですが、その基準については僕はよく知りません。テストフレームワーク自体は通常テストと同様にlitを使っています。
テストコードはvalidation-test
に入っていて、実行はbuild-script
の--validation-test
で行います。
$ cd swift-source/swift
$ utils/build-script --release-debuginfo --validation-test
ベンチマーク
動作性能を検査するためにあるようですが、僕はよく知りません。