Skip to content

Commit 1cf8020

Browse files
authored
test(recipe): add conformance recipe invariant tests (#181)
1 parent 308b381 commit 1cf8020

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

pkg/recipe/conformance_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
// Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// conformance_test.go verifies that conformance-critical recipes contain
16+
// all CNCF-required components and conformance checks. These tests catch
17+
// regressions at PR time (no cluster needed) — e.g., a component removal
18+
// or a conformance check accidentally dropped from an overlay.
19+
//
20+
// Area of Concern: Recipe-level conformance guarantees
21+
// - Required components present in resolved recipe
22+
// - Conformance validation checks declared in overlay chain
23+
// - DRA constraint (K8s >= 1.34) present for all conformance recipes
24+
25+
package recipe
26+
27+
import (
28+
"context"
29+
"strings"
30+
"testing"
31+
)
32+
33+
func TestConformanceRecipeInvariants(t *testing.T) {
34+
tests := []struct {
35+
name string
36+
criteria func() *Criteria
37+
requiredComponents []string
38+
requiredChecks []string
39+
}{
40+
{
41+
name: "h100-kind-inference-dynamo",
42+
criteria: func() *Criteria {
43+
c := NewCriteria()
44+
c.Service = CriteriaServiceKind
45+
c.Accelerator = CriteriaAcceleratorH100
46+
c.Intent = CriteriaIntentInference
47+
c.Platform = CriteriaPlatformDynamo
48+
return c
49+
},
50+
requiredComponents: []string{
51+
"cert-manager",
52+
"gpu-operator",
53+
"kube-prometheus-stack",
54+
"prometheus-adapter",
55+
"nvidia-dra-driver-gpu",
56+
"kai-scheduler",
57+
"kgateway-crds",
58+
"kgateway",
59+
"dynamo-crds",
60+
"dynamo-platform",
61+
},
62+
requiredChecks: []string{
63+
"platform-health",
64+
"gpu-operator-health",
65+
"dra-support",
66+
"accelerator-metrics",
67+
"ai-service-metrics",
68+
"inference-gateway",
69+
"robust-controller",
70+
"secure-accelerator-access",
71+
"pod-autoscaling",
72+
"cluster-autoscaling",
73+
},
74+
},
75+
{
76+
name: "h100-kind-training",
77+
criteria: func() *Criteria {
78+
c := NewCriteria()
79+
c.Service = CriteriaServiceKind
80+
c.Accelerator = CriteriaAcceleratorH100
81+
c.Intent = CriteriaIntentTraining
82+
return c
83+
},
84+
requiredComponents: []string{
85+
"cert-manager",
86+
"gpu-operator",
87+
"kube-prometheus-stack",
88+
"prometheus-adapter",
89+
"nvidia-dra-driver-gpu",
90+
"kai-scheduler",
91+
"dynamo-crds",
92+
"dynamo-platform",
93+
},
94+
requiredChecks: []string{
95+
"platform-health",
96+
"gpu-operator-health",
97+
"dra-support",
98+
"accelerator-metrics",
99+
"ai-service-metrics",
100+
"gang-scheduling",
101+
"robust-controller",
102+
"pod-autoscaling",
103+
"cluster-autoscaling",
104+
},
105+
},
106+
}
107+
108+
for _, tt := range tests {
109+
t.Run(tt.name, func(t *testing.T) {
110+
ctx := context.Background()
111+
builder := NewBuilder()
112+
criteria := tt.criteria()
113+
114+
result, err := builder.BuildFromCriteria(ctx, criteria)
115+
if err != nil {
116+
t.Fatalf("BuildFromCriteria failed: %v", err)
117+
}
118+
if result == nil {
119+
t.Fatal("Recipe result is nil")
120+
}
121+
122+
// 1. All required components present
123+
for _, name := range tt.requiredComponents {
124+
if comp := result.GetComponentRef(name); comp == nil {
125+
t.Errorf("Required component %q not found in resolved recipe", name)
126+
}
127+
}
128+
129+
// 2. Conformance validation configured
130+
if result.Validation == nil {
131+
t.Fatal("result.Validation is nil")
132+
}
133+
if result.Validation.Conformance == nil {
134+
t.Fatal("result.Validation.Conformance is nil")
135+
}
136+
137+
// 3. All required conformance checks present
138+
checkSet := make(map[string]bool)
139+
for _, c := range result.Validation.Conformance.Checks {
140+
checkSet[c] = true
141+
}
142+
for _, check := range tt.requiredChecks {
143+
if !checkSet[check] {
144+
t.Errorf("Required conformance check %q not found (have: %v)",
145+
check, result.Validation.Conformance.Checks)
146+
}
147+
}
148+
149+
// 4. No fewer checks than expected (guards against accidental removal)
150+
if len(result.Validation.Conformance.Checks) < len(tt.requiredChecks) {
151+
t.Errorf("Conformance checks count = %d, want >= %d",
152+
len(result.Validation.Conformance.Checks), len(tt.requiredChecks))
153+
}
154+
155+
// 5. DRA constraint present (K8s >= 1.34 required for DRA GA)
156+
var hasDRAConstraint bool
157+
for _, c := range result.Constraints {
158+
if c.Name == "K8s.server.version" && strings.Contains(c.Value, "1.34") {
159+
hasDRAConstraint = true
160+
break
161+
}
162+
}
163+
if !hasDRAConstraint {
164+
t.Error("Missing K8s >= 1.34 constraint (required for DRA GA)")
165+
}
166+
167+
t.Logf("Recipe %s: %d components, %d conformance checks",
168+
tt.name, len(result.ComponentRefs), len(result.Validation.Conformance.Checks))
169+
})
170+
}
171+
}

0 commit comments

Comments
 (0)