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

[インデックス 10995] ファイルの概要

このコミットは、Goコマンドラインツールにおけるgo testコマンドの2つの重要な改善を導入しています。具体的には、引数なしでgo testを実行した場合、または-vフラグを付けて実行した場合に、テストが成功した際の出力(passing output)を表示するように変更し、さらに、テスト実行時に古くなったパッケージが再ビルドされる場合に警告を出す機能を追加しています。これにより、開発者はテストの実行状況をより詳細に把握できるようになり、また、ビルドプロセスの効率性に関するフィードバックを得られるようになります。

コミット

  • コミットハッシュ: eef71840460669105aca633ad0d22c8ac5281166
  • Author: Russ Cox rsc@golang.org
  • Date: Thu Dec 22 22:24:43 2011 -0500

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/eef71840460669105aca633ad0d22c8ac5281166

元コミット内容

cmd/go: two testing fixes

1. Show passing output for "go test" (no args) and with -v flag.
2. Warn about out-of-date packages being rebuilt.

R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/5504080

変更の背景

このコミットが導入された背景には、go testコマンドのユーザーエクスペリエンスとビルド効率に関する課題がありました。

  1. テスト成功時の出力の不足: 以前のgo testは、テストが成功した場合、デフォルトでは非常に簡潔な出力しか表示しませんでした。特に引数なしでgo testを実行した場合(カレントディレクトリのパッケージをテストする場合)や、詳細な出力を期待する-vフラグを使用した場合でも、成功したテストに関する具体的な出力が表示されないことがありました。これにより、開発者はテストが実際に何を実行し、どのような結果になったのかを詳細に確認することが難しく、デバッグや理解の妨げになることがありました。テストが成功した際にも、そのテストが生成した標準出力や標準エラー出力を見たいというニーズがありました。

  2. 古くなったパッケージの再ビルドに関する情報不足: Goのビルドシステムは、依存関係を解決し、必要に応じてパッケージを再ビルドします。しかし、go testを実行する際に、テスト対象ではないが依存関係にある他のパッケージが古くなっているために再ビルドされる場合、その事実がユーザーに明示的に伝えられませんでした。これにより、テスト実行に時間がかかっている原因が分かりにくく、また、go installを使って事前に依存パッケージをビルドしておくことでテスト時間を短縮できる可能性があるにもかかわらず、その機会が失われていました。開発者にとって、なぜビルドに時間がかかっているのか、そしてそれを改善するために何ができるのかを知ることは重要です。

これらの課題に対処するため、このコミットではgo testの出力とビルドプロセスのフィードバックを改善し、開発者の利便性と効率性を向上させることを目的としています。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGoの概念とgoコマンドの内部動作に関する知識が役立ちます。

  1. go testコマンド:

    • Go言語のテストを実行するための主要なコマンドです。
    • 引数なしで実行すると、カレントディレクトリのパッケージのテストを実行します。
    • パッケージパスを引数として指定すると、そのパッケージのテストを実行します(例: go test ./...で全てのサブパッケージをテスト)。
    • -vフラグを付けると、テストの実行状況や各テスト関数の詳細な出力(標準出力/標準エラー出力)が表示されます。
    • テストが成功すると、通常はokというメッセージが表示されます。
  2. Goのパッケージとビルドシステム:

    • Goのコードは「パッケージ」という単位で管理されます。
    • go buildgo installgo testなどのコマンドは、必要に応じて依存するパッケージを自動的にビルドします。
    • ビルドシステムは、ファイルのタイムスタンプなどに基づいて、パッケージが「古くなっている(out-of-date)」かどうかを判断し、必要であれば再ビルドします。
    • go installコマンドは、パッケージをビルドし、その結果をGOPATH/pkg(またはGOBIN)にインストールします。これにより、次回以降のビルドでそのパッケージが再ビルドされるのを防ぎ、ビルド時間を短縮できます。
  3. ビルドアクショングラフ (Action Graph):

    • goコマンドの内部では、ビルドやテストのプロセスは「アクショングラフ」として表現されます。これは、各ビルドステップ(例: パッケージのコンパイル、テストの実行)をノードとし、それらの依存関係をエッジとする有向非巡回グラフ(DAG)です。
    • src/cmd/go/build.goファイルは、このアクショングラフの構築と実行を担当するGoコマンドのビルドロジックの核心部分です。
    • 各アクションは、その実行に必要な他のアクション(依存関係)を持ちます。
    • builder構造体は、このアクショングラフを管理し、並行して実行するためのロジックを含んでいます。
  4. Package構造体:

    • src/cmd/go/pkg.goで定義されているPackage構造体は、Goのパッケージに関するメタデータ(インポートパス、ソースファイル、依存関係など)を保持します。
    • ビルドシステムはこの構造体を使って、各パッケージの状態と依存関係を追跡します。
  5. コマンドライン引数のパース:

    • go testコマンドは、自身のフラグ(例: -v, -c)と、テストバイナリに渡されるフラグ(例: -test.run, -test.bench)の両方を処理する必要があります。
    • src/cmd/go/testflag.goは、この複雑な引数パースロジックを扱います。

これらの前提知識を理解することで、コミットがGoコマンドの内部でどのように機能し、どのような影響を与えるのかをより深く把握できます。

技術的詳細

このコミットは、主にsrc/cmd/go/build.gosrc/cmd/go/pkg.gosrc/cmd/go/test.gosrc/cmd/go/testflag.goの4つのファイルにわたる変更によって、go testコマンドの動作を改善しています。

1. テスト成功時の出力表示の改善 (testShowPassフラグの導入)

  • 目的: 引数なしのgo testまたは-vフラグ付きのgo testで、テストが成功した場合にも詳細な出力を表示する。
  • 実装:
    • src/cmd/go/test.gotestShowPassという新しいブール型フラグが導入されました。
    • runTest関数内で、pkgArgs(テスト対象のパッケージ引数)が空である場合(つまり、go testが引数なしで実行された場合)またはtestV-vフラグが指定された場合)が真である場合にtestShowPasstrueに設定されます。
    • runTest関数内のb.runTest呼び出し後、テストが成功し、かつtestShowPasstrueの場合に、テストバイナリの標準出力(out変数)がos.Stdoutに書き込まれるようになりました。これにより、テストが成功した場合でも、テストコード内でfmt.Printlnなどで出力された内容が表示されるようになります。
  • testflag.goの変更:
    • testFlags関数が更新され、-vフラグが認識され、testV変数にその値が設定されるようになりました。これにより、go test -vが正しく処理されます。

2. 古くなったパッケージの再ビルド警告

  • 目的: go test実行時に、テスト対象ではないが依存関係のために再ビルドされる古くなったパッケージについて警告する。
  • 実装:
    • src/cmd/go/build.goactionListという新しいヘルパー関数が追加されました。これは、アクショングラフのルートから到達可能な全てのアクションを深さ優先探索(post-order traversal)でリストとして返します。このリストは、アクションの実行順序を決定するために使用されます。
    • src/cmd/go/pkg.goPackage構造体にfakeという新しいブール型フィールドが追加されました。これは、テストのために一時的に生成される「合成された(synthesized)」パッケージ(例: テストバイナリ自体や、テスト用のメインパッケージ)を識別するために使用されます。これらのfakeパッケージは、古くなったパッケージの警告の対象外となります。
    • src/cmd/go/test.gorunTest関数内で、ビルドされる全てのアクション(actionList(root)で取得)をイテレートし、そのアクションがパッケージに関連付けられており(a.p != nil)、かつそのパッケージがokBuildマップに含まれていない(つまり、テスト対象パッケージではない)かつfakeではない場合に、警告メッセージが標準エラー出力に表示されるようになりました。
    • 警告メッセージには、再ビルドされるパッケージのインポートパスが含まれ、最後に「go installでこれらのパッケージをインストールすると、将来のテストが高速化されます」というヒントが表示されます。
    • test関数内で、テストビルドプロセス中に生成されるptest, pxtest, pmainといった合成パッケージに対して、fakeフィールドがtrueに設定されるようになりました。

3. testFlags関数の引数パースロジックの改善

  • 目的: go testコマンドの引数パースをより堅牢にし、パッケージ名とテストバイナリに渡すフラグの区別を正確に行う。
  • 実装:
    • src/cmd/go/testflag.gotestFlags関数のシグネチャが変更され、packageNamespassToTestの2つの文字列スライスを返すようになりました。
    • この関数は、引数をイテレートし、ハイフンで始まらない引数をパッケージ名として、ハイフンで始まる引数をフラグとして解釈します。
    • 特に、未知のフラグ(testFlagnilを返す場合)が見つかった場合、それ以降の引数は全てテストバイナリに渡すフラグと見なされ、パッケージ名のリストはそれ以上追加されないようにinPkgフラグが制御されます。これにより、go test -x mathのような形式や、go test fmt -custom-flag-for-fmt-testのような形式の両方で、引数が正しくパースされるようになります。

これらの変更により、go testはより情報豊富で、開発者にとって使いやすいツールとなりました。

コアとなるコードの変更箇所

src/cmd/go/build.go

--- a/src/cmd/go/build.go
+++ b/src/cmd/go/build.go
@@ -336,6 +336,26 @@ func (b *builder) action(mode buildMode, depMode buildMode, p *Package) *action
 	return a
 }
 
+// actionList returns the list of actions in the dag rooted at root
+// as visited in a depth-first post-order traversal.
+func actionList(root *action) []*action {
+	seen := map[*action]bool{}
+	all := []*action{}
+	var walk func(*action)
+	walk = func(a *action) {
+		if seen[a] {
+			return
+		}
+		seen[a] = true
+		for _, a1 := range a.deps {
+			walk(a1)
+		}
+		all = append(all, a)
+	}
+	walk(root)
+	return all
+}
+
 // do runs the action graph rooted at root.
 func (b *builder) do(root *action) {
 	// Build list of all actions, assigning depth-first post-order priority.
@@ -349,27 +369,16 @@ func (b *builder) do(root *action) {
 	// ensure that, all else being equal, the execution prefers
 	// to do what it would have done first in a simple depth-first
 	// dependency order traversal.
-	all := map[*action]bool{}
-	priority := 0
-	var walk func(*action)
-	walk = func(a *action) {
-		if all[a] {
-			return
-		}
-		all[a] = true
-		priority++
-		for _, a1 := range a.deps {
-			walk(a1)
-		}
-		a.priority = priority
+	all := actionList(root)
+	for i, a := range all {
+		a.priority = i
 	}
-	walk(root)
 
 	b.readySema = make(chan bool, len(all))
 	done := make(chan bool)
 
 	// Initialize per-action execution state.
-	for a := range all {
+	for _, a := range all {
 		for _, a1 := range a.deps {
 			a1.triggers = append(a1.triggers, a)
 		}

src/cmd/go/pkg.go

--- a/src/cmd/go/pkg.go
+++ b/src/cmd/go/pkg.go
@@ -48,6 +48,7 @@ type Package struct {\n 	imports []*Package\n 	gofiles []string // GoFiles+CgoFiles, absolute paths\n 	target  string   // installed file for this package (may be executable)\n+\tfake    bool     // synthesized package\n }\n \n // packageCache is a lookup cache for loadPackage,\n```

### `src/cmd/go/test.go`

```diff
--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -193,25 +193,28 @@ See the documentation of the testing package for more information.\n // For now just use the gotest code.\n \n var (\n-\ttestC     bool     // -c flag\n-\ttestX     bool     // -x flag\n-\ttestFiles []string // -file flag(s)  TODO: not respected\n-\ttestArgs  []string\n+\ttestC        bool     // -c flag\n+\ttestX        bool     // -x flag\n+\ttestV        bool     // -v flag\n+\ttestFiles    []string // -file flag(s)  TODO: not respected\n+\ttestArgs     []string\n+\ttestShowPass bool // whether to display passing output\n )\n \n func runTest(cmd *Command, args []string) {\n-\t// Determine which are the import paths\n-\t// (leading arguments not starting with -).\n-\ti := 0\n-\tfor i < len(args) && !strings.HasPrefix(args[i], \"-\") {\n-\t\ti++\n-\t}\n-\tpkgs := packages(args[:i])\n+\tvar pkgArgs []string\n+\tpkgArgs, testArgs = testFlags(args)\n+\n+\t// show test PASS output when no packages\n+\t// are listed (implicitly current directory: \"go test\")\n+\t// or when the -v flag has been given.\n+\ttestShowPass = len(pkgArgs) == 0 || testV\n+\n+\tpkgs := packages(pkgArgs)\n \tif len(pkgs) == 0 {\n \t\tfatalf(\"no packages to test\")\n \t}\n \n-\ttestArgs = testFlags(args[i:])\n \tif testC && len(pkgs) != 1 {\n \t\tfatalf(\"cannot use -c flag with multiple packages\")\n \t}\n@@ -243,9 +246,31 @@ func runTest(cmd *Command, args []string) {\n \t\t\ta.deps = append(a.deps, runs[i-1])\n \t\t}\n \t}\n+\troot := &action{deps: runs}\n+\n+\t// If we are building any out-of-date packages other\n+\t// than those under test, warn.\n+\tokBuild := map[*Package]bool{}\n+\tfor _, p := range pkgs {\n+\t\tokBuild[p] = true\n+\t}\n+\n+\twarned := false\n+\tfor _, a := range actionList(root) {\n+\t\tif a.p != nil && a.f != nil && !okBuild[a.p] && !a.p.fake {\n+\t\t\tokBuild[a.p] = true // don\'t warn again\n+\t\t\tif !warned {\n+\t\t\t\tfmt.Fprintf(os.Stderr, \"warning: building out-of-date packages:\\n\")\n+\t\t\t\twarned = true\n+\t\t\t}\n+\t\t\tfmt.Fprintf(os.Stderr, \"\\t%s\\n\", a.p.ImportPath)\n+\t\t}\n+\t}\n+\tif warned {\n+\t\tfmt.Fprintf(os.Stderr, \"installing these packages with \'go install\' will speed future tests.\\n\\n\")\n+\t}\n \n-\tallRuns := &action{deps: runs}\n-\tb.do(allRuns)\n+\tb.do(root)\n }\n \n func (b *builder) test(p *Package) (buildAction, runAction *action, err error) {\n@@ -312,6 +337,7 @@ func (b *builder) test(p *Package) (buildAction, runAction *action, err error) {\n \t\tptest.Imports = append(append([]string{}, p.info.Imports...), p.info.TestImports...)\n \t\tptest.imports = append(append([]*Package{}, p.imports...), imports...)\n \t\tptest.pkgdir = testDir\n+\t\tptest.fake = true\n \t\ta := b.action(modeBuild, modeBuild, ptest)\n \t\ta.objdir = testDir + string(filepath.Separator)\n \t\ta.objpkg = ptestObj\n@@ -333,6 +359,7 @@ func (b *builder) test(p *Package) (buildAction, runAction *action, err error) {\n \t\t\tinfo:       &build.DirInfo{},\n \t\t\timports:    imports,\n \t\t\tpkgdir:     testDir,\n+\t\t\tfake:       true,\n \t\t}\n \t\tpxtest.imports = append(pxtest.imports, ptest)\n \t\ta := b.action(modeBuild, modeBuild, pxtest)\n@@ -349,6 +376,7 @@ func (b *builder) test(p *Package) (buildAction, runAction *action, err error) {\n \t\tt:       p.t,\n \t\tinfo:    &build.DirInfo{},\n \t\timports: []*Package{ptest},\n+\t\tfake:    true,\n \t}\n \tif pxtest != nil {\n \t\tpmain.imports = append(pmain.imports, pxtest)\n@@ -407,6 +435,9 @@ func (b *builder) runTest(a *action) error {\n \tout, err := cmd.CombinedOutput()\n \tif err == nil && (bytes.Equal(out, pass[1:]) || bytes.HasSuffix(out, pass)) {\n \t\tfmt.Printf(\"ok  \\t%s\\n\", a.p.ImportPath)\n+\t\tif testShowPass {\n+\t\t\tos.Stdout.Write(out)\n+\t\t}\n \t\treturn nil\n \t}\n \n```

### `src/cmd/go/testflag.go`

```diff
--- a/src/cmd/go/testflag.go
+++ b/src/cmd/go/testflag.go
@@ -78,10 +78,39 @@ var testFlagDefn = []*testFlagSpec{\n // Unfortunately for us, we need to do our own flag processing because go test\n // grabs some flags but otherwise its command line is just a holding place for\n // test.out\'s arguments.\n-func testFlags(args []string) (passToTest []string) {\n+// We allow known flags both before and after the package name list,\n+// to allow both\n+//	go test fmt -custom-flag-for-fmt-test\n+//	go test -x math\n+func testFlags(args []string) (packageNames, passToTest []string) {\n+\tinPkg := false\n \tfor i := 0; i < len(args); i++ {\n+\t\tif !strings.HasPrefix(args[i], \"-\") {\n+\t\t\tif !inPkg && packageNames == nil {\n+\t\t\t\t// First package name we\'ve seen.\n+\t\t\t\tinPkg = true\n+\t\t\t}\n+\t\t\tif inPkg {\n+\t\t\t\tpackageNames = append(packageNames, args[i])\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t}\n+\n+\t\tif inPkg {\n+\t\t\t// Found an argument beginning with \"-\"; end of package list.\n+\t\t\tinPkg = false\n+\t\t}\n+\n \t\tf, value, extraWord := testFlag(args, i)\n \t\tif f == nil {\n+\t\t\t// This is a flag we do not know; we must assume\n+\t\t\t// that any args we see after this might be flag \n+\t\t\t// arguments, not package names.\n+\t\t\tinPkg = false\n+\t\t\tif packageNames == nil {\n+\t\t\t\t// make non-nil: we have seen the empty package list\n+\t\t\t\tpackageNames = []string{}\n+\t\t\t}\n \t\t\tpassToTest = append(passToTest, args[i])\n \t\t\tcontinue\n \t\t}\n@@ -90,6 +119,8 @@ func testFlags(args []string) (passToTest []string) {\n \t\t\tsetBoolFlag(&testC, value)\n \t\tcase \"x\":\n \t\t\tsetBoolFlag(&testX, value)\n+\t\tcase \"v\":\n+\t\t\tsetBoolFlag(&testV, value)\n \t\tcase \"file\":\n \t\t\ttestFiles = append(testFiles, value)\n \t\t}\n```

## コアとなるコードの解説

### `src/cmd/go/build.go`における`actionList`関数の追加と`do`関数の変更

*   **`actionList`関数**:
    *   この関数は、ビルドシステムが構築するアクショングラフ(DAG)を深さ優先探索(post-order traversal)で走査し、全てのアクションをリストとして返します。
    *   `seen`マップを使用して、既に訪問したアクションを追跡し、無限ループを防ぎます。
    *   `walk`という再帰関数が定義されており、依存関係を先に処理してから現在のアクションをリストに追加することで、post-orderの順序を保証します。
    *   このリストは、後続の処理(特に古くなったパッケージの警告)で、グラフ内の全てのアクションを効率的にイテレートするために使用されます。
*   **`do`関数の変更**:
    *   以前は、`do`関数内で直接アクショングラフを走査し、`priority`を割り当てていましたが、このロジックが`actionList`関数に切り出されました。
    *   `actionList(root)`を呼び出すことで、全てのアクションのリストを取得し、そのインデックスを`a.priority`として割り当てるようになりました。これにより、アクションの優先順位付けがより明確かつ効率的になりました。
    *   `b.readySema`と`done`チャネルの初期化、および各アクションの`triggers`の初期化ループは、`actionList`から返された`all`スライスをイテレートするように変更されました。

### `src/cmd/go/pkg.go`における`Package`構造体への`fake`フィールド追加

*   **`fake bool`フィールド**:
    *   `Package`構造体に`fake`という新しいブール型フィールドが追加されました。
    *   このフィールドは、その`Package`インスタンスが、実際のGoソースコードから読み込まれたパッケージではなく、テスト実行のためにGoコマンドによって「合成された(synthesized)」一時的なパッケージ(例: テストバイナリ自体や、テスト用のメインパッケージ)であるかどうかを示します。
    *   このフラグは、古くなったパッケージの警告ロジックにおいて、警告の対象から除外すべきパッケージを識別するために使用されます。テスト実行のために内部的に生成されるパッケージは、ユーザーが`go install`で事前にビルドできるものではないため、警告の対象外とするのが適切です。

### `src/cmd/go/test.go`におけるテスト出力と警告ロジックの変更

*   **`testV`と`testShowPass`フラグの導入**:
    *   `testV`は`-v`フラグが指定されたかどうかを保持します。
    *   `testShowPass`は、テストが成功した場合に詳細な出力を表示するかどうかを制御する新しいフラグです。
    *   `runTest`関数内で、`go test`が引数なしで実行された場合(`len(pkgArgs) == 0`)または`-v`フラグが指定された場合(`testV`が`true`)に`testShowPass`が`true`に設定されます。
*   **テスト成功時の出力表示**:
    *   `runTest`関数内の`b.runTest`呼び出し後、テストが成功し、かつ`testShowPass`が`true`の場合に、テストバイナリの標準出力が`os.Stdout`に書き込まれるようになりました。これにより、テストが成功した場合でも、テストコード内で`fmt.Println`などで出力された内容が表示されるようになります。
*   **古くなったパッケージの警告ロジック**:
    *   `runTest`関数内で、`actionList(root)`を使って全てのアクションを取得し、それらをイテレートします。
    *   各アクションについて、それがパッケージに関連付けられており(`a.p != nil`)、かつそのパッケージがテスト対象パッケージではない(`!okBuild[a.p]`)、かつ合成パッケージではない(`!a.p.fake`)場合に、そのパッケージが古くなっていることを示す警告メッセージが`os.Stderr`に出力されます。
    *   警告は一度だけヘッダーが表示され、その後、再ビルドされる各パッケージのインポートパスがリストアップされます。
    *   最後に、`go install`の使用を促すヒントが表示されます。
*   **合成パッケージへの`fake`フラグの設定**:
    *   `test`関数内で、テストビルドプロセス中に生成される`ptest`(テストパッケージ)、`pxtest`(外部テストパッケージ)、`pmain`(テスト実行用のメインパッケージ)といった`Package`構造体のインスタンスに対して、`fake`フィールドが`true`に設定されるようになりました。これにより、これらの内部的に生成されるパッケージが古くなったパッケージの警告の対象から除外されます。

### `src/cmd/go/testflag.go`における`testFlags`関数の引数パース改善

*   **シグネチャの変更**:
    *   `testFlags`関数は、これまでの`passToTest []string`に加えて、`packageNames []string`も返すようになりました。これにより、`go test`コマンド自身の引数(パッケージ名)と、テストバイナリに渡す引数を明確に分離して処理できるようになります。
*   **引数パースロジックの強化**:
    *   `inPkg`という新しいブール型フラグが導入され、現在処理中の引数がパッケージ名リストの一部であるかどうかを追跡します。
    *   引数がハイフンで始まらない場合、それがパッケージ名として`packageNames`に追加されます。
    *   ハイフンで始まる引数が見つかった場合、または`testFlag`関数が未知のフラグを返した場合、それ以降の引数は全てテストバイナリに渡す引数(`passToTest`)と見なされ、`inPkg`が`false`に設定されます。これにより、`go test -x math`のようにフラグがパッケージ名の前に来る場合や、`go test fmt -custom-flag-for-fmt-test`のようにパッケージ名の後に未知のフラグが来る場合でも、引数が正しく分類されるようになります。
*   **`-v`フラグの認識**:
    *   `testFlag`関数が`-v`フラグを認識し、`testV`変数にその値を設定するようになりました。これにより、`go test -v`が正しく処理され、`testShowPass`のロジックに反映されます。

これらの変更は、`go test`コマンドの内部動作をより堅牢にし、ユーザーへのフィードバックを改善することで、Go開発者の生産性向上に貢献しています。

## 関連リンク

*   Go CL 5504080: [https://golang.org/cl/5504080](https://golang.org/cl/5504080)

## 参考にした情報源リンク

*   (この解説は、提供されたコミット情報とGo言語の一般的な知識に基づいて生成されました。特定の外部情報源へのリンクは含まれていません。)