1package docker
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "testing"
8
9 "github.com/moby/moby/api/types/container"
10 "github.com/moby/moby/api/types/system"
11 "github.com/moby/moby/client"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14
15 "github.com/bnema/gordon/internal/domain"
16)
17
18func podmanVersion(version string) client.ServerVersionResult {
19 return client.ServerVersionResult{
20 Version: version,
21 Components: []system.ComponentVersion{{Name: "Podman Engine", Version: version}},
22 }
23}
24
25func dockerVersion(version string) client.ServerVersionResult {
26 return client.ServerVersionResult{
27 Version: version,
28 Components: []system.ComponentVersion{{Name: "Engine", Version: version}},
29 }
30}
31
32func TestBuildDeviceRequests_EmptyIsNil(t *testing.T) {
33 assert.Nil(t, buildDeviceRequests(&domain.ContainerConfig{}))
34 assert.Nil(t, buildDeviceRequests(&domain.ContainerConfig{CDIDevices: nil}))
35}
36
37func TestBuildDeviceRequests_OneNativeCDIRequest(t *testing.T) {
38 requests := buildDeviceRequests(&domain.ContainerConfig{
39 CDIDevices: []string{"example.com/gpu=GPU-b", "example.com/gpu=GPU-a"},
40 })
41 require.Len(t, requests, 1, "all CDI IDs travel in one native DeviceRequest")
42 request := requests[0]
43 assert.Equal(t, container.DeviceRequest{
44 Driver: "cdi",
45 DeviceIDs: []string{"example.com/gpu=GPU-a", "example.com/gpu=GPU-b"},
46 }, request, "driver is cdi, capabilities/options stay empty, count stays 0, IDs sorted")
47}
48
49func TestBuildDeviceRequests_DoesNotMutateConfig(t *testing.T) {
50 config := &domain.ContainerConfig{CDIDevices: []string{"example.com/gpu=GPU-b", "example.com/gpu=GPU-a"}}
51 _ = buildDeviceRequests(config)
52 assert.Equal(t, []string{"example.com/gpu=GPU-b", "example.com/gpu=GPU-a"}, config.CDIDevices)
53}
54
55func TestCheckCDISupportMatrix(t *testing.T) {
56 cases := []struct {
57 name string
58 version client.ServerVersionResult
59 hint string
60 wantError bool
61 }{
62 {"podman 6.1.1", podmanVersion("6.1.1"), "", false},
63 {"podman 5.4 exact", podmanVersion("5.4.0"), "", false},
64 {"podman 4.9 refused", podmanVersion("4.9.5"), "", true},
65 {"docker 28.3", dockerVersion("28.3.2"), "", false},
66 {"docker 27 refused", dockerVersion("27.5.1"), "", true},
67 {"unknown engine refused", client.ServerVersionResult{Version: "9.9.9"}, "unknown", true},
68 {"socket hint podman accepted", client.ServerVersionResult{Version: "6.1.1"}, "podman", false},
69 {"unparsable version refused", podmanVersion("not-a-version"), "", true},
70 {"prerelease minor refused", podmanVersion("5.4-rc1"), "", true},
71 }
72 for _, tc := range cases {
73 t.Run(tc.name, func(t *testing.T) {
74 err := checkCDISupport(tc.version, tc.hint)
75 if tc.wantError {
76 require.Error(t, err)
77 assert.ErrorIs(t, err, domain.ErrRuntimeUnsupported)
78 } else {
79 require.NoError(t, err)
80 }
81 })
82 }
83}
84
85func TestCompareEngineVersion(t *testing.T) {
86 assert.Greater(t, compareEngineVersion("6.1.1", "5.4"), 0)
87 assert.Less(t, compareEngineVersion("4.9.5", "5.4"), 0)
88 assert.Equal(t, 0, compareEngineVersion("5.4.0", "5.4"))
89 assert.Less(t, compareEngineVersion("not-a-version", "5.4"), 0)
90 assert.Equal(t, 0, compareEngineVersion("28.3.2", "28.3"), "patch releases do not affect the gate")
91
92 // A prerelease suffix must never lift an engine above the gate: the
93 // minor component has to be numeric on its own.
94 assert.Less(t, compareEngineVersion("5.4-rc1", "5.4"), 0)
95 assert.Less(t, compareEngineVersion("28.3-rc1", "28.3"), 0)
96 assert.Less(t, compareEngineVersion("5.", "5.4"), 0)
97}
98
99// TestEngineProbeError proves a failed /version probe keeps cancellation
100// recognizable while every other cause collapses to the unsupported
101// sentinel without the daemon endpoint in the text.
102func TestEngineProbeError(t *testing.T) {
103 t.Run("canceled probe keeps the sentinel and drops the endpoint", func(t *testing.T) {
104 ctx, cancel := context.WithCancel(context.Background())
105 cancel()
106
107 err := engineProbeError(ctx, errors.New("dial unix /run/podman/podman.sock: i/o timeout"))
108 require.ErrorIs(t, err, context.Canceled)
109 require.ErrorIs(t, err, domain.ErrRuntimeUnsupported)
110 assert.NotContains(t, err.Error(), "podman.sock")
111 })
112
113 t.Run("canceled adapter cause is preserved", func(t *testing.T) {
114 err := engineProbeError(context.Background(), fmt.Errorf("probe: %w", context.Canceled))
115 require.ErrorIs(t, err, context.Canceled)
116 })
117
118 t.Run("other probe failures stay redacted", func(t *testing.T) {
119 err := engineProbeError(context.Background(), errors.New("dial unix /run/podman/podman.sock: connection refused"))
120 require.ErrorIs(t, err, domain.ErrRuntimeUnsupported)
121 assert.False(t, errors.Is(err, context.Canceled))
122 assert.NotContains(t, err.Error(), "podman.sock")
123 })
124}