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