[インデックス 10468] ファイルの概要
このコミットは、Go言語の標準ライブラリ html
パッケージ内のパーサーテストインフラストラクチャのリファクタリングに関するものです。具体的には、テストケースの読み込み方法が、チャネルとパイプを使用する複雑なメカニズムから、より直接的なファイル読み込み関数へと変更されました。
コミット
- コミットハッシュ:
05d8d112fe4e78273d2ca0fe7d388a76d9e02407
- Author: Andrew Balholm andybalholm@gmail.com
- Date: Sun Nov 20 22:42:28 2011 +1100
- コミットメッセージ:
html: refactor parse test infrastructure My excuse for doing this is that test cases with newlines in them didn't work. But instead of just fixing that, I rearranged everything in parse_test.go to use fewer channels and pipes, and just call a straightforward function to read test cases from a file. R=nigeltao CC=golang-dev https://golang.org/cl/5410049
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/05d8d112fe4e78273d2ca0fe7d388a76d9e02407
元コミット内容
html: refactor parse test infrastructure
My excuse for doing this is that test cases with newlines in them didn't
work. But instead of just fixing that, I rearranged everything in
parse_test.go to use fewer channels and pipes, and just call a
straightforward function to read test cases from a file.
R=nigeltao
CC=golang-dev
https://golang.org/cl/5410049
変更の背景
この変更の主な背景は、既存のテストインフラストラクチャが、テストケース内に改行が含まれている場合に正しく機能しないという問題があったことです。コミットメッセージによると、この問題を修正するだけでなく、parse_test.go
内の全体的なテスト構造を簡素化し、チャネルとパイプの使用を減らし、より直接的なファイル読み込み関数を使用するように再編成されました。これにより、テストコードの可読性と保守性が向上し、将来的なテストケースの追加が容易になることが期待されます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の概念とテストに関する知識が必要です。
io
パッケージ: Go言語におけるI/Oプリミティブを提供するパッケージです。io.Reader
やio.Writer
といったインターフェースが定義されており、データの読み書きの抽象化に用いられます。io.Pipe
:io.PipeReader
とio.PipeWriter
のペアを生成し、メモリ内でデータをストリームとしてやり取りするためのメカニズムを提供します。一方の端に書き込まれたデータは、もう一方の端から読み出すことができます。これは、並行処理においてデータの受け渡しを行う際によく用いられます。bufio.Reader
: バッファリングされたI/Oを提供する構造体です。これにより、ディスクI/Oの回数を減らし、効率的な読み込みが可能になります。ReadSlice
などのメソッドは、特定のデリミタ(例: 改行)までデータを読み込むのに便利です。testing
パッケージ: Go言語の標準テストフレームワークです。TestXxx
という形式の関数を定義することで、テストを実行できます。*testing.T
はテストの状態を管理し、エラー報告などの機能を提供します。- HTMLパーシング: HTMLドキュメントを解析し、その構造をツリー形式(DOMツリーなど)で表現するプロセスです。テストでは、特定のHTML入力が期待されるパースツリーに変換されることを検証します。
- テストデータファイル (
.dat
ファイル): このコミットでは、testdata/webkit/
ディレクトリ内の.dat
ファイルからテストケースを読み込んでいます。これらのファイルは、通常、入力HTML、期待されるエラー、期待されるパースツリーなどのセクションに分かれており、特定のフォーマットに従っています。コミットメッセージやコードの変更から、これらのファイルが#data
,#errors
,#document
といったセクションマーカーで区切られていることがわかります。
技術的詳細
このコミットの主要な技術的変更点は、src/pkg/html/parse_test.go
におけるテストデータの読み込みロジックの根本的な見直しです。
変更前:
変更前は、readDat
関数が io.Reader
のチャネル (chan io.Reader
) を使用して、テストデータファイルから各セクション(#data
, #errors
, #document
)を個別の io.Reader
としてストリーム処理していました。io.Pipe
を用いて、ファイルから読み込んだデータをパイプに書き込み、そのパイプの読み込み側をチャネルに送信するという、やや複雑なメカニズムが採用されていました。これにより、各セクションが独立したストリームとして扱われ、ioutil.ReadAll
で読み込まれていました。しかし、このアプローチは、特に改行を含むテストケースで問題を引き起こしていました。
変更後:
変更後は、readParseTest
という新しい関数が導入されました。この関数は *bufio.Reader
を引数に取り、単一のテストケース(HTMLテキストと期待されるパースツリーのダンプ)を直接読み込んで返します。
readParseTest
は以下の手順で動作します。
#data
セクションの開始を期待し、そこからHTMLデータを読み込みます。#errors
セクションをスキップします。#document
セクションの開始を期待し、そこから期待されるパースツリーのダンプを読み込みます。
この新しいアプローチでは、io.Pipe
やチャネルの使用が完全に排除され、bufio.Reader
の ReadSlice
メソッドを使って、ファイルから直接行単位でデータを読み込むようになりました。これにより、テストデータの読み込みロジックが大幅に簡素化され、改行を含むテストケースの問題も解決されました。
TestParser
関数も、この新しい readParseTest
関数を使用するように変更されました。以前はチャネルから io.Reader
を受け取っていましたが、変更後は readParseTest
を直接呼び出して text
と want
の文字列を取得するようになりました。これにより、テストコード全体の流れがより直線的で理解しやすくなっています。
また、dumpLevel
関数内の TextNode
のダンプ形式が %q
から \"%s\"
に変更されています。これは、テキストノードのデータが引用符で囲まれて出力されるようにするための調整であり、テストの比較ロジックに影響を与える可能性があります。
コアとなるコードの変更箇所
src/pkg/html/parse_test.go
ファイルにおける主要な変更箇所は以下の通りです。
削除されたコード(旧 readDat
関数と関連ヘルパー):
-func pipeErr(err error) io.Reader {
- pr, pw := io.Pipe()
- pw.CloseWithError(err)
- return pr
-}
-
-func readDat(filename string, c chan io.Reader) {
- defer close(c)
- f, err := os.Open("testdata/webkit/" + filename)
-// readParseTest reads a single test case from r.
-func readParseTest(r *bufio.Reader) (text, want string, err error) {
- line, err := r.ReadSlice('\n')
- if err != nil {
- c <- pipeErr(err)
- return
- }
- defer f.Close()
-
- // Loop through the lines of the file. Each line beginning with "#" denotes
- // a new section, which is returned as a separate io.Reader.
- r := bufio.NewReader(f)
- var pw *io.PipeWriter
- for {
- line, err := r.ReadSlice('\n')
- if err != nil {
- if pw != nil {
- pw.CloseWithError(err)
- pw = nil
- } else {
- c <- pipeErr(err)
- }
- return
- }
- if len(line) == 0 {
- continue
- }
- if line[0] == '#' {
- if pw != nil {
- pw.Close()
- }
- var pr *io.PipeReader
- pr, pw = io.Pipe()
- c <- pr
- continue
- }
- if line[0] != '|' {
- // Strip the trailing '\n'.
- line = line[:len(line)-1]
- }
- if pw != nil {
- if _, err := pw.Write(line); err != nil {
- pw.CloseWithError(err)
- pw = nil
- }
- }
- }
追加されたコード(新 readParseTest
関数):
+// readParseTest reads a single test case from r.
+func readParseTest(r *bufio.Reader) (text, want string, err error) {
+ line, err := r.ReadSlice('\n')
+ if err != nil {
+ return "", "", err
+ }
+ var b []byte
+
+ // Read the HTML.
+ if string(line) != "#data\n" {
+ return "", "", fmt.Errorf(`got %q want "#data\n"`, line)
+ }
+ for {
+ line, err = r.ReadSlice('\n')
+ if err != nil {
+ return "", "", err
+ }
+ if line[0] == '#' {
+ break
+ }
+ b = append(b, line...)
+ }
+ text = strings.TrimRight(string(b), "\n")
+ b = b[:0]
+
+ // Skip the error list.
+ if string(line) != "#errors\n" {
+ return "", "", fmt.Errorf(`got %q want "#errors\n"`, line)
+ }
+ for {
+ line, err = r.ReadSlice('\n')
+ if err != nil {
+ return "", "", err
+ }
+ if line[0] == '#' {
+ break
+ }
+ }
+
+ // Read the dump of what the parse tree should be.
+ if string(line) != "#document\n" {
+ return "", "", fmt.Errorf(`got %q want "#document\n"`, line)
+ }
+ for {
+ line, err = r.ReadSlice('\n')
+ if err != nil && err != io.EOF {
+ return "", "", err
+ }
+ if len(line) == 0 || len(line) == 1 && line[0] == '\n' {
+ break
+ }
+ b = append(b, line...)
+ }
+ return text, string(b), nil
+}
TestParser
関数の変更:
TestParser
関数内で、テストデータの読み込み部分が readDat
から readParseTest
に変更されています。
for _, tf := range testFiles {
- rc := make(chan io.Reader)
- go readDat(tf.filename, rc)
+ f, err := os.Open("testdata/webkit/" + tf.filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer f.Close()
+ r := bufio.NewReader(f)
for i := 0; i != tf.n; i++ {
- // Parse the #data section.
- dataReader := <-rc
- if dataReader == nil {
+ text, want, err := readParseTest(r)
+ if err == io.EOF && tf.n == -1 {
break
}
- b, err := ioutil.ReadAll(dataReader)
if err != nil {
t.Fatal(err)
}
- text := string(b)
doc, err := Parse(strings.NewReader(text))
if err != nil {
t.Fatal(err)
@@ -159,16 +160,8 @@ func TestParser(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- // Skip the #error section.
- if _, err := io.Copy(ioutil.Discard, <-rc); err != nil {
- t.Fatal(err)
- }
// Compare the parsed tree to the #document section.
- b, err = ioutil.ReadAll(<-rc)
- if err != nil {
- t.Fatal(err)
- }
- if want := string(b); got != want {
+ if got != want {
t.Errorf("%s test #%d %q, got vs want:\n----\n%s----\n%s----", tf.filename, i, text, got, want)
continue
}
@@ -193,12 +186,6 @@ func TestParser(t *testing.T) {
continue
}
}
- // Drain any untested cases for the test file.
- for r := range rc {
- if _, err := ioutil.ReadAll(r); err != nil {
- t.Fatal(err)
- }
- }
}
}
dumpLevel
関数の変更:
TextNode
のダンプ形式が変更されています。
case TextNode:
- fmt.Fprintf(w, "%q", n.Data)
+ fmt.Fprintf(w, `\"%s\"`, n.Data)
case CommentNode:
fmt.Fprintf(w, "<!-- %s -->", n.Data)
case DoctypeNode:
コアとなるコードの解説
このコミットの核心は、readParseTest
関数の導入と、それによって可能になった TestParser
関数の簡素化です。
readParseTest
関数:
この関数は、*bufio.Reader
を介してテストデータファイルから単一のテストケースを効率的に読み込む責任を負います。
r.ReadSlice('\n')
を使用して行単位でデータを読み込みます。これにより、改行を含むテストケースの処理が容易になります。#data
,#errors
,#document
といったセクションマーカーを厳密にチェックし、期待されるフォーマットに従っていることを確認します。もしフォーマットが異なる場合、fmt.Errorf
を用いてエラーを返します。- HTMLデータと期待されるドキュメントツリーのダンプをバイトスライス
b
に蓄積し、最終的にstring
に変換して返します。 strings.TrimRight(string(b), "\n")
を使用して、読み込んだHTMLデータの末尾の改行を削除しています。これは、テストデータの整形に役立ちます。
TestParser
関数の変更:
- 以前は
readDat
関数がゴルーチンで実行され、チャネルを通じてio.Reader
を送信していましたが、この複雑な並行処理が不要になりました。 - 代わりに、
os.Open
でテストデータファイルを開き、そのファイルからbufio.NewReader
を作成します。 - ループ内で
readParseTest(r)
を直接呼び出すことで、各テストケースのHTMLデータ (text
) と期待されるパースツリーのダンプ (want
) を取得します。 io.Copy(ioutil.Discard, <-rc)
のような、エラーセクションをスキップするための冗長なコードが削除されました。readParseTest
関数内でエラーセクションが自動的にスキップされるため、TestParser
はよりクリーンになりました。- 最終的な比較は
if got != want
となり、以前のようにioutil.ReadAll
で再度データを読み込む必要がなくなりました。
これらの変更により、テストデータの読み込みロジックがより直接的で理解しやすくなり、テストコード全体の保守性が向上しています。特に、改行を含むテストケースの問題が解決されたことは、テストの堅牢性を高める上で重要です。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/05d8d112fe4e78273d2ca0fe7d388a76d9e02407
- Gerrit Code Review (Go Project): https://golang.org/cl/5410049
参考にした情報源リンク
- Go言語の
io
パッケージドキュメント: https://pkg.go.dev/io - Go言語の
bufio
パッケージドキュメント: https://pkg.go.dev/bufio - Go言語の
testing
パッケージドキュメント: https://pkg.go.dev/testing - Go言語の
strings
パッケージドキュメント: https://pkg.go.dev/strings - Go言語の
fmt
パッケージドキュメント: https://pkg.go.dev/fmt - Go言語の
os
パッケージドキュメント: https://pkg.go.dev/os - HTMLパーシングの基本概念 (一般的な情報源): https://developer.mozilla.org/ja/docs/Glossary/HTML_parsing (MDN Web Docs)