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

[インデックス 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 を検出できるという保証はありませんでした。テストがこのタイミングのずれを考慮していなかったため、Readio.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 ファイルにおいて、主に以下の変更が行われました。

--- 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)