package web
import (
"fmt"
"testing"
)
// bigSample is a repository the size the coarsening exists for: years of
// history, many people, one check-in an hour.
func bigSample(days, people int) activityOut {
act := activityOut{Since: "2019-01-01", Until: "2026-01-01", Window: "all"}
for i := 0; i < people; i++ {
act.Authors = append(act.Authors, activityAuthor{Name: fmt.Sprintf("person-%d", i)})
}
for p := 0; p < 40; p++ {
act.Paths = append(act.Paths, fmt.Sprintf("src/module-%02d/file.c", p))
}
for d := 0; d < days; d++ {
for h := 0; h < 24; h += 3 {
a := (d + h) % people
act.Rows = append(act.Rows, activityRow{Day: d, Hour: h, Author: a, Files: 3,
Hash: fmt.Sprintf("%040x", d*100+h), Branch: "trunk"})
act.Touches = append(act.Touches, activityTouch{Day: d, Path: (d + h) % 40, Author: a})
}
}
return act
}
// The claim: the width is chosen from the data, so the picture is drawn
// ONCE. The page used to render the whole thing, measure it, double the
// bucket and render again — up to five discarded renders of every
// check-in before the one it kept.
//
// Pinned by rendering at the chosen width and checking it fits: if it
// does, the caller's loop breaks on its first pass.
func TestChosenBucketFitsOnTheFirstRender(t *testing.T) {
for _, axis := range []string{"clock", "tree"} {
act := bigSample(2500, 12)
b := sampleBucket(axis, act, activityByteBudget)
got := len(coreSampleSVG("sqlite", axis, act, -1, b))
if got > activityByteBudget && b < activityBucketMax {
t.Errorf("%s: chose bucket %d and the drawing is %d bytes over budget, "+
"so the loop still has to render again", axis, b, got-activityByteBudget)
}
}
}
// ...and not needlessly coarse. Choosing 32 for everything would also
// "fit on the first render" while throwing away every picture worth
// looking at, so the other half of the claim is that a small repository
// is still drawn check-in by check-in.
func TestASmallRepositoryIsStillDrawnFine(t *testing.T) {
act := bigSample(30, 3)
for _, axis := range []string{"clock", "tree"} {
if b := sampleBucket(axis, act, activityByteBudget); b != 1 {
t.Errorf("%s: a 30-day repository was coarsened to %d-day buckets", axis, b)
}
}
}
// The per-mark costs are measurements, and a measurement rots. Each
// carries headroom over what was measured; this fails if the markup
// grows past that headroom (widths that no longer fit, an extra render
// of everything) or shrinks far below it (widths coarser than the
// picture needed).
func TestMarkCostsMatchTheDrawing(t *testing.T) {
act := bigSample(120, 4)
for _, tc := range []struct {
name string
axis string
bucket int
assume int
}{
{"clock, per check-in", "clock", 1, sampleClockFine},
{"clock, coarsened", "clock", 4, sampleClockCoarse},
{"tree, per touch", "tree", 1, sampleTreeFine},
{"tree, coarsened", "tree", 4, sampleTreeCoarse},
} {
marks := marksAt(t, tc.axis, act, tc.bucket)
if marks == 0 {
t.Fatalf("%s: no marks in the fixture", tc.name)
}
per := (len(coreSampleSVG("sqlite", tc.axis, act, -1, tc.bucket)) - sampleFixedBytes) / marks
switch {
case per > tc.assume:
t.Errorf("%s costs %d bytes, past the %d the estimate allows: chosen widths "+
"will not fit and the page renders everything twice", tc.name, per, tc.assume)
case per < tc.assume*2/3:
t.Errorf("%s costs only %d bytes against an assumed %d: the estimate has gone "+
"pessimistic and coarsens pictures that did not need it", tc.name, per, tc.assume)
}
}
}
// marksAt counts what a width would draw, the way sampleBucket does.
func marksAt(t *testing.T, axis string, act activityOut, bucket int) int {
t.Helper()
type cell struct{ b, y, a int }
seen := map[cell]struct{}{}
if axis == "tree" {
n := 0
for _, tc := range act.Touches {
if tc.Inherited != 0 || tc.Path < 0 || tc.Path >= len(act.Paths) {
continue
}
n++
seen[cell{tc.Day / bucket, tc.Path, tc.Author}] = struct{}{}
}
if bucket == 1 {
return n
}
return len(seen)
}
if bucket == 1 {
return len(act.Rows)
}
for _, r := range act.Rows {
seen[cell{r.Day / bucket, r.Hour, r.Author}] = struct{}{}
}
return len(seen)
}