[インデックス 17190] ファイルの概要
このコミットは、Go言語のテストインフラストラクチャにおけるビルドタグの処理方法を改善し、go/build
パッケージ(Goの公式ビルドシステム)の動作と一貫性を持たせることを目的としています。具体的には、test/run.go
内のshouldTest
関数と、test/testlib
スクリプト内の+build
関数が変更され、ビルドタグの評価ロジックがより洗練され、Goのビルドシステムがサポートする複雑なタグの組み合わせ(AND条件、OR条件、否定)を正しく解釈できるようになりました。これにより、テストの実行がより正確になり、特定の環境や条件に合わせたテストの包含・除外が意図通りに行われるようになります。
コミット
commit a5385580035abaf4ce6aeb2835e70371f4fde77a
Author: Anthony Martin <ality@pbrane.org>
Date: Tue Aug 13 12:25:41 2013 -0400
test/run: process build tags like go/build
R=bradfitz, dave, rsc, r
CC=golang-dev
https://golang.org/cl/10001045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/a5385580035abaf4ce6aeb2835e70371f4fde77a
元コミット内容
test/run: process build tags like go/build
変更の背景
Go言語のビルドシステムは、ソースファイルの先頭に記述されるビルドタグ(// +build tag
形式)を使用して、特定の環境(OS、アーキテクチャなど)や条件に基づいてファイルのコンパイルを制御します。例えば、// +build linux,amd64
はLinuxかつAMD64アーキテクチャの場合にのみコンパイルされ、// +build !windows
はWindows以外の環境でコンパイルされます。また、スペースで区切られたタグはOR条件(例: // +build linux darwin
はLinuxまたはmacOSの場合にコンパイル)として扱われ、カンマで区切られたタグはAND条件(例: // +build linux,amd64
はLinuxかつAMD64の場合にコンパイル)として扱われます。
このコミット以前のGoのテストインフラストラクチャ(特にtest/run.go
とtest/testlib
)は、これらのビルドタグの複雑なルールを完全にサポートしていませんでした。そのため、go build
コマンドでは正しく処理されるビルドタグが、テスト実行時には意図しない結果(テストが実行されない、または不適切に実行される)を引き起こす可能性がありました。この不整合は、開発者が特定の環境向けに書かれたテストを正確に実行・除外する上で問題となります。
このコミットの背景には、テストの信頼性と正確性を向上させ、Goのビルドシステムとテストシステムの間でビルドタグの解釈に一貫性を持たせるという明確な目的がありました。これにより、開発者はビルドタグをより効果的に利用して、クロスプラットフォームなテストや特定の条件に依存するテストを管理できるようになります。
前提知識の解説
Goのビルドタグ (Build Tags)
Go言語では、ソースファイルの先頭に特別なコメント行を記述することで、そのファイルが特定のビルド条件を満たす場合にのみコンパイルされるように制御できます。これを「ビルドタグ」と呼びます。
- 形式:
// +build tag1 tag2,tag3 !tag4
- 基本的なルール:
- スペース区切り: スペースで区切られたタグはOR条件として扱われます。例:
// +build linux darwin
は、GOOS
がlinux
またはdarwin
のいずれかであればファイルがビルドされます。 - カンマ区切り: カンマで区切られたタグはAND条件として扱われます。例:
// +build linux,amd64
は、GOOS
がlinux
かつGOARCH
がamd64
の場合にのみファイルがビルドされます。 - 否定: タグの前に
!
を付けると否定を意味します。例:// +build !windows
は、GOOS
がwindows
ではない場合にファイルがビルドされます。 - 複数行: 複数の
+build
行がある場合、それらはAND条件として扱われます。例:
これは、// +build linux // +build amd64
GOOS
がlinux
かつGOARCH
がamd64
の場合にのみビルドされます。
- スペース区切り: スペースで区切られたタグはOR条件として扱われます。例:
GOOS
とGOARCH
Goのビルド環境を決定する重要な環境変数です。
GOOS
: ターゲットとするオペレーティングシステム(例:linux
,windows
,darwin
,android
など)。GOARCH
: ターゲットとするアーキテクチャ(例:amd64
,arm
,386
など)。
ビルドタグは、これらの環境変数の値と照合され、ファイルのコンパイルの可否が決定されます。
test/run.go
とtest/testlib
test/run.go
: Go言語で書かれたテストランナーの一部で、Goのテストスイートを実行する際に、どのテストファイルが現在の環境で実行可能かを判断するロジックを含んでいます。test/testlib
: Goのテストシステムで使用されるシェルスクリプトのライブラリで、テストのセットアップや実行に関連するユーティリティ関数を提供します。ここにもビルドタグの処理ロジックが含まれていました。
このコミットは、これらのテスト関連ツールが、go/build
パッケージが実装しているビルドタグの完全なセマンティクスを理解し、適用するように修正するものです。
技術的詳細
このコミットの主要な技術的変更点は、ビルドタグの評価ロジックを、go/build
パッケージのそれと完全に一致させることです。これは、Goのソースファイルに記述されたビルドタグが、テスト実行時にもビルド時と同じように正確に解釈されることを保証します。
test/run.go
の変更点
-
context
構造体の導入:type context struct { GOOS string GOARCH string }
この構造体は、現在のOS (
GOOS
) とアーキテクチャ (GOARCH
) の情報をカプセル化します。これにより、ビルドタグの評価に必要な環境情報が明確に渡されるようになります。 -
match
メソッドの追加:context
構造体にmatch(name string) bool
メソッドが追加されました。このメソッドは、単一のビルドタグ(例:linux
,amd64
,!windows
,tag1,tag2
)が現在のcontext
(GOOS
,GOARCH
)に合致するかどうかを再帰的に評価します。- カンマ区切りタグの処理:
strings.Index(name, ",")
でカンマを検出し、再帰的にAND条件として評価します。return ctxt.match(name[:i]) && ctxt.match(name[i+1:])
- 否定タグの処理:
strings.HasPrefix(name, "!")
で否定を検出し、再帰的に元のタグの評価結果を反転させます。return len(name) > 1 && !ctxt.match(name[1:])
- 有効なタグ文字のチェック:
unicode.IsLetter
,unicode.IsDigit
,_
,.
を使用して、タグ名が有効な文字(Goの識別子とは異なり、数字のみのタグも許可)で構成されているかを確認します。 - GOOS/GOARCHとの照合: 最終的に、タグ名が
ctxt.GOOS
またはctxt.GOARCH
と一致するかどうかをチェックします。
- カンマ区切りタグの処理:
-
shouldTest
関数の修正:shouldTest
関数は、ソースファイル内の+build
行を解析し、そのファイルが現在の環境でテストされるべきかを判断します。- 以前は
notgoos
,notgoarch
といった単純な否定文字列で処理していましたが、新しいmatch
メソッドを使用するように変更されました。 +build
行内の各ワード(スペース区切り)に対してctxt.match(word)
を呼び出し、いずれか一つでもマッチすればtrue
を返します(OR条件)。+build
行が複数ある場合(異なる行に+build
が記述されている場合)は、それらがAND条件として扱われるというGoのビルドタグのセマンティクスは、shouldTest
関数が各+build
行を独立して評価し、いずれかの行がfalse
を返せば全体としてfalse
となることで実現されています。
- 以前は
test/testlib
の変更点
-
_match
関数の追加: シェルスクリプトであるtest/testlib
にも、Goのmatch
メソッドと同様のロジックを実装した_match
関数が追加されました。case $1 in *,*)
: カンマ区切りのタグをAND条件として再帰的に処理します。case $1 in '!'*)
: 否定タグを処理します。case $1 in $GOARCH|$GOOS)
:GOARCH
またはGOOS
との一致をチェックします。
-
+build
関数の修正:testlib
内の+build
関数は、引数として渡されたビルドタグを評価し、現在の環境でテストが実行されるべきでない場合にexit 0
(テストをスキップ)します。- 新しい
_match
関数を使用して、各タグが現在の環境にマッチするかどうかを判断します。 for tag; do ... done
ループ内で、いずれかのタグが_match
によってtrue
と評価されれば、m=1
を設定します。- ループ終了後、
m
が0
のまま(つまり、どのタグもマッチしなかった)であれば、テストをスキップするためにexit 0
を実行します。これは、+build
行に記述されたタグがOR条件で評価され、どれも現在の環境に合致しない場合にテストをスキップするというGoのビルドタグのセマンティクスを反映しています。
- 新しい
これらの変更により、Goのテストシステムは、go/build
パッケージが提供するビルドタグの強力なフィルタリング機能を完全に活用できるようになり、テストの実行がより正確かつ予測可能になりました。
コアとなるコードの変更箇所
test/run.go
--- a/test/run.go
+++ b/test/run.go
@@ -27,6 +27,7 @@ import (
"sort"
"strconv"
"strings"
+ "unicode"
)
var (
@@ -299,14 +300,17 @@ func goDirPackages(longdir string) ([][]string, error) {
return pkgs, nil
}
+type context struct {
+ GOOS string
+ GOARCH string
+}
+
// shouldTest looks for build tags in a source file and returns
// whether the file should be used according to the tags.
func shouldTest(src string, goos, goarch string) (ok bool, whyNot string) {
if idx := strings.Index(src, "\npackage"); idx >= 0 {
src = src[:idx]
}
- notgoos := "!" + goos
- notgoarch := "!" + goarch
for _, line := range strings.Split(src, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "//") {
@@ -318,29 +322,59 @@ func shouldTest(src string, goos, goarch string) (ok bool, whyNot string) {
if len(line) == 0 || line[0] != '+' {
continue
}
+ ctxt := &context{
+ GOOS: goos,
+ GOARCH: goarch,
+ }
words := strings.Fields(line)
if words[0] == "+build" {
- for _, word := range words {
- switch word {
- case goos, goarch:
- return true, ""
- case notgoos, notgoarch:
- continue
- default:
- if word[0] == '!' {
- // NOT something-else
- return true, ""
- }
+ ok := false
+ for _, word := range words[1:] {
+ if ctxt.match(word) {
+ ok = true
+ break
}
}
- // no matching tag found.
- return false, line
+ if !ok {
+ // no matching tag found.
+ return false, line
+ }
}
}
- // no build tags.
+ // no build tags
return true, ""
}
+func (ctxt *context) match(name string) bool {
+ if name == "" {
+ return false
+ }
+ if i := strings.Index(name, ","); i >= 0 {
+ // comma-separated list
+ return ctxt.match(name[:i]) && ctxt.match(name[i+1:])
+ }
+ if strings.HasPrefix(name, "!!") { // bad syntax, reject always
+ return false
+ }
+ if strings.HasPrefix(name, "!") { // negation
+ return len(name) > 1 && !ctxt.match(name[1:])
+ }
+
+ // Tags must be letters, digits, underscores or dots.
+ // Unlike in Go identifiers, all digits are fine (e.g., "386").
+ for _, c := range name {
+ if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
+ return false
+ }
+ }
+
+ if name == ctxt.GOOS || name == ctxt.GOARCH {
+ return true
+ }
+
+ return false
+}
+
func init() { checkShouldTest() }
// run runs a test.
@@ -815,7 +849,7 @@ func defaultRunOutputLimit() int {
return cpu
}
-// checkShouldTest runs canity checks on the shouldTest function.
+// checkShouldTest runs sanity checks on the shouldTest function.
func checkShouldTest() {
assert := func(ok bool, _ string) {
\tif !ok {
@@ -823,11 +857,28 @@ func checkShouldTest() {
\t}
}
assertNot := func(ok bool, _ string) { assert(!ok, "") }
+\n+\t// Simple tests.\n assert(shouldTest("// +build linux", "linux", "arm"))
assert(shouldTest("// +build !windows", "linux", "arm"))
assertNot(shouldTest("// +build !windows", "windows", "amd64"))
-\tassertNot(shouldTest("// +build arm 386", "linux", "amd64"))
+\n+\t// A file with no build tags will always be tested.\n assert(shouldTest("// This is a test.", "os", "arch"))
+\n+\t// Build tags separated by a space are OR-ed together.\n+\tassertNot(shouldTest("// +build arm 386", "linux", "amd64"))
+\n+\t// Build tags seperated by a comma are AND-ed together.\n+\tassertNot(shouldTest("// +build !windows,!plan9", "windows", "amd64"))
+\tassertNot(shouldTest("// +build !windows,!plan9", "plan9", "386"))
+\n+\t// Build tags on multiple lines are AND-ed together.\n+\tassert(shouldTest("// +build !windows\\n// +build amd64", "linux", "amd64"))
+\tassertNot(shouldTest("// +build !windows\\n// +build amd64", "windows", "amd64"))
+\n+\t// Test that (!a OR !b) matches anything.\n+\tassert(shouldTest("// +build !windows !plan9", "windows", "amd64"))
}
// envForDir returns a copy of the environment
test/testlib
--- a/test/testlib
+++ b/test/testlib
@@ -16,29 +16,50 @@ pkgs() {
done | sort
}
+_match() {
+ case $1 in
+ *,*)
+ #echo >&2 "match comma separated $1"
+ first=$(echo $1 | sed 's/,.*//')
+ rest=$(echo $1 | sed 's/[^,]*,//')
+ if _match $first && _match $rest; then
+ return 0
+ fi
+ return 1
+ ;;
+ '!'*)
+ #echo >&2 "match negation $1"
+ neg=$(echo $1 | sed 's/^!//')
+ if _match $neg; then
+ return 1
+ fi
+ return 0
+ ;;
+ $GOARCH|$GOOS)
+ #echo >&2 "match GOARCH or GOOS $1"
+ return 0
+ ;;
+ esac
+ return 1
+}
+
# +build aborts execution if the supplied tags don't match,
# i.e. none of the tags (x or !x) matches GOARCH or GOOS.
+build() {
if (( $# == 0 )); then
return
fi
+\tm=0
for tag; do
-\t\tcase $tag in
-\t\t$GOARCH|$GOOS)
-\t\t\t#echo >&2 "match $tag in $1"\n-\t\t\treturn # don't exclude.\n-\t\t\t;;\n-\t\t'!'$GOARCH|'!'$GOOS)\n-\t\t\t;;\n-\t\t'!'*)\n-\t\t\t# not x where x is neither GOOS nor GOARCH.\n-\t\t\t#echo >&2 "match $tag in $1"\n-\t\t\treturn # don't exclude\n-\t\t\t;;\n-\t\tesac
+\t\tif _match $tag; then
+\t\t\tm=1
+\t\tfi
done
-\t# no match.
-\texit 0
+\tif [ $m = 0 ]; then
+\t\t#echo >&2 no match
+\t\texit 0
+\tfi
+\tunset m
}
compile() {
コアとなるコードの解説
test/run.go
context
構造体とmatch
メソッド: この変更の核心は、context
構造体とそれに付随するmatch
メソッドの導入です。match
メソッドは、Goのビルドタグの評価ロジックをカプセル化し、再利用可能な形で提供します。match
メソッドは、カンマ区切りのタグ(AND条件)、否定タグ(!
プレフィックス)、および単一のタグ(GOOS
またはGOARCH
との一致)を再帰的に処理します。これにより、go/build
パッケージがサポートする複雑なビルドタグの組み合わせを正確に評価できるようになりました。- タグ名が有効な文字(英字、数字、アンダースコア、ドット)で構成されているかのチェックも行われます。これは、不正なタグ形式を早期に検出するためです。
shouldTest
関数の改善:shouldTest
関数は、ソースファイルから+build
行を抽出し、その行に含まれる各タグ(スペース区切りでOR条件)を新しいmatch
メソッドを使って評価します。- 以前の
shouldTest
は、goos
,goarch
,!goos
,!goarch
といった特定の文字列にのみ対応していましたが、match
メソッドの導入により、より汎用的なタグ評価が可能になりました。 for _, word := range words[1:]
ループで、+build
に続く各タグをmatch
メソッドで評価し、一つでもマッチすればそのファイルはテスト対象となります。これは、スペース区切りタグがOR条件として機能するというGoのセマンティクスを正確に反映しています。
- 以前の
- テストケースの追加:
checkShouldTest
関数に、カンマ区切りタグ(AND条件)、複数行の+build
タグ(AND条件)、および複雑なOR条件を含む新しいテストケースが追加されました。これにより、新しいビルドタグ評価ロジックが正しく機能することが検証されます。
test/testlib
_match
関数の導入: シェルスクリプトであるtest/testlib
にも、Goのmatch
メソッドと同様のロジックを実装した_match
関数が追加されました。これは、シェルスクリプト環境でビルドタグを正確に評価するために必要です。case
文とsed
コマンドを組み合わせて、カンマ区切りタグのAND条件、否定タグ、およびGOARCH
/GOOS
との一致を処理します。Goのmatch
メソッドと同様に、再帰的な呼び出しによって複雑な条件を評価します。
+build
関数の改善:testlib
内の+build
関数は、_match
関数を使用して、引数として渡されたビルドタグを評価します。for tag; do ... done
ループで各タグを_match
で評価し、一つでもマッチすればm=1
を設定します。- ループ終了後、
m
が0
のまま(どのタグもマッチしなかった)であれば、exit 0
を実行してテストをスキップします。これは、+build
行に記述されたタグがOR条件で評価され、どれも現在の環境に合致しない場合にテストをスキップするというGoのビルドタグのセマンティクスを反映しています。
これらの変更により、Goのテストシステム全体でビルドタグの解釈が一貫し、開発者はより信頼性の高いテスト環境を利用できるようになりました。
関連リンク
- Go言語のビルドタグに関する公式ドキュメント(現在のバージョン): https://pkg.go.dev/cmd/go#hdr-Build_constraints
- Goの
go/build
パッケージに関するドキュメント(現在のバージョン): https://pkg.go.dev/go/build
参考にした情報源リンク
- Go言語の公式リポジトリ: https://github.com/golang/go
- Gerrit Change-ID:
https://golang.org/cl/10001045
(このコミットの元のGerritレビューページ) - Goのビルドタグに関する一般的な情報源(Stack Overflow, Goブログなど)
- Goのビルドタグの動作に関する理解を深めるために、一般的なGoのドキュメントやコミュニティの議論を参照しました。特に、カンマ区切りがAND、スペース区切りがOR、複数行がANDというルールは、Goのビルドシステムにおける基本的なセマンティクスです。