diff options
Diffstat (limited to 'pkg/report')
| -rw-r--r-- | pkg/report/title_stat.go | 88 | ||||
| -rw-r--r-- | pkg/report/title_stat_test.go | 76 |
2 files changed, 164 insertions, 0 deletions
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) + }) + } +} |
