aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorTaras Madan <tarasmadan@google.com>2025-08-21 19:00:57 +0200
committerTaras Madan <tarasmadan@google.com>2025-08-28 11:36:44 +0000
commit030f691787a240a447c5ec1491804160afa4050d (patch)
tree2d17dc5a254d3fb6d5586ab1826f316f42642a76 /pkg
parent3984c91733166d63d43fd5d4ca0a61a46e666a26 (diff)
pkg/manager: store titles stat in the crash dir
Diffstat (limited to 'pkg')
-rw-r--r--pkg/manager/crash.go3
-rw-r--r--pkg/report/title_stat.go88
-rw-r--r--pkg/report/title_stat_test.go76
3 files changed, 167 insertions, 0 deletions
diff --git a/pkg/manager/crash.go b/pkg/manager/crash.go
index cc36db721..f0fa58959 100644
--- a/pkg/manager/crash.go
+++ b/pkg/manager/crash.go
@@ -91,6 +91,9 @@ func (cs *CrashStore) SaveCrash(crash *Crash) (bool, error) {
writeOrRemove("tag", []byte(cs.Tag))
writeOrRemove("report", report.MergeReportBytes(reps))
writeOrRemove("machineInfo", crash.MachineInfo)
+ if err := report.AddTitleStat(filepath.Join(dir, "title-stat"), reps); err != nil {
+ return false, fmt.Errorf("report.AddTitleStat: %w", err)
+ }
return first, nil
}
diff --git a/pkg/report/title_stat.go b/pkg/report/title_stat.go
new file mode 100644
index 000000000..92d8079d5
--- /dev/null
+++ b/pkg/report/title_stat.go
@@ -0,0 +1,88 @@
+// Copyright 2025 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 report
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "maps"
+ "os"
+)
+
+func AddTitleStat(file string, reps []*Report) error {
+ var titles []string
+ for _, rep := range reps {
+ titles = append(titles, rep.Title)
+ }
+ stat, err := readStatFile(file)
+ if err != nil {
+ return fmt.Errorf("readStatFile: %w", err)
+ }
+ stat.add(titles)
+ if err := writeStatFile(file, stat); err != nil {
+ return fmt.Errorf("writeStatFile: %w", err)
+ }
+ return nil
+}
+
+func readStatFile(file string) (*titleStat, error) {
+ stat := &titleStat{}
+ if _, err := os.Stat(file); errors.Is(err, os.ErrNotExist) {
+ return stat, nil
+ }
+ data, err := os.ReadFile(file)
+ if err != nil {
+ return nil, err
+ }
+ if len(data) == 0 {
+ return stat, nil
+ }
+ if err := json.Unmarshal(data, stat); err != nil {
+ return nil, err
+ }
+ return stat, nil
+}
+
+func writeStatFile(file string, stat *titleStat) error {
+ data, err := json.MarshalIndent(stat, "", "\t")
+ if err != nil {
+ return err
+ }
+ if err := os.WriteFile(file, data, 0644); err != nil {
+ return err
+ }
+ return nil
+}
+
+type titleStatNodes map[string]*titleStat
+
+type titleStat struct {
+ Count int
+ Nodes titleStatNodes
+}
+
+func (ts *titleStat) add(reps []string) {
+ if len(reps) == 0 {
+ return
+ }
+ if ts.Nodes == nil {
+ ts.Nodes = make(titleStatNodes)
+ }
+ if ts.Nodes[reps[0]] == nil {
+ ts.Nodes[reps[0]] = &titleStat{}
+ }
+ ts.Nodes[reps[0]].Count++
+ ts.Nodes[reps[0]].add(reps[1:])
+}
+
+func (ts *titleStat) visit(cb func(int, ...string), titles ...string) {
+ if len(ts.Nodes) == 0 {
+ cb(ts.Count, titles...)
+ return
+ }
+ for title := range maps.Keys(ts.Nodes) {
+ ts.Nodes[title].visit(cb, append(titles, title)...)
+ }
+}
diff --git a/pkg/report/title_stat_test.go b/pkg/report/title_stat_test.go
new file mode 100644
index 000000000..216af456c
--- /dev/null
+++ b/pkg/report/title_stat_test.go
@@ -0,0 +1,76 @@
+// Copyright 2025 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 report
+
+import (
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestAddTitleStat(t *testing.T) {
+ tests := []struct {
+ name string
+ base string
+ reps [][]*Report
+ want *titleStat
+ }{
+ {
+ name: "read empty",
+ want: &titleStat{},
+ },
+ {
+ name: "add single",
+ reps: [][]*Report{{{Title: "warning 1"}}},
+ want: &titleStat{
+ Nodes: titleStatNodes{
+ "warning 1": {Count: 1},
+ },
+ },
+ },
+ {
+ name: "add chain",
+ reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}},
+ want: &titleStat{
+ Nodes: titleStatNodes{
+ "warning 1": {Count: 1,
+ Nodes: titleStatNodes{
+ "warning 2": {Count: 1},
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "add multi chains",
+ reps: [][]*Report{{{Title: "warning 1"}, {Title: "warning 2"}}, {{Title: "warning 1"}, {Title: "warning 3"}}},
+ want: &titleStat{
+ Nodes: titleStatNodes{
+ "warning 1": {Count: 2,
+ Nodes: titleStatNodes{
+ "warning 2": {Count: 1},
+ "warning 3": {Count: 1},
+ },
+ },
+ },
+ },
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+ tmpFile := t.TempDir() + "/test.input"
+ err := os.WriteFile(tmpFile, []byte(test.base), 0644)
+ assert.NoError(t, err)
+ for _, reps := range test.reps {
+ err = AddTitleStat(tmpFile, reps)
+ assert.NoError(t, err)
+ }
+ got, err := ReadStatFile(tmpFile)
+ assert.NoError(t, err)
+ assert.Equal(t, test.want, got)
+ })
+ }
+}