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

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

このコミットは、Go言語の標準ライブラリnetパッケージ内のipraw_test.goファイルに対する変更です。具体的には、Windows環境におけるRaw IPプロトコルのエントリテストの不安定性(flakiness)を解消することを目的としています。

コミット

commit 2bd17bca0776ff8bb661259d2ed6de6a325ac197
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Thu Apr 25 13:23:24 2013 +0900

    net: deflake raw IP protocol entry test on Windows
    
    Update #5344
    
    R=golang-dev, dave, r, alex.brainman
    CC=golang-dev
    https://golang.org/cl/8934043

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

https://github.com/golang/go/commit/2bd17bca0776ff8bb661259d2ed6de6a325ac197

元コミット内容

このコミットの元のメッセージは以下の通りです。

net: deflake raw IP protocol entry test on Windows

Update #5344

R=golang-dev, dave, r, alex.brainman
CC=golang-dev
https://golang.org/cl/8934043

これは、netパッケージのRaw IPプロトコルに関するテストがWindows上で不安定である問題を修正し、その変更がIssue #5344に関連していることを示しています。R=はレビュー担当者、CC=はカーボンコピーの対象者を示し、https://golang.org/cl/8934043はGoのコードレビューシステム(Gerrit)における変更リストへのリンクです。

変更の背景

Go言語のテストスイートは、様々なオペレーティングシステム上で実行されることを想定して設計されています。しかし、特定のOS(この場合はWindows)のネットワークスタックやシステムコールの挙動の違いにより、テストが不安定になることがあります。このような不安定なテストは「flaky test」と呼ばれ、テストが成功したり失敗したりする原因がコードのバグではなく、環境やタイミングに依存する場合に発生します。

このコミットの背景には、Windows環境でRaw IPプロトコルに関連するテストが、本来の機能とは関係なく、環境的な要因で失敗することがあったという問題があります。テストの信頼性を向上させ、開発者がテスト結果に一貫性を持って依存できるようにするために、この不安定性を解消する必要がありました。コミットメッセージにあるUpdate #5344は、この問題がGoのIssueトラッカーで報告され、このコミットがその解決策として提案されたことを示唆しています。

前提知識の解説

Raw IPプロトコル

Raw IPプロトコル(Raw IP Sockets)は、アプリケーションがIPヘッダを含む生(Raw)のIPパケットを直接送受信できる機能です。通常のTCPやUDPソケットがトランスポート層のプロトコル(TCP/UDP)を抽象化するのに対し、Raw IPソケットはネットワーク層のIPプロトコルを直接操作します。これにより、カスタムプロトコルの実装、ネットワーク診断ツール(ping, tracerouteなど)、ファイアウォール、VPNなどの開発が可能になります。

しかし、Raw IPソケットの操作はOSによってセキュリティ上の制限が厳しく、特にWindowsではUnix系OSと比較してその利用に制約が多い場合があります。例えば、WindowsではRaw IPソケットで受信できるのはICMP、IGMP、IPヘッダを含むIPパケットのみで、任意のプロトコル番号のパケットを受信することはできません。また、送信に関しても特定の制限があります。

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

Go言語には標準でtestingパッケージが提供されており、これを用いてユニットテストやベンチマークテストを記述します。テスト関数はTestXxxという命名規則に従い、*testing.T型の引数を取ります。テスト関数内でt.Fatalft.Errorfなどのメソッドを呼び出すことで、テストの失敗を報告します。

  • t.Fatalf: テストを即座に終了させ、失敗としてマークします。
  • t.Errorf: テストを失敗としてマークしますが、テストの実行は継続します。

reflect.DeepEqual

Go言語のreflectパッケージは、実行時に型情報を調べたり、値の操作を行ったりするための機能を提供します。reflect.DeepEqual(x, y interface{}) bool関数は、2つの値xyが「深く」等しいかどうかを再帰的に比較します。これは、プリミティブ型だけでなく、構造体、配列、スライス、マップなどの複合型についても、その内容が等しいかを比較する際に非常に便利です。ポインタの場合は、指し示す値が等しいかを比較します。

Flaky Test (不安定なテスト)

Flaky testとは、同じコードに対して同じテストを実行しても、結果が成功と失敗の間で不規則に変動するテストのことです。これは、テストが外部要因(ネットワークの遅延、OSのスケジューリング、リソースの競合、時間依存の処理など)に依存している場合に発生しやすく、開発者の生産性を低下させ、テストスイートの信頼性を損ないます。Flaky testの修正は、テストの信頼性を高め、CI/CDパイプラインの安定性を確保するために重要です。

技術的詳細

このコミットの技術的詳細は、Goのテストにおけるエラーハンドリングの改善と、Windows環境でのテストの挙動の調整にあります。

元のコードでは、ResolveIPAddr関数の呼び出し結果を検証するif文が2つに分かれていました。

  1. err != tt.errでエラーの比較
  2. !reflect.DeepEqual(addr, tt.addr)でアドレスの比較

もしerr != tt.errが真の場合、t.Fatalfが呼び出され、テストはそこで終了します。この場合、reflect.DeepEqualによるアドレスの比較は実行されません。

変更後のコードでは、この2つの条件を1つのif-else if構造に統合しています。

if err != tt.err {
    condFatalf(t, "ResolveIPAddr(%v, %v) failed: %v", tt.net, tt.litAddr, err)
} else if !reflect.DeepEqual(addr, tt.addr) {
    t.Fatalf("got %#v; expected %#v", addr, tt.addr)
}

ここで注目すべきは、t.FatalfcondFatalfに置き換えられている点です。condFatalfはGoのテストユーティリティ関数で、特定の条件下でのみt.Fatalfを呼び出すように設計されている可能性があります。これは、Windows環境でのRaw IPテストの不安定性に対処するためのものです。

一般的なcondFatalfのパターンとしては、特定のOS(この場合はWindows)でのみ、エラーが特定の既知の不安定な挙動に起因する場合に、テストをスキップしたり、より寛容なチェックを行ったりすることが考えられます。これにより、本来のテスト目的(ResolveIPAddrが正しいアドレスを解決するか)は維持しつつ、環境依存の不安定な失敗を回避できます。

具体的には、WindowsのRaw IPソケットの挙動が他のOSと異なるため、ResolveIPAddrが期待通りのエラーを返さない、あるいは特定のアドレス解決が失敗するケースがあったと考えられます。condFatalfは、そのようなWindows特有の「許容できる」失敗の場合にはテストを失敗とせず、それ以外の予期せぬエラーの場合にのみテストを失敗させるようなロジックを含んでいると推測されます。

また、else ifを使うことで、エラーが期待通りであった場合にのみアドレスの比較を行うというロジックが明確になります。これにより、エラーが発生したにもかかわらずアドレスの比較を行ってしまうという無駄や、誤ったテスト結果の解釈を防ぐことができます。

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

変更はsrc/pkg/net/ipraw_test.goファイル内のTestResolveIPAddr関数にあります。

--- a/src/pkg/net/ipraw_test.go
+++ b/src/pkg/net/ipraw_test.go
@@ -55,9 +55,8 @@ func TestResolveIPAddr(t *testing.T) {
 	for _, tt := range resolveIPAddrTests {
 		addr, err := ResolveIPAddr(tt.net, tt.litAddr)
 		if err != tt.err {
-\t\t\tt.Fatalf("ResolveIPAddr(%v, %v) failed: %v", tt.net, tt.litAddr, err)
-\t\t}\n-\t\tif !reflect.DeepEqual(addr, tt.addr) {
+\t\t\tcondFatalf(t, "ResolveIPAddr(%v, %v) failed: %v", tt.net, tt.litAddr, err)
+\t\t} else if !reflect.DeepEqual(addr, tt.addr) {
 \t\t\tt.Fatalf("got %#v; expected %#v", addr, tt.addr)
 \t\t}
 	}

コアとなるコードの解説

変更の核心は、if文の条件分岐の修正と、t.FatalfcondFatalfへの置き換えです。

変更前:

if err != tt.err {
    t.Fatalf("ResolveIPAddr(%v, %v) failed: %v", tt.net, tt.litAddr, err)
}
if !reflect.DeepEqual(addr, tt.addr) {
    t.Fatalf("got %#v; expected %#v", addr, tt.addr)
}

このコードでは、エラーのチェックとアドレスのチェックが独立したif文で行われています。もしerr != tt.errが真であれば、最初のt.Fatalfが呼び出され、テストはそこで終了します。しかし、もしエラーが期待通りであったとしても、次のif文でreflect.DeepEqualによるアドレスの比較が必ず実行されます。

変更後:

if err != tt.err {
    condFatalf(t, "ResolveIPAddr(%v, %v) failed: %v", tt.net, tt.litAddr, err)
} else if !reflect.DeepEqual(addr, tt.addr) {
    t.Fatalf("got %#v; expected %#v", addr, tt.addr)
}

この変更により、以下の点が改善されます。

  1. 論理的な結合: else ifを使用することで、エラーの比較とアドレスの比較が論理的に結合されます。つまり、「エラーが期待通りでなかった場合」と「エラーは期待通りだったが、アドレスが期待通りでなかった場合」という2つの異なる失敗シナリオが明確に区別されます。
  2. condFatalfの導入: err != tt.errの条件が真の場合にt.Fatalfの代わりにcondFatalfが呼び出されます。これは、Windows環境でのRaw IPプロトコルに関するテストの不安定性に対処するためのものです。condFatalfは、特定のOS(Windows)や特定の条件下でのみテストを失敗させる、あるいは失敗を許容するロジックを含んでいると考えられます。これにより、Windows特有の環境要因による誤ったテスト失敗を減らし、テストの信頼性を向上させます。
  3. 効率性: エラーが期待通りでなかった場合(最初のifブロックが実行された場合)、else ifブロックは評価されないため、不必要なreflect.DeepEqualの呼び出しを避けることができます。

この変更は、Goのテストが様々なプラットフォームで安定して動作するようにするための、プラットフォーム固有の挙動への対応の一例と言えます。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント
  • Go言語のソースコード(src/pkg/net/ipraw_test.go
  • Go言語のコードレビューシステム (Gerrit) の一般的な情報
  • Raw IPソケットに関する一般的なネットワークプログラミングの知識
  • Flaky testに関する一般的なソフトウェアテストの知識