テスト (hikari test) 仕様
本書は hikari test の仕様を定める。起動ディスパッチ全体は hikari-command.md、言語の意味論は language-spec.md、テストライブラリ本体の網羅仕様は std/test.md・std/assert.md を参照。
Hikari のテストを Hikari 自身で書く。*_test.hika 内に test "name" {block} を並べ、assert 名前空間でアサーションする。CI やローカル開発での回帰検査を想定する。
test / assert は hikari test 専用の魔法束縛ではなく、import で取得する通常のライブラリ (std:test / std:assert) である。hikari test はそれらで組んだテストを収集・実行する専用ランナーであり、REPL・playground・hikari run からも同じ API を使える。
構文
hikari test [path...] [--name <NAME>] [--timeout <MS>] [--report-time] [--smt=z3]
pathは 0 個以上。各pathはファイルまたはディレクトリ。- ディレクトリは配下を再帰し、ファイル名が
_test.hikaで終わるものを収集する。 - ファイルはそのまま対象にする (名前が
_test.hikaで終わらなくてよい)。 - 引数省略時は cwd を対象ディレクトリとする。
- 収集されたファイルはパス昇順で評価する。
--name <NAME>(--name=<NAME>形式も可) を与えると、名前が<NAME>と一致するテストだけを実行する。値を伴わない--nameは引数不正 (終了コード2)。--timeout <MS>(--timeout=<MS>形式も可) は 1 件のテストの締切をミリ秒で与える。既定は60000。0は無制限。値を伴わない--timeout・整数として読めない値は引数不正 (終了コード2)。テスト側がtimeout(ms, trial)(std/test.md §1) で包んだ部分木はそちらが優先する。締切の意味論と効かない区間は std/test.md §4 が定める。--report-timeを与えるとすべてのテストの実行時間を出力する。与えない場合は 100 ミリ秒を超えたテストにだけ出る (下記「出力と終了コード」)。--smt=z3を与えると、テストファイルの静的型検査で refinement 述語の証明に外部 SMT solver (z3) を使う (language-spec.md §17.7)。z3が PATH に無い場合・版が古い場合はエラーで停止する (終了コード2)。位置は問わない (pathの前後どちらでもよい)。hikari check --smt=z3(check.md) と同じ証明が走るが、こちらは評価まで行う点が違う — 証明が増えると型消去 (language-spec.md §17) の対象も増えるため、消去のシャドウ検証を solver 構成で回せる唯一の経路である。
テストライブラリ (std:test / std:assert)
テストの語彙は 2 つの標準ライブラリモジュールとして提供する。ambient 束縛ではないため、import すれば REPL・playground・hikari run を含む任意の文脈で使える。
std:assert — アサーション (状態なし)
equal / not_equal / ok / length / empty / fail を持つ純粋な名前空間。失敗時に panic (Error) を投げるだけで std:test に依存しない。test の外で失敗した場合は通常どおりトップレベルまで伝播する。
import assert := "std:assert" assert.equal(1 + 1, 2)
| 形 | pass 条件 | 失敗メッセージ例 |
|---|---|---|
assert.equal(actual, expected) |
actual == expected (language-spec.md §9.1。ユーザー定義 == スロットも尊重) |
assert.equal: expected 3, got 4 |
assert.not_equal(a, b) |
a == b が偽 |
assert.not_equal: expected values to differ, both 3 |
assert.ok(cond) |
cond が true (Bool、language-spec.md §9.3) |
assert.ok: expected true, got false |
assert.fail(message) |
(常に失敗) | message |
両辺・実値の表示は各値の Inspect を用いる。
std:test — テストの構築と実行 (collect-then-run)
test は receiver を持ち回らない純関数で、テストを構築するだけ (実行しない)。実行・集計は run が担う。
import { test, run } := "std:test" import assert := "std:assert" run [ test "add" { assert.equal(1 + 1, 2) } test "less" { assert.ok ("a" < "b") } ]
| 形 | 返り値 | 意味 |
|---|---|---|
test("name", {block}) / test "name" {block} |
Trial | name (String) のテストを 1 件構築する。block は必須スロット 0 個の本体付きオブジェクト ({body} 形)。実行はしない |
suite("name", [trials]) |
Trial | 子 Trial をまとめたグループを構築する (ネスト可)。実行時に名前は 親/子 でプレフィックスする |
timeout(ms, trial) |
Trial | trial の部分木に締切 ms (ミリ秒) を掛けた Trial を構築する。0 は無制限。入れ子は内側が勝つ |
run([trials]) |
Report | Trial 列を記述順に実行し、集計した Report を返す。出力はしない |
run の合否は各 Trial の block を呼んだ結果で決まる: panic (Error) なら FAIL (メッセージと位置を記録)、それ以外は PASS。締切に達した Trial も FAIL の 1 種である (std/test.md §4)。1 件が失敗しても残りは継続実行する (合否境界は run にある)。group Trial は子を再帰実行する。
Report は集計値で、少なくとも次を持つ:
{
passed: Int
failed: Int
duration_ms: Int
trials: List({ name: String, passed: Bool, duration_ms: Int |})
failures: List({ name: String, message: String, pos: String |})
|}
出力・整形は行わない (printer / reporter は将来課題)。hikari test は返った Report を「出力と終了コード」の形式で整形出力する。REPL では run の返り値をそのまま参照・表示すればよい。
収集 (hikari test の top-level Trial 収集)
hikari test は各対象ファイルについて次を行う:
- 静的検査 (check.md / static-analysis.md §1) を通す。
test/suite/timeout(std:test) とassert(std:assert) をファイルスコープに供給して評価する (ゼロボイラープレート維持。REPL・playground・hikari runはこの供給を受けずimportで明示取得する — 同一意味論・入口 2 つ)。- ファイルの top-level が返す値のうち Trial であるものを記述順に収集する。ファイルトップレベルは 1 つの slot-list 領域なので (language-spec.md §13.1)、そこに置いた bare な式文 (
test "name" {block}/suite …) が対象である。名前束縛 (t := …) の値は収集対象にしない (ヘルパー・import は束縛として共有する)。 - 収集した Trial 列を
runで実行し、返った Report を「出力と終了コード」の形式で整形出力し、終了コードを立てる。
したがって _test.hika は従来どおり top-level に test "name" {block} を並べるだけでよい。名前束縛 (ヘルパー定義・import した被テストコード) は従来どおり共有する。
名前フィルター (--name)
--name <NAME> を与えると、収集した Trial を実行前に名前で枝刈りする。あるノードは、
自身の名前が <NAME> と一致すればその部分木ごと実行対象になる (単体テストなら 1 件、
suite なら配下すべて)。一致しない suite でも、子孫に一致があれば一致子孫だけを残して
実行する。単体テストで一致しなければ除外する。
同名が複数あればすべて実行する。一致 0 件なら 0 passed, 0 failed を出して正常終了 (0) する。
一致判定は各テストの名前の値 (test "…" に渡した文字列。エスケープ解決後) に対して行う。
分離 (isolation)
- 各テストファイルは fresh な root 環境 で評価する (
hikari <file>1 回相当)。ファイル間で状態は共有しない。 - ファイル評価はまず全 top-level 文を評価して Trial (thunk) を構築し、その後
runが各 Trial のblockを実行する。したがって top-level の副作用はテスト body より前に一度だけ起こり、各テストは共有 top-level 束縛 (ヘルパー定義・importした被テストコード) を参照する。テスト間の自動状態リセットは行わないため、共有された mutable 状態を変更すると後続テストに影響しうる。 - 被テストコードは
import <対象> := "<rel>"で取り込む (hikari-command.md §5.3 と同じ caller ディレクトリ基準の解決)。被テストコード側も必要ならimport assert := "std:assert"してアサートヘルパーを共有できる。
静的型検査
各テストファイルは、評価の 前に 静的型検査 (static-analysis.md §2) を受ける。型エラーが 1 件でもあるファイルは Trial を構築せず、診断を stderr に出して失敗として数える (ファイル実行 hikari-command.md §5.5 と同じ水準・同じ規則)。
- 検査単位は他のコマンドと同じくファイルを entry としたモジュールグラフ全体。あるテストファイルが型エラーで落ちても、他のファイルの収集・実行は続ける (「分離」の原則どおり)。
- オプトアウトは無い。テストコードも被テストコードと同じ水準で検査する。
- 型エラーがあったファイルが 1 つでもあれば終了コードは
1(下記のテスト失敗と同じ扱い)。
出力と終了コード
hikari test は run の返した Report をもとに、各ファイルについてファイル名見出しを出し、テストごとに PASS / FAIL 行 (FAIL は失敗メッセージと位置を続ける) を出力。最後に集計行 <P> passed, <F> failed を出す。出力例:
foo_test.hika PASS add は和を返す PASS 重い往復 (15.1s) FAIL add は0で恒等 assert.equal: expected 5, got 6 (foo_test.hika:8:3) FAIL 終わらないもの timeout: exceeded 60000 ms (foo_test.hika:20:5) 3 passed, 2 failed (75.4s)
実行時間の綴りは次のとおり。
- テストごと: 行末に
(<時間>)を付ける。付けるのは 100 ミリ秒を超えたテストだけで、--report-timeを与えたときは全件に付ける。遅いテストが既定で自分から名乗る一方、既存の出力を広く動かさないためである。 - 集計行: 常に
(<時間>)を末尾に付ける。値は Report のduration_ms(実行した単体 Trial の合計) で、静的検査と収集の時間は含まない。<F> failedと<N> files not runの後ろに置く。 - 時間の綴り: 1000 ミリ秒未満は
<N>ms(整数)、以上は<N.N>s(小数第 1 位)。
| 状況 | code |
|---|---|
| 全テスト pass (対象 0 件を含む) | 0 |
| 1 件でも FAIL / 構文・評価エラー | 1 |
| I/O エラー / 引数不正 | 2 |