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

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

このコミットは、Go言語のgo testコマンドに-coverpkgフラグを追加し、テストカバレッジ計測の柔軟性を向上させるものです。これにより、特定のパッケージのテストを実行しながら、別のパッケージ群のコードカバレッジを計測することが可能になります。また、既存のrecompileForTestロジックがpackageListを活用して改善され、より効率的でクリーンなコードベースになっています。

コミット

commit 97c19f0f721b1db634dcae6a766712533a8f1bd5
Author: Russ Cox <rsc@golang.org>
Date:   Thu Jun 27 17:04:39 2013 -0400

    cmd/go: add -coverpkg
    
    The new -coverpkg flag allows computing coverage in
    one set of packages while running the tests of a different set.
    
    Also clean up some of the previous CL's recompileForTest,
    using packageList to avoid the clumsy recursion.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/10705043

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

https://github.com/golang/go/commit/97c19f0f721b1db634dcae6a766712533a8f1bd5

元コミット内容

このコミットの主な内容は以下の通りです。

  1. -coverpkgフラグの追加: go testコマンドに-coverpkgという新しいフラグが導入されました。このフラグを使用すると、テストを実行するパッケージとは異なる、指定されたパッケージ群のコードカバレッジを計測できるようになります。
  2. recompileForTestの改善: 以前の変更リスト(CL)で導入されたrecompileForTest関数がクリーンアップされました。これは、packageListユーティリティ関数を使用することで、以前の扱いにくい再帰的な処理を回避し、より効率的で理解しやすい実装になったことを意味します。

変更の背景

Go言語のテストカバレッジツールは、元々テスト対象のパッケージとその直接的な依存関係のみを対象としていました。しかし、大規模なプロジェクトやモジュール化されたシステムでは、以下のようなニーズがありました。

  • 共通ライブラリのカバレッジ計測: 複数のアプリケーションやサービスが依存する共通ライブラリがある場合、そのライブラリ自体のテストは少ないかもしれませんが、それを利用する上位のアプリケーションのテストを通じて、ライブラリのコードがどれだけ実行されているかを知りたい場合があります。
  • 統合テストでのカバレッジ計測: 複数のパッケージにまたがる統合テストを実行する際に、テスト対象のパッケージだけでなく、そのテストが呼び出す他のパッケージのコードカバレッジもまとめて計測したい場合があります。
  • テストの分離とカバレッジの結合: テストの実行時間を短縮するために、テストを複数の小さなグループに分割して実行しつつ、最終的なカバレッジレポートはプロジェクト全体で結合したいというシナリオです。

これらのニーズに対応するため、テスト実行とカバレッジ計測の対象を分離し、より柔軟に設定できるメカニズムが必要とされました。-coverpkgフラグは、この課題を解決するために導入されました。

また、recompileForTest関数の改善は、既存のコードベースの保守性と効率性を向上させるための一般的なリファクタリングの一環です。テストカバレッジ計測はコンパイル時にコードを変換するため、依存関係の再構築が必要になることがあり、そのロジックをより堅牢にすることが求められました。

前提知識の解説

Go言語のテストとカバレッジ

Go言語には、標準でテストフレームワークが組み込まれており、go testコマンドを使用してテストを実行します。

  • テストファイル: _test.goで終わるファイルにテストコードを記述します。
  • テスト関数: TestXxxという形式の関数がテスト関数として認識されます。
  • カバレッジ計測: go test -coverを実行すると、テストが実行された際にどのコード行が実行されたかを計測し、その割合(カバレッジ率)をレポートします。
    • -covermode: カバレッジの計測モードを指定します。
      • set: 各ステートメントが実行されたかどうかのみを記録します(デフォルト)。
      • count: 各ステートメントが実行された回数を記録します。
      • atomic: countと同様ですが、並行テスト環境でより正確な結果を得るためにアトミック操作を使用します(パフォーマンスコストが高い)。
    • -coverprofile: カバレッジプロファイルを指定されたファイルに出力します。このファイルはgo tool coverコマンドで解析できます。

Goのパッケージと依存関係

Goのコードは「パッケージ」という単位で整理されます。

  • インポートパス: パッケージはユニークなインポートパス(例: fmt, net/http, github.com/user/repo/mypkg)で識別されます。
  • 依存関係: あるパッケージが別のパッケージの機能を利用する場合、そのパッケージをインポートします。Goのビルドシステムは、これらの依存関係を解決して実行可能ファイルを生成します。

goコマンドの内部動作(ビルドとテスト)

goコマンドは、Goのソースコードをコンパイルし、リンクして実行可能ファイルを生成するツールです。go testコマンドも内部的にはビルドプロセスを利用します。

  • テストバイナリの生成: go testは、テスト対象のパッケージとテストコードをコンパイルし、テストを実行するための特別なバイナリを生成します。
  • カバレッジ計測の仕組み: カバレッジを計測する場合、goコマンドはコンパイル時にソースコードに計測用のコード(インストゥルメンテーション)を挿入します。これにより、実行時に各ステートメントの実行状況が記録されます。

packageListユーティリティ

Goのツールチェイン内部では、パッケージの依存関係グラフを効率的に走査し、関連するパッケージのリストを取得するためのユーティリティ関数が多数存在します。packageListはそのようなユーティリティの一つで、特定のパッケージから到達可能なすべての依存パッケージをリストアップするのに役立ちます。これにより、再帰的な処理を手動で実装するよりも、より簡潔でエラーの少ないコードを書くことができます。

技術的詳細

このコミットは、主にsrc/cmd/go/test.goファイルに大きな変更を加えています。

-coverpkgフラグの追加と処理

  • testflag.go: -coverpkgフラグがtestFlagDefnに追加され、コマンドライン引数として認識されるようになりました。値はカンマ区切りのパッケージパスのリストとして解析され、testCoverPaths変数に格納されます。
  • doc.go: go help testで表示されるドキュメントに-coverpkgの説明が追加されました。
  • test.go:
    • testCoverPathsという新しいグローバル変数が追加され、-coverpkgで指定されたパッケージパスを保持します。
    • testCoverPkgsという[]*Package型の変数も追加され、testCoverPathsで指定されたパッケージのPackage構造体を保持します。
    • runTest関数内で、testCoverPathsが設定されている場合、packagesForBuild関数を使用してこれらのパッケージをロードし、testCoverPkgsに格納します。
    • -coverpkgで指定されたパッケージが実際にテスト対象のパッケージの依存関係として使用されているかどうかの警告ロジックが追加されました。これは、指定されたパッケージが全くテストに影響しない場合にユーザーに通知するためです。
    • testCoverPkgs内の各パッケージは、カバレッジ計測のために再ビルドされるようにマークされます(p.Stale = true, p.fake = true, p.coverMode = testCoverMode, p.coverVars = declareCoverVars(...))。
    • localCoverという新しい変数が導入され、-coverpkgが指定されていない場合にのみ、テスト対象のパッケージに対してローカルなカバレッジ計測を行うように制御します。これにより、-coverpkgが優先されるようになります。
    • テストメイン(_testmain.go)を生成する際に、testCoverPkgsに含まれるすべてのパッケージがインポートされるように、pmain.importsに追加されます。これにより、これらのパッケージの計測コードがテストバイナリに含まれるようになります。

recompileForTestの改善

  • recompileForTest関数のシグネチャ変更: pxtest引数が削除され、recompileForTest(pmain, preal, ptest, testDir)となりました。これは、外部テスト(XTestGoFiles)の処理がこの関数から分離されたことを示唆しています。
  • packageListの活用: 以前の再帰的なcloneおよびrewrite関数に代わり、packageList([]*Package{pmain})を使用して、テストメインパッケージ(pmain)から到達可能なすべてのパッケージを効率的に走査するようになりました。
  • Copy-on-Writeメカニズム: testCopyマップとsplit関数を導入することで、依存関係ツリーを走査しながら、テストカバレッジ計測のために再ビルドが必要なパッケージのみを「テストコピー」として作成し、その依存関係を更新する「Copy-on-Write」のようなメカニズムが実装されました。これにより、不要なパッケージの再ビルドを避けつつ、必要な依存関係の解決を正確に行うことができます。
  • pkgdirtargetの更新: テストコピーされたパッケージは、一時的なテストディレクトリ(testDir)にビルドされるようにpkgdirが設定され、targetがクリアされます。また、fakeStaleフラグが設定され、再ビルドが必要であることを示します。

writeTestmaintestFuncsの変更

  • writeTestmainのシグネチャ変更: writeTestmain(out string, p *Package, coverVars map[string]*CoverVar)からwriteTestmain(out string, pmain, p *Package) errorに変更されました。これは、テストメインの生成が単一のパッケージのカバレッジ変数だけでなく、pmain(テストメインパッケージ)がインポートするすべてのカバレッジ対象パッケージの情報を考慮するようになったことを意味します。
  • testFuncs構造体の変更: CoverVars map[string]*CoverVarフィールドが削除され、Cover []coverInfoという新しいフィールドが追加されました。coverInfoは、カバレッジ対象のPackageと、そのパッケージ内のCoverVar(カバレッジ変数)のマップを保持する構造体です。これにより、複数のパッケージのカバレッジ情報を一元的に管理できるようになりました。
  • テンプレートの変更: _testmain.goを生成するためのGoテンプレートが更新され、{{range $i, $p := .Cover}}ループを使用して、複数のカバレッジ対象パッケージのインポートとカバレッジ変数の登録を行うようになりました。これにより、-coverpkgで指定されたすべてのパッケージのカバレッジデータが正しく収集されるようになります。

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

src/cmd/go/doc.go

--- a/src/cmd/go/doc.go
+++ b/src/cmd/go/doc.go
@@ -755,7 +755,12 @@ control the execution of any test:
 		atomic: int: count, but correct in multithreaded tests;
 		   significantly more expensive.
 	    Implies -cover.
-	    Sets -v. TODO: This will change.
+
+	-coverpkg pkg1,pkg2,pkg3
+	    Apply coverage analysis in each test to the given list of packages.
+	    If this option is not present, each test applies coverage analysis to
+	    the package being tested. Packages are specified as import paths.
+	    Implies -cover.
 
 	-coverprofile cover.out
 	    Write a coverage profile to the specified file after all tests

src/cmd/go/test.bash

--- a/src/cmd/go/test.bash
+++ b/src/cmd/go/test.bash
@@ -324,6 +324,11 @@ rm -rf $d
 # Only succeeds if source order is preserved.
 ./testgo test testdata/example[12]_test.go
 
+# Check that coverage analysis works at all.
+# Don't worry about the exact numbers
+./testgo test -coverpkg=strings strings regexp
+./testgo test -cover strings math regexp
+
 # clean up
 rm -rf testdata/bin testdata/bin1
 rm -f testgo

src/cmd/go/test.go

--- a/src/cmd/go/test.go
+++ b/src/cmd/go/test.go
@@ -12,6 +12,7 @@ import (
 	"go/doc"
 	"go/parser"
 	"go/token"
+	"log"
 	"os"
 	"os/exec"
 	"path"
@@ -129,7 +130,6 @@ control the execution of any test:
 
 	-cover
 	    Enable coverage analysis.
-	    TODO: This feature is not yet fully implemented.
 
 	-covermode set,count,atomic
 	    Set the mode for coverage analysis for the package[s]
@@ -140,7 +140,12 @@ control the execution of any test:
 		atomic: int: count, but correct in multithreaded tests;
 		   significantly more expensive.
 	    Implies -cover.
-	    Sets -v. TODO: This will change.
+
+	-coverpkg pkg1,pkg2,pkg3
+	    Apply coverage analysis in each test to the given list of packages.
+	    The default is for each test to analyze only the package being tested.
+	    Packages are specified as import paths.
+	    Implies -cover.
 
 	-coverprofile cover.out
 	    Write a coverage profile to the specified file after all tests
@@ -261,14 +266,16 @@ See the documentation of the testing package for more information.
 }
 
 var (
-	testC            bool     // -c flag
-	testCover        bool     // -cover flag
-	testCoverMode    string   // -covermode flag
-	testProfile      bool     // some profiling flag
-	testI            bool     // -i flag
-	testV            bool     // -v flag
-	testFiles        []string // -file flag(s)  TODO: not respected
-	testTimeout      string   // -timeout flag
+	testC            bool       // -c flag
+	testCover        bool       // -cover flag
+	testCoverMode    string     // -covermode flag
+	testCoverPaths   []string   // -coverpkg flag
+	testCoverPkgs    []*Package // -coverpkg flag
+	testProfile      bool       // some profiling flag
+	testI            bool       // -i flag
+	testV            bool       // -v flag
+	testFiles        []string   // -file flag(s)  TODO: not respected
+	testTimeout      string     // -timeout flag
 	testArgs         []string
 	testBench        bool
 	testStreamOutput bool // show output as it is generated
@@ -277,6 +284,12 @@ var (
 	testKillTimeout = 10 * time.Minute
 )
 
+var testMainDeps = map[string]bool{
+	// Dependencies for testmain.
+	"testing": true,
+	"regexp":  true,
+}
+
 func runTest(cmd *Command, args []string) {
 	var pkgArgs []string
 	pkgArgs, testArgs = testFlags(args)
@@ -323,11 +336,11 @@ func runTest(cmd *Command, args []string) {
 	if testI {
 		buildV = testV
 
-		deps := map[string]bool{
-			// Dependencies for testmain.
-			"testing": true,
-			"regexp":  true,
+		deps := make(map[string]bool)
+		for dep := range testMainDeps {
+			deps[dep] = true
 		}
+
 		for _, p := range pkgs {
 			// Dependencies for each test.
 			for _, path := range p.Imports {
@@ -373,6 +386,34 @@ func runTest(cmd *Command, args []string) {
 
 	var builds, runs, prints []*action
 
+	if testCoverPaths != nil {
+		// Load packages that were asked about for coverage.
+		// packagesForBuild exits if the packages cannot be loaded.
+		testCoverPkgs = packagesForBuild(testCoverPaths)
+
+		// Warn about -coverpkg arguments that are not actually used.
+		used := make(map[string]bool)
+		for _, p := range pkgs {
+			used[p.ImportPath] = true
+			for _, dep := range p.Deps {
+				used[dep] = true
+			}
+		}
+		for _, p := range testCoverPkgs {
+			if !used[p.ImportPath] {
+				log.Printf("warning: no packages being tested depend on %s", p.ImportPath)
+			}
+		}
+
+		// Mark all the coverage packages for rebuilding with coverage.
+		for _, p := range testCoverPkgs {
+			p.Stale = true // rebuild
+			p.fake = true  // do not warn about rebuild
+			p.coverMode = testCoverMode
+			p.coverVars = declareCoverVars(p.ImportPath, p.GoFiles...)
+		}
+	}
+
 	// Prepare build + run + print actions for all packages being tested.
 	for _, p := range pkgs {
 		buildTest, runTest, printTest, err := b.test(p)
@@ -424,11 +465,22 @@ func runTest(cmd *Command, args []string) {
 	for _, p := range pkgs {
 		okBuild[p] = true
 	}
-
 	warned := false
 	for _, a := range actionList(root) {
-		if a.p != nil && a.f != nil && !okBuild[a.p] && !a.p.fake && !a.p.local {
-			okBuild[a.p] = true // don't warn again
+		if a.p == nil || okBuild[a.p] {
+			continue
+		}
+		okBuild[a.p] = true // warn at most once
+
+		// Don't warn about packages being rebuilt because of
+		// things like coverage analysis.
+		for _, p1 := range a.p.imports {
+			if p1.fake {
+				a.p.fake = true
+			}
+		}
+
+		if a.f != nil && !okBuild[a.p] && !a.p.fake && !a.p.local {
 			if !warned {
 				fmt.Fprintf(os.Stderr, "warning: building out-of-date packages:\n")
 				warned = true
@@ -531,8 +583,14 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
 		return nil, nil, nil, err
 	}
 
+	// Should we apply coverage analysis locally,
+	// only for this package and only for this test?
+	// Yes, if -cover is on but -coverpkg has not specified
+	// a list of packages for global coverage.
+	localCover := testCover && testCoverPaths == nil
+
 	// Test package.
-	if len(p.TestGoFiles) > 0 || testCover {
+	if len(p.TestGoFiles) > 0 || localCover {
 		ptest = new(Package)
 		*ptest = *p
 		ptest.GoFiles = nil
@@ -555,19 +613,15 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
 		m[k] = append(m[k], v...)
 		}
 		ptest.build.ImportPos = m
+
+		if localCover {
+			ptest.coverMode = testCoverMode
+			ptest.coverVars = declareCoverVars(ptest.ImportPath, ptest.GoFiles...)
+		}
 	} else {
 		ptest = p
 	}
 
-	if testCover {
-		ptest.coverMode = testCoverMode
-		ptest.coverVars = declareCoverVars(ptest.ImportPath, ptest.GoFiles...)
-	}
-
-	if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), ptest, ptest.coverVars); err != nil {
-		return nil, nil, nil, err
-	}
-
 	// External test package.
 	if len(p.XTestGoFiles) > 0 {
 		pxtest = &Package{
@@ -597,6 +651,7 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
 		Root:       p.Root,
 		imports:    []*Package{ptest},
 		build:      &build.Package{Name: "main"},
+		pkgdir:     testDir,
 		fake:       true,
 		Stale:      true,
 	}
@@ -606,21 +661,35 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
 
 	// The generated main also imports testing and regexp.
 	stk.push("testmain")
-	ptesting := loadImport("testing", "", &stk, nil)
-	if ptesting.Error != nil {
-		return nil, nil, nil, ptesting.Error
+	for dep := range testMainDeps {
+		if ptest.ImportPath != dep {
+			p1 := loadImport("testing", "", &stk, nil)
+			if p1.Error != nil {
+				return nil, nil, nil, p1.Error
+			}
+			pmain.imports = append(pmain.imports, p1)
+		}
 	}
-	pregexp := loadImport("regexp", "", &stk, nil)
-	if pregexp.Error != nil {
-		return nil, nil, nil, pregexp.Error
+
+	if testCoverPkgs != nil {
+		// Add imports, but avoid duplicates.
+		seen := map[*Package]bool{p: true, ptest: true}
+		for _, p1 := range pmain.imports {
+			seen[p1] = true
+		}
+		for _, p1 := range testCoverPkgs {
+			if !seen[p1] {
+				seen[p1] = true
+				pmain.imports = append(pmain.imports, p1)
+			}
+		}
 	}
-	pmain.imports = append(pmain.imports, ptesting, pregexp)
 
-	if ptest != p && testCover {
+	if ptest != p && localCover {
 		// We have made modifications to the package p being tested
 		// and are rebuilding p (as ptest), writing it to the testDir tree.
 		// Arrange to rebuild, writing to that same tree, all packages q
-		// such that the test depends on q and q depends on p.
+		// such that the test depends on q, and q depends on p.
 		// This makes sure that q sees the modifications to p.
 		// Strictly speaking, the rebuild is only necessary if the
 		// modifications to p change its export metadata, but
@@ -629,7 +708,11 @@ func (b *builder) test(p *Package) (buildAction, runAction, printAction *action,
 		// This will cause extra compilation, so for now we only do it
 		// when testCover is set. The conditions are more general, though,
 		// and we may find that we need to do it always in the future.
-		recompileForTest(pmain, p, ptest, pxtest, testDir)
+		recompileForTest(pmain, p, ptest, testDir)
+	}
+
+	if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), pmain, ptest); err != nil {
+		return nil, nil, nil, err
 	}
 
 	computeStale(pmain)
@@ -690,47 +773,46 @@ func (b *builder) runTest(a *action) error {
 		if testShowPass {
 			a.testOutput.Write(out)
 		}
-		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s\n", a.p.ImportPath, t, coveragePercentage(out))
+		coverWhere := ""
+		if testCoverPaths != nil {
+			coverWhere = " in " + strings.Join(testCoverPaths, ", ")
+		}
+		fmt.Fprintf(a.testOutput, "ok  \t%s\t%s%s%s\n", a.p.ImportPath, t, coveragePercentage(out), coverWhere)
 		return nil
 	}
 
@@ -903,12 +985,24 @@ func isTest(name, prefix string) bool {
 	return !unicode.IsLower(rune)
 }
 
+type coverInfo struct {
+	Package *Package
+	Vars    map[string]*CoverVar
+}
+
 // writeTestmain writes the _testmain.go file for package p to
 // the file named out.
-func writeTestmain(out string, p *Package, coverVars map[string]*CoverVar) error {
+func writeTestmain(out string, pmain, p *Package) error {
+	var cover []coverInfo
+	for _, cp := range pmain.imports {
+		if cp.coverVars != nil {
+			cover = append(cover, coverInfo{cp, cp.coverVars})
+		}
+	}
+
 	t := &testFuncs{
-		Package:   p,
-		CoverVars: coverVars,
+		Package: p,
+		Cover:   cover,
 	}
 	for _, file := range p.TestGoFiles {
 		if err := t.load(filepath.Join(p.Dir, file), "_test", &t.NeedTest); err != nil {
@@ -941,7 +1045,7 @@ type testFuncs struct {
 	Package    *Package
 	NeedTest   bool
 	NeedXtest  bool
-	CoverVars  map[string]*CoverVar
+	Cover      []coverInfo
 }
 
 func (t *testFuncs) CoverEnabled() bool {
@@ -1005,12 +1109,15 @@ import (
 	"regexp"
 	"testing"
 
-{{if or .CoverEnabled .NeedTest}}
+{{if .NeedTest}}
 	_test {{.Package.ImportPath | printf "%q"}}
 {{end}}
 {{if .NeedXtest}}
 	_xtest {{.Package.ImportPath | printf "%s_test" | printf "%q"}}
 {{end}}
+{{range $i, $p := .Cover}}
+	_cover{{$i}} {{$p.Package.ImportPath | printf "%q"}}
+{{end}}
 )
 
 var tests = []testing.InternalTest{
@@ -1054,8 +1161,10 @@ var (
 )
 
 func init() {
-	{{range $file, $cover := .CoverVars}}
-	coverRegisterFile({{printf "%q" $cover.File}}, _test.{{$cover.Var}}.Count[:], _test.{{$cover.Var}}.Pos[:], _test.{{$cover.Var}}.NumStmt[:])
+	{{range $i, $p := .Cover}}
+	{{range $file, $cover := $p.Vars}}
+	coverRegisterFile({{printf "%q" $cover.File}}, _cover{{$i}}.{{$cover.Var}}.Count[:], _cover{{$i}}.{{$cover.Var}}.Pos[:], _cover{{$i}}.{{$cover.Var}}.NumStmt[:])
+	{{end}}
 	{{end}}
 }

src/cmd/go/testflag.go

--- a/src/cmd/go/testflag.go
+++ b/src/cmd/go/testflag.go
@@ -29,6 +29,7 @@ var usageMessage = `Usage of go test:
   -benchtime=1s: passes -test.benchtime to test
   -cover=false: enable coverage analysis
   -covermode="set": passes -test.covermode to test if -cover
+  -coverpkg="": comma-separated list of packages for coverage analysis
   -coverprofile="": passes -test.coverprofile to test if -cover
   -cpu="": passes -test.cpu to test
   -cpuprofile="": passes -test.cpuprofile to test
@@ -67,6 +68,7 @@ var testFlagDefn = []*testFlagSpec{
 	{name: "file", multiOK: true},
 	{name: "i", boolVar: &testI},
 	{name: "cover", boolVar: &testCover},
+	{name: "coverpkg"},
 
 	// build flags.
 	{name: "a", boolVar: &buildA},
@@ -178,6 +180,13 @@ func testFlags(args []string) (packageNames, passToTest []string) {
 		case "blockprofile", "cpuprofile", "memprofile":
 			testProfile = true
 		case "coverpkg":
+			testCover = true
+			if value == "" {
+				testCoverPaths = nil
+			} else {
+				testCoverPaths = strings.Split(value, ",")
+			}
+		case "coverprofile":
 			testCover = true
 			testProfile = true

コアとなるコードの解説

src/cmd/go/test.goにおける変更のポイント

  1. testCoverPathstestCoverPkgs:

    • testCoverPathsは、コマンドラインで-coverpkgに渡された文字列(カンマ区切りのパッケージパス)を保持します。
    • testCoverPkgsは、これらのパスに対応する*Package構造体のスライスです。runTest関数内でpackagesForBuildを使ってロードされます。
    • これらの変数が導入されたことで、テスト対象パッケージとは別に、カバレッジ計測の対象となるパッケージ群を明確に管理できるようになりました。
  2. カバレッジ対象パッケージのマーク付け:

    		for _, p := range testCoverPkgs {
    			p.Stale = true // rebuild
    			p.fake = true  // do not warn about rebuild
    			p.coverMode = testCoverMode
    			p.coverVars = declareCoverVars(p.ImportPath, p.GoFiles...)
    		}
    

    -coverpkgで指定されたパッケージは、強制的に再ビルド(p.Stale = true)され、カバレッジ計測のためのインストゥルメンテーションが施されます(p.coverMode = testCoverMode, p.coverVars = declareCoverVars(...))。p.fake = trueは、これらのパッケージがテストのために一時的に作成されたものであることを示し、通常のビルド警告を抑制します。

  3. localCoverの導入:

    	localCover := testCover && testCoverPaths == nil
    

    この変数は、-coverpkgが指定されていない場合にのみ、従来の「テスト対象パッケージのみをカバレッジ計測する」動作を維持するためのものです。これにより、-coverpkgが指定された場合は、その設定が優先され、ローカルなカバレッジ計測は行われません。

  4. recompileForTestの改善:

    func recompileForTest(pmain, preal, ptest *Package, testDir string) {
    	// The "test copy" of preal is ptest.
    	// For each package that depends on preal, make a "test copy"
    	// that depends on ptest. And so on, up the dependency tree.
    	testCopy := map[*Package]*Package{preal: ptest}
    	for _, p := range packageList([]*Package{pmain}) {
    		// Copy on write.
    		didSplit := false
    		split := func() {
    			if didSplit {
    				return
    			}
    			didSplit = true
    			if p.pkgdir != testDir {
    				p1 := new(Package)
    				testCopy[p] = p1
    				*p1 = *p
    				p1.imports = make([]*Package, len(p.imports))
    				copy(p1.imports, p.imports)
    				p = p1
    				p.pkgdir = testDir
    				p.target = ""
    				p.fake = true
    				p.Stale = true
    			}
    		}
    
    		// Update p.deps and p.imports to use at test copies.
    		for i, dep := range p.deps {
    			if p1 := testCopy[dep]; p1 != nil && p1 != dep {
    				split()
    				p.deps[i] = p1
    			}
    		}
    		for i, imp := range p.imports {
    			if p1 := testCopy[imp]; p1 != nil && p1 != imp {
    				split()
    				p.imports[i] = p1
    			}
    		}
    	}
    }
    

    この関数は、テストカバレッジ計測のためにパッケージが再ビルドされる際に、そのパッケージに依存する他のパッケージも適切に再ビルドされるように依存関係を調整します。

    • packageList([]*Package{pmain})は、テストメインパッケージから到達可能なすべてのパッケージを効率的に取得します。
    • testCopyマップは、元のパッケージと、テストのために作成されたそのコピー(再ビルドされたバージョン)のマッピングを保持します。
    • 「Copy-on-Write」のロジックにより、実際に変更が必要なパッケージ(依存関係がテストコピーに切り替わるパッケージ)のみが複製され、そのpkgdirがテストディレクトリに設定され、再ビルドが促されます。これにより、ビルドの効率が向上します。
  5. writeTestmaintestFuncsの変更:

    • testFuncs構造体にCover []coverInfoが追加され、複数のパッケージのカバレッジ情報を保持できるようになりました。
    • writeTestmain関数は、pmain(テストメインパッケージ)がインポートするすべてのカバレッジ対象パッケージの情報を集約し、testFuncs.Coverに格納します。
    • これにより、生成される_testmain.goファイルは、-coverpkgで指定されたすべてのパッケージのカバレッジ変数を正しく登録し、カバレッジデータを収集できるようになります。

src/cmd/go/testflag.goにおける変更のポイント

  • -coverpkgフラグの定義が追加され、testFlags関数内でその値が解析され、testCoverPathsに格納されるようになりました。
  • -coverpkgが指定された場合、自動的に-coverフラグも有効になるようにtestCover = trueが設定されます。

src/cmd/go/test.bashにおける変更のポイント

  • -coverpkgの使用例が追加されました。
    • ./testgo test -coverpkg=strings strings regexpstringsパッケージとregexpパッケージのテストを実行し、stringsパッケージのカバレッジを計測します。
    • ./testgo test -cover strings math regexpstringsmathregexpパッケージのテストを実行し、それぞれのパッケージのカバレッジを計測します(-coverpkgがないため、従来の動作)。

関連リンク

参考にした情報源リンク