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

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

このコミットは、Go言語のhtmlパッケージにおけるテストスイートの改善に関するものです。具体的には、複数のテストデータファイル(.datファイル)を処理し、それぞれのファイルから指定された数のテストケースを実行できるように、テストフレームワークを拡張しています。これにより、より包括的で柔軟なテストが可能になり、将来的なテストケースの追加や管理が容易になります。

コミット

  • コミットハッシュ: bbd173fc3dce58d6eacee750001952371e1c1d23
  • 作者: Nigel Tao nigeltao@golang.org
  • コミット日時: Mon Nov 7 09:38:40 2011 +1100
  • コミットメッセージ:
    html: be able to test more than one testdata file.
    
    R=andybalholm
    CC=golang-dev
    https://golang.org/cl/5351041
    

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

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

元コミット内容

このコミットの目的は、「複数のテストデータファイルをテストできるようにする」ことです。以前のテストコードでは、単一のテストデータファイル(tests1.dat)しか処理できず、そのファイル内のテストケースも一部しか実行していませんでした。この変更により、複数のテストデータファイルを指定し、それぞれのファイルから実行するテストケースの数を制御できるようになります。

変更の背景

このコミットが行われる前のsrc/pkg/html/parse_test.goには、以下のようなTODOコメントが存在していました。

  • // TODO(nigeltao): Process all the .dat files, not just the first one.
  • // TODO(nigeltao): Process all test cases, not just a subset.

これらのコメントが示すように、既存のテストフレームワークは、HTMLパーサーのテストにおいて、すべてのテストデータファイルや、各ファイル内のすべてのテストケースを網羅的に実行する能力が不足していました。これは、テストの網羅性を低下させ、将来的なバグの発見を遅らせる可能性がありました。

このコミットは、これらの課題に対処し、テストの柔軟性と網羅性を向上させることを目的としています。具体的には、テストデータファイルの管理をより構造化し、各ファイルから実行するテストケースの数を細かく制御できるようにすることで、テストの効率と信頼性を高めています。

前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念とテストに関する知識が必要です。

  • Go言語のtestingパッケージ: Go言語の標準ライブラリに含まれるテストフレームワークです。TestXxxという形式の関数を定義することでテストケースを作成し、go testコマンドで実行します。
    • *testing.T: テスト関数に渡される構造体で、テストの失敗を報告したり、ログを出力したりするためのメソッドを提供します。
    • t.Fatal(err): エラーが発生した場合にテストを即座に終了させ、エラーメッセージを出力します。
    • t.Errorf(...): エラーが発生した場合にテストを失敗としてマークしますが、テストの実行は継続します。
  • io.Readerインターフェース: データを読み込むための基本的なインターフェースです。Readメソッドを持ち、様々なデータソース(ファイル、ネットワーク接続など)からデータを統一的に扱うことができます。
  • chan (チャネル): Go言語におけるゴルーチン間の通信メカニズムです。チャネルを通じて値を送受信することで、並行処理におけるデータの同期と安全な受け渡しを実現します。
    • make(chan io.Reader): io.Reader型の値を送受信するためのチャネルを作成します。
    • <-rc: チャネルrcから値を受信します。
    • close(c): チャネルを閉じます。閉じられたチャネルから値を受信しようとすると、チャネルが空になった後にゼロ値が返され、その後の受信操作はブロックされなくなります。
  • go (ゴルーチン): Go言語における軽量なスレッドのようなものです。goキーワードを関数の呼び出しの前に置くことで、その関数を新しいゴルーチンとして並行して実行します。
  • ioutil.ReadAll: io/ioutilパッケージ(Go 1.16以降はioパッケージに統合)の関数で、io.Readerからすべてのデータを読み込み、バイトスライスとして返します。
  • HTMLパーシング: HTMLドキュメントを解析し、その構造をプログラムで扱える形式(通常はDOMツリー)に変換するプロセスです。このコミットは、HTMLパーサーのテストに関するものであり、パーサーが正しくHTMLを解釈できるかを検証しています。
  • テストデータファイル (.dat): テストの入力として使用されるデータを含むファイルです。このケースでは、HTMLの断片や期待されるパース結果などが含まれていると考えられます。

技術的詳細

このコミットの主要な技術的変更点は、TestParser関数におけるテストデータファイルの処理方法の再構築です。

  1. testFiles構造体の導入: 以前はfilenamesという文字列スライスでテストデータファイル名を管理していましたが、このコミットではtestFilesという匿名構造体のスライスを導入しました。

    testFiles := []struct {
        filename string
        // n is the number of test cases to run from that file.
        // -1 means all test cases.
        n int
    }{
        // TODO(nigeltao): Process all the test cases from all the .dat files.
        {"tests1.dat", 87},
        {"tests2.dat", 0},
        {"tests3.dat", 0},
    }
    

    この構造体は、filename(テストデータファイル名)とn(そのファイルから実行するテストケースの数)という2つのフィールドを持ちます。n-1の場合はすべてのテストケースを実行するという意図がコメントで示されていますが、現在のコードでは0が設定されており、これはまだすべてのテストケースを処理する準備ができていないことを示唆しています(TODOコメントも残っています)。

  2. readDat関数のチャネルクローズの改善: readDat関数にdefer close(c)が追加されました。これにより、readDat関数が終了する際に、io.Readerを送信するチャネルcが確実に閉じられるようになります。チャネルを閉じることで、受信側(TestParser関数)は、これ以上データが送信されないことを認識し、チャネルからの読み取りループを適切に終了させることができます。

  3. テストケース実行ループの変更: 以前はfor i := 0; i < 87; i++のように固定の回数でループしていましたが、新しいコードではfor i := 0; i != tf.n; i++となり、testFiles構造体のnフィールドに基づいてループ回数が決定されます。これにより、各テストデータファイルに対して異なる数のテストケースを実行できるようになりました。

  4. チャネルからのデータ読み取りの堅牢化: b, err := ioutil.ReadAll(<-rc)の前に、dataReader := <-rcif dataReader == nil { break }というチェックが追加されました。これは、readDat関数がチャネルを閉じた場合(例えば、tf.nが0でテストケースが一つも実行されない場合など)、<-rcnilを返す可能性があるため、nilポインタ参照を防ぐためのガードです。これにより、テストの実行がより安定します。

  5. 未処理テストケースのドレイン: 各テストデータファイルの処理ループの最後に、以下のコードが追加されました。

    // Drain any untested cases for the test file.
    for r := range rc {
        if _, err := ioutil.ReadAll(r); err != nil {
            t.Fatal(err)
        }
    }
    

    これは、tf.nで指定された数のテストケースを処理した後、チャネルrcに残っている可能性のある未処理のio.Readerをすべて読み飛ばす(ドレインする)ためのものです。これにより、次のテストデータファイルの処理に移る前に、チャネルが完全に空になり、リソースリークや予期せぬ動作を防ぎます。

これらの変更により、TestParserは複数のテストデータファイルをより柔軟に、かつ堅牢に処理できるようになり、テストフレームワークとしての拡張性が向上しました。

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

src/pkg/html/parse_test.goファイルにおける主要な変更箇所は以下の通りです。

--- a/src/pkg/html/parse_test.go
+++ b/src/pkg/html/parse_test.go
@@ -23,6 +23,7 @@ func pipeErr(err error) io.Reader {
 }
 
 func readDat(filename string, c chan io.Reader) {
+	defer close(c) // 追加: チャネルを確実に閉じる
 	f, err := os.Open("testdata/webkit/" + filename)
 	if err != nil {
 		c <- pipeErr(err)
@@ -125,17 +126,27 @@ func dump(n *Node) (string, error) {
 }
 
 func TestParser(t *testing.T) {
-	// TODO(nigeltao): Process all the .dat files, not just the first one.
-	filenames := []string{
-		"tests1.dat",
+	// 変更: 複数のテストデータファイルを構造体で管理
+	testFiles := []struct {
+		filename string
+		// n is the number of test cases to run from that file.
+		// -1 means all test cases.
+		n int
+	}{
+		// TODO(nigeltao): Process all the test cases from all the .dat files.
+		{"tests1.dat", 87},
+		{"tests2.dat", 0},
+		{"tests3.dat", 0},
 	}
-	for _, filename := range filenames {
+	for _, tf := range testFiles { // 変更: testFilesをループ
 		rc := make(chan io.Reader)
-		go readDat(filename, rc)
-		// TODO(nigeltao): Process all test cases, not just a subset.
-		for i := 0; i < 87; i++ {
+		go readDat(tf.filename, rc) // 変更: tf.filenameを使用
+		for i := 0; i != tf.n; i++ { // 変更: tf.nに基づいてループ回数を制御
 			// Parse the #data section.
-			b, err := ioutil.ReadAll(<-rc)
+			dataReader := <-rc // 追加: チャネルからの読み取りを一時変数に格納
+			if dataReader == nil { // 追加: nilチェック
+				break
+			}
+			b, err := ioutil.ReadAll(dataReader) // 変更: dataReaderを使用
 			if err != nil {
 				t.Fatal(err)
 			}
@@ -158,7 +169,7 @@ func TestParser(t *testing.T) {
 			t.Fatal(err)
 		}
 		if want := string(b); got != want {
-			t.Errorf("%s test #%d %q, got vs want:\\n----\\n%s----\\n%s----", filename, i, text, got, want)
+			t.Errorf("%s test #%d %q, got vs want:\\n----\\n%s----\\n%s----", tf.filename, i, text, got, want) // 変更: tf.filenameを使用
 			continue
 		}
 		if renderTestBlacklist[text] {
@@ -178,10 +189,16 @@ func TestParser(t *testing.T) {
 			t.Fatal(err)
 		}
 		if got != got1 {
-			t.Errorf("%s test #%d %q, got vs got1:\\n----\\n%s----\\n%s----", filename, i, text, got, got1)
+			t.Errorf("%s test #%d %q, got vs got1:\\n----\\n%s----\\n%s----", tf.filename, i, text, got, got1) // 変更: tf.filenameを使用
 			continue
 		}
 	}
+	// 追加: 未処理のテストケースをドレイン
+	for r := range rc {
+		if _, err := ioutil.ReadAll(r); err != nil {
+			t.Fatal(err)
+		}
+	}
 	}
 }
 

コアとなるコードの解説

  1. func readDat(filename string, c chan io.Reader)内のdefer close(c): この行は、readDat関数が終了する直前にチャネルcを閉じることを保証します。チャネルを閉じることで、TestParser関数内の受信ループ(for r := range rc)が、これ以上データが送信されないことを認識し、適切に終了できるようになります。これは、リソース管理とデッドロックの回避において重要です。

  2. TestParser関数内のtestFiles構造体スライス:

    testFiles := []struct {
        filename string
        n int
    }{
        {"tests1.dat", 87},
        {"tests2.dat", 0},
        {"tests3.dat", 0},
    }
    

    以前は単なる文字列スライスだったfilenamesが、filenamen(実行するテストケース数)を持つ匿名構造体のスライスに置き換えられました。これにより、各テストデータファイルに対して個別の設定(特に実行するテストケースの数)を柔軟に定義できるようになりました。n0のファイルは、現時点ではテストケースが実行されないことを意味します。

  3. for _, tf := range testFilesループ: このループは、新しく定義されたtestFilesスライスを反復処理します。これにより、TestParser関数は、testFilesにリストされているすべてのテストデータファイルに対して、個別にテストを実行できるようになります。

  4. go readDat(tf.filename, rc): 各テストデータファイルに対して、新しいゴルーチンでreadDat関数が呼び出されます。引数には、現在のtestFilesエントリのfilenameが渡されます。これにより、各テストデータファイルの読み込みが並行して行われます。

  5. for i := 0; i != tf.n; i++ループ: この内部ループは、現在のテストデータファイルからtf.nで指定された数のテストケースを処理します。以前の固定値(87)から動的な値に変更されたことで、各ファイルのテストケース実行数を制御できるようになりました。

  6. dataReader := <-rcif dataReader == nil { break }: チャネルrcからio.Readerを受信し、それをdataReader変数に格納します。その直後にif dataReader == nil { break }というチェックが行われます。これは、readDat関数がチャネルを閉じた場合(例えば、tf.n0でテストケースが一つも実行されない場合など)、<-rcnilを返す可能性があるため、nilポインタ参照によるパニックを防ぐための重要なガードです。

  7. t.Errorf内のtf.filenameへの変更: テストが失敗した場合のエラーメッセージにおいて、以前はfilename変数を使用していた箇所がtf.filenameに変更されました。これにより、どのテストデータファイルでエラーが発生したかが、より正確にエラーメッセージに反映されるようになります。

  8. 未処理テストケースのドレインループ:

    for r := range rc {
        if _, err := ioutil.ReadAll(r); err != nil {
            t.Fatal(err)
        }
    }
    

    このループは、tf.nで指定された数のテストケースを処理した後、チャネルrcに残っている可能性のある未処理のio.Readerをすべて読み飛ばします。これは、readDatゴルーチンがまだデータを送信している可能性がある場合に、チャネルが完全に空になることを保証し、次のテストデータファイルの処理に移る前にクリーンな状態を保つために重要です。これにより、リソースリークを防ぎ、テストの信頼性を向上させます。

これらの変更は、Go言語の並行処理機能(ゴルーチンとチャネル)を効果的に活用し、テストフレームワークの柔軟性と堅牢性を高めるための典型的なパターンを示しています。

関連リンク

  • Go Change List (CL): https://golang.org/cl/5351041
    • : このCL番号は非常に古いためか、現在のgolang.org/clgo-review.googlesource.comでは直接検索しても見つかりませんでした。GoのCL番号は通常、これよりも小さい値です。

参考にした情報源リンク