[インデックス 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
: フォーマットされた入力と出力をファイルに対して行うための関数です。
panic
と recover
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
の変更点
-
updateLogs
フラグの削除:var updateLogs = flag.Bool("update-logs", false, "Update the log files that show the test results")
の定義が削除されました。- これにより、テスト実行時にログファイルを更新するかどうかを制御するコマンドラインオプションがなくなりました。
-
testLogDir
定数の削除:const testLogDir = "testlogs/"
の定義が削除されました。- ログファイルが不要になったため、そのディレクトリパスを保持する必要がなくなりました。
-
parseTestResult
列挙型と関連コードの削除:parseTestResult
型(parseTestFailed
,parseTestParseOnly
,parseTestPassed
)とそのString()
メソッドが削除されました。- これは、テストの合否を詳細に分類し、ログに記録する仕組みが不要になったためです。以前は、パース段階のみ成功し、レンダリングと再パース段階で失敗するケースなどを区別していましたが、パーサーが安定したため、単に成功か失敗かのみを判断すればよくなりました。
-
TestParser
関数の簡素化:- ログファイル (
lf
,lbr
) のオープン、作成、読み書きに関する全てのロジックが削除されました。 - 以前は、
updateLogs
フラグに基づいてログファイルを更新するか、既存のログファイルを読み込んで期待される結果と比較する処理が含まれていました。これらの処理が完全に削除され、テストは純粋にtestParseCase
の結果(エラーの有無)に基づいて合否を判断するようになりました。 expectedResult
の比較ロジックも削除されました。
- ログファイル (
-
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
パッケージのテストスイートがどのように簡素化されたかを明確に示しています。
-
不要なインポートとグローバル変数の削除:
flag
パッケージのインポートと、それに関連するupdateLogs
グローバル変数が削除されました。これは、テストログの更新機能が完全に廃止されたためです。testLogDir
定数も削除され、ログディレクトリへの参照がなくなりました。
-
テスト結果の分類の廃止:
parseTestResult
型(parseTestFailed
,parseTestParseOnly
,parseTestPassed
)が削除されました。これは、パーサーが十分に安定し、テストが「失敗」「パースのみ成功」「完全に成功」といった詳細な状態を区別する必要がなくなったことを意味します。今後は、テストは単に成功したか、エラーが発生したか、の二値で判断されます。
-
TestParser
関数のロギングロジックの削除:TestParser
関数から、ログファイルを開いたり、読み込んだり、書き込んだりする全てのコードが削除されました。以前は、updateLogs
フラグが設定されている場合はログファイルを更新し、そうでない場合は既存のログファイルとテスト結果を比較していました。この複雑なロジックが完全に削除され、テストコードが大幅に簡素化されました。testParseCase
の戻り値がparseTestResult
からerror
に変更されたことに伴い、TestParser
内でのtestParseCase
の呼び出しとエラーハンドリングも簡素化されています。
-
testParseCase
関数の簡素化:testParseCase
関数のシグネチャが変更され、parseTestResult
を返さなくなりました。これにより、関数はエラーが発生した場合にのみエラーを返し、成功した場合はnil
を返すという、Go言語の一般的なエラーハンドリングパターンに沿うようになりました。- 関数内部の
return parseTestFailed, err
やreturn parseTestParseOnly, err
といった記述が、単にreturn err
に変更されました。 - 特に注目すべきは、レンダリングと再パースの段階で
result = parseTestParseOnly
と設定していた行が削除されたことです。これは、この中間状態を追跡する必要がなくなったことを明確に示しています。
これらの変更は、exp/html
パーサーが成熟し、そのテストスイートがよりシンプルで効率的なものになったことを示しています。テストの安定性が向上したため、詳細なログ記録や複雑な合否判定ロジックが不要になったという、開発プロセスの自然な進化を反映しています。
関連リンク
- Go CL (Change List) 6453124: https://golang.org/cl/6453124
参考にした情報源リンク
- Go言語の
testing
パッケージ: https://pkg.go.dev/testing - Go言語の
flag
パッケージ: https://pkg.go.dev/flag - Go言語の
filepath
パッケージ: https://pkg.go.dev/path/filepath - Go言語の
bufio
パッケージ: https://pkg.go.dev/bufio - Go言語の
os
パッケージ: https://pkg.go.dev/os - Go言語の
fmt
パッケージ: https://pkg.go.dev/fmt - Go言語の
panic
とrecover
について: https://go.dev/blog/defer-panic-and-recover - HTML5仕様 (W3C): https://www.w3.org/TR/html5/
- Go言語の
golang.org/x/net/html
パッケージ (現在のexp/html
の後継): https://pkg.go.dev/golang.org/x/net/html