Skip to content

Commit 939f5c2

Browse files
committed
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.
1 parent a2ada0e commit 939f5c2

File tree

4 files changed

+315
-80
lines changed

4 files changed

+315
-80
lines changed

dashboard/app/coverage.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func GetCoverageDBClient(ctx context.Context) spannerclient.SpannerClient {
5050

5151
type funcStyleBodyJS func(
5252
ctx context.Context, client spannerclient.SpannerClient,
53-
scope *coveragedb.SelectScope, onlyUnique bool, sss, managers []string,
53+
scope *coveragedb.SelectScope, onlyUnique bool, sss, managers []string, dataFilters cover.Format,
5454
) (template.CSS, template.HTML, template.HTML, error)
5555

5656
func handleCoverageHeatmap(c context.Context, w http.ResponseWriter, r *http.Request) error {
@@ -120,6 +120,15 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
120120
slices.Sort(subsystems)
121121

122122
onlyUnique := r.FormValue("unique-only") == "1"
123+
orderByCoverLinesDrop := r.FormValue("order-by-cover-lines-drop") == "1"
124+
minPercentDrop, err := strconv.Atoi("0" + r.FormValue("min-percent-drop"))
125+
if err != nil {
126+
return fmt.Errorf("min-percent-drop should be integer")
127+
}
128+
minCoverLinesDrop, err := strconv.Atoi("0" + r.FormValue("min-cover-lines-drop"))
129+
if err != nil {
130+
return fmt.Errorf("min-cover-lines-drop should be integer")
131+
}
123132

124133
var style template.CSS
125134
var body, js template.HTML
@@ -130,7 +139,13 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
130139
Manager: manager,
131140
Periods: periods,
132141
},
133-
onlyUnique, subsystems, managers); err != nil {
142+
onlyUnique, subsystems, managers,
143+
cover.Format{
144+
FilterMinPercentDrop: minPercentDrop,
145+
FilterMinCoveredLinesDrop: minCoverLinesDrop,
146+
OrderByCoveredLinesDrop: orderByCoverLinesDrop,
147+
DropCoveredLines0: onlyUnique,
148+
}); err != nil {
134149
return fmt.Errorf("failed to generate heatmap: %w", err)
135150
}
136151
return serveTemplate(w, "custom_content.html", struct {

pkg/cover/heatmap.go

Lines changed: 101 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
_ "embed"
1010
"fmt"
1111
"html/template"
12+
"slices"
1213
"sort"
1314
"strings"
1415

@@ -19,14 +20,15 @@ import (
1920
)
2021

2122
type templateHeatmapRow struct {
22-
Items []*templateHeatmapRow
23-
Name string
24-
Coverage []int64
25-
IsDir bool
26-
Depth int
27-
LastDayInstrumented int64
28-
Tooltips []string
29-
FileCoverageLink []string
23+
Items []*templateHeatmapRow
24+
Name string
25+
Coverage []int64 // in percents
26+
Covered []int64 // in lines count
27+
IsDir bool
28+
Depth int
29+
Summary int64 // right column, may be negative to show drops
30+
Tooltips []string
31+
FileCoverageLink []string
3032

3133
builder map[string]*templateHeatmapRow
3234
instrumented map[coveragedb.TimePeriod]int64
@@ -41,6 +43,43 @@ type templateHeatmap struct {
4143
Managers []string
4244
}
4345

46+
func (th *templateHeatmap) Filter(pred func(*templateHeatmapRow) bool) {
47+
th.Root.filter(pred)
48+
}
49+
50+
func (th *templateHeatmap) Transform(f func(*templateHeatmapRow)) {
51+
th.Root.transform(f)
52+
}
53+
54+
func (th *templateHeatmap) Sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) {
55+
th.Root.sort(pred)
56+
}
57+
58+
func (thm *templateHeatmapRow) transform(f func(*templateHeatmapRow)) {
59+
for _, item := range thm.Items {
60+
item.transform(f)
61+
}
62+
f(thm)
63+
}
64+
65+
func (thm *templateHeatmapRow) filter(pred func(*templateHeatmapRow) bool) {
66+
var filteredItems []*templateHeatmapRow
67+
for _, item := range thm.Items {
68+
item.filter(pred)
69+
if pred(item) {
70+
filteredItems = append(filteredItems, item)
71+
}
72+
}
73+
thm.Items = filteredItems
74+
}
75+
76+
func (thm *templateHeatmapRow) sort(pred func(*templateHeatmapRow, *templateHeatmapRow) int) {
77+
for _, item := range thm.Items {
78+
item.sort(pred)
79+
}
80+
slices.SortFunc(thm.Items, pred)
81+
}
82+
4483
func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, filePath string, instrumented, covered int64,
4584
timePeriod coveragedb.TimePeriod) {
4685
thm.instrumented[timePeriod] += instrumented
@@ -68,18 +107,9 @@ func (thm *templateHeatmapRow) addParts(depth int, pathLeft []string, filePath s
68107
thm.builder[nextElement].addParts(depth+1, pathLeft[1:], filePath, instrumented, covered, timePeriod)
69108
}
70109

71-
func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, skipEmpty bool) {
110+
func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget) {
72111
for _, item := range thm.builder {
73-
if !skipEmpty {
74-
thm.Items = append(thm.Items, item)
75-
continue
76-
}
77-
for _, hitCount := range item.covered {
78-
if hitCount > 0 {
79-
thm.Items = append(thm.Items, item)
80-
break
81-
}
82-
}
112+
thm.Items = append(thm.Items, item)
83113
}
84114
sort.Slice(thm.Items, func(i, j int) bool {
85115
if thm.Items[i].IsDir != thm.Items[j].IsDir {
@@ -94,6 +124,7 @@ func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, sk
94124
dateCoverage = percent(thm.covered[tp], thm.instrumented[tp])
95125
}
96126
thm.Coverage = append(thm.Coverage, dateCoverage)
127+
thm.Covered = append(thm.Covered, thm.covered[tp])
97128
thm.Tooltips = append(thm.Tooltips, fmt.Sprintf("Instrumented:\t%d blocks\nCovered:\t%d blocks",
98129
thm.instrumented[tp], thm.covered[tp]))
99130
if !thm.IsDir {
@@ -107,10 +138,10 @@ func (thm *templateHeatmapRow) prepareDataFor(pageColumns []pageColumnTarget, sk
107138
}
108139
if len(pageColumns) > 0 {
109140
lastDate := pageColumns[len(pageColumns)-1].TimePeriod
110-
thm.LastDayInstrumented = thm.instrumented[lastDate]
141+
thm.Summary = thm.instrumented[lastDate]
111142
}
112143
for _, item := range thm.builder {
113-
item.prepareDataFor(pageColumns, skipEmpty)
144+
item.prepareDataFor(pageColumns)
114145
}
115146
}
116147

@@ -119,7 +150,7 @@ type pageColumnTarget struct {
119150
Commit string
120151
}
121152

122-
func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails, hideEmpty bool) *templateHeatmap {
153+
func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails) *templateHeatmap {
123154
res := templateHeatmap{
124155
Root: &templateHeatmapRow{
125156
IsDir: true,
@@ -152,7 +183,7 @@ func filesCoverageToTemplateData(fCov []*coveragedb.FileCoverageWithDetails, hid
152183
res.Periods = append(res.Periods, fmt.Sprintf("%s(%d)", tp.DateTo.String(), tp.Days))
153184
}
154185

155-
res.Root.prepareDataFor(targetDateAndCommits, hideEmpty)
186+
res.Root.prepareDataFor(targetDateAndCommits)
156187
return &res
157188
}
158189

@@ -179,22 +210,30 @@ func stylesBodyJSTemplate(templData *templateHeatmap,
179210
template.HTML(js.Bytes()), nil
180211
}
181212

213+
type Format struct {
214+
FilterMinCoveredLinesDrop int
215+
OrderByCoveredLinesDrop bool
216+
DropCoveredLines0 bool
217+
}
218+
182219
func DoHeatMapStyleBodyJS(
183220
ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool,
184-
sss, managers []string) (template.CSS, template.HTML, template.HTML, error) {
221+
sss, managers []string, dataFilters Format) (template.CSS, template.HTML, template.HTML, error) {
185222
covAndDates, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique)
186223
if err != nil {
187224
return "", "", "", fmt.Errorf("failed to FilesCoverageWithDetails: %w", err)
188225
}
189-
templData := filesCoverageToTemplateData(covAndDates, onlyUnique)
226+
templData := filesCoverageToTemplateData(covAndDates)
190227
templData.Subsystems = sss
191228
templData.Managers = managers
229+
FormatResult(templData, dataFilters)
230+
192231
return stylesBodyJSTemplate(templData)
193232
}
194233

195234
func DoSubsystemsHeatMapStyleBodyJS(
196235
ctx context.Context, client spannerclient.SpannerClient, scope *coveragedb.SelectScope, onlyUnique bool,
197-
sss, managers []string) (template.CSS, template.HTML, template.HTML, error) {
236+
sss, managers []string, format Format) (template.CSS, template.HTML, template.HTML, error) {
198237
covWithDetails, err := coveragedb.FilesCoverageWithDetails(ctx, client, scope, onlyUnique)
199238
if err != nil {
200239
panic(err)
@@ -213,20 +252,54 @@ func DoSubsystemsHeatMapStyleBodyJS(
213252
ssCovAndDates = append(ssCovAndDates, &newRecord)
214253
}
215254
}
216-
templData := filesCoverageToTemplateData(ssCovAndDates, onlyUnique)
255+
templData := filesCoverageToTemplateData(ssCovAndDates)
217256
templData.Managers = managers
257+
FormatResult(templData, format)
218258
return stylesBodyJSTemplate(templData)
219259
}
220260

261+
func FormatResult(thm *templateHeatmap, format Format) {
262+
thm.Filter(func(row *templateHeatmapRow) bool {
263+
if row.IsDir && len(row.Items) > 0 {
264+
return true
265+
}
266+
return slices.Max(row.Covered)-row.Covered[len(row.Covered)-1] >= int64(format.FilterMinCoveredLinesDrop)
267+
})
268+
if format.DropCoveredLines0 {
269+
thm.Filter(func(row *templateHeatmapRow) bool {
270+
return slices.Max(row.Covered) > 0
271+
})
272+
}
273+
// The files are sorted lexicographically by default.
274+
if format.OrderByCoveredLinesDrop {
275+
thm.Sort(func(row1 *templateHeatmapRow, row2 *templateHeatmapRow) int {
276+
row1CoveredDrop := slices.Max(row1.Covered) - row1.Covered[len(row1.Covered)-1]
277+
row2CoveredDrop := slices.Max(row2.Covered) - row2.Covered[len(row2.Covered)-1]
278+
return int(row2CoveredDrop - row1CoveredDrop)
279+
})
280+
// We want to show the coverage drop numbers instead of total instrumented blocks.
281+
thm.Transform(func(row *templateHeatmapRow) {
282+
row.Summary = -1 * (slices.Max(row.Covered) - row.Covered[len(row.Covered)-1])
283+
})
284+
}
285+
}
286+
221287
func approximateInstrumented(points int64) string {
222288
dim := "_"
223-
if points > 10000 {
289+
if abs(points) > 10000 {
224290
dim = "K"
225291
points /= 1000
226292
}
227293
return fmt.Sprintf("%d%s", points, dim)
228294
}
229295

296+
func abs(a int64) int64 {
297+
if a < 0 {
298+
return -a
299+
}
300+
return a
301+
}
302+
230303
//go:embed templates/heatmap.html
231304
var templatesHeatmap string
232305
var templateHeatmapFuncs = template.FuncMap{

0 commit comments

Comments
 (0)