aboutsummaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorTaras Madan <tarasmadan@google.com>2025-01-28 15:17:17 +0100
committerTaras Madan <tarasmadan@google.com>2025-01-29 11:10:34 +0000
commitf6dd7a77e4d2c0503672d68ce9f19b42bec99299 (patch)
tree036ff5155e8a412933c92ec76cd958a589a44f09 /pkg
parent323c196b555b718b5bbd19fff0cc82947aa77431 (diff)
pkg/covermerger: stream information about covered functions to dashboard
Diffstat (limited to 'pkg')
-rw-r--r--pkg/coveragedb/coveragedb.go13
-rw-r--r--pkg/covermerger/covermerger.go52
-rw-r--r--pkg/covermerger/covermerger_test.go36
3 files changed, 94 insertions, 7 deletions
diff --git a/pkg/coveragedb/coveragedb.go b/pkg/coveragedb/coveragedb.go
index 0692bc131..f2c1cdd69 100644
--- a/pkg/coveragedb/coveragedb.go
+++ b/pkg/coveragedb/coveragedb.go
@@ -71,6 +71,19 @@ type MergedCoverageRecord struct {
FileData *Coverage
}
+// FuncLines represents the 'functions' table records.
+// It could be used to maps 'hitcounts' from 'files' table to the function names.
+type FuncLines struct {
+ FilePath string
+ FuncName string
+ Lines []int64 // List of lines we know belong to this function name according to the addr2line output.
+}
+
+type JSONLWrapper struct {
+ MCR *MergedCoverageRecord
+ FL *FuncLines
+}
+
func SaveMergeResult(ctx context.Context, client spannerclient.SpannerClient, descr *HistoryRecord, dec *json.Decoder,
sss []*subsystem.Subsystem) (int, error) {
if client == nil {
diff --git a/pkg/covermerger/covermerger.go b/pkg/covermerger/covermerger.go
index 34b534593..82ebe1766 100644
--- a/pkg/covermerger/covermerger.go
+++ b/pkg/covermerger/covermerger.go
@@ -22,9 +22,9 @@ import (
const (
KeyKernelRepo = "kernel_repo"
- KeyKernelBranch = "kernel_branch"
KeyKernelCommit = "kernel_commit"
KeyFilePath = "file_path"
+ KeyFuncName = "func_name"
KeyStartLine = "sl"
KeyHitCount = "hit_count"
KeyManager = "manager"
@@ -32,6 +32,7 @@ const (
type FileRecord struct {
FilePath string
+ FuncName string
RepoCommit
StartLine int
HitCount int
@@ -82,10 +83,15 @@ func MergeCSVWriteJSONL(config *Config, descr *coveragedb.HistoryRecord, csvRead
}
}
for fileMergeResult := range mergeResults {
- dashCoverageRecords := mergedCoverageRecords(fileMergeResult)
+ dashCoverageRecords, dashFuncLines := mergedCoverageRecords(fileMergeResult)
if encoder != nil {
+ for _, record := range dashFuncLines {
+ if err := encoder.Encode(coveragedb.JSONLWrapper{FL: record}); err != nil {
+ return fmt.Errorf("encoder.Encode(FuncLines): %w", err)
+ }
+ }
for _, record := range dashCoverageRecords {
- if err := encoder.Encode(record); err != nil {
+ if err := encoder.Encode(coveragedb.JSONLWrapper{MCR: record}); err != nil {
return fmt.Errorf("encoder.Encode(MergedCoverageRecord): %w", err)
}
}
@@ -107,22 +113,34 @@ func MergeCSVWriteJSONL(config *Config, descr *coveragedb.HistoryRecord, csvRead
const allManagers = "*"
-func mergedCoverageRecords(fmr *FileMergeResult) []*coveragedb.MergedCoverageRecord {
+func mergedCoverageRecords(fmr *FileMergeResult) ([]*coveragedb.MergedCoverageRecord, []*coveragedb.FuncLines) {
if !fmr.FileExists {
- return nil
+ return nil, nil
}
lines := maps.Keys(fmr.HitCounts)
slices.Sort(lines)
mgrStat := make(map[string]*coveragedb.Coverage)
mgrStat[allManagers] = &coveragedb.Coverage{}
+ funcLines := map[string]*coveragedb.FuncLines{}
for _, line := range lines {
mgrStat[allManagers].AddLineHitCount(line, fmr.HitCounts[line])
managerHitCounts := map[string]int64{}
+ var srcFuncs []string
for _, lineDetail := range fmr.LineDetails[line] {
+ srcFuncs = append(srcFuncs, lineDetail.FuncName)
manager := lineDetail.Manager
managerHitCounts[manager] += int64(lineDetail.HitCount)
}
+ if funcName := bestFuncName(srcFuncs); funcName != "" {
+ if _, ok := funcLines[funcName]; !ok {
+ funcLines[funcName] = &coveragedb.FuncLines{
+ FilePath: fmr.FilePath,
+ FuncName: funcName,
+ }
+ }
+ funcLines[funcName].Lines = append(funcLines[funcName].Lines, int64(line))
+ }
for manager, managerHitCount := range managerHitCounts {
if _, ok := mgrStat[manager]; !ok {
mgrStat[manager] = &coveragedb.Coverage{}
@@ -139,7 +157,27 @@ func mergedCoverageRecords(fmr *FileMergeResult) []*coveragedb.MergedCoverageRec
FileData: managerCoverage,
})
}
- return res
+ return res, maps.Values(funcLines)
+}
+
+// bestFuncName selects the most frequent function from the list of candidates.
+// If a function was renamed during the collection period, we have to pick one name to display the coverage.
+//
+// The better alternative is to get the function name from the C code. But it looks more complex for now.
+func bestFuncName(names []string) string {
+ stat := map[string]int{}
+ for _, name := range names {
+ stat[name]++
+ }
+ bestName := ""
+ bestCount := 0
+ for name, count := range stat {
+ if name != "" && count > bestCount {
+ bestName = name
+ bestCount = count
+ }
+ }
+ return bestName
}
func batchFileData(c *Config, targetFilePath string, records []*FileRecord) (*MergeResult, error) {
@@ -172,6 +210,8 @@ func makeRecord(fields, schema []string) (*FileRecord, error) {
switch key {
case KeyFilePath:
record.FilePath = val
+ case KeyFuncName:
+ record.FuncName = val
case KeyKernelRepo:
record.Repo = val
case KeyKernelCommit:
diff --git a/pkg/covermerger/covermerger_test.go b/pkg/covermerger/covermerger_test.go
index ab2b1efc5..e0d03c9a5 100644
--- a/pkg/covermerger/covermerger_test.go
+++ b/pkg/covermerger/covermerger_test.go
@@ -163,11 +163,12 @@ func TestMergerdCoverageRecords(t *testing.T) {
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
- gotRecords := mergedCoverageRecords(test.input)
+ gotRecords, gotFuncs := mergedCoverageRecords(test.input)
sort.Slice(gotRecords, func(i, j int) bool {
return gotRecords[i].Manager < gotRecords[j].Manager
})
assert.Equal(t, test.wantRecords, gotRecords, "records are not equal")
+ assert.Equal(t, 0, len(gotFuncs), "no functions expected")
})
}
}
@@ -241,6 +242,7 @@ samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,change_line.c,func1,3,
[
{
"FilePath":"change_line.c",
+ "FuncName":"func1",
"Repo":"git://repo",
"Commit":"commit1",
"StartLine":3,
@@ -271,6 +273,7 @@ samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit1,add_line.c,func1,2,0,2
[
{
"FilePath":"add_line.c",
+ "FuncName":"func1",
"Repo":"git://repo",
"Commit":"commit1",
"StartLine":2,
@@ -302,6 +305,7 @@ samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit2,not_changed.c,func1,4,
[
{
"FilePath":"not_changed.c",
+ "FuncName":"func1",
"Repo":"git://repo",
"Commit":"commit1",
"StartLine":3,
@@ -313,6 +317,7 @@ samp_time,1,360,arch,b1,ci-mock,git://repo,master,commit2,not_changed.c,func1,4,
[
{
"FilePath":"not_changed.c",
+ "FuncName":"func1",
"Repo":"git://repo",
"Commit":"commit2",
"StartLine":4,
@@ -397,3 +402,32 @@ func testConfig(repo, commit, workdir string) *Config {
FileVersProvider: &fileVersProviderMock{Workdir: workdir},
}
}
+
+func TestCheckedFuncName(t *testing.T) {
+ tests := []struct {
+ name string
+ input []string
+ want string
+ }{
+ {
+ name: "empty input",
+ want: "",
+ },
+ {
+ name: "single func",
+ input: []string{"func1", "func1"},
+ want: "func1",
+ },
+ {
+ name: "multi names",
+ input: []string{"", "", "", "func2", "func2", "func1", "func"},
+ want: "func2",
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ got := bestFuncName(test.input)
+ assert.Equal(t, test.want, got)
+ })
+ }
+}