// Copyright 2023 syzkaller project authors. All rights reserved. // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. package main import ( "fmt" "reflect" "regexp" "sort" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/google/syzkaller/dashboard/dashapi" "github.com/stretchr/testify/assert" db "google.golang.org/appengine/v2/datastore" aemail "google.golang.org/appengine/v2/mail" ) func TestTreeOriginDownstream(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.reportToEmail() ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels(`origin:downstream`) // It should habe been enough to run jobs just once. c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[2].jobsDone, 1) // Test that we can render the bug page. _, err := c.GET(ctx.bugLink()) c.expectEQ(err, nil) // Test that we receive a notification. msg := ctx.emailWithoutURLs() c.expectEQ(msg.Body, `Bug presence analysis results: the bug reproduces only on the downstream tree. syzbot has run the reproducer on other relevant kernel trees and got the following results: downstream (commit ffffffffffff) on 2000/01/11: crash title Report: %URL% lts (commit ffffffffffff) on 2000/01/11: Didn't crash. upstream (commit ffffffffffff) on 2000/01/11: Didn't crash. More details can be found at: %URL% `) // Test that these results are also in the full bug info. info := ctx.fullBugInfo() c.expectEQ(len(info.TreeJobs), 3) c.expectEQ(info.TreeJobs[0].KernelAlias, `downstream`) c.expectNE(info.TreeJobs[0].CrashTitle, ``) c.expectEQ(info.TreeJobs[1].KernelAlias, `lts`) c.expectEQ(info.TreeJobs[1].CrashTitle, ``) c.expectEQ(info.TreeJobs[2].KernelAlias, `upstream`) c.expectEQ(info.TreeJobs[2].CrashTitle, ``) } func TestTreeOriginDownstreamEmail(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) // The report must contain the string. msg := ctx.reportToEmail() assert.Contains(t, msg.Body, `@testapp.appspotmail.com Bug presence analysis results: the bug reproduces only on the downstream tree. report1 --- This report is generated by a bot. It may contain errors.`) // No notification must be sent. c.client.pollNotifs(0) } func TestTreeOriginBetterReport(t *testing.T) { // Ensure that, once a higher priority crash becomes available, we perform origin testing again. c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10, 20, 30} ctx.moveToDay(10) ctx.ensureLabels("origin:downstream") c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[2].jobsDone, 1) // No retets are needed yet. ctx.moveToDay(20) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[2].jobsDone, 1) // Use a "better" manager. ctx.manager = "better-manager" ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) // With that reproducer, lts begins to crash as well. ctx.entries[1].results = []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}} ctx.moveToDay(30) ctx.ensureLabels("origin:lts") c.expectEQ(ctx.entries[1].jobsDone, 2) c.expectEQ(ctx.entries[2].jobsDone, 2) } func TestTreeOriginLts(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels(`origin:lts`) c.expectEQ(ctx.entries[0].jobsDone, 0) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[2].jobsDone, 1) // Test that we don't receive any notification. ctx.reportToEmail() ctx.ctx.expectNoEmail() } // This function is very very big, but the required scenario is unfortunately // also very big, so: // nolint: funlen func TestTreeOriginLtsBisection(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{ { fromDay: 0, result: treeTestCrash, commit: "badc0ffee", }, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels(`origin:lts`) ctx.reportToEmail() ctx.ctx.advanceTime(time.Hour) // Expect a cross tree bisection request. job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, dashapi.JobBisectFix, job.Type) assert.Equal(t, "https://upstream.repo/repo", job.KernelRepo) assert.Equal(t, "upstream-master", job.KernelBranch) assert.Equal(t, "https://lts.repo/repo", job.MergeBaseRepo) assert.Equal(t, "lts-master", job.MergeBaseBranch) assert.Equal(t, "badc0ffee", job.KernelCommit) ctx.ctx.advanceTime(time.Hour) // Make sure we don't create the same job twice. job2 := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, "", job2.ID) ctx.ctx.advanceTime(time.Hour) // Let the bisection fail. done := &dashapi.JobDoneReq{ ID: job.ID, Log: []byte("bisect log"), Error: []byte("bisect error"), } c.expectOK(ctx.client.JobDone(done)) ctx.ctx.advanceTime(time.Hour) // Ensure there are no new bisection requests. job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, job.ID, "") // Wait for the cooldown and request the job once more. ctx.ctx.advanceTime(15 * 24 * time.Hour) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.ctx.advanceTime(15 * 24 * time.Hour) job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, job.KernelRepo, "https://upstream.repo/repo") assert.Equal(t, job.KernelCommit, "badc0ffee") // This time pretend we have found the commit. build := testBuild(2) build.KernelRepo = job.KernelRepo build.KernelBranch = job.KernelBranch build.KernelCommit = "deadf00d" done = &dashapi.JobDoneReq{ ID: job.ID, Build: *build, Log: []byte("bisect log 2"), CrashTitle: "bisect crash title", CrashLog: []byte("bisect crash log"), CrashReport: []byte("bisect crash report"), Commits: []dashapi.Commit{ { AuthorName: "Someone", Author: "someone@somewhere.com", Hash: "deadf00d", Title: "kernel: fix a bug", Date: time.Date(2000, 2, 9, 4, 5, 6, 7, time.UTC), }, }, } done.Build.ID = job.ID ctx.ctx.advanceTime(time.Hour) c.expectOK(ctx.client.JobDone(done)) // Ensure the job is no longer created. ctx.ctx.advanceTime(time.Hour) job = ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, job.ID, "") msg := ctx.emailWithoutURLs() c.expectEQ(msg.Body, `syzbot suspects this issue could be fixed by backporting the following commit: commit deadf00d git tree: upstream Author: Someone Date: Wed Feb 9 04:05:06 2000 +0000 kernel: fix a bug bisection log: %URL% final oops: %URL% console output: %URL% kernel config: %URL% dashboard link: %URL% syz repro: %URL% C reproducer: %URL% Please keep in mind that other backports might be required as well. For information about bisection process see: %URL%#bisection `) ctx.ctx.expectNoEmail() info := ctx.fullBugInfo() assert.NotNil(t, info.FixCandidate) fix := info.FixCandidate assert.Equal(t, "upstream", fix.KernelRepoAlias) assert.NotNil(t, fix.BisectFix) assert.NotNil(t, fix.BisectFix.Commit) commit := fix.BisectFix.Commit assert.Equal(t, "deadf00d", commit.Hash) assert.Equal(t, "kernel: fix a bug", commit.Title) // Ensure the bug is not automatically closed. bug := ctx.loadBug() assert.Len(t, bug.Commits, 0) // Ensure the bug is present on the backports list page. reply, err := ctx.ctx.AuthGET(AccessAdmin, "/tree-tests/backports") c.expectOK(err) assert.Contains(t, string(reply), treeTestCrashTitle) assert.Contains(t, string(reply), "deadf00d") // But don't show this to all users. reply, err = ctx.ctx.AuthGET(AccessPublic, "/tree-tests/backports") c.expectOK(err) assert.NotContains(t, string(reply), treeTestCrashTitle) // Check that we display it in another related namespace. upstreamBuild := testBuild(100) upstreamBuild.KernelRepo = "https://upstream.repo/repo" upstreamBuild.KernelBranch = "upstream-master" ctx.ctx.publicClient.UploadBuild(upstreamBuild) reply, err = ctx.ctx.AuthGET(AccessAdmin, "/access-public-email/backports") c.expectOK(err) assert.Contains(t, string(reply), treeTestCrashTitle) // .. but, again, not to everyone. reply, err = ctx.ctx.AuthGET(AccessPublic, "/access-public-email/backports") c.expectOK(err) assert.NotContains(t, string(reply), treeTestCrashTitle) // The bug must appear in commit poll. commitPollResp, err := ctx.client.CommitPoll() c.expectOK(err) assert.Contains(t, commitPollResp.Commits, "kernel: fix a bug") // Pretend that we have found a commit. c.expectOK(ctx.client.UploadCommits([]dashapi.Commit{ { Hash: "newhash", Title: "kernel: fix a bug", AuthorName: "Someone", Author: "someone@somewhere.com", Date: time.Date(2000, 3, 4, 5, 6, 7, 8, time.UTC), }, })) // An email must be sent. msg = ctx.emailWithoutURLs() fmt.Printf("%s", msg) c.expectEQ(msg.Body, `The commit that was suspected to fix the issue was backported to the fuzzed kernel trees. commit newhash Author: Someone Date: Sat Mar 4 05:06:07 2000 +0000 kernel: fix a bug If you believe this is correct, please reply with #syz fix: kernel: fix a bug The commit was initially detected here: commit deadf00d git tree: upstream Author: Someone Date: Wed Feb 9 04:05:06 2000 +0000 kernel: fix a bug bisection log: %URL% final oops: %URL% console output: %URL% kernel config: %URL% dashboard link: %URL% syz repro: %URL% C reproducer: %URL% `) // Only one email. ctx.ctx.expectNoEmail() // The commit should disappear from the missing backports list. reply, err = ctx.ctx.AuthGET(AccessAdmin, "/tree-tests/backports") c.expectOK(err) assert.NotContains(t, string(reply), treeTestCrashTitle) assert.NotContains(t, string(reply), "deadf00d") } func TestNonfinalFixCandidateBisect(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, // Ignore these jobs. results: []treeTestEntryPeriod{}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.reportToEmail() ctx.ctx.advanceTime(time.Hour) // Ensure the code does not fail. job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, "", job.ID) } func TestTreeBisectionBeforeOrigin(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.reportToEmail() // Ensure the job is no longer created. ctx.ctx.advanceTime(time.Hour) job := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{BisectFix: true}) assert.Equal(t, "", job.ID) } func TestTreeOriginErrors(t *testing.T) { c := NewCtx(t) defer c.Close() // Make sure testing works fine despite patch testing errors. ctx := setUpTreeTest(c, downstreamUpstreamRepos) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestError}, {fromDay: 16, result: treeTestCrash}, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestError}, {fromDay: 31, result: treeTestCrash}, }, }, } ctx.jobTestDays = []int{1, 16, 31} ctx.moveToDay(1) ctx.ensureLabels() // Not enough information yet. // Lts got unbroken. ctx.moveToDay(16) ctx.ensureLabels(`origin:lts`) // We don't know any better so far. // Upstream got unbroken. ctx.moveToDay(31) ctx.ensureLabels(`origin:upstream`) c.expectEQ(ctx.entries[0].jobsDone, 0) c.expectEQ(ctx.entries[1].jobsDone, 2) c.expectEQ(ctx.entries[2].jobsDone, 3) } var downstreamUpstreamRepos = []KernelRepo{ { URL: `https://downstream.repo/repo`, Branch: `master`, Alias: `downstream`, LabelIntroduced: `downstream`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, }, { Alias: `lts`, Merge: true, }, }, }, { URL: `https://lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, LabelIntroduced: `lts`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: false, BisectFixes: true, }, }, }, { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, LabelIntroduced: `upstream`, }, } func TestOriginTreeNoMergeLts(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, ltsUpstreamRepos) ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `lts`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels(`origin:lts-only`) c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) } func TestOriginTreeNoMergeNoLabel(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, ltsUpstreamRepos) ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `lts`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels() // It should habe been enough to run jobs just once. c.expectEQ(ctx.entries[0].jobsDone, 0) c.expectEQ(ctx.entries[1].jobsDone, 1) } func TestTreeOriginRepoChanged(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, ltsUpstreamRepos) // First do tests from one repository. ctx.uploadBug(`https://lts.repo/repo`, `lts-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `lts`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10, 20, 25, 30, 62} ctx.moveToDay(10) ctx.ensureLabels(`origin:lts-only`) c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) // Now update the repository. ctx.updateRepos([]KernelRepo{ { URL: `https://new-lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, LabelIntroduced: `lts-only`, ReportingPriority: 9, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: false, }, }, }, { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, }, }) ctx.entries = []treeTestEntry{ { alias: `lts`, results: []treeTestEntryPeriod{ {fromDay: 30, result: treeTestError}, {fromDay: 60, result: treeTestCrash}, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.moveToDay(20) ctx.ensureLabels(`origin:lts-only`) // No new builds -- nothing we can do. // Upload a new manager build. build := ctx.uploadBuild(`https://new-lts.repo/repo`, `lts-master`) ctx.moveToDay(25) ctx.ensureLabels(`origin:lts-only`) // Still nothing we can do, no crashes so far. // Now upload a new crash. ctx.uploadBuildCrash(build, dashapi.ReproLevelC) ctx.moveToDay(30) ctx.ensureLabels() // We are no longer sure about tags. // After the new tree starts to build again, we can calculate the results again. ctx.moveToDay(62) ctx.ensureLabels(`origin:lts-only`) // We are no longer sure about tags. c.expectEQ(ctx.entries[0].jobsDone, 2) c.expectEQ(ctx.entries[1].jobsDone, 1) } var ltsUpstreamRepos = []KernelRepo{ { URL: `https://lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, LabelIntroduced: `lts-only`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: false, }, }, }, { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, }, } func TestOriginNoNextTree(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, upstreamNextRepos) ctx.uploadBug(`https://upstream.repo/repo`, `upstream-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{} ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels() } func TestOriginNoNextFixed(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, upstreamNextRepos) ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `next`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels() c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) } func TestOriginNoNext(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, upstreamNextRepos) ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `next`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels() c.expectEQ(ctx.entries[0].jobsDone, 0) c.expectEQ(ctx.entries[1].jobsDone, 1) } func TestOriginNext(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, upstreamNextRepos) ctx.uploadBug(`https://next.repo/repo`, `next-master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `next`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `upstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestOK}}, }, } ctx.jobTestDays = []int{10} ctx.moveToDay(10) ctx.ensureLabels(`origin:next`) c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) } var upstreamNextRepos = []KernelRepo{ { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, CommitInflow: []KernelRepoLink{ { Alias: `next`, Merge: false, }, }, }, { URL: `https://next.repo/repo`, Branch: `next-master`, Alias: `next`, LabelReached: `next`, }, } func TestMissingLtsBackport(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamBackports) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `lts`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, {fromDay: 46, result: treeTestOK}, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, } ctx.jobTestDays = []int{0, 46} ctx.moveToDay(46) ctx.ensureLabels(`missing-backport`) c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 1) } func TestMissingUpstreamBackport(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamBackports) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `lts`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, {fromDay: 31, result: treeTestOK}, }, }, } ctx.jobTestDays = []int{0, 46} ctx.moveToDay(46) ctx.ensureLabels(`missing-backport`) c.expectEQ(ctx.entries[0].jobsDone, 1) c.expectEQ(ctx.entries[1].jobsDone, 2) c.expectEQ(ctx.entries[1].jobsDone, 2) } func TestNotMissingBackport(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, downstreamUpstreamBackports) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestOK}, }, }, { alias: `lts`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestOK}, }, }, { alias: `upstream`, results: []treeTestEntryPeriod{ {fromDay: 0, result: treeTestCrash}, }, }, } ctx.jobTestDays = []int{0, 46} ctx.moveToDay(46) ctx.ensureLabels() c.expectEQ(ctx.entries[0].jobsDone, 0) c.expectEQ(ctx.entries[1].jobsDone, 1) c.expectEQ(ctx.entries[2].jobsDone, 1) c.expectEQ(ctx.entries[3].jobsDone, 2) } var downstreamUpstreamBackports = []KernelRepo{ { URL: `https://downstream.repo/repo`, Branch: `master`, Alias: `downstream`, CommitInflow: []KernelRepoLink{ { Alias: `lts`, Merge: true, }, { Alias: `upstream`, }, }, DetectMissingBackports: true, }, { URL: `https://lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: false, }, }, }, { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, }, } func TestTreeConfigAppend(t *testing.T) { c := NewCtx(t) defer c.Close() ctx := setUpTreeTest(c, []KernelRepo{ { URL: `https://downstream.repo/repo`, Branch: `master`, Alias: `downstream`, CommitInflow: []KernelRepoLink{ { Alias: `lts`, Merge: true, }, }, LabelIntroduced: `downstream`, }, { URL: `https://lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, LabelIntroduced: `lts`, AppendConfig: "\nCONFIG_TEST=y", }, }) ctx.uploadBug(`https://downstream.repo/repo`, `master`, dashapi.ReproLevelC) ctx.entries = []treeTestEntry{ { alias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, { alias: `lts`, mergeAlias: `downstream`, results: []treeTestEntryPeriod{{fromDay: 0, result: treeTestCrash}}, }, } ctx.jobTestDays = []int{10} tested := false ctx.validateJob = func(resp *dashapi.JobPollResp) { if resp.KernelBranch == "lts-master" { tested = true assert.Contains(t, string(resp.KernelConfig), "\nCONFIG_TEST=y") } } ctx.moveToDay(10) assert.True(t, tested) } func setUpTreeTest(ctx *Ctx, repos []KernelRepo) *treeTestCtx { ret := &treeTestCtx{ ctx: ctx, client: ctx.makeClient(clientTreeTests, keyTreeTests, true), manager: "test-manager", } ret.updateRepos(repos) return ret } type treeTestCtx struct { ctx *Ctx client *apiClient bug *Bug bugReport *dashapi.BugReport start time.Time entries []treeTestEntry perAlias map[string]KernelRepo jobTestDays []int manager string validateJob func(*dashapi.JobPollResp) } func (ctx *treeTestCtx) now() time.Time { // Yep, that's a bit too much repetition. return timeNow(ctx.ctx.ctx) } func (ctx *treeTestCtx) updateRepos(repos []KernelRepo) { checkKernelRepos("tree-tests", ctx.ctx.config().Namespaces["tree-tests"], repos) ctx.perAlias = map[string]KernelRepo{} for _, repo := range repos { ctx.perAlias[repo.Alias] = repo } ctx.ctx.setKernelRepos("tree-tests", repos) } func (ctx *treeTestCtx) uploadBuild(repo, branch string) *dashapi.Build { build := testBuild(1) build.ID = fmt.Sprintf("%d", ctx.now().Unix()) build.Manager = ctx.manager build.KernelRepo = repo build.KernelBranch = branch build.KernelCommit = build.ID ctx.client.UploadBuild(build) return build } const treeTestCrashTitle = "cross-tree bug title" func (ctx *treeTestCtx) uploadBuildCrash(build *dashapi.Build, lvl dashapi.ReproLevel) { crash := testCrash(build, 1) crash.Title = treeTestCrashTitle if lvl > dashapi.ReproLevelNone { crash.ReproSyz = []byte("getpid()") } if lvl == dashapi.ReproLevelC { crash.ReproC = []byte("getpid()") } ctx.client.ReportCrash(crash) if ctx.bug == nil || ctx.bug.ReproLevel < lvl { ctx.bugReport = ctx.client.pollBug() if ctx.bug == nil { bug, _, err := findBugByReportingID(ctx.ctx.ctx, ctx.bugReport.ID) ctx.ctx.expectOK(err) ctx.bug = bug } } } func (ctx *treeTestCtx) uploadBug(repo, branch string, lvl dashapi.ReproLevel) { build := ctx.uploadBuild(repo, branch) ctx.uploadBuildCrash(build, lvl) } func (ctx *treeTestCtx) moveToDay(tillDay int) { ctx.ctx.t.Helper() if ctx.start.IsZero() { ctx.start = ctx.now() } for _, seqDay := range ctx.jobTestDays { if seqDay > tillDay { break } now := ctx.now() day := ctx.start.Add(time.Hour * 24 * time.Duration(seqDay)) if day.Before(now) || ctx.start != ctx.now() && day.Equal(now) { continue } ctx.ctx.advanceTime(day.Sub(now)) ctx.ctx.t.Logf("executing jobs on day %d", seqDay) // Execute jobs until they exist. for { pollResp := ctx.client.pollSpecificJobs(ctx.manager, dashapi.ManagerJobs{ TestPatches: true, }) if pollResp.ID == "" { break } if ctx.validateJob != nil { ctx.validateJob(pollResp) } ctx.ctx.advanceTime(time.Minute) ctx.doJob(pollResp, seqDay) } } } func (ctx *treeTestCtx) doJob(resp *dashapi.JobPollResp, day int) { respValues := []string{ resp.KernelRepo, resp.KernelBranch, resp.MergeBaseRepo, resp.MergeBaseBranch, } sort.Strings(respValues) var found *treeTestEntry for i, entry := range ctx.entries { entryValues := []string{ ctx.perAlias[entry.alias].URL, ctx.perAlias[entry.alias].Branch, } if entry.mergeAlias != "" { entryValues = append(entryValues, ctx.perAlias[entry.mergeAlias].URL, ctx.perAlias[entry.mergeAlias].Branch) } else { entryValues = append(entryValues, "", "") } sort.Strings(entryValues) if reflect.DeepEqual(respValues, entryValues) { found = &ctx.entries[i] break } } if found == nil { ctx.ctx.t.Fatalf("unknown job request: %#v", resp) return // to avoid staticcheck false positive about nil deref } // Figure out what should the result be. result := treeTestOK build := testBuild(1) var anyFound bool for _, item := range found.results { if day >= item.fromDay { result = item.result build.KernelCommit = item.commit anyFound = true } } if !anyFound { // Just ignore the job. return } if build.KernelCommit == "" { build.KernelCommit = strings.Repeat("f", 40)[:40] } build.KernelRepo = resp.KernelRepo build.KernelBranch = resp.KernelBranch build.ID = fmt.Sprintf("%s_%s_%s_%d", resp.KernelRepo, resp.KernelBranch, resp.KernelCommit, day) jobDoneReq := &dashapi.JobDoneReq{ ID: resp.ID, Build: *build, } switch result { case treeTestOK: case treeTestCrash: jobDoneReq.CrashTitle = "crash title" jobDoneReq.CrashLog = []byte("test crash log") jobDoneReq.CrashReport = []byte("test crash report") case treeTestError: jobDoneReq.Error = []byte("failed to apply patch") } found.jobsDone++ ctx.ctx.expectOK(ctx.client.JobDone(jobDoneReq)) } func (ctx *treeTestCtx) ensureLabels(labels ...string) { ctx.ctx.t.Helper() bug := ctx.loadBug() var bugLabels []string for _, item := range bug.Labels { bugLabels = append(bugLabels, item.String()) } assert.ElementsMatch(ctx.ctx.t, labels, bugLabels) } func (ctx *treeTestCtx) loadBug() *Bug { ctx.ctx.t.Helper() if ctx.bug == nil { ctx.ctx.t.Fatalf("no bug has been created so far") } bug := new(Bug) ctx.ctx.expectOK(db.Get(ctx.ctx.ctx, ctx.bug.key(ctx.ctx.ctx), bug)) ctx.bug = bug return bug } func (ctx *treeTestCtx) bugLink() string { return fmt.Sprintf("/bug?id=%v", ctx.bug.key(ctx.ctx.ctx).StringID()) } func (ctx *treeTestCtx) reportToEmail() *aemail.Message { ctx.client.updateBug(ctx.bugReport.ID, dashapi.BugStatusUpstream, "") return ctx.ctx.pollEmailBug() } func (ctx *treeTestCtx) fullBugInfo() *dashapi.FullBugInfo { info, err := ctx.client.LoadFullBug(&dashapi.LoadFullBugReq{ BugID: ctx.bugReport.ID, }) ctx.ctx.expectOK(err) return info } var urlRe = regexp.MustCompile(`(https?://[\w\./\?\=&]+)`) func (ctx *treeTestCtx) emailWithoutURLs() *aemail.Message { msg := ctx.ctx.pollEmailBug() msg.Body = urlRe.ReplaceAllString(msg.Body, "%URL%") return msg } type treeTestEntry struct { alias string mergeAlias string results []treeTestEntryPeriod jobsDone int } type treeTestResult string const ( treeTestCrash treeTestResult = "crash" treeTestOK treeTestResult = "ok" treeTestError treeTestResult = "error" ) type treeTestEntryPeriod struct { fromDay int result treeTestResult commit string } func TestRepoGraph(t *testing.T) { g, err := makeRepoGraph(downstreamUpstreamRepos) if err != nil { t.Fatal(err) } downstream := g.nodeByAlias(`downstream`) lts := g.nodeByAlias(`lts`) upstream := g.nodeByAlias(`upstream`) // Test the downstream node. if diff := cmp.Diff(map[*repoNode]bool{ lts: true, upstream: false, }, downstream.reachable(true)); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(map[*repoNode]bool{}, downstream.reachable(false)); diff != "" { t.Fatal(diff) } // Test the lts node. if diff := cmp.Diff(map[*repoNode]bool{ upstream: false, }, lts.reachable(true)); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(map[*repoNode]bool{ downstream: true, }, lts.reachable(false)); diff != "" { t.Fatal(diff) } // Test the upstream node. if diff := cmp.Diff(map[*repoNode]bool{}, upstream.reachable(true)); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(map[*repoNode]bool{ downstream: false, lts: false, }, upstream.reachable(false)); diff != "" { t.Fatal(diff) } } func TestRepoGraphMergeFirst(t *testing.T) { // Test whether we prioritize merge links. g, err := makeRepoGraph([]KernelRepo{ { URL: `https://downstream.repo/repo`, Branch: `master`, Alias: `downstream`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: false, }, { Alias: `lts`, Merge: true, }, }, }, { URL: `https://lts.repo/repo`, Branch: `lts-master`, Alias: `lts`, CommitInflow: []KernelRepoLink{ { Alias: `upstream`, Merge: true, }, }, }, { URL: `https://upstream.repo/repo`, Branch: `upstream-master`, Alias: `upstream`, }, }) if err != nil { t.Fatal(err) } downstream := g.nodeByAlias(`downstream`) lts := g.nodeByAlias(`lts`) upstream := g.nodeByAlias(`upstream`) // Test the downstream node. if diff := cmp.Diff(map[*repoNode]bool{ lts: true, upstream: true, }, downstream.reachable(true)); diff != "" { t.Fatal(diff) } if diff := cmp.Diff(map[*repoNode]bool{}, downstream.reachable(false)); diff != "" { t.Fatal(diff) } }