Skip to content

Commit ddd4b53

Browse files
committed
tests: add test cases for the read deadlines
Signed-off-by: Ryan Phillips <rphillips@redhat.com>
1 parent 63460d8 commit ddd4b53

2 files changed

Lines changed: 109 additions & 0 deletions

File tree

image/docker/body_reader_test.go

Lines changed: 58 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,62 @@ import (
1214
"github.com/stretchr/testify/require"
1315
)
1416

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