dashboard/app: heavylifting of email reporting

- save Message-ID and use In-Reply-To in subsequent messages
- remember additional CC entries added manually
- don't mail to maintainers if maintainers list is empty
- improve mail formatting and add a footer
- implement upstream/fix/dup/invalid commands over email
- add tests
This commit is contained in:
Dmitry Vyukov 2017-08-17 19:09:07 +02:00
parent 2dfba870d0
commit 172189e955
15 changed files with 766 additions and 152 deletions

View File

@ -16,6 +16,7 @@ import (
"unicode/utf8"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"github.com/google/syzkaller/pkg/hash"
"golang.org/x/net/context"
"google.golang.org/appengine"
@ -319,9 +320,7 @@ func apiReportCrash(c context.Context, ns string, r *http.Request) (interface{},
return nil, fmt.Errorf("failed to unmarshal request: %v", err)
}
req.Title = limitLength(req.Title, maxTextLen)
if len(req.Maintainers) > maxMaintainers {
req.Maintainers = req.Maintainers[:maxMaintainers]
}
req.Maintainers = email.MergeEmailLists(req.Maintainers)
build, err := loadBuild(c, ns, req.BuildID)
if err != nil {

View File

@ -83,6 +83,10 @@ func (cfg *TestConfig) Type() string {
return "test"
}
func (cfg *TestConfig) NeedMaintainers() bool {
return false
}
func (cfg *TestConfig) Validate() error {
return nil
}

View File

@ -57,6 +57,8 @@ type Reporting struct {
type ReportingType interface {
// Type returns a unique string that identifies this reporting type (e.g. "email").
Type() string
// NeedMaintainers says if this reporting requires non-empty maintainers list.
NeedMaintainers() bool
// Validate validates the current object, this is called only during init.
Validate() error
}

View File

@ -6,8 +6,11 @@
package dash
import (
"fmt"
"strings"
"testing"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
)
@ -22,35 +25,417 @@ func TestEmailReport(t *testing.T) {
crash.Maintainers = []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`}
c.expectOK(c.API(client2, key2, "report_crash", crash, nil))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
sender, _, err := email.RemoveAddrContext(msg.Sender)
if err != nil {
t.Fatalf("failed to remove sender context: %v", err)
}
c.expectEQ(sender, fromAddr(c.ctx))
to := config.Namespaces["test2"].Reporting[0].Config.(*EmailConfig).Email
c.expectEQ(msg.To, []string{to})
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(len(msg.Attachments), 1)
c.expectEQ(msg.Attachments[0].Name, "config.txt")
c.expectEQ(msg.Attachments[0].Data, build.KernelConfig)
body := `
Hello,syzkaller hit the following crash on kernel_commit1
// Report the crash over email and check all fields.
sender0 := ""
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
sender0 = msg.Sender
sender, _, err := email.RemoveAddrContext(msg.Sender)
if err != nil {
t.Fatalf("failed to remove sender context: %v", err)
}
c.expectEQ(sender, fromAddr(c.ctx))
to := config.Namespaces["test2"].Reporting[0].Config.(*EmailConfig).Email
c.expectEQ(msg.To, []string{to})
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(len(msg.Attachments), 2)
c.expectEQ(msg.Attachments[0].Name, "config.txt")
c.expectEQ(msg.Attachments[0].Data, build.KernelConfig)
c.expectEQ(msg.Attachments[1].Name, "raw.log")
c.expectEQ(msg.Attachments[1].Data, crash.Log)
body := `Hello,
syzkaller hit the following crash on kernel_commit1
repo1/branch1
compiler: compiler1
.config is attached
Raw console output is attached.
CC: ["Foo Bar" <foo@bar.com> bar@foo.com]
CC: [bar@foo.com foo@bar.com]
report1
---
This bug is generated by a dumb bot. It may contain errors.
See https://goo.gl/tpsmEJ for details.
Direct all questions to syzkaller@googlegroups.com.
syzbot will keep track of this bug report.
Once a fix for this bug is committed, please reply to this email with:
#syz fix: exact-commit-title
To mark this as a duplicate of another syzbot report, please reply with:
#syz dup: exact-subject-of-another-report
If it's a one-off invalid bug report, please reply with:
#syz invalid
Note: if the crash happens again, it will cause creation of a new bug report.
To upstream this report, please reply with:
#syz upstream`
c.expectEQ(msg.Body, body)
}
// Emulate receive of the report from a mailing list.
// This should update the bug with the link/Message-ID.
incoming1 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <1234>
Subject: crash1
From: %v
To: foo@bar.com
Content-Type: text/plain
Hello
syzbot will keep track of this bug report.
Once a fix for this bug is committed, please reply to this email with:
#syz fix: exact-commit-title
To mark this as a duplicate of another syzbot report, please reply with:
#syz dup: exact-subject-of-another-report
If it's a one-off invalid bug report, please reply with:
#syz invalid
--
You received this message because you are subscribed to the Google Groups "syzkaller" group.
To unsubscribe from this group and stop receiving emails from it, send an email to syzkaller+unsubscribe@googlegroups.com.
To post to this group, send email to syzkaller@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/syzkaller/1234@google.com.
For more options, visit https://groups.google.com/d/optout.
`, sender0)
c.expectOK(c.POST("/_ah/mail/", incoming1))
// Now report syz reproducer and check updated email.
crash.ReproOpts = []byte("repro opts")
crash.ReproSyz = []byte("getpid()")
syzRepro := []byte(fmt.Sprintf("#%s\n%s", crash.ReproOpts, crash.ReproSyz))
c.expectOK(c.API(client2, key2, "report_crash", crash, nil))
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
c.expectEQ(msg.Sender, sender0)
sender, _, err := email.RemoveAddrContext(msg.Sender)
if err != nil {
t.Fatalf("failed to remove sender context: %v", err)
}
c.expectEQ(sender, fromAddr(c.ctx))
var to []string
to = append(to, "foo@bar.com")
to = append(to, config.Namespaces["test2"].Reporting[0].Config.(*EmailConfig).Email)
c.expectEQ(msg.To, to)
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(len(msg.Attachments), 3)
c.expectEQ(msg.Attachments[0].Name, "config.txt")
c.expectEQ(msg.Attachments[0].Data, build.KernelConfig)
c.expectEQ(msg.Attachments[1].Name, "raw.log")
c.expectEQ(msg.Attachments[1].Data, crash.Log)
c.expectEQ(msg.Attachments[2].Name, "repro.txt")
c.expectEQ(msg.Attachments[2].Data, syzRepro)
c.expectEQ(msg.Headers["In-Reply-To"], []string{"<1234>"})
body := `syzkaller has found reproducer for the following crash on kernel_commit1
repo1/branch1
compiler: compiler1
.config is attached
Raw console output is attached.
syzkaller reproducer is attached. See https://goo.gl/kgGztJ
for information about syzkaller reproducers
CC: [bar@foo.com foo@bar.com]
report1
`
c.expectEQ(msg.Body, body)
c.expectEQ(msg.Body, body)
}
// Now upstream the bug and check that it reaches the next reporting.
incoming2 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <1234>
Subject: crash1
From: foo@bar.com
To: foo@bar.com
Cc: %v
Content-Type: text/plain
#syz upstream
`, sender0)
c.expectOK(c.POST("/_ah/mail/", incoming2))
sender1 := ""
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
sender1 = msg.Sender
if sender1 == sender0 {
t.Fatalf("same ID in different reporting")
}
sender, _, err := email.RemoveAddrContext(msg.Sender)
if err != nil {
t.Fatalf("failed to remove sender context: %v", err)
}
c.expectEQ(sender, fromAddr(c.ctx))
c.expectEQ(msg.To, []string{"bar@foo.com", "bugs@syzkaller.com", "foo@bar.com"})
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(len(msg.Attachments), 3)
c.expectEQ(msg.Attachments[0].Name, "config.txt")
c.expectEQ(msg.Attachments[0].Data, build.KernelConfig)
c.expectEQ(msg.Attachments[1].Name, "raw.log")
c.expectEQ(msg.Attachments[1].Data, crash.Log)
c.expectEQ(msg.Attachments[2].Name, "repro.txt")
c.expectEQ(msg.Attachments[2].Data, syzRepro)
body := `Hello,
syzkaller hit the following crash on kernel_commit1
repo1/branch1
compiler: compiler1
.config is attached
Raw console output is attached.
syzkaller reproducer is attached. See https://goo.gl/kgGztJ
for information about syzkaller reproducers
report1
---
This bug is generated by a dumb bot. It may contain errors.
See https://goo.gl/tpsmEJ for details.
Direct all questions to syzkaller@googlegroups.com.
syzbot will keep track of this bug report.
Once a fix for this bug is committed, please reply to this email with:
#syz fix: exact-commit-title
To mark this as a duplicate of another syzbot report, please reply with:
#syz dup: exact-subject-of-another-report
If it's a one-off invalid bug report, please reply with:
#syz invalid
Note: if the crash happens again, it will cause creation of a new bug report.
`
c.expectEQ(msg.Body, body)
}
// Model that somebody adds more emails to CC list.
incoming3 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <1234>
Subject: crash1
From: foo@bar.com
To: %v
CC: new@new.com, "another" <another@another.com>, bar@foo.com, bugs@syzkaller.com, foo@bar.com
Content-Type: text/plain
+more people
`, sender1)
c.expectOK(c.POST("/_ah/mail/", incoming3))
// Now upload a C reproducer.
crash.ReproC = []byte("int main() {}")
crash.Maintainers = []string{"\"qux\" <qux@qux.com>"}
c.expectOK(c.API(client2, key2, "report_crash", crash, nil))
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
c.expectEQ(msg.Sender, sender1)
sender, _, err := email.RemoveAddrContext(msg.Sender)
if err != nil {
t.Fatalf("failed to remove sender context: %v", err)
}
c.expectEQ(sender, fromAddr(c.ctx))
c.expectEQ(msg.To, []string{"another@another.com", "bar@foo.com", "bugs@syzkaller.com", "foo@bar.com", "new@new.com", "qux@qux.com"})
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(len(msg.Attachments), 4)
c.expectEQ(msg.Attachments[0].Name, "config.txt")
c.expectEQ(msg.Attachments[0].Data, build.KernelConfig)
c.expectEQ(msg.Attachments[1].Name, "raw.log")
c.expectEQ(msg.Attachments[1].Data, crash.Log)
c.expectEQ(msg.Attachments[2].Name, "repro.txt")
c.expectEQ(msg.Attachments[2].Data, syzRepro)
c.expectEQ(msg.Attachments[3].Name, "repro.c")
c.expectEQ(msg.Attachments[3].Data, crash.ReproC)
body := `syzkaller has found reproducer for the following crash on kernel_commit1
repo1/branch1
compiler: compiler1
.config is attached
Raw console output is attached.
C reproducer is attached
syzkaller reproducer is attached. See https://goo.gl/kgGztJ
for information about syzkaller reproducers
report1
`
c.expectEQ(msg.Body, body)
}
// Send an invalid command.
incoming4 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <abcdef>
Subject: title1
From: foo@bar.com
To: %v
Content-Type: text/plain
#syz bad-command
`, sender1)
c.expectOK(c.POST("/_ah/mail/", incoming4))
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
c.expectEQ(msg.To, []string{"<foo@bar.com>"})
c.expectEQ(msg.Subject, crash.Title)
c.expectEQ(msg.Headers["In-Reply-To"], []string{"<abcdef>"})
if !strings.Contains(msg.Body, `> #syz bad-command
unknown command "bad-command"
`) {
t.Fatal("no unknown command reply for bad command")
}
}
// Now mark the bug as invalid.
incoming5 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <abcdef>
Subject: title1
From: foo@bar.com
To: %v
Content-Type: text/plain
#syz fix: some: commit title
`, sender1)
c.expectOK(c.POST("/_ah/mail/", incoming5))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 0)
// Check that the commit is now passed to builders.
builderPollReq := &dashapi.BuilderPollReq{build.Manager}
builderPollResp := new(dashapi.BuilderPollResp)
c.expectOK(c.API(client2, key2, "builder_poll", builderPollReq, builderPollResp))
c.expectEQ(len(builderPollResp.PendingCommits), 1)
c.expectEQ(builderPollResp.PendingCommits[0], "some: commit title")
build2 := testBuild(2)
build2.Manager = build.Manager
build2.Commits = []string{"some: commit title"}
c.expectOK(c.API(client2, key2, "upload_build", build2, nil))
// New crash must produce new bug in the first reporting.
c.expectOK(c.API(client2, key2, "report_crash", crash, nil))
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
c.expectEQ(msg.Subject, crash.Title+" (2)")
if msg.Sender == sender0 {
t.Fatalf("same reporting ID for new bug")
}
}
}
// Bug must not be mailed to maintainers if maintainers list is empty.
func TestEmailNoMaintainers(t *testing.T) {
c := NewCtx(t)
defer c.Close()
build := testBuild(1)
c.expectOK(c.API(client2, key2, "upload_build", build, nil))
crash := testCrash(build, 1)
c.expectOK(c.API(client2, key2, "report_crash", crash, nil))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
sender := (<-c.emailSink).Sender
incoming1 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <1234>
Subject: crash1
From: %v
To: foo@bar.com
Content-Type: text/plain
#syz upstream
`, sender)
c.expectOK(c.POST("/_ah/mail/", incoming1))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 0)
}
// Basic dup scenario: mark one bug as dup of another.
func TestEmailDup(t *testing.T) {
c := NewCtx(t)
defer c.Close()
build := testBuild(1)
c.expectOK(c.API(client2, key2, "upload_build", build, nil))
crash1 := testCrash(build, 1)
crash1.Title = "BUG: slightly more elaborate title"
c.expectOK(c.API(client2, key2, "report_crash", crash1, nil))
crash2 := testCrash(build, 2)
crash1.Title = "KASAN: another title"
c.expectOK(c.API(client2, key2, "report_crash", crash2, nil))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 2)
msg1 := <-c.emailSink
msg2 := <-c.emailSink
// Dup crash2 to crash1.
incoming1 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <12345>
Subject: title1
From: foo@bar.com
To: %v
Content-Type: text/plain
#syz dup: BUG: slightly more elaborate title
`, msg2.Sender)
c.expectOK(c.POST("/_ah/mail/", incoming1))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 0)
// Second crash happens again
crash2.ReproC = []byte("int main() {}")
c.expectOK(c.API(client2, key2, "report_crash", crash2, nil))
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 0)
// Now close the original bug, and check that new bugs for dup are now created.
incoming2 := fmt.Sprintf(`Sender: syzkaller@googlegroups.com
Date: Tue, 15 Aug 2017 14:59:00 -0700
Message-ID: <12345>
Subject: title1
From: foo@bar.com
To: %v
Content-Type: text/plain
#syz invalid
`, msg1.Sender)
c.expectOK(c.POST("/_ah/mail/", incoming2))
// New crash must produce new bug in the first reporting.
c.expectOK(c.API(client2, key2, "report_crash", crash2, nil))
{
c.expectOK(c.GET("/email_poll"))
c.expectEQ(len(c.emailSink), 1)
msg := <-c.emailSink
c.expectEQ(msg.Subject, crash2.Title+" (2)")
}
}

View File

@ -5,6 +5,8 @@ package dash
import (
"fmt"
"regexp"
"strconv"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
@ -16,9 +18,8 @@ import (
// This file contains definitions of entities stored in datastore.
const (
maxMaintainers = 50
maxTextLen = 200
MaxStringLen = 1024
maxTextLen = 200
MaxStringLen = 1024
maxCrashes = 20
)
@ -56,7 +57,9 @@ type Bug struct {
type BugReporting struct {
Name string // refers to Reporting.Name
ID string // unique ID per BUG/BugReporting used in commucation with external systems
ExtID string // arbitrary reporting ID that is passed back in dashapi.BugReport
Link string
CC string // additional emails added to CC list (|-delimited list)
ReproLevel dashapi.ReproLevel
Reported time.Time
Closed time.Time
@ -136,6 +139,25 @@ func (bug *Bug) displayTitle() string {
return fmt.Sprintf("%v (%v)", bug.Title, bug.Seq+1)
}
var displayTitleRe = regexp.MustCompile("^(.*) \\(([0-9]+)\\)$")
func splitDisplayTitle(display string) (string, int64, error) {
match := displayTitleRe.FindStringSubmatchIndex(display)
if match == nil {
return display, 0, nil
}
title := display[match[2]:match[3]]
seqStr := display[match[4]:match[5]]
seq, err := strconv.ParseInt(seqStr, 10, 64)
if err != nil {
return "", 0, fmt.Errorf("failed to parse bug title: %v", err)
}
if seq <= 0 || seq > 1e6 {
return "", 0, fmt.Errorf("failed to parse bug title: seq=%v", seq)
}
return title, seq - 1, nil
}
func canonicalBug(c context.Context, bug *Bug) (*Bug, error) {
for {
if bug.Status != BugStatusDup {

View File

@ -1,18 +1,34 @@
{{if .First}}
{{if .First -}}
Hello,
{{- end -}}
{{end -}}
syzkaller {{if .First}}hit{{else}}has found reproducer for{{end}} the following crash on {{.KernelCommit}}
{{.KernelRepo}}/{{.KernelBranch}}
compiler: {{.CompilerID}}
.config is attached
{{if .HasLog}}Raw console output is attached.{{end}}
{{if .ReproC}}C reproducer is attached{{end}}
{{if .ReproSyz}}syzkaller reproducer is attached. See https://github.com/google/syzkaller/blob/master/docs/executing_syzkaller_programs.md for information about syzkaller reproducers{{end}}
{{if .ReproSyz}}syzkaller reproducer is attached. See https://goo.gl/kgGztJ
for information about syzkaller reproducers{{end}}
{{if .Moderation}}CC: {{.Maintainers}}{{end}}
{{printf "%s" .Report}}
{{if .First}}
---
This bug is generated by a dumb bot. It may contain errors.
See https://goo.gl/tpsmEJ for details.
Direct all questions to syzkaller@googlegroups.com.
{{end}}
syzbot will keep track of this bug report.
Once a fix for this bug is committed, please reply to this email with:
#syz fix: exact-commit-title
To mark this as a duplicate of another syzbot report, please reply with:
#syz dup: exact-subject-of-another-report
If it's a one-off invalid bug report, please reply with:
#syz invalid
Note: if the crash happens again, it will cause creation of a new bug report.
{{if .Moderation -}}
To upstream this report, please reply with:
#syz upstream
{{- end -}}
{{- end -}}

View File

@ -167,7 +167,7 @@ func fetchBugs(c context.Context) ([]*uiBugGroup, error) {
}
func createUIBug(c context.Context, bug *Bug, state *ReportingState) *uiBug {
_, _, reportingIdx, status, link, err := needReport(c, "", state, bug)
_, _, _, reportingIdx, status, link, err := needReport(c, "", state, bug)
if err != nil {
status = err.Error()
}

View File

@ -8,9 +8,11 @@ import (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/google/syzkaller/dashboard/dashapi"
"github.com/google/syzkaller/pkg/email"
"golang.org/x/net/context"
"google.golang.org/appengine/datastore"
"google.golang.org/appengine/log"
@ -60,11 +62,11 @@ func reportingPoll(c context.Context, typ string) []*dashapi.BugReport {
}
func handleReportBug(c context.Context, typ string, state *ReportingState, bug *Bug) (*dashapi.BugReport, error) {
reporting, bugReporting, _, _, _, err := needReport(c, typ, state, bug)
reporting, bugReporting, crash, _, _, _, err := needReport(c, typ, state, bug)
if err != nil || reporting == nil {
return nil, err
}
rep, err := createBugReport(c, bug, bugReporting, reporting.Config)
rep, err := createBugReport(c, bug, crash, bugReporting, reporting.Config)
if err != nil {
return nil, err
}
@ -72,15 +74,14 @@ func handleReportBug(c context.Context, typ string, state *ReportingState, bug *
return rep, nil
}
func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (reporting *Reporting, bugReporting *BugReporting, reportingIdx int, status, link string, err error) {
func needReport(c context.Context, typ string, state *ReportingState, bug *Bug) (reporting *Reporting, bugReporting *BugReporting, crash *Crash, reportingIdx int, status, link string, err error) {
reporting, bugReporting, reportingIdx, status, err = currentReporting(c, bug)
if err != nil || reporting == nil {
return
}
if typ != "" && typ != reporting.Config.Type() {
status = "on a different reporting"
reporting = nil
bugReporting = nil
reporting, bugReporting = nil, nil
return
}
link = bugReporting.Link
@ -88,40 +89,49 @@ func needReport(c context.Context, typ string, state *ReportingState, bug *Bug)
status = fmt.Sprintf("%v: reported%v on %v",
reporting.Name, reproStr(bugReporting.ReproLevel),
formatTime(bugReporting.Reported))
reporting = nil
bugReporting = nil
reporting, bugReporting = nil, nil
return
}
ent := state.getEntry(timeNow(c), bug.Namespace, reporting.Name)
cfg := config.Namespaces[bug.Namespace]
if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
status = fmt.Sprintf("%v: waiting for C repro", reporting.Name)
reporting, bugReporting = nil, nil
return
}
if !cfg.MailWithoutReport && !bug.HasReport {
status = fmt.Sprintf("%v: no report", reporting.Name)
reporting, bugReporting = nil, nil
return
}
crash, err = findCrashForBug(c, bug)
if err != nil {
status = fmt.Sprintf("%v: no crashes!", reporting.Name)
reporting, bugReporting = nil, nil
return
}
if reporting.Config.NeedMaintainers() && len(crash.Maintainers) == 0 {
status = fmt.Sprintf("%v: no maintainers", reporting.Name)
reporting, bugReporting = nil, nil
return
}
// Limit number of reports sent per day,
// but don't limit sending repros to already reported bugs.
if bugReporting.Reported.IsZero() && reporting.DailyLimit != 0 &&
ent.Sent >= reporting.DailyLimit {
status = fmt.Sprintf("%v: out of quota for today", reporting.Name)
reporting = nil
bugReporting = nil
reporting, bugReporting = nil, nil
return
}
// Ready to be reported.
if bugReporting.Reported.IsZero() {
// This update won't be committed, but it will prevent us from
// reporting too many bugs in a single poll.
ent.Sent++
}
cfg := config.Namespaces[bug.Namespace]
if bug.ReproLevel < ReproLevelC && timeSince(c, bug.FirstTime) < cfg.WaitForRepro {
status = fmt.Sprintf("%v: waiting for C repro", reporting.Name)
reporting = nil
bugReporting = nil
return
}
if !cfg.MailWithoutReport && !bug.HasReport {
status = fmt.Sprintf("%v: no report", reporting.Name)
reporting = nil
bugReporting = nil
return
}
// Ready to be reported.
status = fmt.Sprintf("%v: ready to report", reporting.Name)
if !bugReporting.Reported.IsZero() {
status += fmt.Sprintf(" (reported%v on %v)",
@ -162,16 +172,11 @@ func reproStr(level dashapi.ReproLevel) string {
}
}
func createBugReport(c context.Context, bug *Bug, bugReporting *BugReporting, config interface{}) (*dashapi.BugReport, error) {
func createBugReport(c context.Context, bug *Bug, crash *Crash, bugReporting *BugReporting, config interface{}) (*dashapi.BugReport, error) {
reportingConfig, err := json.Marshal(config)
if err != nil {
return nil, err
}
bugKey := datastore.NewKey(c, "Bug", bugKeyHash(bug.Namespace, bug.Title, bug.Seq), 0, nil)
crash, err := findCrashForBug(c, bug, bugKey)
if err != nil {
return nil, err
}
crashLog, err := getText(c, "CrashLog", crash.Log)
if err != nil {
return nil, err
@ -212,6 +217,7 @@ func createBugReport(c context.Context, bug *Bug, bugReporting *BugReporting, co
rep := &dashapi.BugReport{
Config: reportingConfig,
ID: bugReporting.ID,
ExtID: bugReporting.ExtID,
First: bugReporting.Reported.IsZero(),
Title: bug.displayTitle(),
Log: crashLog,
@ -225,12 +231,15 @@ func createBugReport(c context.Context, bug *Bug, bugReporting *BugReporting, co
ReproC: reproC,
ReproSyz: reproSyz,
}
if bugReporting.CC != "" {
rep.CC = strings.Split(bugReporting.CC, "|")
}
return rep, nil
}
// incomingCommand is entry point to bug status updates.
func incomingCommand(c context.Context, cmd *dashapi.BugUpdate) (string, bool) {
log.Infof(c, "got command: %+v", cmd)
log.Infof(c, "got command: %+q", cmd)
reply, err := incomingCommandImpl(c, cmd)
if err != nil {
log.Errorf(c, "%v", err)
@ -247,9 +256,25 @@ func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (string, err
now := timeNow(c)
dupHash := ""
if cmd.Status == dashapi.BugStatusDup {
bugReporting, _ := bugReportingByID(bug, cmd.ID, now)
dup, dupKey, err := findBugByReportingID(c, cmd.DupOf)
if err != nil {
return "can't find the dup bug", err
// Email reporting passes bug title in cmd.DupOf, try to find bug by title.
dup, dupKey, err = findDupByTitle(c, bug.Namespace, cmd.DupOf)
if err != nil {
return "can't find the dup bug", err
}
cmd.DupOf = ""
for i := range dup.Reporting {
if dup.Reporting[i].Name == bugReporting.Name {
cmd.DupOf = dup.Reporting[i].ID
break
}
}
if cmd.DupOf == "" {
return "can't find the dup bug",
fmt.Errorf("dup does not have reporting %q", bugReporting.Name)
}
}
if bugKey.StringID() == dupKey.StringID() {
return "can't dup bug to itself", fmt.Errorf("can't dup bug to itself")
@ -258,7 +283,6 @@ func incomingCommandImpl(c context.Context, cmd *dashapi.BugUpdate) (string, err
return "can't find the dup bug",
fmt.Errorf("inter-namespace dup: %v->%v", bug.Namespace, dup.Namespace)
}
bugReporting, _ := bugReportingByID(bug, cmd.ID, now)
dupReporting, _ := bugReportingByID(dup, cmd.DupOf, now)
if bugReporting == nil || dupReporting == nil {
return internalError, fmt.Errorf("can't find bug reporting")
@ -348,6 +372,8 @@ func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate,
bug.Status = BugStatusDup
bug.Closed = now
bug.DupOf = dupHash
case dashapi.BugStatusUpdate:
// Just update Link, Commits, etc below.
default:
return "unknown bug status", fmt.Errorf("unknown bug status %v", cmd.Status)
}
@ -380,9 +406,16 @@ func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate,
bug.PatchedOn = nil
}
}
if cmd.Link != "" {
if bugReporting.ExtID == "" {
bugReporting.ExtID = cmd.ExtID
}
if bugReporting.Link == "" {
bugReporting.Link = cmd.Link
}
if len(cmd.CC) != 0 {
merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC)
bugReporting.CC = strings.Join(merged, "|")
}
if bugReporting.ReproLevel < cmd.ReproLevel {
bugReporting.ReproLevel = cmd.ReproLevel
}
@ -416,6 +449,20 @@ func findBugByReportingID(c context.Context, id string) (*Bug, *datastore.Key, e
return bugs[0], keys[0], nil
}
func findDupByTitle(c context.Context, ns, title string) (*Bug, *datastore.Key, error) {
title, seq, err := splitDisplayTitle(title)
if err != nil {
return nil, nil, err
}
bugHash := bugKeyHash(ns, title, seq)
bugKey := datastore.NewKey(c, "Bug", bugHash, 0, nil)
bug := new(Bug)
if err := datastore.Get(c, bugKey, bug); err != nil {
return nil, nil, fmt.Errorf("failed to get dup: %v", err)
}
return bug, bugKey, nil
}
func bugReportingByID(bug *Bug, id string, now time.Time) (*BugReporting, bool) {
for i := range bug.Reporting {
if bug.Reporting[i].ID == id {
@ -442,7 +489,8 @@ func queryCrashesForBug(c context.Context, bugKey *datastore.Key, limit int) ([]
return crashes, nil
}
func findCrashForBug(c context.Context, bug *Bug, bugKey *datastore.Key) (*Crash, error) {
func findCrashForBug(c context.Context, bug *Bug) (*Crash, error) {
bugKey := datastore.NewKey(c, "Bug", bugKeyHash(bug.Namespace, bug.Title, bug.Seq), 0, nil)
crashes, err := queryCrashesForBug(c, bugKey, 1)
if err != nil {
return nil, fmt.Errorf("failed to fetch crashes: %v", err)

View File

@ -38,6 +38,10 @@ func (cfg *EmailConfig) Type() string {
return emailType
}
func (cfg *EmailConfig) NeedMaintainers() bool {
return cfg.MailMaintainers
}
func (cfg *EmailConfig) Validate() error {
if _, err := mail.ParseAddress(cfg.Email); err != nil {
return fmt.Errorf("bad email address %q: %v", cfg.Email, err)
@ -81,12 +85,19 @@ func emailReport(c context.Context, rep *dashapi.BugReport) error {
}
to = append(to, rep.Maintainers...)
}
to = email.MergeEmailLists(to, rep.CC)
attachments := []aemail.Attachment{
{
Name: "config.txt",
Data: rep.KernelConfig,
},
}
if len(rep.Log) != 0 {
attachments = append(attachments, aemail.Attachment{
Name: "raw.log",
Data: rep.Log,
})
}
repro := dashapi.ReproLevelNone
if len(rep.ReproSyz) != 0 {
repro = dashapi.ReproLevelSyz
@ -116,6 +127,7 @@ func emailReport(c context.Context, rep *dashapi.BugReport) error {
KernelBranch string
KernelCommit string
Report []byte
HasLog bool
ReproSyz bool
ReproC bool
}
@ -128,10 +140,12 @@ func emailReport(c context.Context, rep *dashapi.BugReport) error {
KernelBranch: rep.KernelBranch,
KernelCommit: rep.KernelCommit,
Report: rep.Report,
HasLog: len(rep.Log) != 0,
ReproSyz: len(rep.ReproSyz) != 0,
ReproC: len(rep.ReproC) != 0,
}
if err := sendMailTemplate(c, rep.Title, from, to, attachments, "mail_bug.txt", data); err != nil {
err = sendMailTemplate(c, rep.Title, from, to, rep.ExtID, attachments, "mail_bug.txt", data)
if err != nil {
return err
}
cmd := &dashapi.BugUpdate{
@ -157,30 +171,49 @@ func incomingMail(c context.Context, r *http.Request) error {
if err != nil {
return err
}
log.Infof(c, "received email: subject '%v', from '%v', cc '%v', msg '%v', bug '%v', cmd '%v'",
msg.Subject, msg.From, msg.Cc, msg.MessageID, msg.BugID, msg.Command)
var status dashapi.BugStatus
log.Infof(c, "received email: subject %q, from %q, cc %q, msg %q, bug %q, cmd %q, link %q",
msg.Subject, msg.From, msg.Cc, msg.MessageID, msg.BugID, msg.Command, msg.Link)
// Don't send replies yet.
// It is not tested and it is unclear how verbose we want to be.
sendReply := false
cmd := &dashapi.BugUpdate{
ID: msg.BugID,
ExtID: msg.MessageID,
Link: msg.Link,
CC: msg.Cc,
}
switch msg.Command {
case "":
return nil
cmd.Status = dashapi.BugStatusUpdate
case "upstream":
status = dashapi.BugStatusUpstream
cmd.Status = dashapi.BugStatusUpstream
case "invalid":
status = dashapi.BugStatusInvalid
cmd.Status = dashapi.BugStatusInvalid
case "fix:":
if msg.CommandArgs == "" {
return replyTo(c, msg, fmt.Sprintf("no commit title"), nil)
}
cmd.Status = dashapi.BugStatusOpen
cmd.FixCommits = []string{msg.CommandArgs}
case "dup:":
if msg.CommandArgs == "" {
return replyTo(c, msg, fmt.Sprintf("no dup title"), nil)
}
cmd.Status = dashapi.BugStatusDup
cmd.DupOf = msg.CommandArgs
default:
return replyTo(c, msg, fmt.Sprintf("unknown command %q", msg.Command), nil)
}
cmd := &dashapi.BugUpdate{
ID: msg.BugID,
Status: status,
}
reply, _ := incomingCommand(c, cmd)
if !sendReply {
return nil
}
return replyTo(c, msg, reply, nil)
}
var mailTemplates = template.Must(template.New("").ParseGlob("mail_*.txt"))
func sendMailTemplate(c context.Context, subject, from string, to []string,
func sendMailTemplate(c context.Context, subject, from string, to []string, replyTo string,
attachments []aemail.Attachment, template string, data interface{}) error {
body := new(bytes.Buffer)
if err := mailTemplates.ExecuteTemplate(body, template, data); err != nil {
@ -193,6 +226,9 @@ func sendMailTemplate(c context.Context, subject, from string, to []string,
Body: body.String(),
Attachments: attachments,
}
if replyTo != "" {
msg.Headers = mail.Header{"In-Reply-To": []string{replyTo}}
}
return sendEmail(c, msg)
}

View File

@ -50,7 +50,7 @@ func TestReportBug(t *testing.T) {
ID: rep.ID,
First: true,
Title: "title1",
Maintainers: []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`},
Maintainers: []string{"bar@foo.com", "foo@bar.com"},
CompilerID: "compiler1",
KernelRepo: "repo1",
KernelBranch: "branch1",

View File

@ -136,8 +136,17 @@ func (c *Ctx) API(client, key, method string, req, reply interface{}) error {
// GET sends authorized HTTP GET request to the app.
func (c *Ctx) GET(url string) error {
c.t.Logf("GET: %v", url)
r, err := c.inst.NewRequest("GET", url, nil)
return c.httpRequest("GET", url, "")
}
// POST sends authorized HTTP POST request to the app.
func (c *Ctx) POST(url, body string) error {
return c.httpRequest("POST", url, body)
}
func (c *Ctx) httpRequest(method, url, body string) error {
c.t.Logf("%v: %v", method, url)
r, err := c.inst.NewRequest(method, url, strings.NewReader(body))
if err != nil {
c.t.Fatal(err)
}

View File

@ -136,9 +136,11 @@ func (dash *Dashboard) LogError(name, msg string, args ...interface{}) {
type BugReport struct {
Config []byte
ID string
First bool // Set for first report for this bug.
ExtID string // arbitrary reporting ID forwarded from BugUpdate.ExtID
First bool // Set for first report for this bug.
Title string
Maintainers []string
CC []string // additional CC emails
CompilerID string
KernelRepo string
KernelBranch string
@ -152,11 +154,13 @@ type BugReport struct {
type BugUpdate struct {
ID string
ExtID string
Link string
Status BugStatus
ReproLevel ReproLevel
DupOf string
FixCommits []string // Titles of commits that fix this bug.
CC []string // Additional emails to add to CC list in future emails.
}
type BugUpdateReply struct {
@ -182,6 +186,7 @@ const (
BugStatusUpstream
BugStatusInvalid
BugStatusDup
BugStatusUpdate // aux info update (i.e. ExtID/Link/CC)
)
const (

View File

@ -12,22 +12,27 @@ import (
"mime"
"mime/multipart"
"net/mail"
"regexp"
"sort"
"strings"
)
type Email struct {
BugID string
MessageID string
Link string
Subject string
From string
Cc []string
Body string // text/plain part
Patch string // attached patch, if any
Command string // command to bot (#syzbot is stripped)
CommandArgs []string // arguments for the command
Body string // text/plain part
Patch string // attached patch, if any
Command string // command to bot (#syz is stripped)
CommandArgs string // arguments for the command
}
const commandPrefix = "#syzbot "
const commandPrefix = "#syz "
var groupsLinkRe = regexp.MustCompile("\nTo view this discussion on the web visit (https://groups\\.google\\.com/.*?)\\.(?:\r)?\n")
func Parse(r io.Reader, ownEmail string) (*Email, error) {
msg, err := mail.ReadMessage(r)
@ -52,7 +57,14 @@ func Parse(r io.Reader, ownEmail string) (*Email, error) {
if addr, err := mail.ParseAddress(ownEmail); err == nil {
ownEmail = addr.Address
}
for _, addr := range append(cc, to...) {
fromMe := false
for _, addr := range from {
cleaned, _, _ := RemoveAddrContext(addr.Address)
if addr, err := mail.ParseAddress(cleaned); err == nil && addr.Address == ownEmail {
fromMe = true
}
}
for _, addr := range append(append(cc, to...), from...) {
cleaned, context, _ := RemoveAddrContext(addr.Address)
if addr, err := mail.ParseAddress(cleaned); err == nil {
cleaned = addr.Address
@ -62,27 +74,36 @@ func Parse(r io.Reader, ownEmail string) (*Email, error) {
bugID = context
}
} else {
ccList = append(ccList, addr.String())
ccList = append(ccList, cleaned)
}
}
ccList = MergeEmailLists(ccList)
body, attachments, err := parseBody(msg.Body, msg.Header)
if err != nil {
return nil, err
}
patch := ""
for _, a := range attachments {
_, patch, _ = ParsePatch(string(a))
if patch != "" {
break
bodyStr := string(body)
patch, cmd, cmdArgs := "", "", ""
if !fromMe {
for _, a := range attachments {
_, patch, _ = ParsePatch(string(a))
if patch != "" {
break
}
}
if patch == "" {
_, patch, _ = ParsePatch(bodyStr)
}
cmd, cmdArgs = extractCommand(body)
}
if patch == "" {
_, patch, _ = ParsePatch(string(body))
link := ""
if match := groupsLinkRe.FindStringSubmatchIndex(bodyStr); match != nil {
link = bodyStr[match[2]:match[3]]
}
cmd, cmdArgs := extractCommand(body)
email := &Email{
BugID: bugID,
MessageID: msg.Header.Get("Message-ID"),
Link: link,
Subject: msg.Header.Get("Subject"),
From: from[0].String(),
Cc: ccList,
@ -131,13 +152,13 @@ func RemoveAddrContext(email string) (string, string, error) {
// extractCommand extracts command to syzbot from email body.
// Commands are of the following form:
// ^#syzbot cmd args...
func extractCommand(body []byte) (cmd string, args []string) {
// ^#syz cmd args...
func extractCommand(body []byte) (cmd, args string) {
cmdPos := bytes.Index(append([]byte{'\n'}, body...), []byte("\n"+commandPrefix))
if cmdPos == -1 {
return
}
cmdPos += 8
cmdPos += len(commandPrefix)
cmdEnd := bytes.IndexByte(body[cmdPos:], '\n')
if cmdEnd == -1 {
cmdEnd = len(body) - cmdPos
@ -148,10 +169,8 @@ func extractCommand(body []byte) (cmd string, args []string) {
}
split := strings.Split(cmdLine, " ")
cmd = split[0]
for _, arg := range split[1:] {
if trimmed := strings.TrimSpace(arg); trimmed != "" {
args = append(args, trimmed)
}
if len(split) > 1 {
args = strings.TrimSpace(strings.Join(split[1:], " "))
}
return
}
@ -202,3 +221,30 @@ func parseBody(r io.Reader, headers mail.Header) (body []byte, attachments [][]b
attachments = append(attachments, attachments1...)
}
}
// MergeEmailLists merges several email lists removing duplicates and invalid entries.
func MergeEmailLists(lists ...[]string) []string {
const (
maxEmailLen = 1000
maxEmails = 50
)
merged := make(map[string]bool)
for _, list := range lists {
for _, email := range list {
addr, err := mail.ParseAddress(email)
if err != nil || len(addr.Address) > maxEmailLen {
continue
}
merged[addr.Address] = true
}
}
var result []string
for e := range merged {
result = append(result, e)
}
sort.Strings(result)
if len(result) > maxEmails {
result = result[:maxEmails]
}
return result
}

View File

@ -10,6 +10,7 @@ import (
"testing"
)
//!!! add tests with \r\n
func TestExtractCommand(t *testing.T) {
for i, test := range extractCommandTests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
@ -77,58 +78,65 @@ func TestAddRemoveAddrContext(t *testing.T) {
func TestParse(t *testing.T) {
for i, test := range parseTests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
body := func(t *testing.T, test ParseTest) {
email, err := Parse(strings.NewReader(test.email), "bot <foo@bar.com>")
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(email, test.res) {
t.Logf("expect:\n%#v", test.res)
if !reflect.DeepEqual(email, &test.res) {
t.Logf("expect:\n%#v", &test.res)
t.Logf("got:\n%#v", email)
t.Fail()
}
})
}
t.Run(fmt.Sprint(i), func(t *testing.T) { body(t, test) })
test.email = strings.Replace(test.email, "\n", "\r\n", -1)
test.res.Body = strings.Replace(test.res.Body, "\n", "\r\n", -1)
t.Run(fmt.Sprint(i)+"rn", func(t *testing.T) { body(t, test) })
}
}
var extractCommandTests = []struct {
body string
cmd string
args []string
args string
}{
{
body: `Hello,
line1
#syzbot foo bar baz`,
#syz foo bar baz `,
cmd: "foo",
args: []string{"bar", "baz"},
args: "bar baz",
},
{
body: `Hello,
line1
#syzbot foo bar baz
#syz foo bar baz
line 2
`,
cmd: "foo",
args: []string{"bar", "baz"},
cmd: "foo",
args: "bar baz",
},
{
body: `
line1
> #syzbot foo bar baz
> #syz foo bar baz
line 2
`,
cmd: "",
args: nil,
args: "",
},
}
var parseTests = []struct {
type ParseTest struct {
email string
res *Email
}{
res Email
}
var parseTests = []ParseTest{
{`Date: Sun, 7 May 2017 19:54:00 -0700
Message-ID: <123>
Subject: test subject
@ -138,20 +146,54 @@ Content-Type: text/plain; charset="UTF-8"
text body
second line
#syzbot command arg1 arg2 arg3
#syz command arg1 arg2 arg3
last line
--
You received this message because you are subscribed to the Google Groups "syzkaller" group.
To unsubscribe from this group and stop receiving emails from it, send an email to syzkaller+unsubscribe@googlegroups.com.
To post to this group, send email to syzkaller@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/syzkaller/abcdef@google.com.
For more options, visit https://groups.google.com/d/optout.`,
Email{
BugID: "4564456",
MessageID: "<123>",
Link: "https://groups.google.com/d/msgid/syzkaller/abcdef@google.com",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
Cc: []string{"bob@example.com"},
Body: `text body
second line
#syz command arg1 arg2 arg3
last line
--
You received this message because you are subscribed to the Google Groups "syzkaller" group.
To unsubscribe from this group and stop receiving emails from it, send an email to syzkaller+unsubscribe@googlegroups.com.
To post to this group, send email to syzkaller@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/syzkaller/abcdef@google.com.
For more options, visit https://groups.google.com/d/optout.`,
Patch: "",
Command: "command",
CommandArgs: "arg1 arg2 arg3",
}},
{`Date: Sun, 7 May 2017 19:54:00 -0700
Message-ID: <123>
Subject: test subject
From: syzbot <foo+4564456@bar.com>
To: Bob <bob@example.com>
Content-Type: text/plain; charset="UTF-8"
text body
last line`,
&Email{
Email{
BugID: "4564456",
MessageID: "<123>",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
From: "\"syzbot\" <foo+4564456@bar.com>",
Cc: []string{"bob@example.com"},
Body: `text body
second line
#syzbot command arg1 arg2 arg3
last line`,
Patch: "",
Command: "command",
CommandArgs: []string{"arg1", "arg2", "arg3"},
Patch: "",
}},
{`Date: Sun, 7 May 2017 19:54:00 -0700
@ -161,22 +203,22 @@ From: Bob <bob@example.com>
To: syzbot <bot@example.com>, Alice <alice@example.com>
Content-Type: text/plain
#syzbot command
#syz command
text body
second line
last line`,
&Email{
Email{
MessageID: "<123>",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
Cc: []string{"\"syzbot\" <bot@example.com>", "\"Alice\" <alice@example.com>"},
Body: `#syzbot command
Cc: []string{"alice@example.com", "bob@example.com", "bot@example.com"},
Body: `#syz command
text body
second line
last line`,
Patch: "",
Command: "command",
CommandArgs: nil,
CommandArgs: "",
}},
{`Date: Sun, 7 May 2017 19:54:00 -0700
@ -189,19 +231,19 @@ Content-Type: text/plain
text body
second line
last line
#syzbot command`,
&Email{
#syz command`,
Email{
MessageID: "<123>",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
Cc: []string{"\"syzbot\" <bot@example.com>", "\"Alice\" <alice@example.com>"},
Cc: []string{"alice@example.com", "bob@example.com", "bot@example.com"},
Body: `text body
second line
last line
#syzbot command`,
#syz command`,
Patch: "",
Command: "command",
CommandArgs: nil,
CommandArgs: "",
}},
{`Date: Sun, 7 May 2017 19:54:00 -0700
@ -215,7 +257,7 @@ Content-Type: multipart/mixed; boundary="001a114ce0b01684a6054f0d8b81"
Content-Type: text/plain; charset="UTF-8"
body text
>#syzbot test
>#syz test
--001a114ce0b01684a6054f0d8b81
Content-Type: text/x-patch; charset="US-ASCII"; name="patch.patch"
@ -230,13 +272,13 @@ YXNrX3N0cnVjdCAqdCkKIAlrY292ID0gdC0+a2NvdjsKIAlpZiAoa2NvdiA9PSBOVUxMKQogCQly
ZXR1cm47Ci0Jc3Bpbl9sb2NrKCZrY292LT5sb2NrKTsKIAlpZiAoV0FSTl9PTihrY292LT50ICE9
IHQpKSB7CiAJCXNwaW5fdW5sb2NrKCZrY292LT5sb2NrKTsKIAkJcmV0dXJuOwo=
--001a114ce0b01684a6054f0d8b81--`,
&Email{
Email{
MessageID: "<123>",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
Cc: []string{"\"syzbot\" <bot@example.com>"},
Cc: []string{"bob@example.com", "bot@example.com"},
Body: `body text
>#syzbot test
>#syz test
`,
Patch: `--- a/kernel/kcov.c
+++ b/kernel/kcov.c
@ -250,7 +292,7 @@ IHQpKSB7CiAJCXNwaW5fdW5sb2NrKCZrY292LT5sb2NrKTsKIAkJcmV0dXJuOwo=
return;
`,
Command: "",
CommandArgs: nil,
CommandArgs: "",
}},
{`Date: Sun, 7 May 2017 19:54:00 -0700
@ -266,7 +308,7 @@ Content-Type: text/plain; charset="UTF-8"
On Mon, May 8, 2017 at 6:47 PM, Bob wrote:
> body text
#syzbot test
#syz test
commit 59372bbf3abd5b24a7f6f676a3968685c280f955
Date: Thu Apr 27 13:54:11 2017 +0200
@ -295,7 +337,7 @@ Content-Transfer-Encoding: quoted-printable
<div dir=3D"ltr">On Mon, May 8, 2017 at 6:47 PM, Dmitry Vyukov &lt;<a href=
=3D"mailto:bob@example.com">bob@example.com</a>&gt; wrote:<br>&gt; bo=
dy text<br><br>#syzbot test<br><br><div><div>commit 59372bbf3abd5b24a7f6f67=
dy text<br><br>#syz test<br><br><div><div>commit 59372bbf3abd5b24a7f6f67=
6a3968685c280f955</div><div>Date: =C2=A0 Thu Apr 27 13:54:11 2017 +0200</di=
v><div><br></div><div>=C2=A0 =C2=A0 statx: correct error handling of NULL p=
athname</div><div>=C2=A0 =C2=A0=C2=A0</div><div>=C2=A0 =C2=A0 test patch.</=
@ -316,15 +358,15 @@ ce:pre">=09=09</span>return -EINVAL;</div><div>=C2=A0</div><div>=C2=A0<span=
or)</div></div></div>
--f403043eee70018593054f0d9f1f--`,
&Email{
Email{
MessageID: "<123>",
Subject: "test subject",
From: "\"Bob\" <bob@example.com>",
Cc: []string{"\"syzbot\" <bot@example.com>"},
Cc: []string{"bob@example.com", "bot@example.com"},
Body: `On Mon, May 8, 2017 at 6:47 PM, Bob wrote:
> body text
#syzbot test
#syz test
commit 59372bbf3abd5b24a7f6f676a3968685c280f955
Date: Thu Apr 27 13:54:11 2017 +0200
@ -360,6 +402,6 @@ index 3d85747bd86e..a257b872a53d 100644
if (error)
`,
Command: "test",
CommandArgs: nil,
CommandArgs: "",
}},
}

View File

@ -29,13 +29,13 @@ var formReplyTests = []struct {
{
email: `line1
line2
#syzbot foo
#syz foo
line3
`,
reply: "this is reply",
result: `> line1
> line2
> #syzbot foo
> #syz foo
this is reply
@ -45,13 +45,13 @@ this is reply
{
email: `> line1
> line2
#syzbot foo
#syz foo
line3
`,
reply: "this is reply\n",
result: `>> line1
>> line2
> #syzbot foo
> #syz foo
this is reply
@ -61,11 +61,11 @@ this is reply
{
email: `line1
line2
#syzbot foo`,
#syz foo`,
reply: "this is reply 1\nthis is reply 2",
result: `> line1
> line2
> #syzbot foo
> #syz foo
this is reply 1
this is reply 2