※ この記事は Qiita/Zenn への同時投稿です。
はじめに
筆者はマークダウンエディタ viMarkdown を開発中だ。Qt C++ によるネイティブデスクトップアプリである。
Windows用バイナリ、ソースコードは下記で公開中だ。
https://github.com/vivisuke/viMarkdown
個人利用は無料で、社会人の業務利用にはライセンス料(2,000〜4,000円)が必要である。
ver 0.3 は、5月に開発版、8月にアルファ版、10月にベータ版と進み、12月に RC・Stable のリリースを予定している。その目玉のひとつが vi/ex コマンドサポートだ。
ただし、viMarkdown は Vim の全コマンドをサポートするものではない。対応しているのは、ノーマル・ビジュアル・挿入の3モードを中心とした基本操作で、undo も回数制限なく利用できる。ex コマンドについても、対応は一部にとどまっている。
にもかかわらず、vi コマンドの挙動が1文字でもずれていると Vim おじさんが怒ることが大きな懸案事項だ。
なぜVimおじさんは1文字のずれで怒るのか
Vim おじさんは、カーソル移動時にどのコマンドを使うか意識していない。移動先を認識するだけで、手が勝手に vi コマンドを奏でる。
なので、カーソル移動後編集コマンドを実行すると、編集結果が意図しないことになる危険性があるのだ。
そのため、カーソル移動が1文字でもずれるのには耐えられないのである。
自動テストの必要性
そこで、徹底的なテスト・調整が必要なのだが、vi コマンドは組み合わせが爆発的で、膨大な工数を要する。工数が operator × motion × count の掛け算になるからだ。
- operator:
cdy<> - motion:
hjklSpcBSwWeEbB0^$fFtT,; - count:
2w3ddなど
カーソル位置が1文字でもずれると即アウトだ。手動テストは無理ゲーである。局所的な対処による副作用も心配だ。
テストランナー
テストランナーはあらかじめ用意したテストケースを実行し、動作が期待通りかどうかをチェックするもので、 viMarkdown 本体に組み込んだ。専用のテストツールを別に作るのではなく、アプリ自身にテストケースを一括実行する仕組みを持たせた。
使い方は単純で、その他 > テスト > vi commands メニューからテストを実行すると、登録されたテストケースをすべて流し、結果を表示する。
テストケース
テストケースは、次の形式のデータとして定義する。
- テスト名
- 初期テキスト・カーソル位置
- (vi/ex コマンド, 期待されるテキスト・カーソル位置)の繰り返し
カーソル位置は ┃、選択範囲は 《 》 で表現する。┃ が 《 》 の内側にある場合は、選択範囲の端にカーソルがあることを表す。
{ "Move cursor left", "h┃ello\n",
{"h", "┃hello\n", "h", "┃hello\n", } },
{ "Visual mode", "h┃ello\n",
{"v", "h《┃e》llo\n", "l", "h《e┃l》lo\n", } },
{ "Ex Delete - Current Line (:d)", "line 1\nli┃ne 2\nline 3\n",
{ ":d", "line 1\n┃line 3\n" } },
コマンドを1つ実行するたびに、その時点のテキストとカーソル位置が期待どおりかを検証する。1ケースの中でコマンドを連続して実行し、途中の状態まで含めてチェックできるのがポイントだ。
テスト結果メッセージ
正常時は、何をテストしたかとテスト件数を表示する。
テスト失敗時は、Before:(実行前)➔ Commands: ➔ Expected: ➔ Actual: を表示する。実行前の状態、実行したコマンド、期待した結果、実際の結果を並べるので、どこで・どうずれたかが一目で分かる。
成功例(出力が長いので、途中を省略している):
Test vi commands...
Move cursor left: ..
Visual mode: ..
Change word (cw) - Single character word: .
Total: 0 failed / 648 tested. (Success:100.0%)
失敗例:
Change word (cw) - Single character word: .
[FAILED #1] wrong document text.
Before: '┃a b c\n'
Commands: 'cw'
Expected: '┃ b c\n'
Actual: '┃ c\n'
この失敗例は、実際に検出されたバグ(SPR #1272、修正済み)である。1文字単語への cw が、次の単語の末尾まで削除してしまっていた。
テストケース作成
テストケースは、基本的に生成 AI に作成してもらう。Muse 先生(Meta のパーソナル AI エージェント)・Gemini 先生が作成したテストケースを、チャッピー先生(ChatGPT)・Claude 先生がレビュー・修正・追加する体制だ。
テスト失敗時も AI に評価させる。失敗の原因は、vi/ex 実装、テストケース、自動テストランナーのいずれにもありうるからだ。
AI が生成したテストケースにも誤りがあった。正確には集計していないが、全体の1〜2%程度である。
実装の誤りとテストケースの誤りが一致すると、問題が表面化しない。そこで Muse が、自身の VM 上で Vim 9.1 をヘッドレス実行し、テストケースの期待値が本家 Vim の挙動と一致するかをチェックする体制にした。
結果
11/04 時点の結果は次のとおりだ。
Total: 0 failed / 648 tested. (Success:100.0%)
なお、648件はコマンドの種類数ではなく、テストケース内で検証したステップの合計である。
vi/ex 品質はかなり高水準になったと自負している。
まとめ
- vi の挙動を忠実に再現するのは想像以上に難しく、コストがかかる
- だが自動テストがあると安心して実装できる
- テストケース作成は手間がかかるが、生成 AI に任せられる(ただし人間・他AI・実際の Vim でのクロスチェックは必要)
- AIによって実装のスピードは上がるが、品質を保証する仕組みまでAI任せにしてはいけない
- 結果:Vim おじさんにも怒られにくくなったはずだ
viMarkdown のダウンロード・試用よろしく〜。