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

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

このコミットは、Go言語の実験的なHTMLパーサーパッケージである exp/html のテストメカニズムを改善するものです。具体的には、src/pkg/exp/html/parse_test.go ファイルが変更され、より包括的なテスト実行と結果のロギング機能が導入されました。これにより、HTMLパーサーの開発者が、個々のバグ修正に限定されず、パーサー全体の健全性をより容易に評価できるようになります。

コミット

このコミットは、Go言語の exp/html パッケージにおけるテストの網羅性を大幅に向上させることを目的としています。これまでのテストは限定的であり、特定の失敗するテストケースの修正に開発が集中しがちでした。この変更により、すべてのテストケースを実行し、その結果を詳細なログとして出力する機能が追加されました。また、ログが更新されていない場合でも、既存のログに基づいて回帰テストを実行できるようになり、開発の柔軟性を保ちつつ、品質を維持することが可能になりました。

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

https://github.com/golang/go/commit/9a6cef8bbf1d62cbeb786f7a3eb9c009ab190ec9

元コミット内容

commit 9a6cef8bbf1d62cbeb786f7a3eb9c009ab190ec9
Author: Andrew Balholm <andybalholm@gmail.com>
Date:   Tue Apr 17 17:17:22 2012 +1000

    exp/html: more comprehensive tests
    
    Currently, the html package only runs a limited subset of the tests
    in the testdata directory. This tends to limit development of the
    parser to fixing the bug that causes the first failing test.
    
    This CL gives it the ability to run all the tests and produce a
    log showing the status of each test. (It does it when tests are run with
    'go test --update-logs') The status is listed as PASS, FAIL, or PARSE
    (PARSE means that parsing produced the correct tree, but rendering and
    re-parsing does not produce the same tree).
    
    When 'go test' is run without --update-logs, it runs the tests marked
    'PASS' in the logs (and the parsing portion of the tests marked 'PARSE').
    Thus it will fail if there has been a regression since the last
    time the logs were updated.
    
    My goal for this CL is to allow develoment of the html package to
    be less test-driven, while still having the advantages of regression
    tests. In other words, one can work on any portion of the parser
    and quickly see whether he is breaking things or improving them.
    
    Current statistics of the tests:
    $ grep ^PASS *.log|wc -l
            1017
    $ grep ^PARSE *.log|wc -l
              46
    $ grep ^FAIL *.log|wc -l
             181
    
    R=nigeltao
    CC=golang-dev
    https://golang.org/cl/6031049

変更の背景

この変更の背景には、Go言語の exp/html パッケージのテストプロセスにおける課題がありました。以前は、testdata ディレクトリ内のテストケースのごく一部しか実行されていませんでした。この限定的なテスト実行は、開発者がパーサーのバグを修正する際に、最初に失敗するテストケースにのみ焦点を当ててしまい、パーサー全体の健全性や他の部分への影響を見落とす可能性がありました。

開発者は、特定のバグを修正するたびに、その修正が他の既存の機能に悪影響を与えていないか(回帰バグが発生していないか)を確認する必要があります。しかし、テストの網羅性が低いと、この回帰テストが不十分になり、開発効率とコード品質の両方に悪影響を及ぼしていました。

このコミットの目的は、HTMLパッケージの開発を「テスト駆動」から解放しつつ、回帰テストの利点を維持することです。つまり、開発者はパーサーのどの部分でも作業でき、その変更が既存の機能を壊していないか、あるいは改善しているかを迅速に確認できるようになることを目指しています。

前提知識の解説

HTMLパーシング

HTMLパーシングとは、HTMLドキュメント(テキスト形式のマークアップ)を読み込み、それをコンピュータが理解しやすい構造(通常はDOMツリーと呼ばれるツリー構造)に変換するプロセスです。このプロセスは、ウェブブラウザがウェブページを表示したり、検索エンジンがウェブコンテンツをインデックス化したりする上で不可欠です。HTMLは非常に柔軟な(そしてしばしば不完全な)マークアップ言語であるため、パーサーはエラー耐性があり、不正なHTMLでも適切に処理できる必要があります。

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

exp/html は、Go言語の標準ライブラリの一部として提供されているHTMLパーサーです。exp というプレフィックスが示す通り、これは実験的なパッケージであり、将来的に安定版の html パッケージとして昇格されることを目指していました。このパッケージは、WHATWG (Web Hypertext Application Technology Working Group) のHTML Living Standardに準拠したパーシングルールを実装しており、ウェブブラウザがHTMLを解析するのと同じ方法でHTMLを解析することを目指しています。

回帰テスト (Regression Testing)

回帰テストとは、ソフトウェアの変更(バグ修正、新機能追加など)が、既存の機能に予期せぬ副作用(回帰バグ)を引き起こしていないことを確認するために行われるテストです。このコミットでは、HTMLパーサーの変更が、以前は正しく動作していたパーシング結果を壊していないことを確認するために、包括的な回帰テストが重要視されています。

Go言語の go test コマンド

go test は、Go言語のテストを実行するためのコマンドです。Goのテストは、_test.go で終わるファイルに記述され、Test で始まる関数として定義されます。go test コマンドは、これらのテスト関数を自動的に発見し、実行します。

コマンドラインフラグ (flag パッケージ)

Go言語の flag パッケージは、コマンドライン引数を解析するための標準ライブラリです。このコミットでは、--update-logs という新しいフラグが導入されており、go test コマンドにこのフラグを渡すことで、テストの動作を変更できるようになっています。

技術的詳細

このコミットは、exp/html パッケージのテストフレームワークに2つの主要な機能を追加します。

  1. 全テストケースの実行とログ生成 (--update-logs フラグ):

    • go test --update-logs コマンドを実行すると、testdata ディレクトリ内のすべての .dat テストファイルが読み込まれます。
    • 各テストケースについて、HTMLパーシングが実行され、その結果が期待されるDOMツリーと比較されます。
    • テスト結果は、PASSFAILPARSE のいずれかのステータスで記録されます。
      • PASS: パーシングが成功し、期待されるDOMツリーと完全に一致し、さらにレンダリングと再パーシングを行っても同じツリーが生成される場合。
      • FAIL: パーシング中にエラーが発生したか、パーシング結果が期待されるDOMツリーと一致しない場合。
      • PARSE: パーシング自体は成功し、期待されるDOMツリーと一致するが、その結果をレンダリングして再度パーシングすると、元のツリーと異なるツリーが生成される場合。これは、パーサーがHTMLを正しく解釈しているものの、その後の処理(例えば、DOMツリーのシリアライズ)に問題がある可能性を示唆します。
    • これらのステータスは、各テストファイルに対応する .log ファイル(例: testdata/webkit/adoption01.dat に対応する testlogs/adoption01.dat.log)に記録されます。
  2. ログに基づいた回帰テスト (通常実行):

    • go test (フラグなし) コマンドを実行すると、テストは .log ファイルに記録された情報に基づいて実行されます。
    • 具体的には、ログで PASS とマークされたテストケースは完全に実行されます。
    • PARSE とマークされたテストケースは、パーシング部分のみが実行されます。これは、レンダリングと再パーシングのフェーズで問題がある可能性のあるテストを、パーシングの回帰テストから除外するためです。
    • FAIL とマークされたテストケースはスキップされます。
    • このメカニズムにより、ログが最後に更新されてから回帰が発生した場合(つまり、以前 PASS または PARSE だったテストが FAIL になった場合)、テストが失敗するようになります。これにより、開発者は既存の機能が壊れていないことを確認できます。

この新しいテストフレームワークは、開発者がパーサーの特定のバグ修正に集中するのではなく、パーサー全体の改善に注力できるようにすることを目的としています。--update-logs を使用してログを定期的に更新することで、パーサーの現在の状態を正確に反映した回帰テストスイートを維持できます。

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

このコミットにおけるコアとなるコードの変更は、主に src/pkg/exp/html/parse_test.go ファイルに集中しています。

  1. flag パッケージの導入と --update-logs フラグの追加:

    import (
        // ...
        "flag"
        // ...
    )
    
    var updateLogs = flag.Bool("update-logs", false, "Update the log files that show the test results")
    

    これにより、go test コマンドに --update-logs というブール型フラグを追加できるようになりました。

  2. parseTestResult 型の定義:

    type parseTestResult int
    
    const (
        parseTestFailed parseTestResult = iota
        parseTestParseOnly
        parseTestPassed
    )
    
    func (r parseTestResult) String() string {
        // ...
    }
    

    テスト結果の状態 (FAIL, PARSE, PASS) を表現するための列挙型が導入されました。

  3. TestParser 関数の大幅な変更:

    • filepath.Glob(testDataDir + "*.dat") を使用して、testdata/webkit/ ディレクトリ内のすべての .dat ファイルを動的に読み込むようになりました。これにより、新しいテストファイルが追加されても、自動的にテスト対象に含まれるようになります。
    • テストログファイル (.log 拡張子) の作成または読み込みロジックが追加されました。--update-logs フラグが true の場合はログファイルが作成され、false の場合は既存のログファイルが読み込まれます。
    • テストループが変更され、各テストケースの実行前にログファイルから期待される結果を読み込むか、テスト結果をログファイルに書き込むようになりました。
    • testParseCase 関数が導入され、個々のテストケースの解析と検証ロジックが分離されました。
  4. testParseCase 関数の追加:

    func testParseCase(text, want, context string) (result parseTestResult, err error) {
        // ...
    }
    

    この新しい関数は、単一のHTMLパーシングテストケースを実行し、その結果を parseTestResult 型で返します。この関数は、以下のステップを実行します。

    • HTMLテキストを解析し、期待されるDOMツリーと比較します。
    • 解析結果が期待と異なる場合、parseTestFailed を返します。
    • 解析が成功した場合、さらにレンダリングと再パーシングを行い、その結果が元のツリーと一致するかを検証します。
    • レンダリングと再パーシングが成功した場合、parseTestPassed を返します。
    • レンダリングと再パーシングが失敗した場合、parseTestParseOnly を返します。
  5. 多数の新しいテストログファイルの追加: src/pkg/exp/html/testlogs/ ディレクトリ以下に、既存の testdata ファイルに対応する多数の .log ファイルが新規追加されています。これらは、--update-logs フラグが導入された際に生成された初期のテスト結果ログです。

コアとなるコードの解説

parse_test.go の主要な変更点は、テストの実行方法と結果の管理方法を根本的に変えたことです。

以前は、TestParser 関数内にハードコードされたテストファイルのリストがあり、その中から一部のテストケースのみを実行していました。このコミットでは、filepath.Glob(testDataDir + "*.dat") を使用することで、testdata/webkit/ ディレクトリ内のすべての .dat ファイルを動的に発見し、テスト対象とします。これにより、新しいテストデータが追加された際に、テストコードを変更することなく自動的にテストに含めることができるようになりました。

最も重要な変更は、--update-logs フラグの導入と、それに関連するテスト結果のロギングメカニズムです。

  • go test --update-logs の実行時:

    • TestParser 関数は、各 .dat テストファイルに対応する .log ファイルを testlogs/ ディレクトリに作成します。
    • 各テストケースが testParseCase 関数によって実行され、その結果 (PASS, FAIL, PARSE) と元のHTMLテキストが .log ファイルに書き込まれます。
    • testParseCase 関数は、HTMLの解析だけでなく、解析されたDOMツリーを再度HTMLにレンダリングし、そのHTMLを再解析して元のツリーと一致するかどうかも検証します。これにより、パーサーの出力が安定しているかどうかも確認できます。
  • go test (フラグなし) の実行時:

    • TestParser 関数は、既存の .log ファイルを読み込みます。
    • 各テストケースについて、ログファイルに記録された期待される結果 (expectedResult) と元のHTMLテキストを読み取ります。
    • expectedResultFAIL の場合、そのテストケースはスキップされます。これは、既知の失敗テストを毎回実行しないことで、テスト時間を短縮し、開発者が新しい回帰に集中できるようにするためです。
    • expectedResultPASS または PARSE の場合、testParseCase 関数が実行されます。
    • testParseCase の実際の実行結果 (result) が expectedResult よりも悪い場合(例: ログでは PASS だったが、今回は FAIL になった場合)、t.Errorf が呼び出され、テストが失敗します。これにより、回帰バグが検出されます。

このメカニズムにより、開発者は go test --update-logs を実行してパーサーの現在の状態を「スナップショット」としてログに記録し、その後は通常の go test を実行することで、そのスナップショットからの回帰を効率的に検出できるようになります。これにより、開発者はパーサーの特定のバグ修正に集中するのではなく、パーサー全体の改善に注力できるようになります。

関連リンク

参考にした情報源リンク