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

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

このコミットは、Go言語のコマンドラインツール go にコードカバレッジ分析機能を追加するものです。具体的には、go test コマンドに -cover フラグを導入し、テスト実行時にコードのどの部分が実行されたかを測定する機能の初期実装を提供します。

コミット

commit caefc5d0caa46f032f6929037371c24f4c7f9b47
Author: Rob Pike <r@golang.org>
Date:   Tue Jun 11 09:35:10 2013 -0700

    cmd/go: add coverage analysis
    This feature is not yet ready for real use. The CL marks a bite-sized
    piece that is ready for review. TODOs that remain:
            provide control over output
            produce output without setting -v
            make work on reflect, sync and time packages
                    (fail now due to link errors caused by inlining)
            better documentation
    Almost all packages work now, though, if clumsily; try:
            go test -v -cover=count encoding/binary
    
    R=rsc
    CC=gobot, golang-dev, remyoudompheng
    https://golang.org/cl/10050045

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

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

元コミット内容

cmd/go: add coverage analysis

この機能はまだ実用段階ではありません。この変更リスト(CL)は、レビューの準備ができた一口サイズの変更を示しています。残りのTODO:

  • 出力の制御を提供する
  • -v を設定せずに結果を出力する
  • reflectsynctime パッケージで動作するようにする(インライン化によるリンクエラーのため現在失敗する)
  • より良いドキュメント

ただし、ほとんどのパッケージは現在、不器用ながらも動作します。試してみてください: go test -v -cover=count encoding/binary

変更の背景

このコミットは、Go言語の標準ツールチェーンにコードカバレッジ分析機能を追加するための初期ステップです。ソフトウェア開発において、テストがコードのどの程度を網羅しているかを把握することは、品質保証の観点から非常に重要です。コードカバレッジは、テストスイートが実行時にソースコードのどの部分(ステートメント、関数、ブランチなど)をカバーしたかを示す指標を提供します。

このコミットの時点では、機能はまだ開発途上であり、実用には至っていません。コミットメッセージには、以下の具体的な課題が挙げられています。

  1. 出力制御の不足: カバレッジ結果の表示形式や詳細度を制御するオプションがまだ提供されていませんでした。
  2. -v フラグの依存: カバレッジ結果を見るためには、冗長な出力を行う -v フラグを go test に指定する必要がありました。これは、カバレッジ情報だけを簡潔に得たい場合には不便です。
  3. 特定のパッケージでの問題: reflectsynctime といったGoのコアパッケージでカバレッジ分析が失敗していました。これは、コンパイラのインライン化最適化が原因で発生するリンクエラーに起因すると説明されています。これは、カバレッジ計測のためにコードが変換される際に、コンパイラの挙動と衝突する可能性を示唆しています。
  4. ドキュメントの不足: 新しい機能であるため、適切なドキュメントがまだ整備されていませんでした。

これらの課題にもかかわらず、コミットメッセージは「ほとんどのパッケージは現在、不器用ながらも動作する」と述べており、基本的なカバレッジ計測のメカニズムが機能し始めていることを示しています。これは、将来の完全な機能実装に向けた重要な一歩でした。

前提知識の解説

このコミットを理解するためには、以下の概念が重要です。

  1. コードカバレッジ (Code Coverage):

    • ソフトウェアテストの品質を評価する指標の一つで、テストスイートが実行されたときに、ソースコードのどの部分が実行されたか(「カバーされたか」)を示すものです。
    • 一般的なカバレッジの種類には、ステートメントカバレッジ(各ステートメントが実行されたか)、ブランチカバレッジ(各条件分岐の真/偽パスが実行されたか)、ファンクションカバレッジ(各関数が呼び出されたか)などがあります。
    • カバレッジツールは通常、ソースコードを計測(instrumentation)し、実行時にどのコードパスが通ったかを記録します。
  2. go test コマンド:

    • Go言語の標準的なテスト実行ツールです。Goのテストは、_test.go で終わるファイルに記述され、go test コマンドによって自動的に発見・実行されます。
    • go test は、テストのビルド、実行、結果の表示、ベンチマークの実行など、テストに関する多くの機能を提供します。
  3. go tool コマンド:

    • go コマンドのサブコマンドで、Goツールチェーンに含まれる低レベルのツール(コンパイラ、リンカ、アセンブラなど)を実行するために使用されます。
    • このコミットでは、go tool cover という新しいツールが導入され、カバレッジ分析の具体的な処理(ソースコードの計測など)を担当します。
  4. 計測 (Instrumentation):

    • コードカバレッジツールがカバレッジ情報を収集するために、元のソースコードに追跡コード(計測コード)を挿入するプロセスです。
    • 例えば、各ステートメントの前にカウンタをインクリメントするコードを挿入することで、そのステートメントが実行された回数を記録できます。
  5. Goのビルドプロセス:

    • Goのソースコードは、コンパイラによって中間コードに変換され、その後リンカによって実行可能バイナリにリンクされます。
    • このコミットでは、カバレッジ分析のために、ビルドプロセス中にソースコードが計測されるよう変更が加えられています。
  6. GOMAXPROCS:

    • Goランタイムが同時に実行できるOSスレッドの最大数を制御する環境変数です。並行テストの挙動に影響を与える可能性があります。

技術的詳細

このコミットの主要な技術的変更点は、go test コマンドにカバレッジ分析機能を統合するために、Goのビルドシステムとテスト実行ロジックを拡張したことです。

  1. go tool cover の統合:

    • このコミットは、go tool cover という新しいツールがGoツールチェーンに存在することを前提としています。このツールは、Goのソースファイルを読み込み、カバレッジ計測のためのコードを挿入して、計測済みのソースコードを出力します。
    • src/cmd/go/build.gobuilder.cover 関数が、この go tool cover を呼び出す役割を担っています。この関数は、元のソースファイル (src) を go tool cover に渡し、計測されたコードを一時ファイル (dst) に書き込みます。
    • go tool cover は、-mode (set, count, atomic), -count, -pos といった引数を受け取ります。これらは、カバレッジ計測のモード(実行されたか否か、実行回数、マルチスレッド対応の実行回数)や、計測コードが参照するカウンタ変数や位置情報の名前を制御します。
  2. ビルドプロセスの変更:

    • src/cmd/go/build.gobuilder.build 関数が修正され、カバレッジモードが有効な場合 (a.p.coverMode != "")、通常のGoソースファイル (a.p.GoFiles) を直接ビルドする代わりに、builder.cover を通して計測済みのファイルを生成し、それをビルド対象に含めるようになりました。
    • これにより、コンパイルされるGoファイルは、カバレッジ計測のための追加コードを含むことになります。
  3. テスト実行の変更:

    • src/cmd/go/test.go が大幅に修正され、go test コマンドがカバレッジ分析をサポートするようになりました。
    • 新しいフラグ testCover が導入され、-cover フラグの値を保持します(例: "set", "count", "atomic")。
    • test 関数内で testCover が設定されている場合、p.coverModep.coverVars が設定されます。p.coverVars は、各Goファイルに対応するカバレッジ変数の名前(カウンタ配列名と位置情報配列名)を保持するマップです。
    • declareCoverVars 関数が追加され、テスト対象の各Goファイルに対して一意なカバレッジ変数名を生成します。
    • writeTestmain 関数が変更され、生成される _testmain.go ファイルにカバレッジ関連のコード(カウンタの初期化、カバレッジ結果のダンプなど)が埋め込まれるようになりました。
  4. _testmain.go の生成:

    • _testmain.go は、go test がテストを実行するために動的に生成するGoファイルです。このファイルは、テスト関数、ベンチマーク関数、例(Examples)を登録し、testing.Main を呼び出してテスト実行を制御します。
    • このコミットにより、_testmain.go には、カバレッジ計測のために必要なグローバル変数(coverCounters, coverBlocks)、初期化関数 (init 内で coverRegisterFile を呼び出す)、そしてテスト終了時にカバレッジ結果を標準出力にダンプする coverDump 関数が追加されました。
    • coverRegisterFile は、go tool cover によって生成されたカウンタ配列と位置情報配列を、ランタイムのカバレッジデータ構造に登録します。
    • coverDump は、登録されたカウンタと位置情報を使って、filename:line0.col0,line1.col1 count の形式でカバレッジ結果を出力します。
  5. パッケージ構造の変更:

    • src/cmd/go/pkg.goPackage 構造体に coverModecoverVars フィールドが追加され、カバレッジ分析に関する情報がパッケージレベルで管理されるようになりました。
    • isGoTool マップに code.google.com/p/go.tools/cmd/cover が追加され、go tool cover がGoツールとして認識されるようになりました。
  6. フラグ処理の追加:

    • src/cmd/go/testflag.go-cover フラグの処理が追加されました。このフラグは "set", "count", "atomic" のいずれかの値を取ることができ、それ以外の値が指定された場合はエラーとなります。

このコミットは、Goのビルドシステムとテストフレームワークの深い部分に手を加え、コンパイル時にコードを計測し、実行時にその計測結果を収集・報告するエンドツーエンドのメカニズムを構築しています。

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

このコミットにおける主要な変更ファイルと、その中で特に重要なコードブロックを以下に示します。

  1. src/cmd/go/build.go:

    • func (b *builder) build(a *action) (err error):
      • カバレッジモードが有効な場合 (a.p.coverMode != "")、Goソースファイルを直接ビルドする代わりに、builder.cover を呼び出して計測済みのファイルを生成し、それを gofiles に追加するロジックが追加されました。
    • func (b *builder) cover(a *action, dst, src string, perm os.FileMode, count, pos string) error:
      • go tool cover コマンドを実行し、指定されたソースファイル (src) を計測し、その出力を指定された宛先ファイル (dst) に書き込む新しいヘルパー関数です。
  2. src/cmd/go/pkg.go:

    • type Package struct { ... }:
      • coverMode stringcoverVars map[string]*CoverVar フィールドが追加されました。
    • type CoverVar struct { ... }:
      • カバレッジ変数情報を保持するための新しい構造体 CoverVar が定義されました。
    • var isGoTool = map[string]bool{ ... }:
      • "code.google.com/p/go.tools/cmd/cover": true, が追加され、go tool cover がGoツールとして認識されるようになりました。
  3. src/cmd/go/test.go:

    • var testCover string:
      • -cover フラグの値を保持する新しいグローバル変数が追加されました。
    • func (b *builder) test(p *Package) (buildAction, runAction, printAction *action, err error):
      • testCover が設定されている場合、p.coverModep.coverVars を設定するロジックが追加されました。
      • writeTestmain の呼び出しが変更され、p.coverVars が引数として渡されるようになりました。
      • len(p.TestGoFiles) > 0 || testCover != "" という条件が追加され、テストファイルがない場合でもカバレッジが有効であればテストパッケージがビルドされるようになりました。
    • var coverIndex = 0func declareCoverVars(files ...string) map[string]*CoverVar:
      • カバレッジ変数名を生成するためのヘルパー関数が追加されました。
    • func writeTestmain(out string, p *Package, coverVars map[string]*CoverVar) error:
      • 生成される _testmain.go にカバレッジ関連のコード(coverBlock 構造体、coverCounters, coverBlocks マップ、init 関数、coverRegisterFile 関数、coverDump 関数)を埋め込むためのテンプレートロジックが追加されました。
  4. src/cmd/go/testflag.go:

    • var testFlagDefn = []*testFlagSpec{ ... }:
      • -cover フラグの定義が追加されました。
    • func testFlags(args []string) (packageNames, passToTest []string):
      • -cover フラグの値を解析し、testCover 変数に設定するロジックが追加されました。

コアとなるコードの解説

src/cmd/go/build.go の変更

// If we're doing coverage, preprocess the .go files and put them in the work directory
if a.p.coverMode != "" {
    for _, file := range a.p.GoFiles {
        sourceFile := filepath.Join(a.p.Dir, file)
        cover := a.p.coverVars[file]
        if cover == nil {
            // Not covering this file
            gofiles = append(gofiles, file)
            continue
        }
        coverFile := filepath.Join(obj, file)
        if err := b.cover(a, coverFile, sourceFile, 0666, cover.Count, cover.Pos); err != nil {
            return err
        }
        gofiles = append(gofiles, coverFile)
    }
} else {
    gofiles = append(gofiles, a.p.GoFiles...)
}

このコードブロックは、go build コマンドのビルドプロセスにおいて、カバレッジ分析が有効な場合のGoソースファイルの処理方法を変更しています。 a.p.coverMode != "" は、カバレッジモードが指定されているかどうか(つまり、go test -cover が実行されたかどうか)をチェックします。 もしカバレッジが有効であれば、元のGoファイル (a.p.GoFiles) を直接ビルドする代わりに、各ファイルに対してループを回し、b.cover 関数を呼び出します。b.covergo tool cover を実行して、元のソースファイル (sourceFile) を計測し、計測済みのコードを一時ディレクトリ (obj) 内の新しいファイル (coverFile) に出力します。この coverFile が実際のビルド対象として gofiles リストに追加されます。 これにより、コンパイルされるバイナリには、カバレッジ計測のための追加コードが含まれることになります。

// cover runs, in effect,
//	go tool cover -mode=b.coverMode -count="count" -pos="pos" src.go >dst.go
func (b *builder) cover(a *action, dst, src string, perm os.FileMode, count, pos string) error {
    out, err := b.runOut(a.objdir, "cover "+a.p.ImportPath, nil, tool("cover"), "-mode="+a.p.coverMode, "-count="+count, "-pos="+pos, src)
    if err != nil {
        return err
    }
    // Output is processed source code. Write it to destination.
    return ioutil.WriteFile(dst, out, perm)
}

builder.cover 関数は、go tool cover コマンドを呼び出すラッパーです。 tool("cover") は、go tool cover の実行パスを解決します。 引数として、カバレッジモード (-mode), カウンタ変数名 (-count), 位置情報変数名 (-pos), そして元のソースファイル (src) を go tool cover に渡します。 go tool cover の標準出力は、計測済みのGoソースコードです。この出力は out 変数にキャプチャされ、最終的に ioutil.WriteFile を使って指定された宛先ファイル (dst) に書き込まれます。

src/cmd/go/test.go の変更

if testCover != "" {
    p.coverMode = testCover
    p.coverVars = declareCoverVars(p.GoFiles...)
}

if err := writeTestmain(filepath.Join(testDir, "_testmain.go"), p, p.coverVars); err != nil {
    return nil, nil, nil, err
}

この部分では、go test コマンドが -cover フラグを受け取った場合の処理を定義しています。 testCover != "" は、-cover フラグが指定されたかどうかをチェックします。 もし指定されていれば、現在のパッケージ pcoverMode を設定し、declareCoverVars 関数を呼び出して、このパッケージ内の各Goファイルに対応するカバレッジ変数名(カウンタと位置情報)を生成し、p.coverVars に格納します。 その後、writeTestmain 関数を呼び出して、テスト実行のための _testmain.go ファイルを生成します。この際、生成された p.coverVars が引数として渡され、_testmain.go 内にカバレッジ計測のためのコードが埋め込まれるようにします。

// 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 {
    t := &testFuncs{
        Package:   p,
        CoverVars: coverVars, // ここでcoverVarsが渡される
    }
    // ... (既存のテスト関数ロードロジック) ...
    return b.create(out, testmainTmpl, t) // テンプレートにtを渡す
}

writeTestmain 関数は、テスト実行のメインエントリポイントとなる _testmain.go ファイルを生成します。 変更点として、coverVars が新しい引数として追加され、testFuncs 構造体の CoverVars フィールドに設定されます。 この testFuncs 構造体は、testmainTmpl というGoテンプレートに渡され、テンプレート内で CoverVars の情報を使ってカバレッジ関連のコードが動的に生成されます。

// _testmain.go テンプレートの一部 (testmainTmpl)
{{if .CoverEnabled}}
type coverBlock struct {
    line0 uint32
    col0 uint16
    line1 uint32
    col1 uint16
}

// Only updated by init functions, so no need for atomicity.
var (
    coverCounters = make(map[string][]uint32)
    coverBlocks = make(map[string][]coverBlock)
)

func init() {
    {{range $file, $cover := .CoverVars}}
    coverRegisterFile({{printf "%q" $file}}, _test.{{$cover.Count}}[:], _test.{{$cover.Pos}}[:]...)
    {{end}}
}

func coverRegisterFile(fileName string, counter []uint32, pos ...uint32) {
    // ... (実装は上記「元コミット内容」参照) ...
}

func coverDump() {
    // ... (実装は上記「元コミット内容」参照) ...
}
{{end}}

func main() {
    testing.Main(matchString, tests, benchmarks, examples)
{{if .CoverEnabled}}
    coverDump() // テスト終了後にカバレッジ結果をダンプ
{{end}}
}

これは _testmain.go の生成に使われるテンプレートの一部です。 {{if .CoverEnabled}} ブロックは、カバレッジが有効な場合にのみ、以下のコードが生成されることを意味します。

  • coverBlock 構造体: カバレッジブロックの開始/終了位置を保持します。
  • coverCounterscoverBlocks グローバルマップ: 各ファイルのカバレッジカウンタとブロック情報を保持します。
  • init() 関数: 各ファイルの coverRegisterFile を呼び出し、go tool cover によって生成されたカウンタ配列と位置情報配列を登録します。_test.{{$cover.Count}}[:]_test.{{$cover.Pos}}[:] は、計測済みのGoファイルに埋め込まれたグローバル変数への参照です。
  • coverRegisterFile 関数: カバレッジデータを内部マップに登録します。
  • coverDump 関数: テスト実行終了後(main 関数の最後)に呼び出され、収集されたカバレッジデータを標準出力に特定のフォーマットでダンプします。このフォーマットは filename:line0.col0,line1.col1 count の形式です。

これらの変更により、go test -cover を実行すると、Goソースコードがコンパイル時に計測され、テスト実行中にカバレッジ情報が収集され、テスト終了後にその結果が自動的に出力されるようになります。

関連リンク

参考にした情報源リンク