blob: e2e5b8fd071e63422f8f1736ccbffe72c385f906 [file] [log] [blame]
// 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 <someone@somewhere.com>
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 <someone@somewhere.com>
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 <someone@somewhere.com>
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)
}
}