[インデックス 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
関数におけるテストデータファイルの処理方法の再構築です。
-
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
コメントも残っています)。 -
readDat
関数のチャネルクローズの改善:readDat
関数にdefer close(c)
が追加されました。これにより、readDat
関数が終了する際に、io.Reader
を送信するチャネルc
が確実に閉じられるようになります。チャネルを閉じることで、受信側(TestParser
関数)は、これ以上データが送信されないことを認識し、チャネルからの読み取りループを適切に終了させることができます。 -
テストケース実行ループの変更: 以前は
for i := 0; i < 87; i++
のように固定の回数でループしていましたが、新しいコードではfor i := 0; i != tf.n; i++
となり、testFiles
構造体のn
フィールドに基づいてループ回数が決定されます。これにより、各テストデータファイルに対して異なる数のテストケースを実行できるようになりました。 -
チャネルからのデータ読み取りの堅牢化:
b, err := ioutil.ReadAll(<-rc)
の前に、dataReader := <-rc
とif dataReader == nil { break }
というチェックが追加されました。これは、readDat
関数がチャネルを閉じた場合(例えば、tf.n
が0でテストケースが一つも実行されない場合など)、<-rc
がnil
を返す可能性があるため、nil
ポインタ参照を防ぐためのガードです。これにより、テストの実行がより安定します。 -
未処理テストケースのドレイン: 各テストデータファイルの処理ループの最後に、以下のコードが追加されました。
// 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)
+ }
+ }
}
}
コアとなるコードの解説
-
func readDat(filename string, c chan io.Reader)
内のdefer close(c)
: この行は、readDat
関数が終了する直前にチャネルc
を閉じることを保証します。チャネルを閉じることで、TestParser
関数内の受信ループ(for r := range rc
)が、これ以上データが送信されないことを認識し、適切に終了できるようになります。これは、リソース管理とデッドロックの回避において重要です。 -
TestParser
関数内のtestFiles
構造体スライス:testFiles := []struct { filename string n int }{ {"tests1.dat", 87}, {"tests2.dat", 0}, {"tests3.dat", 0}, }
以前は単なる文字列スライスだった
filenames
が、filename
とn
(実行するテストケース数)を持つ匿名構造体のスライスに置き換えられました。これにより、各テストデータファイルに対して個別の設定(特に実行するテストケースの数)を柔軟に定義できるようになりました。n
が0
のファイルは、現時点ではテストケースが実行されないことを意味します。 -
for _, tf := range testFiles
ループ: このループは、新しく定義されたtestFiles
スライスを反復処理します。これにより、TestParser
関数は、testFiles
にリストされているすべてのテストデータファイルに対して、個別にテストを実行できるようになります。 -
go readDat(tf.filename, rc)
: 各テストデータファイルに対して、新しいゴルーチンでreadDat
関数が呼び出されます。引数には、現在のtestFiles
エントリのfilename
が渡されます。これにより、各テストデータファイルの読み込みが並行して行われます。 -
for i := 0; i != tf.n; i++
ループ: この内部ループは、現在のテストデータファイルからtf.n
で指定された数のテストケースを処理します。以前の固定値(87
)から動的な値に変更されたことで、各ファイルのテストケース実行数を制御できるようになりました。 -
dataReader := <-rc
とif dataReader == nil { break }
: チャネルrc
からio.Reader
を受信し、それをdataReader
変数に格納します。その直後にif dataReader == nil { break }
というチェックが行われます。これは、readDat
関数がチャネルを閉じた場合(例えば、tf.n
が0
でテストケースが一つも実行されない場合など)、<-rc
がnil
を返す可能性があるため、nil
ポインタ参照によるパニックを防ぐための重要なガードです。 -
t.Errorf
内のtf.filename
への変更: テストが失敗した場合のエラーメッセージにおいて、以前はfilename
変数を使用していた箇所がtf.filename
に変更されました。これにより、どのテストデータファイルでエラーが発生したかが、より正確にエラーメッセージに反映されるようになります。 -
未処理テストケースのドレインループ:
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/cl
やgo-review.googlesource.com
では直接検索しても見つかりませんでした。GoのCL番号は通常、これよりも小さい値です。
- 注: このCL番号は非常に古いためか、現在の
参考にした情報源リンク
- Go言語の
testing
パッケージに関する公式ドキュメント: https://pkg.go.dev/testing - Go言語のチャネルに関する公式ドキュメント: https://go.dev/tour/concurrency/2
- Go言語の
io.Reader
インターフェースに関する公式ドキュメント: https://pkg.go.dev/io#Reader - Go言語の
io/ioutil
パッケージ(ioutil.ReadAll
など)に関する公式ドキュメント: https://pkg.go.dev/io/ioutil (Go 1.16以降はio
パッケージに統合) - Go言語における
defer
ステートメント: https://go.dev/tour/flowcontrol/12 - Go言語におけるHTMLパーシング(
golang.org/x/net/html
パッケージなど)に関する一般的な情報。