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

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

このコミットは、Go言語の実験的なHTMLパーサーパッケージ exp/html におけるテストコードの簡素化を目的としています。具体的には、パーサーが全てのテストスイートに合格するようになったため、テスト結果をログファイルに記録し、その合否を追跡する仕組みが不要になったことを受けて、関連するコードとログファイルを削除しています。

コミット

commit d624f0c92281f73879b573213acf736170a68145
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Thu Aug 16 09:31:22 2012 +1000

    exp/html: simplify testing code
    
    Now that the parser passes all tests in the test suite,
    it is no longer necessary to keep track of which tests
    pass and which don't. So remove the testlogs directory
    and the code that uses it.
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6453124

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

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

元コミット内容

exp/html: simplify testing code

Now that the parser passes all tests in the test suite, it is no longer necessary to keep track of which tests pass and which don't. So remove the testlogs directory and the code that uses it.

R=nigeltao CC=golang-dev https://golang.org/cl/6453124

変更の背景

このコミットが行われた背景には、Go言語の実験的なHTMLパーサーである exp/html パッケージの開発段階における進捗があります。初期の開発段階では、パーサーの正確性を検証するために、多数のテストケースに対してパーサーがどのように振る舞うかを詳細に記録し、どのテストが成功し、どのテストが失敗したかを追跡する必要がありました。これは、パーサーのバグを特定し、修正する上で重要なデバッグ情報となります。

しかし、コミットメッセージにある通り、「パーサーがテストスイート内の全てのテストに合格するようになった」というマイルストーンに到達しました。これは、パーサーが十分に安定し、期待通りの動作をするようになったことを意味します。この段階に至ると、個々のテストの合否を詳細にログに記録し続ける必要性が薄れます。むしろ、そのようなログメカニズムは、テストコードを複雑にし、メンテナンスの負担を増やす要因となります。

したがって、このコミットは、パーサーの成熟度に合わせてテストインフラを合理化し、不要な複雑さを排除することを目的としています。これにより、コードベースがクリーンになり、将来的な開発やメンテナンスが容易になります。

前提知識の解説

Go言語の exp/html パッケージ

exp/html は、Go言語で書かれたHTML5のパーサーです。HTMLドキュメントを解析し、DOM(Document Object Model)ツリーのような構造に変換する機能を提供します。ウェブブラウザがHTMLを解釈して表示するのと同様に、プログラムがHTMLコンテンツを構造的に扱うことを可能にします。このパッケージは、Goの標準ライブラリの一部として golang.org/x/net/html に移動する前の実験的な段階のものでした。

HTMLパーシングとテスト

HTMLパーシングは、不完全または不正なHTML入力に対しても堅牢に動作する必要があるため、非常に複雑なタスクです。HTML5の仕様は、エラー処理に関する詳細なルールを定めており、パーサーはこれに従う必要があります。

パーサーのテストでは、以下のような要素が重要になります。

  • テストケース: 様々な種類のHTMLスニペット(正しいもの、不正なもの、エッジケースなど)。
  • 期待される出力: 各テストケースのHTMLがパースされた結果として生成されるべきDOMツリーの構造。
  • 比較メカニズム: 実際のパーサーの出力と期待される出力を比較し、一致するかどうかを判断するロジック。
  • テストログ: 開発段階では、テストの実行結果(成功、失敗、パースのみ成功など)を記録し、デバッグや進捗管理に役立てるためのログファイルが生成されることがあります。

Go言語のテストフレームワーク

Go言語には、標準で testing パッケージが提供されており、これを用いてユニットテストやベンチマークテストを記述します。

  • func TestXxx(t *testing.T): テスト関数は Test で始まり、*testing.T 型の引数を取ります。
  • t.Fatal(err): テストを即座に失敗させ、エラーメッセージを出力します。
  • t.Errorf(format, args...): テストを失敗としてマークしますが、テストの実行は継続します。
  • flag パッケージ: コマンドライン引数を解析するためのパッケージです。テストにおいて、特定の動作(例: ログファイルの更新)を制御するために使用されることがあります。
  • filepath.Glob: 指定されたパターンに一致するファイルパスを見つけるために使用されます。
  • bufio.Reader: バッファリングされたI/O操作を提供し、効率的なファイル読み込みを可能にします。
  • os.Create, os.Open, os.File: ファイルの作成、オープン、およびファイル操作のための関数と型です。
  • fmt.Fscanf, fmt.Fprintf: フォーマットされた入力と出力をファイルに対して行うための関数です。

panicrecover

Go言語におけるエラーハンドリングのメカニズムの一つです。

  • panic: プログラムの異常終了を引き起こします。通常、回復不可能なエラーやプログラミングミスを示すために使用されます。
  • recover: defer 関数内で呼び出されることで、panic からの回復を試みることができます。これにより、プログラムがクラッシュするのを防ぎ、エラーを適切に処理する機会を得られます。

Node, DocumentNode, Parse, ParseFragment, dump

これらは exp/html パッケージ内の主要な要素です。

  • Node: HTMLツリーの各要素(要素、テキスト、コメントなど)を表す構造体です。
  • DocumentNode: HTMLドキュメント全体のルートノードを表す Node のタイプです。
  • Parse(io.Reader) (*Node, error): HTMLの入力ストリームを解析し、DOMツリーを構築する関数です。
  • ParseFragment(io.Reader, *Node) ([]*Node, error): 特定のコンテキストノード(例: <body> タグ内)でHTMLフラグメントを解析する関数です。
  • dump(*Node) (string, error): 構築されたDOMツリーを文字列形式でダンプ(出力)する関数です。これは、テストにおいて期待される出力と比較するために使用されます。

技術的詳細

このコミットは、主に src/pkg/exp/html/parse_test.go ファイルと src/pkg/exp/html/testlogs/ ディレクトリに影響を与えています。

parse_test.go の変更点

  1. updateLogs フラグの削除:

    • var updateLogs = flag.Bool("update-logs", false, "Update the log files that show the test results") の定義が削除されました。
    • これにより、テスト実行時にログファイルを更新するかどうかを制御するコマンドラインオプションがなくなりました。
  2. testLogDir 定数の削除:

    • const testLogDir = "testlogs/" の定義が削除されました。
    • ログファイルが不要になったため、そのディレクトリパスを保持する必要がなくなりました。
  3. parseTestResult 列挙型と関連コードの削除:

    • parseTestResult 型(parseTestFailed, parseTestParseOnly, parseTestPassed)とその String() メソッドが削除されました。
    • これは、テストの合否を詳細に分類し、ログに記録する仕組みが不要になったためです。以前は、パース段階のみ成功し、レンダリングと再パース段階で失敗するケースなどを区別していましたが、パーサーが安定したため、単に成功か失敗かのみを判断すればよくなりました。
  4. TestParser 関数の簡素化:

    • ログファイル (lf, lbr) のオープン、作成、読み書きに関する全てのロジックが削除されました。
    • 以前は、updateLogs フラグに基づいてログファイルを更新するか、既存のログファイルを読み込んで期待される結果と比較する処理が含まれていました。これらの処理が完全に削除され、テストは純粋に testParseCase の結果(エラーの有無)に基づいて合否を判断するようになりました。
    • expectedResult の比較ロジックも削除されました。
  5. testParseCase 関数の変更:

    • 関数のシグネチャが func testParseCase(text, want, context string) (result parseTestResult, err error) から func testParseCase(text, want, context string) (err error) に変更されました。
    • parseTestResult を返す必要がなくなったため、関数はエラーの有無のみを返すようになりました。
    • 関数内の parseTestFailed, parseTestParseOnly, parseTestPassed の返却が、単に err または nil の返却に置き換えられました。
    • 特に、レンダリングと再パースの段階で parseTestParseOnly を設定していた部分が削除され、エラーが発生した場合にのみエラーを返すようになりました。

testlogs ディレクトリ内のログファイルの削除

src/pkg/exp/html/testlogs/ ディレクトリ内にあった多数の .log ファイルが全て削除されました。これらのファイルは、過去のテスト実行結果を記録していたものであり、テストコードの簡素化に伴い不要となりました。削除されたファイルはコミットログに多数リストアップされています。

これらの変更は、パーサーが安定し、テストの合否を詳細に追跡する必要がなくなったという事実を反映しており、テストインフラのオーバーヘッドを削減し、コードベースをよりクリーンに保つことに貢献しています。

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

このコミットのコアとなる変更は、src/pkg/exp/html/parse_test.go ファイル内のテストロジックの削除と簡素化、および src/pkg/exp/html/testlogs/ ディレクトリ内のログファイルの削除です。

src/pkg/exp/html/parse_test.go

--- a/src/pkg/exp/html/parse_test.go
+++ b/src/pkg/exp/html/parse_test.go
@@ -9,7 +9,6 @@ import (
 	"bytes"
 	"errors"
 	"exp/html/atom"
-	"flag"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -21,8 +20,6 @@ import (
 	"testing"
 )
 
-var updateLogs = flag.Bool("update-logs", false, "Update the log files that show the test results")
-
 // readParseTest reads a single test case from r.
 func readParseTest(r *bufio.Reader) (text, want, context string, err error) {
 	line, err := r.ReadSlice('\n')
@@ -202,32 +199,6 @@ func dump(n *Node) (string, error) {
 }
 
 const testDataDir = "testdata/webkit/"
-const testLogDir = "testlogs/"
-
-type parseTestResult int
-
-const (
-	// parseTestFailed indicates that an error occurred during parsing or that
-	// the parse tree did not match the expected result.
-	parseTestFailed parseTestResult = iota
-	// parseTestParseOnly indicates that the first stage of the test (parsing)
-	// passed, but rendering and re-parsing did not.
-	parseTestParseOnly
-	// parseTestPassed indicates that both stages of the test passed.
-	parseTestPassed
-)
-
-func (r parseTestResult) String() string {
-	switch r {
-	case parseTestFailed:
-		return "FAIL"
-	case parseTestParseOnly:
-		return "PARSE"
-	case parseTestPassed:
-		return "PASS"
-	}
-	return "invalid parseTestResult value"
-}
 
 func TestParser(t *testing.T) {
 	testFiles, err := filepath.Glob(testDataDir + "*.dat")
@@ -242,20 +213,6 @@ func TestParser(t *testing.T) {
 		defer f.Close()
 		r := bufio.NewReader(f)
 
-		logName := testLogDir + tf[len(testDataDir):] + ".log"
-		var lf *os.File
-		var lbr *bufio.Reader
-		if *updateLogs {
-			lf, err = os.Create(logName)
-		} else {
-			lf, err = os.Open(logName)
-			lbr = bufio.NewReader(lf)
-		}
-		if err != nil {
-			t.Fatal(err)
-		}
-		defer lf.Close()
-
 		for i := 0; ; i++ {
 			text, want, context, err := readParseTest(r)
 			if err == io.EOF {
@@ -265,46 +213,20 @@ func TestParser(t *testing.T) {
 			\tt.Fatal(err)
 			}
 
-			var expectedResult parseTestResult
-			if !*updateLogs {
-				var expectedText, expectedResultString string
-				_, err = fmt.Fscanf(lbr, "%s %q\n", &expectedResultString, &expectedText)
-				if err != nil {
-					t.Fatal(err)
-				}
-				if expectedText != text {
-					t.Fatalf("Log does not match tests: log has %q, tests have %q", expectedText, text)
-				}
-				switch expectedResultString {
-				case "FAIL":
-					// Skip this test.
-					continue
-				case "PARSE":
-					expectedResult = parseTestParseOnly
-				case "PASS":
-					expectedResult = parseTestPassed
-				default:
-					t.Fatalf("Log has invalid test result: %q", expectedResultString)
-				}
-			}
-
-			result, err := testParseCase(text, want, context)
+			err = testParseCase(text, want, context)
 
-			if *updateLogs {
-				fmt.Fprintf(lf, "%s %q\n", result, text)
-			} else if result < expectedResult {
+			if err != nil {
 				t.Errorf("%s test #%d %q, %s", tf, i, text, err)
 			}
 		}
 	}
 }
 
-// testParseCase tests one test case from the test files. It returns a 
-// parseTestResult indicating how much of the test passed. If the result
-// is not parseTestPassed, it also returns an error that explains the failure.\n// text is the HTML to be parsed, want is a dump of the correct parse tree,\n// and context is the name of the context node, if any.\n-func testParseCase(text, want, context string) (result parseTestResult, err error) {
+// testParseCase tests one test case from the test files. If the test does not
+// pass, it returns an error that explains the failure.
+// text is the HTML to be parsed, want is a dump of the correct parse tree,
+// and context is the name of the context node, if any.
+func testParseCase(text, want, context string) (err error) {
 	defer func() {
 	\tif x := recover(); x != nil {
 	\t\tswitch e := x.(type) {
@@ -320,7 +241,7 @@ func testParseCase(text, want, context string) (result parseTestResult, err erro
 	if context == "" {
 		doc, err = Parse(strings.NewReader(text))
 		if err != nil {
-			return parseTestFailed, err
+			return err
 		}
 	} else {
 		contextNode := &Node{
@@ -330,7 +261,7 @@ func testParseCase(text, want, context string) (result parseTestResult, err erro
 		}
 		nodes, err := ParseFragment(strings.NewReader(text), contextNode)
 		if err != nil {
-			return parseTestFailed, err
+			return err
 		}
 		doc = &Node{
 			Type: DocumentNode,
@@ -342,21 +273,17 @@ func testParseCase(text, want, context string) (result parseTestResult, err erro
 
 	got, err := dump(doc)
 	if err != nil {
-		return parseTestFailed, err
+		return err
 	}
 	// Compare the parsed tree to the #document section.
 	if got != want {
-		return parseTestFailed, fmt.Errorf("got vs want:\n----\n%s----\n%s----", got, want)
+		return fmt.Errorf("got vs want:\n----\n%s----\n%s----", got, want)
 	}
 
 	if renderTestBlacklist[text] || context != "" {
-		return parseTestPassed, nil
+		return nil
 	}
 
-	// Set result so that if a panic occurs during the render and re-parse
-	// the calling function will know that the parsing phase was successful.
-	result = parseTestParseOnly
-
 	// Check that rendering and re-parsing results in an identical tree.
 	pr, pw := io.Pipe()
 	go func() {
@@ -364,17 +291,17 @@ func testParseCase(text, want, context string) (result parseTestResult, err erro
 	}()
 	doc1, err := Parse(pr)
 	if err != nil {
-		return parseTestParseOnly, err
+		return err
 	}
 	got1, err := dump(doc1)
 	if err != nil {
-		return parseTestParseOnly, err
+		return err
 	}
 	if got != got1 {
-		return parseTestParseOnly, fmt.Errorf("got vs got1:\n----\n%s----\n%s----", got, got1)
+		return fmt.Errorf("got vs got1:\n----\n%s----\n%s----", got, got1)
 	}
 
-	return parseTestPassed, nil
+	return nil
 }
 
 // Some test input result in parse trees are not 'well-formed' despite

src/pkg/exp/html/testlogs/ ディレクトリ

このディレクトリ内の全ての .log ファイルが削除されています。コミットログには、adoption01.dat.log から webkit02.dat.log まで、多数のファイルが削除されたことが示されています。

コアとなるコードの解説

上記の差分は、exp/html パッケージのテストスイートがどのように簡素化されたかを明確に示しています。

  1. 不要なインポートとグローバル変数の削除:

    • flag パッケージのインポートと、それに関連する updateLogs グローバル変数が削除されました。これは、テストログの更新機能が完全に廃止されたためです。
    • testLogDir 定数も削除され、ログディレクトリへの参照がなくなりました。
  2. テスト結果の分類の廃止:

    • parseTestResult 型(parseTestFailed, parseTestParseOnly, parseTestPassed)が削除されました。これは、パーサーが十分に安定し、テストが「失敗」「パースのみ成功」「完全に成功」といった詳細な状態を区別する必要がなくなったことを意味します。今後は、テストは単に成功したか、エラーが発生したか、の二値で判断されます。
  3. TestParser 関数のロギングロジックの削除:

    • TestParser 関数から、ログファイルを開いたり、読み込んだり、書き込んだりする全てのコードが削除されました。以前は、updateLogs フラグが設定されている場合はログファイルを更新し、そうでない場合は既存のログファイルとテスト結果を比較していました。この複雑なロジックが完全に削除され、テストコードが大幅に簡素化されました。
    • testParseCase の戻り値が parseTestResult から error に変更されたことに伴い、TestParser 内での testParseCase の呼び出しとエラーハンドリングも簡素化されています。
  4. testParseCase 関数の簡素化:

    • testParseCase 関数のシグネチャが変更され、parseTestResult を返さなくなりました。これにより、関数はエラーが発生した場合にのみエラーを返し、成功した場合は nil を返すという、Go言語の一般的なエラーハンドリングパターンに沿うようになりました。
    • 関数内部の return parseTestFailed, errreturn parseTestParseOnly, err といった記述が、単に return err に変更されました。
    • 特に注目すべきは、レンダリングと再パースの段階で result = parseTestParseOnly と設定していた行が削除されたことです。これは、この中間状態を追跡する必要がなくなったことを明確に示しています。

これらの変更は、exp/html パーサーが成熟し、そのテストスイートがよりシンプルで効率的なものになったことを示しています。テストの安定性が向上したため、詳細なログ記録や複雑な合否判定ロジックが不要になったという、開発プロセスの自然な進化を反映しています。

関連リンク

参考にした情報源リンク