diff options
| author | Taras Madan <tarasmadan@google.com> | 2025-04-08 12:06:03 +0200 |
|---|---|---|
| committer | Taras Madan <tarasmadan@google.com> | 2025-04-11 08:23:33 +0000 |
| commit | c0efc34f5a3f6a494541381c0f5770200c2640b5 (patch) | |
| tree | 2710566336a9b334696831f12dd65331f5f69b56 /pkg/cover | |
| parent | 9448684652fb89ec79e794dbc1799837018f9bc6 (diff) | |
dashboard/app: enable coverage pages formatting
cover.Format controls the resulting view.
It allows to:
1. Remove records with 0 covered blocks.
2. Remove lines with low (user defined) coverage.
3. Order records by the covered lines drop value.
The corresponding GET parameters are:
1. Implicitly enabled for onlyUnique records.
2. min-cover-lines-drop=%d
3. order-by-cover-lines-drop=1
Diffstat (limited to 'pkg/cover')
| -rw-r--r-- | pkg/cover/heatmap.go | 129 | ||||
| -rw-r--r-- | pkg/cover/heatmap_test.go | 244 | ||||
| -rw-r--r-- | pkg/cover/templates/heatmap.html | 4 |
3 files changed, 299 insertions, 78 deletions
diff --git a/pkg/cover/heatmap.go b/pkg/cover/heatmap.go index efde4fed4..bdb79eec4 100644 --- a/pkg/cover/heatmap.go +++ b/pkg/cover/heatmap.go @@ -9,6 +9,7 @@ import ( _ "embed" "fmt" "html/template" + "slices" "sort" "strings" @@ -19,14 +20,15 @@ import ( ) type templateHeatmapRow struct { - Items []*templateHeatmapRow - Name string - Coverage []int64 - IsDir bool - Depth int - LastDayInstrumented int64 - Tooltips []string - FileCoverageLink []string + Items []*templateHeatmapRow + Name string + Coverage []int64 // in percent + Covered []int64 // in lines count + IsDir bool + Depth int + Summary int64 // right column, may be negative to show drops + Tooltips []string + FileCoverageLink []string builder map[string]*templateHeatmapRow instrumented map[coveragedb.TimePeriod]int64 @@ -41,6 +43,43 @@ type templateHeatmap struct { Managers []string } +func (th *templateHeatmap) Filter(pred func(*templateHeatmapRow) bool) { + th.Root.filter(pred) +} + +func (th *templateHeatmap) Transform(f func(*templateHeatmapRow)) { + th.Root.transform(f) +} + +func (th *templateHeatmap) Sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) { + th.Root.sort(pred) +} + +func (thm *templateHeatmapRow) transform(f func(*templateHeatmapRow)) { + for _, item := range thm.Items { + item.transform(f) + } + f(thm) +} + +func (thm *templateHeatmapRow) filter(pred func(*templateHeatmapRow) bool) { + var filteredItems []*templateHeatmapRow + for _, item := range thm.Items { + item.filter(pred) + if pred(item) { + filteredItems = append(filteredItems, item) + } + } + thm.Items = filteredItems +} + +func (thm *templateHeatmapRow) sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) { + for _, item := range thm.Items { + item.sort(pred) + } + slices.SortFunc(thm.Items, pred) +} + func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, filePath string, instrumented, covered int64, timePeriod coveragedb.TimePeriod) { thm.instrumented[timePeriod] += instrumented @@ -68,18 +107,9 @@ func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, filePath s thm.builder[nextElement].addParts(depth+1, pathLeft[1:], filePath, instrumented, covered, timePeriod) } -func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, skipEmpty bool) { +func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget) { for _, item := range thm.builder { - if !skipEmpty { - thm.Items = append(thm.Items, item) - continue - } - for _, hitCount := range item.covered { - if hitCount > 0 { - thm.Items = append(thm.Items, item) - break - } - } + thm.Items = append(thm.Items, item) } sort.Slice(thm.Items, func(i, j int) bool { if thm.Items[i].IsDir != thm.Items[j].IsDir { @@ -94,6 +124,7 @@ func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, sk dateCoverage = Percent(thm.covered[tp], thm.instrumented[tp]) } thm.Coverage = append(thm.Coverage, dateCoverage) + thm.Covered = append(thm.Covered, thm.covered[tp]) thm.Tooltips = append(thm.Tooltips, fmt.Sprintf("Instrumented:\t%d blocks\nCovered:\t%d blocks", thm.instrumented[tp], thm.covered[tp])) if !thm.IsDir { @@ -107,10 +138,10 @@ func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, sk } if len(pageColumns) > 0 { lastDate := pageColumns[len(pageColumns)-1].TimePeriod - thm.LastDayInstrumented = thm.instrumented[lastDate] + thm.Summary = thm.instrumented[lastDate] } for _, item := range thm.builder { - item.prepareDataFor(pageColumns, skipEmpty) + item.prepareDataFor(pageColumns) } } @@ -119,7 +150,7 @@ type pageColumnTarget struct { Commit string } -func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails, hideEmpty bool) *templateHeatmap { +func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails) *templateHeatmap { res := templateHeatmap{ Root: &templateHeatmapRow{ IsDir: true, @@ -152,7 +183,7 @@ func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails, hid res.Periods = append(res.Periods, fmt.Sprintf("%s(%d)", tp.DateTo.String(), tp.Days)) } - res.Root.prepareDataFor(targetDateAndCommits, hideEmpty) + res.Root.prepareDataFor(targetDateAndCommits) return &res } @@ -179,22 +210,30 @@ func stylesBodyJSTemplate(templData *templateHeatmap, template.HTML(js.Bytes()), nil } +type Format struct { + FilterMinCoveredLinesDrop int + OrderByCoveredLinesDrop bool + DropCoveredLines0 bool +} + func DoHeatMapStyleBodyJS( ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool, - sss, managers []string) (template.CSS, template.HTML, template.HTML, error) { + sss, managers []string, dataFilters Format) (template.CSS, template.HTML, template.HTML, error) { covAndDates, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique) if err != nil { return "", "", "", fmt.Errorf("failed to FilesCoverageWithDetails: %w", err) } - templData := filesCoverageToTemplateData(covAndDates, onlyUnique) + templData := filesCoverageToTemplateData(covAndDates) templData.Subsystems = sss templData.Managers = managers + FormatResult(templData, dataFilters) + return stylesBodyJSTemplate(templData) } func DoSubsystemsHeatMapStyleBodyJS( ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool, - sss, managers []string) (template.CSS, template.HTML, template.HTML, error) { + sss, managers []string, format Format) (template.CSS, template.HTML, template.HTML, error) { covWithDetails, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique) if err != nil { panic(err) @@ -213,20 +252,54 @@ func DoSubsystemsHeatMapStyleBodyJS( ssCovAndDates = append(ssCovAndDates, &newRecord) } } - templData := filesCoverageToTemplateData(ssCovAndDates, onlyUnique) + templData := filesCoverageToTemplateData(ssCovAndDates) templData.Managers = managers + FormatResult(templData, format) return stylesBodyJSTemplate(templData) } +func FormatResult(thm *templateHeatmap, format Format) { + thm.Filter(func(row *templateHeatmapRow) bool { + if row.IsDir && len(row.Items) > 0 { + return true + } + return slices.Max(row.Covered)-row.Covered[len(row.Covered)-1] >= int64(format.FilterMinCoveredLinesDrop) + }) + if format.DropCoveredLines0 { + thm.Filter(func(row *templateHeatmapRow) bool { + return slices.Max(row.Covered) > 0 + }) + } + // The files are sorted lexicographically by default. + if format.OrderByCoveredLinesDrop { + thm.Sort(func(row1 *templateHeatmapRow, row2 *templateHeatmapRow) int { + row1CoveredDrop := slices.Max(row1.Covered) - row1.Covered[len(row1.Covered)-1] + row2CoveredDrop := slices.Max(row2.Covered) - row2.Covered[len(row2.Covered)-1] + return int(row2CoveredDrop - row1CoveredDrop) + }) + // We want to show the coverage drop numbers instead of total instrumented blocks. + thm.Transform(func(row *templateHeatmapRow) { + row.Summary = -1 * (slices.Max(row.Covered) - row.Covered[len(row.Covered)-1]) + }) + } +} + func approximateInstrumented(points int64) string { dim := "_" - if points > 10000 { + if abs(points) > 10000 { dim = "K" points /= 1000 } return fmt.Sprintf("%d%s", points, dim) } +func abs(a int64) int64 { + if a < 0 { + return -a + } + return a +} + //go:embed templates/heatmap.html var templatesHeatmap string var templateHeatmapFuncs = template.FuncMap{ diff --git a/pkg/cover/heatmap_test.go b/pkg/cover/heatmap_test.go index 6a88ffee2..41e643cf2 100644 --- a/pkg/cover/heatmap_test.go +++ b/pkg/cover/heatmap_test.go @@ -14,10 +14,9 @@ import ( func TestFilesCoverageToTemplateData(t *testing.T) { tests := []struct { - name string - input []*coveragedb.FileCoverageWithDetails - hideEmpty bool - want *templateHeatmap + name string + input []*coveragedb.FileCoverageWithDetails + want *templateHeatmap }{ { name: "empty input", @@ -43,11 +42,12 @@ func TestFilesCoverageToTemplateData(t *testing.T) { Root: &templateHeatmapRow{ Items: []*templateHeatmapRow{ { - Name: "file1", - Coverage: []int64{100}, - IsDir: false, - Depth: 0, - LastDayInstrumented: 1, + Name: "file1", + Coverage: []int64{100}, + Covered: []int64{1}, + IsDir: false, + Depth: 0, + Summary: 1, Tooltips: []string{ "Instrumented:\t1 blocks\nCovered:\t1 blocks", }, @@ -55,11 +55,12 @@ func TestFilesCoverageToTemplateData(t *testing.T) { "/coverage/file?dateto=2024-07-01&period=day&commit=commit1&filepath=file1"}, }, }, - Name: "", - Coverage: []int64{100}, - IsDir: true, - Depth: 0, - LastDayInstrumented: 1, + Name: "", + Coverage: []int64{100}, + Covered: []int64{1}, + IsDir: true, + Depth: 0, + Summary: 1, Tooltips: []string{ "Instrumented:\t1 blocks\nCovered:\t1 blocks", }, @@ -91,11 +92,12 @@ func TestFilesCoverageToTemplateData(t *testing.T) { { Items: []*templateHeatmapRow{ { - Name: "file1", - Coverage: []int64{100, 0}, - IsDir: false, - Depth: 1, - LastDayInstrumented: 0, + Name: "file1", + Coverage: []int64{100, 0}, + Covered: []int64{1, 0}, + IsDir: false, + Depth: 1, + Summary: 0, Tooltips: []string{ "Instrumented:\t1 blocks\nCovered:\t1 blocks", "Instrumented:\t0 blocks\nCovered:\t0 blocks", @@ -105,11 +107,12 @@ func TestFilesCoverageToTemplateData(t *testing.T) { "/coverage/file?dateto=2024-07-02&period=day&commit=commit2&filepath=dir/file1"}, }, { - Name: "file2", - Coverage: []int64{0, 0}, - IsDir: false, - Depth: 1, - LastDayInstrumented: 1, + Name: "file2", + Coverage: []int64{0, 0}, + Covered: []int64{0, 0}, + IsDir: false, + Depth: 1, + Summary: 1, Tooltips: []string{ "Instrumented:\t0 blocks\nCovered:\t0 blocks", "Instrumented:\t1 blocks\nCovered:\t0 blocks", @@ -119,20 +122,22 @@ func TestFilesCoverageToTemplateData(t *testing.T) { "/coverage/file?dateto=2024-07-02&period=day&commit=commit2&filepath=dir/file2"}, }, }, - Name: "dir", - Coverage: []int64{100, 0}, - IsDir: true, - Depth: 0, - LastDayInstrumented: 1, + Name: "dir", + Coverage: []int64{100, 0}, + Covered: []int64{1, 0}, + IsDir: true, + Depth: 0, + Summary: 1, Tooltips: []string{ "Instrumented:\t1 blocks\nCovered:\t1 blocks", "Instrumented:\t1 blocks\nCovered:\t0 blocks", }, }, }, - Name: "", - Coverage: []int64{100, 0}, - LastDayInstrumented: 1, + Name: "", + Coverage: []int64{100, 0}, + Covered: []int64{1, 0}, + Summary: 1, Tooltips: []string{ "Instrumented:\t1 blocks\nCovered:\t1 blocks", "Instrumented:\t1 blocks\nCovered:\t0 blocks", @@ -142,33 +147,176 @@ func TestFilesCoverageToTemplateData(t *testing.T) { Periods: []string{"2024-07-01(1)", "2024-07-02(1)"}, }, }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := filesCoverageToTemplateData(test.input) + assert.EqualExportedValues(t, test.want, got) + }) + } +} + +// nolint: dupl +func TestFormatResult(t *testing.T) { + tests := []struct { + name string + sample *templateHeatmap + input Format + want *templateHeatmap + }{ { - name: "hide empty", - hideEmpty: true, - input: []*coveragedb.FileCoverageWithDetails{ - { - Filepath: "file1", - Instrumented: 1, - Covered: 0, - TimePeriod: makeTimePeriod(t, civil.Date{Year: 2024, Month: time.July, Day: 1}, coveragedb.DayPeriod), - Commit: "commit1", + name: "remove empty", + input: Format{ + DropCoveredLines0: true, + }, + sample: &templateHeatmap{ + Root: &templateHeatmapRow{ + Items: []*templateHeatmapRow{ + { + Items: []*templateHeatmapRow{ + { + Name: "file1", + Covered: []int64{0, 0}, + Depth: 1, + }, + }, + Name: "dir", + Covered: []int64{1, 0}, + IsDir: true, + }, + }, + Covered: []int64{1, 0}, + IsDir: true, }, }, want: &templateHeatmap{ Root: &templateHeatmapRow{ - Coverage: []int64{0}, - IsDir: true, - LastDayInstrumented: 1, - Tooltips: []string{"Instrumented:\t1 blocks\nCovered:\t0 blocks"}, + Items: []*templateHeatmapRow{ + { + Name: "dir", + Covered: []int64{1, 0}, + IsDir: true, + }, + }, + Covered: []int64{1, 0}, + IsDir: true, + }, + }, + }, + { + name: "remove small lines drop", + input: Format{ + FilterMinCoveredLinesDrop: 10, + }, + sample: &templateHeatmap{ + Root: &templateHeatmapRow{ + Items: []*templateHeatmapRow{ + { + Items: []*templateHeatmapRow{ + { + Name: "file1", + Covered: []int64{5, 0}, + Depth: 1, + }, + { + Name: "file2", + Covered: []int64{10, 0}, + Depth: 1, + }, + }, + Name: "dir", + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + want: &templateHeatmap{ + Root: &templateHeatmapRow{ + Items: []*templateHeatmapRow{ + { + Items: []*templateHeatmapRow{ + { + Name: "file2", + Covered: []int64{10, 0}, + Depth: 1, + }, + }, + Name: "dir", + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + }, + { + name: "order by the lines drop", + input: Format{ + OrderByCoveredLinesDrop: true, + }, + sample: &templateHeatmap{ + Root: &templateHeatmapRow{ + Items: []*templateHeatmapRow{ + { + Items: []*templateHeatmapRow{ + { + Name: "file1", + Covered: []int64{5, 0}, + Depth: 1, + }, + { + Name: "file2", + Covered: []int64{10, 0}, + Depth: 1, + }, + }, + Name: "dir", + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + want: &templateHeatmap{ + Root: &templateHeatmapRow{ + Items: []*templateHeatmapRow{ + { + Items: []*templateHeatmapRow{ + { + Name: "file2", + Covered: []int64{10, 0}, + Depth: 1, + Summary: -10, + }, + { + Name: "file1", + Covered: []int64{5, 0}, + Depth: 1, + Summary: -5, + }, + }, + Name: "dir", + Covered: []int64{1, 1}, + IsDir: true, + }, + }, + Covered: []int64{1, 1}, + IsDir: true, }, - Periods: []string{"2024-07-01(1)"}, }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - got := filesCoverageToTemplateData(test.input, test.hideEmpty) - assert.EqualExportedValues(t, test.want, got) + FormatResult(test.sample, test.input) + assert.Equal(t, test.want, test.sample) }) } } diff --git a/pkg/cover/templates/heatmap.html b/pkg/cover/templates/heatmap.html index cc5da9bcc..281fe84ef 100644 --- a/pkg/cover/templates/heatmap.html +++ b/pkg/cover/templates/heatmap.html @@ -159,7 +159,7 @@ </div> {{ end }} <div class="instrumented_column vertical_text"> - {{ approxInstr .Root.LastDayInstrumented }} blocks + {{ approxInstr .Root.Summary }} blocks </div> </li> <li> @@ -215,7 +215,7 @@ </div> {{ end }} <div class="instrumented_column"> - {{ approxInstr $child.LastDayInstrumented }} + {{ approxInstr $child.Summary }} </div> </div> {{ if $child.IsDir }} |
