k8s.io/perf-tests/clusterloader2@v0.0.0-20240304094227-64bdb12da87e/pkg/measurement/common/generic_query_measurement_test.go (about) 1 /* 2 Copyright 2022 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 common 18 19 import ( 20 "encoding/json" 21 "testing" 22 "time" 23 24 "github.com/prometheus/common/model" 25 "github.com/stretchr/testify/assert" 26 "github.com/stretchr/testify/require" 27 "k8s.io/perf-tests/clusterloader2/pkg/measurement" 28 measurementutil "k8s.io/perf-tests/clusterloader2/pkg/measurement/util" 29 ) 30 31 type fakeQueryExecutor struct { 32 samples map[string][]*model.Sample 33 } 34 35 func (f fakeQueryExecutor) Query(query string, _ time.Time) ([]*model.Sample, error) { 36 return f.samples[query], nil 37 } 38 39 func TestGather(t *testing.T) { 40 testCases := []struct { 41 desc string 42 params map[string]interface{} 43 samples map[string][]*model.Sample 44 wantDataItems []measurementutil.DataItem 45 wantConfigureErr string 46 wantErr string 47 }{ 48 { 49 desc: "happy path", 50 params: map[string]interface{}{ 51 "metricName": "happy-path", 52 "metricVersion": "v1", 53 "unit": "ms", 54 "queries": []map[string]interface{}{ 55 { 56 "name": "no-samples", 57 "query": "no-samples-query[%v]", 58 "threshold": 42, 59 }, 60 { 61 "name": "below-threshold", 62 "query": "below-threshold-query[%v]", 63 "threshold": 30, 64 }, 65 { 66 "name": "no-threshold", 67 "query": "no-threshold-query[%v]", 68 }, 69 { 70 "name": "multiple-duration-placeholders", 71 "query": "placeholder-a[%v] + placeholder-b[%v]", 72 }, 73 }, 74 }, 75 samples: map[string][]*model.Sample{ 76 "below-threshold-query[60s]": {{Value: model.SampleValue(7)}}, 77 "no-threshold-query[60s]": {{Value: model.SampleValue(120)}}, 78 "placeholder-a[60s] + placeholder-b[60s]": {{Value: model.SampleValue(5)}}, 79 }, 80 wantDataItems: []measurementutil.DataItem{ 81 { 82 Unit: "ms", 83 Data: map[string]float64{ 84 "below-threshold": 7.0, 85 "no-threshold": 120.0, 86 "multiple-duration-placeholders": 5.0, 87 }, 88 }, 89 }, 90 }, 91 { 92 desc: "no samples, but samples not required", 93 params: map[string]interface{}{ 94 "metricName": "no-samples", 95 "metricVersion": "v1", 96 "unit": "ms", 97 "queries": []map[string]interface{}{ 98 { 99 "name": "no-samples", 100 "query": "no-samples-query[%v]", 101 }, 102 }, 103 }, 104 }, 105 { 106 desc: "no samples, but samples required", 107 params: map[string]interface{}{ 108 "metricName": "no-samples", 109 "metricVersion": "v1", 110 "unit": "ms", 111 "queries": []map[string]interface{}{ 112 { 113 "name": "no-samples", 114 "query": "no-samples-query[%v]", 115 "requireSamples": true, 116 }, 117 }, 118 }, 119 wantErr: "no samples", 120 }, 121 { 122 desc: "too many samples", 123 params: map[string]interface{}{ 124 "metricName": "many-samples", 125 "metricVersion": "v1", 126 "unit": "ms", 127 "queries": []map[string]interface{}{ 128 { 129 "name": "many-samples", 130 "query": "many-samples-query[%v]", 131 }, 132 }, 133 }, 134 samples: map[string][]*model.Sample{ 135 "many-samples-query[60s]": { 136 {Value: model.SampleValue(1)}, 137 {Value: model.SampleValue(2)}, 138 }, 139 }, 140 wantErr: "too many samples", 141 // When too many samples, first value is returned and error is raised. 142 wantDataItems: []measurementutil.DataItem{ 143 { 144 Unit: "ms", 145 Data: map[string]float64{ 146 "many-samples": 1.0, 147 }, 148 }, 149 }, 150 }, 151 { 152 desc: "sample above threshold", 153 params: map[string]interface{}{ 154 "metricName": "above-threshold", 155 "metricVersion": "v1", 156 "unit": "ms", 157 "queries": []map[string]interface{}{ 158 { 159 "name": "above-threshold", 160 "query": "above-threshold-query[%v]", 161 "threshold": 60, 162 }, 163 }, 164 }, 165 samples: map[string][]*model.Sample{ 166 "above-threshold-query[60s]": {{Value: model.SampleValue(123)}}, 167 }, 168 wantErr: "sample above threshold: want: less or equal than 60, got: 123", 169 wantDataItems: []measurementutil.DataItem{ 170 { 171 Unit: "ms", 172 Data: map[string]float64{ 173 "above-threshold": 123.0, 174 }, 175 }, 176 }, 177 }, 178 { 179 desc: "sample above lower bound", 180 params: map[string]interface{}{ 181 "metricName": "below-threshold", 182 "metricVersion": "v1", 183 "unit": "ms", 184 "queries": []map[string]interface{}{ 185 { 186 "name": "above-threshold", 187 "query": "above-threshold-query[%v]", 188 "threshold": 60, 189 "lowerBound": true, 190 }, 191 }, 192 }, 193 samples: map[string][]*model.Sample{ 194 "above-threshold-query[60s]": {{Value: model.SampleValue(74)}}, 195 }, 196 wantDataItems: []measurementutil.DataItem{ 197 { 198 Unit: "ms", 199 Data: map[string]float64{ 200 "above-threshold": 74.0, 201 }, 202 }, 203 }, 204 }, 205 { 206 desc: "sample below lower bound", 207 params: map[string]interface{}{ 208 "metricName": "below-threshold", 209 "metricVersion": "v1", 210 "unit": "ms", 211 "queries": []map[string]interface{}{ 212 { 213 "name": "below-threshold", 214 "query": "below-threshold-query[%v]", 215 "threshold": 60, 216 "lowerBound": true, 217 }, 218 }, 219 }, 220 samples: map[string][]*model.Sample{ 221 "below-threshold-query[60s]": {{Value: model.SampleValue(42)}}, 222 }, 223 wantErr: "sample below threshold: want: greater or equal than 60, got: 42", 224 wantDataItems: []measurementutil.DataItem{ 225 { 226 Unit: "ms", 227 Data: map[string]float64{ 228 "below-threshold": 42.0, 229 }, 230 }, 231 }, 232 }, 233 { 234 desc: "missing field metricName", 235 params: map[string]interface{}{ 236 "metricVersion": "v1", 237 "unit": "ms", 238 "queries": []map[string]interface{}{ 239 { 240 "name": "no-samples", 241 "query": "no-samples-query[%v]", 242 "threshold": 42, 243 }, 244 { 245 "name": "below-threshold", 246 "query": "below-threshold-query[%v]", 247 "threshold": 30, 248 }, 249 { 250 "name": "no-threshold", 251 "query": "no-threshold-query[%v]", 252 }, 253 }, 254 }, 255 wantConfigureErr: "metricName is required", 256 }, 257 { 258 desc: "dimensions", 259 params: map[string]interface{}{ 260 "metricName": "dimensions", 261 "metricVersion": "v1", 262 "unit": "ms", 263 "dimensions": []interface{}{ 264 "d1", 265 "d2", 266 }, 267 "queries": []map[string]interface{}{ 268 { 269 "name": "perc99", 270 "query": "query-perc99[%v]", 271 }, 272 { 273 "name": "perc90", 274 "query": "query-perc90[%v]", 275 }, 276 }, 277 }, 278 samples: map[string][]*model.Sample{ 279 "query-perc99[60s]": { 280 { 281 Metric: model.Metric{ 282 model.LabelName("d1"): model.LabelValue("d1-val1"), 283 model.LabelName("d2"): model.LabelValue("d2-val1"), 284 model.LabelName("d3"): model.LabelValue("d3-val1"), // Ignored 285 }, 286 Value: model.SampleValue(1), 287 }, 288 { 289 Metric: model.Metric{ 290 model.LabelName("d1"): model.LabelValue("d1-val1"), 291 model.LabelName("d2"): model.LabelValue("d2-val2"), 292 }, 293 Value: model.SampleValue(2), 294 }, 295 }, 296 "query-perc90[60s]": { 297 { 298 Metric: model.Metric{ 299 model.LabelName("d1"): model.LabelValue("d1-val1"), 300 model.LabelName("d2"): model.LabelValue("d2-val1"), 301 }, 302 Value: model.SampleValue(3), 303 }, 304 { 305 Metric: model.Metric{ 306 model.LabelName("d1"): model.LabelValue("d1-val1"), 307 model.LabelName("d2"): model.LabelValue("d2-val2"), 308 }, 309 Value: model.SampleValue(4), 310 }, 311 { 312 Metric: model.Metric{ 313 model.LabelName("d1"): model.LabelValue("d1-val1"), 314 // d2 not set 315 }, 316 Value: model.SampleValue(5), 317 }, 318 }, 319 }, 320 wantDataItems: []measurementutil.DataItem{ 321 { 322 Labels: map[string]string{ 323 "d1": "d1-val1", 324 "d2": "d2-val1", 325 }, 326 Unit: "ms", 327 Data: map[string]float64{ 328 "perc99": 1.0, 329 "perc90": 3.0, 330 }, 331 }, 332 { 333 Labels: map[string]string{ 334 "d1": "d1-val1", 335 "d2": "d2-val2", 336 }, 337 Unit: "ms", 338 Data: map[string]float64{ 339 "perc99": 2.0, 340 "perc90": 4.0, 341 }, 342 }, 343 { 344 Labels: map[string]string{ 345 "d1": "d1-val1", 346 "d2": "", 347 }, 348 Unit: "ms", 349 Data: map[string]float64{ 350 // perc99 doesn't return this combination. 351 "perc90": 5.0, 352 }, 353 }, 354 }, 355 }, 356 { 357 desc: "multiple values for single dimension", 358 params: map[string]interface{}{ 359 "metricName": "dimensions", 360 "metricVersion": "v1", 361 "unit": "ms", 362 "dimensions": []interface{}{ 363 "d1", 364 "d2", 365 }, 366 "queries": []map[string]interface{}{ 367 { 368 "name": "perc99", 369 "query": "query-perc99[%v]", 370 }, 371 }, 372 }, 373 samples: map[string][]*model.Sample{ 374 "query-perc99[60s]": { 375 { 376 Metric: model.Metric{ 377 model.LabelName("d1"): model.LabelValue("d1-val1"), 378 model.LabelName("d2"): model.LabelValue("d2-val1"), 379 }, 380 Value: model.SampleValue(1), 381 }, 382 { 383 Metric: model.Metric{ 384 model.LabelName("d1"): model.LabelValue("d1-val1"), 385 model.LabelName("d2"): model.LabelValue("d2-val1"), 386 }, 387 Value: model.SampleValue(2), 388 }, 389 }, 390 }, 391 wantErr: "too many samples for [d1-val1 d2-val1]", 392 wantDataItems: []measurementutil.DataItem{ 393 { 394 Labels: map[string]string{ 395 "d1": "d1-val1", 396 "d2": "d2-val1", 397 }, 398 Unit: "ms", 399 Data: map[string]float64{ 400 "perc99": 1.0, 401 }, 402 }, 403 }, 404 }, 405 } 406 407 for _, tc := range testCases { 408 t.Run(tc.desc, func(t *testing.T) { 409 gatherer := &genericQueryGatherer{} 410 err := gatherer.Configure(&measurement.Config{Params: tc.params}) 411 if tc.wantConfigureErr != "" { 412 assert.Contains(t, err.Error(), tc.wantConfigureErr) 413 return 414 } 415 assert.Nil(t, err) 416 startTime := time.Now() 417 endTime := startTime.Add(1 * time.Minute) 418 executor := fakeQueryExecutor{tc.samples} 419 420 summaries, err := gatherer.Gather(executor, startTime, endTime, nil) 421 if tc.wantErr != "" { 422 assert.Contains(t, err.Error(), tc.wantErr) 423 } else { 424 assert.Nil(t, err) 425 } 426 require.Len(t, summaries, 1) 427 content := summaries[0].SummaryContent() 428 perfData := measurementutil.PerfData{} 429 err = json.Unmarshal([]byte(content), &perfData) 430 require.Nil(t, err) 431 assert.ElementsMatch(t, perfData.DataItems, tc.wantDataItems) 432 }) 433 } 434 }