github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/bisect/bisect_test.go (about) 1 // Copyright 2019 syzkaller project authors. All rights reserved. 2 // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. 3 4 package bisect 5 6 import ( 7 "errors" 8 "fmt" 9 "strconv" 10 "testing" 11 12 "github.com/google/syzkaller/pkg/build" 13 "github.com/google/syzkaller/pkg/debugtracer" 14 "github.com/google/syzkaller/pkg/hash" 15 "github.com/google/syzkaller/pkg/instance" 16 "github.com/google/syzkaller/pkg/mgrconfig" 17 "github.com/google/syzkaller/pkg/report" 18 "github.com/google/syzkaller/pkg/report/crash" 19 "github.com/google/syzkaller/pkg/vcs" 20 "github.com/google/syzkaller/sys/targets" 21 "github.com/stretchr/testify/assert" 22 ) 23 24 // testEnv will implement instance.BuilderTester. This allows us to 25 // set bisect.env.inst to a testEnv object. 26 type testEnv struct { 27 t *testing.T 28 r vcs.Repo 29 // Kernel config used in "build" 30 config string 31 test BisectionTest 32 } 33 34 func (env *testEnv) BuildSyzkaller(repo, commit string) (string, error) { 35 return "", nil 36 } 37 38 func (env *testEnv) BuildKernel(buildCfg *instance.BuildKernelConfig) (string, build.ImageDetails, error) { 39 commit := env.headCommit() 40 configHash := hash.String(buildCfg.KernelConfig) 41 details := build.ImageDetails{} 42 details.Signature = fmt.Sprintf("%v-%v", commit, configHash) 43 if commit >= env.test.sameBinaryStart && commit <= env.test.sameBinaryEnd { 44 details.Signature = "same-sign-" + configHash 45 } 46 env.config = string(buildCfg.KernelConfig) 47 if env.config == "baseline-fails" { 48 return "", details, fmt.Errorf("failure") 49 } 50 return "", details, nil 51 } 52 53 func (env *testEnv) Test(numVMs int, reproSyz, reproOpts, reproC []byte) ([]instance.EnvTestResult, error) { 54 commit := env.headCommit() 55 if commit >= env.test.brokenStart && commit <= env.test.brokenEnd || 56 env.config == "baseline-skip" { 57 var ret []instance.EnvTestResult 58 for i := 0; i < numVMs; i++ { 59 ret = append(ret, instance.EnvTestResult{ 60 Error: &instance.TestError{ 61 Boot: true, 62 Title: "kernel doesn't boot", 63 }, 64 }) 65 } 66 return ret, nil 67 } 68 if commit >= env.test.infraErrStart && commit <= env.test.infraErrEnd { 69 var ret []instance.EnvTestResult 70 for i := 0; i < numVMs; i++ { 71 var err error 72 // More than 50% failures. 73 if i*2 <= numVMs { 74 err = &instance.TestError{ 75 Infra: true, 76 Title: "failed to create a VM", 77 } 78 } 79 ret = append(ret, instance.EnvTestResult{ 80 Error: err, 81 }) 82 } 83 return ret, nil 84 } 85 var ret []instance.EnvTestResult 86 87 fixed := false 88 if env.test.fixCommit != "" { 89 commit, err := env.r.GetCommitByTitle(env.test.fixCommit) 90 if err != nil { 91 return ret, err 92 } 93 fixed = commit != nil 94 } 95 96 introduced := true 97 if env.test.introduced != "" { 98 commit, err := env.r.GetCommitByTitle(env.test.introduced) 99 if err != nil { 100 return ret, err 101 } 102 introduced = commit != nil 103 } 104 105 if (env.config == "baseline-repro" || env.config == "new-minimized-config" || env.config == "original config") && 106 introduced && !fixed { 107 if env.test.flaky { 108 ret = crashErrors(1, numVMs-1, "crash occurs", env.test.reportType) 109 } else { 110 ret = crashErrors(numVMs, 0, "crash occurs", env.test.reportType) 111 } 112 return ret, nil 113 } 114 ret = make([]instance.EnvTestResult, numVMs-1) 115 if env.test.injectSyzFailure { 116 ret = append(ret, instance.EnvTestResult{ 117 Error: &instance.TestError{ 118 Report: &report.Report{ 119 Title: "SYZFATAL: test", 120 Type: crash.SyzFailure, 121 }, 122 }, 123 }) 124 } else { 125 ret = append(ret, instance.EnvTestResult{}) 126 } 127 return ret, nil 128 } 129 130 func (env *testEnv) headCommit() int { 131 com, err := env.r.HeadCommit() 132 if err != nil { 133 env.t.Fatal(err) 134 } 135 commit, err := strconv.ParseUint(com.Title, 10, 64) 136 if err != nil { 137 env.t.Fatalf("invalid commit title: %v", com.Title) 138 } 139 return int(commit) 140 } 141 142 func createTestRepo(t *testing.T) string { 143 baseDir := t.TempDir() 144 repo := vcs.CreateTestRepo(t, baseDir, "") 145 if !repo.SupportsBisection() { 146 t.Skip("bisection is unsupported by git (probably too old version)") 147 } 148 for rv := 4; rv < 10; rv++ { 149 for i := 0; i < 6; i++ { 150 if rv == 7 && i == 0 { 151 // Create a slightly special commit graph here (for #1527): 152 // Commit 650 is part of 700 release, but it does not have 153 // 600 (the previous release) in parents, instead it's based 154 // on the previous-previous release 500. 155 repo.Git("checkout", "v5.0") 156 com := repo.CommitChange("650") 157 repo.Git("checkout", "master") 158 repo.Git("merge", "-m", "700", com.Hash) 159 } else if rv == 8 && i == 4 { 160 // Let's construct a more elaborate case. See #4117. 161 // We branch off at 700 and merge it into 804. 162 repo.Git("checkout", "v7.0") 163 repo.CommitChange("790") 164 repo.CommitChange("791") 165 com := repo.CommitChange("792") 166 repo.Git("checkout", "master") 167 repo.Git("merge", "-m", "804", com.Hash) 168 } else { 169 repo.CommitChange(fmt.Sprintf("%v", rv*100+i)) 170 } 171 if i == 0 { 172 repo.SetTag(fmt.Sprintf("v%v.0", rv)) 173 } 174 } 175 } 176 // Emulate another tree, that's needed for cross-tree tests and 177 // for cause bisections for commits not reachable from master. 178 repo.Git("checkout", "v8.0") 179 repo.Git("checkout", "-b", "v8-branch") 180 repo.CommitFileChange("850", "v8-branch") 181 repo.CommitChange("851") 182 repo.CommitChange("852") 183 return baseDir 184 } 185 186 func testBisection(t *testing.T, baseDir string, test BisectionTest) { 187 r, err := vcs.NewRepo(targets.TestOS, targets.TestArch64, baseDir, vcs.OptPrecious) 188 if err != nil { 189 t.Fatal(err) 190 } 191 if test.startCommitBranch != "" { 192 r.SwitchCommit(test.startCommitBranch) 193 } else { 194 r.SwitchCommit("master") 195 } 196 sc, err := r.GetCommitByTitle(fmt.Sprint(test.startCommit)) 197 if err != nil { 198 t.Fatal(err) 199 } 200 if sc == nil { 201 t.Fatalf("start commit %v is not found", test.startCommit) 202 } 203 r.SwitchCommit("master") 204 cfg := &Config{ 205 Fix: test.fix, 206 Trace: &debugtracer.TestTracer{T: t}, 207 Manager: &mgrconfig.Config{ 208 Derived: mgrconfig.Derived{ 209 TargetOS: targets.TestOS, 210 TargetVMArch: targets.TestArch64, 211 }, 212 Type: "qemu", 213 KernelSrc: baseDir, 214 }, 215 Kernel: KernelConfig{ 216 Repo: baseDir, 217 Branch: "master", 218 Commit: sc.Hash, 219 CommitTitle: sc.Title, 220 Config: []byte("original config"), 221 BaselineConfig: []byte(test.baselineConfig), 222 }, 223 CrossTree: test.crossTree, 224 } 225 inst := &testEnv{ 226 t: t, 227 r: r, 228 test: test, 229 } 230 231 checkBisectionError := func(test BisectionTest, res *Result, err error) { 232 if test.expectErr != (err != nil) { 233 t.Fatalf("expected error %v, got %v", test.expectErr, err) 234 } 235 if test.expectErrType != nil && !errors.As(err, &test.expectErrType) { 236 t.Fatalf("expected %#v error, got %#v", test.expectErrType, err) 237 } 238 if err != nil { 239 if res != nil { 240 t.Fatalf("got both result and error: '%v' %+v", err, *res) 241 } 242 } else { 243 checkBisectionResult(t, test, res) 244 } 245 if test.extraTest != nil { 246 test.extraTest(t, res) 247 } 248 } 249 250 res, err := runImpl(cfg, r, inst) 251 checkBisectionError(test, res, err) 252 if !test.crossTree && !test.noFakeHashTest { 253 // Should be mitigated via GetCommitByTitle during bisection. 254 cfg.Kernel.Commit = fmt.Sprintf("fake-hash-for-%v-%v", cfg.Kernel.Commit, cfg.Kernel.CommitTitle) 255 res, err = runImpl(cfg, r, inst) 256 checkBisectionError(test, res, err) 257 } 258 } 259 260 func checkBisectionResult(t *testing.T, test BisectionTest, res *Result) { 261 if len(res.Commits) != test.commitLen { 262 t.Fatalf("expected %d commits got %d commits", test.commitLen, len(res.Commits)) 263 } 264 expectedTitle := test.introduced 265 if test.fix { 266 expectedTitle = test.fixCommit 267 } 268 if len(res.Commits) == 1 && expectedTitle != res.Commits[0].Title { 269 t.Fatalf("expected commit '%v' got '%v'", expectedTitle, res.Commits[0].Title) 270 } 271 if test.expectRep != (res.Report != nil) { 272 t.Fatalf("got rep: %v, want: %v", res.Report, test.expectRep) 273 } 274 if res.NoopChange != test.noopChange { 275 t.Fatalf("got noop change: %v, want: %v", res.NoopChange, test.noopChange) 276 } 277 if res.IsRelease != test.isRelease { 278 t.Fatalf("got release change: %v, want: %v", res.IsRelease, test.isRelease) 279 } 280 if test.oldestLatest != 0 && fmt.Sprint(test.oldestLatest) != res.Commit.Title || 281 test.oldestLatest == 0 && res.Commit != nil { 282 t.Fatalf("expected latest/oldest: %v got '%v'", 283 test.oldestLatest, res.Commit.Title) 284 } 285 if test.resultingConfig != "" && test.resultingConfig != string(res.Config) { 286 t.Fatalf("expected resulting config: %q got %q", 287 test.resultingConfig, res.Config) 288 } 289 } 290 291 type BisectionTest struct { 292 // input environment 293 name string 294 fix bool 295 // By default it's set to "master". 296 startCommitBranch string 297 startCommit int 298 brokenStart int 299 brokenEnd int 300 infraErrStart int 301 infraErrEnd int 302 reportType crash.Type 303 // Range of commits that result in the same kernel binary signature. 304 sameBinaryStart int 305 sameBinaryEnd int 306 // expected output 307 expectErr bool 308 expectErrType any 309 // Expect res.Report != nil. 310 expectRep bool 311 noopChange bool 312 isRelease bool 313 flaky bool 314 injectSyzFailure bool 315 // Expected number of returned commits for inconclusive bisection. 316 commitLen int 317 // For cause bisection: Oldest commit returned by bisection. 318 // For fix bisection: Newest commit returned by bisection. 319 oldestLatest int 320 // The commit introducing the bug. 321 // If empty, the bug is assumed to exist from the beginning. 322 introduced string 323 // The commit fixing the bug. 324 // If empty, the bug is never fixed. 325 fixCommit string 326 327 baselineConfig string 328 resultingConfig string 329 crossTree bool 330 noFakeHashTest bool 331 332 extraTest func(t *testing.T, res *Result) 333 } 334 335 var bisectionTests = []BisectionTest{ 336 // Tests that bisection returns the correct cause commit. 337 { 338 name: "cause-finds-cause", 339 startCommit: 905, 340 commitLen: 1, 341 expectRep: true, 342 introduced: "602", 343 extraTest: func(t *testing.T, res *Result) { 344 assert.Greater(t, res.Confidence, 0.99) 345 }, 346 }, 347 { 348 name: "cause-finds-cause-flaky", 349 startCommit: 905, 350 commitLen: 1, 351 expectRep: true, 352 flaky: true, 353 introduced: "605", 354 extraTest: func(t *testing.T, res *Result) { 355 // False negative probability of each run is ~35%. 356 // We get three "good" results, so our accumulated confidence is ~27%. 357 assert.Less(t, res.Confidence, 0.3) 358 assert.Greater(t, res.Confidence, 0.2) 359 }, 360 }, 361 // Test bisection returns correct cause with different baseline/config combinations. 362 { 363 name: "cause-finds-cause-baseline-repro", 364 startCommit: 905, 365 commitLen: 1, 366 expectRep: true, 367 introduced: "602", 368 baselineConfig: "baseline-repro", 369 resultingConfig: "baseline-repro", 370 }, 371 { 372 name: "cause-finds-cause-baseline-does-not-repro", 373 startCommit: 905, 374 commitLen: 1, 375 expectRep: true, 376 introduced: "602", 377 baselineConfig: "baseline-not-reproducing", 378 resultingConfig: "original config", 379 }, 380 { 381 name: "cause-finds-cause-baseline-fails", 382 startCommit: 905, 383 commitLen: 1, 384 expectRep: true, 385 introduced: "602", 386 baselineConfig: "baseline-fails", 387 resultingConfig: "original config", 388 }, 389 { 390 name: "cause-finds-cause-baseline-skip", 391 startCommit: 905, 392 commitLen: 1, 393 expectRep: true, 394 introduced: "602", 395 baselineConfig: "baseline-skip", 396 resultingConfig: "original config", 397 }, 398 { 399 name: "cause-finds-cause-minimize-succeeds", 400 startCommit: 905, 401 commitLen: 1, 402 expectRep: true, 403 introduced: "602", 404 baselineConfig: "minimize-succeeds", 405 resultingConfig: "new-minimized-config", 406 }, 407 { 408 name: "cause-finds-cause-minimize-fails", 409 startCommit: 905, 410 baselineConfig: "minimize-fails", 411 expectErr: true, 412 }, 413 { 414 name: "config-minimize-same-hash", 415 startCommit: 905, 416 commitLen: 1, 417 expectRep: true, 418 introduced: "905", 419 sameBinaryStart: 904, 420 sameBinaryEnd: 905, 421 noopChange: true, 422 baselineConfig: "minimize-succeeds", 423 resultingConfig: "new-minimized-config", 424 }, 425 // Tests that cause bisection returns error when crash does not reproduce 426 // on the original commit. 427 { 428 name: "cause-does-not-repro", 429 startCommit: 400, 430 expectErr: true, 431 }, 432 // Tests that no commits are returned when crash occurs on oldest commit 433 // for cause bisection. 434 { 435 name: "cause-crashes-oldest", 436 startCommit: 905, 437 commitLen: 0, 438 expectRep: true, 439 oldestLatest: 400, 440 }, 441 // Tests that more than 1 commit is returned when cause bisection is inconclusive. 442 { 443 name: "cause-inconclusive", 444 startCommit: 802, 445 brokenStart: 500, 446 brokenEnd: 700, 447 commitLen: 15, 448 introduced: "605", 449 }, 450 // All releases are build broken. 451 { 452 name: "all-releases-broken", 453 startCommit: 802, 454 brokenStart: 100, 455 brokenEnd: 800, 456 // We mark these as failed, because build/boot failures of ancient releases are unlikely to get fixed 457 // without manual intervention by syz-ci admins. 458 commitLen: 0, 459 expectRep: false, 460 expectErr: true, 461 }, 462 // Tests that bisection returns the correct fix commit. 463 { 464 name: "fix-finds-fix", 465 fix: true, 466 startCommit: 400, 467 commitLen: 1, 468 fixCommit: "500", 469 isRelease: true, 470 }, 471 // Tests that we do not confuse revisions where the bug was not yet introduced and where it's fixed. 472 // In this case, we have a 700-790-791-792-804 branch, which will be visited during bisection. 473 // As the faulty commit 704 is not reachable from there, kernel wouldn't crash and, without the 474 // special care, we'd incorrectly designate "790" as the fix commit. 475 // See #4117. 476 { 477 name: "fix-after-bug", 478 fix: true, 479 startCommit: 802, 480 commitLen: 1, 481 fixCommit: "803", 482 introduced: "704", 483 }, 484 // Tests that bisection returns the correct fix commit despite SYZFATAL. 485 { 486 name: "fix-finds-fix-despite-syzfatal", 487 fix: true, 488 startCommit: 400, 489 injectSyzFailure: true, 490 commitLen: 1, 491 fixCommit: "500", 492 isRelease: true, 493 }, 494 // Tests that bisection returns the correct fix commit in case of SYZFATAL. 495 { 496 name: "fix-finds-fix-for-syzfatal", 497 fix: true, 498 startCommit: 400, 499 reportType: crash.SyzFailure, 500 commitLen: 1, 501 fixCommit: "500", 502 isRelease: true, 503 }, 504 // Tests that fix bisection returns error when crash does not reproduce 505 // on the original commit. 506 { 507 name: "fix-does-not-repro", 508 fix: true, 509 startCommit: 905, 510 expectErr: true, 511 fixCommit: "900", 512 }, 513 // Tests that no commits are returned when HEAD is build broken. 514 // Fix bisection equivalent of all-releases-broken. 515 { 516 name: "fix-HEAD-broken", 517 fix: true, 518 startCommit: 400, 519 brokenStart: 500, 520 brokenEnd: 1000, 521 fixCommit: "1000", 522 oldestLatest: 905, 523 // We mark these as re-tryable, because build/boot failures of HEAD will also be caught during regular fuzzing 524 // and are fixed by kernel devs or syz-ci admins in a timely manner. 525 commitLen: 0, 526 expectRep: true, 527 expectErr: false, 528 }, 529 // Tests that no commits are returned when crash occurs on HEAD 530 // for fix bisection. 531 { 532 name: "fix-HEAD-crashes", 533 fix: true, 534 startCommit: 400, 535 fixCommit: "1000", 536 oldestLatest: 905, 537 commitLen: 0, 538 expectRep: true, 539 expectErr: false, 540 }, 541 // Tests that more than 1 commit is returned when fix bisection is inconclusive. 542 { 543 name: "fix-inconclusive", 544 fix: true, 545 startCommit: 500, 546 brokenStart: 600, 547 brokenEnd: 700, 548 commitLen: 9, 549 fixCommit: "601", 550 }, 551 { 552 name: "cause-same-binary", 553 startCommit: 905, 554 commitLen: 1, 555 expectRep: true, 556 introduced: "503", 557 sameBinaryStart: 502, 558 sameBinaryEnd: 503, 559 noopChange: true, 560 }, 561 { 562 name: "cause-same-binary-off-by-one", 563 startCommit: 905, 564 commitLen: 1, 565 expectRep: true, 566 introduced: "503", 567 sameBinaryStart: 400, 568 sameBinaryEnd: 502, 569 }, 570 { 571 name: "cause-same-binary-off-by-one-2", 572 startCommit: 905, 573 commitLen: 1, 574 expectRep: true, 575 introduced: "503", 576 sameBinaryStart: 503, 577 sameBinaryEnd: 905, 578 }, 579 { 580 name: "fix-same-binary", 581 fix: true, 582 startCommit: 400, 583 commitLen: 1, 584 fixCommit: "503", 585 sameBinaryStart: 502, 586 sameBinaryEnd: 504, 587 noopChange: true, 588 }, 589 { 590 name: "cause-same-binary-release1", 591 startCommit: 905, 592 commitLen: 1, 593 expectRep: true, 594 introduced: "500", 595 sameBinaryStart: 405, 596 sameBinaryEnd: 500, 597 noopChange: true, 598 isRelease: true, 599 }, 600 { 601 name: "cause-same-binary-release2", 602 startCommit: 905, 603 commitLen: 1, 604 expectRep: true, 605 introduced: "501", 606 sameBinaryStart: 500, 607 sameBinaryEnd: 501, 608 noopChange: true, 609 }, 610 { 611 name: "cause-same-binary-release3", 612 startCommit: 905, 613 commitLen: 1, 614 expectRep: true, 615 introduced: "405", 616 sameBinaryStart: 404, 617 sameBinaryEnd: 405, 618 noopChange: true, 619 }, 620 { 621 name: "fix-same-binary-last", 622 fix: true, 623 startCommit: 400, 624 commitLen: 1, 625 fixCommit: "905", 626 sameBinaryStart: 904, 627 sameBinaryEnd: 905, 628 noopChange: true, 629 }, 630 { 631 name: "fix-release", 632 fix: true, 633 startCommit: 400, 634 commitLen: 1, 635 fixCommit: "900", 636 isRelease: true, 637 }, 638 { 639 name: "cause-not-in-previous-release-issue-1527", 640 startCommit: 905, 641 introduced: "650", 642 commitLen: 1, 643 expectRep: true, 644 sameBinaryStart: 500, 645 sameBinaryEnd: 650, 646 noopChange: true, 647 }, 648 { 649 name: "cause-infra-problems", 650 startCommit: 905, 651 expectRep: false, 652 expectErr: true, 653 expectErrType: &InfraError{}, 654 infraErrStart: 600, 655 infraErrEnd: 800, 656 introduced: "602", 657 }, 658 { 659 name: "fix-cross-tree", 660 fix: true, 661 startCommit: 851, 662 startCommitBranch: "v8-branch", 663 commitLen: 1, 664 crossTree: true, 665 fixCommit: "903", 666 }, 667 { 668 name: "cause-finds-other-branch-commit", 669 startCommit: 852, 670 startCommitBranch: "v8-branch", 671 commitLen: 1, 672 expectRep: true, 673 introduced: "602", 674 noFakeHashTest: true, 675 }, 676 { 677 // There's no fix for the bug because it was introduced 678 // in another tree. 679 name: "no-fix-cross-tree", 680 fix: true, 681 startCommit: 852, 682 startCommitBranch: "v8-branch", 683 commitLen: 0, 684 crossTree: true, 685 introduced: "851", 686 oldestLatest: 800, 687 }, 688 { 689 // We are unable to test the merge base commit. 690 name: "fix-cross-tree-broken-start", 691 fix: true, 692 startCommit: 851, 693 startCommitBranch: "v8-branch", 694 commitLen: 0, 695 crossTree: true, 696 fixCommit: "903", 697 brokenStart: 800, 698 brokenEnd: 800, 699 oldestLatest: 800, 700 }, 701 } 702 703 func TestBisectionResults(t *testing.T) { 704 t.Parallel() 705 // Creating new repos takes majority of the test time, 706 // so we reuse them across tests. 707 repoCache := make(chan string, len(bisectionTests)) 708 t.Run("group", func(tt *testing.T) { 709 for _, test := range bisectionTests { 710 test := test 711 tt.Run(test.name, func(t *testing.T) { 712 t.Parallel() 713 checkTest(t, test) 714 repoDir := "" 715 select { 716 case repoDir = <-repoCache: 717 default: 718 repoDir = createTestRepo(tt) 719 } 720 defer func() { 721 repoCache <- repoDir 722 }() 723 testBisection(t, repoDir, test) 724 }) 725 } 726 }) 727 } 728 729 func checkTest(t *testing.T, test BisectionTest) { 730 if test.expectErr && 731 (test.commitLen != 0 || 732 test.expectRep || 733 test.oldestLatest != 0 || 734 test.resultingConfig != "") { 735 t.Fatalf("expecting non-default values on error") 736 } 737 if !test.expectErr && test.baselineConfig != "" && test.resultingConfig == "" { 738 t.Fatalf("specify resultingConfig with baselineConfig") 739 } 740 if test.brokenStart > test.brokenEnd { 741 t.Fatalf("bad broken start/end: %v/%v", 742 test.brokenStart, test.brokenEnd) 743 } 744 if test.sameBinaryStart > test.sameBinaryEnd { 745 t.Fatalf("bad same binary start/end: %v/%v", 746 test.sameBinaryStart, test.sameBinaryEnd) 747 } 748 } 749 750 func crashErrors(crashing, nonCrashing int, title string, typ crash.Type) []instance.EnvTestResult { 751 var ret []instance.EnvTestResult 752 for i := 0; i < crashing; i++ { 753 ret = append(ret, instance.EnvTestResult{ 754 Error: &instance.CrashError{ 755 Report: &report.Report{ 756 Title: fmt.Sprintf("crashes at %v", title), 757 Type: typ, 758 }, 759 }, 760 }) 761 } 762 for i := 0; i < nonCrashing; i++ { 763 ret = append(ret, instance.EnvTestResult{}) 764 } 765 return ret 766 } 767 768 func TestBisectVerdict(t *testing.T) { 769 t.Parallel() 770 tests := []struct { 771 name string 772 flaky bool 773 total int 774 good int 775 bad int 776 infra int 777 skip int 778 verdict vcs.BisectResult 779 abort bool 780 }{ 781 { 782 name: "bad-but-many-infra", 783 total: 10, 784 bad: 1, 785 infra: 8, 786 skip: 1, 787 abort: true, 788 }, 789 { 790 name: "many-good-and-infra", 791 total: 10, 792 good: 5, 793 infra: 3, 794 skip: 2, 795 verdict: vcs.BisectGood, 796 }, 797 { 798 name: "many-total-and-infra", 799 total: 10, 800 good: 5, 801 bad: 1, 802 infra: 2, 803 skip: 2, 804 verdict: vcs.BisectBad, 805 }, 806 { 807 name: "too-many-skips", 808 total: 10, 809 good: 2, 810 bad: 2, 811 infra: 3, 812 skip: 3, 813 verdict: vcs.BisectSkip, 814 }, 815 { 816 name: "flaky-need-more-good", 817 flaky: true, 818 total: 20, 819 // For flaky bisections, we'd want 15. 820 good: 10, 821 infra: 3, 822 skip: 7, 823 verdict: vcs.BisectSkip, 824 }, 825 { 826 name: "flaky-enough-good", 827 flaky: true, 828 total: 20, 829 good: 15, 830 infra: 3, 831 skip: 2, 832 verdict: vcs.BisectGood, 833 }, 834 { 835 name: "flaky-too-many-skips", 836 flaky: true, 837 total: 20, 838 // We want (good+bad) take at least 50%. 839 good: 6, 840 bad: 1, 841 infra: 0, 842 skip: 13, 843 verdict: vcs.BisectSkip, 844 }, 845 { 846 name: "flaky-many-skips", 847 flaky: true, 848 total: 20, 849 good: 9, 850 bad: 1, 851 infra: 0, 852 skip: 10, 853 verdict: vcs.BisectBad, 854 }, 855 } 856 857 for _, test := range tests { 858 test := test 859 t.Run(test.name, func(t *testing.T) { 860 sum := test.good + test.bad + test.infra + test.skip 861 assert.Equal(t, test.total, sum) 862 env := &env{ 863 cfg: &Config{ 864 Trace: &debugtracer.NullTracer{}, 865 }, 866 flaky: test.flaky, 867 } 868 ret, err := env.bisectionDecision(test.total, test.bad, test.good, test.infra) 869 assert.Equal(t, test.abort, err != nil) 870 if !test.abort { 871 assert.Equal(t, test.verdict, ret) 872 } 873 }) 874 } 875 } 876 877 // nolint: dupl 878 func TestMostFrequentReport(t *testing.T) { 879 tests := []struct { 880 name string 881 reports []*report.Report 882 report string 883 types []crash.Type 884 other bool 885 }{ 886 { 887 name: "one infrequent", 888 reports: []*report.Report{ 889 {Title: "A", Type: crash.KASAN}, 890 {Title: "B", Type: crash.KASAN}, 891 {Title: "C", Type: crash.Bug}, 892 {Title: "D", Type: crash.KASAN}, 893 {Title: "E", Type: crash.Bug}, 894 {Title: "F", Type: crash.KASAN}, 895 {Title: "G", Type: crash.LockdepBug}, 896 }, 897 // LockdepBug was too infrequent. 898 types: []crash.Type{crash.KASAN, crash.Bug}, 899 report: "A", 900 other: true, 901 }, 902 { 903 name: "ignore hangs", 904 reports: []*report.Report{ 905 {Title: "A", Type: crash.KASAN}, 906 {Title: "B", Type: crash.KASAN}, 907 {Title: "C", Type: crash.Hang}, 908 {Title: "D", Type: crash.KASAN}, 909 {Title: "E", Type: crash.Hang}, 910 {Title: "F", Type: crash.Hang}, 911 {Title: "G", Type: crash.Warning}, 912 }, 913 // Hang is not a preferred report type. 914 types: []crash.Type{crash.KASAN, crash.Warning}, 915 report: "A", 916 other: true, 917 }, 918 { 919 name: "take hangs", 920 reports: []*report.Report{ 921 {Title: "A", Type: crash.KASAN}, 922 {Title: "B", Type: crash.KASAN}, 923 {Title: "C", Type: crash.Hang}, 924 {Title: "D", Type: crash.Hang}, 925 {Title: "E", Type: crash.Hang}, 926 {Title: "F", Type: crash.Hang}, 927 }, 928 // There are so many Hangs that we can't ignore it. 929 types: []crash.Type{crash.Hang, crash.KASAN}, 930 report: "C", 931 }, 932 { 933 name: "take unknown", 934 reports: []*report.Report{ 935 {Title: "A", Type: crash.UnknownType}, 936 {Title: "B", Type: crash.UnknownType}, 937 {Title: "C", Type: crash.Hang}, 938 {Title: "D", Type: crash.UnknownType}, 939 {Title: "E", Type: crash.Hang}, 940 {Title: "F", Type: crash.UnknownType}, 941 }, 942 // UnknownType is also a type. 943 types: []crash.Type{crash.UnknownType}, 944 report: "A", 945 other: true, 946 }, 947 } 948 for _, test := range tests { 949 test := test 950 t.Run(test.name, func(t *testing.T) { 951 rep, types, other := mostFrequentReports(test.reports) 952 assert.ElementsMatch(t, types, test.types) 953 assert.Equal(t, rep.Title, test.report) 954 assert.Equal(t, other, test.other) 955 }) 956 } 957 } 958 959 func TestPickReleaseTags(t *testing.T) { 960 tests := []struct { 961 name string 962 tags []string 963 ret []string 964 }{ 965 { 966 name: "upstream-clang", 967 tags: []string{ 968 "v6.5", "v6.4", "v6.3", "v6.2", "v6.1", "v6.0", "v5.19", 969 "v5.18", "v5.17", "v5.16", "v5.15", "v5.14", "v5.13", 970 "v5.12", "v5.11", "v5.10", "v5.9", "v5.8", "v5.7", "v5.6", 971 "v5.5", "v5.4", 972 }, 973 ret: []string{ 974 "v6.5", "v6.4", "v6.3", "v6.1", "v5.19", "v5.17", "v5.15", 975 "v5.13", "v5.10", "v5.7", "v5.4", 976 }, 977 }, 978 { 979 name: "upstream-gcc", 980 tags: []string{ 981 "v6.5", "v6.4", "v6.3", "v6.2", "v6.1", "v6.0", "v5.19", 982 "v5.18", "v5.17", "v5.16", "v5.15", "v5.14", "v5.13", 983 "v5.12", "v5.11", "v5.10", "v5.9", "v5.8", "v5.7", "v5.6", 984 "v5.5", "v5.4", "v5.3", "v5.2", "v5.1", "v5.0", "v4.20", "v4.19", 985 "v4.18", 986 }, 987 ret: []string{ 988 "v6.5", "v6.4", "v6.3", "v6.1", "v5.19", "v5.17", "v5.15", 989 "v5.13", "v5.10", "v5.7", "v5.4", "v5.1", "v4.19", "v4.18", 990 }, 991 }, 992 { 993 name: "lts", 994 tags: []string{ 995 "v5.15.10", "v5.15.9", "v5.15.8", "v5.15.7", "v5.15.6", 996 "v5.15.5", "v5.15.4", "v5.15.3", "v5.15.2", "v5.15.1", 997 "v5.15", "v5.14", "v5.13", "v5.12", "v5.11", "v5.10", 998 "v5.9", "v5.8", "v5.7", "v5.6", "v5.5", "v5.4", 999 }, 1000 ret: []string{ 1001 "v5.15.10", "v5.15.9", "v5.15.5", "v5.15", "v5.14", "v5.13", 1002 "v5.11", "v5.9", "v5.7", "v5.5", "v5.4", 1003 }, 1004 }, 1005 } 1006 for _, test := range tests { 1007 test := test 1008 t.Run(test.name, func(t *testing.T) { 1009 ret := pickReleaseTags(append([]string{}, test.tags...)) 1010 assert.Equal(t, test.ret, ret) 1011 }) 1012 } 1013 }