Skip to content

Commit 5662998

Browse files
committed
tests: add test cases for the read deadlines
1 parent 5f31a6b commit 5662998

2 files changed

Lines changed: 114 additions & 0 deletions

File tree

image/docker/body_reader_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package docker
22

33
import (
44
"errors"
5+
"fmt"
56
"math"
7+
"net"
68
"net/http"
79
"testing"
810
"time"
@@ -12,6 +14,67 @@ import (
1214
"github.com/stretchr/testify/require"
1315
)
1416

17+
// timeoutError implements net.Error with Timeout() returning true.
18+
type timeoutError struct{}
19+
20+
func (e *timeoutError) Error() string { return "timeout" }
21+
func (e *timeoutError) Timeout() bool { return true }
22+
func (e *timeoutError) Temporary() bool { return false }
23+
24+
// nonTimeoutNetError implements net.Error with Timeout() returning false.
25+
type nonTimeoutNetError struct{}
26+
27+
func (e *nonTimeoutNetError) Error() string { return "non-timeout net error" }
28+
func (e *nonTimeoutNetError) Timeout() bool { return false }
29+
func (e *nonTimeoutNetError) Temporary() bool { return false }
30+
31+
func TestIsRetryableNetworkError(t *testing.T) {
32+
for _, c := range []struct {
33+
name string
34+
err error
35+
expected bool
36+
}{
37+
{
38+
// A direct timeout error from the network layer should be retryable.
39+
name: "net.Error with Timeout() true",
40+
err: &timeoutError{},
41+
expected: true,
42+
},
43+
{
44+
// Timeout errors wrapped by fmt.Errorf should still be detected
45+
// via errors.As unwrapping.
46+
name: "wrapped net.Error with Timeout() true",
47+
err: fmt.Errorf("read failed: %w", &timeoutError{}),
48+
expected: true,
49+
},
50+
{
51+
// A net.Error that is not a timeout (e.g. a connection refused)
52+
// should not be retryable.
53+
name: "net.Error with Timeout() false",
54+
err: &nonTimeoutNetError{},
55+
expected: false,
56+
},
57+
{
58+
// A plain error with no net.Error in the chain should not be retryable.
59+
name: "plain error",
60+
err: errors.New("something broke"),
61+
expected: false,
62+
},
63+
{
64+
// net.OpError is the concrete type returned by real socket operations;
65+
// verify that errors.As can unwrap through it to find the timeout.
66+
name: "net.OpError wrapping timeout",
67+
err: &net.OpError{Op: "read", Err: &timeoutError{}},
68+
expected: true,
69+
},
70+
} {
71+
t.Run(c.name, func(t *testing.T) {
72+
result := isRetryableNetworkError(c.err)
73+
assert.Equal(t, c.expected, result)
74+
})
75+
}
76+
}
77+
1578
func TestParseDecimalInString(t *testing.T) {
1679
for _, prefix := range []string{"", "text", "0"} {
1780
for _, suffix := range []string{"", "text"} {

image/docker/docker_client_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"context"
77
"fmt"
88
"io"
9+
"net"
910
"net/http"
1011
"net/http/httptest"
1112
"net/url"
@@ -20,6 +21,56 @@ import (
2021
"go.podman.io/image/v5/types"
2122
)
2223

24+
// TestDeadlineConnReadTimeout verifies that a stalled connection (no data
25+
// arriving) returns a timeout error after the configured readTimeout.
26+
// This is the key behavior that lets bodyReader treat it as a reconnectable
27+
// condition and resume the download with a Range request.
28+
func TestDeadlineConnReadTimeout(t *testing.T) {
29+
server, client := net.Pipe()
30+
defer server.Close()
31+
defer client.Close()
32+
33+
dc := &deadlineConn{
34+
Conn: client,
35+
readTimeout: 50 * time.Millisecond,
36+
}
37+
38+
// No data is written to the server side, so the read should time out.
39+
buf := make([]byte, 64)
40+
_, err := dc.Read(buf)
41+
require.Error(t, err)
42+
43+
// Verify the error satisfies net.Error and reports as a timeout, which
44+
// is what isRetryableNetworkError checks.
45+
var netErr net.Error
46+
require.ErrorAs(t, err, &netErr)
47+
assert.True(t, netErr.Timeout(), "expected a timeout error")
48+
}
49+
50+
// TestDeadlineConnReadSuccess verifies that the deadline wrapper does not
51+
// interfere with normal reads — when data arrives promptly, it is returned
52+
// without error.
53+
func TestDeadlineConnReadSuccess(t *testing.T) {
54+
server, client := net.Pipe()
55+
defer server.Close()
56+
defer client.Close()
57+
58+
dc := &deadlineConn{
59+
Conn: client,
60+
readTimeout: 5 * time.Second,
61+
}
62+
63+
expected := []byte("hello")
64+
go func() {
65+
_, _ = server.Write(expected)
66+
}()
67+
68+
buf := make([]byte, 64)
69+
n, err := dc.Read(buf)
70+
require.NoError(t, err)
71+
assert.Equal(t, expected, buf[:n])
72+
}
73+
2374
func TestDockerCertDir(t *testing.T) {
2475
const nondefaultFullPath = "/this/is/not/the/default/full/path"
2576
const nondefaultPerHostDir = "/this/is/not/the/default/certs.d"

0 commit comments

Comments
 (0)