CircleCI上でgo言語のユニットテストを差分実行する方法
背景
リモートブランチにコミット・プッシュする度に、CircleCI を用いて go 言語で書かれたユニットテストを実行していたのですが、
全てのユニットテストを実行するのに約 20 分要していました。
ゴール
変更箇所にのみテストを実行することで、テスト実行時間を短縮する。
やってみた結果
変更ファイル数にも依存しますが、1度のPRで生じるファイル変更数くらいであれば、
テスト実行に要する時間を約 20 分 → 約 4 分に短縮することができました。
差分実行にする前のテスト実行方法
- run:
name: Run tests & make report
command: |
echo "" > coverage.txt
for package_path in $(go list ./... ); do
go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done
上記のコマンドによって、カレントディレクトリ以下の全てのパッケージをテスト実行し、カバレッジを取得することができます。
以下に、上記コマンドの重要な部分を抜粋します。
-
go list ./...
:カレントディレクトリ以下の全てのパッケージパスを取得する -
go test "$package_path"
:取得したパッケージパスをテスト実行する。ファイル名やパッケージ名を指定することもできる。
go test
に以下のオプションを指定しています。
-
-v
:テスト実行時に詳細なログを出力する -
-race
:データ競合を検知する -
-coverprofile=profile.out
:カバレッジを取得する -
-covermode=atomic
:カバレッジを取得する際に、複数のテスト実行結果を統合する
差分実行にする方法
ブランチ名は環境変数$CIRCLE_BRANCH で取得
masterブランチなどのルートブランチでは保守性と品質確保の観点から、全てのユニットテストを実行したいです。
一方で、それ以外のフィーチャーブランチやトピックブランチでは、開発の高速化のために、変更箇所にのみテストを実行したいです。
CircleCI では、環境変数$CIRCLE_BRANCHで現在のブランチ名を取得することができます。
従って、$CIRCLE_BRANCH を用いて、master ブランチかどうかを判定します。
if [ "$CIRCLE_BRANCH" = "master" ]; then
echo "Branch is master"
else
echo "Branch is not master"
fi
git diffでmasterブランチとの差分を取得
git diff ブランチ名
で、現在のブランチと指定したブランチの差分ファイルを取得することもできます。
また、
git diff HEAD^
とすることで、直前のコミットとの差分ファイルを取得することができます。
--diff-fileter オプションで取得するファイルを絞り込む
gitの公式ドキュメントによると、
--diff-filter=[(A|C|D|M|R|T|U|X|B)…[*]]
Select only files that are Added (A), Copied (C), Deleted (D), Modified (M), Renamed (R), have their type (i.e. regular file, symlink, submodule, …) changed (T), are Unmerged (U), are Unknown (X), or have had their pairing Broken (B). Any combination of the filter characters (including none) can be used. When * (All-or-none) is added to the combination, all paths are selected if there is any file that matches other criteria in the comparison; if there is no file that matches other criteria, nothing is selected.
Also, these upper-case letters can be downcased to exclude. E.g. --diff-filter=ad excludes added and deleted paths.
Note that not all diffs can feature all types. For instance, copied and renamed entries cannot appear if detection for those types is disabled.
要は、--diff-filter=
で指定したフラグに該当する変更ファイルのみを取得することができます。
--diff-filter=ACMR
- A: ファイルが新たに追加された
- C: ファイルがコピーされた
- M: ファイルが変更された
- R: ファイルが名前を変更した
削除されたファイルを go test の対象とすると、テスト実行時にエラーが発生するため、D は含めませんでした。
ファイルパスをパッケージパスに変換する
git diff
で取得したファイルパスを、go test で指定するパッケージパスに変換する必要があります。
例えば、src/controller/hoge/fuga.go
というファイルが変更された場合、そのファイルパスをcontroller/hoge
というパッケージパスに変換します。
package_path=$(echo "$file_name" | sed 's#^src/##' | sed 's#/[^/]*$##')
差分実行コマンドまとめ
以上のことを踏まえた、最終的な差分実行コマンドは以下のようになります。
run:
name: Run tests & make report
command: |
echo "" > coverage.txt
if [ "$CIRCLE_BRANCH" = "master" ]; then
echo "Branch is master"
for package_path in $(go list ./... ); do
go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done
else
echo "Branch is not master"
visited_packages=()
for file_name in $(git diff origin/master...HEAD --name-only --diff-filter=ACMR | grep "\.go$"); do
package_path=$(echo "$file_name" | sed 's#^src/##' | sed 's#/[^/]*$##')
if [[ " ${visited_packages[@]} " =~ " ${package_path} " ]]; then
continue
fi
visited_packages+=("$package_path")
go test -v -race -coverprofile=profile.out -covermode=atomic "$package_path"
if [ -f profile.out ]; then
cat profile.out >> coverage.txt
rm profile.out
fi
done
fi
echo "" > coverage.txt
参考文献
CircleCIの公式ドキュメント
CircleCIではなく、GitHub Actions を用いて差分実行をしたい場合は、こちらの記事が参考になります。
Discussion