Swiftコンパイラ開発におけるテスト

この記事ではSwiftコンパイラ開発におけるテストについて解説します。
この記事は以下の記事の既読者を対象にしています。

[Swiftコンパイラ開発用の環境構築](https://qiita.com/omochimetaru/items/fd485185bcd07c21b49f)

説明の際にディレクトリ名などの環境は、これに従っていると想定しています。

# 文書

テストについて書かれた文書は[docs/Testing.md](https://github.com/apple/swift/blob/master/docs/Testing.md)があります。わからない事がある場合、まずはこれを読むと良いです。

# テストの種類

Swiftコンパイラプロジェクトでは、主に4種類のテストがあります。

- ユニットテスト
- 通常テスト
- validationテスト
- ベンチマーク

以下では、個別に説明していきます。

# ユニットテスト

ユニットテストはC++で書かれたテストです。テストフレームワークとして、[googletest](https://github.com/google/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](https://llvm.org/docs/TestingGuide.html)

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](https://llvm.org/docs/CommandGuide/FileCheck.html)

`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
```

# ベンチマーク

動作性能を検査するためにあるようですが、僕はよく知りません。