Skip to content

Commit b5bc283

Browse files
jpbetzthockinaaron-prindleyongruilin
committed
ReplicationController: Add declarative validation test suite
Introduce a test suite that ensures declarative test cases are fully tested and that validation errors are compared with handwritten validation to ensure consistency. Co-authored-by: Tim Hockin <[email protected]> Co-authored-by: Aaron Prindle <[email protected]> Co-authored-by: Yongrui Lin <[email protected]>
1 parent 5a5ed81 commit b5bc283

File tree

1 file changed

+164
-0
lines changed

1 file changed

+164
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
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 replicationcontroller
18+
19+
import (
20+
"testing"
21+
22+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
"k8s.io/apimachinery/pkg/util/validation/field"
24+
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
25+
utilfeature "k8s.io/apiserver/pkg/util/feature"
26+
featuregatetesting "k8s.io/component-base/featuregate/testing"
27+
podtest "k8s.io/kubernetes/pkg/api/pod/testing"
28+
apitesting "k8s.io/kubernetes/pkg/api/testing"
29+
api "k8s.io/kubernetes/pkg/apis/core"
30+
"k8s.io/kubernetes/pkg/features"
31+
)
32+
33+
func TestDeclarativeValidateForDeclarative(t *testing.T) {
34+
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
35+
APIGroup: "",
36+
APIVersion: "v1",
37+
})
38+
testCases := map[string]struct {
39+
input api.ReplicationController
40+
expectedErrs field.ErrorList
41+
}{
42+
"empty resource": {
43+
input: mkValidReplicationController(),
44+
},
45+
// TODO: Add test cases
46+
}
47+
for k, tc := range testCases {
48+
t.Run(k, func(t *testing.T) {
49+
var declarativeTakeoverErrs field.ErrorList
50+
var imperativeErrs field.ErrorList
51+
for _, gateVal := range []bool{true, false} {
52+
// We only need to test both gate enabled and disabled together, because
53+
// 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled.
54+
// 2) the validation output, when only DeclarativeValidation is enabled, is the same as when both gates are disabled.
55+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal)
56+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal)
57+
58+
errs := Strategy.Validate(ctx, &tc.input)
59+
if gateVal {
60+
declarativeTakeoverErrs = errs
61+
} else {
62+
imperativeErrs = errs
63+
}
64+
// The errOutputMatcher is used to verify the output matches the expected errors in test cases.
65+
errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
66+
if len(tc.expectedErrs) > 0 {
67+
errOutputMatcher.Test(t, tc.expectedErrs, errs)
68+
} else if len(errs) != 0 {
69+
t.Errorf("expected no errors, but got: %v", errs)
70+
}
71+
}
72+
// The equivalenceMatcher is used to verify the output errors from hand-written imperative validation
73+
// are equivalent to the output errors when DeclarativeValidationTakeover is enabled.
74+
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
75+
equivalenceMatcher.Test(t, imperativeErrs, declarativeTakeoverErrs)
76+
77+
apitesting.VerifyVersionedValidationEquivalence(t, &tc.input, nil)
78+
})
79+
}
80+
}
81+
82+
func TestValidateUpdateForDeclarative(t *testing.T) {
83+
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
84+
APIGroup: "",
85+
APIVersion: "v1",
86+
})
87+
testCases := map[string]struct {
88+
old api.ReplicationController
89+
update api.ReplicationController
90+
expectedErrs field.ErrorList
91+
}{
92+
// TODO: Add test cases
93+
}
94+
for k, tc := range testCases {
95+
t.Run(k, func(t *testing.T) {
96+
tc.old.ObjectMeta.ResourceVersion = "1"
97+
tc.update.ObjectMeta.ResourceVersion = "1"
98+
var declarativeTakeoverErrs field.ErrorList
99+
var imperativeErrs field.ErrorList
100+
for _, gateVal := range []bool{true, false} {
101+
// We only need to test both gate enabled and disabled together, because
102+
// 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled.
103+
// 2) the validation output, when only DeclarativeValidation is enabled, is the same as when both gates are disabled.
104+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal)
105+
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal)
106+
errs := Strategy.ValidateUpdate(ctx, &tc.update, &tc.old)
107+
if gateVal {
108+
declarativeTakeoverErrs = errs
109+
} else {
110+
imperativeErrs = errs
111+
}
112+
// The errOutputMatcher is used to verify the output matches the expected errors in test cases.
113+
errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
114+
115+
if len(tc.expectedErrs) > 0 {
116+
errOutputMatcher.Test(t, tc.expectedErrs, errs)
117+
} else if len(errs) != 0 {
118+
t.Errorf("expected no errors, but got: %v", errs)
119+
}
120+
}
121+
// The equivalenceMatcher is used to verify the output errors from hand-written imperative validation
122+
// are equivalent to the output errors when DeclarativeValidationTakeover is enabled.
123+
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
124+
// TODO: remove this once ErrorMatcher has been extended to handle this form of deduplication.
125+
dedupedImperativeErrs := field.ErrorList{}
126+
for _, err := range imperativeErrs {
127+
found := false
128+
for _, existingErr := range dedupedImperativeErrs {
129+
if equivalenceMatcher.Matches(existingErr, err) {
130+
found = true
131+
break
132+
}
133+
}
134+
if !found {
135+
dedupedImperativeErrs = append(dedupedImperativeErrs, err)
136+
}
137+
}
138+
equivalenceMatcher.Test(t, dedupedImperativeErrs, declarativeTakeoverErrs)
139+
140+
apitesting.VerifyVersionedValidationEquivalence(t, &tc.update, &tc.old)
141+
})
142+
}
143+
}
144+
145+
// Helper function for RC tests.
146+
func mkValidReplicationController(tweaks ...func(rc *api.ReplicationController)) api.ReplicationController {
147+
rc := api.ReplicationController{
148+
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
149+
Spec: api.ReplicationControllerSpec{
150+
Replicas: 1,
151+
Selector: map[string]string{"a": "b"},
152+
Template: &api.PodTemplateSpec{
153+
ObjectMeta: metav1.ObjectMeta{
154+
Labels: map[string]string{"a": "b"},
155+
},
156+
Spec: podtest.MakePodSpec(),
157+
},
158+
},
159+
}
160+
for _, tweak := range tweaks {
161+
tweak(&rc)
162+
}
163+
return rc
164+
}

0 commit comments

Comments
 (0)