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

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

このコミットは、Go言語の標準ライブラリであるnetパッケージ内のマルチキャストリスナーテストに関する修正です。具体的には、既存のマルチキャストテストをIPv4とIPv6のそれぞれに分割し、テストの安定性を向上させるための変更が含まれています。

コミット

commit 2ac799cfbcf212675d64ea0df835ca6c2e2aa368
Author: Mikio Hara <mikioh.mikioh@gmail.com>
Date:   Fri Mar 8 06:51:06 2013 +0900

    net: fix multicast listener tests
    
    This CL splits multicast listener tests into two; for IPv4 and
    for IPv6. It also removes redundant test inputs and makes sure
    that assignment of multicast interface to stablize the tests.
    
    Fixes #4059.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/7565043

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

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

元コミット内容

このコミットは、Go言語のnetパッケージにおけるマルチキャストリスナーのテストに関するものです。元のコミットメッセージによると、以下の点が変更されています。

  • マルチキャストリスナーのテストをIPv4用とIPv6用に分割。
  • 冗長なテスト入力の削除。
  • マルチキャストインターフェースの割り当てを確実に行い、テストの安定性を向上。
  • Issue #4059 を修正。

変更の背景

このコミットの背景には、Go言語のnetパッケージにおけるマルチキャスト機能のテストが不安定であったという問題があります。特に、IPv4とIPv6のマルチキャストテストが混在しており、特定の環境や実行タイミングによってテストが失敗する可能性がありました。

マルチキャスト通信は、特定のグループに属する複数のホストに対して同時にデータを送信する通信方式です。UDP(User Datagram Protocol)上で実装されることが多く、ストリーミング配信やサービスディスカバリなどで利用されます。しかし、マルチキャスト通信はネットワーク環境やOSの設定に依存する部分が多く、テストの安定性を確保するのが難しい側面があります。

元のテストコードでは、IPv4とIPv6の両方に対応するマルチキャストリスナーのテストが単一のテスト関数内で実行されていました。これにより、以下のような問題が発生していた可能性があります。

  1. 環境依存性: IPv4とIPv6のネットワーク設定が異なる環境では、一方のテストが成功してももう一方が失敗するといった問題が発生しやすくなります。特に、IPv6が完全にサポートされていない、または設定が不完全な環境では、IPv6関連のテストが不安定になる傾向があります。
  2. テストの複雑性: 単一のテスト関数内でIPv4とIPv6の両方を扱うことで、テストコードが複雑になり、デバッグやメンテナンスが困難になります。
  3. リソースの競合: マルチキャストグループへの参加やインターフェースのバインドといった操作は、システムのリソースを消費します。IPv4とIPv6のテストが同時に実行されることで、リソースの競合が発生し、テストの不安定化を招く可能性がありました。
  4. テストの再現性: テストが不安定であると、開発者がコード変更を行った際に、その変更が原因でテストが失敗したのか、それとも環境要因による一時的な失敗なのかを判断するのが難しくなります。これは開発効率を低下させ、バグの発見を遅らせる原因にもなります。

これらの問題を解決し、マルチキャスト機能の信頼性を高めるために、テストをIPv4とIPv6に明確に分割し、テスト環境の安定化を図る必要がありました。特に、マルチキャストインターフェースの明示的な割り当ては、テストが特定のネットワークインターフェースに依存しないようにし、より予測可能な結果を得るために重要です。

前提知識の解説

このコミットを理解するためには、以下のネットワークおよびGo言語に関する前提知識が必要です。

1. マルチキャスト通信

  • ユニキャスト: 1対1の通信。特定の送信元から特定の宛先へデータを送信します。
  • ブロードキャスト: 1対Nの通信。特定の送信元から同一ネットワーク内の全てのホストへデータを送信します。
  • マルチキャスト: 1対多の通信。特定の送信元から特定のグループに属する複数のホストへデータを送信します。マルチキャストグループに参加しているホストのみがデータを受信します。IPマルチキャストでは、特定のIPアドレス(マルチキャストアドレス)がグループを識別するために使用されます。

2. IPv4とIPv6のマルチキャストアドレス

  • IPv4マルチキャストアドレス: 224.0.0.0から239.255.255.255の範囲が予約されています。
    • 224.0.0.0/24はローカルネットワーク制御ブロック(Link-Local Multicast)として予約されており、ルータを越えて転送されません。
  • IPv6マルチキャストアドレス: ff00::/8の範囲が予約されています。IPv6マルチキャストアドレスは、スコープ(リンクローカル、サイトローカル、グローバルなど)やグループIDなどの情報を含みます。
    • ff01::/16: インターフェースローカルスコープ
    • ff02::/16: リンクローカルスコープ
    • ff05::/16: サイトローカルスコープ
    • ff0e::/16: グローバルスコープ

3. UDP (User Datagram Protocol)

マルチキャスト通信は通常、UDP上で実装されます。UDPはコネクションレス型のプロトコルであり、データの信頼性や順序保証は行いませんが、オーバーヘッドが少なく高速な通信が可能です。マルチキャストでは、送信元は特定のマルチキャストアドレスとポート番号にデータを送信し、受信側は同じマルチキャストアドレスとポート番号をリッスンすることでデータを受信します。

4. ネットワークインターフェース

コンピュータがネットワークに接続するための物理的または仮想的な装置です。各インターフェースにはIPアドレスが割り当てられ、データ送受信の経路となります。マルチキャスト通信では、どのインターフェースでマルチキャストグループに参加するか、またはどのインターフェースからマルチキャストパケットを送信するかが重要になります。

5. マルチキャストルーティング情報ベース (RIB: Routing Information Base)

RIBは、ルータやホストがネットワークのルーティング情報を格納するデータベースです。マルチキャストRIBは、特にマルチキャストルーティングに関する情報を管理し、マルチキャストグループのメンバーシップや、マルチキャストパケットの転送経路を決定するために使用されます。multicastRIBContains関数は、特定のIPアドレスがマルチキャストRIBに含まれているか(つまり、そのIPアドレスのマルチキャストグループに参加しているか)を確認するために使用されます。

6. Go言語のnetパッケージ

Go言語のnetパッケージは、ネットワークI/Oのプリミティブを提供します。TCP/IP、UDP、IP、Unixドメインソケットなどのネットワークプロトコルをサポートし、ネットワーク接続の確立、データの送受信、アドレスの解決など、様々なネットワーク関連の機能を提供します。

  • net.ListenMulticastUDP: 指定されたネットワークインターフェースとマルチキャストUDPアドレスでマルチキャストリスナーを確立します。
  • net.UDPAddr: UDPアドレス(IPアドレスとポート番号)を表す構造体です。
  • net.Interface: ネットワークインターフェースの情報を表す構造体です。
  • net.Interfaces(): システム上の全てのネットワークインターフェースのリストを返します。
  • net.IP: IPアドレスを表す型です。
  • net.ParseIP: 文字列からIPアドレスをパースします。
  • net.IPv4: IPv4アドレスを生成します。

7. テストフレームワーク (Go testingパッケージ)

Go言語には標準でtestingパッケージが用意されており、ユニットテストやベンチマークテストを記述できます。

  • testing.T: テスト関数に渡される型で、テストの実行状態やエラー報告に使用されます。
  • t.Fatalf(): テストを失敗させ、すぐに終了します。
  • t.Skipf(): テストをスキップします。特定のOSやアーキテクチャでテストが実行できない場合などに使用されます。
  • runtime.GOOS, runtime.GOARCH: 現在のOSとアーキテクチャを示す定数です。

技術的詳細

このコミットの技術的詳細は、主にGo言語のnetパッケージにおけるマルチキャストリスナーのテストの改善に焦点を当てています。

1. テストファイルの分割

最も大きな変更点は、src/pkg/net/multicast_posix_test.goが削除され、src/pkg/net/multicast_test.goが新規作成されたことです。

  • multicast_posix_test.goは、!plan9ビルドタグを持つファイルであり、Plan 9以外のPOSIX互換システム向けのテストを含んでいました。このファイルはIPv4とIPv6のマルチキャストテストを混在させていました。
  • 新しく作成されたmulticast_test.goでは、TestIPv4MulticastListenerTestIPv6MulticastListenerという2つの独立したテスト関数が導入されました。これにより、IPv4とIPv6のテストが明確に分離され、それぞれのプロトコルに特化したテストロジックを記述できるようになりました。

2. テスト入力の冗長性排除

元のmulticastListenerTests変数には、IPv4とIPv6のマルチキャストアドレスが混在しており、netタイプ(udp, udp4, udp6)とipv6フラグによってテストの対象を切り替えていました。これはテストケースの定義を複雑にし、見通しを悪くしていました。

新しいテストファイルでは、ipv4MulticastListenerTestsipv6MulticastListenerTestsという2つの独立したテストデータ構造が定義されました。

  • ipv4MulticastListenerTests: IPv4マルチキャストアドレスのみを含みます。
  • ipv6MulticastListenerTests: IPv6マルチキャストアドレスのみを含みます。

これにより、テストケースの定義がより明確になり、各テスト関数が対象とするプロトコルに特化したデータのみを使用するようになりました。

3. マルチキャストインターフェースの明示的な割り当て

元のテストでは、availMulticastInterface関数を使用して、FlagUp | FlagLoopbackなどのフラグを持つ利用可能なマルチキャストインターフェースを検索していました。しかし、この方法ではシステムが自動的に選択するインターフェースに依存するため、テストの安定性が損なわれる可能性がありました。特に、複数のインターフェースが存在する場合や、ネットワーク設定が動的に変化する環境では、テストの再現性が低くなる原因となります。

新しいテストでは、loopbackInterface()nil(システムによる自動選択)の両方を試すループが導入されました。

  • loopbackInterface(): 明示的にループバックインターフェースを指定することで、テストが特定の物理インターフェースに依存せず、ローカル環境で安定して実行されることを保証します。ループバックインターフェースは、ネットワーク接続がなくても利用できるため、テスト環境の独立性を高めます。
  • nil: インターフェースを明示的に指定しない場合、システムが適切なマルチキャストインターフェースを自動的に選択します。これは、実際のアプリケーションがインターフェースを明示的に指定しない場合の挙動をテストするために重要です。ただし、コミットメッセージにもあるように、「システムによるマルチキャストインターフェースの割り当ては、通常、ネットワークとリンク層の両方の隣接性を含む適切なネクストホップを見つけるためのルーティングに依存するため、推奨されません」と注意書きがあります。これは、システムによる自動選択が不安定な結果をもたらす可能性があることを示唆しています。

この変更により、テストはより制御された環境で実行されるようになり、テストの安定性が向上しました。

4. checkMulticastListener関数の改善

元のcheckMulticastListener関数は、t *testing.Tを引数に取り、内部でt.Errorft.Errorを呼び出してエラーを報告していました。これは、テスト関数内でエラーハンドリングを行う必要があり、コードの重複や複雑さを招く可能性がありました。

新しいcheckMulticastListener関数は、errorを返すように変更されました。これにより、テスト関数側でエラーを適切に処理できるようになり、テストロジックがよりクリーンになりました。また、multicastRIBContains関数もerrorを返すように変更され、より堅牢なエラーハンドリングが可能になりました。

5. multicastRIBContains関数のプラットフォーム依存性

multicastRIBContains関数は、特定のIPアドレスがマルチキャストRIBに含まれているかを確認する関数です。この関数は、netbsd, openbsd, plan9, solaris, windows、およびlinuxarmまたはalphaアーキテクチャでは、常にtrueを返すようにスキップされています。これは、これらのプラットフォームではマルチキャストRIBの情報を取得する機能がまだ実装されていないか、またはテスト環境の制約により正確なRIB情報を取得できないためと考えられます。このスキップにより、未実装のプラットフォームでのテスト失敗を防ぎつつ、実装されているプラットフォームでのみRIBのチェックを行うことができます。

6. closer関数の導入

新しいテストでは、closerという匿名関数が導入されました。これは、テスト中に開かれたUDPConnインスタンスを確実にクローズするためのヘルパー関数です。これにより、リソースリークを防ぎ、テストのクリーンアップを簡素化しています。

これらの変更は、Go言語のnetパッケージにおけるマルチキャスト機能のテストの信頼性と安定性を大幅に向上させることを目的としています。

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

このコミットにおけるコアとなるコードの変更は、主に以下の2つのファイルに集約されます。

  1. src/pkg/net/multicast_posix_test.go の削除

    • このファイルは完全に削除されました。元のマルチキャストリスナーテストのロジックとデータ定義が含まれていました。
  2. src/pkg/net/multicast_test.go の新規作成と内容の変更

    • このファイルが新しく作成され、IPv4とIPv6に分割されたマルチキャストリスナーテストが実装されました。

具体的な変更箇所は以下の通りです。

削除されたファイル: src/pkg/net/multicast_posix_test.go

--- a/src/pkg/net/multicast_posix_test.go
+++ /dev/null
@@ -1,180 +0,0 @@
-// Copyright 2011 The Go Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// +build !plan9
-
-package net
-
-import (
-	"errors"
-	"os"
-	"runtime"
-	"testing"
-)
-
-var multicastListenerTests = []struct {
-	net   string
-	gaddr *UDPAddr
-	flags Flags
-	ipv6  bool // test with underlying AF_INET6 socket
-}{
-	// cf. RFC 4727: Experimental Values in IPv4, IPv6, ICMPv4, ICMPv6, UDP, and TCP Headers
-
-	{"udp", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}, FlagUp | FlagLoopback, false},
-	{"udp", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}, 0, false},
-	{"udp", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}, 0, true},
-
-	{"udp4", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}, FlagUp | FlagLoopback, false},
-	{"udp4", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}, 0, false},
-
-	{"udp6", &UDPAddr{IP: ParseIP("ff01::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff01::114"), Port: 12345}, 0, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff02::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff02::114"), Port: 12345}, 0, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff04::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff04::114"), Port: 12345}, 0, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff05::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff05::114"), Port: 12345}, 0, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff08::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff08::114"), Port: 12345}, 0, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}, FlagUp | FlagLoopback, true},
-	{"udp6", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}, 0, true},
-}
-
-// TestMulticastListener tests both single and double listen to a test
-// listener with same address family, same group address and same port.
-func TestMulticastListener(t *testing.T) {
-	switch runtime.GOOS {
-	case "netbsd", "openbsd", "plan9", "solaris", "windows":
-		t.Skipf("skipping test on %q", runtime.GOOS)
-	case "linux":
-		if runtime.GOARCH == "arm" || runtime.GOARCH == "alpha" {
-			t.Skipf("skipping test on %q/%q", runtime.GOOS, runtime.GOARCH)
-		}
-	}
-
-	for _, tt := range multicastListenerTests {
-		if tt.ipv6 && (!*testIPv6 || !supportsIPv6 || os.Getuid() != 0) {
-			continue
-		}
-		ifi, err := availMulticastInterface(t, tt.flags)
-		if err != nil {
-			continue
-		}
-		c1, err := ListenMulticastUDP(tt.net, ifi, tt.gaddr)
-		if err != nil {
-			t.Fatalf("First ListenMulticastUDP failed: %v", err)
-		}
-		checkMulticastListener(t, err, c1, tt.gaddr)
-		c2, err := ListenMulticastUDP(tt.net, ifi, tt.gaddr)
-		if err != nil {
-			t.Fatalf("Second ListenMulticastUDP failed: %v", err)
-		}
-		checkMulticastListener(t, err, c2, tt.gaddr)
-		c2.Close()
-		c1.Close()
-	}
-}
-
-func TestSimpleMulticastListener(t *testing.T) {
-	switch runtime.GOOS {
-	case "plan9":
-		t.Skipf("skipping test on %q", runtime.GOOS)
-	case "windows":
-		if testing.Short() || !*testExternal {
-			t.Skip("skipping test on windows to avoid firewall")
-		}
-	}
-
-	for _, tt := range multicastListenerTests {
-		if tt.ipv6 {
-			continue
-		}
-		tt.flags = FlagUp | FlagMulticast // for windows testing
-		ifi, err := availMulticastInterface(t, tt.flags)
-		if err != nil {
-			continue
-		}
-		c1, err := ListenMulticastUDP(tt.net, ifi, tt.gaddr)
-		if err != nil {
-			t.Fatalf("First ListenMulticastUDP failed: %v", err)
-		}
-		checkSimpleMulticastListener(t, err, c1, tt.gaddr)
-		c2, err := ListenMulticastUDP(tt.net, ifi, tt.gaddr)
-		if err != nil {
-			t.Fatalf("Second ListenMulticastUDP failed: %v", err)
-		}
-		checkSimpleMulticastListener(t, err, c2, tt.gaddr)
-		c2.Close()
-		c1.Close()
-	}
-}
-
-func checkMulticastListener(t *testing.T, err error, c *UDPConn, gaddr *UDPAddr) {
-	if !multicastRIBContains(t, gaddr.IP) {
-		t.Errorf("%q not found in RIB", gaddr.String())
-		return
-	}
-	la := c.LocalAddr()
-	if la == nil {
-		t.Error("LocalAddr failed")
-		return
-	}
-	if a, ok := la.(*UDPAddr); !ok || a.Port == 0 {
-		t.Errorf("got %v; expected a proper address with non-zero port number", la)
-		return
-	}
-}
-
-func checkSimpleMulticastListener(t *testing.T, err error, c *UDPConn, gaddr *UDPAddr) {
-	la := c.LocalAddr()
-	if la == nil {
-		t.Error("LocalAddr failed")
-		return
-	}
-	if a, ok := la.(*UDPAddr); !ok || a.Port == 0 {
-		t.Errorf("got %v; expected a proper address with non-zero port number", la)
-		return
-	}
-}
-
-func availMulticastInterface(t *testing.T, flags Flags) (*Interface, error) {
-	var ifi *Interface
-	if flags != Flags(0) {
-		ift, err := Interfaces()
-		if err != nil {
-			t.Fatalf("Interfaces failed: %v", err)
-		}
-		for _, x := range ift {
-			if x.Flags&flags == flags {
-				ifi = &x
-				break
-			}
-		}
-		if ifi == nil {
-			return nil, errors.New("an appropriate multicast interface not found")
-		}
-	}
-	return ifi, nil
-}
-
-func multicastRIBContains(t *testing.T, ip IP) bool {
-	ift, err := Interfaces()
-	if err != nil {
-		t.Fatalf("Interfaces failed: %v", err)
-	}
-	for _, ifi := range ift {
-		ifmat, err := ifi.MulticastAddrs()
-		if err != nil {
-			t.Fatalf("MulticastAddrs failed: %v", err)
-		}
-		for _, ifma := range ifmat {
-			if ifma.(*IPAddr).IP.Equal(ip) {
-				return true
-			}
-		}
-	}
-	return false
-}

新規作成されたファイル: src/pkg/net/multicast_test.go

--- /dev/null
+++ b/src/pkg/net/multicast_test.go
@@ -0,0 +1,184 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package net
+
+import (
+	"fmt"
+	"os"
+	"runtime"
+	"testing"
+)
+
+var ipv4MulticastListenerTests = []struct {
+	net   string
+	gaddr *UDPAddr // see RFC 4727
+}{
+	{"udp", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}},
+
+	{"udp4", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}},
+}
+
+// TestIPv4MulticastListener tests both single and double listen to a
+// test listener with same address family, same group address and same
+// port.
+func TestIPv4MulticastListener(t *testing.T) {
+	switch runtime.GOOS {
+	case "plan9":
+		t.Skipf("skipping test on %q", runtime.GOOS)
+	}
+
+	closer := func(cs []*UDPConn) {
+		for _, c := range cs {
+			if c != nil {
+				c.Close()
+			}
+		}
+	}
+
+	for _, ifi := range []*Interface{loopbackInterface(), nil} {
+		// Note that multicast interface assignment by system
+		// is not recommended because it usually relies on
+		// routing stuff for finding out an appropriate
+		// nexthop containing both network and link layer
+		// adjacencies.
+		if ifi == nil && !*testExternal {
+			continue
+		}
+		for _, tt := range ipv4MulticastListenerTests {
+			var err error
+			cs := make([]*UDPConn, 2)
+			if cs[0], err = ListenMulticastUDP(tt.net, ifi, tt.gaddr); err != nil {
+				t.Fatalf("First ListenMulticastUDP on %v failed: %v", ifi, err)
+			}
+			if err := checkMulticastListener(cs[0], tt.gaddr.IP); err != nil {
+				closer(cs)
+				t.Fatal(err)
+			}
+			if cs[1], err = ListenMulticastUDP(tt.net, ifi, tt.gaddr); err != nil {
+				closer(cs)
+				t.Fatalf("Second ListenMulticastUDP on %v failed: %v", ifi, err)
+			}
+			if err := checkMulticastListener(cs[1], tt.gaddr.IP); err != nil {
+				closer(cs)
+				t.Fatal(err)
+			}
+			closer(cs)
+		}
+	}
+}
+
+var ipv6MulticastListenerTests = []struct {
+	net   string
+	gaddr *UDPAddr // see RFC 4727
+}{
+	{"udp", &UDPAddr{IP: ParseIP("ff01::114"), Port: 12345}},
+	{"udp", &UDPAddr{IP: ParseIP("ff02::114"), Port: 12345}},
+	{"udp", &UDPAddr{IP: ParseIP("ff04::114"), Port: 12345}},
+	{"udp", &UDPAddr{IP: ParseIP("ff05::114"), Port: 12345}},
+	{"udp", &UDPAddr{IP: ParseIP("ff08::114"), Port: 12345}},
+	{"udp", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}},
+
+	{"udp6", &UDPAddr{IP: ParseIP("ff01::114"), Port: 12345}},
+	{"udp6", &UDPAddr{IP: ParseIP("ff02::114"), Port: 12345}},
+	{"udp6", &UDPAddr{IP: ParseIP("ff04::114"), Port: 12345}},
+	{"udp6", &UDPAddr{IP: ParseIP("ff05::114"), Port: 12345}},
+	{"udp6", &UDPAddr{IP: ParseIP("ff08::114"), Port: 12345}},
+	{"udp6", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}},
+}
+
+// TestIPv6MulticastListener tests both single and double listen to a
+// test listener with same address family, same group address and same
+// port.
+func TestIPv6MulticastListener(t *testing.T) {
+	switch runtime.GOOS {
+	case "plan9", "solaris", "windows":
+		t.Skipf("skipping test on %q", runtime.GOOS)
+	}
+	if !supportsIPv6 {
+		t.Skip("ipv6 is not supported")
+	}
+	if os.Getuid() != 0 {
+		t.Skip("skipping test; must be root")
+	}
+
+	closer := func(cs []*UDPConn) {
+		for _, c := range cs {
+			if c != nil {
+				c.Close()
+			}
+		}
+	}
+
+	for _, ifi := range []*Interface{loopbackInterface(), nil} {
+		// Note that multicast interface assignment by system
+		// is not recommended because it usually relies on
+		// routing stuff for finding out an appropriate
+		// nexthop containing both network and link layer
+		// adjacencies.
+		if ifi == nil && !*testExternal {
+			continue
+		}
+		for _, tt := range ipv6MulticastListenerTests {
+			var err error
+			cs := make([]*UDPConn, 2)
+			if cs[0], err = ListenMulticastUDP(tt.net, ifi, tt.gaddr); err != nil {
+				t.Fatalf("First ListenMulticastUDP on %v failed: %v", ifi, err)
+			}
+			if err := checkMulticastListener(cs[0], tt.gaddr.IP); err != nil {
+				closer(cs)
+				t.Fatal(err)
+			}
+			if cs[1], err = ListenMulticastUDP(tt.net, ifi, tt.gaddr); err != nil {
+				closer(cs)
+				t.Fatalf("Second ListenMulticastUDP on %v failed: %v", ifi, err)
+			}
+			if err := checkMulticastListener(cs[1], tt.gaddr.IP); err != nil {
+				closer(cs)
+				t.Fatal(err)
+			}
+			closer(cs)
+		}
+	}
+}
+
+func checkMulticastListener(c *UDPConn, ip IP) error {
+	if ok, err := multicastRIBContains(ip); err != nil {
+		return err
+	} else if !ok {
+		return fmt.Errorf("%q not found in multicast RIB", ip.String())
+	}
+	la := c.LocalAddr()
+	if la, ok := la.(*UDPAddr); !ok || la.Port == 0 {
+		return fmt.Errorf("got %v; expected a proper address with non-zero port number", la)
+	}
+	return nil
+}
+
+func multicastRIBContains(ip IP) (bool, error) {
+	switch runtime.GOOS {
+	case "netbsd", "openbsd", "plan9", "solaris", "windows":
+		return true, nil // not implemented yet
+	case "linux":
+		if runtime.GOARCH == "arm" || runtime.GOARCH == "alpha" {
+			return true, nil // not implemented yet
+		}
+	}
+	ift, err := Interfaces()
+	if err != nil {
+		return false, err
+	}
+	for _, ifi := range ift {
+		ifmat, err := ifi.MulticastAddrs()
+		if err != nil {
+			return false, err
+		}
+		for _, ifma := range ifmat {
+			if ifma.(*IPAddr).IP.Equal(ip) {
+				return true, nil
+			}
+		}
+	}
+	return false, nil
+}

コアとなるコードの解説

1. multicast_posix_test.go の削除と multicast_test.go の新規作成

この変更の最も根本的な部分は、既存のテストファイルmulticast_posix_test.goを削除し、multicast_test.goという新しいファイルを作成したことです。

  • multicast_posix_test.go: このファイルは、+build !plan9というビルドタグを持っていました。これは、Plan 9以外のPOSIX互換システム(Linux, macOSなど)でビルドされることを意味します。このファイルには、IPv4とIPv6のマルチキャストテストが混在しており、multicastListenerTestsという単一のテストデータセットを使用していました。
  • multicast_test.go: 新しいテストファイルでは、特定のOSに依存しないようにビルドタグが削除されました。これにより、より広範なプラットフォームでテストが実行されるようになります。また、このファイル内でIPv4とIPv6のテストが明確に分離されました。

2. テストデータ構造の分割

元のmulticastListenerTestsは、ipv6というブール値フラグを使ってIPv4とIPv6のテストケースを区別していました。

var multicastListenerTests = []struct {
	net   string
	gaddr *UDPAddr
	flags Flags
	ipv6  bool // test with underlying AF_INET6 socket
}{
	// ... IPv4 and IPv6 test cases mixed ...
}

新しいテストファイルでは、これをipv4MulticastListenerTestsipv6MulticastListenerTestsの2つの独立したデータ構造に分割しました。

var ipv4MulticastListenerTests = []struct {
	net   string
	gaddr *UDPAddr // see RFC 4727
}{
	{"udp", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}},
	{"udp4", &UDPAddr{IP: IPv4(224, 0, 0, 254), Port: 12345}},
}

var ipv6MulticastListenerTests = []struct {
	net   string
	gaddr *UDPAddr // see RFC 4727
}{
	{"udp", &UDPAddr{IP: ParseIP("ff01::114"), Port: 12345}},
	// ... other IPv6 test cases ...
	{"udp6", &UDPAddr{IP: ParseIP("ff0e::114"), Port: 12345}},
}

この分割により、各テスト関数が対象とするIPバージョンに特化したテストデータのみを使用できるようになり、テストの意図がより明確になりました。

3. テスト関数の分割とインターフェースの明示的指定

元のテストファイルにはTestMulticastListenerTestSimpleMulticastListenerという2つのテスト関数がありました。これらはmulticastListenerTestsをループし、ipv6フラグに基づいて条件分岐していました。

新しいテストファイルでは、TestIPv4MulticastListenerTestIPv6MulticastListenerという2つの独立したテスト関数が導入されました。

TestIPv4MulticastListener

func TestIPv4MulticastListener(t *testing.T) {
	switch runtime.GOOS {
	case "plan9":
		t.Skipf("skipping test on %q", runtime.GOOS)
	}

	closer := func(cs []*UDPConn) { /* ... */ } // ヘルパー関数

	for _, ifi := range []*Interface{loopbackInterface(), nil} { // インターフェースのループ
		// ...
		for _, tt := range ipv4MulticastListenerTests { // IPv4テストデータのループ
			// ... ListenMulticastUDP呼び出しとチェック ...
		}
	}
}

TestIPv6MulticastListener

func TestIPv6MulticastListener(t *testing.T) {
	switch runtime.GOOS {
	case "plan9", "solaris", "windows":
		t.Skipf("skipping test on %q", runtime.GOOS)
	}
	if !supportsIPv6 {
		t.Skip("ipv6 is not supported")
	}
	if os.Getuid() != 0 {
		t.Skip("skipping test; must be root")
	}

	closer := func(cs []*UDPConn) { /* ... */ } // ヘルパー関数

	for _, ifi := range []*Interface{loopbackInterface(), nil} { // インターフェースのループ
		// ...
		for _, tt := range ipv6MulticastListenerTests { // IPv6テストデータのループ
			// ... ListenMulticastUDP呼び出しとチェック ...
		}
	}
}

両方のテスト関数で、for _, ifi := range []*Interface{loopbackInterface(), nil}というループが導入されています。

  • loopbackInterface(): システムのループバックインターフェースを明示的に取得します。これにより、ネットワーク接続に依存しない、安定したテスト環境を提供します。
  • nil: インターフェースを明示的に指定しない場合(システムが自動選択する場合)の挙動をテストします。ただし、コメントで「システムによるマルチキャストインターフェースの割り当ては推奨されない」と明記されており、不安定な結果をもたらす可能性があることが示唆されています。

このループにより、各テストケースがループバックインターフェースとシステム自動選択の両方でテストされるようになり、テストの網羅性と安定性が向上しました。

4. checkMulticastListener関数の戻り値の変更

元のcheckMulticastListener関数はt *testing.Tを引数に取り、内部でt.Errorfを呼び出してエラーを報告していました。

func checkMulticastListener(t *testing.T, err error, c *UDPConn, gaddr *UDPAddr) {
	if !multicastRIBContains(t, gaddr.IP) {
		t.Errorf("%q not found in RIB", gaddr.String())
		return
	}
	// ...
}

新しいcheckMulticastListener関数は、エラーが発生した場合にerrorを返すように変更されました。

func checkMulticastListener(c *UDPConn, ip IP) error {
	if ok, err := multicastRIBContains(ip); err != nil {
		return err
	} else if !ok {
		return fmt.Errorf("%q not found in multicast RIB", ip.String())
	}
	la := c.LocalAddr()
	if la, ok := la.(*UDPAddr); !ok || la.Port == 0 {
		return fmt.Errorf("got %v; expected a proper address with non-zero port number", la)
	}
	return nil
}

これにより、テスト関数側でif err := checkMulticastListener(...)のようにエラーを明示的にチェックし、t.Fatal(err)でテストを終了させることができるようになり、テストロジックがよりクリーンで堅牢になりました。

5. multicastRIBContains関数の改善とプラットフォーム依存性

multicastRIBContains関数も、元のt *testing.Tを引数に取る形式から、errorを返す形式に変更されました。

func multicastRIBContains(ip IP) (bool, error) {
	switch runtime.GOOS {
	case "netbsd", "openbsd", "plan9", "solaris", "windows":
		return true, nil // not implemented yet
	case "linux":
		if runtime.GOARCH == "arm" || runtime.GOARCH == "alpha" {
			return true, nil // not implemented yet
		}
	}
	// ... 実際のRIBチェックロジック ...
	return false, nil
}

この関数は、特定のOSやアーキテクチャではマルチキャストRIBのチェックが実装されていないため、true, nilを返すようにスキップされています。これにより、未実装のプラットフォームでのテスト失敗を防ぎつつ、実装されているプラットフォームでのみ正確なチェックが行われるようになります。

これらの変更は、Go言語のマルチキャストテストの安定性、可読性、および保守性を大幅に向上させるものです。

関連リンク

参考にした情報源リンク