Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 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.gotest/testlib)は、これらのビルドタグの複雑なルールを完全にサポートしていませんでした。そのため、go buildコマンドでは正しく処理されるビルドタグが、テスト実行時には意図しない結果(テストが実行されない、または不適切に実行される)を引き起こす可能性がありました。この不整合は、開発者が特定の環境向けに書かれたテストを正確に実行・除外する上で問題となります。

このコミットの背景には、テストの信頼性と正確性を向上させ、Goのビルドシステムとテストシステムの間でビルドタグの解釈に一貫性を持たせるという明確な目的がありました。これにより、開発者はビルドタグをより効果的に利用して、クロスプラットフォームなテストや特定の条件に依存するテストを管理できるようになります。

前提知識の解説

Goのビルドタグ (Build Tags)

Go言語では、ソースファイルの先頭に特別なコメント行を記述することで、そのファイルが特定のビルド条件を満たす場合にのみコンパイルされるように制御できます。これを「ビルドタグ」と呼びます。

  • 形式: // +build tag1 tag2,tag3 !tag4
  • 基本的なルール:
    • スペース区切り: スペースで区切られたタグはOR条件として扱われます。例: // +build linux darwin は、GOOSlinuxまたはdarwinのいずれかであればファイルがビルドされます。
    • カンマ区切り: カンマで区切られたタグはAND条件として扱われます。例: // +build linux,amd64 は、GOOSlinuxかつGOARCHamd64の場合にのみファイルがビルドされます。
    • 否定: タグの前に!を付けると否定を意味します。例: // +build !windows は、GOOSwindowsではない場合にファイルがビルドされます。
    • 複数行: 複数の+build行がある場合、それらはAND条件として扱われます。例:
      // +build linux
      // +build amd64
      
      これは、GOOSlinuxかつGOARCHamd64の場合にのみビルドされます。

GOOSGOARCH

Goのビルド環境を決定する重要な環境変数です。

  • GOOS: ターゲットとするオペレーティングシステム(例: linux, windows, darwin, androidなど)。
  • GOARCH: ターゲットとするアーキテクチャ(例: amd64, arm, 386など)。

ビルドタグは、これらの環境変数の値と照合され、ファイルのコンパイルの可否が決定されます。

test/run.gotest/testlib

  • test/run.go: Go言語で書かれたテストランナーの一部で、Goのテストスイートを実行する際に、どのテストファイルが現在の環境で実行可能かを判断するロジックを含んでいます。
  • test/testlib: Goのテストシステムで使用されるシェルスクリプトのライブラリで、テストのセットアップや実行に関連するユーティリティ関数を提供します。ここにもビルドタグの処理ロジックが含まれていました。

このコミットは、これらのテスト関連ツールが、go/buildパッケージが実装しているビルドタグの完全なセマンティクスを理解し、適用するように修正するものです。

技術的詳細

このコミットの主要な技術的変更点は、ビルドタグの評価ロジックを、go/buildパッケージのそれと完全に一致させることです。これは、Goのソースファイルに記述されたビルドタグが、テスト実行時にもビルド時と同じように正確に解釈されることを保証します。

test/run.goの変更点

  1. context構造体の導入:

    type context struct {
        GOOS   string
        GOARCH string
    }
    

    この構造体は、現在のOS (GOOS) とアーキテクチャ (GOARCH) の情報をカプセル化します。これにより、ビルドタグの評価に必要な環境情報が明確に渡されるようになります。

  2. matchメソッドの追加: context構造体にmatch(name string) boolメソッドが追加されました。このメソッドは、単一のビルドタグ(例: linux, amd64, !windows, tag1,tag2)が現在のcontextGOOS, 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と一致するかどうかをチェックします。
  3. shouldTest関数の修正: shouldTest関数は、ソースファイル内の+build行を解析し、そのファイルが現在の環境でテストされるべきかを判断します。

    • 以前はnotgoos, notgoarchといった単純な否定文字列で処理していましたが、新しいmatchメソッドを使用するように変更されました。
    • +build行内の各ワード(スペース区切り)に対してctxt.match(word)を呼び出し、いずれか一つでもマッチすればtrueを返します(OR条件)。
    • +build行が複数ある場合(異なる行に+buildが記述されている場合)は、それらがAND条件として扱われるというGoのビルドタグのセマンティクスは、shouldTest関数が各+build行を独立して評価し、いずれかの行がfalseを返せば全体としてfalseとなることで実現されています。

test/testlibの変更点

  1. _match関数の追加: シェルスクリプトであるtest/testlibにも、Goのmatchメソッドと同様のロジックを実装した_match関数が追加されました。

    • case $1 in *,*): カンマ区切りのタグをAND条件として再帰的に処理します。
    • case $1 in '!'*): 否定タグを処理します。
    • case $1 in $GOARCH|$GOOS): GOARCHまたはGOOSとの一致をチェックします。
  2. +build関数の修正: testlib内の+build関数は、引数として渡されたビルドタグを評価し、現在の環境でテストが実行されるべきでない場合にexit 0(テストをスキップ)します。

    • 新しい_match関数を使用して、各タグが現在の環境にマッチするかどうかを判断します。
    • for tag; do ... doneループ内で、いずれかのタグが_matchによってtrueと評価されれば、m=1を設定します。
    • ループ終了後、m0のまま(つまり、どのタグもマッチしなかった)であれば、テストをスキップするために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を設定します。
    • ループ終了後、m0のまま(どのタグもマッチしなかった)であれば、exit 0を実行してテストをスキップします。これは、+build行に記述されたタグがOR条件で評価され、どれも現在の環境に合致しない場合にテストをスキップするというGoのビルドタグのセマンティクスを反映しています。

これらの変更により、Goのテストシステム全体でビルドタグの解釈が一貫し、開発者はより信頼性の高いテスト環境を利用できるようになりました。

関連リンク

参考にした情報源リンク

  • 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のビルドシステムにおける基本的なセマンティクスです。