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

     1  // Copyright 2023 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  	"fmt"
     8  	"reflect"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  	"testing"
    13  	"time"
    14  
    15  	"github.com/google/go-cmp/cmp"
    16  	"github.com/google/syzkaller/dashboard/dashapi"
    17  	"github.com/stretchr/testify/assert"
    18  	db "google.golang.org/appengine/v2/datastore"
    19  	aemail "google.golang.org/appengine/v2/mail"
    20  )
    21  
    22  func TestTreeOriginDownstream(t *testing.T) {
    23  	c := NewCtx(t)
    24  	defer c.Close()
    25  
    26  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
    27  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
    28  	ctx.entries = []treeTestEntry{
    29  		{
    30  			alias:   `downstream`,
    31  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
    32  		},
    33  		{
    34  			alias:      `lts`,
    35  			mergeAlias: `downstream`,
    36  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
    37  		},
    38  		{
    39  			alias:   `upstream`,
    40  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
    41  		},
    42  	}
    43  	ctx.reportToEmail()
    44  	ctx.jobTestDays = []int{10}
    45  	ctx.moveToDay(10)
    46  	ctx.ensureLabels(`origin:downstream`)
    47  	// It should habe been enough to run jobs just once.
    48  	c.expectEQ(ctx.entries[0].jobsDone, 1)
    49  	c.expectEQ(ctx.entries[1].jobsDone, 1)
    50  	c.expectEQ(ctx.entries[2].jobsDone, 1)
    51  	// Test that we can render the bug page.
    52  	_, err := c.GET(ctx.bugLink())
    53  	c.expectEQ(err, nil)
    54  	// Test that we receive a notification.
    55  	msg := ctx.emailWithoutURLs()
    56  	c.expectEQ(msg.Body, `Bug presence analysis results: the bug reproduces only on the downstream tree.
    57  
    58  syzbot has run the reproducer on other relevant kernel trees and got
    59  the following results:
    60  
    61  downstream (commit ffffffffffff) on 2000/01/11:
    62  crash title
    63  Report: %URL%
    64  
    65  lts (commit ffffffffffff) on 2000/01/11:
    66  Didn't crash.
    67  
    68  upstream (commit ffffffffffff) on 2000/01/11:
    69  Didn't crash.
    70  
    71  More details can be found at:
    72  %URL%
    73  `)
    74  	// Test that these results are also in the full bug info.
    75  	info := ctx.fullBugInfo()
    76  	c.expectEQ(len(info.TreeJobs), 3)
    77  	c.expectEQ(info.TreeJobs[0].KernelAlias, `downstream`)
    78  	c.expectNE(info.TreeJobs[0].CrashTitle, ``)
    79  	c.expectEQ(info.TreeJobs[1].KernelAlias, `lts`)
    80  	c.expectEQ(info.TreeJobs[1].CrashTitle, ``)
    81  	c.expectEQ(info.TreeJobs[2].KernelAlias, `upstream`)
    82  	c.expectEQ(info.TreeJobs[2].CrashTitle, ``)
    83  }
    84  
    85  func TestTreeOriginDownstreamEmail(t *testing.T) {
    86  	c := NewCtx(t)
    87  	defer c.Close()
    88  
    89  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
    90  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
    91  	ctx.entries = []treeTestEntry{
    92  		{
    93  			alias:   `downstream`,
    94  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
    95  		},
    96  		{
    97  			alias:      `lts`,
    98  			mergeAlias: `downstream`,
    99  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   100  		},
   101  		{
   102  			alias:   `upstream`,
   103  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   104  		},
   105  	}
   106  	ctx.jobTestDays = []int{10}
   107  	ctx.moveToDay(10)
   108  
   109  	// The report must contain the string.
   110  	msg := ctx.reportToEmail()
   111  	assert.Contains(t, msg.Body, `@testapp.appspotmail.com
   112  
   113  Bug presence analysis results: the bug reproduces only on the downstream tree.
   114  
   115  
   116  report1
   117  
   118  ---
   119  This report is generated by a bot. It may contain errors.`)
   120  	// No notification must be sent.
   121  	c.client.pollNotifs(0)
   122  }
   123  
   124  func TestTreeOriginBetterReport(t *testing.T) {
   125  	// Ensure that, once a higher priority crash becomes available, we perform origin testing again.
   126  	c := NewCtx(t)
   127  	defer c.Close()
   128  
   129  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   130  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   131  	ctx.entries = []treeTestEntry{
   132  		{
   133  			alias:   `downstream`,
   134  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   135  		},
   136  		{
   137  			alias:      `lts`,
   138  			mergeAlias: `downstream`,
   139  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   140  		},
   141  		{
   142  			alias:   `upstream`,
   143  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   144  		},
   145  	}
   146  	ctx.jobTestDays = []int{10, 20, 30}
   147  	ctx.moveToDay(10)
   148  	ctx.ensureLabels("origin:downstream")
   149  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   150  	c.expectEQ(ctx.entries[2].jobsDone, 1)
   151  
   152  	// No retets are needed yet.
   153  	ctx.moveToDay(20)
   154  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   155  	c.expectEQ(ctx.entries[2].jobsDone, 1)
   156  
   157  	// Use a "better" manager.
   158  	ctx.manager = "better-manager"
   159  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   160  
   161  	// With that reproducer, lts begins to crash as well.
   162  	ctx.entries[1].results = []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}
   163  	ctx.moveToDay(30)
   164  	ctx.ensureLabels("origin:lts")
   165  	c.expectEQ(ctx.entries[1].jobsDone, 2)
   166  	c.expectEQ(ctx.entries[2].jobsDone, 2)
   167  }
   168  
   169  func TestTreeOriginLts(t *testing.T) {
   170  	c := NewCtx(t)
   171  	defer c.Close()
   172  
   173  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   174  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   175  	ctx.entries = []treeTestEntry{
   176  		{
   177  			alias:   `downstream`,
   178  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   179  		},
   180  		{
   181  			alias:      `lts`,
   182  			mergeAlias: `downstream`,
   183  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   184  		},
   185  		{
   186  			alias:   `upstream`,
   187  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   188  		},
   189  	}
   190  	ctx.jobTestDays = []int{10}
   191  	ctx.moveToDay(10)
   192  	ctx.ensureLabels(`origin:lts`)
   193  	c.expectEQ(ctx.entries[0].jobsDone, 0)
   194  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   195  	c.expectEQ(ctx.entries[2].jobsDone, 1)
   196  	// Test that we don't receive any notification.
   197  	ctx.reportToEmail()
   198  	ctx.ctx.expectNoEmail()
   199  }
   200  
   201  // This function is very very big, but the required scenario is unfortunately
   202  // also very big, so:
   203  // nolint: funlen
   204  func TestTreeOriginLtsBisection(t *testing.T) {
   205  	c := NewCtx(t)
   206  	defer c.Close()
   207  
   208  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   209  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   210  	ctx.entries = []treeTestEntry{
   211  		{
   212  			alias:   `downstream`,
   213  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   214  		},
   215  		{
   216  			alias:      `lts`,
   217  			mergeAlias: `downstream`,
   218  			results: []treeTestEntryPeriod{
   219  				{
   220  					fromDay: 0,
   221  					result:  treeTestCrash,
   222  					commit:  "badc0ffee",
   223  				},
   224  			},
   225  		},
   226  		{
   227  			alias:   `upstream`,
   228  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   229  		},
   230  	}
   231  	ctx.jobTestDays = []int{10}
   232  	ctx.moveToDay(10)
   233  	ctx.ensureLabels(`origin:lts`)
   234  	ctx.reportToEmail()
   235  	ctx.ctx.advanceTime(time.Hour)
   236  
   237  	// Expect a cross tree bisection request.
   238  	job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   239  	assert.Equal(t, dashapi.JobBisectFix, job.Type)
   240  	assert.Equal(t, "https://upstream.repo/repo", job.KernelRepo)
   241  	assert.Equal(t, "upstream-master", job.KernelBranch)
   242  	assert.Equal(t, "https://lts.repo/repo", job.MergeBaseRepo)
   243  	assert.Equal(t, "lts-master", job.MergeBaseBranch)
   244  	assert.Equal(t, "badc0ffee", job.KernelCommit)
   245  	ctx.ctx.advanceTime(time.Hour)
   246  
   247  	// Make sure we don't create the same job twice.
   248  	job2 := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   249  	assert.Equal(t, "", job2.ID)
   250  	ctx.ctx.advanceTime(time.Hour)
   251  
   252  	// Let the bisection fail.
   253  	done := &dashapi.JobDoneReq{
   254  		ID:    job.ID,
   255  		Log:   []byte("bisect log"),
   256  		Error: []byte("bisect error"),
   257  	}
   258  	c.expectOK(ctx.client.JobDone(done))
   259  	ctx.ctx.advanceTime(time.Hour)
   260  
   261  	// Ensure there are no new bisection requests.
   262  	job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   263  	assert.Equal(t, job.ID, "")
   264  
   265  	// Wait for the cooldown and request the job once more.
   266  	ctx.ctx.advanceTime(15 * 24 * time.Hour)
   267  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   268  	ctx.ctx.advanceTime(15 * 24 * time.Hour)
   269  	job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   270  	assert.Equal(t, job.KernelRepo, "https://upstream.repo/repo")
   271  	assert.Equal(t, job.KernelCommit, "badc0ffee")
   272  
   273  	// This time pretend we have found the commit.
   274  	build := testBuild(2)
   275  	build.KernelRepo = job.KernelRepo
   276  	build.KernelBranch = job.KernelBranch
   277  	build.KernelCommit = "deadf00d"
   278  	done = &dashapi.JobDoneReq{
   279  		ID:          job.ID,
   280  		Build:       *build,
   281  		Log:         []byte("bisect log 2"),
   282  		CrashTitle:  "bisect crash title",
   283  		CrashLog:    []byte("bisect crash log"),
   284  		CrashReport: []byte("bisect crash report"),
   285  		Commits: []dashapi.Commit{
   286  			{
   287  				AuthorName: "Someone",
   288  				Author:     "someone@somewhere.com",
   289  				Hash:       "deadf00d",
   290  				Title:      "kernel: fix a bug",
   291  				Date:       time.Date(2000, 2, 9, 4, 5, 6, 7, time.UTC),
   292  			},
   293  		},
   294  	}
   295  	done.Build.ID = job.ID
   296  	ctx.ctx.advanceTime(time.Hour)
   297  	c.expectOK(ctx.client.JobDone(done))
   298  
   299  	// Ensure the job is no longer created.
   300  	ctx.ctx.advanceTime(time.Hour)
   301  	job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   302  	assert.Equal(t, job.ID, "")
   303  
   304  	msg := ctx.emailWithoutURLs()
   305  	c.expectEQ(msg.Body, `syzbot suspects this issue could be fixed by backporting the following commit:
   306  
   307  commit deadf00d
   308  git tree: upstream
   309  Author: Someone <someone@somewhere.com>
   310  Date:   Wed Feb 9 04:05:06 2000 +0000
   311  
   312      kernel: fix a bug
   313  
   314  bisection log:  %URL%
   315  final oops:     %URL%
   316  console output: %URL%
   317  kernel config:  %URL%
   318  dashboard link: %URL%
   319  syz repro:      %URL%
   320  C reproducer:   %URL%
   321  
   322  
   323  Please keep in mind that other backports might be required as well.
   324  
   325  For information about bisection process see: %URL%#bisection
   326  `)
   327  	ctx.ctx.expectNoEmail()
   328  
   329  	info := ctx.fullBugInfo()
   330  	assert.NotNil(t, info.FixCandidate)
   331  	fix := info.FixCandidate
   332  	assert.Equal(t, "upstream", fix.KernelRepoAlias)
   333  	assert.NotNil(t, fix.BisectFix)
   334  	assert.NotNil(t, fix.BisectFix.Commit)
   335  	commit := fix.BisectFix.Commit
   336  	assert.Equal(t, "deadf00d", commit.Hash)
   337  	assert.Equal(t, "kernel: fix a bug", commit.Title)
   338  
   339  	// Ensure the bug is not automatically closed.
   340  	bug := ctx.loadBug()
   341  	assert.Len(t, bug.Commits, 0)
   342  
   343  	// Ensure the bug is present on the backports list page.
   344  	reply, err := ctx.ctx.AuthGET(AccessAdmin, "/tree-tests/backports")
   345  	c.expectOK(err)
   346  	assert.Contains(t, string(reply), treeTestCrashTitle)
   347  	assert.Contains(t, string(reply), "deadf00d")
   348  
   349  	// But don't show this to all users.
   350  	reply, err = ctx.ctx.AuthGET(AccessPublic, "/tree-tests/backports")
   351  	c.expectOK(err)
   352  	assert.NotContains(t, string(reply), treeTestCrashTitle)
   353  
   354  	// Check that we display it in another related namespace.
   355  	upstreamBuild := testBuild(100)
   356  	upstreamBuild.KernelRepo = "https://upstream.repo/repo"
   357  	upstreamBuild.KernelBranch = "upstream-master"
   358  	ctx.ctx.publicClient.UploadBuild(upstreamBuild)
   359  	reply, err = ctx.ctx.AuthGET(AccessAdmin, "/access-public-email/backports")
   360  	c.expectOK(err)
   361  	assert.Contains(t, string(reply), treeTestCrashTitle)
   362  
   363  	// .. but, again, not to everyone.
   364  	reply, err = ctx.ctx.AuthGET(AccessPublic, "/access-public-email/backports")
   365  	c.expectOK(err)
   366  	assert.NotContains(t, string(reply), treeTestCrashTitle)
   367  
   368  	// The bug must appear in commit poll.
   369  	commitPollResp, err := ctx.client.CommitPoll()
   370  	c.expectOK(err)
   371  	assert.Contains(t, commitPollResp.Commits, "kernel: fix a bug")
   372  
   373  	// Pretend that we have found a commit.
   374  	c.expectOK(ctx.client.UploadCommits([]dashapi.Commit{
   375  		{
   376  			Hash:       "newhash",
   377  			Title:      "kernel: fix a bug",
   378  			AuthorName: "Someone",
   379  			Author:     "someone@somewhere.com",
   380  			Date:       time.Date(2000, 3, 4, 5, 6, 7, 8, time.UTC),
   381  		},
   382  	}))
   383  
   384  	// An email must be sent.
   385  	msg = ctx.emailWithoutURLs()
   386  	fmt.Printf("%s", msg)
   387  	c.expectEQ(msg.Body, `The commit that was suspected to fix the issue was backported to the fuzzed
   388  kernel trees.
   389  
   390  commit newhash
   391  Author: Someone <someone@somewhere.com>
   392  Date:   Sat Mar 4 05:06:07 2000 +0000
   393  
   394      kernel: fix a bug
   395  
   396  If you believe this is correct, please reply with
   397  #syz fix: kernel: fix a bug
   398  
   399  The commit was initially detected here:
   400  
   401  commit deadf00d
   402  git tree: upstream
   403  Author: Someone <someone@somewhere.com>
   404  Date:   Wed Feb 9 04:05:06 2000 +0000
   405  
   406      kernel: fix a bug
   407  
   408  bisection log:  %URL%
   409  final oops:     %URL%
   410  console output: %URL%
   411  kernel config:  %URL%
   412  dashboard link: %URL%
   413  syz repro:      %URL%
   414  C reproducer:   %URL%
   415  `)
   416  	// Only one email.
   417  	ctx.ctx.expectNoEmail()
   418  
   419  	// The commit should disappear from the missing backports list.
   420  	reply, err = ctx.ctx.AuthGET(AccessAdmin, "/tree-tests/backports")
   421  	c.expectOK(err)
   422  	assert.NotContains(t, string(reply), treeTestCrashTitle)
   423  	assert.NotContains(t, string(reply), "deadf00d")
   424  }
   425  
   426  func TestNonfinalFixCandidateBisect(t *testing.T) {
   427  	c := NewCtx(t)
   428  	defer c.Close()
   429  
   430  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   431  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   432  	ctx.entries = []treeTestEntry{
   433  		{
   434  			alias:   `downstream`,
   435  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   436  		},
   437  		{
   438  			alias:      `lts`,
   439  			mergeAlias: `downstream`,
   440  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   441  		},
   442  		{
   443  			alias: `upstream`,
   444  			// Ignore these jobs.
   445  			results: []treeTestEntryPeriod{},
   446  		},
   447  	}
   448  	ctx.jobTestDays = []int{10}
   449  	ctx.moveToDay(10)
   450  	ctx.reportToEmail()
   451  	ctx.ctx.advanceTime(time.Hour)
   452  
   453  	// Ensure the code does not fail.
   454  	job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   455  	assert.Equal(t, "", job.ID)
   456  }
   457  
   458  func TestTreeBisectionBeforeOrigin(t *testing.T) {
   459  	c := NewCtx(t)
   460  	defer c.Close()
   461  
   462  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   463  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   464  	ctx.reportToEmail()
   465  	// Ensure the job is no longer created.
   466  	ctx.ctx.advanceTime(time.Hour)
   467  	job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true})
   468  	assert.Equal(t, "", job.ID)
   469  }
   470  
   471  func TestTreeOriginErrors(t *testing.T) {
   472  	c := NewCtx(t)
   473  	defer c.Close()
   474  
   475  	// Make sure testing works fine despite patch testing errors.
   476  	ctx := setUpTreeTest(c, downstreamUpstreamRepos)
   477  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   478  	ctx.entries = []treeTestEntry{
   479  		{
   480  			alias: `downstream`,
   481  			results: []treeTestEntryPeriod{
   482  				{fromDay: 0, result: treeTestCrash},
   483  			},
   484  		},
   485  		{
   486  			alias:      `lts`,
   487  			mergeAlias: `downstream`,
   488  			results: []treeTestEntryPeriod{
   489  				{fromDay: 0, result: treeTestError},
   490  				{fromDay: 16, result: treeTestCrash},
   491  			},
   492  		},
   493  		{
   494  			alias: `upstream`,
   495  			results: []treeTestEntryPeriod{
   496  				{fromDay: 0, result: treeTestError},
   497  				{fromDay: 31, result: treeTestCrash},
   498  			},
   499  		},
   500  	}
   501  	ctx.jobTestDays = []int{1, 16, 31}
   502  	ctx.moveToDay(1)
   503  	ctx.ensureLabels() // Not enough information yet.
   504  	// Lts got unbroken.
   505  	ctx.moveToDay(16)
   506  	ctx.ensureLabels(`origin:lts`) // We don't know any better so far.
   507  	// Upstream got unbroken.
   508  	ctx.moveToDay(31)
   509  	ctx.ensureLabels(`origin:upstream`)
   510  	c.expectEQ(ctx.entries[0].jobsDone, 0)
   511  	c.expectEQ(ctx.entries[1].jobsDone, 2)
   512  	c.expectEQ(ctx.entries[2].jobsDone, 3)
   513  }
   514  
   515  var downstreamUpstreamRepos = []KernelRepo{
   516  	{
   517  		URL:             `https://downstream.repo/repo`,
   518  		Branch:          `master`,
   519  		Alias:           `downstream`,
   520  		LabelIntroduced: `downstream`,
   521  		CommitInflow: []KernelRepoLink{
   522  			{
   523  				Alias: `upstream`,
   524  			},
   525  			{
   526  				Alias: `lts`,
   527  				Merge: true,
   528  			},
   529  		},
   530  	},
   531  	{
   532  		URL:             `https://lts.repo/repo`,
   533  		Branch:          `lts-master`,
   534  		Alias:           `lts`,
   535  		LabelIntroduced: `lts`,
   536  		CommitInflow: []KernelRepoLink{
   537  			{
   538  				Alias:       `upstream`,
   539  				Merge:       false,
   540  				BisectFixes: true,
   541  			},
   542  		},
   543  	},
   544  	{
   545  		URL:             `https://upstream.repo/repo`,
   546  		Branch:          `upstream-master`,
   547  		Alias:           `upstream`,
   548  		LabelIntroduced: `upstream`,
   549  	},
   550  }
   551  
   552  func TestOriginTreeNoMergeLts(t *testing.T) {
   553  	c := NewCtx(t)
   554  	defer c.Close()
   555  
   556  	ctx := setUpTreeTest(c, ltsUpstreamRepos)
   557  	ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC)
   558  	ctx.entries = []treeTestEntry{
   559  		{
   560  			alias:   `lts`,
   561  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   562  		},
   563  		{
   564  			alias:   `upstream`,
   565  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   566  		},
   567  	}
   568  	ctx.jobTestDays = []int{10}
   569  	ctx.moveToDay(10)
   570  	ctx.ensureLabels(`origin:lts-only`)
   571  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   572  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   573  }
   574  
   575  func TestOriginTreeNoMergeNoLabel(t *testing.T) {
   576  	c := NewCtx(t)
   577  	defer c.Close()
   578  
   579  	ctx := setUpTreeTest(c, ltsUpstreamRepos)
   580  	ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC)
   581  	ctx.entries = []treeTestEntry{
   582  		{
   583  			alias:   `lts`,
   584  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   585  		},
   586  		{
   587  			alias:   `upstream`,
   588  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   589  		},
   590  	}
   591  	ctx.jobTestDays = []int{10}
   592  	ctx.moveToDay(10)
   593  	ctx.ensureLabels()
   594  	// It should habe been enough to run jobs just once.
   595  	c.expectEQ(ctx.entries[0].jobsDone, 0)
   596  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   597  }
   598  
   599  func TestTreeOriginRepoChanged(t *testing.T) {
   600  	c := NewCtx(t)
   601  	defer c.Close()
   602  
   603  	ctx := setUpTreeTest(c, ltsUpstreamRepos)
   604  
   605  	// First do tests from one repository.
   606  	ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC)
   607  	ctx.entries = []treeTestEntry{
   608  		{
   609  			alias:   `lts`,
   610  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   611  		},
   612  		{
   613  			alias:   `upstream`,
   614  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   615  		},
   616  	}
   617  	ctx.jobTestDays = []int{10, 20, 25, 30, 62}
   618  	ctx.moveToDay(10)
   619  	ctx.ensureLabels(`origin:lts-only`)
   620  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   621  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   622  
   623  	// Now update the repository.
   624  	ctx.updateRepos([]KernelRepo{
   625  		{
   626  			URL:               `https://new-lts.repo/repo`,
   627  			Branch:            `lts-master`,
   628  			Alias:             `lts`,
   629  			LabelIntroduced:   `lts-only`,
   630  			ReportingPriority: 9,
   631  			CommitInflow: []KernelRepoLink{
   632  				{
   633  					Alias: `upstream`,
   634  					Merge: false,
   635  				},
   636  			},
   637  		},
   638  		{
   639  			URL:    `https://upstream.repo/repo`,
   640  			Branch: `upstream-master`,
   641  			Alias:  `upstream`,
   642  		},
   643  	})
   644  	ctx.entries = []treeTestEntry{
   645  		{
   646  			alias: `lts`,
   647  			results: []treeTestEntryPeriod{
   648  				{fromDay: 30, result: treeTestError},
   649  				{fromDay: 60, result: treeTestCrash},
   650  			},
   651  		},
   652  		{
   653  			alias:   `upstream`,
   654  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   655  		},
   656  	}
   657  	ctx.moveToDay(20)
   658  	ctx.ensureLabels(`origin:lts-only`) // No new builds -- nothing we can do.
   659  
   660  	// Upload a new manager build.
   661  	build := ctx.uploadBuild(`https://new-lts.repo/repo`, `lts-master`)
   662  	ctx.moveToDay(25)
   663  	ctx.ensureLabels(`origin:lts-only`) // Still nothing we can do, no crashes so far.
   664  
   665  	// Now upload a new crash.
   666  	ctx.uploadBuildCrash(build, dashapi.ReproLevelC)
   667  	ctx.moveToDay(30)
   668  	ctx.ensureLabels() // We are no longer sure about tags.
   669  
   670  	// After the new tree starts to build again, we can calculate the results again.
   671  	ctx.moveToDay(62)
   672  	ctx.ensureLabels(`origin:lts-only`) // We are no longer sure about tags.
   673  	c.expectEQ(ctx.entries[0].jobsDone, 2)
   674  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   675  }
   676  
   677  var ltsUpstreamRepos = []KernelRepo{
   678  	{
   679  		URL:             `https://lts.repo/repo`,
   680  		Branch:          `lts-master`,
   681  		Alias:           `lts`,
   682  		LabelIntroduced: `lts-only`,
   683  		CommitInflow: []KernelRepoLink{
   684  			{
   685  				Alias: `upstream`,
   686  				Merge: false,
   687  			},
   688  		},
   689  	},
   690  	{
   691  		URL:    `https://upstream.repo/repo`,
   692  		Branch: `upstream-master`,
   693  		Alias:  `upstream`,
   694  	},
   695  }
   696  
   697  func TestOriginNoNextTree(t *testing.T) {
   698  	c := NewCtx(t)
   699  	defer c.Close()
   700  
   701  	ctx := setUpTreeTest(c, upstreamNextRepos)
   702  	ctx.uploadBug(`https://upstream.repo/repo`, `upstream-master`, dashapi.ReproLevelC)
   703  	ctx.entries = []treeTestEntry{}
   704  	ctx.jobTestDays = []int{10}
   705  	ctx.moveToDay(10)
   706  	ctx.ensureLabels()
   707  }
   708  
   709  func TestOriginNoNextFixed(t *testing.T) {
   710  	c := NewCtx(t)
   711  	defer c.Close()
   712  
   713  	ctx := setUpTreeTest(c, upstreamNextRepos)
   714  	ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC)
   715  	ctx.entries = []treeTestEntry{
   716  		{
   717  			alias:   `next`,
   718  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   719  		},
   720  		{
   721  			alias:   `upstream`,
   722  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   723  		},
   724  	}
   725  	ctx.jobTestDays = []int{10}
   726  	ctx.moveToDay(10)
   727  	ctx.ensureLabels()
   728  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   729  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   730  }
   731  
   732  func TestOriginNoNext(t *testing.T) {
   733  	c := NewCtx(t)
   734  	defer c.Close()
   735  
   736  	ctx := setUpTreeTest(c, upstreamNextRepos)
   737  	ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC)
   738  	ctx.entries = []treeTestEntry{
   739  		{
   740  			alias:   `next`,
   741  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   742  		},
   743  		{
   744  			alias:   `upstream`,
   745  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   746  		},
   747  	}
   748  	ctx.jobTestDays = []int{10}
   749  	ctx.moveToDay(10)
   750  	ctx.ensureLabels()
   751  	c.expectEQ(ctx.entries[0].jobsDone, 0)
   752  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   753  }
   754  
   755  func TestOriginNext(t *testing.T) {
   756  	c := NewCtx(t)
   757  	defer c.Close()
   758  
   759  	ctx := setUpTreeTest(c, upstreamNextRepos)
   760  	ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC)
   761  	ctx.entries = []treeTestEntry{
   762  		{
   763  			alias:   `next`,
   764  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   765  		},
   766  		{
   767  			alias:   `upstream`,
   768  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}},
   769  		},
   770  	}
   771  	ctx.jobTestDays = []int{10}
   772  	ctx.moveToDay(10)
   773  	ctx.ensureLabels(`origin:next`)
   774  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   775  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   776  }
   777  
   778  var upstreamNextRepos = []KernelRepo{
   779  	{
   780  		URL:    `https://upstream.repo/repo`,
   781  		Branch: `upstream-master`,
   782  		Alias:  `upstream`,
   783  		CommitInflow: []KernelRepoLink{
   784  			{
   785  				Alias: `next`,
   786  				Merge: false,
   787  			},
   788  		},
   789  	},
   790  	{
   791  		URL:          `https://next.repo/repo`,
   792  		Branch:       `next-master`,
   793  		Alias:        `next`,
   794  		LabelReached: `next`,
   795  	},
   796  }
   797  
   798  func TestMissingLtsBackport(t *testing.T) {
   799  	c := NewCtx(t)
   800  	defer c.Close()
   801  
   802  	ctx := setUpTreeTest(c, downstreamUpstreamBackports)
   803  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   804  	ctx.entries = []treeTestEntry{
   805  		{
   806  			alias: `downstream`,
   807  			results: []treeTestEntryPeriod{
   808  				{fromDay: 0, result: treeTestCrash},
   809  			},
   810  		},
   811  		{
   812  			alias:      `lts`,
   813  			mergeAlias: `downstream`,
   814  			results: []treeTestEntryPeriod{
   815  				{fromDay: 0, result: treeTestCrash},
   816  			},
   817  		},
   818  		{
   819  			alias: `lts`,
   820  			results: []treeTestEntryPeriod{
   821  				{fromDay: 0, result: treeTestCrash},
   822  				{fromDay: 46, result: treeTestOK},
   823  			},
   824  		},
   825  		{
   826  			alias: `upstream`,
   827  			results: []treeTestEntryPeriod{
   828  				{fromDay: 0, result: treeTestCrash},
   829  			},
   830  		},
   831  	}
   832  	ctx.jobTestDays = []int{0, 46}
   833  	ctx.moveToDay(46)
   834  	ctx.ensureLabels(`missing-backport`)
   835  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   836  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   837  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   838  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   839  }
   840  
   841  func TestMissingUpstreamBackport(t *testing.T) {
   842  	c := NewCtx(t)
   843  	defer c.Close()
   844  
   845  	ctx := setUpTreeTest(c, downstreamUpstreamBackports)
   846  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   847  	ctx.entries = []treeTestEntry{
   848  		{
   849  			alias: `downstream`,
   850  			results: []treeTestEntryPeriod{
   851  				{fromDay: 0, result: treeTestCrash},
   852  			},
   853  		},
   854  		{
   855  			alias: `lts`,
   856  			results: []treeTestEntryPeriod{
   857  				{fromDay: 0, result: treeTestCrash},
   858  			},
   859  		},
   860  		{
   861  			alias: `upstream`,
   862  			results: []treeTestEntryPeriod{
   863  				{fromDay: 0, result: treeTestCrash},
   864  				{fromDay: 31, result: treeTestOK},
   865  			},
   866  		},
   867  	}
   868  	ctx.jobTestDays = []int{0, 46}
   869  	ctx.moveToDay(46)
   870  	ctx.ensureLabels(`missing-backport`)
   871  	c.expectEQ(ctx.entries[0].jobsDone, 1)
   872  	c.expectEQ(ctx.entries[1].jobsDone, 2)
   873  	c.expectEQ(ctx.entries[1].jobsDone, 2)
   874  }
   875  
   876  func TestNotMissingBackport(t *testing.T) {
   877  	c := NewCtx(t)
   878  	defer c.Close()
   879  
   880  	ctx := setUpTreeTest(c, downstreamUpstreamBackports)
   881  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   882  	ctx.entries = []treeTestEntry{
   883  		{
   884  			alias: `downstream`,
   885  			results: []treeTestEntryPeriod{
   886  				{fromDay: 0, result: treeTestCrash},
   887  			},
   888  		},
   889  		{
   890  			alias:      `lts`,
   891  			mergeAlias: `downstream`,
   892  			results: []treeTestEntryPeriod{
   893  				{fromDay: 0, result: treeTestOK},
   894  			},
   895  		},
   896  		{
   897  			alias: `lts`,
   898  			results: []treeTestEntryPeriod{
   899  				{fromDay: 0, result: treeTestOK},
   900  			},
   901  		},
   902  		{
   903  			alias: `upstream`,
   904  			results: []treeTestEntryPeriod{
   905  				{fromDay: 0, result: treeTestCrash},
   906  			},
   907  		},
   908  	}
   909  	ctx.jobTestDays = []int{0, 46}
   910  	ctx.moveToDay(46)
   911  	ctx.ensureLabels()
   912  	c.expectEQ(ctx.entries[0].jobsDone, 0)
   913  	c.expectEQ(ctx.entries[1].jobsDone, 1)
   914  	c.expectEQ(ctx.entries[2].jobsDone, 1)
   915  	c.expectEQ(ctx.entries[3].jobsDone, 2)
   916  }
   917  
   918  var downstreamUpstreamBackports = []KernelRepo{
   919  	{
   920  		URL:    `https://downstream.repo/repo`,
   921  		Branch: `master`,
   922  		Alias:  `downstream`,
   923  		CommitInflow: []KernelRepoLink{
   924  			{
   925  				Alias: `lts`,
   926  				Merge: true,
   927  			},
   928  			{
   929  				Alias: `upstream`,
   930  			},
   931  		},
   932  		DetectMissingBackports: true,
   933  	},
   934  	{
   935  		URL:    `https://lts.repo/repo`,
   936  		Branch: `lts-master`,
   937  		Alias:  `lts`,
   938  		CommitInflow: []KernelRepoLink{
   939  			{
   940  				Alias: `upstream`,
   941  				Merge: false,
   942  			},
   943  		},
   944  	},
   945  	{
   946  		URL:    `https://upstream.repo/repo`,
   947  		Branch: `upstream-master`,
   948  		Alias:  `upstream`,
   949  	},
   950  }
   951  
   952  func TestTreeConfigAppend(t *testing.T) {
   953  	c := NewCtx(t)
   954  	defer c.Close()
   955  
   956  	ctx := setUpTreeTest(c, []KernelRepo{
   957  		{
   958  			URL:    `https://downstream.repo/repo`,
   959  			Branch: `master`,
   960  			Alias:  `downstream`,
   961  			CommitInflow: []KernelRepoLink{
   962  				{
   963  					Alias: `lts`,
   964  					Merge: true,
   965  				},
   966  			},
   967  			LabelIntroduced: `downstream`,
   968  		},
   969  		{
   970  			URL:             `https://lts.repo/repo`,
   971  			Branch:          `lts-master`,
   972  			Alias:           `lts`,
   973  			LabelIntroduced: `lts`,
   974  			AppendConfig:    "\nCONFIG_TEST=y",
   975  		},
   976  	})
   977  	ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC)
   978  	ctx.entries = []treeTestEntry{
   979  		{
   980  			alias:   `downstream`,
   981  			results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   982  		},
   983  		{
   984  			alias:      `lts`,
   985  			mergeAlias: `downstream`,
   986  			results:    []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}},
   987  		},
   988  	}
   989  	ctx.jobTestDays = []int{10}
   990  	tested := false
   991  	ctx.validateJob = func(resp *dashapi.JobPollResp) {
   992  		if resp.KernelBranch == "lts-master" {
   993  			tested = true
   994  			assert.Contains(t, string(resp.KernelConfig), "\nCONFIG_TEST=y")
   995  		}
   996  	}
   997  	ctx.moveToDay(10)
   998  	assert.True(t, tested)
   999  }
  1000  
  1001  func setUpTreeTest(ctx *Ctx, repos []KernelRepo) *treeTestCtx {
  1002  	ret := &treeTestCtx{
  1003  		ctx:     ctx,
  1004  		client:  ctx.makeClient(clientTreeTests, keyTreeTests, true),
  1005  		manager: "test-manager",
  1006  	}
  1007  	ret.updateRepos(repos)
  1008  	return ret
  1009  }
  1010  
  1011  type treeTestCtx struct {
  1012  	ctx         *Ctx
  1013  	client      *apiClient
  1014  	bug         *Bug
  1015  	bugReport   *dashapi.BugReport
  1016  	start       time.Time
  1017  	entries     []treeTestEntry
  1018  	perAlias    map[string]KernelRepo
  1019  	jobTestDays []int
  1020  	manager     string
  1021  	validateJob func(*dashapi.JobPollResp)
  1022  }
  1023  
  1024  func (ctx *treeTestCtx) now() time.Time {
  1025  	// Yep, that's a bit too much repetition.
  1026  	return timeNow(ctx.ctx.ctx)
  1027  }
  1028  
  1029  func (ctx *treeTestCtx) updateRepos(repos []KernelRepo) {
  1030  	checkKernelRepos("tree-tests", ctx.ctx.config().Namespaces["tree-tests"], repos)
  1031  	ctx.perAlias = map[string]KernelRepo{}
  1032  	for _, repo := range repos {
  1033  		ctx.perAlias[repo.Alias] = repo
  1034  	}
  1035  	ctx.ctx.setKernelRepos("tree-tests", repos)
  1036  }
  1037  
  1038  func (ctx *treeTestCtx) uploadBuild(repo, branch string) *dashapi.Build {
  1039  	build := testBuild(1)
  1040  	build.ID = fmt.Sprintf("%d", ctx.now().Unix())
  1041  	build.Manager = ctx.manager
  1042  	build.KernelRepo = repo
  1043  	build.KernelBranch = branch
  1044  	build.KernelCommit = build.ID
  1045  	ctx.client.UploadBuild(build)
  1046  	return build
  1047  }
  1048  
  1049  const treeTestCrashTitle = "cross-tree bug title"
  1050  
  1051  func (ctx *treeTestCtx) uploadBuildCrash(build *dashapi.Build, lvl dashapi.ReproLevel) {
  1052  	crash := testCrash(build, 1)
  1053  	crash.Title = treeTestCrashTitle
  1054  	if lvl > dashapi.ReproLevelNone {
  1055  		crash.ReproSyz = []byte("getpid()")
  1056  	}
  1057  	if lvl == dashapi.ReproLevelC {
  1058  		crash.ReproC = []byte("getpid()")
  1059  	}
  1060  	ctx.client.ReportCrash(crash)
  1061  	if ctx.bug == nil || ctx.bug.ReproLevel < lvl {
  1062  		ctx.bugReport = ctx.client.pollBug()
  1063  		if ctx.bug == nil {
  1064  			bug, _, err := findBugByReportingID(ctx.ctx.ctx, ctx.bugReport.ID)
  1065  			ctx.ctx.expectOK(err)
  1066  			ctx.bug = bug
  1067  		}
  1068  	}
  1069  }
  1070  
  1071  func (ctx *treeTestCtx) uploadBug(repo, branch string, lvl dashapi.ReproLevel) {
  1072  	build := ctx.uploadBuild(repo, branch)
  1073  	ctx.uploadBuildCrash(build, lvl)
  1074  }
  1075  
  1076  func (ctx *treeTestCtx) moveToDay(tillDay int) {
  1077  	ctx.ctx.t.Helper()
  1078  	if ctx.start.IsZero() {
  1079  		ctx.start = ctx.now()
  1080  	}
  1081  	for _, seqDay := range ctx.jobTestDays {
  1082  		if seqDay > tillDay {
  1083  			break
  1084  		}
  1085  		now := ctx.now()
  1086  		day := ctx.start.Add(time.Hour * 24 * time.Duration(seqDay))
  1087  		if day.Before(now) || ctx.start != ctx.now() && day.Equal(now) {
  1088  			continue
  1089  		}
  1090  		ctx.ctx.advanceTime(day.Sub(now))
  1091  		ctx.ctx.t.Logf("executing jobs on day %d", seqDay)
  1092  		// Execute jobs until they exist.
  1093  		for {
  1094  			pollResp := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{
  1095  				TestPatches: true,
  1096  			})
  1097  			if pollResp.ID == "" {
  1098  				break
  1099  			}
  1100  			if ctx.validateJob != nil {
  1101  				ctx.validateJob(pollResp)
  1102  			}
  1103  			ctx.ctx.advanceTime(time.Minute)
  1104  			ctx.doJob(pollResp, seqDay)
  1105  		}
  1106  	}
  1107  }
  1108  
  1109  func (ctx *treeTestCtx) doJob(resp *dashapi.JobPollResp, day int) {
  1110  	respValues := []string{
  1111  		resp.KernelRepo,
  1112  		resp.KernelBranch,
  1113  		resp.MergeBaseRepo,
  1114  		resp.MergeBaseBranch,
  1115  	}
  1116  	sort.Strings(respValues)
  1117  	var found *treeTestEntry
  1118  	for i, entry := range ctx.entries {
  1119  		entryValues := []string{
  1120  			ctx.perAlias[entry.alias].URL,
  1121  			ctx.perAlias[entry.alias].Branch,
  1122  		}
  1123  		if entry.mergeAlias != "" {
  1124  			entryValues = append(entryValues,
  1125  				ctx.perAlias[entry.mergeAlias].URL,
  1126  				ctx.perAlias[entry.mergeAlias].Branch)
  1127  		} else {
  1128  			entryValues = append(entryValues, "", "")
  1129  		}
  1130  		sort.Strings(entryValues)
  1131  		if reflect.DeepEqual(respValues, entryValues) {
  1132  			found = &ctx.entries[i]
  1133  			break
  1134  		}
  1135  	}
  1136  	if found == nil {
  1137  		ctx.ctx.t.Fatalf("unknown job request: %#v", resp)
  1138  		return // to avoid staticcheck false positive about nil deref
  1139  	}
  1140  	// Figure out what should the result be.
  1141  	result := treeTestOK
  1142  	build := testBuild(1)
  1143  	var anyFound bool
  1144  	for _, item := range found.results {
  1145  		if day >= item.fromDay {
  1146  			result = item.result
  1147  			build.KernelCommit = item.commit
  1148  			anyFound = true
  1149  		}
  1150  	}
  1151  	if !anyFound {
  1152  		// Just ignore the job.
  1153  		return
  1154  	}
  1155  	if build.KernelCommit == "" {
  1156  		build.KernelCommit = strings.Repeat("f", 40)[:40]
  1157  	}
  1158  	build.KernelRepo = resp.KernelRepo
  1159  	build.KernelBranch = resp.KernelBranch
  1160  	build.ID = fmt.Sprintf("%s_%s_%s_%d", resp.KernelRepo, resp.KernelBranch, resp.KernelCommit, day)
  1161  	jobDoneReq := &dashapi.JobDoneReq{
  1162  		ID:    resp.ID,
  1163  		Build: *build,
  1164  	}
  1165  	switch result {
  1166  	case treeTestOK:
  1167  	case treeTestCrash:
  1168  		jobDoneReq.CrashTitle = "crash title"
  1169  		jobDoneReq.CrashLog = []byte("test crash log")
  1170  		jobDoneReq.CrashReport = []byte("test crash report")
  1171  	case treeTestError:
  1172  		jobDoneReq.Error = []byte("failed to apply patch")
  1173  	}
  1174  	found.jobsDone++
  1175  	ctx.ctx.expectOK(ctx.client.JobDone(jobDoneReq))
  1176  }
  1177  
  1178  func (ctx *treeTestCtx) ensureLabels(labels ...string) {
  1179  	ctx.ctx.t.Helper()
  1180  	bug := ctx.loadBug()
  1181  	var bugLabels []string
  1182  	for _, item := range bug.Labels {
  1183  		bugLabels = append(bugLabels, item.String())
  1184  	}
  1185  	assert.ElementsMatch(ctx.ctx.t, labels, bugLabels)
  1186  }
  1187  
  1188  func (ctx *treeTestCtx) loadBug() *Bug {
  1189  	ctx.ctx.t.Helper()
  1190  	if ctx.bug == nil {
  1191  		ctx.ctx.t.Fatalf("no bug has been created so far")
  1192  	}
  1193  	bug := new(Bug)
  1194  	ctx.ctx.expectOK(db.Get(ctx.ctx.ctx, ctx.bug.key(ctx.ctx.ctx), bug))
  1195  	ctx.bug = bug
  1196  	return bug
  1197  }
  1198  
  1199  func (ctx *treeTestCtx) bugLink() string {
  1200  	return fmt.Sprintf("/bug?id=%v", ctx.bug.key(ctx.ctx.ctx).StringID())
  1201  }
  1202  
  1203  func (ctx *treeTestCtx) reportToEmail() *aemail.Message {
  1204  	ctx.client.updateBug(ctx.bugReport.ID, dashapi.BugStatusUpstream, "")
  1205  	return ctx.ctx.pollEmailBug()
  1206  }
  1207  
  1208  func (ctx *treeTestCtx) fullBugInfo() *dashapi.FullBugInfo {
  1209  	info, err := ctx.client.LoadFullBug(&dashapi.LoadFullBugReq{
  1210  		BugID: ctx.bugReport.ID,
  1211  	})
  1212  	ctx.ctx.expectOK(err)
  1213  	return info
  1214  }
  1215  
  1216  var urlRe = regexp.MustCompile(`(https?://[\w\./\?\=&]+)`)
  1217  
  1218  func (ctx *treeTestCtx) emailWithoutURLs() *aemail.Message {
  1219  	msg := ctx.ctx.pollEmailBug()
  1220  	msg.Body = urlRe.ReplaceAllString(msg.Body, "%URL%")
  1221  	return msg
  1222  }
  1223  
  1224  type treeTestEntry struct {
  1225  	alias      string
  1226  	mergeAlias string
  1227  	results    []treeTestEntryPeriod
  1228  	jobsDone   int
  1229  }
  1230  
  1231  type treeTestResult string
  1232  
  1233  const (
  1234  	treeTestCrash treeTestResult = "crash"
  1235  	treeTestOK    treeTestResult = "ok"
  1236  	treeTestError treeTestResult = "error"
  1237  )
  1238  
  1239  type treeTestEntryPeriod struct {
  1240  	fromDay int
  1241  	result  treeTestResult
  1242  	commit  string
  1243  }
  1244  
  1245  func TestRepoGraph(t *testing.T) {
  1246  	g, err := makeRepoGraph(downstreamUpstreamRepos)
  1247  	if err != nil {
  1248  		t.Fatal(err)
  1249  	}
  1250  
  1251  	downstream := g.nodeByAlias(`downstream`)
  1252  	lts := g.nodeByAlias(`lts`)
  1253  	upstream := g.nodeByAlias(`upstream`)
  1254  
  1255  	// Test the downstream node.
  1256  	if diff := cmp.Diff(map[*repoNode]bool{
  1257  		lts:      true,
  1258  		upstream: false,
  1259  	}, downstream.reachable(true)); diff != "" {
  1260  		t.Fatal(diff)
  1261  	}
  1262  	if diff := cmp.Diff(map[*repoNode]bool{}, downstream.reachable(false)); diff != "" {
  1263  		t.Fatal(diff)
  1264  	}
  1265  
  1266  	// Test the lts node.
  1267  	if diff := cmp.Diff(map[*repoNode]bool{
  1268  		upstream: false,
  1269  	}, lts.reachable(true)); diff != "" {
  1270  		t.Fatal(diff)
  1271  	}
  1272  	if diff := cmp.Diff(map[*repoNode]bool{
  1273  		downstream: true,
  1274  	}, lts.reachable(false)); diff != "" {
  1275  		t.Fatal(diff)
  1276  	}
  1277  
  1278  	// Test the upstream node.
  1279  	if diff := cmp.Diff(map[*repoNode]bool{}, upstream.reachable(true)); diff != "" {
  1280  		t.Fatal(diff)
  1281  	}
  1282  	if diff := cmp.Diff(map[*repoNode]bool{
  1283  		downstream: false,
  1284  		lts:        false,
  1285  	}, upstream.reachable(false)); diff != "" {
  1286  		t.Fatal(diff)
  1287  	}
  1288  }
  1289  
  1290  func TestRepoGraphMergeFirst(t *testing.T) {
  1291  	// Test whether we prioritize merge links.
  1292  	g, err := makeRepoGraph([]KernelRepo{
  1293  		{
  1294  			URL:    `https://downstream.repo/repo`,
  1295  			Branch: `master`,
  1296  			Alias:  `downstream`,
  1297  			CommitInflow: []KernelRepoLink{
  1298  				{
  1299  					Alias: `upstream`,
  1300  					Merge: false,
  1301  				},
  1302  				{
  1303  					Alias: `lts`,
  1304  					Merge: true,
  1305  				},
  1306  			},
  1307  		},
  1308  		{
  1309  			URL:    `https://lts.repo/repo`,
  1310  			Branch: `lts-master`,
  1311  			Alias:  `lts`,
  1312  			CommitInflow: []KernelRepoLink{
  1313  				{
  1314  					Alias: `upstream`,
  1315  					Merge: true,
  1316  				},
  1317  			},
  1318  		},
  1319  		{
  1320  			URL:    `https://upstream.repo/repo`,
  1321  			Branch: `upstream-master`,
  1322  			Alias:  `upstream`,
  1323  		},
  1324  	})
  1325  	if err != nil {
  1326  		t.Fatal(err)
  1327  	}
  1328  
  1329  	downstream := g.nodeByAlias(`downstream`)
  1330  	lts := g.nodeByAlias(`lts`)
  1331  	upstream := g.nodeByAlias(`upstream`)
  1332  
  1333  	// Test the downstream node.
  1334  	if diff := cmp.Diff(map[*repoNode]bool{
  1335  		lts:      true,
  1336  		upstream: true,
  1337  	}, downstream.reachable(true)); diff != "" {
  1338  		t.Fatal(diff)
  1339  	}
  1340  	if diff := cmp.Diff(map[*repoNode]bool{}, downstream.reachable(false)); diff != "" {
  1341  		t.Fatal(diff)
  1342  	}
  1343  }