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

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

コミット

commit 280eb703a2d7e2f9630755048d13f259945743e5
Author: Dmitriy Vyukov <dvyukov@google.com>
Date:   Tue Jun 24 17:19:10 2014 -0700

    regexp: skip TestOnePassCutoff in short mode
    Runs for 4 seconds on my mac.
    Also this is the only test that times out on freebsd in -race mode.
    
    R=golang-codereviews, bradfitz
    CC=golang-codereviews
    https://golang.org/cl/110150045

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

https://github.com/golang/go/commit/280eb703a2d7e2f9630755048d13f259945743e5

元コミット内容

regexp: skip TestOnePassCutoff in short mode
Runs for 4 seconds on my mac.
Also this is the only test that times out on freebsd in -race mode.

変更の背景

このコミットの背景には、Go言語の標準ライブラリであるregexpパッケージのテストスイートにおけるパフォーマンスと安定性の問題があります。具体的には、TestOnePassCutoffというテストケースが、特定の環境(コミットメッセージによるとMac上で4秒、FreeBSDの-raceモードでタイムアウト)で実行時間が長く、継続的インテグレーション(CI)システムにおいて問題を引き起こしていました。

CIシステムでは、テストの実行時間は非常に重要です。テストスイート全体の実行時間が長くなると、開発サイクルが遅延し、フィードバックループが長くなります。また、特定のテストが不安定にタイムアウトすることは、CIの信頼性を損ない、開発者が実際のバグとテストの不安定性を区別するのを困難にします。

このTestOnePassCutoffは、以前のissue 7608で報告されたスタックオーバーフローの問題を修正した後に導入されたテストであり、正規表現エンジンの特定の最適化パス(one-pass compilation)におけるカットオフ挙動を検証するためのものです。しかし、そのテストケースが非常に複雑な正規表現パターンを使用しているため、実行に時間がかかっていました。

このコミットは、開発者が日常的に実行する高速なテスト(go test -short)からこの時間のかかるテストを除外することで、開発体験を向上させ、CIの安定性を確保することを目的としています。

前提知識の解説

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

Go言語には、標準で強力なテストフレームワークが組み込まれています。テストファイルは通常、テスト対象のGoファイルと同じディレクトリに_test.goというサフィックスを付けて配置されます。テスト関数はTestで始まり、*testing.T型の引数を取ります。

go testコマンド

go testコマンドは、Goプロジェクトのテストを実行するための主要なツールです。

  • go test: デフォルトでは、すべてのテストを実行します。
  • go test -short: このフラグは、テストの実行時間を短縮するために使用されます。テストコード内でtesting.Short()関数を呼び出すことで、このフラグが設定されているかどうかをプログラム的にチェックし、時間のかかるテストをスキップすることができます。これは、開発者がローカルで素早くテストを実行したい場合や、CIシステムで高速なビルドとテストのサイクルを維持したい場合に非常に有用です。
  • go test -race: このフラグは、データ競合(data race)を検出するための競合検出器(race detector)を有効にしてテストを実行します。データ競合は並行処理における一般的なバグであり、検出が困難なため、このツールはGoの並行プログラミングにおいて非常に重要です。競合検出器は実行時に追加のオーバーヘッドを発生させるため、テストの実行時間が長くなる傾向があります。

testing.T.Skip()

*testing.T型には、テストの実行をスキップするためのメソッドがいくつか用意されています。

  • t.Skip(args ...interface{}): このメソッドは、テストの実行を中断し、そのテストをスキップ済みとしてマークします。引数にはスキップの理由を記述できます。これは、特定の条件が満たされない場合にテストを実行しないようにするために使用されます。

正規表現のバックトラッキングとパフォーマンス

正規表現エンジンは、パターンマッチングのために様々なアルゴリズムを使用します。一部の正規表現エンジン(特に伝統的なバックトラッキングエンジン)では、複雑なパターンや特定の入力に対して指数関数的な時間計算量を持つ場合があります。これは「ReDoS (Regular expression Denial of Service)」攻撃の原因となることもあります。

このコミットで言及されている^(?:x{1,1000}){1,1000}$のような正規表現は、ネストされた量指定子({1,1000})と非キャプチャグループ((?:...))を組み合わせたもので、非常に多くのバックトラッキングを必要とする可能性があります。このようなパターンは、正規表現エンジンのパフォーマンス特性を厳しくテストするために使用されます。

技術的詳細

このコミットは、Go言語のregexpパッケージ内のテストファイルsrc/pkg/regexp/all_test.goに対して行われました。変更の核心は、TestOnePassCutoffというテスト関数に、testing.Short()のチェックを追加し、その結果に基づいてテストをスキップするロジックを導入したことです。

変更前は、TestOnePassCutoffは常に実行されていました。このテストは、MustCompile(^(?:x{1,1000}){1,1000}$)という非常に複雑な正規表現をコンパイルするもので、この正規表現はGoの正規表現エンジンが「one-pass compilation」と呼ばれる最適化パスを適用できるかどうか、およびその際のカットオフ挙動を検証するために設計されています。この種の正規表現は、バックトラッキングの深さや状態遷移の数が非常に多くなるため、コンパイルおよびマッチングに時間がかかることがあります。

コミットメッセージによると、このテストはDmitriy Vyukov氏のMac上で4秒かかり、FreeBSDの-raceモードではタイムアウトするという問題がありました。これは、開発者がローカルでテストを実行する際の生産性を低下させ、CIシステムでのビルド時間を不必要に長くし、さらには不安定なテスト結果(タイムアウト)によってCIの信頼性を損なう可能性がありました。

この変更により、go test -shortコマンドが実行された場合、testing.Short()関数がtrueを返します。この条件が満たされると、t.Skip("Skipping in short mode")が呼び出され、TestOnePassCutoffテストは実行されずにスキップされます。これにより、開発者は日常的な作業で高速なテスト実行を享受でき、CIシステムもより迅速かつ安定したフィードバックを提供できるようになります。

このアプローチは、Goのテストフレームワークが提供する柔軟性を活用したものであり、テストスイート全体の実行時間を最適化するための一般的なプラクティスです。特に、リソースを大量に消費するテストや、特定の環境でのみ関連するテストに対して有効です。

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

変更はsrc/pkg/regexp/all_test.goファイル内のTestOnePassCutoff関数に対して行われました。

--- a/src/pkg/regexp/all_test.go
+++ b/src/pkg/regexp/all_test.go
@@ -475,6 +475,9 @@ func TestSplit(t *testing.T) {
 
 // This ran out of stack before issue 7608 was fixed.
 func TestOnePassCutoff(t *testing.T) {
+\tif testing.Short() {
+\t\tt.Skip("Skipping in short mode")
+\t}\n     MustCompile(`^(?:x{1,1000}){1,1000}$`)\n     }\n     \n```

追加されたコードは以下の3行です。

```go
	if testing.Short() {
		t.Skip("Skipping in short mode")
	}

コアとなるコードの解説

追加されたコードは、Goの標準ライブラリtestingパッケージの機能を利用しています。

  1. testing.Short(): この関数は、go testコマンドが-shortフラグ付きで実行された場合にtrueを返します。このフラグは、テストの実行時間を短縮したい場合に開発者が明示的に指定するものです。通常、時間のかかるテストや、ネットワークアクセス、ファイルI/O、大量の計算を伴うテストなどで使用されます。

  2. t.Skip("Skipping in short mode"): tは現在のテストを表す*testing.T型のインスタンスです。t.Skip()メソッドは、現在のテストの実行を直ちに中断し、そのテストを「スキップ済み」としてマークします。引数として渡された文字列(この場合は"Skipping in short mode")は、テスト結果の出力にスキップ理由として表示されます。

このコードブロック全体は、次のようなロジックを実装しています。 「もしgo test -shortコマンドが実行されているならば、このTestOnePassCutoffテストは実行せずにスキップし、『shortモードでのスキップ』というメッセージを表示する。」

これにより、開発者は日常的な開発サイクルで高速なテスト実行を維持しつつ、完全なテストスイート(-shortフラグなしで実行)では、この時間のかかるが重要なテストも引き続き実行されるというバランスの取れたアプローチが可能になります。特に、CI環境では通常、フルテストとショートテストの両方が異なるステージで実行されることが多いため、この変更はCIパイプラインの効率化に貢献します。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • GitHubのGoリポジトリのコミット履歴と関連するissue
  • Go言語のテストに関する一般的な知識とプラクティス
  • 正規表現エンジンのパフォーマンスに関する一般的な知識