From f6dd7a77e4d2c0503672d68ce9f19b42bec99299 Mon Sep 17 00:00:00 2001 From: Taras Madan Date: Tue, 28 Jan 2025 15:17:17 +0100 Subject: pkg/covermerger: stream information about covered functions to dashboard --- pkg/coveragedb/coveragedb.go | 13 ++++++++++ pkg/covermerger/covermerger.go | 52 ++++++++++++++++++++++++++++++++----- pkg/covermerger/covermerger_test.go | 36 ++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 7 deletions(-) (limited to 'pkg') 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) + }) + } +} -- cgit mrf-deployment