🧪

Tinymist CLIでTypstコードの自動テストをする

に公開

はじめに

Typstはマークアップ言語であると同時にスクリプト言語でもあります。従来の組版システムに比べてより自由な表現がしやすくなった反面、その分コードは複雑になりやすく、品質の維持が難しくなることがあります。複雑なTypstコードはソフトウェアとしての性質が強く、継続的な保守のための品質を保つ仕組みが重要です。そこで、本記事ではTinymist CLIを利用してTypstコードに自動テストを導入する方法を紹介します。

https://typst.app/

Tinymistについて

TinymistはTypstの統合言語サービスです。具体的には、Typstの分析ツール、言語サーバー、プレビューなどの様々な機能を提供しています。

https://github.com/Myriad-Dreamin/tinymist

https://myriad-dreamin.github.io/tinymist/introduction.html

同名のVS Code Extensionが有名だと思われますが、TinymistはCLIからも利用できます。

https://marketplace.visualstudio.com/items?itemName=myriad-dreamin.tinymist

Tinymist CLIの導入

リポジトリのReleasesからバイナリが入手可能です。

https://github.com/Myriad-Dreamin/tinymist/releases

https://repology.org/project/tinymist/versions

導入にはaqua, mise, Nixなどを利用すると便利です。例えば、miseを利用している場合は次のコマンドで導入できます。

https://mise.jdx.dev/

mise use tinymist

単体テストの実行

説明のための題材として、ページ番号の配列を受け取り、ページ番号の範囲を文字列として返す関数を考えます。例えば、(1,2,3,5,6,8)という配列を与えた場合、"1-3,5-6,8"という文字列を返す関数です。

main.typ
#let compress-pages(
  pages,
  sep: ",",
  dash: "-",
) = {
  let filtered = pages.filter(p => p >= 1)
  let sorted = filtered.sorted()
  let unique = sorted
    .fold((out: (), last: none), (acc, p) => {
      if acc.last == none or p != acc.last {
        (out: acc.out + (p,), last: p)
      } else {
        acc
      }
    })
    .out

  if unique.len() == 0 {
    ""
  } else {
    let state = unique.fold((ranges: (), start: none, end: none), (acc, p) => {
      if acc.start == none {
        (ranges: acc.ranges, start: p, end: p)
      } else if p == acc.end + 1 {
        (ranges: acc.ranges, start: acc.start, end: p)
      } else {
        (
          ranges: acc.ranges + ((start: acc.start, end: acc.end),),
          start: p,
          end: p,
        )
      }
    })

    let ranges = state.ranges + ((start: state.start, end: state.end),)

    ranges
      .map(r => if r.start == r.end { str(r.start) } else {
        str(r.start) + dash + str(r.end)
      })
      .join(sep)
  }
}

この関数に対してテストを実行してみましょう。Tinymist CLIでは次のコマンドでテストを実行できます。ここで、--coverageはコードカバレッジを表示するためのオプションです。

https://myriad-dreamin.github.io/tinymist/feature/testing.html#label-CLI Support

tinymist test main.typ --coverage
   Info Found 0 tests and 0 examples
   Info Written coverage to target/coverage.json ...
    Cov Coverage Summary 0/0 (100.00%)
   Info All test cases passed...

Tinymistでは、test-*という名前の関数がテストとして認識されます。この時点ではテストケースが存在せず、関数内のすべての条件分岐が実行されていないため、コードカバレッジは0/0となっています。ここで、次のようなテストケースを追加します。

main.typ
#let test-compress-pages() = {
  assert(compress-pages((1, 2, 3, 5, 6, 8)) == "1-3,5-6,8")
}
   Info Found 1 tests and 0 examples
Running test(test-compress-pages)
 Passed test(test-compress-pages)
   Info Written coverage to target/coverage.json ...
    Cov Coverage Summary 28/32 (87.50%)
   Info All test cases passed...

テストを1件追加したことで、コードカバレッジが87.50%まで向上しました。計測結果はtarget/coverage.jsonに保存されます。

target/coverage.json
{
  "/path/to/main.typ": [
    /* `"executed": true`は省略 */
    {
      "executed": false,
      "location": { "start": { "line": 12, "character": 13 }, "end": { "line": 12, "character": 14 } }
    },
    {
      "executed": false,
      "location": { "start": { "line": 14, "character": 6 }, "end": { "line": 14, "character": 7 } }
    }
  ]
}

網羅されていないケースを確認し、さらにいくつかのテストケースを追加します。

main.typ
#let test-compress-pages() = {
  assert(compress-pages((1, 2, 3, 5, 6, 8)) == "1-3,5-6,8")
  assert(compress-pages((2, 2)) == "2")
  assert(compress-pages(()) == "")
}
   Info Found 1 tests and 0 examples
Running test(test-compress-pages)
 Passed test(test-compress-pages)
   Info Written coverage to target/coverage.json ...
    Cov Coverage Summary 32/32 (100.00%)
   Info All test cases passed...

テストケースを網羅したことで、コードカバレッジが100%になりました。実際にテストを書く際には、複数のケースを細かく分けて記述すると、失敗時の原因特定やユースケースの把握がしやすくなります。最終的に次のようなテストケースが考えられます。

main.typ
#let test-compress-pages() = {
  // Empty
  assert(compress-pages(()) == "")

  // Single page
  assert(compress-pages((5,)) == "5")

  // Adjacent pages
  assert(compress-pages((1, 2)) == "1-2")

  // Gapped pages
  assert(compress-pages((1, 3)) == "1,3")

  // Multiple ranges
  assert(compress-pages((1, 2, 3, 5, 7, 8, 9)) == "1-3,5,7-9")

  // Unsorted duplicates
  assert(compress-pages((3, 1, 2, 2)) == "1-3")

  // Drops nonpositive values
  assert(compress-pages((0, -2, 4)) == "4")
}

Tinymistではこのほかにも、ベンチマークテスト用のbench-*やパニックテスト用のpanic-on-*がサポートされています。さらに、ファイル名をexample-*.typとすることで、PNGやHTMLエクスポートに対するスナップショットテストが実行できます。詳細はTinymistのドキュメントを参照してください。

https://myriad-dreamin.github.io/tinymist/feature/testing.html

おわりに

本記事では、Tinymist CLIを利用してTypstコードに自動テストを導入する方法を紹介しました。Tinymistは発展途上のプロジェクトであり、Typstでの開発や保守に役立つ強力なツールとして、今後もますます期待されます。ぜひTinymistを活用して、Typstプロジェクトの品質向上に役立ててください。

Discussion