设置持续集成


为你的学习轨道设置持续集成(CI)非常重要,因为它有助于发现错误。

GitHub Actions

Exercism 的代码仓库(包括学习轨道仓库)使用 GitHub Actions 来运行 CI。 GitHub Actions 基于_工作流_,工作流定义了在特定事件发生时(例如推送提交)自动运行的脚本。 想进一步了解 GitHub Actions 工作流,请查看工作流文档。

预装的工作流

学习轨道预装了许多工作流,其中大多数你都_不应该_修改(它们被称为_共享工作流_)。 不过有一个工作流你_应该_修改,那就是 test.yml工作流。

测试工作流

test.yml工作流的目标是验证学习轨道中的练习是否状态正常。 当向main分支或拉取请求的分支推送时,该工作流会自动运行(用 GitHub Actions 的术语说,就是被_触发_)。

工作流本身不应做太多事情,只需要:

  • 检出代码(已实现)
  • 安装依赖(例如安装软件包,可选)
  • 安装工具(例如安装 SDK,可选)
  • 运行验证练习脚本(已实现)

实现验证练习脚本

如前所述,练习是通过一个脚本来验证的,即 bin/verify-exercises(bash)脚本。 这个脚本_差不多_完成了,它会做以下事情:

  • 遍历所有练习目录
  • 对于每个练习目录,它会:
    • 把示例/范例解答复制到(存根)解答文件(已实现)
    • 调用 unskip_tests函数,你可以在其中取消跳过测试文件中的测试(可选)
    • 调用 run_tests函数,你需要在其中运行测试(必需)

run_tests和 unskip_tests函数是你唯一需要实现的东西。

取消跳过测试

如果你的学习轨道支持跳过测试,我们必须确保在验证练习的示例/范例解答时没有测试被跳过。 一般来说,学习轨道支持“取消跳过”测试的方式有两种:

  1. 从测试文件中移除注解/代码/文本。 例如,把 test.skip改为 test。
  2. 提供环境变量。 例如,设置 SKIP_TESTS=false。

从测试文件中移除注解/代码/文本

如果跳过测试是基于文件的(上面提到的第一种方式),就编辑 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函数负责运行练习的测试。 调用该函数时,示例/范例文件已经复制到(存根)解答文件,所以你只需要调用正确的命令来运行测试。

如果所有测试都通过,函数必须返回退出码 0,否则返回非零的退出码。

Note

run_tests函数运行在练习目录的副本上,所以你可以随意修改文件。

方案 1:使用语言工具

验证练习脚本的默认方案是使用语言本身的工具(SDK/二进制文件等),大多数学习轨道都采用这种方式。 每个学习轨道运行测试的方式各不相同,但通常只是一条命令。

示例

Arturo 学习轨道的 bin/verify-exercises file 修改 run_tests函数,让它直接在测试文件上调用 arturo 命令:

run_tests() {
    arturo tester.art
}

方案 2:使用测试运行器 Docker 镜像

第二种方案是通过运行学习轨道的测试运行器来验证练习。 这当然取决于该学习轨道是否有一个可用的测试运行器。

如果你的学习轨道还没有测试运行器,你可以:

  • 构建一个可用的测试运行器,或者
  • 使用方案 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 镜像

第二种方案是通过运行学习轨道的测试运行器来验证练习。 这个方案需要满足两个条件:

  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 缓存。 之后的运行就可以直接从缓存读取 Docker 镜像,而不必下载,这对性能可能更好(请自行测量确认)。

方案 3:在测试运行器 Docker 镜像中运行验证练习脚本

第三种备选方案是前两种方案的混合。 这里我们同样使用测试运行器 Docker 镜像,只不过这次是在_该 Docker 镜像内部_运行 verify-exercises脚本。 要启用这个方案,我们需要把工作流的容器设置为测试运行器:

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