github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/fix_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  	"testing"
     8  	"time"
     9  
    10  	"github.com/google/syzkaller/dashboard/dashapi"
    11  )
    12  
    13  // Basic scenario of marking a bug as fixed by a particular commit,
    14  // discovering this commit on builder and marking the bug as ultimately fixed.
    15  func TestFixBasic(t *testing.T) {
    16  	c := NewCtx(t)
    17  	defer c.Close()
    18  
    19  	build1 := testBuild(1)
    20  	c.client.UploadBuild(build1)
    21  
    22  	crash1 := testCrash(build1, 1)
    23  	c.client.ReportCrash(crash1)
    24  
    25  	builderPollResp, _ := c.client.BuilderPoll(build1.Manager)
    26  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
    27  
    28  	needRepro, _ := c.client.NeedRepro(testCrashID(crash1))
    29  	c.expectEQ(needRepro, true)
    30  
    31  	rep := c.client.pollBug()
    32  
    33  	// Specify fixing commit for the bug.
    34  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
    35  		ID:         rep.ID,
    36  		Status:     dashapi.BugStatusOpen,
    37  		FixCommits: []string{"foo: fix the crash"},
    38  	})
    39  	c.expectEQ(reply.OK, true)
    40  
    41  	// Don't need repro once there are fixing commits.
    42  	needRepro, _ = c.client.NeedRepro(testCrashID(crash1))
    43  	c.expectEQ(needRepro, false)
    44  
    45  	// Check that the commit is now passed to builders.
    46  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
    47  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
    48  	c.expectEQ(builderPollResp.PendingCommits[0], "foo: fix the crash")
    49  
    50  	// Patches must not be reset on other actions.
    51  	c.client.updateBug(rep.ID, dashapi.BugStatusOpen, "")
    52  
    53  	// Upstream commands must fail if patches are already present.
    54  	// Right course of action is unclear in this situation,
    55  	// so this test merely documents the current behavior.
    56  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
    57  		ID:     rep.ID,
    58  		Status: dashapi.BugStatusUpstream,
    59  	})
    60  	c.expectEQ(reply.OK, false)
    61  
    62  	c.client.ReportCrash(crash1)
    63  	c.client.pollBugs(0)
    64  
    65  	// Upload another build with the commit present.
    66  	build2 := testBuild(2)
    67  	build2.Manager = build1.Manager
    68  	build2.Commits = []string{"foo: fix the crash"}
    69  	c.client.UploadBuild(build2)
    70  
    71  	// Check that the commit is now not passed to this builder.
    72  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
    73  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
    74  
    75  	// Ensure that a new crash creates a new bug (the old one must be marked as fixed).
    76  	c.client.ReportCrash(crash1)
    77  	rep2 := c.client.pollBug()
    78  	c.expectEQ(rep2.Title, "title1 (2)")
    79  
    80  	// Regression test: previously upstreamming failed because the new bug had fixing commits.
    81  	c.client.ReportCrash(crash1)
    82  	c.client.updateBug(rep2.ID, dashapi.BugStatusUpstream, "")
    83  	c.client.pollBug()
    84  }
    85  
    86  // Test bug that is fixed by 2 commits.
    87  func TestFixedByTwoCommits(t *testing.T) {
    88  	c := NewCtx(t)
    89  	defer c.Close()
    90  
    91  	build1 := testBuild(1)
    92  	c.client.UploadBuild(build1)
    93  
    94  	crash1 := testCrash(build1, 1)
    95  	c.client.ReportCrash(crash1)
    96  
    97  	builderPollResp, _ := c.client.BuilderPoll(build1.Manager)
    98  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
    99  
   100  	rep := c.client.pollBug()
   101  
   102  	// Specify fixing commit for the bug.
   103  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   104  		ID:         rep.ID,
   105  		Status:     dashapi.BugStatusOpen,
   106  		FixCommits: []string{"bar: prepare for fixing", "\"foo: fix the crash\""},
   107  	})
   108  	c.expectEQ(reply.OK, true)
   109  
   110  	// Check that the commit is now passed to builders.
   111  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   112  	c.expectEQ(len(builderPollResp.PendingCommits), 2)
   113  	c.expectEQ(builderPollResp.PendingCommits[0], "bar: prepare for fixing")
   114  	c.expectEQ(builderPollResp.PendingCommits[1], "foo: fix the crash")
   115  
   116  	// Upload another build with only one of the commits.
   117  	build2 := testBuild(2)
   118  	build2.Manager = build1.Manager
   119  	build2.Commits = []string{"bar: prepare for fixing"}
   120  	c.client.UploadBuild(build2)
   121  
   122  	// Check that it has not fixed the bug.
   123  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   124  	c.expectEQ(len(builderPollResp.PendingCommits), 2)
   125  	c.expectEQ(builderPollResp.PendingCommits[0], "bar: prepare for fixing")
   126  	c.expectEQ(builderPollResp.PendingCommits[1], "foo: fix the crash")
   127  
   128  	c.client.ReportCrash(crash1)
   129  	c.client.pollBugs(0)
   130  
   131  	// Now upload build with both commits.
   132  	build3 := testBuild(3)
   133  	build3.Manager = build1.Manager
   134  	build3.Commits = []string{"foo: fix the crash", "bar: prepare for fixing"}
   135  	c.client.UploadBuild(build3)
   136  
   137  	// Check that the commit is now not passed to this builder.
   138  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   139  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   140  
   141  	// Ensure that a new crash creates a new bug (the old one must be marked as fixed).
   142  	c.client.ReportCrash(crash1)
   143  	rep2 := c.client.pollBug()
   144  	c.expectEQ(rep2.Title, "title1 (2)")
   145  }
   146  
   147  // A bug is marked as fixed by one commit and then remarked as fixed by another.
   148  func TestReFixed(t *testing.T) {
   149  	c := NewCtx(t)
   150  	defer c.Close()
   151  
   152  	build1 := testBuild(1)
   153  	c.client.UploadBuild(build1)
   154  
   155  	crash1 := testCrash(build1, 1)
   156  	c.client.ReportCrash(crash1)
   157  
   158  	builderPollResp, _ := c.client.BuilderPoll(build1.Manager)
   159  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   160  
   161  	c.advanceTime(time.Hour)
   162  	rep := c.client.pollBug()
   163  
   164  	bug, _, _ := c.loadBug(rep.ID)
   165  	c.expectEQ(bug.LastActivity, c.mockedTime)
   166  	c.expectEQ(bug.FixTime, time.Time{})
   167  
   168  	// Specify fixing commit for the bug.
   169  	c.advanceTime(time.Hour)
   170  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   171  		ID:         rep.ID,
   172  		Status:     dashapi.BugStatusOpen,
   173  		FixCommits: []string{"a wrong one"},
   174  	})
   175  	c.expectEQ(reply.OK, true)
   176  
   177  	bug, _, _ = c.loadBug(rep.ID)
   178  	c.expectEQ(bug.LastActivity, c.mockedTime)
   179  	c.expectEQ(bug.FixTime, c.mockedTime)
   180  
   181  	c.advanceTime(time.Hour)
   182  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   183  		ID:         rep.ID,
   184  		Status:     dashapi.BugStatusOpen,
   185  		FixCommits: []string{"the right one"},
   186  	})
   187  	c.expectEQ(reply.OK, true)
   188  
   189  	bug, _, _ = c.loadBug(rep.ID)
   190  	c.expectEQ(bug.LastActivity, c.mockedTime)
   191  	c.expectEQ(bug.FixTime, c.mockedTime)
   192  
   193  	// No updates, just check that LastActivity time is updated, FixTime preserved.
   194  	fixTime := c.mockedTime
   195  	c.advanceTime(time.Hour)
   196  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   197  		ID:     rep.ID,
   198  		Status: dashapi.BugStatusOpen,
   199  	})
   200  	c.expectEQ(reply.OK, true)
   201  	bug, _, _ = c.loadBug(rep.ID)
   202  	c.expectEQ(bug.LastActivity, c.mockedTime)
   203  	c.expectEQ(bug.FixTime, fixTime)
   204  
   205  	// Send the same fixing commit, check that LastActivity time is updated, FixTime preserved.
   206  	c.advanceTime(time.Hour)
   207  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   208  		ID:         rep.ID,
   209  		Status:     dashapi.BugStatusOpen,
   210  		FixCommits: []string{"the right one"},
   211  	})
   212  	c.expectEQ(reply.OK, true)
   213  	bug, _, _ = c.loadBug(rep.ID)
   214  	c.expectEQ(bug.LastActivity, c.mockedTime)
   215  	c.expectEQ(bug.FixTime, fixTime)
   216  
   217  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   218  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   219  	c.expectEQ(builderPollResp.PendingCommits[0], "the right one")
   220  
   221  	// Upload another build with the wrong commit.
   222  	build2 := testBuild(2)
   223  	build2.Manager = build1.Manager
   224  	build2.Commits = []string{"a wrong one"}
   225  	c.client.UploadBuild(build2)
   226  
   227  	// Check that it has not fixed the bug.
   228  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   229  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   230  	c.expectEQ(builderPollResp.PendingCommits[0], "the right one")
   231  
   232  	c.client.ReportCrash(crash1)
   233  	c.client.pollBugs(0)
   234  
   235  	// Now upload build with the right commit.
   236  	build3 := testBuild(3)
   237  	build3.Manager = build1.Manager
   238  	build3.Commits = []string{"the right one"}
   239  	c.client.UploadBuild(build3)
   240  
   241  	// Check that the commit is now not passed to this builder.
   242  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   243  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   244  }
   245  
   246  // Fixing commit is present on one manager, but missing on another.
   247  func TestFixTwoManagers(t *testing.T) {
   248  	c := NewCtx(t)
   249  	defer c.Close()
   250  
   251  	build1 := testBuild(1)
   252  	c.client.UploadBuild(build1)
   253  
   254  	crash1 := testCrash(build1, 1)
   255  	c.client.ReportCrash(crash1)
   256  
   257  	builderPollResp, _ := c.client.BuilderPoll(build1.Manager)
   258  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   259  
   260  	rep := c.client.pollBug()
   261  
   262  	// Specify fixing commit for the bug.
   263  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   264  		ID:         rep.ID,
   265  		Status:     dashapi.BugStatusOpen,
   266  		FixCommits: []string{"foo: fix the crash"},
   267  	})
   268  	c.expectEQ(reply.OK, true)
   269  
   270  	// Now the second manager appears.
   271  	build2 := testBuild(2)
   272  	c.client.UploadBuild(build2)
   273  
   274  	// Check that the commit is now passed to builders.
   275  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   276  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   277  	c.expectEQ(builderPollResp.PendingCommits[0], "foo: fix the crash")
   278  
   279  	builderPollResp, _ = c.client.BuilderPoll(build2.Manager)
   280  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   281  	c.expectEQ(builderPollResp.PendingCommits[0], "foo: fix the crash")
   282  
   283  	// Now first manager picks up the commit.
   284  	build3 := testBuild(3)
   285  	build3.Manager = build1.Manager
   286  	build3.Commits = []string{"foo: fix the crash"}
   287  	c.client.UploadBuild(build3)
   288  
   289  	// Check that the commit is now not passed to this builder.
   290  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   291  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   292  
   293  	// But still passed to another.
   294  	builderPollResp, _ = c.client.BuilderPoll(build2.Manager)
   295  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   296  	c.expectEQ(builderPollResp.PendingCommits[0], "foo: fix the crash")
   297  
   298  	// Check that the bug is still open.
   299  	c.client.ReportCrash(crash1)
   300  	c.client.pollBugs(0)
   301  
   302  	// Now the second manager picks up the commit.
   303  	build4 := testBuild(4)
   304  	build4.Manager = build2.Manager
   305  	build4.Commits = []string{"foo: fix the crash"}
   306  	c.client.UploadBuild(build4)
   307  
   308  	// Now the bug must be fixed.
   309  	builderPollResp, _ = c.client.BuilderPoll(build2.Manager)
   310  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   311  
   312  	c.client.ReportCrash(crash1)
   313  	rep2 := c.client.pollBug()
   314  	c.expectEQ(rep2.Title, "title1 (2)")
   315  }
   316  
   317  func TestReFixedTwoManagers(t *testing.T) {
   318  	c := NewCtx(t)
   319  	defer c.Close()
   320  
   321  	build1 := testBuild(1)
   322  	c.client.UploadBuild(build1)
   323  
   324  	crash1 := testCrash(build1, 1)
   325  	c.client.ReportCrash(crash1)
   326  
   327  	builderPollResp, _ := c.client.BuilderPoll(build1.Manager)
   328  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   329  
   330  	rep := c.client.pollBug()
   331  
   332  	// Specify fixing commit for the bug.
   333  	reply, _ := c.client.ReportingUpdate(&dashapi.BugUpdate{
   334  		ID:         rep.ID,
   335  		Status:     dashapi.BugStatusOpen,
   336  		FixCommits: []string{"foo: fix the crash"},
   337  	})
   338  	c.expectEQ(reply.OK, true)
   339  
   340  	// Now the second manager appears.
   341  	build2 := testBuild(2)
   342  	c.client.UploadBuild(build2)
   343  
   344  	// Now first manager picks up the commit.
   345  	build3 := testBuild(3)
   346  	build3.Manager = build1.Manager
   347  	build3.Commits = []string{"foo: fix the crash"}
   348  	c.client.UploadBuild(build3)
   349  
   350  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   351  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   352  
   353  	// Now we change the fixing commit.
   354  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   355  		ID:         rep.ID,
   356  		Status:     dashapi.BugStatusOpen,
   357  		FixCommits: []string{"the right one"},
   358  	})
   359  	c.expectEQ(reply.OK, true)
   360  
   361  	// Now it must again appear on both managers.
   362  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   363  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   364  	c.expectEQ(builderPollResp.PendingCommits[0], "the right one")
   365  
   366  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   367  	c.expectEQ(len(builderPollResp.PendingCommits), 1)
   368  	c.expectEQ(builderPollResp.PendingCommits[0], "the right one")
   369  
   370  	// Now the second manager picks up the second commit.
   371  	build4 := testBuild(4)
   372  	build4.Manager = build2.Manager
   373  	build4.Commits = []string{"the right one"}
   374  	c.client.UploadBuild(build4)
   375  
   376  	// The bug must be still open.
   377  	c.client.ReportCrash(crash1)
   378  	c.client.pollBugs(0)
   379  
   380  	// Specify fixing commit again, but it's the same one as before, so nothing changed.
   381  	reply, _ = c.client.ReportingUpdate(&dashapi.BugUpdate{
   382  		ID:         rep.ID,
   383  		Status:     dashapi.BugStatusOpen,
   384  		FixCommits: []string{"the right one"},
   385  	})
   386  	c.expectEQ(reply.OK, true)
   387  
   388  	// Now the first manager picks up the second commit.
   389  	build5 := testBuild(5)
   390  	build5.Manager = build1.Manager
   391  	build5.Commits = []string{"the right one"}
   392  	c.client.UploadBuild(build5)
   393  
   394  	// Now the bug must be fixed.
   395  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   396  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   397  
   398  	c.client.ReportCrash(crash1)
   399  	rep2 := c.client.pollBug()
   400  	c.expectEQ(rep2.Title, "title1 (2)")
   401  }
   402  
   403  // TestFixedWithCommitTags tests fixing of bugs with Reported-by commit tags.
   404  func TestFixedWithCommitTags(t *testing.T) {
   405  	c := NewCtx(t)
   406  	defer c.Close()
   407  
   408  	build1 := testBuild(1)
   409  	c.client.UploadBuild(build1)
   410  
   411  	build2 := testBuild(2)
   412  	c.client.UploadBuild(build2)
   413  
   414  	crash1 := testCrash(build1, 1)
   415  	c.client.ReportCrash(crash1)
   416  
   417  	rep := c.client.pollBug()
   418  
   419  	// Upload build with 2 fixing commits for this bug.
   420  	build1.FixCommits = []dashapi.Commit{
   421  		{Title: "fix commit 1", BugIDs: []string{rep.ID}},
   422  		{Title: "fix commit 2", BugIDs: []string{rep.ID}},
   423  	}
   424  	c.client.UploadBuild(build1)
   425  
   426  	// Now the commits must be associated with the bug and the second
   427  	// manager must get them as pending.
   428  	builderPollResp, _ := c.client.BuilderPoll(build2.Manager)
   429  	c.expectEQ(len(builderPollResp.PendingCommits), 2)
   430  	c.expectEQ(builderPollResp.PendingCommits[0], "fix commit 1")
   431  	c.expectEQ(builderPollResp.PendingCommits[1], "fix commit 2")
   432  
   433  	// The first manager must not get them.
   434  	builderPollResp, _ = c.client.BuilderPoll(build1.Manager)
   435  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   436  
   437  	// The bug is still not fixed.
   438  	c.client.ReportCrash(crash1)
   439  	c.client.pollBugs(0)
   440  
   441  	// Now the second manager reports the same commits.
   442  	// This must close the bug.
   443  	build2.FixCommits = build1.FixCommits
   444  	c.client.UploadBuild(build2)
   445  
   446  	// Commits must not be passed to managers.
   447  	builderPollResp, _ = c.client.BuilderPoll(build2.Manager)
   448  	c.expectEQ(len(builderPollResp.PendingCommits), 0)
   449  
   450  	// Ensure that a new crash creates a new bug.
   451  	c.client.ReportCrash(crash1)
   452  	rep2 := c.client.pollBug()
   453  	c.expectEQ(rep2.Title, "title1 (2)")
   454  }
   455  
   456  // TestFixedDup tests Reported-by commit tag that comes for a dup.
   457  // In such case we need to associate it with the canonical bugs.
   458  func TestFixedDup(t *testing.T) {
   459  	c := NewCtx(t)
   460  	defer c.Close()
   461  
   462  	build := testBuild(1)
   463  	c.client.UploadBuild(build)
   464  
   465  	crash1 := testCrash(build, 1)
   466  	c.client.ReportCrash(crash1)
   467  	rep1 := c.client.pollBug()
   468  
   469  	crash2 := testCrash(build, 2)
   470  	c.client.ReportCrash(crash2)
   471  	rep2 := c.client.pollBug()
   472  
   473  	// rep2 is a dup of rep1.
   474  	c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
   475  
   476  	// Upload build that fixes rep2.
   477  	build.FixCommits = []dashapi.Commit{
   478  		{Title: "fix commit 1", BugIDs: []string{rep2.ID}},
   479  	}
   480  	c.client.UploadBuild(build)
   481  
   482  	// This must fix rep1.
   483  	c.client.ReportCrash(crash1)
   484  	rep3 := c.client.pollBug()
   485  	c.expectEQ(rep3.Title, rep1.Title+" (2)")
   486  }
   487  
   488  // TestFixedDup2 tests Reported-by commit tag that comes for a dup.
   489  // Ensure that non-canonical bug gets fixing commit too.
   490  func TestFixedDup2(t *testing.T) {
   491  	c := NewCtx(t)
   492  	defer c.Close()
   493  
   494  	build1 := testBuild(1)
   495  	c.client.UploadBuild(build1)
   496  
   497  	build2 := testBuild(2)
   498  	c.client.UploadBuild(build2)
   499  
   500  	crash1 := testCrash(build1, 1)
   501  	c.client.ReportCrash(crash1)
   502  	rep1 := c.client.pollBug()
   503  
   504  	crash2 := testCrash(build1, 2)
   505  	c.client.ReportCrash(crash2)
   506  	rep2 := c.client.pollBug()
   507  
   508  	// rep2 is a dup of rep1.
   509  	c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
   510  
   511  	// Upload build that fixes rep2.
   512  	build1.FixCommits = []dashapi.Commit{
   513  		{Title: "fix commit 1", BugIDs: []string{rep2.ID}},
   514  	}
   515  	c.client.UploadBuild(build1)
   516  
   517  	// Now undup the bugs. They are still unfixed as only 1 manager uploaded the commit.
   518  	c.client.updateBug(rep2.ID, dashapi.BugStatusOpen, "")
   519  
   520  	// Now the second manager reports the same commits. This must close both bugs.
   521  	build2.FixCommits = build1.FixCommits
   522  	c.client.UploadBuild(build2)
   523  	c.client.pollBugs(0)
   524  
   525  	c.advanceTime(24 * time.Hour)
   526  	c.client.ReportCrash(crash1)
   527  	rep3 := c.client.pollBug()
   528  	c.expectEQ(rep3.Title, rep1.Title+" (2)")
   529  
   530  	c.client.ReportCrash(crash2)
   531  	rep4 := c.client.pollBug()
   532  	c.expectEQ(rep4.Title, rep2.Title+" (2)")
   533  }
   534  
   535  // TestFixedDup3 tests Reported-by commit tag that comes for both dup and canonical bug.
   536  func TestFixedDup3(t *testing.T) {
   537  	c := NewCtx(t)
   538  	defer c.Close()
   539  
   540  	build1 := testBuild(1)
   541  	c.client.UploadBuild(build1)
   542  
   543  	build2 := testBuild(2)
   544  	c.client.UploadBuild(build2)
   545  
   546  	crash1 := testCrash(build1, 1)
   547  	c.client.ReportCrash(crash1)
   548  	rep1 := c.client.pollBug()
   549  
   550  	crash2 := testCrash(build1, 2)
   551  	c.client.ReportCrash(crash2)
   552  	rep2 := c.client.pollBug()
   553  
   554  	// rep2 is a dup of rep1.
   555  	c.client.updateBug(rep2.ID, dashapi.BugStatusDup, rep1.ID)
   556  
   557  	// Upload builds that fix rep1 and rep2 with different commits.
   558  	// This must fix rep1 eventually and we must not livelock in such scenario.
   559  	build1.FixCommits = []dashapi.Commit{
   560  		{Title: "fix commit 1", BugIDs: []string{rep1.ID}},
   561  		{Title: "fix commit 2", BugIDs: []string{rep2.ID}},
   562  	}
   563  	build2.FixCommits = build1.FixCommits
   564  	c.client.UploadBuild(build1)
   565  	c.client.UploadBuild(build2)
   566  	c.client.UploadBuild(build1)
   567  	c.client.UploadBuild(build2)
   568  
   569  	c.client.ReportCrash(crash1)
   570  	rep3 := c.client.pollBug()
   571  	c.expectEQ(rep3.Title, rep1.Title+" (2)")
   572  }