継続的インテグレーションを設定する


トラックの継続的インテグレーション(CI)を設定することはとても重要です。ミスを見つける助けになるからです。

GitHub Actions

Exercismのリポジトリ(トラックのリポジトリも含みます)では、CIの実行にGitHub Actionsを使っています。 GitHub Actionsは_ワークフロー_を基本としていて、特定のイベント(たとえばコミットのプッシュ)が発生するたびに自動的に実行するスクリプトを定義します。 GitHub Actionsのワークフローについて詳しくは、ワークフローのドキュメントを参照してください。

あらかじめインストールされているワークフロー

トラックにはあらかじめいくつものワークフローがインストールされています。そのほとんどは変更すべきでは_ありません_(これらは_共有ワークフロー_と呼ばれます)。 ただし、変更すべきワークフローが1つだけあります。それがtest.ymlワークフローです。

テストワークフロー

test.ymlワークフローの目的は、トラックの演習が正しい状態にあることを検証することです。 このワークフローは、mainブランチまたはプルリクエストのブランチにプッシュが行われたときに自動的に実行されるよう設定されています(GitHub Actionsの用語では_トリガー_されます)。

ワークフロー自体が行うことは多くありません。次のことだけです。

  • コードをチェックアウトする(実装済み)
  • 依存関係をインストールする(例:パッケージのインストール、任意)
  • ツールをインストールする(例:SDKのインストール、任意)
  • 演習を検証するスクリプトを実行する(実装済み)

演習を検証するスクリプトを実装する

すでに触れたとおり、演習はスクリプト、つまりbin/verify-exercises(bash)スクリプトで検証します。 このスクリプトは_ほぼ_完成していて、次のことを行います。

  • すべての演習ディレクトリをループする
  • 各演習ディレクトリについて、次を行います。
    • example/exemplarの解答を(スタブの)解答ファイルにコピーする(実装済み)
    • unskip_tests関数を呼び出す。この中でテストファイルのテストのスキップを解除できます(任意)
    • run_tests関数を呼び出す。この中でテストを実行します(必須)

run_testsとunskip_tests関数が、実装する必要のある唯一のものです。

テストのスキップを解除する

トラックがテストのスキップをサポートしている場合、演習のexample/exemplarの解答を検証するときは、テストが1つもスキップされないようにする必要があります。 一般に、トラックがテストの「スキップ解除」をサポートする方法は2つあります。

  1. テストファイルからアノテーション・コード・テキストを削除する。 たとえば、test.skipをtestに変更します。
  2. 環境変数を用意する。 たとえば、SKIP_TESTS=falseを設定します。

テストファイルからアノテーション・コード・テキストを削除する

テストのスキップがファイルベースの場合(上記の1つ目の方法)、unskip_tests関数を編集してテストファイルを変更します(テストファイルをループする処理は既存のコードにすでにあります)。

Note

unskip_test関数は演習ディレクトリのコピーに対して実行されるので、ファイルは自由に変更してかまいません。

例

Arturoトラックのbin/verify-exercises fileは、sedを使ってテストファイル内のテストのスキップを解除します。

unskip_tests() {
    jq -r '.files.test[]' .meta/config.json | while read -r test_file; do
        sed -i 's/test.skip/test/g' "${test_file}"
    done
}

環境変数を用意する

Caution

テストのスキップ解除に環境変数の設定が必要な場合は、それがrun_tests関数で設定されていることを確認してください。

テストを実行する

run_tests関数は、演習のテストを実行する役割を担います。 この関数が呼ばれるときには、example/exemplarのファイルはすでに(スタブの)解答ファイルにコピーされているので、テストを実行するための正しいコマンドを呼び出すだけでかまいません。

すべてのテストがパスしたら、この関数は終了コードとしてゼロを返さなければなりません。そうでなければ、ゼロ以外の終了コードを返します。

Note

run_tests関数は演習ディレクトリのコピーに対して実行されるので、ファイルは自由に変更してかまいません。

オプション1:言語のツールを使う

演習を検証するスクリプトの既定の方法は、その言語のツール(SDKやバイナリなど)を使うもので、ほとんどのトラックがこの方法を使っています。 テストの実行方法はトラックごとに異なりますが、通常はコマンド1つだけです。

例

Arturoトラックのbin/verify-exercises fileは、run_tests関数を変更して、テストファイルに対して単にarturoコマンドを呼び出します。

run_tests() {
    arturo tester.art
}

オプション2:テストランナーのDockerイメージを使う

2つ目の方法は、トラックのテストランナーを実行して演習を検証するものです。 もちろんこれは、トラックに動作するテストランナーがあることが前提です。

トラックにまだテストランナーがない場合は、次のどちらかを行います。

  • 動作するテストランナーをビルドする
  • オプション1を使い、言語のツールを直接使う

既定のbin/verify-exercisesスクリプトには、次の変更を加える必要があります。

  1. dockerコマンドが使えることを確認する
  2. テストランナーのDockerイメージをプル(ダウンロード)する
  3. docker runを使って、各演習でテストランナーのDockerイメージを実行する
  4. jqを使って、Dockerコンテナが返すresults.jsonファイルがすべてのテストのパスを示していることを検証する
  5. unskip_test関数と、その呼び出しを削除する
Note

この方法の主な利点は、本番環境(ウェブサイト上)でテストがどのように実行されるかを最もよく模倣できることです。 この方法なら、CIではパスしたのに本番環境で失敗するという事態が起こりにくくなります。 欠点は、DockerイメージのプルとDockerのオーバーヘッドのため、通常は遅くなることです。

例

Unisonトラックのbin/verify-exercises fileは、dockerコマンドもインストールされていることを検証するチェックを追加します。

required_tool docker

続いて、トラックのテストランナーイメージをプルします。

docker pull exercism/unison-test-runner

次に、run_tests関数を変更して、docker runで現在の演習(作業ディレクトリにあります)に対してテストランナーを実行し、続けてjqコマンドで正しいステータスを確認します。

run_tests() {
    local slug

    slug="${1}"

    docker run \
        --rm \
        --network none \
        --mount type=bind,src="${PWD}",dst=/solution \
        --mount type=bind,src="${PWD}",dst=/output \
        --tmpfs /tmp:rw \
        exercism/unison-test-runner "${slug}" "/solution" "/output"
    jq -e '.status == "pass"' "${PWD}/results.json" >/dev/null 2>&1
}

最後に、run_testsコマンドの呼び出しを変更する必要があります。slugが必要になったからです。

run_tests "${slug}"

テストワークフローを実装する

verify-exercisesスクリプトが完成したので、次はtest.ymlワークフローを仕上げます。 その方法は、verify-exercisesスクリプトの実装でどのオプションを選んだかによって変わります。

オプション1:言語のツールを使う

verify-exercisesスクリプトが言語のツールを直接使う場合、テストワークフローでは次をインストールする必要があります。

  • 言語ツールの依存関係(opensshやC/C++コンパイラなど)
  • 言語ツール(SDKやバイナリなど) 言語ツールのインストールで、インストールされたバイナリがパスに追加され_ない_場合は、必ずGitHub Actionsのシステムパスに追加してください。

それが済めば、verify-exercisesは期待どおりに動作するはずです。これでCIの設定は完了です。

例については、Arturoトラックのtest.ymlワークフローを参照してください。

name: Test

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:

jobs:
  ci:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout repository
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install libgtk-3-dev libwebkit2gtk-4.0-dev libmpfr-dev

      - name: Install Arturo
        run: bin/install-arturo
        env:
          GH_TOKEN: ${{ github.token }}

      - name: Verify all exercises
        run: bin/verify-exercises

オプション2:テストランナーのDockerイメージを使う

2つ目の方法は、トラックのテストランナーを実行して演習を検証するものです。 この方法には2つの条件があります。

  1. トラックに動作するテストランナーがあること
  2. verify-exercisesスクリプトがテストランナーのDockerイメージを使って演習のテストを実行すること

トラックにまだテストランナーがない場合は、次のどちらかを行います。

  • 動作するテストランナーをビルドする
  • オプション1を使い、言語のツールを直接使う

この方法にはいくつかの利点があります。

  1. テストワークフロー内で依存関係やツールをインストールする必要がありません(それらはDockerイメージ内にインストール済みだからです)
  2. 本番環境(ウェブサイト上)でテストがどのように実行されるかを最もよく模倣でき、本番環境での問題が起こりにくくなります。

主な欠点は、DockerイメージのプルとDockerのオーバーヘッドのため、遅くなる可能性が高いことです。

テストランナーのDockerイメージをプルする方法はいくつかあります。

  1. verify-exercisesファイル内でイメージをダウンロードする。 これはUnisonトラックが採用している方法です。
  2. ワークフロー内でイメージをダウンロードする。 これはStandard MLトラックが採用している方法です。
  3. ワークフロー内でイメージをビルドする。 これは8thトラックが採用している方法です。

では、どの方法を使えばよいのでしょうか? _少なくとも_1番の方法を実装して、verify-exercisesスクリプトを_単体で動く_ようにすることをおすすめします。 イメージが特に大きい場合は、3番の方法も実装するとよいかもしれません。ビルドしたDockerイメージがGitHub Actionsのキャッシュに保存され、以降の実行ではイメージをダウンロードする代わりにキャッシュから読み込めるので、パフォーマンスが良くなる可能性があります(必ず計測して確認してください)。

オプション3:テストランナーのDockerイメージ内で演習を検証するスクリプトを実行する

3つ目の代替案は、前の2つを組み合わせたハイブリッドです。 ここでもテストランナーのDockerイメージを使いますが、今回はverify-exercisesスクリプトを_そのDockerイメージの中で_実行します。 この方法を有効にするには、ワークフローのコンテナをテストランナーに設定します。

container:
  image: exercism/vimscript-test-runner

そうすれば、依存関係とツールのインストール手順(それらはテストランナーのDockerイメージ内にインストール済みだからです)を省略でき、そのままbin/verify-exercisesスクリプトの実行に進めます。

例

vimscriptトラックのtest.ymlワークフローはこの方法を使っています。

name: Verify Exercises

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  ci:
    runs-on: ubuntu-24.04
    container:
      image: exercism/vimscript-test-runner

    steps:
      - name: Checkout repository
        uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332

      - name: Verify all exercises
        run: bin/verify-exercises