[インデックス 19012] ファイルの概要
このコミットは、Go言語の標準ライブラリである crypto/tls
パッケージのテストファイル src/pkg/crypto/tls/tls_test.go
に関連するものです。このファイルは、TLS (Transport Layer Security) プロトコルを実装したGoの crypto/tls
パッケージの機能が正しく動作するかを検証するための単体テストや統合テストを含んでいます。具体的には、TLS接続の確立、データの送受信、エラーハンドリング、タイムアウト処理など、様々なシナリオがテストされています。
コミット
commit 84db9e09d9e3ff7db8aa8c49282487beacecea07
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Wed Apr 2 14:31:57 2014 -0700
crypto/tls: deflake TestConnReadNonzeroAndEOF
Fixes #7683
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/83080048
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/84db9e09d9e3ff7db8aa8c49282487beacecea07
元コミット内容
crypto/tls: TestConnReadNonzeroAndEOF の不安定性を解消 (deflake)
このコミットは、crypto/tls
パッケージ内の TestConnReadNonzeroAndEOF
というテストが時折失敗する「不安定性 (flakiness)」を修正することを目的としています。具体的には、Issue #7683 を解決します。
変更の背景
TestConnReadNonzeroAndEOF
テストは、TLS接続において、アプリケーションデータが送信された直後に接続がクローズされた(CloseNotify
アラートが送信された)場合に、io.Reader
インターフェースを実装する Conn.Read
メソッドがどのように振る舞うかを検証するものです。具体的には、データが読み取られた後に io.EOF
が返されることを期待しています。
しかし、このテストは「不安定 (flaky)」でした。これは、テストが実行される環境やタイミングによって、成功したり失敗したりする現象を指します。コミットメッセージに記載されているように、このテストは「ローカルホストのTCP接続への書き込み後、ピアのTCP接続がそれをすぐに読み取れると仮定しているため、競合状態にある」ことが原因でした。
TCP/IPネットワーク通信では、データが送信されてから受信側で完全に処理されるまでには、わずかながら時間差が生じます。特に、srv.Close()
が呼び出された際に送信される CloseNotify
アラートは、アプリケーションデータとは異なるTCPセグメントで送信される可能性があり、受信側がアプリケーションデータを読み取った直後に EOF
を検出できるという保証はありませんでした。テストがこのタイミングのずれを考慮していなかったため、Read
が io.EOF
を返す前にテストがタイムアウトしたり、予期せぬ結果になったりすることがありました。この不安定性は、CI/CD環境など、様々な負荷条件下でテストが実行される場合に顕著になります。
前提知識の解説
TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間の通信を盗聴、改ざん、なりすましから保護します。Go言語の crypto/tls
パッケージは、このTLSプロトコルをGoアプリケーションで利用するための機能を提供します。
io.EOF
Go言語の io
パッケージで定義されている EOF
(End Of File) は、入力ストリームの終端に達したことを示すエラーです。io.Reader
インターフェースを実装するメソッド(例: Read
)が、これ以上読み取るデータがない場合に、読み取ったバイト数と共に io.EOF
を返します。これは、ファイルやネットワーク接続など、様々な入力ソースの終端を示すために使われます。
競合状態 (Race Condition) と不安定なテスト (Flaky Test)
競合状態とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、その実行順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、サーバーがデータを書き込み、接続をクローズするタイミングと、クライアントがデータを読み取り、EOF
を検出するタイミングの間に競合状態が存在していました。
不安定なテスト (Flaky Test) は、同じコードに対して同じテストを複数回実行したときに、成功したり失敗したりするテストのことです。これは、テストが外部要因(ネットワークの遅延、OSのスケジューリング、他のテストとの干渉など)に依存している場合や、競合状態を含んでいる場合に発生しやすいです。不安定なテストは、開発者がコードの変更が本当に問題を修正したのか、それとも単にテストがたまたま成功しただけなのかを判断することを困難にし、CI/CDパイプラインの信頼性を損ないます。
testing.Short()
Go言語の testing
パッケージには testing.Short()
という関数があります。これは、テストが「ショートモード」で実行されているかどうかを判定するために使用されます。go test -short
コマンドでテストを実行すると、testing.Short()
は true
を返します。これにより、時間のかかるテストや、外部リソースに依存するテスト、あるいは不安定なテストを、通常の開発サイクルではスキップし、より完全なテストスイート(例: CI環境)でのみ実行するように制御できます。
技術的詳細
このコミットは、TestConnReadNonzeroAndEOF
テストの不安定性を解消するために、以下の技術的なアプローチを採用しています。
-
テストの分離とリトライロジックの導入:
- 元の
TestConnReadNonzeroAndEOF
関数は、新しいヘルパー関数testConnReadNonzeroAndEOF
を呼び出す形に変更されました。 TestConnReadNonzeroAndEOF
は、testConnReadNonzeroAndEOF
を複数回リトライするループを持つようになりました。このリトライは、delay
というパラメータを指数関数的に増加させながら行われます(time.Millisecond
から64*time.Millisecond
まで倍々に増加)。- いずれかのリトライでテストが成功すれば、全体のテストは成功とみなされます。
- 元の
-
意図的な遅延の導入:
- 新しいヘルパー関数
testConnReadNonzeroAndEOF
の中で、サーバー側がsrv.Close()
を呼び出した直後にtime.Sleep(delay)
が追加されました。 - この
delay
は、クライアント側がio.EOF
を検出する前に、サーバー側からのCloseNotify
アラートがネットワークを介してクライアントに到達し、処理されるための時間を与えます。これにより、ネットワークのタイミングに起因する競合状態が緩和されます。
- 新しいヘルパー関数
-
ショートモードでのスキップ:
TestConnReadNonzeroAndEOF
の冒頭でif testing.Short() { t.Skip("skipping in short mode") }
というチェックが追加されました。- これにより、
go test -short
でテストを実行した場合、この不安定なテストはスキップされ、開発者のローカル環境での迅速なテスト実行を妨げなくなります。これは、テストが本質的に競合状態を含み、完全に予測可能な動作を保証するのが難しい場合に、実用的な解決策としてよく用いられます。
-
エラーハンドリングの改善:
- サーバー側のゴルーチン内で発生するエラー(
ln.Accept()
やsrv.Handshake()
のエラー)が、以前はt.Error()
で直接報告されていましたが、serr
変数に格納され、チャネルを通じてメインのテスト関数に返されるようになりました。これにより、エラーが適切に伝播され、リトライロジックが機能するようになりました。
- サーバー側のゴルーチン内で発生するエラー(
これらの変更により、テストはネットワークのタイミングの変動に対してより堅牢になり、不安定な失敗が減少しました。
コアとなるコードの変更箇所
src/pkg/crypto/tls/tls_test.go
ファイルにおいて、主に以下の変更が行われました。
--- a/src/pkg/crypto/tls/tls_test.go
+++ b/src/pkg/crypto/tls/tls_test.go
@@ -5,6 +5,7 @@
package tls
import (
+\t"fmt"
\t"io"
\t"net"
\t"strings"
@@ -161,21 +162,41 @@ func TestDialTimeout(t *testing.T) {
// (non-zero, nil) when a Close (alertCloseNotify) is sitting right
// behind the application data in the buffer.
func TestConnReadNonzeroAndEOF(t *testing.T) {
+\t// This test is racy: it assumes that after a write to a
+\t// localhost TCP connection, the peer TCP connection can
+\t// immediately read it. Because it's racy, we skip this test
+\t// in short mode, and then retry it several times with an
+\t// increasing sleep in between our final write (via srv.Close
+\t// below) and the following read.
+\tif testing.Short() {
+\t\tt.Skip("skipping in short mode")
+\t}\n+\tvar err error
+\tfor delay := time.Millisecond; delay <= 64*time.Millisecond; delay *= 2 {\n+\t\tif err = testConnReadNonzeroAndEOF(t, delay); err == nil {\n+\t\t\treturn
+\t\t}\n+\t}\n+\tt.Error(err)
+}\n+\n+func testConnReadNonzeroAndEOF(t *testing.T, delay time.Duration) error {
\tln := newLocalListener(t)
\tdefer ln.Close()\n \n \tsrvCh := make(chan *Conn, 1)\n+\tvar serr error
\tgo func() {\n \t\tsconn, err := ln.Accept()\n \t\tif err != nil {\n-\t\t\tt.Error(err)\n+\t\t\tserr = err
\t\t\tsrvCh <- nil
\t\t\treturn
\t\t}\n \t\tserverConfig := *testConfig
\t\tsrv := Server(sconn, &serverConfig)\n \t\tif err := srv.Handshake(); err != nil {\n-\t\t\tt.Error("handshake: %v", err)\n+\t\t\tserr = fmt.Errorf("handshake: %v", err)
\t\t\tsrvCh <- nil
\t\t\treturn
\t\t}\n@@ -191,7 +212,7 @@ func TestConnReadNonzeroAndEOF(t *testing.T) {\n \n \tsrv := <-srvCh\n \tif srv == nil {\n-\t\treturn
+\t\treturn serr
\t}\n \n \tbuf := make([]byte, 6)\n@@ -199,16 +220,18 @@ func TestConnReadNonzeroAndEOF(t *testing.T) {\n \tsrv.Write([]byte("foobar"))\n \tn, err := conn.Read(buf)\n \tif n != 6 || err != nil || string(buf) != "foobar" {\n-\t\tt.Fatalf("Read = %d, %v, data %q; want 6, nil, foobar", n, err, buf)\n+\t\treturn fmt.Errorf("Read = %d, %v, data %q; want 6, nil, foobar", n, err, buf)
\t}\n \n \tsrv.Write([]byte("abcdef"))\n \tsrv.Close()\n+\ttime.Sleep(delay)
\tn, err = conn.Read(buf)\n \tif n != 6 || string(buf) != "abcdef" {\n-\t\tt.Fatalf("Read = %d, buf= %q; want 6, abcdef", n, buf)\n+\t\treturn fmt.Errorf("Read = %d, buf= %q; want 6, abcdef", n, buf)
\t}\n \tif err != io.EOF {\n-\t\tt.Errorf("Second Read error = %v; want io.EOF", err)\n+\t\treturn fmt.Errorf("Second Read error = %v; want io.EOF", err)
\t}\n+\treturn nil
}\n```
## コアとなるコードの解説
### `TestConnReadNonzeroAndEOF` の変更点
* **`testing.Short()` によるスキップ**:
```go
if testing.Short() {
t.Skip("skipping in short mode")
}
```
このコードは、テストがショートモードで実行されている場合に、テスト全体をスキップします。これにより、開発者がローカルで迅速にテストを実行する際に、この不安定なテストによる遅延や失敗を避けることができます。
* **リトライループ**:
```go
var err error
for delay := time.Millisecond; delay <= 64*time.Millisecond; delay *= 2 {
if err = testConnReadNonzeroAndEOF(t, delay); err == nil {
return
}
}
t.Error(err)
```
このループは、`testConnReadNonzeroAndEOF` ヘルパー関数を複数回呼び出します。`delay` 変数は `time.Millisecond` から始まり、ループごとに倍増していきます。もし `testConnReadNonzeroAndEOF` がエラーなく完了すれば、テストは成功とみなされ、関数は終了します。すべてのリトライが失敗した場合、最後に発生したエラーが `t.Error(err)` によって報告されます。これにより、一時的なネットワークのタイミングの問題による失敗を吸収し、テストの信頼性を向上させます。
### `testConnReadNonzeroAndEOF` ヘルパー関数の追加
* **新しい関数シグネチャ**:
```go
func testConnReadNonzeroAndEOF(t *testing.T, delay time.Duration) error {
```
元のテストロジックがこの新しい関数に移動され、`delay` という `time.Duration` 型のパラメータを受け取るようになりました。この関数はエラーを返すことで、リトライロジックにテストの成否を伝えます。
* **サーバー側エラーの伝播**:
```go
var serr error
go func() {
sconn, err := ln.Accept()
if err != nil {
serr = err
srvCh <- nil
return
}
// ...
if err := srv.Handshake(); err != nil {
serr = fmt.Errorf("handshake: %v", err)
srvCh <- nil
return
}
// ...
}()
srv := <-srvCh
if srv == nil {
return serr
}
```
サーバー側のゴルーチン内で発生したエラー(`Accept` や `Handshake`)は、`serr` 変数に格納され、`srvCh` チャネルを通じてメインのテスト関数に通知されます。これにより、サーバー側のセットアップ中に問題が発生した場合でも、テストが適切にエラーを報告し、リトライロジックが機能するようになります。
* **`srv.Close()` 後の遅延**:
```go
srv.Write([]byte("abcdef"))
srv.Close()
time.Sleep(delay) // <-- 追加された行
n, err = conn.Read(buf)
```
これが最も重要な変更点です。サーバーがデータを書き込み、接続をクローズした直後に、`delay` で指定された時間だけスリープします。このスリープにより、サーバーが `CloseNotify` アラートを送信し、それがクライアントに到達して処理されるための十分な時間が確保されます。これにより、クライアントの `conn.Read(buf)` が `io.EOF` を正しく検出する可能性が高まり、競合状態が解消されます。
* **エラーの返却**:
テストの各アサーションが失敗した場合、以前は `t.Fatalf` や `t.Errorf` を直接呼び出していましたが、新しいヘルパー関数では `fmt.Errorf` を使ってエラーを構築し、それを `return` するようになりました。これにより、呼び出し元の `TestConnReadNonzeroAndEOF` 関数がエラーを捕捉し、リトライロジックを適切に実行できます。
これらの変更は、テストのロバスト性を高め、ネットワークのタイミングに依存する不安定な挙動を抑制することを目的としています。
## 関連リンク
* Go GitHub Issue #7683: [https://github.com/golang/go/issues/7683](https://github.com/golang/go/issues/7683)
* Go Code Review 83080048: [https://golang.org/cl/83080048](https://golang.org/cl/83080048)
## 参考にした情報源リンク
* Go `testing` package documentation: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go `io` package documentation: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go `crypto/tls` package documentation: [https://pkg.go.dev/crypto/tls](https://pkg.go.dev/crypto/tls)
* Race condition (Wikipedia): [https://en.wikipedia.org/wiki/Race_condition](https://en.wikipedia.org/wiki/Race_condition)
* Flaky tests (various software testing resources)```markdown
# [インデックス 19012] ファイルの概要
このコミットは、Go言語の標準ライブラリである `crypto/tls` パッケージのテストファイル `src/pkg/crypto/tls/tls_test.go` に関連するものです。このファイルは、TLS (Transport Layer Security) プロトコルを実装したGoの `crypto/tls` パッケージの機能が正しく動作するかを検証するための単体テストや統合テストを含んでいます。具体的には、TLS接続の確立、データの送受信、エラーハンドリング、タイムアウト処理など、様々なシナリオがテストされています。
## コミット
commit 84db9e09d9e3ff7db8aa8c49282487beacecea07 Author: Brad Fitzpatrick bradfitz@golang.org Date: Wed Apr 2 14:31:57 2014 -0700
crypto/tls: deflake TestConnReadNonzeroAndEOF
Fixes #7683
LGTM=rsc
R=rsc
CC=golang-codereviews
https://golang.org/cl/83080048
## GitHub上でのコミットページへのリンク
[https://github.com/golang/go/commit/84db9e09d9e3ff7db8aa8c49282487beacecea07](https://github.com/golang/go/commit/84db9e09d9e3ff7db8aa8c49282487beacecea07)
## 元コミット内容
`crypto/tls: TestConnReadNonzeroAndEOF の不安定性を解消 (deflake)`
このコミットは、`crypto/tls` パッケージ内の `TestConnReadNonzeroAndEOF` というテストが時折失敗する「不安定性 (flakiness)」を修正することを目的としています。具体的には、Issue #7683 を解決します。
## 変更の背景
`TestConnReadNonzeroAndEOF` テストは、TLS接続において、アプリケーションデータが送信された直後に接続がクローズされた(`CloseNotify` アラートが送信された)場合に、`io.Reader` インターフェースを実装する `Conn.Read` メソッドがどのように振る舞うかを検証するものです。具体的には、データが読み取られた後に `io.EOF` が返されることを期待しています。
しかし、このテストは「不安定 (flaky)」でした。これは、テストが実行される環境やタイミングによって、成功したり失敗したりする現象を指します。コミットメッセージに記載されているように、このテストは「ローカルホストのTCP接続への書き込み後、ピアのTCP接続がそれをすぐに読み取れると仮定しているため、競合状態にある」ことが原因でした。
TCP/IPネットワーク通信では、データが送信されてから受信側で完全に処理されるまでには、わずかながら時間差が生じます。特に、`srv.Close()` が呼び出された際に送信される `CloseNotify` アラートは、アプリケーションデータとは異なるTCPセグメントで送信される可能性があり、受信側がアプリケーションデータを読み取った直後に `EOF` を検出できるという保証はありませんでした。テストがこのタイミングのずれを考慮していなかったため、`Read` が `io.EOF` を返す前にテストがタイムアウトしたり、予期せぬ結果になったりすることがありました。この不安定性は、CI/CD環境など、様々な負荷条件下でテストが実行される場合に顕著になります。
## 前提知識の解説
### TLS (Transport Layer Security)
TLSは、インターネット上で安全な通信を行うための暗号化プロトコルです。ウェブブラウジング(HTTPS)、電子メール、VoIPなど、様々なアプリケーションで利用されています。TLSは、クライアントとサーバー間の通信を盗聴、改ざん、なりすましから保護します。Go言語の `crypto/tls` パッケージは、このTLSプロトコルをGoアプリケーションで利用するための機能を提供します。
### `io.EOF`
Go言語の `io` パッケージで定義されている `EOF` (End Of File) は、入力ストリームの終端に達したことを示すエラーです。`io.Reader` インターフェースを実装するメソッド(例: `Read`)が、これ以上読み取るデータがない場合に、読み取ったバイト数と共に `io.EOF` を返します。これは、ファイルやネットワーク接続など、様々な入力ソースの終端を示すために使われます。
### 競合状態 (Race Condition) と不安定なテスト (Flaky Test)
**競合状態**とは、複数の並行プロセスやスレッドが共有リソースにアクセスする際に、その実行順序によって結果が非決定的に変わってしまう状態を指します。今回のケースでは、サーバーがデータを書き込み、接続をクローズするタイミングと、クライアントがデータを読み取り、`EOF` を検出するタイミングの間に競合状態が存在していました。
**不安定なテスト (Flaky Test)** は、同じコードに対して同じテストを複数回実行したときに、成功したり失敗したりするテストのことです。これは、テストが外部要因(ネットワークの遅延、OSのスケジューリング、他のテストとの干渉など)に依存している場合や、競合状態を含んでいる場合に発生しやすいです。不安定なテストは、開発者がコードの変更が本当に問題を修正したのか、それとも単にテストがたまたま成功しただけなのかを判断することを困難にし、CI/CDパイプラインの信頼性を損ないます。
### `testing.Short()`
Go言語の `testing` パッケージには `testing.Short()` という関数があります。これは、テストが「ショートモード」で実行されているかどうかを判定するために使用されます。`go test -short` コマンドでテストを実行すると、`testing.Short()` は `true` を返します。これにより、時間のかかるテストや、外部リソースに依存するテスト、あるいは不安定なテストを、通常の開発サイクルではスキップし、より完全なテストスイート(例: CI環境)でのみ実行するように制御できます。
## 技術的詳細
このコミットは、`TestConnReadNonzeroAndEOF` テストの不安定性を解消するために、以下の技術的なアプローチを採用しています。
1. **テストの分離とリトライロジックの導入**:
* 元の `TestConnReadNonzeroAndEOF` 関数は、新しいヘルパー関数 `testConnReadNonzeroAndEOF` を呼び出す形に変更されました。
* `TestConnReadNonzeroAndEOF` は、`testConnReadNonzeroAndEOF` を複数回リトライするループを持つようになりました。このリトライは、`delay` というパラメータを指数関数的に増加させながら行われます(`time.Millisecond` から `64*time.Millisecond` まで倍々に増加)。
* いずれかのリトライでテストが成功すれば、全体のテストは成功とみなされます。
2. **意図的な遅延の導入**:
* 新しいヘルパー関数 `testConnReadNonzeroAndEOF` の中で、サーバー側が `srv.Close()` を呼び出した直後に `time.Sleep(delay)` が追加されました。
* この `delay` は、クライアント側が `io.EOF` を検出する前に、サーバー側からの `CloseNotify` アラートがネットワークを介してクライアントに到達し、処理されるための時間を与えます。これにより、ネットワークのタイミングに起因する競合状態が緩和されます。
3. **ショートモードでのスキップ**:
* `TestConnReadNonzeroAndEOF` の冒頭で `if testing.Short() { t.Skip("skipping in short mode") }` というチェックが追加されました。
* これにより、`go test -short` でテストを実行した場合、この不安定なテストはスキップされ、開発者のローカル環境での迅速なテスト実行を妨げなくなります。これは、テストが本質的に競合状態を含み、完全に予測可能な動作を保証するのが難しい場合に、実用的な解決策としてよく用いられます。
4. **エラーハンドリングの改善**:
* サーバー側のゴルーチン内で発生するエラー(`ln.Accept()` や `srv.Handshake()` のエラー)が、以前は `t.Error()` で直接報告されていましたが、`serr` 変数に格納され、チャネルを通じてメインのテスト関数に返されるようになりました。これにより、エラーが適切に伝播され、リトライロジックが機能するようになりました。
これらの変更により、テストはネットワークのタイミングの変動に対してより堅牢になり、不安定な失敗が減少しました。
## コアとなるコードの変更箇所
`src/pkg/crypto/tls/tls_test.go` ファイルにおいて、主に以下の変更が行われました。
```diff
--- a/src/pkg/crypto/tls/tls_test.go
+++ b/src/pkg/crypto/tls/tls_test.go
@@ -5,6 +5,7 @@
package tls
import (
+\t"fmt"
\t"io"
\t"net"
\t"strings"
@@ -161,21 +162,41 @@ func TestDialTimeout(t *testing.T) {
// (non-zero, nil) when a Close (alertCloseNotify) is sitting right
// behind the application data in the buffer.
func TestConnReadNonzeroAndEOF(t *testing.T) {
+\t// This test is racy: it assumes that after a write to a
+\t// localhost TCP connection, the peer TCP connection can
+\t// immediately read it. Because it's racy, we skip this test
+\t// in short mode, and then retry it several times with an
+\t// increasing sleep in between our final write (via srv.Close
+\t// below) and the following read.
+\tif testing.Short() {
+\t\tt.Skip("skipping in short mode")
+\t}\n+\tvar err error
+\tfor delay := time.Millisecond; delay <= 64*time.Millisecond; delay *= 2 {\n+\t\tif err = testConnReadNonzeroAndEOF(t, delay); err == nil {\n+\t\t\treturn
+\t\t}\n+\t}\n+\tt.Error(err)
+}\n+\n+func testConnReadNonzeroAndEOF(t *testing.T, delay time.Duration) error {
\tln := newLocalListener(t)
\tdefer ln.Close()\n \n \tsrvCh := make(chan *Conn, 1)\n+\tvar serr error
\tgo func() {\n \t\tsconn, err := ln.Accept()\n \t\tif err != nil {\n-\t\t\tt.Error(err)\n+\t\t\tserr = err
\t\t\tsrvCh <- nil
\t\t\treturn
\t\t}\n \t\tserverConfig := *testConfig
\t\tsrv := Server(sconn, &serverConfig)\n \t\tif err := srv.Handshake(); err != nil {\n-\t\t\tt.Error("handshake: %v", err)\n+\t\t\tserr = fmt.Errorf("handshake: %v", err)
\t\t\tsrvCh <- nil
\t\t\treturn
\t\t}\n@@ -191,7 +212,7 @@ func TestConnReadNonzeroAndEOF(t *testing.T) {\n \n \tsrv := <-srvCh\n \tif srv == nil {\n-\t\treturn
+\t\treturn serr
\t}\n \n \tbuf := make([]byte, 6)\n@@ -199,16 +220,18 @@ func TestConnReadNonzeroAndEOF(t *testing.T) {\n \tsrv.Write([]byte("foobar"))\n \tn, err := conn.Read(buf)\n \tif n != 6 || err != nil || string(buf) != "foobar" {\n-\t\tt.Fatalf("Read = %d, %v, data %q; want 6, nil, foobar", n, err, buf)\n+\t\treturn fmt.Errorf("Read = %d, %v, data %q; want 6, nil, foobar", n, err, buf)
\t}\n \n \tsrv.Write([]byte("abcdef"))\n \tsrv.Close()\n+\ttime.Sleep(delay)
\tn, err = conn.Read(buf)\n \tif n != 6 || string(buf) != "abcdef" {\n-\t\tt.Fatalf("Read = %d, buf= %q; want 6, abcdef", n, buf)\n+\t\treturn fmt.Errorf("Read = %d, buf= %q; want 6, abcdef", n, buf)
\t}\n \tif err != io.EOF {\n-\t\tt.Errorf("Second Read error = %v; want io.EOF", err)\n+\t\treturn fmt.Errorf("Second Read error = %v; want io.EOF", err)
\t}\n+\treturn nil
}\n```
## コアとなるコードの解説
### `TestConnReadNonzeroAndEOF` の変更点
* **`testing.Short()` によるスキップ**:
```go
if testing.Short() {
t.Skip("skipping in short mode")
}
```
このコードは、テストがショートモードで実行されている場合に、テスト全体をスキップします。これにより、開発者がローカルで迅速にテストを実行する際に、この不安定なテストによる遅延や失敗を避けることができます。
* **リトライループ**:
```go
var err error
for delay := time.Millisecond; delay <= 64*time.Millisecond; delay *= 2 {
if err = testConnReadNonzeroAndEOF(t, delay); err == nil {
return
}
}
t.Error(err)
```
このループは、`testConnReadNonzeroAndEOF` ヘルパー関数を複数回呼び出します。`delay` 変数は `time.Millisecond` から始まり、ループごとに倍増していきます。もし `testConnReadNonzeroAndEOF` がエラーなく完了すれば、テストは成功とみなされ、関数は終了します。すべてのリトライが失敗した場合、最後に発生したエラーが `t.Error(err)` によって報告されます。これにより、一時的なネットワークのタイミングの問題による失敗を吸収し、テストの信頼性を向上させます。
### `testConnReadNonzeroAndEOF` ヘルパー関数の追加
* **新しい関数シグネチャ**:
```go
func testConnReadNonzeroAndEOF(t *testing.T, delay time.Duration) error {
```
元のテストロジックがこの新しい関数に移動され、`delay` という `time.Duration` 型のパラメータを受け取るようになりました。この関数はエラーを返すことで、リトライロジックにテストの成否を伝えます。
* **サーバー側エラーの伝播**:
```go
var serr error
go func() {
sconn, err := ln.Accept()
if err != nil {
serr = err
srvCh <- nil
return
}
// ...
if err := srv.Handshake(); err != nil {
serr = fmt.Errorf("handshake: %v", err)
srvCh <- nil
return
}
// ...
}()
srv := <-srvCh
if srv == nil {
return serr
}
```
サーバー側のゴルーチン内で発生したエラー(`Accept` や `Handshake`)は、`serr` 変数に格納され、`srvCh` チャネルを通じてメインのテスト関数に通知されます。これにより、サーバー側のセットアップ中に問題が発生した場合でも、テストが適切にエラーを報告し、リトライロジックが機能するようになります。
* **`srv.Close()` 後の遅延**:
```go
srv.Write([]byte("abcdef"))
srv.Close()
time.Sleep(delay) // <-- 追加された行
n, err = conn.Read(buf)
```
これが最も重要な変更点です。サーバーがデータを書き込み、接続をクローズした直後に、`delay` で指定された時間だけスリープします。このスリープにより、サーバーが `CloseNotify` アラートを送信し、それがクライアントに到達して処理されるための十分な時間が確保されます。これにより、クライアントの `conn.Read(buf)` が `io.EOF` を正しく検出する可能性が高まり、競合状態が解消されます。
* **エラーの返却**:
テストの各アサーションが失敗した場合、以前は `t.Fatalf` や `t.Errorf` を直接呼び出していましたが、新しいヘルパー関数では `fmt.Errorf` を使ってエラーを構築し、それを `return` するようになりました。これにより、呼び出し元の `TestConnReadNonzeroAndEOF` 関数がエラーを捕捉し、リトライロジックを適切に実行できます。
これらの変更は、テストのロバスト性を高め、ネットワークのタイミングに依存する不安定な挙動を抑制することを目的としています。
## 関連リンク
* Go GitHub Issue #7683: [https://github.com/golang/go/issues/7683](https://github.com/golang/go/issues/7683)
* Go Code Review 83080048: [https://golang.org/cl/83080048](https://golang.org/cl/83080048)
## 参考にした情報源リンク
* Go `testing` package documentation: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go `io` package documentation: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go `crypto/tls` package documentation: [https://pkg.go.dev/crypto/tls](https://pkg.go.dev/crypto/tls)
* Race condition (Wikipedia): [https://en.wikipedia.org/wiki/Race_condition](https://en.wikipedia.org/wiki/Race_condition)
* Flaky tests (various software testing resources)