github.com/GoogleCloudPlatform/testgrid@v0.0.174/pkg/tabulator/tabstate_test.go (about) 1 /* 2 Copyright 2022 The TestGrid 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 tabulator 18 19 import ( 20 "context" 21 "testing" 22 23 "github.com/google/go-cmp/cmp" 24 "github.com/sirupsen/logrus" 25 "google.golang.org/protobuf/testing/protocmp" 26 27 configpb "github.com/GoogleCloudPlatform/testgrid/pb/config" 28 statepb "github.com/GoogleCloudPlatform/testgrid/pb/state" 29 tspb "github.com/GoogleCloudPlatform/testgrid/pb/test_status" 30 "github.com/GoogleCloudPlatform/testgrid/pkg/updater" 31 "github.com/GoogleCloudPlatform/testgrid/util/gcs" 32 "github.com/GoogleCloudPlatform/testgrid/util/gcs/fake" 33 ) 34 35 func TestTabStatePath(t *testing.T) { 36 path := newPathOrDie("gs://bucket/config") 37 cases := []struct { 38 name string 39 dashboardName string 40 tabName string 41 tabStatePrefix string 42 expected *gcs.Path 43 }{ 44 { 45 name: "basically works", 46 expected: path, 47 }, 48 { 49 name: "invalid dashboard name errors", 50 dashboardName: "---://foo", 51 tabName: "ok", 52 }, 53 { 54 name: "invalid tab name errors", 55 dashboardName: "cool", 56 tabName: "--??!f///", 57 }, 58 { 59 name: "bucket change errors", 60 dashboardName: "gs://honey-bucket/config", 61 tabName: "tab", 62 }, 63 { 64 name: "normal behavior works", 65 dashboardName: "dashboard", 66 tabName: "some-tab", 67 expected: newPathOrDie("gs://bucket/dashboard/some-tab"), 68 }, 69 { 70 name: "target a subfolder works", 71 tabStatePrefix: "tab-state", 72 dashboardName: "dashboard", 73 tabName: "some-tab", 74 expected: newPathOrDie("gs://bucket/tab-state/dashboard/some-tab"), 75 }, 76 } 77 78 for _, tc := range cases { 79 t.Run(tc.name, func(t *testing.T) { 80 actual, err := TabStatePath(*path, tc.tabStatePrefix, tc.dashboardName, tc.tabName) 81 switch { 82 case err != nil: 83 if tc.expected != nil { 84 t.Errorf("tabStatePath(%v, %v) got unexpected error: %v", tc.dashboardName, tc.tabName, err) 85 } 86 case tc.expected == nil: 87 t.Errorf("tabStatePath(%v, %v) failed to receive an error", tc.dashboardName, tc.tabName) 88 default: 89 if diff := cmp.Diff(actual, tc.expected, cmp.AllowUnexported(gcs.Path{})); diff != "" { 90 t.Errorf("tabStatePath(%v, %v) got unexpected diff (-have, +want):\n%s", tc.dashboardName, tc.tabName, diff) 91 } 92 } 93 }) 94 } 95 } 96 97 func newPathOrDie(s string) *gcs.Path { 98 p, err := gcs.NewPath(s) 99 if err != nil { 100 panic(err) 101 } 102 return p 103 } 104 105 func Test_DropEmptyColumns(t *testing.T) { 106 testcases := []struct { 107 name string 108 grid []updater.InflatedColumn 109 expected []updater.InflatedColumn 110 }{ 111 { 112 name: "empty", 113 grid: []updater.InflatedColumn{}, 114 expected: []updater.InflatedColumn{}, 115 }, 116 { 117 name: "nil", 118 grid: nil, 119 expected: []updater.InflatedColumn{}, 120 }, 121 { 122 name: "drops empty column", 123 grid: []updater.InflatedColumn{ 124 { 125 Column: &statepb.Column{Name: "full"}, 126 Cells: map[string]updater.Cell{ 127 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 128 "second": {Result: tspb.TestStatus_PASS_WITH_SKIPS}, 129 "third": {Result: tspb.TestStatus_FAIL}, 130 }, 131 }, 132 { 133 Column: &statepb.Column{Name: "empty"}, 134 Cells: map[string]updater.Cell{ 135 "first": {Result: tspb.TestStatus_NO_RESULT}, 136 "second": {Result: tspb.TestStatus_NO_RESULT}, 137 "third": {Result: tspb.TestStatus_NO_RESULT}, 138 }, 139 }, 140 { 141 Column: &statepb.Column{Name: "sparse"}, 142 Cells: map[string]updater.Cell{ 143 "first": {Result: tspb.TestStatus_NO_RESULT}, 144 "second": {Result: tspb.TestStatus_TIMED_OUT}, 145 "third": {Result: tspb.TestStatus_NO_RESULT}, 146 }, 147 }, 148 }, 149 expected: []updater.InflatedColumn{ 150 { 151 Column: &statepb.Column{Name: "full"}, 152 Cells: map[string]updater.Cell{ 153 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 154 "second": {Result: tspb.TestStatus_PASS_WITH_SKIPS}, 155 "third": {Result: tspb.TestStatus_FAIL}, 156 }, 157 }, 158 { 159 Column: &statepb.Column{Name: "sparse"}, 160 Cells: map[string]updater.Cell{ 161 "first": {Result: tspb.TestStatus_NO_RESULT}, 162 "second": {Result: tspb.TestStatus_TIMED_OUT}, 163 "third": {Result: tspb.TestStatus_NO_RESULT}, 164 }, 165 }, 166 }, 167 }, 168 { 169 name: "drop multiple columns in a row", 170 grid: []updater.InflatedColumn{ 171 { 172 Column: &statepb.Column{Name: "empty"}, 173 Cells: map[string]updater.Cell{ 174 "first": {Result: tspb.TestStatus_NO_RESULT}, 175 "second": {Result: tspb.TestStatus_NO_RESULT}, 176 "third": {Result: tspb.TestStatus_NO_RESULT}, 177 }, 178 }, 179 { 180 Column: &statepb.Column{Name: "nothing"}, 181 Cells: map[string]updater.Cell{ 182 "first": {Result: tspb.TestStatus_NO_RESULT}, 183 "second": {Result: tspb.TestStatus_NO_RESULT}, 184 "third": {Result: tspb.TestStatus_NO_RESULT}, 185 }, 186 }, 187 { 188 Column: &statepb.Column{Name: "full"}, 189 Cells: map[string]updater.Cell{ 190 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 191 "second": {Result: tspb.TestStatus_PASS_WITH_SKIPS}, 192 "third": {Result: tspb.TestStatus_FAIL}, 193 }, 194 }, 195 { 196 Column: &statepb.Column{Name: "zero"}, 197 Cells: map[string]updater.Cell{ 198 "first": {Result: tspb.TestStatus_NO_RESULT}, 199 "second": {Result: tspb.TestStatus_NO_RESULT}, 200 "third": {Result: tspb.TestStatus_NO_RESULT}, 201 }, 202 }, 203 { 204 Column: &statepb.Column{Name: "nada"}, 205 Cells: map[string]updater.Cell{ 206 "first": {Result: tspb.TestStatus_NO_RESULT}, 207 "second": {Result: tspb.TestStatus_NO_RESULT}, 208 "third": {Result: tspb.TestStatus_NO_RESULT}, 209 }, 210 }, 211 }, 212 expected: []updater.InflatedColumn{ 213 { 214 Column: &statepb.Column{Name: "full"}, 215 Cells: map[string]updater.Cell{ 216 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 217 "second": {Result: tspb.TestStatus_PASS_WITH_SKIPS}, 218 "third": {Result: tspb.TestStatus_FAIL}, 219 }, 220 }, 221 }, 222 }, 223 { 224 name: "don't drop everything", 225 grid: []updater.InflatedColumn{ 226 { 227 Column: &statepb.Column{Name: "first"}, 228 Cells: map[string]updater.Cell{ 229 "first": {Result: tspb.TestStatus_NO_RESULT}, 230 "second": {Result: tspb.TestStatus_NO_RESULT}, 231 "third": {Result: tspb.TestStatus_NO_RESULT}, 232 }, 233 }, 234 { 235 Column: &statepb.Column{Name: "empty"}, 236 Cells: map[string]updater.Cell{ 237 "first": {Result: tspb.TestStatus_NO_RESULT}, 238 "second": {Result: tspb.TestStatus_NO_RESULT}, 239 "third": {Result: tspb.TestStatus_NO_RESULT}, 240 }, 241 }, 242 { 243 Column: &statepb.Column{Name: "nada"}, 244 Cells: map[string]updater.Cell{ 245 "first": {Result: tspb.TestStatus_NO_RESULT}, 246 "second": {Result: tspb.TestStatus_NO_RESULT}, 247 "third": {Result: tspb.TestStatus_NO_RESULT}, 248 }, 249 }, 250 }, 251 expected: []updater.InflatedColumn{ 252 { 253 Column: &statepb.Column{Name: "first"}, 254 Cells: map[string]updater.Cell{ 255 "first": {Result: tspb.TestStatus_NO_RESULT}, 256 "second": {Result: tspb.TestStatus_NO_RESULT}, 257 "third": {Result: tspb.TestStatus_NO_RESULT}, 258 }, 259 }, 260 }, 261 }, 262 } 263 264 for _, tc := range testcases { 265 t.Run(tc.name, func(t *testing.T) { 266 actual := dropEmptyColumns(tc.grid) 267 if diff := cmp.Diff(actual, tc.expected, protocmp.Transform()); diff != "" { 268 t.Errorf("(-got, +want): %s", diff) 269 } 270 }) 271 } 272 } 273 274 func Test_Tabulate(t *testing.T) { 275 testcases := []struct { 276 name string 277 grid *statepb.Grid 278 dashCfg *configpb.DashboardTab 279 groupCfg *configpb.TestGroup 280 calculateStats bool 281 useTabAlert bool 282 expected *statepb.Grid 283 }{ 284 { 285 name: "empty grid is tolerated", 286 grid: &statepb.Grid{}, 287 dashCfg: &configpb.DashboardTab{}, 288 groupCfg: &configpb.TestGroup{}, 289 expected: &statepb.Grid{}, 290 }, 291 { 292 name: "nil grid is not tolerated", 293 dashCfg: &configpb.DashboardTab{}, 294 groupCfg: &configpb.TestGroup{}, 295 }, 296 { 297 name: "nil config is not tolerated", 298 grid: &statepb.Grid{}, 299 }, 300 { 301 name: "basic grid", 302 grid: buildGrid(t, 303 updater.InflatedColumn{ 304 Column: &statepb.Column{Name: "okay"}, 305 Cells: map[string]updater.Cell{ 306 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 307 "second": {Result: tspb.TestStatus_BUILD_PASSED}, 308 }, 309 }, 310 updater.InflatedColumn{ 311 Column: &statepb.Column{Name: "still-ok"}, 312 Cells: map[string]updater.Cell{ 313 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 314 "second": {Result: tspb.TestStatus_BUILD_PASSED}, 315 }, 316 }), 317 dashCfg: &configpb.DashboardTab{ 318 Name: "tab", 319 }, 320 groupCfg: &configpb.TestGroup{}, 321 expected: &statepb.Grid{ 322 Columns: []*statepb.Column{ 323 {Name: "okay"}, 324 {Name: "still-ok"}, 325 }, 326 Rows: []*statepb.Row{ 327 { 328 Name: "first", 329 Id: "first", 330 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2}, 331 }, 332 { 333 Name: "second", 334 Id: "second", 335 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2}, 336 }, 337 }, 338 }, 339 }, 340 { 341 name: "Filters out regex tabs", 342 grid: buildGrid(t, 343 updater.InflatedColumn{ 344 Column: &statepb.Column{Name: "okay"}, 345 Cells: map[string]updater.Cell{ 346 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 347 "bad": {Result: tspb.TestStatus_BUILD_PASSED}, 348 }, 349 }, 350 updater.InflatedColumn{ 351 Column: &statepb.Column{Name: "still-ok"}, 352 Cells: map[string]updater.Cell{ 353 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 354 "bad": {Result: tspb.TestStatus_BUILD_PASSED}, 355 }, 356 }), 357 dashCfg: &configpb.DashboardTab{ 358 Name: "tab", 359 BaseOptions: "exclude-filter-by-regex=bad", 360 }, 361 groupCfg: &configpb.TestGroup{}, 362 expected: &statepb.Grid{ 363 Columns: []*statepb.Column{ 364 {Name: "okay"}, 365 {Name: "still-ok"}, 366 }, 367 Rows: []*statepb.Row{ 368 { 369 Name: "first", 370 Id: "first", 371 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2}, 372 }, 373 }, 374 }, 375 }, 376 { 377 name: "Filters out regex tabs, and drops empty columns", 378 grid: buildGrid(t, 379 updater.InflatedColumn{ 380 Column: &statepb.Column{Name: "okay"}, 381 Cells: map[string]updater.Cell{ 382 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 383 }, 384 }, 385 updater.InflatedColumn{ 386 Column: &statepb.Column{Name: "weird"}, 387 Cells: map[string]updater.Cell{ 388 "bad": {Result: tspb.TestStatus_BUILD_PASSED}, 389 }, 390 }, 391 updater.InflatedColumn{ 392 Column: &statepb.Column{Name: "still-ok"}, 393 Cells: map[string]updater.Cell{ 394 "first": {Result: tspb.TestStatus_BUILD_PASSED}, 395 }, 396 }), 397 dashCfg: &configpb.DashboardTab{ 398 Name: "tab", 399 BaseOptions: "exclude-filter-by-regex=bad", 400 }, 401 groupCfg: &configpb.TestGroup{}, 402 expected: &statepb.Grid{ 403 Columns: []*statepb.Column{ 404 {Name: "okay"}, 405 {Name: "still-ok"}, 406 }, 407 Rows: []*statepb.Row{ 408 { 409 Name: "first", 410 Id: "first", 411 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 2}, 412 }, 413 }, 414 }, 415 }, 416 { 417 name: "calculate alerts using test group", 418 grid: buildGrid(t, 419 updater.InflatedColumn{ 420 Column: &statepb.Column{Name: "final"}, 421 Cells: map[string]updater.Cell{ 422 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 423 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 424 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 425 }, 426 }, 427 updater.InflatedColumn{ 428 Column: &statepb.Column{Name: "middle"}, 429 Cells: map[string]updater.Cell{ 430 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 431 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 432 "flaky": {Result: tspb.TestStatus_BUILD_FAIL}, 433 }, 434 }, 435 updater.InflatedColumn{ 436 Column: &statepb.Column{Name: "initial"}, 437 Cells: map[string]updater.Cell{ 438 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 439 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 440 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 441 }, 442 }), 443 dashCfg: &configpb.DashboardTab{}, 444 groupCfg: &configpb.TestGroup{ 445 Name: "group", 446 NumFailuresToAlert: 1, 447 NumPassesToDisableAlert: 1, 448 }, 449 useTabAlert: false, 450 expected: &statepb.Grid{ 451 Columns: []*statepb.Column{ 452 {Name: "final"}, 453 {Name: "middle"}, 454 {Name: "initial"}, 455 }, 456 Rows: []*statepb.Row{ 457 { 458 Name: "okay", 459 Id: "okay", 460 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3}, 461 }, 462 { 463 Name: "broken", 464 Id: "broken", 465 Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3}, 466 AlertInfo: &statepb.AlertInfo{ 467 FailCount: 3, 468 }, 469 }, 470 { 471 Name: "flaky", 472 Id: "flaky", 473 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1}, 474 }, 475 }, 476 }, 477 }, 478 { 479 name: "calculate alerts using tab state", 480 grid: buildGrid(t, 481 updater.InflatedColumn{ 482 Column: &statepb.Column{Name: "final"}, 483 Cells: map[string]updater.Cell{ 484 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 485 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 486 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 487 }, 488 }, 489 updater.InflatedColumn{ 490 Column: &statepb.Column{Name: "middle"}, 491 Cells: map[string]updater.Cell{ 492 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 493 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 494 "flaky": {Result: tspb.TestStatus_BUILD_FAIL}, 495 }, 496 }, 497 updater.InflatedColumn{ 498 Column: &statepb.Column{Name: "initial"}, 499 Cells: map[string]updater.Cell{ 500 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 501 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 502 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 503 }, 504 }), 505 dashCfg: &configpb.DashboardTab{ 506 Name: "tab", 507 AlertOptions: &configpb.DashboardTabAlertOptions{ 508 NumFailuresToAlert: 1, 509 NumPassesToDisableAlert: 1, 510 }, 511 }, 512 groupCfg: &configpb.TestGroup{}, 513 useTabAlert: true, 514 expected: &statepb.Grid{ 515 Columns: []*statepb.Column{ 516 {Name: "final"}, 517 {Name: "middle"}, 518 {Name: "initial"}, 519 }, 520 Rows: []*statepb.Row{ 521 { 522 Name: "okay", 523 Id: "okay", 524 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3}, 525 }, 526 { 527 Name: "broken", 528 Id: "broken", 529 Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3}, 530 AlertInfo: &statepb.AlertInfo{ 531 FailCount: 3, 532 }, 533 }, 534 { 535 Name: "flaky", 536 Id: "flaky", 537 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1}, 538 }, 539 }, 540 }, 541 }, 542 { 543 name: "calculate stats", 544 grid: buildGrid(t, 545 updater.InflatedColumn{ 546 Column: &statepb.Column{Name: "final"}, 547 Cells: map[string]updater.Cell{ 548 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 549 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 550 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 551 }, 552 }, 553 updater.InflatedColumn{ 554 Column: &statepb.Column{Name: "middle"}, 555 Cells: map[string]updater.Cell{ 556 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 557 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 558 "flaky": {Result: tspb.TestStatus_BUILD_FAIL}, 559 }, 560 }, 561 updater.InflatedColumn{ 562 Column: &statepb.Column{Name: "initial"}, 563 Cells: map[string]updater.Cell{ 564 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 565 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 566 "flaky": {Result: tspb.TestStatus_BUILD_PASSED}, 567 }, 568 }), 569 dashCfg: &configpb.DashboardTab{ 570 Name: "tab", 571 AlertOptions: &configpb.DashboardTabAlertOptions{ 572 NumFailuresToAlert: 1, 573 NumPassesToDisableAlert: 1, 574 }, 575 BrokenColumnThreshold: 0.5, 576 }, 577 groupCfg: &configpb.TestGroup{}, 578 useTabAlert: true, 579 calculateStats: true, 580 expected: &statepb.Grid{ 581 Columns: []*statepb.Column{ 582 { 583 Name: "final", 584 Stats: &statepb.Stats{ 585 PassCount: 2, 586 FailCount: 1, 587 TotalCount: 3, 588 }, 589 }, 590 { 591 Name: "middle", 592 Stats: &statepb.Stats{ 593 PassCount: 1, 594 FailCount: 2, 595 TotalCount: 3, 596 Broken: true, 597 }, 598 }, 599 { 600 Name: "initial", 601 Stats: &statepb.Stats{ 602 PassCount: 2, 603 FailCount: 1, 604 TotalCount: 3, 605 }, 606 }, 607 }, 608 Rows: []*statepb.Row{ 609 { 610 Name: "okay", 611 Id: "okay", 612 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 3}, 613 }, 614 { 615 Name: "broken", 616 Id: "broken", 617 Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 3}, 618 AlertInfo: &statepb.AlertInfo{ 619 FailCount: 3, 620 }, 621 }, 622 { 623 Name: "flaky", 624 Id: "flaky", 625 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1, int32(tspb.TestStatus_BUILD_FAIL), 1, int32(tspb.TestStatus_BUILD_PASSED), 1}, 626 }, 627 }, 628 }, 629 }, 630 { 631 name: "calculate stats, no broken threshold", 632 grid: buildGrid(t, 633 updater.InflatedColumn{ 634 Column: &statepb.Column{Name: "initial"}, 635 Cells: map[string]updater.Cell{ 636 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 637 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 638 }, 639 }), 640 dashCfg: &configpb.DashboardTab{ 641 Name: "tab", 642 AlertOptions: &configpb.DashboardTabAlertOptions{ 643 NumFailuresToAlert: 1, 644 NumPassesToDisableAlert: 1, 645 }, 646 }, 647 groupCfg: &configpb.TestGroup{}, 648 useTabAlert: true, 649 calculateStats: true, 650 expected: &statepb.Grid{ 651 Columns: []*statepb.Column{ 652 { 653 Name: "initial", 654 }, 655 }, 656 Rows: []*statepb.Row{ 657 { 658 Name: "okay", 659 Id: "okay", 660 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1}, 661 }, 662 { 663 Name: "broken", 664 Id: "broken", 665 Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 1}, 666 AlertInfo: &statepb.AlertInfo{ 667 FailCount: 1, 668 }, 669 }, 670 }, 671 }, 672 }, 673 { 674 name: "calculate stats, calculate stats false", 675 grid: buildGrid(t, 676 updater.InflatedColumn{ 677 Column: &statepb.Column{Name: "initial"}, 678 Cells: map[string]updater.Cell{ 679 "okay": {Result: tspb.TestStatus_BUILD_PASSED}, 680 "broken": {Result: tspb.TestStatus_BUILD_FAIL}, 681 }, 682 }), 683 dashCfg: &configpb.DashboardTab{ 684 Name: "tab", 685 AlertOptions: &configpb.DashboardTabAlertOptions{ 686 NumFailuresToAlert: 1, 687 NumPassesToDisableAlert: 1, 688 }, 689 BrokenColumnThreshold: 0.5, 690 }, 691 groupCfg: &configpb.TestGroup{}, 692 useTabAlert: true, 693 calculateStats: false, 694 expected: &statepb.Grid{ 695 Columns: []*statepb.Column{ 696 { 697 Name: "initial", 698 }, 699 }, 700 Rows: []*statepb.Row{ 701 { 702 Name: "okay", 703 Id: "okay", 704 Results: []int32{int32(tspb.TestStatus_BUILD_PASSED), 1}, 705 }, 706 { 707 Name: "broken", 708 Id: "broken", 709 Results: []int32{int32(tspb.TestStatus_BUILD_FAIL), 1}, 710 AlertInfo: &statepb.AlertInfo{ 711 FailCount: 1, 712 }, 713 }, 714 }, 715 }, 716 }, 717 } 718 719 for _, tc := range testcases { 720 t.Run(tc.name, func(t *testing.T) { 721 ctx, cancel := context.WithCancel(context.Background()) 722 defer cancel() 723 724 actual, err := tabulate(ctx, logrus.New(), tc.grid, tc.dashCfg, tc.groupCfg, tc.calculateStats, tc.useTabAlert, nil) // TODO(slchase): add tests for not nil 725 if tc.expected == nil { 726 if err == nil { 727 t.Error("Expected an error, but got none") 728 } 729 return 730 } 731 732 if err != nil { 733 t.Fatalf("Unexpected error: %v", err) 734 } 735 736 diff := cmp.Diff(actual, tc.expected, protocmp.Transform(), 737 protocmp.IgnoreFields(&statepb.Row{}, "cell_ids", "icons", "messages", "user_property", "properties"), // mostly empty 738 protocmp.IgnoreFields(&statepb.AlertInfo{}, "fail_time"), // import not needed to determine if alert was set 739 protocmp.SortRepeatedFields(&statepb.Grid{}, "rows")) // rows have no canonical order 740 if diff != "" { 741 t.Errorf("(-got, +want): %s", diff) 742 } 743 }) 744 } 745 } 746 747 func buildGrid(t *testing.T, cols ...updater.InflatedColumn) *statepb.Grid { 748 t.Helper() 749 var g statepb.Grid 750 r := map[string]*statepb.Row{} 751 for _, col := range cols { 752 updater.AppendColumn(&g, r, col) 753 } 754 return &g 755 } 756 757 func Test_CreateTabState(t *testing.T) { 758 var exampleGrid statepb.Grid 759 updater.AppendColumn(&exampleGrid, map[string]*statepb.Row{}, updater.InflatedColumn{ 760 Column: &statepb.Column{Name: "full"}, 761 Cells: map[string]updater.Cell{ 762 "some data": {Result: tspb.TestStatus_BUILD_PASSED}, 763 }, 764 }) 765 766 testcases := []struct { 767 name string 768 state *statepb.Grid 769 confirm bool 770 expectError bool 771 expectUpload bool 772 }{ 773 { 774 name: "Fails if data is missing", 775 expectError: true, 776 }, 777 { 778 name: "Does not write without confirm", 779 state: &exampleGrid, 780 confirm: false, 781 expectError: false, 782 }, 783 { 784 name: "Writes data when upload is expected", 785 state: &exampleGrid, 786 confirm: true, 787 expectUpload: true, 788 }, 789 } 790 791 expectedPath := newPathOrDie("gs://example/prefix/dashboard/tab") 792 configPath := newPathOrDie("gs://example/config") 793 794 for _, tc := range testcases { 795 t.Run(tc.name, func(t *testing.T) { 796 ctx, cancel := context.WithCancel(context.Background()) 797 defer cancel() 798 client := fake.UploadClient{ 799 Uploader: fake.Uploader{}, 800 } 801 802 task := writeTask{ 803 dashboard: &configpb.Dashboard{ 804 Name: "dashboard", 805 }, 806 tab: &configpb.DashboardTab{ 807 Name: "tab", 808 }, 809 group: &configpb.TestGroup{ 810 Name: "testgroup", 811 }, 812 data: tc.state, 813 } 814 815 err := createTabState(ctx, logrus.New(), client, task, *configPath, "prefix", tc.confirm, true, true, false) 816 if tc.expectError == (err == nil) { 817 t.Errorf("Wrong error: want %t, got %v", tc.expectError, err) 818 } 819 res, ok := client.Uploader[*expectedPath] 820 uploaded := ok && (len(res.Buf) != 0) 821 if uploaded != tc.expectUpload { 822 t.Errorf("Wrong upload: want %t, got %v", tc.expectUpload, ok) 823 } 824 }) 825 } 826 } 827 828 func Test_MergeGrids(t *testing.T) { 829 testcases := []struct { 830 name string 831 current []updater.InflatedColumn 832 add []updater.InflatedColumn 833 expect []updater.InflatedColumn 834 }{ 835 { 836 name: "Creating a grid", 837 current: []updater.InflatedColumn{}, 838 add: []updater.InflatedColumn{ 839 { 840 Column: &statepb.Column{ 841 Name: "cool results", 842 Started: 12345678, 843 }, 844 Cells: map[string]updater.Cell{ 845 "cell": {Result: tspb.TestStatus_PASS}, 846 }, 847 }, 848 { 849 Column: &statepb.Column{ 850 Name: "result too big :(", 851 Started: 123456, 852 }, 853 Cells: map[string]updater.Cell{ 854 "cell": {Result: tspb.TestStatus_RUNNING}, 855 }, 856 }, 857 }, 858 expect: []updater.InflatedColumn{ 859 { 860 Column: &statepb.Column{ 861 Name: "cool results", 862 Started: 12345678, 863 }, 864 Cells: map[string]updater.Cell{ 865 "cell": {Result: tspb.TestStatus_PASS}, 866 }, 867 }, 868 { 869 Column: &statepb.Column{ 870 Name: "result too big :(", 871 Started: 123456, 872 }, 873 Cells: map[string]updater.Cell{ 874 "cell": {Result: tspb.TestStatus_RUNNING}, 875 }, 876 }, 877 }, 878 }, 879 { 880 name: "two identical results: displays new result", 881 current: []updater.InflatedColumn{ 882 { 883 Column: &statepb.Column{ 884 Name: "result 2", 885 Build: "build", 886 Started: 1234, 887 }, 888 Cells: map[string]updater.Cell{ 889 "cell": {Result: tspb.TestStatus_RUNNING}, 890 }, 891 }, 892 { 893 Column: &statepb.Column{ 894 Name: "result 1", 895 Build: "build", 896 Started: 123, 897 }, 898 Cells: map[string]updater.Cell{ 899 "cell": {Result: tspb.TestStatus_RUNNING}, 900 }, 901 }, 902 }, 903 add: []updater.InflatedColumn{ 904 { 905 Column: &statepb.Column{ 906 Name: "result 2", 907 Build: "build", 908 Started: 1234, 909 }, 910 Cells: map[string]updater.Cell{ 911 "cell": {Result: tspb.TestStatus_PASS}, 912 }, 913 }, 914 { 915 Column: &statepb.Column{ 916 Name: "result 1", 917 Build: "build", 918 Started: 123, 919 }, 920 Cells: map[string]updater.Cell{ 921 "cell": {Result: tspb.TestStatus_PASS}, 922 }, 923 }, 924 { 925 Column: &statepb.Column{ 926 Name: "too big", 927 Build: "build", 928 Started: 12, 929 }, 930 Cells: map[string]updater.Cell{ 931 "cell": {Result: tspb.TestStatus_UNKNOWN}, 932 }, 933 }, 934 }, 935 expect: []updater.InflatedColumn{ 936 { 937 Column: &statepb.Column{ 938 Name: "result 2", 939 Build: "build", 940 Started: 1234, 941 }, 942 Cells: map[string]updater.Cell{ 943 "cell": {Result: tspb.TestStatus_PASS}, 944 }, 945 }, 946 { 947 Column: &statepb.Column{ 948 Name: "result 1", 949 Build: "build", 950 Started: 123, 951 }, 952 Cells: map[string]updater.Cell{ 953 "cell": {Result: tspb.TestStatus_PASS}, 954 }, 955 }, 956 { 957 Column: &statepb.Column{ 958 Name: "too big", 959 Build: "build", 960 Started: 12, 961 }, 962 Cells: map[string]updater.Cell{ 963 "cell": {Result: tspb.TestStatus_UNKNOWN}, 964 }, 965 }, 966 }, 967 }, 968 { 969 name: "adds new info: keeps historical data", 970 current: []updater.InflatedColumn{ 971 { 972 Column: &statepb.Column{ 973 Name: "fourth", 974 Started: 1234, 975 }, 976 Cells: map[string]updater.Cell{ 977 "cell": {Result: tspb.TestStatus_PASS}, 978 }, 979 }, 980 { 981 Column: &statepb.Column{ 982 Name: "third", 983 Started: 123, 984 }, 985 Cells: map[string]updater.Cell{ 986 "cell": {Result: tspb.TestStatus_PASS}, 987 }, 988 }, 989 { 990 Column: &statepb.Column{ 991 Name: "second", 992 Started: 12, 993 }, 994 Cells: map[string]updater.Cell{ 995 "cell": {Result: tspb.TestStatus_PASS}, 996 }, 997 }, 998 { 999 Column: &statepb.Column{ 1000 Name: "first", 1001 Started: 1, 1002 }, 1003 Cells: map[string]updater.Cell{ 1004 "cell": { 1005 Result: tspb.TestStatus_UNKNOWN, 1006 Icon: "...", 1007 }, 1008 }, 1009 }, 1010 }, 1011 add: []updater.InflatedColumn{ 1012 { 1013 Column: &statepb.Column{ 1014 Name: "sixth", 1015 Started: 123456, 1016 }, 1017 Cells: map[string]updater.Cell{ 1018 "cell": {Result: tspb.TestStatus_PASS}, 1019 }, 1020 }, 1021 { 1022 Column: &statepb.Column{ 1023 Name: "fifth", 1024 Started: 12345, 1025 }, 1026 Cells: map[string]updater.Cell{ 1027 "cell": {Result: tspb.TestStatus_PASS}, 1028 }, 1029 }, 1030 { 1031 Column: &statepb.Column{ 1032 Name: "fourth", 1033 Started: 1234, 1034 }, 1035 Cells: map[string]updater.Cell{ 1036 "cell": { 1037 Result: tspb.TestStatus_UNKNOWN, 1038 Icon: "...", 1039 }, 1040 }, 1041 }, 1042 }, 1043 expect: []updater.InflatedColumn{ 1044 { 1045 Column: &statepb.Column{ 1046 Name: "sixth", 1047 Started: 123456, 1048 }, 1049 Cells: map[string]updater.Cell{ 1050 "cell": {Result: tspb.TestStatus_PASS}, 1051 }, 1052 }, 1053 { 1054 Column: &statepb.Column{ 1055 Name: "fifth", 1056 Started: 12345, 1057 }, 1058 Cells: map[string]updater.Cell{ 1059 "cell": {Result: tspb.TestStatus_PASS}, 1060 }, 1061 }, 1062 { 1063 Column: &statepb.Column{ 1064 Name: "fourth", 1065 Started: 1234, 1066 }, 1067 Cells: map[string]updater.Cell{ 1068 "cell": {Result: tspb.TestStatus_PASS}, 1069 }, 1070 }, 1071 { 1072 Column: &statepb.Column{ 1073 Name: "third", 1074 Started: 123, 1075 }, 1076 Cells: map[string]updater.Cell{ 1077 "cell": {Result: tspb.TestStatus_PASS}, 1078 }, 1079 }, 1080 { 1081 Column: &statepb.Column{ 1082 Name: "second", 1083 Started: 12, 1084 }, 1085 Cells: map[string]updater.Cell{ 1086 "cell": {Result: tspb.TestStatus_PASS}, 1087 }, 1088 }, 1089 { 1090 Column: &statepb.Column{ 1091 Name: "first", 1092 Started: 1, 1093 }, 1094 Cells: map[string]updater.Cell{ 1095 "cell": { 1096 Result: tspb.TestStatus_UNKNOWN, 1097 Icon: "...", 1098 }, 1099 }, 1100 }, 1101 }, 1102 }, 1103 } 1104 1105 for _, test := range testcases { 1106 t.Run(test.name, func(t *testing.T) { 1107 actual := mergeGrids(test.current, test.add) 1108 if diff := cmp.Diff(actual, test.expect, protocmp.Transform()); diff != "" { 1109 t.Errorf("Unexpected (-got, +want): %s", diff) 1110 t.Logf("Got %#v", actual) 1111 } 1112 }) 1113 } 1114 }