Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions dashboard/app/coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func GetCoverageDBClient(ctx context.Context) spannerclient.SpannerClient {

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

func handleCoverageHeatmap(c context.Context, w http.ResponseWriter, r *http.Request) error {
Expand Down Expand Up @@ -127,6 +127,12 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
slices.Sort(subsystems)

onlyUnique := r.FormValue("unique-only") == "1"
orderByCoverLinesDrop := r.FormValue("order-by-cover-lines-drop") == "1"
// Prefixing "0" we don't fail on empty string.
minCoverLinesDrop, err := strconv.Atoi("0" + r.FormValue("min-cover-lines-drop"))
if err != nil {
return fmt.Errorf("min-cover-lines-drop should be integer")
}

var style template.CSS
var body, js template.HTML
Expand All @@ -137,7 +143,12 @@ func handleHeatmap(c context.Context, w http.ResponseWriter, r *http.Request, f
Manager: manager,
Periods: periods,
},
onlyUnique, subsystems, managers); err != nil {
onlyUnique, subsystems, managers,
cover.Format{
FilterMinCoveredLinesDrop: minCoverLinesDrop,
OrderByCoveredLinesDrop: orderByCoverLinesDrop,
DropCoveredLines0: onlyUnique,
}); err != nil {
return fmt.Errorf("failed to generate heatmap: %w", err)
}
return serveTemplate(w, "custom_content.html", struct {
Expand Down
129 changes: 101 additions & 28 deletions pkg/cover/heatmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
_ "embed"
"fmt"
"html/template"
"slices"
"sort"
"strings"

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
}

Expand All @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand All @@ -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{
Expand Down
Loading