github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/reporting_test.go (about)

     1  // Copyright 2017 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 main
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"html"
    10  	"reflect"
    11  	"regexp"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/google/syzkaller/dashboard/dashapi"
    17  	"github.com/google/syzkaller/pkg/email"
    18  	"github.com/google/syzkaller/sys/targets"
    19  	"github.com/stretchr/testify/assert"
    20  )
    21  
    22  func TestReportBug(t *testing.T) {
    23  	c := NewCtx(t)
    24  	defer c.Close()
    25  
    26  	build := testBuild(1)
    27  	c.client.UploadBuild(build)
    28  
    29  	crash1 := &dashapi.Crash{
    30  		BuildID:     "build1",
    31  		Title:       "title1",
    32  		Maintainers: []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`},
    33  		Log:         []byte("log1"),
    34  		Flags:       dashapi.CrashUnderStrace,
    35  		Report:      []byte("report1"),
    36  		MachineInfo: []byte("machine info 1"),
    37  		GuiltyFiles: []string{"a.c"},
    38  	}
    39  	c.client.ReportCrash(crash1)
    40  
    41  	// Must get no reports for "unknown" type.
    42  	resp, _ := c.client.ReportingPollBugs("unknown")
    43  	c.expectEQ(len(resp.Reports), 0)
    44  
    45  	// Must get a proper report for "test" type.
    46  	resp, _ = c.client.ReportingPollBugs("test")
    47  	c.expectEQ(len(resp.Reports), 1)
    48  	rep := resp.Reports[0]
    49  	c.expectNE(rep.ID, "")
    50  	_, dbCrash, dbBuild := c.loadBug(rep.ID)
    51  	want := &dashapi.BugReport{
    52  		Type:              dashapi.ReportNew,
    53  		BugStatus:         dashapi.BugStatusOpen,
    54  		Namespace:         "test1",
    55  		Config:            []byte(`{"Index":1}`),
    56  		ID:                rep.ID,
    57  		OS:                targets.Linux,
    58  		Arch:              targets.AMD64,
    59  		VMArch:            targets.AMD64,
    60  		UserSpaceArch:     targets.AMD64,
    61  		First:             true,
    62  		Moderation:        true,
    63  		Title:             "title1",
    64  		Link:              fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep.ID),
    65  		CreditEmail:       fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep.ID),
    66  		Maintainers:       []string{"bar@foo.com", "foo@bar.com", "subsystemA@list.com", "subsystemA@person.com"},
    67  		CompilerID:        "compiler1",
    68  		BuildID:           "build1",
    69  		BuildTime:         timeNow(c.ctx),
    70  		KernelRepo:        "repo1",
    71  		KernelRepoAlias:   "repo1 branch1",
    72  		KernelBranch:      "branch1",
    73  		KernelCommit:      "1111111111111111111111111111111111111111",
    74  		KernelCommitTitle: build.KernelCommitTitle,
    75  		KernelCommitDate:  buildCommitDate,
    76  		KernelConfig:      []byte("config1"),
    77  		KernelConfigLink:  externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig),
    78  		SyzkallerCommit:   "syzkaller_commit1",
    79  		MachineInfo:       []byte("machine info 1"),
    80  		MachineInfoLink:   externalLink(c.ctx, textMachineInfo, dbCrash.MachineInfo),
    81  		Log:               []byte("log1"),
    82  		LogLink:           externalLink(c.ctx, textCrashLog, dbCrash.Log),
    83  		LogHasStrace:      true,
    84  		Report:            []byte("report1"),
    85  		ReportLink:        externalLink(c.ctx, textCrashReport, dbCrash.Report),
    86  		ReproOpts:         []uint8{},
    87  		CrashID:           rep.CrashID,
    88  		CrashTime:         timeNow(c.ctx),
    89  		NumCrashes:        1,
    90  		Manager:           "manager1",
    91  		HappenedOn:        []string{"repo1 branch1"},
    92  		Assets:            []dashapi.Asset{},
    93  		ReportElements:    &dashapi.ReportElements{GuiltyFiles: []string{"a.c"}},
    94  		Subsystems: []dashapi.BugSubsystem{
    95  			{
    96  				Name: "subsystemA",
    97  				Link: "https://testapp.appspot.com/test1/s/subsystemA",
    98  			},
    99  		},
   100  	}
   101  	c.expectEQ(want, rep)
   102  
   103  	// Since we did not update bug status yet, should get the same report again.
   104  	c.expectEQ(c.client.pollBug(), want)
   105  
   106  	// Now add syz repro and check that we get another bug report.
   107  	crash1.ReproOpts = []byte("some opts")
   108  	crash1.ReproSyz = []byte("getpid()")
   109  	want.Type = dashapi.ReportRepro
   110  	want.First = false
   111  	want.ReproSyz = []byte(syzReproPrefix + "#some opts\ngetpid()")
   112  	want.ReproOpts = []byte("some opts")
   113  	c.client.ReportCrash(crash1)
   114  	rep1 := c.client.pollBug()
   115  	c.expectNE(want.CrashID, rep1.CrashID)
   116  	_, dbCrash, _ = c.loadBug(rep.ID)
   117  	want.CrashID = rep1.CrashID
   118  	want.NumCrashes = 2
   119  	want.ReproSyzLink = externalLink(c.ctx, textReproSyz, dbCrash.ReproSyz)
   120  	want.LogLink = externalLink(c.ctx, textCrashLog, dbCrash.Log)
   121  	want.ReportLink = externalLink(c.ctx, textCrashReport, dbCrash.Report)
   122  	c.expectEQ(want, rep1)
   123  
   124  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   125  		ID:         rep.ID,
   126  		Status:     dashapi.BugStatusOpen,
   127  		ReproLevel: dashapi.ReproLevelSyz,
   128  	})
   129  	c.expectEQ(reply.OK, true)
   130  
   131  	// After bug update should not get the report again.
   132  	c.client.pollBugs(0)
   133  
   134  	// Now close the bug in the first reporting.
   135  	c.client.updateBug(rep.ID, dashapi.BugStatusUpstream, "")
   136  
   137  	// Check that bug updates for the first reporting fail now.
   138  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{ID: rep.ID, Status: dashapi.BugStatusOpen})
   139  	c.expectEQ(reply.OK, false)
   140  
   141  	// Report another crash with syz repro for this bug,
   142  	// ensure that we report the new crash in the next reporting.
   143  	crash1.Report = []byte("report2")
   144  	c.client.ReportCrash(crash1)
   145  
   146  	// Check that we get the report in the second reporting.
   147  	rep2 := c.client.pollBug()
   148  	c.expectNE(rep2.ID, "")
   149  	c.expectNE(rep2.ID, rep.ID)
   150  	want.Type = dashapi.ReportNew
   151  	want.ID = rep2.ID
   152  	want.Report = []byte("report2")
   153  	want.LogLink = rep2.LogLink
   154  	want.ReportLink = rep2.ReportLink
   155  	want.CrashID = rep2.CrashID
   156  	want.ReproSyzLink = rep2.ReproSyzLink
   157  	want.ReproOpts = []byte("some opts")
   158  	want.Link = fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep2.ID)
   159  	want.CreditEmail = fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep2.ID)
   160  	want.First = true
   161  	want.Moderation = false
   162  	want.Config = []byte(`{"Index":2}`)
   163  	want.NumCrashes = 3
   164  	c.expectEQ(want, rep2)
   165  
   166  	// Check that that we can't upstream the bug in the final reporting.
   167  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   168  		ID:     rep2.ID,
   169  		Status: dashapi.BugStatusUpstream,
   170  	})
   171  	c.expectEQ(reply.OK, false)
   172  }
   173  
   174  func TestInvalidBug(t *testing.T) {
   175  	c := NewCtx(t)
   176  	defer c.Close()
   177  
   178  	build := testBuild(1)
   179  	c.client.UploadBuild(build)
   180  
   181  	crash1 := testCrashWithRepro(build, 1)
   182  	c.client.ReportCrash(crash1)
   183  
   184  	rep := c.client.pollBug()
   185  	c.expectEQ(rep.Title, "title1")
   186  
   187  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   188  		ID:         rep.ID,
   189  		Status:     dashapi.BugStatusOpen,
   190  		ReproLevel: dashapi.ReproLevelC,
   191  	})
   192  	c.expectEQ(reply.OK, true)
   193  
   194  	{
   195  		closed, _ := c.client.ReportingPollClosed([]string{rep.ID, "foobar"})
   196  		c.expectEQ(len(closed), 0)
   197  	}
   198  
   199  	// Mark the bug as invalid.
   200  	c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "")
   201  
   202  	{
   203  		closed, _ := c.client.ReportingPollClosed([]string{rep.ID, "foobar"})
   204  		c.expectEQ(len(closed), 1)
   205  		c.expectEQ(closed[0], rep.ID)
   206  	}
   207  
   208  	// Now it should not be reported in either reporting.
   209  	c.client.pollBugs(0)
   210  
   211  	// Now a similar crash happens again.
   212  	crash2 := &dashapi.Crash{
   213  		BuildID: "build1",
   214  		Title:   "title1",
   215  		Log:     []byte("log2"),
   216  		Report:  []byte("report2"),
   217  		ReproC:  []byte("int main() { return 1; }"),
   218  	}
   219  	c.client.ReportCrash(crash2)
   220  
   221  	// Now it should be reported again.
   222  	rep = c.client.pollBug()
   223  	c.expectNE(rep.ID, "")
   224  	_, dbCrash, dbBuild := c.loadBug(rep.ID)
   225  	want := &dashapi.BugReport{
   226  		Type:              dashapi.ReportNew,
   227  		BugStatus:         dashapi.BugStatusOpen,
   228  		Namespace:         "test1",
   229  		Config:            []byte(`{"Index":1}`),
   230  		ID:                rep.ID,
   231  		OS:                targets.Linux,
   232  		Arch:              targets.AMD64,
   233  		VMArch:            targets.AMD64,
   234  		UserSpaceArch:     targets.AMD64,
   235  		First:             true,
   236  		Moderation:        true,
   237  		Title:             "title1 (2)",
   238  		Link:              fmt.Sprintf("https://testapp.appspot.com/bug?extid=%v", rep.ID),
   239  		CreditEmail:       fmt.Sprintf("syzbot+%v@testapp.appspotmail.com", rep.ID),
   240  		BuildID:           "build1",
   241  		BuildTime:         timeNow(c.ctx),
   242  		CompilerID:        "compiler1",
   243  		KernelRepo:        "repo1",
   244  		KernelRepoAlias:   "repo1 branch1",
   245  		KernelBranch:      "branch1",
   246  		KernelCommit:      "1111111111111111111111111111111111111111",
   247  		KernelCommitTitle: build.KernelCommitTitle,
   248  		KernelCommitDate:  buildCommitDate,
   249  		KernelConfig:      []byte("config1"),
   250  		KernelConfigLink:  externalLink(c.ctx, textKernelConfig, dbBuild.KernelConfig),
   251  		SyzkallerCommit:   "syzkaller_commit1",
   252  		Log:               []byte("log2"),
   253  		LogLink:           externalLink(c.ctx, textCrashLog, dbCrash.Log),
   254  		Report:            []byte("report2"),
   255  		ReportLink:        externalLink(c.ctx, textCrashReport, dbCrash.Report),
   256  		ReproC:            []byte("int main() { return 1; }"),
   257  		ReproCLink:        externalLink(c.ctx, textReproC, dbCrash.ReproC),
   258  		ReproOpts:         []uint8{},
   259  		CrashID:           rep.CrashID,
   260  		CrashTime:         timeNow(c.ctx),
   261  		NumCrashes:        1,
   262  		Manager:           "manager1",
   263  		HappenedOn:        []string{"repo1 branch1"},
   264  		Assets:            []dashapi.Asset{},
   265  		ReportElements:    &dashapi.ReportElements{},
   266  	}
   267  	c.expectEQ(want, rep)
   268  	c.client.ReportFailedRepro(testCrashID(crash1))
   269  }
   270  
   271  func TestReportingQuota(t *testing.T) {
   272  	c := NewCtx(t)
   273  	defer c.Close()
   274  
   275  	c.updateReporting("test1", "reporting1",
   276  		func(r Reporting) Reporting {
   277  			// Set a low daily limit.
   278  			r.DailyLimit = 2
   279  			return r
   280  		})
   281  
   282  	build := testBuild(1)
   283  	c.client.UploadBuild(build)
   284  
   285  	const numReports = 5
   286  	for i := 0; i < numReports; i++ {
   287  		c.client.ReportCrash(testCrash(build, i))
   288  	}
   289  
   290  	for _, reports := range []int{2, 2, 1, 0, 0} {
   291  		c.advanceTime(24 * time.Hour)
   292  		c.client.pollBugs(reports)
   293  		// Out of quota for today, so must get 0 reports.
   294  		c.client.pollBugs(0)
   295  	}
   296  }
   297  
   298  func TestReproReportingQuota(t *testing.T) {
   299  	// Test that new repro reports are also covered by daily limits.
   300  	c := NewCtx(t)
   301  	defer c.Close()
   302  
   303  	client := c.client
   304  	c.updateReporting("test1", "reporting1",
   305  		func(r Reporting) Reporting {
   306  			// Set a low daily limit.
   307  			r.DailyLimit = 2
   308  			return r
   309  		})
   310  
   311  	build := testBuild(1)
   312  	client.UploadBuild(build)
   313  
   314  	// First report of two.
   315  	c.advanceTime(time.Minute)
   316  	client.ReportCrash(testCrash(build, 1))
   317  	client.pollBug()
   318  
   319  	// Second report of two.
   320  	c.advanceTime(time.Minute)
   321  	crash := testCrash(build, 2)
   322  	client.ReportCrash(crash)
   323  	client.pollBug()
   324  
   325  	// Now we "find" a reproducer.
   326  	c.advanceTime(time.Minute)
   327  	client.ReportCrash(testCrashWithRepro(build, 1))
   328  
   329  	// But there's no quota for it.
   330  	client.pollBugs(0)
   331  
   332  	// Wait a day and the quota appears.
   333  	c.advanceTime(time.Hour * 24)
   334  	client.pollBug()
   335  }
   336  
   337  // Basic dup scenario: mark one bug as dup of another.
   338  func TestReportingDup(t *testing.T) {
   339  	c := NewCtx(t)
   340  	defer c.Close()
   341  
   342  	build := testBuild(1)
   343  	c.client.UploadBuild(build)
   344  
   345  	crash1 := testCrash(build, 1)
   346  	c.client.ReportCrash(crash1)
   347  
   348  	crash2 := testCrash(build, 2)
   349  	c.client.ReportCrash(crash2)
   350  
   351  	reports := c.client.pollBugs(2)
   352  	rep1 := reports[0]
   353  	rep2 := reports[1]
   354  
   355  	// Dup.
   356  	c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
   357  	{
   358  		// Both must be reported as open.
   359  		closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID})
   360  		c.expectEQ(len(closed), 0)
   361  	}
   362  
   363  	// Undup.
   364  	c.client.updateBug(rep2.ID, dashapi.BugStatusOpen, "")
   365  
   366  	// Dup again.
   367  	c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
   368  
   369  	// Dup crash happens again, new bug must not be created.
   370  	c.client.ReportCrash(crash2)
   371  	c.client.pollBugs(0)
   372  
   373  	// Now close the original bug, and check that new bugs for dup are now created.
   374  	c.client.updateBug(rep1.ID, dashapi.BugStatusInvalid, "")
   375  	{
   376  		// Now both must be reported as closed.
   377  		closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID})
   378  		c.expectEQ(len(closed), 2)
   379  		c.expectEQ(closed[0], rep1.ID)
   380  		c.expectEQ(closed[1], rep2.ID)
   381  	}
   382  
   383  	c.client.ReportCrash(crash2)
   384  	rep3 := c.client.pollBug()
   385  	c.expectEQ(rep3.Title, crash2.Title+" (2)")
   386  
   387  	// Unduping after the canonical bugs was closed must not work
   388  	// (we already created new bug for this report).
   389  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   390  		ID:     rep2.ID,
   391  		Status: dashapi.BugStatusOpen,
   392  	})
   393  	c.expectEQ(reply.OK, false)
   394  }
   395  
   396  // Dup bug onto a closed bug.
   397  // A new crash report must create a new bug.
   398  func TestReportingDupToClosed(t *testing.T) {
   399  	c := NewCtx(t)
   400  	defer c.Close()
   401  
   402  	build := testBuild(1)
   403  	c.client.UploadBuild(build)
   404  
   405  	crash1 := testCrash(build, 1)
   406  	c.client.ReportCrash(crash1)
   407  
   408  	crash2 := testCrash(build, 2)
   409  	c.client.ReportCrash(crash2)
   410  
   411  	reports := c.client.pollBugs(2)
   412  	c.client.updateBug(reports[0].ID, dashapi.BugStatusInvalid, "")
   413  	c.client.updateBug(reports[1].ID, dashapi.BugStatusDup, reports[0].ID)
   414  
   415  	c.client.ReportCrash(crash2)
   416  	rep2 := c.client.pollBug()
   417  	c.expectEQ(rep2.Title, crash2.Title+" (2)")
   418  }
   419  
   420  // Test that marking dups across reporting levels is not permitted.
   421  func TestReportingDupCrossReporting(t *testing.T) {
   422  	c := NewCtx(t)
   423  	defer c.Close()
   424  
   425  	build := testBuild(1)
   426  	c.client.UploadBuild(build)
   427  
   428  	crash1 := testCrash(build, 1)
   429  	c.client.ReportCrash(crash1)
   430  
   431  	crash2 := testCrash(build, 2)
   432  	c.client.ReportCrash(crash2)
   433  
   434  	reports := c.client.pollBugs(2)
   435  	rep1 := reports[0]
   436  	rep2 := reports[1]
   437  
   438  	// Upstream second bug.
   439  	c.client.updateBug(rep2.ID, dashapi.BugStatusUpstream, "")
   440  	rep3 := c.client.pollBug()
   441  
   442  	{
   443  		closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID, rep3.ID})
   444  		c.expectEQ(len(closed), 1)
   445  		c.expectEQ(closed[0], rep2.ID)
   446  	}
   447  
   448  	// Duping must fail all ways.
   449  	cmds := []*dashapi.BugUpdate{
   450  		{ID: rep1.ID, DupOf: rep1.ID},
   451  		{ID: rep1.ID, DupOf: rep2.ID},
   452  		{ID: rep2.ID, DupOf: rep1.ID},
   453  		{ID: rep2.ID, DupOf: rep2.ID},
   454  		{ID: rep2.ID, DupOf: rep3.ID},
   455  		{ID: rep3.ID, DupOf: rep1.ID},
   456  		{ID: rep3.ID, DupOf: rep2.ID},
   457  		{ID: rep3.ID, DupOf: rep3.ID},
   458  	}
   459  	for _, cmd := range cmds {
   460  		t.Logf("duping %v -> %v", cmd.ID, cmd.DupOf)
   461  		cmd.Status = dashapi.BugStatusDup
   462  		reply, _ := c.client.ReportingUpdate(cmd)
   463  		c.expectEQ(reply.OK, false)
   464  	}
   465  	// Special case of cross-reporting duping:
   466  	cmd := &dashapi.BugUpdate{
   467  		Status: dashapi.BugStatusDup,
   468  		ID:     rep1.ID,
   469  		DupOf:  rep3.ID,
   470  	}
   471  	t.Logf("duping %v -> %v", cmd.ID, cmd.DupOf)
   472  	reply, _ := c.client.ReportingUpdate(cmd)
   473  	c.expectTrue(reply.OK)
   474  }
   475  
   476  // Test that dups can't form a cycle.
   477  // The test builds cycles of length 1..4.
   478  func TestReportingDupCycle(t *testing.T) {
   479  	c := NewCtx(t)
   480  	defer c.Close()
   481  
   482  	build := testBuild(1)
   483  	c.client.UploadBuild(build)
   484  
   485  	const N = 4
   486  	reps := make([]*dashapi.BugReport, N)
   487  	for i := 0; i < N; i++ {
   488  		t.Logf("*************** %v ***************", i)
   489  		c.client.ReportCrash(testCrash(build, i))
   490  		reps[i] = c.client.pollBug()
   491  		replyError := "Can't dup bug to itself."
   492  		if i != 0 {
   493  			replyError = "Setting this dup would lead to a bug cycle, cycles are not allowed."
   494  			reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   495  				Status: dashapi.BugStatusDup,
   496  				ID:     reps[i-1].ID,
   497  				DupOf:  reps[i].ID,
   498  			})
   499  			c.expectEQ(reply.OK, true)
   500  		}
   501  		reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   502  			Status: dashapi.BugStatusDup,
   503  			ID:     reps[i].ID,
   504  			DupOf:  reps[0].ID,
   505  		})
   506  		c.expectEQ(reply.OK, false)
   507  		c.expectEQ(reply.Error, false)
   508  		c.expectEQ(reply.Text, replyError)
   509  		c.advanceTime(24 * time.Hour)
   510  	}
   511  }
   512  
   513  func TestReportingFilter(t *testing.T) {
   514  	c := NewCtx(t)
   515  	defer c.Close()
   516  
   517  	build := testBuild(1)
   518  	c.client.UploadBuild(build)
   519  
   520  	crash1 := testCrash(build, 1)
   521  	crash1.Title = "skip with repro 1"
   522  	c.client.ReportCrash(crash1)
   523  
   524  	// This does not skip first reporting, because it does not have repro.
   525  	rep1 := c.client.pollBug()
   526  	c.expectEQ(string(rep1.Config), `{"Index":1}`)
   527  
   528  	crash1.ReproSyz = []byte("getpid()")
   529  	c.client.ReportCrash(crash1)
   530  
   531  	// This has repro but was already reported to first reporting,
   532  	// so repro must go to the first reporting as well.
   533  	rep2 := c.client.pollBug()
   534  	c.expectEQ(string(rep2.Config), `{"Index":1}`)
   535  
   536  	// Now upstream it and it must go to the second reporting.
   537  	c.client.updateBug(rep1.ID, dashapi.BugStatusUpstream, "")
   538  
   539  	rep3 := c.client.pollBug()
   540  	c.expectEQ(string(rep3.Config), `{"Index":2}`)
   541  
   542  	// Now report a bug that must go to the second reporting right away.
   543  	crash2 := testCrash(build, 2)
   544  	crash2.Title = "skip with repro 2"
   545  	crash2.ReproSyz = []byte("getpid()")
   546  	c.client.ReportCrash(crash2)
   547  
   548  	rep4 := c.client.pollBug()
   549  	c.expectEQ(string(rep4.Config), `{"Index":2}`)
   550  }
   551  
   552  func TestMachineInfo(t *testing.T) {
   553  	c := NewCtx(t)
   554  	defer c.Close()
   555  
   556  	build := testBuild(1)
   557  	c.client.UploadBuild(build)
   558  
   559  	machineInfo := []byte("info1")
   560  
   561  	// Create a crash with machine information and check the returned machine
   562  	// information field is equal.
   563  	crash := &dashapi.Crash{
   564  		BuildID:     "build1",
   565  		Title:       "title1",
   566  		Maintainers: []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`},
   567  		Log:         []byte("log1"),
   568  		Report:      []byte("report1"),
   569  		MachineInfo: machineInfo,
   570  	}
   571  	c.client.ReportCrash(crash)
   572  	rep := c.client.pollBug()
   573  	c.expectEQ(machineInfo, rep.MachineInfo)
   574  
   575  	// Check that a link to machine information page is created on the dashboard,
   576  	// and the content is correct.
   577  	indexPage, err := c.AuthGET(AccessAdmin, "/test1")
   578  	c.expectOK(err)
   579  	bugLinkRegex := regexp.MustCompile(`<a href="(/bug\?extid=[^"]+)">title1</a>`)
   580  	bugLinkSubmatch := bugLinkRegex.FindSubmatch(indexPage)
   581  	c.expectEQ(len(bugLinkSubmatch), 2)
   582  	bugURL := html.UnescapeString(string(bugLinkSubmatch[1]))
   583  
   584  	bugPage, err := c.AuthGET(AccessAdmin, bugURL)
   585  	c.expectOK(err)
   586  	infoLinkRegex := regexp.MustCompile(`<a href="(/text\?tag=MachineInfo[^"]+)">info</a>`)
   587  	infoLinkSubmatch := infoLinkRegex.FindSubmatch(bugPage)
   588  	c.expectEQ(len(infoLinkSubmatch), 2)
   589  	infoURL := html.UnescapeString(string(infoLinkSubmatch[1]))
   590  
   591  	receivedInfo, err := c.AuthGET(AccessAdmin, infoURL)
   592  	c.expectOK(err)
   593  	c.expectEQ(receivedInfo, machineInfo)
   594  }
   595  
   596  func TestAltTitles1(t *testing.T) {
   597  	c := NewCtx(t)
   598  	defer c.Close()
   599  
   600  	build := testBuild(1)
   601  	c.client.UploadBuild(build)
   602  
   603  	// crash2.AltTitles matches crash1.Title.
   604  	crash1 := testCrash(build, 1)
   605  	crash2 := testCrashWithRepro(build, 2)
   606  	crash2.AltTitles = []string{crash1.Title}
   607  
   608  	c.client.ReportCrash(crash1)
   609  	rep := c.client.pollBug()
   610  	c.expectEQ(rep.Title, crash1.Title)
   611  	c.expectEQ(rep.Log, crash1.Log)
   612  
   613  	c.client.ReportCrash(crash2)
   614  	rep = c.client.pollBug()
   615  	c.expectEQ(rep.Title, crash1.Title)
   616  	c.expectEQ(rep.Log, crash2.Log)
   617  }
   618  
   619  func TestAltTitles2(t *testing.T) {
   620  	c := NewCtx(t)
   621  	defer c.Close()
   622  
   623  	build := testBuild(1)
   624  	c.client.UploadBuild(build)
   625  
   626  	// crash2.Title matches crash1.AltTitles, but reported in opposite order.
   627  	crash1 := testCrash(build, 1)
   628  	crash2 := testCrash(build, 2)
   629  	crash2.AltTitles = []string{crash1.Title}
   630  
   631  	c.client.ReportCrash(crash2)
   632  	rep := c.client.pollBug()
   633  	c.expectEQ(rep.Title, crash2.Title)
   634  	c.expectEQ(rep.Log, crash2.Log)
   635  
   636  	c.client.ReportCrash(crash1)
   637  	c.client.pollBugs(0)
   638  }
   639  
   640  func TestAltTitles3(t *testing.T) {
   641  	c := NewCtx(t)
   642  	defer c.Close()
   643  
   644  	build := testBuild(1)
   645  	c.client.UploadBuild(build)
   646  
   647  	// crash2.AltTitles matches crash1.AltTitles.
   648  	crash1 := testCrash(build, 1)
   649  	crash1.AltTitles = []string{"foobar"}
   650  	crash2 := testCrash(build, 2)
   651  	crash2.AltTitles = crash1.AltTitles
   652  
   653  	c.client.ReportCrash(crash1)
   654  	c.client.pollBugs(1)
   655  	c.client.ReportCrash(crash2)
   656  	c.client.pollBugs(0)
   657  }
   658  
   659  func TestAltTitles4(t *testing.T) {
   660  	c := NewCtx(t)
   661  	defer c.Close()
   662  
   663  	build := testBuild(1)
   664  	c.client.UploadBuild(build)
   665  
   666  	// crash1.AltTitles matches crash2.AltTitles which matches crash3.AltTitles.
   667  	crash1 := testCrash(build, 1)
   668  	crash1.AltTitles = []string{"foobar1"}
   669  	crash2 := testCrash(build, 2)
   670  	crash2.AltTitles = []string{"foobar1", "foobar2"}
   671  	crash3 := testCrash(build, 3)
   672  	crash3.AltTitles = []string{"foobar2"}
   673  
   674  	c.client.ReportCrash(crash1)
   675  	c.client.pollBugs(1)
   676  	c.client.ReportCrash(crash2)
   677  	c.client.pollBugs(0)
   678  	c.client.ReportCrash(crash3)
   679  	c.client.pollBugs(0)
   680  }
   681  
   682  func TestAltTitles5(t *testing.T) {
   683  	c := NewCtx(t)
   684  	defer c.Close()
   685  
   686  	build := testBuild(1)
   687  	c.client.UploadBuild(build)
   688  
   689  	// Test which of the possible existing bugs we choose for merging.
   690  	crash1 := testCrash(build, 1)
   691  	crash1.AltTitles = []string{"foo"}
   692  	c.client.ReportCrash(crash1)
   693  	c.client.pollBugs(1)
   694  
   695  	crash2 := testCrash(build, 2)
   696  	crash2.Title = "bar"
   697  	c.client.ReportCrash(crash2)
   698  	c.client.pollBugs(1)
   699  
   700  	crash3 := testCrash(build, 3)
   701  	c.client.ReportCrash(crash3)
   702  	c.client.pollBugs(1)
   703  	crash3.AltTitles = []string{"bar"}
   704  	c.client.ReportCrash(crash3)
   705  	c.client.pollBugs(0)
   706  
   707  	crash := testCrashWithRepro(build, 10)
   708  	crash.Title = "foo"
   709  	crash.AltTitles = []string{"bar"}
   710  	c.client.ReportCrash(crash)
   711  	rep := c.client.pollBug()
   712  	c.expectEQ(rep.Title, crash2.Title)
   713  	c.expectEQ(rep.Log, crash.Log)
   714  }
   715  
   716  func TestAltTitles6(t *testing.T) {
   717  	c := NewCtx(t)
   718  	defer c.Close()
   719  
   720  	build := testBuild(1)
   721  	c.client.UploadBuild(build)
   722  
   723  	// Test which of the possible existing bugs we choose for merging in presence of closed bugs.
   724  	crash1 := testCrash(build, 1)
   725  	crash1.AltTitles = []string{"foo"}
   726  	c.client.ReportCrash(crash1)
   727  	rep := c.client.pollBug()
   728  	c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "")
   729  	c.client.ReportCrash(crash1)
   730  	c.client.pollBug()
   731  
   732  	crash2 := testCrash(build, 2)
   733  	crash2.Title = "bar"
   734  	c.client.ReportCrash(crash2)
   735  	rep = c.client.pollBug()
   736  	c.client.updateBug(rep.ID, dashapi.BugStatusInvalid, "")
   737  
   738  	c.advanceTime(24 * time.Hour)
   739  	crash3 := testCrash(build, 3)
   740  	c.client.ReportCrash(crash3)
   741  	c.client.pollBugs(1)
   742  	crash3.AltTitles = []string{"foo"}
   743  	c.client.ReportCrash(crash3)
   744  	c.client.pollBugs(0)
   745  
   746  	crash := testCrashWithRepro(build, 10)
   747  	crash.Title = "foo"
   748  	crash.AltTitles = []string{"bar"}
   749  	c.client.ReportCrash(crash)
   750  	rep = c.client.pollBug()
   751  	c.expectEQ(rep.Title, crash1.Title+" (2)")
   752  	c.expectEQ(rep.Log, crash.Log)
   753  }
   754  
   755  func TestAltTitles7(t *testing.T) {
   756  	c := NewCtx(t)
   757  	defer c.Close()
   758  
   759  	build := testBuild(1)
   760  	c.client.UploadBuild(build)
   761  
   762  	// Test that bug merging is stable: if we started merging into a bug, we continue merging into that bug
   763  	// even if a better candidate appears.
   764  	crash1 := testCrash(build, 1)
   765  	crash1.AltTitles = []string{"foo"}
   766  	c.client.ReportCrash(crash1)
   767  	c.client.pollBug()
   768  
   769  	// This will be merged into crash1.
   770  	crash2 := testCrash(build, 2)
   771  	crash2.AltTitles = []string{"foo"}
   772  	c.client.ReportCrash(crash2)
   773  	c.client.pollBugs(0)
   774  
   775  	// Now report a better candidate.
   776  	crash3 := testCrash(build, 3)
   777  	crash3.Title = "aaa"
   778  	c.client.ReportCrash(crash3)
   779  	c.client.pollBug()
   780  	crash3.AltTitles = []string{crash2.Title}
   781  	c.client.ReportCrash(crash3)
   782  	c.client.pollBugs(0)
   783  
   784  	// Now report crash2 with a repro and ensure that it's still merged into crash1.
   785  	crash2.ReproOpts = []byte("some opts")
   786  	crash2.ReproSyz = []byte("getpid()")
   787  	c.client.ReportCrash(crash2)
   788  	rep := c.client.pollBug()
   789  	c.expectEQ(rep.Title, crash1.Title)
   790  	c.expectEQ(rep.Log, crash2.Log)
   791  }
   792  
   793  func TestDetachExternalTracker(t *testing.T) {
   794  	c := NewCtx(t)
   795  	defer c.Close()
   796  
   797  	build := testBuild(1)
   798  	c.client.UploadBuild(build)
   799  
   800  	crash1 := testCrash(build, 1)
   801  	c.client.ReportCrash(crash1)
   802  
   803  	// Get single report for "test" type.
   804  	resp, _ := c.client.ReportingPollBugs("test")
   805  	c.expectEQ(len(resp.Reports), 1)
   806  	rep1 := resp.Reports[0]
   807  	c.expectNE(rep1.ID, "")
   808  	c.expectEQ(string(rep1.Config), `{"Index":1}`)
   809  
   810  	// Signal detach_reporting for current bug.
   811  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   812  		ID:         rep1.ID,
   813  		Status:     dashapi.BugStatusUpstream,
   814  		ReproLevel: dashapi.ReproLevelNone,
   815  		Link:       "http://URI/1",
   816  		CrashID:    rep1.CrashID,
   817  	})
   818  	c.expectEQ(reply.OK, true)
   819  
   820  	// Now add syz repro to check it doesn't use first reporting.
   821  	crash1.ReproOpts = []byte("some opts")
   822  	crash1.ReproSyz = []byte("getpid()")
   823  	c.client.ReportCrash(crash1)
   824  
   825  	// Fetch bug and check reporting path (Config) is different.
   826  	rep2 := c.client.pollBug()
   827  	c.expectNE(rep2.ID, "")
   828  	c.expectEQ(string(rep2.Config), `{"Index":2}`)
   829  
   830  	closed, _ := c.client.ReportingPollClosed([]string{rep1.ID, rep2.ID})
   831  	c.expectEQ(len(closed), 1)
   832  	c.expectEQ(closed[0], rep1.ID)
   833  }
   834  
   835  func TestUpdateBugReporting(t *testing.T) {
   836  	c := NewCtx(t)
   837  	defer c.Close()
   838  	setIDs := func(bug *Bug, arr []BugReporting) {
   839  		for i := range arr {
   840  			arr[i].ID = bugReportingHash(bug.keyHash(c.ctx), arr[i].Name)
   841  		}
   842  	}
   843  	now := timeNow(c.ctx)
   844  	// We test against the test2 namespace.
   845  	cfg := c.config().Namespaces["test2"]
   846  	tests := []struct {
   847  		Before []BugReporting
   848  		After  []BugReporting
   849  		Error  bool
   850  	}{
   851  		// Initially empty object.
   852  		{
   853  			Before: []BugReporting{},
   854  			After: []BugReporting{
   855  				{
   856  					Name: "reporting1",
   857  				},
   858  				{
   859  					Name: "reporting2",
   860  				},
   861  				{
   862  					Name: "reporting3",
   863  				},
   864  			},
   865  		},
   866  		// Prepending and appending new reporting objects, the bug is not reported yet.
   867  		{
   868  			Before: []BugReporting{
   869  				{
   870  					Name: "reporting2",
   871  				},
   872  			},
   873  			After: []BugReporting{
   874  				{
   875  					Name: "reporting1",
   876  				},
   877  				{
   878  					Name: "reporting2",
   879  				},
   880  				{
   881  					Name: "reporting3",
   882  				},
   883  			},
   884  		},
   885  		// The order or reportings is changed.
   886  		{
   887  			Before: []BugReporting{
   888  				{
   889  					Name: "reporting2",
   890  				},
   891  				{
   892  					Name: "reporting1",
   893  				},
   894  				{
   895  					Name: "reporting3",
   896  				},
   897  			},
   898  			After: []BugReporting{},
   899  			Error: true,
   900  		},
   901  		// Prepending and appending new reporting objects, the bug is already reported.
   902  		{
   903  			Before: []BugReporting{
   904  				{
   905  					Name:     "reporting2",
   906  					Reported: now,
   907  					ExtID:    "abcd",
   908  				},
   909  			},
   910  			After: []BugReporting{
   911  				{
   912  					Name:     "reporting1",
   913  					Closed:   now,
   914  					Reported: now,
   915  					Dummy:    true,
   916  				},
   917  				{
   918  					Name:     "reporting2",
   919  					Reported: now,
   920  					ExtID:    "abcd",
   921  				},
   922  				{
   923  					Name: "reporting3",
   924  				},
   925  			},
   926  		},
   927  		// It must look like as if the new Reporting was immediate.
   928  		{
   929  			Before: []BugReporting{
   930  				{
   931  					Name:     "reporting1",
   932  					Reported: now.Add(-24 * time.Hour),
   933  					ExtID:    "abcd",
   934  				},
   935  				{
   936  					Name:     "reporting3",
   937  					Reported: now,
   938  					ExtID:    "efgh",
   939  				},
   940  			},
   941  			After: []BugReporting{
   942  				{
   943  					Name:     "reporting1",
   944  					Reported: now.Add(-24 * time.Hour),
   945  					ExtID:    "abcd",
   946  				},
   947  				{
   948  					Name:     "reporting2",
   949  					Reported: now.Add(-24 * time.Hour),
   950  					Closed:   now.Add(-24 * time.Hour),
   951  					Dummy:    true,
   952  				},
   953  				{
   954  					Name:     "reporting3",
   955  					Reported: now,
   956  					ExtID:    "efgh",
   957  				},
   958  			},
   959  		},
   960  	}
   961  	for _, test := range tests {
   962  		bug := &Bug{
   963  			Title:     "bug",
   964  			Reporting: test.Before,
   965  			Namespace: "test2",
   966  		}
   967  		setIDs(bug, bug.Reporting)
   968  		setIDs(bug, test.After)
   969  		hasError := bug.updateReportings(c.ctx, cfg, now) != nil
   970  		if hasError != test.Error {
   971  			t.Errorf("before: %#v, expected error: %v, got error: %v", test.Before, test.Error, hasError)
   972  		}
   973  		if !test.Error && !reflect.DeepEqual(bug.Reporting, test.After) {
   974  			t.Errorf("before: %#v, expected after: %#v, got after: %#v", test.Before, test.After, bug.Reporting)
   975  		}
   976  	}
   977  }
   978  
   979  func TestFullBugInfo(t *testing.T) {
   980  	c := NewCtx(t)
   981  	defer c.Close()
   982  
   983  	build := testBuild(1)
   984  	c.client.UploadBuild(build)
   985  
   986  	const crashTitle = "WARNING: abcd"
   987  
   988  	// Oldest crash: with strace.
   989  	crashStrace := testCrashWithRepro(build, 1)
   990  	crashStrace.Title = crashTitle
   991  	crashStrace.Flags = dashapi.CrashUnderStrace
   992  	crashStrace.Report = []byte("with strace")
   993  	c.client.ReportCrash(crashStrace)
   994  	rep := c.client.pollBug()
   995  
   996  	// Newer: just with repro.
   997  	c.advanceTime(24 * 7 * time.Hour)
   998  	crashRepro := testCrashWithRepro(build, 1)
   999  	crashRepro.Title = crashTitle
  1000  	crashRepro.Report = []byte("with repro")
  1001  	c.client.ReportCrash(crashRepro)
  1002  
  1003  	// Ensure we have some bisect jobs done.
  1004  	pollResp := c.client.pollJobs(build.Manager)
  1005  	c.expectNE(pollResp.ID, "")
  1006  	jobID := pollResp.ID
  1007  	done := &dashapi.JobDoneReq{
  1008  		ID:    jobID,
  1009  		Build: *testBuild(3),
  1010  		Log:   []byte("bisect log"),
  1011  		Commits: []dashapi.Commit{
  1012  			{
  1013  				Hash:   "111111111111111111111111",
  1014  				Title:  "kernel: break build",
  1015  				Author: "hacker@kernel.org",
  1016  				CC:     []string{"reviewer1@kernel.org"},
  1017  				Date:   time.Date(2000, 2, 9, 4, 5, 6, 7, time.UTC),
  1018  			},
  1019  		},
  1020  	}
  1021  	c.client.expectOK(c.client.JobDone(done))
  1022  	c.client.pollBug()
  1023  
  1024  	// Yet newer: no repro.
  1025  	c.advanceTime(24 * 7 * time.Hour)
  1026  	crashNew := testCrash(build, 1)
  1027  	crashNew.Title = crashTitle
  1028  	c.client.ReportCrash(crashNew)
  1029  
  1030  	// And yet newer.
  1031  	c.advanceTime(24 * time.Hour)
  1032  	crashNew2 := testCrash(build, 1)
  1033  	crashNew2.Title = crashTitle
  1034  	crashNew2.Report = []byte("newest")
  1035  	c.client.ReportCrash(crashNew2)
  1036  
  1037  	// Also create a bug in another namespace.
  1038  	otherBuild := testBuild(2)
  1039  	c.client2.UploadBuild(otherBuild)
  1040  
  1041  	otherCrash := testCrash(otherBuild, 1)
  1042  	otherCrash.Title = crashTitle
  1043  	otherCrash.ReproOpts = []byte("repro opts")
  1044  	otherCrash.ReproSyz = []byte("repro syz")
  1045  	c.client2.ReportCrash(otherCrash)
  1046  	otherPollMsg := c.client2.pollEmailBug()
  1047  
  1048  	_, err := c.POST("/_ah/mail/", fmt.Sprintf(`Sender: syzkaller@googlegroups.com
  1049  Date: Tue, 15 Aug 2017 14:59:00 -0700
  1050  Message-ID: <1234>
  1051  Subject: crash1
  1052  From: %v
  1053  To: foo@bar.com
  1054  Content-Type: text/plain
  1055  
  1056  This email is only needed to capture the link
  1057  --
  1058  To post to this group, send email to syzkaller@googlegroups.com.
  1059  To view this discussion on the web visit https://groups.google.com/d/msgid/syzkaller/1234@google.com.
  1060  For more options, visit https://groups.google.com/d/optout.
  1061  `, otherPollMsg.Sender))
  1062  	c.expectOK(err)
  1063  
  1064  	_, otherExtBugID, _ := email.RemoveAddrContext(otherPollMsg.Sender)
  1065  
  1066  	// Query the full bug info.
  1067  	info, err := c.client.LoadFullBug(&dashapi.LoadFullBugReq{BugID: rep.ID})
  1068  	c.expectOK(err)
  1069  	if info.BisectCause == nil {
  1070  		t.Fatalf("info.BisectCause is empty")
  1071  	}
  1072  	if info.BisectCause.BisectCause == nil {
  1073  		t.Fatalf("info.BisectCause.BisectCause is empty")
  1074  	}
  1075  	c.expectEQ(info.SimilarBugs, []*dashapi.SimilarBugInfo{{
  1076  		Title:      crashTitle,
  1077  		Namespace:  "test2",
  1078  		Status:     dashapi.BugStatusOpen,
  1079  		ReproLevel: dashapi.ReproLevelSyz,
  1080  		Link:       "https://testapp.appspot.com/bug?extid=" + otherExtBugID,
  1081  		ReportLink: "https://groups.google.com/d/msgid/syzkaller/1234@google.com",
  1082  	}})
  1083  
  1084  	// There must be 3 crashes.
  1085  	reportsOrder := [][]byte{[]byte("newest"), []byte("with repro"), []byte("with strace")}
  1086  	c.expectEQ(len(info.Crashes), len(reportsOrder))
  1087  	for i, report := range reportsOrder {
  1088  		c.expectEQ(info.Crashes[i].Report, report)
  1089  	}
  1090  }
  1091  
  1092  func TestUpdateReportApi(t *testing.T) {
  1093  	c := NewCtx(t)
  1094  	defer c.Close()
  1095  
  1096  	build := testBuild(1)
  1097  	c.client.UploadBuild(build)
  1098  
  1099  	// Report a crash.
  1100  	c.client.ReportCrash(testCrashWithRepro(build, 1))
  1101  	c.client.pollBug()
  1102  
  1103  	listResp, err := c.client.BugList()
  1104  	c.expectOK(err)
  1105  	c.expectEQ(len(listResp.List), 1)
  1106  
  1107  	// Load the bug info.
  1108  	bugID := listResp.List[0]
  1109  	rep, err := c.client.LoadBug(bugID)
  1110  	c.expectOK(err)
  1111  
  1112  	// Now update the crash.
  1113  	setGuiltyFiles := []string{"fs/a.c", "net/b.c"}
  1114  	err = c.client.UpdateReport(&dashapi.UpdateReportReq{
  1115  		BugID:       bugID,
  1116  		CrashID:     rep.CrashID,
  1117  		GuiltyFiles: &setGuiltyFiles,
  1118  	})
  1119  	c.expectOK(err)
  1120  
  1121  	// And make sure it's been updated.
  1122  	ret, err := c.client.LoadBug(bugID)
  1123  	if err != nil {
  1124  		t.Fatal(err)
  1125  	}
  1126  	if ret.ReportElements == nil {
  1127  		t.Fatalf("ReportElements is nil")
  1128  	}
  1129  	if diff := cmp.Diff(ret.ReportElements.GuiltyFiles, setGuiltyFiles); diff != "" {
  1130  		t.Fatal(diff)
  1131  	}
  1132  }
  1133  
  1134  func TestReportDecommissionedBugs(t *testing.T) {
  1135  	c := NewCtx(t)
  1136  	defer c.Close()
  1137  
  1138  	client := c.makeClient(clientTestDecomm, keyTestDecomm, true)
  1139  	build := testBuild(1)
  1140  	client.UploadBuild(build)
  1141  
  1142  	crash := testCrash(build, 1)
  1143  	client.ReportCrash(crash)
  1144  	rep := client.pollBug()
  1145  
  1146  	closed, _ := client.ReportingPollClosed([]string{rep.ID})
  1147  	c.expectEQ(len(closed), 0)
  1148  
  1149  	// And now let's decommission the namespace.
  1150  	c.decommission(rep.Namespace)
  1151  
  1152  	closed, _ = client.ReportingPollClosed([]string{rep.ID})
  1153  	c.expectEQ(len(closed), 1)
  1154  	c.expectEQ(closed[0], rep.ID)
  1155  }
  1156  
  1157  func TestObsoletePeriod(t *testing.T) {
  1158  	base := time.Now()
  1159  	c := context.Background()
  1160  	config := getConfig(c)
  1161  	tests := []struct {
  1162  		name   string
  1163  		bug    *Bug
  1164  		period time.Duration
  1165  	}{
  1166  		{
  1167  			name: "frequent final bug",
  1168  			bug: &Bug{
  1169  				// Once in a day.
  1170  				NumCrashes: 30,
  1171  				FirstTime:  base,
  1172  				LastTime:   base.Add(time.Hour * 24 * 30),
  1173  				Reporting:  []BugReporting{{Reported: base}},
  1174  			},
  1175  			// 80 days are definitely enough.
  1176  			period: config.Obsoleting.MinPeriod,
  1177  		},
  1178  		{
  1179  			name: "very short-living final bug",
  1180  			bug: &Bug{
  1181  				NumCrashes: 5,
  1182  				FirstTime:  base,
  1183  				LastTime:   base.Add(time.Hour * 24),
  1184  				Reporting:  []BugReporting{{Reported: base}},
  1185  			},
  1186  			// Too few crashes, wait max time.
  1187  			period: config.Obsoleting.MaxPeriod,
  1188  		},
  1189  		{
  1190  			name: "rare stable final bug",
  1191  			bug: &Bug{
  1192  				// Once in 20 days.
  1193  				NumCrashes: 20,
  1194  				FirstTime:  base,
  1195  				LastTime:   base.Add(time.Hour * 24 * 400),
  1196  				Reporting:  []BugReporting{{Reported: base}},
  1197  			},
  1198  			// Wait max time.
  1199  			period: config.Obsoleting.MaxPeriod,
  1200  		},
  1201  		{
  1202  			name: "frequent non-final bug",
  1203  			bug: &Bug{
  1204  				// Once in a day.
  1205  				NumCrashes: 10,
  1206  				FirstTime:  base,
  1207  				LastTime:   base.Add(time.Hour * 24 * 10),
  1208  				Reporting:  []BugReporting{{}},
  1209  			},
  1210  			// 40 days are also enough.
  1211  			period: config.Obsoleting.NonFinalMinPeriod,
  1212  		},
  1213  	}
  1214  
  1215  	for _, test := range tests {
  1216  		test := test
  1217  		t.Run(test.name, func(t *testing.T) {
  1218  			ret := test.bug.obsoletePeriod(c)
  1219  			assert.Equal(t, test.period, ret)
  1220  		})
  1221  	}
  1222  }
  1223  
  1224  func TestReportRevokedRepro(t *testing.T) {
  1225  	// There was a bug (#4412) where syzbot infinitely re-reported reproducers
  1226  	// for a bug that was upstreamed after its repro was revoked.
  1227  	// Recreate this situation.
  1228  	c := NewCtx(t)
  1229  	defer c.Close()
  1230  
  1231  	client := c.makeClient(clientPublic, keyPublic, true)
  1232  	build := testBuild(1)
  1233  	build.KernelRepo = "git://mygit.com/git.git"
  1234  	build.KernelBranch = "main"
  1235  	client.UploadBuild(build)
  1236  
  1237  	crash := testCrash(build, 1)
  1238  	crash.ReproOpts = []byte("repro opts")
  1239  	crash.ReproSyz = []byte("repro syz")
  1240  	client.ReportCrash(crash)
  1241  	rep1 := client.pollBug()
  1242  	client.expectNE(rep1.ReproSyz, nil)
  1243  
  1244  	// Revoke the reproducer.
  1245  	c.advanceTime(c.config().Obsoleting.ReproRetestStart + time.Hour)
  1246  	jobResp := client.pollSpecificJobs(build.Manager, dashapi.ManagerJobs{TestPatches: true})
  1247  	c.expectEQ(jobResp.Type, dashapi.JobTestPatch)
  1248  	client.expectOK(client.JobDone(&dashapi.JobDoneReq{
  1249  		ID: jobResp.ID,
  1250  	}))
  1251  
  1252  	c.advanceTime(time.Hour)
  1253  	client.ReportCrash(testCrash(build, 1))
  1254  
  1255  	// Upstream the bug.
  1256  	c.advanceTime(time.Hour)
  1257  	client.updateBug(rep1.ID, dashapi.BugStatusUpstream, "")
  1258  	rep2 := client.pollBug()
  1259  
  1260  	// Also ensure that we do not report the revoked reproducer.
  1261  	client.expectEQ(rep2.Type, dashapi.ReportNew)
  1262  	client.expectEQ(rep2.ReproSyz, []byte(nil))
  1263  
  1264  	// Expect no further reports.
  1265  	client.pollBugs(0)
  1266  }
  1267  
  1268  func TestWaitForRepro(t *testing.T) {
  1269  	c := NewCtx(t)
  1270  	defer c.Close()
  1271  
  1272  	client := c.client
  1273  	c.setWaitForRepro("test1", time.Hour*24)
  1274  
  1275  	build := testBuild(1)
  1276  	client.UploadBuild(build)
  1277  
  1278  	// Normal crash witout repro.
  1279  	client.ReportCrash(testCrash(build, 1))
  1280  	client.pollBugs(0)
  1281  	c.advanceTime(time.Hour * 24)
  1282  	client.pollBug()
  1283  
  1284  	// A crash first without repro, then with it.
  1285  	client.ReportCrash(testCrash(build, 2))
  1286  	c.advanceTime(time.Hour * 12)
  1287  	client.pollBugs(0)
  1288  	client.ReportCrash(testCrashWithRepro(build, 2))
  1289  	client.pollBug()
  1290  
  1291  	// A crash with a reproducer.
  1292  	c.advanceTime(time.Minute)
  1293  	client.ReportCrash(testCrashWithRepro(build, 3))
  1294  	client.pollBug()
  1295  
  1296  	// A crahs that will never have a reproducer.
  1297  	c.advanceTime(time.Minute)
  1298  	crash := testCrash(build, 4)
  1299  	crash.Title = "upstream test error: abcd"
  1300  	client.ReportCrash(crash)
  1301  	client.pollBug()
  1302  }