Skip to content

Commit 0f5ead4

Browse files
committed
Add unit tests for cmd/gpuop-cfg config validators
Cover the csv and clusterpolicy validators under cmd/gpuop-cfg/validate: alm-examples parsing (including apiVersion-ignored, empty/null list, and unmarshal error boundaries), image validation, options load/getContents (file and stdin), and the flag/subcommand wiring. Signed-off-by: Abrar Shivani <ashivani@nvidia.com>
1 parent d8be354 commit 0f5ead4

8 files changed

Lines changed: 578 additions & 0 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package clusterpolicy
18+
19+
import (
20+
"slices"
21+
"testing"
22+
23+
"github.com/sirupsen/logrus"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
"github.com/urfave/cli/v3"
27+
)
28+
29+
func TestNewCommand(t *testing.T) {
30+
cmd := NewCommand(logrus.New())
31+
32+
require.NotNil(t, cmd)
33+
assert.Equal(t, "clusterpolicy", cmd.Name)
34+
assert.NotEmpty(t, cmd.Usage)
35+
36+
idx := slices.IndexFunc(cmd.Flags, func(f cli.Flag) bool {
37+
return slices.Contains(f.Names(), "input")
38+
})
39+
require.GreaterOrEqual(t, idx, 0)
40+
41+
inputFlag, ok := cmd.Flags[idx].(*cli.StringFlag)
42+
require.True(t, ok)
43+
assert.Contains(t, inputFlag.Names(), "input")
44+
assert.NotEmpty(t, inputFlag.Usage)
45+
assert.Equal(t, "-", inputFlag.Value)
46+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package clusterpolicy
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
"github.com/stretchr/testify/require"
24+
25+
v1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1"
26+
)
27+
28+
func TestValidateImage_InvalidReference(t *testing.T) {
29+
testCases := []struct {
30+
description string
31+
path string
32+
}{
33+
{
34+
description: "empty reference",
35+
path: "",
36+
},
37+
{
38+
description: "malformed reference",
39+
path: "@@bad::ref",
40+
},
41+
}
42+
43+
for _, tc := range testCases {
44+
t.Run(tc.description, func(t *testing.T) {
45+
err := validateImage(context.Background(), tc.path)
46+
require.ErrorContains(t, err, "failed to construct an image reference")
47+
})
48+
}
49+
}
50+
51+
func TestValidateImages_EmptyDriverSpecImagePathError(t *testing.T) {
52+
t.Setenv("DRIVER_IMAGE", "")
53+
54+
spec := &v1.ClusterPolicySpec{}
55+
56+
err := validateImages(context.Background(), spec)
57+
require.ErrorContains(t, err, "failed to construct the image path")
58+
}
59+
60+
func TestValidateImages_InvalidDriverImageRefError(t *testing.T) {
61+
spec := &v1.ClusterPolicySpec{}
62+
spec.Driver.Image = "@@bad::ref"
63+
64+
err := validateImages(context.Background(), spec)
65+
require.ErrorContains(t, err, "failed to validate image")
66+
require.ErrorContains(t, err, "failed to construct an image reference")
67+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package clusterpolicy
18+
19+
import (
20+
"os"
21+
"path/filepath"
22+
"testing"
23+
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestOptionsLoad(t *testing.T) {
29+
const validManifest = `apiVersion: nvidia.com/v1
30+
kind: ClusterPolicy
31+
metadata:
32+
name: cluster-policy
33+
`
34+
35+
t.Run("valid manifest", func(t *testing.T) {
36+
path := filepath.Join(t.TempDir(), "clusterpolicy.yaml")
37+
require.NoError(t, os.WriteFile(path, []byte(validManifest), 0o600))
38+
39+
spec, err := (options{input: path}).load()
40+
41+
require.NoError(t, err)
42+
assert.Equal(t, "cluster-policy", spec.Name)
43+
})
44+
45+
t.Run("missing file", func(t *testing.T) {
46+
path := filepath.Join(t.TempDir(), "does-not-exist.yaml")
47+
48+
_, err := (options{input: path}).load()
49+
50+
require.ErrorContains(t, err, "failed to read file")
51+
})
52+
53+
t.Run("malformed yaml", func(t *testing.T) {
54+
path := filepath.Join(t.TempDir(), "malformed.yaml")
55+
require.NoError(t, os.WriteFile(path, []byte("\tnot: : valid: yaml"), 0o600))
56+
57+
_, err := (options{input: path}).load()
58+
59+
require.ErrorContains(t, err, "failed to unmarshal spec")
60+
})
61+
62+
t.Run("empty file", func(t *testing.T) {
63+
path := filepath.Join(t.TempDir(), "empty.yaml")
64+
require.NoError(t, os.WriteFile(path, []byte(""), 0o600))
65+
66+
spec, err := (options{input: path}).load()
67+
68+
require.NoError(t, err)
69+
assert.Empty(t, spec.Name)
70+
})
71+
}
72+
73+
func TestOptionsGetContentsStdin(t *testing.T) {
74+
r, w, err := os.Pipe()
75+
require.NoError(t, err)
76+
77+
orig := os.Stdin
78+
os.Stdin = r
79+
t.Cleanup(func() { os.Stdin = orig })
80+
81+
go func() {
82+
_, _ = w.Write([]byte("hello"))
83+
_ = w.Close()
84+
}()
85+
86+
got, err := (options{input: "-"}).getContents()
87+
88+
require.NoError(t, err)
89+
assert.Equal(t, "hello", string(got))
90+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package csv
18+
19+
import (
20+
"testing"
21+
22+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestValidateALMExample(t *testing.T) {
28+
testCases := []struct {
29+
description string
30+
annotations map[string]string
31+
expectError bool
32+
errContains string
33+
}{
34+
{
35+
description: "first item with Kind ClusterPolicy returns nil",
36+
annotations: map[string]string{
37+
"alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}}]`,
38+
},
39+
expectError: false,
40+
},
41+
{
42+
description: "Kind ClusterPolicy with wrong apiVersion returns nil (apiVersion is not checked)",
43+
annotations: map[string]string{
44+
"alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"example.com/v99","spec":{}}]`,
45+
},
46+
expectError: false,
47+
},
48+
{
49+
description: "multi-entry with ClusterPolicy first ignores later entries and returns nil",
50+
annotations: map[string]string{
51+
"alm-examples": `[{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}},{"kind":"SomethingElse","apiVersion":"nvidia.com/v1"}]`,
52+
},
53+
expectError: false,
54+
},
55+
{
56+
description: "malformed JSON returns an unmarshal error",
57+
annotations: map[string]string{
58+
"alm-examples": `{not valid json`,
59+
},
60+
expectError: true,
61+
errContains: "invalid character",
62+
},
63+
{
64+
description: "missing alm-examples annotation returns an unmarshal error on empty string",
65+
annotations: map[string]string{},
66+
expectError: true,
67+
errContains: "unexpected end of JSON input",
68+
},
69+
{
70+
description: "nil annotations returns an unmarshal error on empty string",
71+
annotations: nil,
72+
expectError: true,
73+
errContains: "unexpected end of JSON input",
74+
},
75+
{
76+
description: "empty alm-examples annotation returns an unmarshal error on empty string",
77+
annotations: map[string]string{
78+
"alm-examples": "",
79+
},
80+
expectError: true,
81+
errContains: "unexpected end of JSON input",
82+
},
83+
{
84+
description: "empty list returns 'no example clusterpolicy found'",
85+
annotations: map[string]string{
86+
"alm-examples": `[]`,
87+
},
88+
expectError: true,
89+
errContains: "no example clusterpolicy found",
90+
},
91+
{
92+
description: "JSON null returns 'no example clusterpolicy found'",
93+
annotations: map[string]string{
94+
"alm-examples": "null",
95+
},
96+
expectError: true,
97+
errContains: "no example clusterpolicy found",
98+
},
99+
{
100+
description: "JSON object instead of array returns an unmarshal error",
101+
annotations: map[string]string{
102+
"alm-examples": `{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1"}`,
103+
},
104+
expectError: true,
105+
errContains: "cannot unmarshal object",
106+
},
107+
{
108+
description: "first item with Kind != ClusterPolicy returns 'invalid example clusterpolicy'",
109+
annotations: map[string]string{
110+
"alm-examples": `[{"kind":"NotAClusterPolicy","apiVersion":"nvidia.com/v1"}]`,
111+
},
112+
expectError: true,
113+
errContains: "invalid example clusterpolicy",
114+
},
115+
{
116+
description: "multi-entry with ClusterPolicy not first returns 'invalid example clusterpolicy'",
117+
annotations: map[string]string{
118+
"alm-examples": `[{"kind":"SomethingElse","apiVersion":"nvidia.com/v1"},{"kind":"ClusterPolicy","apiVersion":"nvidia.com/v1","spec":{}}]`,
119+
},
120+
expectError: true,
121+
errContains: "invalid example clusterpolicy",
122+
},
123+
}
124+
125+
for _, tc := range testCases {
126+
t.Run(tc.description, func(t *testing.T) {
127+
csv := &v1alpha1.ClusterServiceVersion{}
128+
csv.Annotations = tc.annotations
129+
130+
err := validateALMExample(csv)
131+
132+
if !tc.expectError {
133+
require.NoError(t, err)
134+
return
135+
}
136+
137+
assert.ErrorContains(t, err, tc.errContains)
138+
})
139+
}
140+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
**/
16+
17+
package csv
18+
19+
import (
20+
"slices"
21+
"testing"
22+
23+
"github.com/sirupsen/logrus"
24+
"github.com/stretchr/testify/assert"
25+
"github.com/stretchr/testify/require"
26+
"github.com/urfave/cli/v3"
27+
)
28+
29+
func TestNewCommand(t *testing.T) {
30+
cmd := NewCommand(logrus.New())
31+
32+
require.NotNil(t, cmd)
33+
assert.Equal(t, "csv", cmd.Name)
34+
assert.NotEmpty(t, cmd.Usage)
35+
36+
idx := slices.IndexFunc(cmd.Flags, func(f cli.Flag) bool {
37+
return slices.Contains(f.Names(), "input")
38+
})
39+
require.GreaterOrEqual(t, idx, 0)
40+
41+
inputFlag, ok := cmd.Flags[idx].(*cli.StringFlag)
42+
require.True(t, ok)
43+
assert.Contains(t, inputFlag.Names(), "input")
44+
assert.NotEmpty(t, inputFlag.Usage)
45+
assert.Equal(t, "-", inputFlag.Value)
46+
}

0 commit comments

Comments
 (0)