1
16
17 package suite
18
19 import (
20 "errors"
21 "fmt"
22 "strings"
23 "sync"
24 "testing"
25 "time"
26
27 v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/util/sets"
29
30 "sigs.k8s.io/gateway-api/conformance"
31 confv1a1 "sigs.k8s.io/gateway-api/conformance/apis/v1alpha1"
32 "sigs.k8s.io/gateway-api/conformance/utils/config"
33 "sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
34 "sigs.k8s.io/gateway-api/conformance/utils/roundtripper"
35 )
36
37
38
39
40
41
42
43
44
45
46 type ExperimentalConformanceTestSuite struct {
47 ConformanceTestSuite
48
49
50
51 implementation confv1a1.Implementation
52
53
54
55 conformanceProfiles sets.Set[ConformanceProfileName]
56
57
58 running bool
59
60
61
62 results map[string]testResult
63
64
65
66 extendedSupportedFeatures map[ConformanceProfileName]sets.Set[SupportedFeature]
67
68
69
70 extendedUnsupportedFeatures map[ConformanceProfileName]sets.Set[SupportedFeature]
71
72
73 lock sync.RWMutex
74 }
75
76
77 type ExperimentalConformanceOptions struct {
78 Options
79
80 Implementation confv1a1.Implementation
81 ConformanceProfiles sets.Set[ConformanceProfileName]
82 }
83
84
85 func NewExperimentalConformanceTestSuite(s ExperimentalConformanceOptions) (*ExperimentalConformanceTestSuite, error) {
86 config.SetupTimeoutConfig(&s.TimeoutConfig)
87
88 roundTripper := s.RoundTripper
89 if roundTripper == nil {
90 roundTripper = &roundtripper.DefaultRoundTripper{Debug: s.Debug, TimeoutConfig: s.TimeoutConfig}
91 }
92
93 suite := &ExperimentalConformanceTestSuite{
94 results: make(map[string]testResult),
95 extendedUnsupportedFeatures: make(map[ConformanceProfileName]sets.Set[SupportedFeature]),
96 extendedSupportedFeatures: make(map[ConformanceProfileName]sets.Set[SupportedFeature]),
97 conformanceProfiles: s.ConformanceProfiles,
98 implementation: s.Implementation,
99 }
100
101
102
103 if s.SupportedFeatures == nil && s.ConformanceProfiles.Len() == 0 && !s.EnableAllSupportedFeatures {
104 return nil, fmt.Errorf("no conformance profile was selected for test run, and no supported features were provided so no tests could be selected")
105 }
106
107
108
109
110 if s.EnableAllSupportedFeatures {
111 s.SupportedFeatures = AllFeatures
112 } else {
113 if s.SupportedFeatures == nil {
114 s.SupportedFeatures = sets.New[SupportedFeature]()
115 }
116
117 for _, conformanceProfileName := range s.ConformanceProfiles.UnsortedList() {
118 conformanceProfile, err := getConformanceProfileForName(conformanceProfileName)
119 if err != nil {
120 return nil, fmt.Errorf("failed to retrieve conformance profile: %w", err)
121 }
122
123
124 for _, f := range conformanceProfile.CoreFeatures.UnsortedList() {
125 if !s.SupportedFeatures.Has(f) {
126 s.SupportedFeatures.Insert(f)
127 }
128 }
129 for _, f := range conformanceProfile.ExtendedFeatures.UnsortedList() {
130 if s.SupportedFeatures.Has(f) {
131 if suite.extendedSupportedFeatures[conformanceProfileName] == nil {
132 suite.extendedSupportedFeatures[conformanceProfileName] = sets.New[SupportedFeature]()
133 }
134 suite.extendedSupportedFeatures[conformanceProfileName].Insert(f)
135 } else {
136 if suite.extendedUnsupportedFeatures[conformanceProfileName] == nil {
137 suite.extendedUnsupportedFeatures[conformanceProfileName] = sets.New[SupportedFeature]()
138 }
139 suite.extendedUnsupportedFeatures[conformanceProfileName].Insert(f)
140 }
141
142 if s.ExemptFeatures.Has(f) {
143 suite.extendedUnsupportedFeatures[conformanceProfileName].Insert(f)
144 }
145 }
146 }
147 }
148
149 for feature := range s.ExemptFeatures {
150 s.SupportedFeatures.Delete(feature)
151 }
152
153 if s.FS == nil {
154 s.FS = &conformance.Manifests
155 }
156
157 suite.ConformanceTestSuite = ConformanceTestSuite{
158 Client: s.Client,
159 Clientset: s.Clientset,
160 RestConfig: s.RestConfig,
161 RoundTripper: roundTripper,
162 GatewayClassName: s.GatewayClassName,
163 Debug: s.Debug,
164 Cleanup: s.CleanupBaseResources,
165 BaseManifests: s.BaseManifests,
166 MeshManifests: s.MeshManifests,
167 Applier: kubernetes.Applier{
168 NamespaceLabels: s.NamespaceLabels,
169 NamespaceAnnotations: s.NamespaceAnnotations,
170 },
171 SupportedFeatures: s.SupportedFeatures,
172 TimeoutConfig: s.TimeoutConfig,
173 SkipTests: sets.New(s.SkipTests...),
174 FS: *s.FS,
175 UsableNetworkAddresses: s.UsableNetworkAddresses,
176 UnusableNetworkAddresses: s.UnusableNetworkAddresses,
177 }
178
179
180 if suite.BaseManifests == "" {
181 suite.BaseManifests = "base/manifests.yaml"
182 }
183 if suite.MeshManifests == "" {
184 suite.MeshManifests = "mesh/manifests.yaml"
185 }
186
187 return suite, nil
188 }
189
190
191
192
193
194
195
196 func (suite *ExperimentalConformanceTestSuite) Setup(t *testing.T) {
197 suite.ConformanceTestSuite.Setup(t)
198 }
199
200
201 func (suite *ExperimentalConformanceTestSuite) Run(t *testing.T, tests []ConformanceTest) error {
202
203
204 suite.lock.Lock()
205 if suite.running {
206 suite.lock.Unlock()
207 return fmt.Errorf("can't run the test suite multiple times in parallel: the test suite is already running")
208 }
209
210
211
212 suite.running = true
213 suite.results = nil
214 suite.lock.Unlock()
215
216
217 results := make(map[string]testResult)
218 for _, test := range tests {
219 succeeded := t.Run(test.ShortName, func(t *testing.T) {
220 test.Run(t, &suite.ConformanceTestSuite)
221 })
222 res := testSucceeded
223 if suite.SkipTests.Has(test.ShortName) {
224 res = testSkipped
225 }
226 if !suite.SupportedFeatures.HasAll(test.Features...) {
227 res = testNotSupported
228 }
229
230 if !succeeded {
231 res = testFailed
232 }
233
234 results[test.ShortName] = testResult{
235 test: test,
236 result: res,
237 }
238 }
239
240
241
242 suite.lock.Lock()
243 suite.running = false
244 suite.results = results
245 suite.lock.Unlock()
246
247 return nil
248 }
249
250
251
252 func (suite *ExperimentalConformanceTestSuite) Report() (*confv1a1.ConformanceReport, error) {
253 suite.lock.RLock()
254 if suite.running {
255 suite.lock.RUnlock()
256 return nil, fmt.Errorf("can't generate report: the test suite is currently running")
257 }
258 defer suite.lock.RUnlock()
259
260 profileReports := newReports()
261 for _, testResult := range suite.results {
262 conformanceProfiles := getConformanceProfilesForTest(testResult.test, suite.conformanceProfiles)
263 for _, profile := range conformanceProfiles.UnsortedList() {
264 profileReports.addTestResults(*profile, testResult)
265 }
266 }
267
268 profileReports.compileResults(suite.extendedSupportedFeatures, suite.extendedUnsupportedFeatures)
269
270 return &confv1a1.ConformanceReport{
271 TypeMeta: v1.TypeMeta{
272 APIVersion: "gateway.networking.k8s.io/v1alpha1",
273 Kind: "ConformanceReport",
274 },
275 Date: time.Now().Format(time.RFC3339),
276 Implementation: suite.implementation,
277 GatewayAPIVersion: "TODO",
278 ProfileReports: profileReports.list(),
279 }, nil
280 }
281
282
283
284 func ParseImplementation(org, project, url, version, contact string) (*confv1a1.Implementation, error) {
285 if org == "" {
286 return nil, errors.New("implementation's organization can not be empty")
287 }
288 if project == "" {
289 return nil, errors.New("implementation's project can not be empty")
290 }
291 if url == "" {
292 return nil, errors.New("implementation's url can not be empty")
293 }
294 if version == "" {
295 return nil, errors.New("implementation's version can not be empty")
296 }
297 contacts := strings.Split(contact, ",")
298 if len(contacts) == 0 {
299 return nil, errors.New("implementation's contact can not be empty")
300 }
301
302
303
304 return &confv1a1.Implementation{
305 Organization: org,
306 Project: project,
307 URL: url,
308 Version: version,
309 Contact: contacts,
310 }, nil
311 }
312
313
314
315 func ParseConformanceProfiles(p string) sets.Set[ConformanceProfileName] {
316 res := sets.Set[ConformanceProfileName]{}
317 if p == "" {
318 return res
319 }
320
321 for _, value := range strings.Split(p, ",") {
322 res.Insert(ConformanceProfileName(value))
323 }
324 return res
325 }
326
View as plain text