[インデックス 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パッケージなど)に関する一般的な情報。