その他の CI システム
TestDetta は、.NET と git を実行できるあらゆる CI で動作します。GitHub Actions 以外では、2 つのコマンドと 1 つのフォルダーで対応できます。
- 既定のブランチでは、
td record --maps-dir <maps>がテストスイート全体をカバレッジ付きで 1 回実行し、テストプロジェクトごとに 1 つのカバレッジマップを<maps>/<commit>/に保存します。 - プルリクエストまたはマージリクエストでは、
td test --base origin/<target> --maps-dir <maps>がマージベースかそれ以前の最新のマップを復元します。50 個の first-parent コミット(--max-map-age)の範囲内で最も近いマップを使い、その間の変更を橋渡しします。そのうえで、影響を受けるテストだけを実行します。
<maps> は通常のフォルダーです。CI がパイプライン間でファイルを保持できる場所であれば、どこに置いてもかまいません。
- エージェント間で共有されるディスク(オンプレミスで一般的):
/srv/testdetta-maps/<repository> - オブジェクトストレージ(S3、Azure Blob、GCS、MinIO)。実行の前後に同期します。
TestDetta 自体がマップのためにネットワークを使用することはありません。
すべての環境で必要なもの
- 完全な git 履歴:TestDetta はマージベースと比較し、履歴をたどってマップを探します。シャロークローンではどちらも行えません。
- ターゲットブランチのフェッチ:
origin/<target>が存在している必要があります。明示的な refspec+refs/heads/<target>:refs/remotes/origin/<target>でフェッチしてください。CI のクローンでは、リモートの refspec がパイプライン自身のブランチに限定されていることがよくあります(GitLab や Bitbucket がそうです)。その場合、git fetch origin <target>だけではFETCH_HEADしか更新されず、origin/<target>は存在しないままになります。 - ライセンス:
TESTDETTA_LICENSE(マスクされた変数またはシークレット変数)か、TESTDETTA_LICENSE_FILEで指定したファイルに置きます。GitLab CI と GitHub Actions では、パブリックリポジトリにライセンスは不要です。Jenkins、Azure DevOps、Bitbucket はリポジトリがパブリックかどうかを示さないため、これらではすべてのリポジトリにライセンスが必要です。有効なライセンスがない場合はすべてのテストを実行し、それが原因でビルドが失敗することはありません。ライセンスを参照してください。 - 終了コード:
0は成功、1は分析の失敗、2は無効なコマンドライン、3はテストの失敗です。 - クリーンアップ:古いコミットのマップが必要なのは 50 コミット分だけです。それより古いフォルダーは、スケジュールされたジョブかストレージのライフサイクルルールで削除してください。
インストール
dotnet tool install --global <package>
パッケージ名はリリース時に決定します。それまでは dotnet build src/TestDetta.Cli -c Release -o testdetta-bin でソースから TestDetta をビルドし、以下で td と書かれている箇所では代わりに dotnet testdetta-bin/TestDetta.Cli.dll を呼び出してください。
GitLab CI
variables:
GIT_DEPTH: "0" # full history
TESTDETTA_MAPS: /srv/testdetta-maps/$CI_PROJECT_PATH # a disk the runners share; or sync with object storage
record-coverage:
stage: test
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- td record --maps-dir "$TESTDETTA_MAPS"
affected-tests:
stage: test
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- git fetch --no-tags origin "+refs/heads/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME:refs/remotes/origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- td test --base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" --maps-dir "$TESTDETTA_MAPS" --summary-file testdetta-summary.md
artifacts:
when: always
paths: [testdetta-summary.md]
TESTDETTA_LICENSE は Settings → CI/CD → Variables で設定し、必要に応じてマスクと保護を有効にしてください。TestDetta は CI_PROJECT_VISIBILITY によってパブリックプロジェクトを判定します。
Jenkins
宣言型のマルチブランチパイプラインの例です。
pipeline {
agent any
environment {
TESTDETTA_LICENSE = credentials('testdetta-license') // a "secret text" credential
TESTDETTA_MAPS = "/srv/testdetta-maps/${env.JOB_NAME.split('/')[0]}"
}
options { skipDefaultCheckout() }
stages {
stage('Checkout') {
steps {
checkout scmGit(branches: scm.branches, userRemoteConfigs: scm.userRemoteConfigs,
extensions: [cloneOption(shallow: false, depth: 0, noTags: true, reference: '')])
}
}
stage('Record coverage') {
when { branch 'main' }
steps { sh 'td record --maps-dir "$TESTDETTA_MAPS"' }
}
stage('Affected tests') {
when { changeRequest() }
steps {
sh 'git fetch --no-tags origin "+refs/heads/$CHANGE_TARGET:refs/remotes/origin/$CHANGE_TARGET"'
sh 'td test --base "origin/$CHANGE_TARGET" --maps-dir "$TESTDETTA_MAPS"'
}
}
}
}
Jenkins は、リモート URL に資格情報を含めてクローンすることがよくあります。TestDetta はライセンスのスコープと照合する前にそれを取り除き、ログに出力することはありません。リモートがライセンスの対象リポジトリを示していない場合(ローカルのミラーなど)は、git.acme.corp/scm/pay/app のように TESTDETTA_LICENSE_SCOPE を設定してください。
Azure DevOps Pipelines
trigger: [main]
pr: [main]
steps:
- checkout: self
fetchDepth: 0
- script: td record --maps-dir "$(TESTDETTA_MAPS)"
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
env:
TESTDETTA_LICENSE: $(TestDettaLicense) # a secret pipeline variable
- script: |
target="${SYSTEM_PULLREQUEST_TARGETBRANCH#refs/heads/}"
git fetch --no-tags origin "+refs/heads/$target:refs/remotes/origin/$target"
td test --base "origin/$target" --maps-dir "$(TESTDETTA_MAPS)" --summary-file "$(Agent.TempDirectory)/testdetta.md"
echo "##vso[task.uploadsummary]$(Agent.TempDirectory)/testdetta.md"
condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'))
env:
TESTDETTA_LICENSE: $(TestDettaLicense)
セルフホステッドエージェントでは、TESTDETTA_MAPS は共有フォルダーを指します。Microsoft ホステッドエージェントでは、Azure Blob Storage と同期してください(オブジェクトストレージを参照)。
Bitbucket Pipelines
clone:
depth: full
pipelines:
branches:
main:
- step:
script:
- td record --maps-dir maps
- ./sync-maps.sh upload maps # see "Object storage"
pull-requests:
'**':
- step:
script:
- git fetch --no-tags origin "+refs/heads/$BITBUCKET_PR_DESTINATION_BRANCH:refs/remotes/origin/$BITBUCKET_PR_DESTINATION_BRANCH"
- ./sync-maps.sh download maps "origin/$BITBUCKET_PR_DESTINATION_BRANCH"
- td test --base "origin/$BITBUCKET_PR_DESTINATION_BRANCH" --maps-dir maps
TESTDETTA_LICENSE はセキュリティで保護されたリポジトリ変数として設定してください。
共有ディスクの代わりにオブジェクトストレージを使う
ホステッドランナーには共有ディスクがありません。記録の後に、新しいフォルダーをアップロードします。テストの前には、使用できるフォルダー、つまりマージベースとその前の最大 50 個の first-parent コミットのフォルダーだけをダウンロードします。新しいものから順にダウンロードを試み、最初に存在したものの時点で止めます。以下は S3 の例です。cp と ls を備えたツールであれば、同じ方法で使えます。
#!/usr/bin/env bash
# sync-maps.sh upload <maps> | after td record on the default branch
# sync-maps.sh download <maps> <base> | before td test on a pull request
set -euo pipefail
remote="s3://my-bucket/testdetta-maps/my-repository"
case "$1" in
upload)
aws s3 cp --recursive "$2/$(git rev-parse HEAD)" "$remote/$(git rev-parse HEAD)" ;;
download)
for sha in $(git rev-list --first-parent -n 51 "$(git merge-base "$3" HEAD)"); do
if aws s3 ls "$remote/$sha/" > /dev/null 2>&1; then
aws s3 cp --recursive "$remote/$sha" "$2/$sha"
break
fi
done ;;
esac
マップがない場合
セットアップ直後の最初のプルリクエストや、大きく遅れているプルリクエストでは、到達できる範囲にマップが見つかりません。その場合 TestDetta は名前だけで選択します。安全ですが選択されるテストは多くなり、その旨をログに出力します。既定のブランチで td record が実行されると、選択は精密になります。