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  }