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

[インデックス 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.Readerio.Writer といったインターフェースが定義されており、データの読み書きの抽象化に用いられます。
  • io.Pipe: io.PipeReaderio.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 は以下の手順で動作します。

  1. #data セクションの開始を期待し、そこからHTMLデータを読み込みます。
  2. #errors セクションをスキップします。
  3. #document セクションの開始を期待し、そこから期待されるパースツリーのダンプを読み込みます。

この新しいアプローチでは、io.Pipe やチャネルの使用が完全に排除され、bufio.ReaderReadSlice メソッドを使って、ファイルから直接行単位でデータを読み込むようになりました。これにより、テストデータの読み込みロジックが大幅に簡素化され、改行を含むテストケースの問題も解決されました。

TestParser 関数も、この新しい readParseTest 関数を使用するように変更されました。以前はチャネルから io.Reader を受け取っていましたが、変更後は readParseTest を直接呼び出して textwant の文字列を取得するようになりました。これにより、テストコード全体の流れがより直線的で理解しやすくなっています。

また、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 で再度データを読み込む必要がなくなりました。

これらの変更により、テストデータの読み込みロジックがより直接的で理解しやすくなり、テストコード全体の保守性が向上しています。特に、改行を含むテストケースの問題が解決されたことは、テストの堅牢性を高める上で重要です。

関連リンク

参考にした情報源リンク