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

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

このコミットは、Go言語のランタイムにおけるデータ競合検出器(Race Detector)のエンドツーエンドテストを追加するものです。具体的には、src/pkg/runtime/race/output_test.goという新しいテストファイルが追加され、データ競合が検出された際の出力形式を検証するテストケースが実装されています。また、既存のsrc/pkg/runtime/crash_test.gosrc/run.bashにも関連する修正が加えられています。

コミット

commit 2791ef0b6784f487738b7dbe6bda520b426131f3
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Mon Aug 12 22:04:10 2013 +0400

    runtime/race: add end-to-end test
    Fixes #5933.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/12699051

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

https://github.com/golang/go/commit/2791ef0b6784f487738b7dbe6bda520b426131f3

元コミット内容

このコミットの目的は、Goランタイムのデータ競合検出器に対して、その出力が期待通りであることを検証するエンドツーエンドテストを追加することです。コミットメッセージには「Fixes #5933」とあり、これはGoのIssueトラッカーにおける特定の課題を解決するものであることを示唆しています。

変更の背景

Go言語のデータ競合検出器は、並行処理におけるデバッグが困難なデータ競合バグを特定するための重要なツールです。この検出器が正しく機能し、データ競合を検出した際に開発者にとって有用な情報を正確な形式で出力することは極めて重要です。

このコミットが「Fixes #5933」と関連していることから、おそらく既存のデータ競合検出器の出力に関する問題、あるいはその検証が不十分であるという課題が存在したと考えられます。エンドツーエンドテストの追加は、検出器が実際のコードベースでどのように振る舞い、どのような出力を生成するかを包括的に検証し、その信頼性を向上させることを目的としています。これにより、将来的な変更やリファクタリングが行われた際にも、検出器の出力の一貫性と正確性が保証されるようになります。

前提知識の解説

データ競合 (Data Race)

データ競合とは、複数のゴルーチン(Goにおける軽量スレッド)が同じメモリ位置に同時にアクセスし、そのうち少なくとも1つのアクセスが書き込みであり、かつそれらのアクセスが適切な同期メカニズム(ミューテックス、チャネルなど)によって順序付けされていない場合に発生する並行処理のバグです。データ競合はプログラムの非決定的な動作やクラッシュを引き起こす可能性があり、デバッグが非常に困難です。

Go Race Detector (データ競合検出器)

Go言語には、データ競合を検出するための組み込みツールである「Race Detector」が提供されています。これは、プログラムの実行時にメモリアクセスを監視し、データ競合のパターンを検出する動的解析ツールです。go build -racego test -raceのように-raceフラグを付けてコンパイル・実行することで有効になります。

Race Detectorは、GoogleのThreadSanitizer (TSan) ランタイムライブラリをベースにしており、以下のメカニズムで動作します。

  1. コンパイル時の計測 (Instrumentation): -raceフラグを付けてコンパイルすると、Goコンパイラは共有メモリへのすべてのメモリアクセスを計測するコードを挿入します。これにより、各読み書き操作がランタイムライブラリへの呼び出しでラップされます。これらのフックは、アクセス時刻、ゴルーチンID、メモリアドレス、スタックトレースなどのメタデータを記録します。
  2. ランタイム検出 (Shadow Memory): 実行時には、「シャドウメモリ」システムが使用されます。これは、各実際のメモリアドレスへの最後のアクセスに関する情報を記録する並列メモリマップです。ゴルーチンがメモリにアクセスすると、検出器はこのシャドウメモリをチェックし、別のゴルーチンが以前に同じ場所に非同期的にアクセスしたかどうか(特に書き込みアクセスがあった場合)を判断します。このような「競合する」パターンが検出されると、警告が出力されます。

Race Detectorがデータ競合を検出すると、通常、以下の詳細なレポートが出力されます。

  • 競合に関与したゴルーチンのスタックトレース。
  • 競合するアクセスが発生した正確なソースコードの場所(ファイルと行番号)。
  • 競合を引き起こしたアクセスの種類(読み取りまたは書き込み)。

エンドツーエンドテスト (End-to-End Test)

エンドツーエンドテストは、ソフトウェアシステム全体が、ユーザーの視点から期待通りに機能するかどうかを検証するテスト手法です。この場合、Go Race Detectorが実際のGoプログラムを実行し、データ競合を検出して、その出力が期待される形式と内容であることを検証するプロセス全体を指します。

技術的詳細

このコミットの主要な技術的詳細は、Go Race Detectorの出力形式を検証するためのテストフレームワークとテストケースの実装にあります。

output_test.goでは、TestOutput関数が定義されており、複数のテストケースをループで実行します。各テストケースは、データ競合を引き起こすGoのソースコードスニペットと、その競合検出器の出力に期待される正規表現パターンを含んでいます。

テストの実行フローは以下の通りです。

  1. 一時ディレクトリの作成: 各テストケースの実行前に、ioutil.TempDirを使用して一時ディレクトリが作成されます。これは、テスト対象のGoプログラムのソースファイルを配置するためです。
  2. ソースファイルの書き込み: テストケースで定義されたGoのソースコードスニペットが、一時ディレクトリ内のmain.goファイルに書き込まれます。
  3. Goプログラムの実行: exec.Commandを使用して、go run -race -gcflags=-l [一時ディレクトリ]/main.goコマンドが実行されます。
    • -race: データ競合検出器を有効にします。
    • -gcflags=-l: コンパイラにインライン化を無効にするよう指示します。これは、スタックトレースのテストにおいて、関数呼び出しのスタックフレームが期待通りに表示されるようにするために重要です。インライン化が行われると、特定の関数呼び出しがスタックトレースに現れない可能性があるため、テストの安定性を確保するために無効化されます。
    • GODEBUGGOMAXPROCSといった環境変数は、プログラムの出力に影響を与えたり、テストを不安定にしたりする可能性があるため、cmd.Envから除外されます。
  4. 出力の検証: 実行されたGoプログラムの標準出力と標準エラー出力がCombinedOutput()で取得され、その内容がテストケースで定義された正規表現パターンと一致するかどうかがregexp.MustCompile(test.re).MatchString(string(got))によって検証されます。これにより、Race Detectorが生成する警告メッセージ、スタックトレース、およびその他の詳細情報が期待通りの形式で出力されていることを確認します。

src/pkg/runtime/crash_test.goの変更は、ファイルクローズ時のエラーハンドリングを改善しています。これは、テストの堅牢性を高めるための一般的な改善であり、直接的にRace Detectorの機能に影響を与えるものではありませんが、テストインフラストラクチャ全体の信頼性向上に寄与します。

src/run.bashの変更は、Goのテストスイート実行スクリプトに、新しく追加されたRace Detectorのエンドツーエンドテストを含めるためのものです。go test -race -run=Output runtime/raceという行が追加され、runtime/raceパッケージ内のTestOutputテストが明示的に実行されるようになります。これにより、CI/CDパイプラインや開発者のローカル環境で、Race Detectorの出力検証テストが自動的に実行されるようになります。

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

このコミットで変更された主要なファイルは以下の3つです。

  1. src/pkg/runtime/crash_test.go
  2. src/pkg/runtime/race/output_test.go (新規追加)
  3. src/run.bash

src/pkg/runtime/crash_test.go の変更

--- a/src/pkg/runtime/crash_test.go
+++ b/src/pkg/runtime/crash_test.go
@@ -44,14 +44,16 @@ func executeTest(t *testing.T, templ string, data interface{}) string {
 	src := filepath.Join(dir, "main.go")
 	f, err := os.Create(src)
 	if err != nil {
-		t.Fatalf("failed to create %v: %v", src, err)
+		t.Fatalf("failed to create file: %v", err)
 	}
 	err = st.Execute(f, data)
 	if err != nil {
 		f.Close()
 		t.Fatalf("failed to execute template: %v", err)
 	}
-	f.Close()
+	if err := f.Close(); err != nil {
+		t.Fatalf("failed to close file: %v", err)
+	}
 
 	got, _ := testEnv(exec.Command("go", "run", src)).CombinedOutput()
 	return string(got)

src/pkg/runtime/race/output_test.go の変更 (新規追加)

--- /dev/null
+++ b/src/pkg/runtime/race/output_test.go
@@ -0,0 +1,109 @@
+// Copyright 2013 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build race
+
+package race_test
+
+import (
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+	"testing"
+)
+
+func TestOutput(t *testing.T) {
+	for _, test := range tests {
+		dir, err := ioutil.TempDir("", "go-build")
+		if err != nil {
+			t.Fatalf("failed to create temp directory: %v", err)
+		}
+		defer os.RemoveAll(dir)
+		src := filepath.Join(dir, "main.go")
+		f, err := os.Create(src)
+		if err != nil {
+			t.Fatalf("failed to create file: %v", err)
+		}
+		_, err = f.WriteString(test.source)
+		if err != nil {
+			f.Close()
+			t.Fatalf("failed to write: %v", err)
+		}
+		if err := f.Close(); err != nil {
+			t.Fatalf("failed to close file: %v", err)
+		}
+		// Pass -l to the compiler to test stack traces.
+		cmd := exec.Command("go", "run", "-race", "-gcflags=-l", src)
+		// GODEBUG spoils program output, GOMAXPROCS makes it flaky.
+		for _, env := range os.Environ() {
+			if strings.HasPrefix(env, "GODEBUG=") ||
+				strings.HasPrefix(env, "GOMAXPROCS=") {
+				continue
+			}
+			cmd.Env = append(cmd.Env, env)
+		}
+		got, _ := cmd.CombinedOutput()
+		if !regexp.MustCompile(test.re).MatchString(string(got)) {
+			t.Fatalf("failed test case %v, expect:\n%v\ngot:\n%s",
+				test.name, test.re, got)
+		}
+	}
+}
+
+var tests = []struct {
+	name   string
+	source string
+	re     string
+}{
+	{"simple", `
+package main
+func main() {
+	done := make(chan bool)
+	x := 0
+	startRacer(&x, done)
+	store(&x, 43)
+	<-done
+}
+func store(x *int, v int) {
+	*x = v
+}
+func startRacer(x *int, done chan bool) {
+	go racer(x, done)
+}
+func racer(x *int, done chan bool) {
+	store(x, 42)
+	done <- true
+}
+`, `==================
+WARNING: DATA RACE
+Write by goroutine [0-9]:
+  main\.store\(\)
+      .*/main\.go:11 \+0x[0-9,a-f]+
+  main\.racer\(\)
+      .*/main\.go:17 \+0x[0-9,a-f]+
+
+Previous write by goroutine 1:
+  main\.store\(\)
+      .*/main\.go:11 \+0x[0-9,a-f]+
+  main\.main\(\)
+      .*/main\.go:7 \+0x[0-9,a-f]+
+
+Goroutine 3 \(running\) created at:
+  main\.startRacer\(\)
+      .*/main\.go:14 \+0x[0-9,a-f]+
+  main\.main\(\)
+      .*/main\.go:6 \+0x[0-9,a-f]+
+
+Goroutine 1 \(running\) created at:
+  _rt0_go\(\)
+      .*/src/pkg/runtime/asm_amd64\.s:[0-9]+ \+0x[0-9,a-f]+
+
+==================
+Found 1 data race\(s\)
+exit status 66
+`},
+}

src/run.bash の変更

--- a/src/run.bash
+++ b/src/run.bash
@@ -61,7 +61,8 @@ case "$GOHOSTOS-$GOOS-$GOARCH-$CGO_ENABLED" in
 linux-linux-amd64-1 | darwin-darwin-amd64-1)
 	echo
 	echo '# Testing race detector.'
-	go test -race -i flag
+	go test -race -i runtime/race flag
+	go test -race -run=Output runtime/race
 	go test -race -short flag
 esac

コアとなるコードの解説

src/pkg/runtime/race/output_test.go

このファイルは、Go Race Detectorの出力形式を検証するためのエンドツーエンドテストを定義しています。

  • // +build race: このビルドタグは、このファイルがgo build -raceまたはgo test -raceが指定された場合にのみコンパイルおよび実行されることを示します。これにより、Race Detectorが有効な環境でのみテストが実行されるようになります。
  • TestOutput(t *testing.T): メインのテスト関数です。testsスライスに定義された各テストケースをループで処理します。
    • 一時ファイルの作成とクリーンアップ: ioutil.TempDirで一時ディレクトリを作成し、defer os.RemoveAll(dir)でテスト終了後にクリーンアップします。この一時ディレクトリ内に、テスト対象のGoソースコード(main.go)が作成されます。
    • Goプログラムの実行: exec.Command("go", "run", "-race", "-gcflags=-l", src)で、作成したmain.goを実行します。
      • -race: Race Detectorを有効にします。
      • -gcflags=-l: コンパイラのインライン化を無効にします。これにより、生成されるスタックトレースが予測可能になり、テストの信頼性が向上します。
      • 環境変数GODEBUGGOMAXPROCSは、テストの出力を不安定にする可能性があるため、cmd.Envから除外されます。
    • 出力の検証: cmd.CombinedOutput()でプログラムの出力を取得し、regexp.MustCompile(test.re).MatchString(string(got))を使って、その出力が期待される正規表現パターンと一致するかどうかを検証します。
  • testsスライス: 構造体のスライスで、各要素が1つのテストケースを表します。
    • name: テストケースの名前。
    • source: データ競合を引き起こすGoのソースコードスニペット。この例では、xという整数変数に対して、メインゴルーチンと別のゴルーチン(racer)が同時に書き込みを行うことでデータ競合を発生させています。
    • re: 期待されるRace Detectorの出力にマッチする正規表現パターン。この正規表現は、WARNING: DATA RACEのヘッダー、競合が発生したゴルーチンの情報、スタックトレース、および最終的なサマリー(Found 1 data race(s)exit status 66)を厳密にチェックします。スタックトレース内のファイルパスやアドレスは正規表現で柔軟にマッチするように記述されています。

src/pkg/runtime/crash_test.go

このファイルは、Goランタイムのクラッシュテストに関連するものです。今回の変更は、executeTest関数内でファイル(f)をクローズする際のエラーハンドリングを改善しています。以前はf.Close()がエラーを返しても無視されていましたが、変更後はif err := f.Close(); err != nil { t.Fatalf(...) }という形でエラーがチェックされ、エラーが発生した場合にはテストが失敗するようになっています。これは、テストの堅牢性を高めるための一般的なベストプラクティスです。

src/run.bash

このシェルスクリプトは、Goプロジェクトのテストスイートを実行するためのものです。今回の変更は、Race Detectorのテスト実行に関するセクションに新しい行を追加しています。

  • go test -race -i runtime/race flag: この行は、runtime/raceパッケージとflagパッケージのテストを-raceモードで実行し、依存関係をインストール(-i)します。
  • go test -race -run=Output runtime/race: この行が今回のコミットの核心的な変更です。runtime/raceパッケージ内のTestOutputという名前のテスト(正規表現でマッチ)を-raceモードで実行するよう指示しています。これにより、新しく追加されたエンドツーエンドテストがGoの標準テストスイートの一部として実行されるようになります。

関連リンク

参考にした情報源リンク

  • Go Race Detectorに関するWeb検索結果
  • Go言語のIssueトラッカー(ただし、Issue #5933の直接的な情報は特定できませんでした)
  • Go言語のソースコード(特にsrc/pkg/runtime/raceディレクトリ)
  • Goのos/execパッケージおよびregexpパッケージのドキュメント
  • Goのテストに関するドキュメント