aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/cover
diff options
context:
space:
mode:
authorTaras Madan <tarasmadan@google.com>2025-04-08 12:06:03 +0200
committerTaras Madan <tarasmadan@google.com>2025-04-11 08:23:33 +0000
commitc0efc34f5a3f6a494541381c0f5770200c2640b5 (patch)
tree2710566336a9b334696831f12dd65331f5f69b56 /pkg/cover
parent9448684652fb89ec79e794dbc1799837018f9bc6 (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.go129
-rw-r--r--pkg/cover/heatmap_test.go244
-rw-r--r--pkg/cover/templates/heatmap.html4
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 }}