diff --git a/dashboard/app/bug.html b/dashboard/app/bug.html
index 8ca0b5dd..1836a2bc 100644
--- a/dashboard/app/bug.html
+++ b/dashboard/app/bug.html
@@ -1,3 +1,10 @@
+{{/*
+Copyright 2017 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.
+
+Page with details about a single bug.
+*/}}
+
@@ -7,18 +14,21 @@
{{template "header" .Header}}
- Title: {{.Bug.Title}}
- Namespace: {{.Bug.Namespace}}
- Crashes: {{.Bug.NumCrashes}}
- First: {{formatTime .Bug.FirstTime}}
- Last: {{formatTime .Bug.LastTime}}
- Reporting: {{if .Bug.Link}}{{.Bug.Status}}{{else}}{{.Bug.Status}}{{end}}
- Commits: {{.Bug.Commits}}
- Patched on: {{.Bug.PatchedOn}}
- Missing on: {{.Bug.MissingOn}}
+ [{{.Bug.Namespace}}] {{.Bug.Title}}
+ Status: {{if .Bug.ExternalLink}}{{.Bug.Status}}{{else}}{{.Bug.Status}}{{end}}
+ {{if .Bug.Commits}}
+ Commits: {{.Bug.Commits}}
+ Patched on: {{.Bug.PatchedOn}}, missing on: {{.Bug.MissingOn}}
+ {{end}}
+ First: {{formatLateness $.Now $.Bug.FirstTime}}, last: {{formatLateness $.Now $.Bug.LastTime}}
+
+
+ {{template "bug_list" .DupOf}}
+ {{template "bug_list" .Dups}}
+ {{template "bug_list" .Similar}}
- Crashes:
+ Crashes ({{.Bug.NumCrashes}}):
Manager |
Time |
diff --git a/dashboard/app/main.go b/dashboard/app/main.go
index 70e5f05b..1091abbe 100644
--- a/dashboard/app/main.go
+++ b/dashboard/app/main.go
@@ -61,13 +61,19 @@ type uiBuild struct {
type uiBugPage struct {
Header *uiHeader
+ Now time.Time
Bug *uiBug
+ DupOf *uiBugGroup
+ Dups *uiBugGroup
+ Similar *uiBugGroup
Crashes []*uiCrash
}
type uiBugGroup struct {
- Namespace string
- Bugs []*uiBug
+ Now time.Time
+ Caption string
+ ShowNamespace bool
+ Bugs []*uiBug
}
type uiBug struct {
@@ -184,14 +190,38 @@ func handleBug(c context.Context, w http.ResponseWriter, r *http.Request) error
if err != nil {
return err
}
+ var dupOf *uiBugGroup
+ if bug.DupOf != "" {
+ dup := new(Bug)
+ if err := datastore.Get(c, datastore.NewKey(c, "Bug", bug.DupOf, 0, nil), dup); err != nil {
+ return err
+ }
+ dupOf = &uiBugGroup{
+ Now: timeNow(c),
+ Caption: "Duplicate of",
+ Bugs: []*uiBug{createUIBug(c, dup, state, managers)},
+ }
+ }
uiBug := createUIBug(c, bug, state, managers)
crashes, err := loadCrashesForBug(c, bug)
if err != nil {
return err
}
+ dups, err := loadDupsForBug(c, bug, state, managers)
+ if err != nil {
+ return err
+ }
+ similar, err := loadSimilarBugs(c, bug, state)
+ if err != nil {
+ return err
+ }
data := &uiBugPage{
Header: h,
+ Now: timeNow(c),
Bug: uiBug,
+ DupOf: dupOf,
+ Dups: dups,
+ Similar: similar,
Crashes: crashes,
}
return serveTemplate(w, "bug.html", data)
@@ -237,25 +267,111 @@ func fetchBugs(c context.Context) ([]*uiBugGroup, error) {
uiBug := createUIBug(c, bug, state, managers[bug.Namespace])
groups[bug.Namespace] = append(groups[bug.Namespace], uiBug)
}
+ now := timeNow(c)
var res []*uiBugGroup
for ns, bugs := range groups {
sort.Sort(uiBugSorter(bugs))
res = append(res, &uiBugGroup{
- Namespace: ns,
- Bugs: bugs,
+ Now: now,
+ Caption: fmt.Sprintf("%v (%v)", ns, len(bugs)),
+ Bugs: bugs,
})
}
sort.Sort(uiBugGroupSorter(res))
return res, nil
}
-func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug {
- _, _, _, reportingIdx, status, link, err := needReport(c, "", state, bug)
+func loadDupsForBug(c context.Context, bug *Bug, state *ReportingState, managers []string) (
+ *uiBugGroup, error) {
+ bugHash := bugKeyHash(bug.Namespace, bug.Title, bug.Seq)
+ var dups []*Bug
+ _, err := datastore.NewQuery("Bug").
+ Filter("Status=", BugStatusDup).
+ Filter("DupOf=", bugHash).
+ GetAll(c, &dups)
if err != nil {
- status = err.Error()
+ return nil, err
}
- if status == "" {
- status = "???"
+ var results []*uiBug
+ for _, dup := range dups {
+ results = append(results, createUIBug(c, dup, state, managers))
+ }
+ group := &uiBugGroup{
+ Now: timeNow(c),
+ Caption: "Duplicates",
+ Bugs: results,
+ }
+ return group, nil
+}
+
+func loadSimilarBugs(c context.Context, bug *Bug, state *ReportingState) (*uiBugGroup, error) {
+ var similar []*Bug
+ _, err := datastore.NewQuery("Bug").
+ Filter("Title=", bug.Title).
+ GetAll(c, &similar)
+ if err != nil {
+ return nil, err
+ }
+ managers := make(map[string][]string)
+ var results []*uiBug
+ for _, similar := range similar {
+ if similar.Namespace == bug.Namespace && similar.Seq == bug.Seq {
+ continue
+ }
+ if managers[similar.Namespace] == nil {
+ mgrs, err := managerList(c, similar.Namespace)
+ if err != nil {
+ return nil, err
+ }
+ managers[similar.Namespace] = mgrs
+ }
+ results = append(results, createUIBug(c, similar, state, managers[similar.Namespace]))
+ }
+ group := &uiBugGroup{
+ Now: timeNow(c),
+ Caption: "Similar Bugs",
+ ShowNamespace: true,
+ Bugs: results,
+ }
+ return group, nil
+}
+
+func createUIBug(c context.Context, bug *Bug, state *ReportingState, managers []string) *uiBug {
+ reportingIdx, status, link := 0, "", ""
+ var err error
+ if bug.Status == BugStatusOpen {
+ _, _, _, reportingIdx, status, link, err = needReport(c, "", state, bug)
+ if err != nil {
+ status = err.Error()
+ }
+ if status == "" {
+ status = "???"
+ }
+ } else {
+ for i := range bug.Reporting {
+ bugReporting := &bug.Reporting[i]
+ if i == len(bug.Reporting)-1 ||
+ bug.Status == BugStatusInvalid && !bug.Reporting[i].Closed.IsZero() &&
+ bug.Reporting[i+1].Closed.IsZero() ||
+ (bug.Status == BugStatusFixed || bug.Status == BugStatusDup) &&
+ bug.Reporting[i].Closed.IsZero() {
+ reportingIdx = i
+ link = bugReporting.Link
+ switch bug.Status {
+ case BugStatusInvalid:
+ status = "invalid"
+ case BugStatusFixed:
+ status = "fixed"
+ case BugStatusDup:
+ status = "dup"
+ default:
+ status = fmt.Sprintf("unknown (%v)", bug.Status)
+ }
+ status = fmt.Sprintf("%v: closed as %v on %v",
+ bugReporting.Name, status, formatTime(bug.Closed))
+ break
+ }
+ }
}
id := bugKeyHash(bug.Namespace, bug.Title, bug.Seq)
uiBug := &uiBug{
@@ -499,6 +615,9 @@ type uiBugSorter []*uiBug
func (a uiBugSorter) Len() int { return len(a) }
func (a uiBugSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a uiBugSorter) Less(i, j int) bool {
+ if a[i].Namespace != a[j].Namespace {
+ return a[i].Namespace < a[j].Namespace
+ }
if a[i].ReportingIndex != a[j].ReportingIndex {
return a[i].ReportingIndex > a[j].ReportingIndex
}
@@ -515,4 +634,4 @@ type uiBugGroupSorter []*uiBugGroup
func (a uiBugGroupSorter) Len() int { return len(a) }
func (a uiBugGroupSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
-func (a uiBugGroupSorter) Less(i, j int) bool { return a[i].Namespace < a[j].Namespace }
+func (a uiBugGroupSorter) Less(i, j int) bool { return a[i].Caption < a[j].Caption }
diff --git a/dashboard/app/main.html b/dashboard/app/main.html
index 6a9c84f7..abdc632f 100644
--- a/dashboard/app/main.html
+++ b/dashboard/app/main.html
@@ -1,3 +1,10 @@
+{{/*
+Copyright 2017 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.
+
+Main page.
+*/}}
+
@@ -101,28 +108,7 @@
{{range $group := $.BugGroups}}
-
- {{.Namespace}} ({{len $group.Bugs}}):
-
- Title |
- Count |
- Repro |
- Last |
- Status |
- Patched |
-
- {{range $b := $group.Bugs}}
-
- {{$b.Title}} |
- {{$b.NumCrashes}} |
- {{formatReproLevel $b.ReproLevel}} |
- {{formatLateness $.Now $b.LastTime}} |
- {{if $b.Link}}{{$b.Status}}{{else}}{{$b.Status}}{{end}} |
- {{if $b.Commits}}{{len $b.PatchedOn}}/{{len $b.MissingOn}}{{end}} |
-
- {{end}}
-
-
+ {{template "bug_list" $group}}
{{end}}
diff --git a/dashboard/app/templates.html b/dashboard/app/templates.html
new file mode 100644
index 00000000..6fe5e011
--- /dev/null
+++ b/dashboard/app/templates.html
@@ -0,0 +1,36 @@
+{{/*
+Copyright 2017 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.
+*/}}
+
+{{/* List of bugs, invoked with *uiBugGroup */}}
+{{define "bug_list"}}
+{{if .}}
+{{if .Bugs}}
+
+ {{$.Caption}}:
+
+ {{if $.ShowNamespace}}Kernel | {{end}}
+ Title |
+ Count |
+ Repro |
+ Last |
+ Patched |
+ Status |
+
+ {{range $b := .Bugs}}
+
+ {{if $.ShowNamespace}}{{$b.Namespace}} | {{end}}
+ {{$b.Title}} |
+ {{$b.NumCrashes}} |
+ {{formatReproLevel $b.ReproLevel}} |
+ {{formatLateness $.Now $b.LastTime}} |
+ {{if $b.Commits}}{{len $b.PatchedOn}}/{{len $b.MissingOn}}{{end}} |
+ {{if $b.Link}}{{$b.Status}}{{else}}{{$b.Status}}{{end}} |
+
+ {{end}}
+
+
+{{end}}
+{{end}}
+{{end}}
diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go
index 5c868b1b..c9b8f868 100644
--- a/dashboard/app/util_test.go
+++ b/dashboard/app/util_test.go
@@ -28,6 +28,7 @@ import (
"golang.org/x/net/context"
"google.golang.org/appengine"
"google.golang.org/appengine/aetest"
+ "google.golang.org/appengine/datastore"
aemail "google.golang.org/appengine/mail"
"google.golang.org/appengine/user"
)
@@ -93,8 +94,16 @@ func caller(skip int) string {
func (c *Ctx) Close() {
if !c.t.Failed() {
- // Ensure that we can render bugs in the final test state.
+ // Ensure that we can render main page and all bugs in the final test state.
c.expectOK(c.GET("/"))
+ var bugs []*Bug
+ keys, err := datastore.NewQuery("Bug").GetAll(c.ctx, &bugs)
+ if err != nil {
+ c.t.Errorf("ERROR: failed to query bugs: %v", err)
+ }
+ for _, key := range keys {
+ c.expectOK(c.GET(fmt.Sprintf("/bug?id=%v", key.StringID())))
+ }
c.expectOK(c.GET("/email_poll"))
for len(c.emailSink) != 0 {
c.t.Errorf("ERROR: leftover email: %v", (<-c.emailSink).Body)