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