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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージにおける並列テストのシグナリングメカニズムを改善し、より安全にするための変更です。具体的には、各並列テストが独自のシグナルチャネルを持つようにすることで、テスト間の競合状態を防ぎ、並列テストが正しく実行されないバグを修正しています。

コミット

commit 66155134a7daa2a28bf0ecd55bcf36be3b21e473
Author: Rob Pike <r@golang.org>
Date:   Thu Dec 22 10:43:54 2011 -0800

    testing: make signalling safer for parallel tests
    Each test gets a private signal channel.
    Also fix a bug that prevented parallel tests from running.
    
    R=r, r
    CC=golang-dev
    https://golang.org/cl/5505061

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

https://github.com/golang/go/commit/66155134a7daa2a28bf0ecd55bcf36be3b21e473

元コミット内容

testing: make signalling safer for parallel tests
Each test gets a private signal channel.
Also fix a bug that prevented parallel tests from running.

変更の背景

Goのtestingパッケージでは、t.Parallel()を呼び出すことでテストを並列実行させることができます。しかし、初期の実装では、並列テストの完了を通知するためのシグナルチャネルがテストランナー全体で共有されていました。この共有チャネルの設計には、以下の2つの主要な問題がありました。

  1. シグナリングの安全性と競合状態: 複数の並列テストが同じチャネルに完了シグナルを送るため、どのシグナルがどのテストからのものかを正確に識別することが困難でした。これにより、テストの完了カウントが誤ったり、意図しないテストが完了したと見なされたりする競合状態が発生する可能性がありました。特に、あるテストが失敗してすぐに終了した場合でも、その完了シグナルが他のテストの完了と混同され、テストランナーのロジックを狂わせる可能性がありました。コミットメッセージにある「If all tests pump to the same channel, a bug can occur where a goroutine kicks off a test, fails, and still delivers a completion signal, which skews the counting.」という記述がこの問題を示唆しています。
  2. 並列テストの実行阻害バグ: 共有チャネルのロジックに起因するバグにより、一部の並列テストが正しく開始または完了シグナルを送信できず、結果として並列テストが全く実行されない、あるいは途中で停止してしまう問題が発生していました。

これらの問題を解決し、testingパッケージの並列テスト機能をより堅牢で信頼性の高いものにするために、このコミットが導入されました。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念とtestingパッケージの基本的な動作を把握しておく必要があります。

  • Goのtestingパッケージ: Go言語に組み込まれているテストフレームワークです。go testコマンドで実行され、TestXxxという形式の関数をテストとして認識します。
  • *testing.T: 各テスト関数に渡される構造体で、テストの実行状態(成功/失敗)、ログ出力、サブテストの実行、並列テストの制御など、テストに関する様々な機能を提供します。
  • t.Parallel(): *testing.Tのメソッドで、このメソッドが呼び出されたテストは、他の並列テストと共に並行して実行されることをテストランナーに通知します。これにより、テストの実行時間を短縮できます。
  • Goroutine: Go言語の軽量な並行処理単位です。数千、数万のgoroutineを同時に実行してもオーバーヘッドが少ないのが特徴です。goキーワードを使って関数をgoroutineとして起動します。
  • チャネル (Channel): Goroutine間でデータを安全に送受信するためのGo言語のプリミティブです。チャネルは、並行処理における同期と通信の主要なメカニズムとして機能します。データはチャネルに送信され(ch <- data)、別のgoroutineがチャネルからデータを受信します(data := <-ch)。チャネルはデフォルトでブロックするため、送信側は受信側が準備できるまで、受信側は送信側が準備できるまで待機します。これはgoroutine間の同期に利用されます。
  • runtime.GOMAXPROCS: Goプログラムが同時に実行できるOSスレッドの最大数を設定する関数です。この値は、GoランタイムがgoroutineをOSスレッドにどのようにマッピングするかに影響します。testingパッケージでは、異なるGOMAXPROCS設定でテストを実行し、並行処理の挙動を確認することがあります。

技術的詳細

このコミットの核心は、並列テストの完了シグナルを処理する方法の根本的な変更にあります。

  1. 各テストへのプライベートシグナルチャネルの割り当て: 以前は、すべての並列テストが単一の共有signalチャネルを使用していました。この変更により、*testing.T構造体の各インスタンス(つまり、各テスト)が独自のsignalチャネルを持つようになりました。これにより、特定のテストからの完了シグナルが他のテストのシグナルと混同されることがなくなり、シグナリングの安全性が大幅に向上します。

  2. collectorチャネルの導入: 各テストがプライベートなシグナルチャネルを持つようになったため、テストランナーはこれらの個別のチャネルからシグナルを収集し、全体としての並列テストの完了を監視する必要があります。このために、RunTests関数内に新しいcollectorチャネルが導入されました。各並列テストが完了シグナルを自身のプライベートチャネルに送信すると、そのシグナルは新しいgoroutineによってcollectorチャネルに転送されます。テストランナーは、このcollectorチャネルからシグナルを受信することで、すべての並列テストの完了を効率的かつ正確に追跡できるようになります。これにより、共有チャネルの競合問題を解決しつつ、複数の並列テストの完了を単一の場所で集約できるようになります。

  3. t.Parallel()内のシグナル送信の変更: t.Parallel()メソッドは、テストが並列実行されることをテストランナーに通知し、メインのテストループを解放するためにシグナルを送信します。以前はt.signal <- nilとしていましたが、これはinterface{}型のチャネルにnilを送信していました。変更後はt.signal <- (*T)(nil)となり、*testing.T型のnilポインタを送信するようになりました。これは、チャネルの型がinterface{}であっても、送信する値の具体的な型を明示することで、より意図が明確になり、将来的な型安全性の向上に寄与します。このシグナルは、テストが並列実行モードに入ったことをテストランナーに伝えるためのものです。

  4. 並列テスト実行バグの修正: 共有チャネルの設計では、テストの開始と完了のシグナルが混在し、テストランナーが並列テストの数を正確にカウントできない、あるいは完了シグナルを誤って処理してしまうことがありました。各テストにプライベートチャネルを割り当て、collectorチャネルを通じて集約する新しいメカニズムにより、シグナルが明確に分離され、テストランナーが並列テストのライフサイクルを正確に管理できるようになりました。これにより、以前は並列テストの実行を妨げていた根本的なバグが解消されました。

これらの変更により、Goのtestingパッケージは、より大規模で複雑なテストスイートにおいても、並列テストを安全かつ信頼性高く実行できるようになりました。

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

変更はsrc/pkg/testing/testing.goファイルに集中しています。

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -182,8 +182,8 @@ func (c *common) Fatalf(format string, args ...interface{}) {
 // Parallel signals that this test is to be run in parallel with (and only with) 
 // other parallel tests in this CPU group.
 func (t *T) Parallel() {
-	t.signal <- nil   // Release main testing loop
-	<-t.startParallel // Wait for serial tests to finish
+	t.signal <- (*T)(nil) // Release main testing loop
+	<-t.startParallel     // Wait for serial tests to finish
 }
 
 // An internal type but exported because it is cross-package; part of the implementation
@@ -236,11 +236,14 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 		fmt.Fprintln(os.Stderr, "testing: warning: no tests to run")
 		return
 	}
-	// TODO: each test should have its own channel, although that means
-	// keeping track of the channels when we're running parallel tests.
-	signal := make(chan interface{})
 	for _, procs := range cpuList {
 		runtime.GOMAXPROCS(procs)
+		// We build a new channel tree for each run of the loop.
+		// collector merges in one channel all the upstream signals from parallel tests.
+		// If all tests pump to the same channel, a bug can occur where a goroutine
+		// kicks off a test, fails, and still delivers a completion signal, which skews the
+		// counting.
+		var collector = make(chan interface{})
  
 		numParallel := 0
 		startParallel := make(chan bool)
@@ -260,7 +263,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 			}
 			t := &T{
 				common: common{
-					signal: signal,
+					signal: make(chan interface{}),
 				},
 				name:          testName,
 				startParallel: startParallel,
@@ -272,6 +275,9 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 			go tRunner(t, &tests[i])
 			out := (<-t.signal).(*T)
 			if out == nil { // Parallel run.
+				go func() {
+					collector <- <-t.signal
+				}()
 				numParallel++
 				continue
 			}
@@ -287,7 +293,7 @@ func RunTests(matchString func(pat, str string) (bool, error), tests []InternalT
 				numParallel--
 				continue
 			}
-			t := (<-signal).(*T)
+			t := (<-collector).(*T)
 			t.report()
 			ok = ok && !t.failed
 			running--

コアとなるコードの解説

  1. func (t *T) Parallel() の変更:

    • 変更前: t.signal <- nil
    • 変更後: t.signal <- (*T)(nil)
    • t.Parallel()が呼び出された際、テストが並列実行モードに入ったことをテストランナーに通知するために、t.signalチャネルにシグナルを送信します。以前はnilを直接送信していましたが、これはinterface{}型のチャネルにnilを送信する際に、値がnilであることと型がnilであることの区別が曖昧になる可能性がありました。変更後は、明示的に*testing.T型のnilポインタを送信することで、より型安全で意図が明確なシグナリングを実現しています。このシグナルは、テストランナーがテストを並列キューに入れるためのトリガーとなります。
  2. func RunTests(...) 内の変更:

    • 共有signalチャネルの削除:

      • 変更前: signal := make(chan interface{})
      • 変更後: この行が削除されました。
      • 以前は、すべてのテストが完了シグナルを送るための単一のsignalチャネルがRunTests関数内で作成されていました。この共有チャネルが競合状態の原因となっていたため、削除されました。
    • collectorチャネルの導入:

      • 変更後: var collector = make(chan interface{})
      • RunTests関数のfor _, procs := range cpuListループ内で、各GOMAXPROCS設定の実行ごとに新しいcollectorチャネルが作成されるようになりました。このcollectorチャネルは、各並列テストが自身のプライベートチャネルに送信した完了シグナルを一つに集約するためのものです。これにより、テストランナーは単一のチャネルからすべての並列テストの完了を監視できるようになります。
    • *testing.Tインスタンスのsignalチャネルの初期化:

      • 変更前: signal: signal,
      • 変更後: signal: make(chan interface{}),
      • *testing.Tインスタンスが作成される際に、以前は共有のsignalチャネルを割り当てていましたが、変更後はmake(chan interface{})によって各テストに専用のプライベートなシグナルチャネルが割り当てられるようになりました。これが「Each test gets a private signal channel.」というコミットメッセージの核心部分です。
    • 並列テストの完了シグナル転送ロジックの追加:

      • 変更後:
        if out == nil { // Parallel run.
            go func() {
                collector <- <-t.signal
            }()
            numParallel++
            continue
        }
        
      • テストがt.Parallel()を呼び出して並列実行モードに入った場合(out == nilの場合)、新しいgoroutineが起動されます。このgoroutineは、そのテストのプライベートなt.signalチャネルから完了シグナルを受信し、それをcollectorチャネルに転送します。これにより、テストランナーはcollectorチャネルを通じて、すべての並列テストの完了を非同期に、かつ安全に監視できるようになります。
    • collectorチャネルからのシグナル受信:

      • 変更前: t := (<-signal).(*T)
      • 変更後: t := (<-collector).(*T)
      • テストランナーが並列テストの完了を待つ際、以前は共有のsignalチャネルからシグナルを受信していましたが、変更後は新しく導入されたcollectorチャネルからシグナルを受信するようになりました。これにより、テストランナーは各テストのプライベートチャネルから集約された完了シグナルを処理し、テストのレポートや状態更新を正確に行うことができます。

これらの変更により、Goのtestingパッケージは、並列テストのシグナリングメカニズムを大幅に改善し、より堅牢で信頼性の高い並列テスト実行環境を提供できるようになりました。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分 (git diff)
  • Go言語の公式ドキュメント (testingパッケージ、goroutine、チャネルに関する記述)
  • Go言語のソースコード (src/pkg/testing/testing.go)