k8s.io/test-infra/triage@v0.0.0-20240520184403-27c6b4c223d8/summarize/summarize_test.go (about) 1 /* 2 Copyright 2020 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 summarize 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "os" 23 "testing" 24 ) 25 26 // smear takes a slice of map deltas and returns a slice of maps. 27 func smear(deltas []map[string]string) []map[string]string { 28 cur := make(map[string]string) 29 out := make([]map[string]string, 0, len(deltas)) 30 31 for _, delta := range deltas { 32 for key, val := range delta { 33 // Create a key-value mapping, or replace the value with the new one if it exists 34 cur[key] = val 35 } 36 37 // Copy cur to avoid messing with the original map 38 curCopy := make(map[string]string, len(cur)) 39 for key, val := range cur { 40 curCopy[key] = val 41 } 42 43 out = append(out, curCopy) 44 } 45 46 return out 47 } 48 49 // failOnDifferentLengths fails the provided test if wantedLen != gotLen. Returns true if the test 50 // failed, false otherwise. 51 func failOnDifferentLengths(t *testing.T, wantedLen int, gotLen int) bool { 52 if wantedLen != gotLen { 53 t.Errorf("Wanted result and actual result have different lengths (%d vs. %d)", wantedLen, gotLen) 54 return true 55 } 56 return false 57 } 58 59 // failOnMismatchedSlices fails the provided test if the provided slices have different lengths or 60 // if their elements do not match (in order). It returns true if it failed and false otherwise. 61 // 62 // The slices must both be of the same type, and must be string slices or int slices. The function 63 // panics if either of these is not true. 64 func failOnMismatchedSlices(t *testing.T, want interface{}, got interface{}) bool { 65 switch want := want.(type) { 66 case []string: 67 switch got := got.(type) { 68 case []string: 69 if failOnDifferentLengths(t, len(want), len(got)) { 70 return true 71 } 72 for i := range want { 73 if want[i] != got[i] { 74 t.Errorf("Wanted value and actual value did not match.\nWanted: %#v\nActual: %#v", want, got) 75 return true 76 } 77 } 78 default: 79 t.Logf("Type of want does not equal type of got") 80 t.FailNow() 81 } 82 case []int: 83 switch got := got.(type) { 84 case []int: 85 if failOnDifferentLengths(t, len(want), len(got)) { 86 return true 87 } 88 for i := range want { 89 if want[i] != got[i] { 90 t.Errorf("Wanted value and actual value did not match.\nWanted: %#v\nActual: %#v", want, got) 91 return true 92 } 93 } 94 default: 95 t.Logf("Type of want does not equal type of got") 96 t.FailNow() 97 } 98 default: 99 t.Logf("want and got must be of type []string or []int") 100 t.FailNow() 101 } 102 103 return false 104 } 105 106 // failOnMismatchedTestSlices fails the provided test t if the provided test slices have different 107 // lengths or if their elements do not match (in order). It creates a series of subtests for teach 108 // pair of tests in the slices. 109 func failOnMismatchedTestSlices(t *testing.T, want []test, got []test) { 110 if failOnDifferentLengths(t, len(want), len(want)) { 111 return 112 } 113 for j := range want { 114 wantTest := want[j] 115 gotTest := got[j] 116 117 t.Run(fmt.Sprintf("tests[%d]", j), func(t *testing.T) { 118 if wantTest.Name != gotTest.Name { 119 t.Errorf("name = %s, wanted %s", gotTest.Name, wantTest.Name) 120 return 121 } 122 if failOnDifferentLengths(t, len(wantTest.Jobs), len(gotTest.Jobs)) { 123 return 124 } 125 for k := range wantTest.Jobs { 126 wantJobs := wantTest.Jobs[k] 127 gotJobs := gotTest.Jobs[k] 128 if wantJobs.Name != gotJobs.Name { 129 t.Errorf("name = %s, wanted %s", gotJobs.Name, wantJobs.Name) 130 return 131 } 132 failOnMismatchedSlices(t, wantJobs.BuildNumbers, gotJobs.BuildNumbers) 133 } 134 }) 135 } 136 } 137 138 func TestSummarize(t *testing.T) { 139 // Setup 140 tmpdir := t.TempDir() 141 142 // Save the old working directory 143 olddir, err := os.Getwd() 144 if err != nil { 145 t.Errorf("Could not get the application working directory: %s", err) 146 return 147 } 148 err = os.Chdir(tmpdir) // Set the working directory to the temp directory 149 if err != nil { 150 t.Errorf("Could not set the application working directory to the temp directory: %s", err) 151 return 152 } 153 defer func() { 154 err = os.Chdir(olddir) // Set the working directory back to the normal directory 155 if err != nil { 156 t.Errorf("Could not set the application working directory back to the normal directory: %s", err) 157 return 158 } 159 }() 160 161 // Create some test input files 162 163 // builds 164 buildsPath := "builds.json" 165 builds := smear([]map[string]string{ 166 {"started": "1234", "number": "1", "tests_failed": "1", "tests_run": "2", "elapsed": "4", 167 "path": "gs://logs/some-job/1", "job": "some-job", "result": "SUCCESS"}, 168 {"number": "2", "path": "gs://logs/some-job/2"}, 169 {"number": "3", "path": "gs://logs/some-job/3"}, 170 {"number": "4", "path": "gs://logs/some-job/4"}, 171 {"number": "5", "path": "gs://logs/other-job/5", "job": "other-job", "elapsed": "8"}, 172 {"number": "7", "path": "gs://logs/other-job/7", "result": "FAILURE"}, 173 }) 174 err = writeJSON(buildsPath, builds) 175 if err != nil { 176 t.Errorf("Could not write builds.json: %s", err) 177 return 178 } 179 180 // tests 181 testsPath := "tests.json" 182 testsTemp := smear([]map[string]string{ 183 {"name": "example test", "build": "gs://logs/some-job/1", 184 "failure_text": "some awful stack trace exit 1"}, 185 {"build": "gs://logs/some-job/2"}, 186 {"build": "gs://logs/some-job/3"}, 187 {"build": "gs://logs/some-job/4"}, 188 {"name": "another test", "failure_text": "some other error message"}, 189 {"name": "unrelated test", "build": "gs://logs/other-job/5"}, 190 {}, // Intentional dupe 191 {"build": "gs://logs/other-job/7"}, 192 }) 193 tests := make([]byte, 0) 194 for _, obj := range testsTemp { 195 result, err := json.Marshal(obj) 196 if err != nil { 197 t.Errorf("Could not encode JSON.\nError: %s\nObject: %#v", err, obj) 198 return 199 } 200 201 tests = append(tests, result...) 202 tests = append(tests, []byte("\n")...) 203 } 204 err = os.WriteFile(testsPath, tests, 0644) 205 if err != nil { 206 t.Errorf("Could not write JSON to file: %s", err) 207 return 208 } 209 210 // owners 211 ownersPath := "owners.json" 212 err = writeJSON(ownersPath, map[string][]string{"node": {"example"}}) 213 if err != nil { 214 t.Errorf("Could not write JSON to file: %s", err) 215 return 216 } 217 218 // Call summarize() 219 summarize(summarizeFlags{ 220 builds: buildsPath, 221 tests: []string{testsPath}, 222 previous: "", 223 owners: ownersPath, 224 output: "failure_data.json", 225 outputSlices: "failure_data_PREFIX.json", 226 numWorkers: 4, // Arbitrary number to keep tests more or less consistent across platforms 227 memoize: false, 228 maxClusterTextLength: 10000, 229 }) 230 231 // Test the output 232 var output jsonOutput 233 err = getJSON("failure_data.json", &output) 234 if err != nil { 235 t.Errorf("Could not retrieve summarize() results: %s", err) 236 } 237 238 // Grab two random hashes for use across some of the following tests 239 randomHash1 := output.Clustered[0].ID 240 randomHash2 := output.Clustered[1].ID 241 242 t.Run("Main output", func(t *testing.T) { 243 // Test each field as a subtest 244 245 t.Run("builds", func(t *testing.T) { 246 want := columns{ 247 Cols: columnarBuilds{ 248 Elapsed: []int{8, 8, 4, 4, 4, 4}, 249 Executor: []string{"", "", "", "", "", ""}, 250 PR: []string{"", "", "", "", "", ""}, 251 Result: []string{ 252 "SUCCESS", 253 "FAILURE", 254 "SUCCESS", 255 "SUCCESS", 256 "SUCCESS", 257 "SUCCESS", 258 }, 259 Started: []int{1234, 1234, 1234, 1234, 1234, 1234}, 260 TestsFailed: []int{1, 1, 1, 1, 1, 1}, 261 TestsRun: []int{2, 2, 2, 2, 2, 2}, 262 }, 263 JobPaths: map[string]string{ 264 "other-job": "gs://logs/other-job", 265 "some-job": "gs://logs/some-job", 266 }, 267 Jobs: map[string]jobCollection{ 268 // JSON keys are always strings, so although this is created as a map[int]x, 269 // we'll check it as a map[string]x 270 "other-job": map[string]int{"5": 0, "7": 1}, 271 "some-job": []int{1, 4, 2}, 272 }, 273 } 274 275 got := output.Builds 276 277 // Go through each of got's sections and determine if they match want 278 t.Run("cols", func(t *testing.T) { 279 wantCols := want.Cols 280 gotCols := got.Cols 281 282 intTestCases := []struct { 283 name string 284 got []int 285 want []int 286 }{ 287 {"elapsed", gotCols.Elapsed, wantCols.Elapsed}, 288 {"started", gotCols.Started, wantCols.Started}, 289 {"testsFailed", gotCols.TestsFailed, wantCols.TestsFailed}, 290 {"testsRun", gotCols.TestsRun, wantCols.TestsRun}, 291 } 292 293 for _, tc := range intTestCases { 294 t.Run(tc.name, func(t *testing.T) { 295 failOnMismatchedSlices(t, tc.want, tc.got) 296 }) 297 } 298 299 stringTestCases := []struct { 300 name string 301 got []string 302 want []string 303 }{ 304 {"executor", gotCols.Executor, wantCols.Executor}, 305 {"pr", gotCols.PR, wantCols.PR}, 306 {"result", gotCols.Result, wantCols.Result}, 307 } 308 309 for _, tc := range stringTestCases { 310 t.Run(tc.name, func(t *testing.T) { 311 failOnMismatchedSlices(t, tc.want, tc.got) 312 }) 313 } 314 }) 315 316 t.Run("jobPaths", func(t *testing.T) { 317 wantJobPaths := want.JobPaths 318 gotJobPaths := got.JobPaths 319 320 if failOnDifferentLengths(t, len(wantJobPaths), len(gotJobPaths)) { 321 return 322 } 323 324 failed := false 325 for key, wantedResult := range wantJobPaths { 326 // Check if each want key exists in got 327 if gotResult, ok := gotJobPaths[key]; ok { 328 // If so, do their values match? 329 if wantedResult != gotResult { 330 failed = true 331 break 332 } 333 } else { 334 failed = true 335 break 336 } 337 } 338 339 if failed { 340 t.Errorf("Wanted result (%#v) and actual result (%#v) do not match", wantJobPaths, gotJobPaths) 341 } 342 }) 343 344 t.Run("jobs", func(t *testing.T) { 345 wantJobs := want.Jobs 346 gotJobs := got.Jobs 347 348 if failOnDifferentLengths(t, len(wantJobs), len(gotJobs)) { 349 return 350 } 351 352 for jobName := range wantJobs { 353 t.Run(jobName, func(t *testing.T) { 354 // The values before they are type-checked 355 wantJobCollection := wantJobs[jobName] 356 gotJobCollection := gotJobs[jobName] 357 358 switch wantJobCollection := wantJobCollection.(type) { 359 case map[string]int: 360 wantMap := wantJobCollection 361 // json.Unmarshal converts objects to map[string]interface{}, we'll have 362 // to check the type of each value separately 363 gotMap := gotJobCollection.(map[string]interface{}) 364 365 if failOnDifferentLengths(t, len(wantMap), len(gotMap)) { 366 return 367 } 368 for key := range wantMap { 369 wantVal := wantMap[key] 370 if gotVal, ok := gotMap[key]; ok { 371 // json.Unmarshal represents all numbers as float64, convert to int 372 if wantVal != int(gotVal.(float64)) { 373 t.Errorf("Wanted value of %d for key '%s', got %d", wantVal, key, int(gotVal.(float64))) 374 } 375 } else { 376 t.Errorf("No value in gotMap for key '%s'", key) 377 } 378 } 379 case []int: 380 wantSlice := wantJobCollection 381 // json.Unmarshal converts slices to []interface{}, we'll have 382 // to check the type of each value separately 383 gotSlice := gotJobCollection.([]interface{}) 384 if failOnDifferentLengths(t, len(wantSlice), len(gotSlice)) { 385 return 386 } 387 for i := range wantSlice { 388 // json.Unmarshal represents all numbers as float64, convert to int 389 if wantSlice[i] != int(gotSlice[i].(float64)) { 390 t.Errorf("Want slice (%#v) does not match got slice (%#v)", wantSlice, gotSlice) 391 return 392 } 393 } 394 } 395 }) 396 } 397 }) 398 }) 399 400 t.Run("clustered", func(t *testing.T) { 401 want := []jsonCluster{ 402 { 403 ID: randomHash1, 404 Key: "some awful stack trace exit 1", 405 Tests: []test{ 406 { 407 Jobs: []job{ 408 { 409 BuildNumbers: []string{"4", "3", "2", "1"}, 410 Name: "some-job", 411 }, 412 }, 413 Name: "example test", 414 }, 415 }, 416 Spans: []int{29}, 417 Owner: "node", 418 Text: "some awful stack trace exit 1", 419 }, 420 { 421 ID: randomHash2, 422 Key: "some other error message", 423 Tests: []test{ 424 { 425 Jobs: []job{ 426 { 427 BuildNumbers: []string{"7", "5"}, 428 Name: "other-job", 429 }, 430 }, 431 Name: "unrelated test", 432 }, 433 { 434 Jobs: []job{ 435 { 436 BuildNumbers: []string{"4"}, 437 Name: "some-job", 438 }, 439 }, 440 Name: "another test", 441 }, 442 }, 443 Spans: []int{24}, 444 Owner: "testing", 445 Text: "some other error message", 446 }, 447 } 448 449 got := output.Clustered 450 451 // Check lengths 452 if failOnDifferentLengths(t, len(want), len(got)) { 453 return 454 } 455 456 // Go through each of got's sections and determine if they match want 457 for i := range want { 458 currentWant := want[i] 459 currentGot := got[i] 460 t.Run(fmt.Sprintf("want[%d]", i), func(t *testing.T) { 461 // Simple string equality checking 462 stringTestCases := []struct { 463 name string 464 got string 465 want string 466 }{ 467 {"id", currentWant.ID, currentWant.ID}, 468 {"key", currentWant.Key, currentWant.Key}, 469 {"owner", currentWant.Owner, currentWant.Owner}, 470 {"text", currentWant.Text, currentWant.Text}, 471 } 472 for _, tc := range stringTestCases { 473 t.Run(tc.name, func(t *testing.T) { 474 if tc.want != tc.got { 475 t.Errorf("Wanted result (%s) and actual result (%s) do not match", tc.want, tc.got) 476 } 477 }) 478 } 479 480 // Simple int slice 481 t.Run("spans", func(t *testing.T) { 482 failOnMismatchedSlices(t, currentWant.Spans, currentWant.Spans) 483 }) 484 485 // tests 486 t.Run("tests", func(t *testing.T) { 487 failOnMismatchedTestSlices(t, currentWant.Tests, currentGot.Tests) 488 }) 489 }) 490 } 491 }) 492 }) 493 494 t.Run("Slices", func(t *testing.T) { 495 var renderedSlice renderedSliceOutput 496 497 filepath := fmt.Sprintf("failure_data_%s.json", randomHash1[:2]) 498 499 err := getJSON(filepath, &renderedSlice) 500 if err != nil { 501 t.Error(err) 502 return 503 } 504 505 t.Run("clustered", func(t *testing.T) { 506 want := []jsonCluster{output.Clustered[0]} 507 got := renderedSlice.Clustered 508 509 if failOnDifferentLengths(t, len(want), len(got)) { 510 return 511 } 512 for i := range want { 513 t.Run(fmt.Sprintf("got[%d]", i), func(t *testing.T) { 514 stringTestCases := []struct { 515 name string 516 want string 517 got string 518 }{ 519 {"key", want[i].Key, got[i].Key}, 520 {"id", want[i].ID, got[i].ID}, 521 {"text", want[i].Text, got[i].Text}, 522 {"owner", want[i].Owner, got[i].Owner}, 523 } 524 for _, tc := range stringTestCases { 525 t.Run(tc.name, func(t *testing.T) { 526 if tc.got != tc.want { 527 t.Errorf("Wanted value (%#v) did not match actual value (%#v)", want, got) 528 } 529 }) 530 } 531 532 t.Run("spans", func(t *testing.T) { 533 failOnMismatchedSlices(t, want[i].Spans, got[i].Spans) 534 }) 535 536 t.Run("tests", func(t *testing.T) { 537 failOnMismatchedTestSlices(t, want[i].Tests, got[i].Tests) 538 }) 539 }) 540 } 541 }) 542 543 t.Run("builds.cols.started", func(t *testing.T) { 544 want := []int{1234, 1234, 1234, 1234} 545 got := renderedSlice.Builds.Cols.Started 546 547 failOnMismatchedSlices(t, want, got) 548 }) 549 }) 550 551 // Call summarize() with no owners file 552 t.Run("No owners file", func(t *testing.T) { 553 summarize(summarizeFlags{ 554 builds: buildsPath, 555 tests: []string{testsPath}, 556 previous: "", 557 owners: "", 558 output: "failure_data.json", 559 outputSlices: "failure_data_PREFIX.json", 560 }) 561 }) 562 }