Vim の "make test" を読み解く 第 5 回 (runtest.vim)

Vim の new-style-testing を実行している張本人の runtest.vim に、いよいよ潜入していきたいと思います。まずはファイル先頭部分の訳。

> このスクリプトはテスト付きの .vim ファイルを開いているときに読み込まれます。スクリプトが正常終了すると .res ファイルが生成されます。エラーは test.log ファイルに追加されます。

> 特定のテスト関数のみを実行するには 2 つめの引数を追加します。それは Test_ 関数の名前と一致します。

> 出力は "messages" ファイルの中に見つけることができます。

>テストスクリプトには何でも含むことができますが、"Test_" で始まる関数だけは特別です。これらは呼び出されるので、アサート関数を含んでいるべきです。例として test_assert.vim を参照してください。

> "Test_" 関数を含む他のファイルを読み込むことも可能です。これは Vim を再起動する必要がないので、テストを早くすることができます。ただし、テストがお互いに干渉しないように留意してください。

> エラーがアサート関数によって適切に検出できない場合は、エラーを v:errors リストに追加してください。

> 各 Test_ 関数に準備が必要な場合は、SetUp 関数を定義します。これは各 Test_ 関数の前に呼び出されます。

> 各 Test_ 関数の後にクリーンアップが必要な場合は、TearDown 関数を定義します。これは各 Test_ 関数の後に呼び出されます。

> テストをデバッグするときには v:errors にメッセージを追加するのが便利です。

```vim
" Support function: get the alloc ID by name.
function GetAllocId(name)
```
これは、[第 3 回](https://qiita.com/m_nish/items/957f52c71884fa055dd9) に出てきた `test_alloc_fail()` を呼ぶ際に、fail させるメモリ割り当ての ID を取得するための関数ですね。

```vim
func RunTheTest(test)
```
ファイル中の 1 つのテストを実行するメインの関数

* 初期設定いろいろ
* SetUp が存在しているときはそれを呼ぶ
* "Test_nocatch_" で始まるときはエラーを自分でハンドルする
* 意図せず Vim が終了した時の処理を VimLeavePre で設定
* テスト関数を呼び出し
* skipped や error が起きたときの処理
* TearDown が存在しているときはそれを呼ぶ
* すべての autocommand を消して swap の処理だけ設定する
* 不要なタブやウィンドウを消す

```vim
func AfterTheTest()
```
ファイル中の 1 つのテストの終わりの処理

* `v:errors` が 1 以上であれば、
    * `s:fail` のカウント、メッセージの出力
    * テスト関数で見つかったエラーは `v:errors` に入るので、それを `s:errors` に渡して `v:errors` をクリア

```vim
func EarlyExit(test)
```
VimLeavePre の自動コマンドに割り当てられている。Vim が意図せず終了した時に `v:errors` を追加している。

```vim
" This function can be called by a test if it wants to abort testing.
func FinishTesting()
```
テストを中止したいときに呼ぶこともできるが、通常はすべてのテストを実行した後に呼ばれて後処理を行う。

* クリーンアップとか
* `s:fail` が 0 であれば .res ファイルを作成
* `s:errors` があれば test.log に書き込む
* いくつのテストが実行されたか表示
* FAILED や SKIPPED の表示、messages への書き込み
* Vim を終了させる

```vim
let s:done = 0
let s:fail = 0
let s:errors = []
let s:messages = []
let s:skipped = []
```
* `s:done` : 実行されたテストの数
* `s:fail` : failしたテストの数
* `s:errors` : errorの内容、s:messagesにも追加される
* `s:messages` : テスト中のメッセージの表示、エラー以外も含む
* `s:skipped` : スキップされた内容の表示

```vim
if expand('%') =~ 'test_vimscript.vim'
  " this test has intentional s:errors, don't use try/catch.
  source %
else
  try
    source %
  catch
    let s:fail += 1
    call add(s:errors, 'Caught exception: ' . v:exception . ' @ ' . v:throwpoint)
  endtry
endif
```
Vim の引数としてファイルを開いただけだと、単に開いただけで Vim script として読み込まれていない (テストファイル内の関数等が認識されていない) のでここで読み込む。

```vim
" Names of flaky tests.
let s:flaky_tests = [
      \ 'Test_call()',
      \ 'Test_channel_handler()',
      \ 'Test_client_server()',
      \ ....
      \ 'Test_zz1_terminal_in_gui()',
      \ ]

" Pattern indicating a common flaky test failure.
let s:flaky_errors_re = 'StopVimInTerminal\|VerifyScreenDump'
```
"flaky" をなんと訳すのがいいのか分かりませんが、タイミングやリソースの状態？によっては fail する可能性のあるテストのリストです。詳細は後述します。`s:flaky_errors_re` の関数を実行中に fail した場合には一律 flaky として扱われるようです。

```vim
" Locate Test_ functions and execute them.
redir @q
silent function /^Test_
redir END
let s:tests = split(substitute(@q, 'function \(\k*()\)', '\1', 'g'))

" If there is an extra argument filter the function names against it.
if argc() > 1
  let s:tests = filter(s:tests, 'v:val =~ argv(1)')
endif
```
`:function` の出力結果を利用して、"Test_" から始まる関数の一覧を `s:tests` に作っています。もしくは引数で特定のテストが与えられている場合はそれ単体のみに `filter()` しています。

```vim
" Execute the tests in alphabetical order.
for s:test in sort(s:tests)
  ....

  call RunTheTest(s:test)

  " Repeat a flaky test.  Give up when:
  " - it fails again with the same message
  " - it fails five times (with a different mesage)
  if len(v:errors) > 0
        \ && (index(s:flaky_tests, s:test) >= 0
        \      || v:errors[0] =~ s:flaky_errors_re)
    while 1
      ....

      if run_nr == 5 || prev_error == v:errors[0]
        call add(total_errors, 'Flaky test failed too often, giving up')
        let v:errors = total_errors
        break
      endif

      ....

      call RunTheTest(s:test)

      if len(v:errors) == 0
        " Test passed on rerun.
        break
      endif
    endwhile
  endif

  call AfterTheTest()
endfor

call FinishTesting()
```
各テストをアルファベット順に実行していきます。たまに "Test_zz" で始まる名前のテストがありますが、最後に実行したいという意図のようですね。
`RunTheTest()` でテストを実行します。`v:errors` が空でなくて (fail していて) かつ flaky テストの場合、

* 同じメッセージで 2 回 fail する
* もしくは 5 回 fail する

まで繰り返します。
各テストの終わりに `AfterTheTest()` を実行し、対象のテストをすべて実行し終えたら最後に `FinishTesting()` を実行しています。

第 5 回はここまで。