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

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

ベンチマーク

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

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.