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  }