지속적 통합 설정하기


트랙의 지속적 통합(CI)을 설정하는 것은 실수를 잡아내는 데 도움이 되므로 매우 중요해요.

GitHub Actions

Exercism 저장소(트랙 저장소 포함)는 CI를 실행하기 위해 GitHub Actions를 사용해요. 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을 반환해야 하고, 그렇지 않으면 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나 바이너리) 언어 도구 설치가 설치된 바이너리를 PATH에 추가하지 않는다면, 반드시 GitHub Actions의 시스템 PATH에 추가해요.

이걸 마치면 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 캐시에 저장돼요. 그러면 이후 실행에서는 이미지를 다운로드하는 대신 캐시에서 바로 읽을 수 있어서 성능에 더 좋을 수 있어요(확실히 하려면 직접 측정해 봐요).

방법 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