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

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

このコミットは、Go言語の標準ライブラリnetパッケージにおけるテストの信頼性向上を目的としています。具体的には、go test -cpuフラグを使用してテストを繰り返し実行した際に発生するテストデータの破損を防ぐための修正です。ipraw_test.gotcp_test.goudp_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パッケージでは、IPAddrTCPAddrUDPAddr構造体において、このゾーンIDをZoneフィールドで扱います。

技術的詳細

このコミットの技術的な核心は、テストデータの初期化方法を、テスト実行時に動的に変更するのではなく、パッケージのロード時に一度だけ初期化するように変更した点にあります。

元のコードでは、resolveIPAddrTestsresolveTCPAddrTestsresolveUDPAddrTestsといったテストケースのスライスがグローバル変数として定義されていました。これらのスライスには、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スライス内のデータが書き換えられ、その変更が次の実行に引き継がれてしまうため、テストが不安定になっていました。

このコミットでは、以下の変更によってこの問題を解決しています。

  1. テストケース構造体の型定義: 匿名構造体として定義されていたテストケースのスライスが、type resolveIPAddrTest struct { ... }のように名前付きの構造体として定義されるようになりました。これは直接的なバグ修正とは関係ありませんが、コードの可読性と保守性を向上させます。
  2. 動的テストケースの削除: resolve...Testsスライスの初期リテラルから、動的にゾーンIDを決定する必要があったIPv6リンクローカルアドレスのテストケースが削除されました。
  3. 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

それぞれのファイルで、resolveIPAddrTestsresolveTCPAddrTestsresolveUDPAddrTestsというテストデータスライスの定義と、それらを処理する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.gosrc/pkg/net/udp_test.goresolveTCPAddrTestsresolveUDPAddrTestsに対しても適用されています。

コアとなるコードの解説

このコミットの核心は、Go言語のinit()関数の適切な利用にあります。

  1. テストデータ構造の変更:

    • var resolveIPAddrTests = []struct { ... } のように匿名構造体で定義されていたテストケースのスライスが、type resolveIPAddrTest struct { ... } と名前付き構造体として定義され、var resolveIPAddrTests = []resolveIPAddrTest{ ... } と明示的に型が指定されました。これにより、コードの意図がより明確になります。
  2. 動的テストケースの分離とinit()関数への移行:

    • 元のコードでは、fe80::1のようなIPv6リンクローカルアドレスのテストケースは、Zoneフィールドに"name""index"といったプレースホルダーを持っていました。そして、TestResolveIPAddr関数内で、これらのプレースホルダーを実際のネットワークインターフェース情報(名前やインデックス)に動的に置き換えていました。
    • このコミットでは、これらの動的なテストケースを初期のresolveIPAddrTestsスライスリテラルから削除しました。
    • 代わりに、func init() { ... } ブロックが追加されました。このinit()関数は、パッケージがロードされる際に一度だけ実行されます。
    • init()関数内でloopbackInterface()を呼び出し、システムにループバックインターフェースが存在するかどうかを確認します。
    • ループバックインターフェースが存在する場合、そのインターフェースの名前とインデックスを取得し、それらを使用して実際のIPv6リンクローカルアドレスのテストケース(例: fe80::1%lo0fe80::1%1)を生成します。
    • 生成されたテストケースは、append関数を使ってグローバルなresolveIPAddrTestsスライスに追加されます。
  3. テスト関数の簡素化:

    • TestResolveIPAddr関数内から、テストケースのZoneフィールドが"name"または"index"である場合に動的にアドレスを書き換えるロジックが完全に削除されました。
    • これにより、TestResolveIPAddr関数は、既に完全に初期化されたテストデータスライスを単純にループ処理するだけでよくなり、テスト実行中のデータ変更がなくなりました。

この変更により、resolveIPAddrTests(および他のテストスライス)は、パッケージがロードされた時点で最終的な状態になり、その後のテスト実行中に変更されることはありません。これにより、go test -cpuのような並行・繰り返し実行環境下でも、テストデータの一貫性が保証され、テストの信頼性が大幅に向上しました。これは、テストの冪等性(何度実行しても同じ結果が得られること)を確保するための重要な修正です。

関連リンク

参考にした情報源リンク