本文へ移動
Hikari 仕様

テスト (hikari test) 仕様

本書は hikari test の仕様を定める。起動ディスパッチ全体は hikari-command.md、言語の意味論は language-spec.md、テストライブラリ本体の網羅仕様は std/test.mdstd/assert.md を参照。

Hikari のテストを Hikari 自身で書く。*_test.hika 内に test "name" {block} を並べ、assert 名前空間でアサーションする。CI やローカル開発での回帰検査を想定する。

test / asserthikari 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 件のテストの締切をミリ秒で与える。既定は 600000 は無制限。値を伴わない --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) condtrue (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 は各対象ファイルについて次を行う:

  1. 静的検査 (check.md / static-analysis.md §1) を通す。
  2. test / suite / timeout (std:test) と assert (std:assert) をファイルスコープに供給して評価する (ゼロボイラープレート維持。REPL・playground・hikari run はこの供給を受けず import で明示取得する — 同一意味論・入口 2 つ)。
  3. ファイルの top-level が返す値のうち Trial であるものを記述順に収集する。ファイルトップレベルは 1 つの slot-list 領域なので (language-spec.md §13.1)、そこに置いた bare な式文 (test "name" {block} / suite …) が対象である。名前束縛 (t := …) の値は収集対象にしない (ヘルパー・import は束縛として共有する)。
  4. 収集した 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 testrun の返した 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