[インデックス 15948] ファイルの概要
このコミットは、Go言語の標準ライブラリnet
パッケージにおけるテストの信頼性向上を目的としています。具体的には、go test -cpu
フラグを使用してテストを繰り返し実行した際に発生するテストデータの破損を防ぐための修正です。ipraw_test.go
、tcp_test.go
、udp_test.go
の3つのテストファイルにおいて、テストケースの初期化方法が変更され、テストデータが繰り返し実行間で共有されることによる問題が回避されています。
コミット
commit 9d97b55d387ebbd24691bd16af41d74330b920aa
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date: Wed Mar 27 01:06:48 2013 +0900
net: fix test data corruption in repetitive test runs by -cpu
This CL avoids test data sharing in repetitive test runs;
e.g., go test net -cpu=1,1,1
R=golang-dev, fullung, bradfitz
CC=golang-dev
https://golang.org/cl/8011043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9d97b55d387ebbd24691bd16af41d74330b920aa
元コミット内容
このコミットは、net
パッケージのテストにおいて、go test -cpu
フラグを用いた繰り返しテスト実行時に発生するテストデータの破損を修正します。具体的には、テストデータが繰り返し実行間で共有されることを防ぎます。
変更の背景
Go言語のテストフレームワークには、go test -cpu
というフラグがあります。これは、テストを複数のCPUコアで並行して実行したり、指定されたCPU数でテストを複数回繰り返したりするために使用されます。例えば、go test -cpu=1,1,1
は、テストを3回実行し、それぞれ1つのCPUコアを使用することを意味します。
このコミットが修正しようとしている問題は、テストデータがグローバル変数やパッケージレベルの変数として定義されており、かつそのデータがテスト実行中に変更される場合に発生します。go test -cpu
のようにテストが複数回実行されると、前回のテスト実行で変更されたデータが次回のテスト実行に引き継がれてしまい、テスト結果が不安定になったり、誤った結果を返したりする「テストデータの破損」が発生する可能性がありました。
特に、IPv6のリンクローカルアドレス(fe80::/10
)に関連するテストケースでは、システム上のネットワークインターフェース情報(インターフェース名やインデックス)を動的に取得してテストデータに組み込む必要がありました。元の実装では、この動的なデータ生成がテスト関数内で直接行われており、テストが繰り返し実行されるたびに既存のテストデータスライスが変更されてしまう構造になっていました。これが、go test -cpu
のような繰り返し実行環境下でのデータ破損の根本原因でした。
前提知識の解説
Go言語のテストとgo test
コマンド
Go言語には、標準でテストフレームワークが組み込まれており、go test
コマンドを使用してテストを実行します。テストファイルは通常、テスト対象のファイルと同じディレクトリに_test.go
というサフィックスを付けて配置されます。
go test -cpu
フラグ
go test -cpu
フラグは、テストの並行実行や繰り返し実行を制御します。
go test -cpu N
: テストをN個のCPUコアで並行して実行します。go test -cpu 1,2,4
: テストを3回実行し、それぞれ1、2、4個のCPUコアを使用します。 このフラグは、並行処理に関連するバグや、リソース競合によるテストの不安定性を検出するのに役立ちます。しかし、テストが適切に分離されていない場合、前述のようなテストデータの破損を引き起こす可能性があります。
Go言語のinit()
関数
Go言語のinit()
関数は、パッケージがインポートされた際に自動的に実行される特殊な関数です。各パッケージは複数のinit()
関数を持つことができ、それらは定義された順序で実行されます。init()
関数は、プログラムの起動時に一度だけ実行されるため、グローバル変数の初期化や、プログラムの実行に必要なセットアップ処理を行うのに適しています。
IPv6リンクローカルアドレスとゾーンID
IPv6のリンクローカルアドレス(例: fe80::1
)は、単一のリンク(ネットワークセグメント)内でのみ有効なアドレスです。これらのアドレスは、通常、どのネットワークインターフェースに属するかを示す「ゾーンID」を伴って使用されます。ゾーンIDは、インターフェース名(例: eth0
, en0
)またはインターフェースインデックス(例: 1
, 2
)の形式で指定されます。Goのnet
パッケージでは、IPAddr
やTCPAddr
、UDPAddr
構造体において、このゾーンIDをZone
フィールドで扱います。
技術的詳細
このコミットの技術的な核心は、テストデータの初期化方法を、テスト実行時に動的に変更するのではなく、パッケージのロード時に一度だけ初期化するように変更した点にあります。
元のコードでは、resolveIPAddrTests
、resolveTCPAddrTests
、resolveUDPAddrTests
といったテストケースのスライスがグローバル変数として定義されていました。これらのスライスには、IPv6リンクローカルアドレスに関するテストケースが含まれていましたが、そのゾーンID(インターフェース名やインデックス)はプレースホルダー("name"や"index")として設定されていました。
そして、各TestResolve...Addr
関数内で、テストケースをループ処理する際に、tt.addr.Zone == "name" || tt.addr.Zone == "index"
という条件でチェックし、もし該当するテストケースであれば、loopbackInterface()
関数を呼び出して実際のループバックインターフェース情報を取得し、tt.litAddr
(リテラルアドレス文字列)とtt.addr.Zone
を動的に書き換えていました。
この動的な書き換えが問題でした。go test -cpu
によってテストが複数回実行されると、最初の実行でresolve...Tests
スライス内のデータが書き換えられ、その変更が次の実行に引き継がれてしまうため、テストが不安定になっていました。
このコミットでは、以下の変更によってこの問題を解決しています。
- テストケース構造体の型定義: 匿名構造体として定義されていたテストケースのスライスが、
type resolveIPAddrTest struct { ... }
のように名前付きの構造体として定義されるようになりました。これは直接的なバグ修正とは関係ありませんが、コードの可読性と保守性を向上させます。 - 動的テストケースの削除:
resolve...Tests
スライスの初期リテラルから、動的にゾーンIDを決定する必要があったIPv6リンクローカルアドレスのテストケースが削除されました。 init()
関数による動的テストケースの追加: 各テストファイルにinit()
関数が追加されました。このinit()
関数内でloopbackInterface()
を呼び出し、ループバックインターフェースが存在する場合にのみ、実際のインターフェース名とインデックスを使用してIPv6リンクローカルアドレスのテストケースを生成し、append
関数を使ってresolve...Tests
スライスに一度だけ追加するように変更されました。
これにより、resolve...Tests
スライスはパッケージがロードされる際に完全に初期化され、その後のテスト実行中に変更されることがなくなります。結果として、go test -cpu
のような繰り返し実行環境下でも、テストデータの一貫性が保たれ、テストの信頼性が向上しました。
コアとなるコードの変更箇所
変更は主に以下の3つのファイルで行われています。
src/pkg/net/ipraw_test.go
src/pkg/net/tcp_test.go
src/pkg/net/udp_test.go
それぞれのファイルで、resolveIPAddrTests
、resolveTCPAddrTests
、resolveUDPAddrTests
というテストデータスライスの定義と、それらを処理するTestResolve...Addr
関数が変更されています。
変更の例 (src/pkg/net/ipraw_test.go
):
変更前:
var resolveIPAddrTests = []struct {
net string
litAddr string
addr *IPAddr
err error
}{
// ... 既存のテストケース ...
{"ip6", "fe80::1", &IPAddr{IP: ParseIP("fe80::1"), Zone: "name"}, nil},
{"ip6", "fe80::1", &IPAddr{IP: ParseIP("fe80::1"), Zone: "index"}, nil},
// ...
}
func TestResolveIPAddr(t *testing.T) {
for _, tt := range resolveIPAddrTests {
if tt.addr != nil && (tt.addr.Zone == "name" || tt.addr.Zone == "index") {
ifi := loopbackInterface()
if ifi == nil {
continue
}
switch tt.addr.Zone {
case "name":
tt.litAddr += "%" + ifi.Name
tt.addr.Zone = zoneToString(ifi.Index)
case "index":
index := fmt.Sprintf("%v", ifi.Index)
tt.litAddr += "%" + index
tt.addr.Zone = index
}
}
// ... テストロジック ...
}
}
変更後:
type resolveIPAddrTest struct { // 名前付き構造体に変更
net string
litAddr string
addr *IPAddr
err error
}
var resolveIPAddrTests = []resolveIPAddrTest{ // 型指定
// ... 既存のテストケース (fe80::1関連は削除) ...
}
func init() { // init関数を追加
if ifi := loopbackInterface(); ifi != nil {
index := fmt.Sprintf("%v", ifi.Index)
resolveIPAddrTests = append(resolveIPAddrTests, []resolveIPAddrTest{
{"ip6", "fe80::1%" + ifi.Name, &IPAddr{IP: ParseIP("fe80::1"), Zone: zoneToString(ifi.Index)}, nil},
{"ip6", "fe80::1%" + index, &IPAddr{IP: ParseIP("fe80::1"), Zone: index}, nil},
}...)
}
}
func TestResolveIPAddr(t *testing.T) {
for _, tt := range resolveIPAddrTests {
// 動的な書き換えロジックを削除
// ... テストロジック ...
}
}
同様の変更がsrc/pkg/net/tcp_test.go
とsrc/pkg/net/udp_test.go
のresolveTCPAddrTests
とresolveUDPAddrTests
に対しても適用されています。
コアとなるコードの解説
このコミットの核心は、Go言語のinit()
関数の適切な利用にあります。
-
テストデータ構造の変更:
var resolveIPAddrTests = []struct { ... }
のように匿名構造体で定義されていたテストケースのスライスが、type resolveIPAddrTest struct { ... }
と名前付き構造体として定義され、var resolveIPAddrTests = []resolveIPAddrTest{ ... }
と明示的に型が指定されました。これにより、コードの意図がより明確になります。
-
動的テストケースの分離と
init()
関数への移行:- 元のコードでは、
fe80::1
のようなIPv6リンクローカルアドレスのテストケースは、Zone
フィールドに"name"
や"index"
といったプレースホルダーを持っていました。そして、TestResolveIPAddr
関数内で、これらのプレースホルダーを実際のネットワークインターフェース情報(名前やインデックス)に動的に置き換えていました。 - このコミットでは、これらの動的なテストケースを初期の
resolveIPAddrTests
スライスリテラルから削除しました。 - 代わりに、
func init() { ... }
ブロックが追加されました。このinit()
関数は、パッケージがロードされる際に一度だけ実行されます。 init()
関数内でloopbackInterface()
を呼び出し、システムにループバックインターフェースが存在するかどうかを確認します。- ループバックインターフェースが存在する場合、そのインターフェースの名前とインデックスを取得し、それらを使用して実際のIPv6リンクローカルアドレスのテストケース(例:
fe80::1%lo0
やfe80::1%1
)を生成します。 - 生成されたテストケースは、
append
関数を使ってグローバルなresolveIPAddrTests
スライスに追加されます。
- 元のコードでは、
-
テスト関数の簡素化:
TestResolveIPAddr
関数内から、テストケースのZone
フィールドが"name"
または"index"
である場合に動的にアドレスを書き換えるロジックが完全に削除されました。- これにより、
TestResolveIPAddr
関数は、既に完全に初期化されたテストデータスライスを単純にループ処理するだけでよくなり、テスト実行中のデータ変更がなくなりました。
この変更により、resolveIPAddrTests
(および他のテストスライス)は、パッケージがロードされた時点で最終的な状態になり、その後のテスト実行中に変更されることはありません。これにより、go test -cpu
のような並行・繰り返し実行環境下でも、テストデータの一貫性が保証され、テストの信頼性が大幅に向上しました。これは、テストの冪等性(何度実行しても同じ結果が得られること)を確保するための重要な修正です。
関連リンク
- Go言語のテストに関する公式ドキュメント: https://go.dev/doc/tutorial/add-a-test
- Go言語の
init()
関数に関する解説: https://go.dev/doc/effective_go#initialization - Go言語の
net
パッケージに関する公式ドキュメント: https://pkg.go.dev/net
参考にした情報源リンク
- Go CL 8011043 (このコミットのChangeList): https://golang.org/cl/8011043
go test
コマンドのドキュメント: https://go.dev/cmd/go/#hdr-Test_packages- IPv6リンクローカルアドレスとゾーンIDに関する一般的な情報 (例: Wikipedia): https://ja.wikipedia.org/wiki/IPv6%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9#%E3%83%AA%E3%83%B3%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%AB%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9
- Go言語の
append
関数に関する情報: https://go.dev/blog/slices