Skip to content
quarry Artifact [42bfea40a2]
Login

Artifact [42bfea40a2]

Artifact 42bfea40a245ee06b050af32fc9d50244cabdd78d915429d83853d06a271ea15:


package obs

import (
	"reflect"
	"testing"
	"time"
)

var t0 = time.Date(2026, 9, 16, 12, 0, 0, 0, time.UTC)

// The line answers the same question as the number above it: how many
// were running, not how many started.
func TestConcurrencyCountsWhatWasRunning(t *testing.T) {
	// A window of 4 bins over 4 seconds: bins start at +0s, +1s, +2s, +3s.
	now := t0.Add(4 * time.Second)
	subs := []Subprocess{
		// Runs the whole window: counted in every bin.
		{At: t0.Add(-time.Second), Duration: 10 * time.Second},
		// Starts and ends inside the second bin: counted once.
		{At: t0.Add(1100 * time.Millisecond), Duration: 300 * time.Millisecond},
		// Ends exactly at a bin start: not running at that instant.
		{At: t0.Add(2 * time.Second), Duration: time.Second},
	}
	got := ConcurrencyBins(subs, now, 4*time.Second, 4)
	if want := []int{1, 1, 2, 1}; !reflect.DeepEqual(got, want) {
		t.Fatalf("concurrency = %v, want %v", got, want)
	}
	// Counting starts instead would read 1,1,1,0 — a different shape
	// under the same number, which is the reason this counts overlap.
}

// Events outside the window are dropped, not piled into an end bin.
func TestCountBinsKeepsTheWindowHonest(t *testing.T) {
	now := t0.Add(4 * time.Second)
	ats := []time.Time{
		t0.Add(-time.Hour),              // long before: dropped
		t0.Add(100 * time.Millisecond),  // bin 0
		t0.Add(200 * time.Millisecond),  // bin 0
		t0.Add(3500 * time.Millisecond), // bin 3
		now,                             // the instant now: last bin
	}
	got := CountBins(ats, now, 4*time.Second, 4)
	if want := []int{2, 0, 0, 2}; !reflect.DeepEqual(got, want) {
		t.Fatalf("counts = %v, want %v", got, want)
	}
}

// A degenerate ask draws nothing rather than dividing by zero.
func TestSeriesRefuseDegenerateWindows(t *testing.T) {
	if got := ConcurrencyBins(nil, t0, 0, 4); got != nil {
		t.Errorf("zero window = %v, want nil", got)
	}
	if got := CountBins(nil, t0, time.Minute, 0); got != nil {
		t.Errorf("zero bins = %v, want nil", got)
	}
}

// The panel's question is "what asked for these", and the answer has
// two namespaces: a background activity names itself, a request-driven
// call is named by the route it served.
func TestAttributionNamesBackgroundAndRoutes(t *testing.T) {
	subs := []Subprocess{
		{Verb: "log", Repo: "pkgsrc", Reason: ReasonWarm},
		{Verb: "log", Repo: "pkgsrc", Reason: ReasonWarm},
		{Verb: "log", Repo: "tcl", RenderID: "r1"},
		{Verb: "cat", Repo: "tcl", RenderID: "r1"},
		{Verb: "cat", Repo: "tcl", RenderID: "r2"},
		{Verb: "cat", Repo: "sqlite", Reason: ReasonIngest},
		// A render that has fallen out of the ring: counted nowhere
		// rather than attributed to a route nobody can name.
		{Verb: "cat", Repo: "sqlite", RenderID: "gone"},
	}
	routes := map[string]string{"r1": "GET /r/{repo}/file", "r2": "GET /r/{repo}/file"}
	got := AttributeVerbs(subs, func(id string) string { return routes[id] }, 2)

	if len(got) != 2 || got[0].Verb != "cat" || got[1].Verb != "log" {
		t.Fatalf("verbs = %+v, want cat then log", got)
	}
	log := got[1]
	if len(log.Repos) != 2 || log.Repos[0] != (Tally{"pkgsrc", 2}) || log.Repos[1] != (Tally{"tcl", 1}) {
		t.Errorf("log repos = %+v", log.Repos)
	}
	// The warm pass is named, and it outranks the one render.
	if len(log.Causes) != 2 || log.Causes[0] != (Tally{ReasonWarm, 2}) {
		t.Errorf("log causes = %+v, want warm first", log.Causes)
	}
	cat := got[0]
	if len(cat.Causes) != 2 || cat.Causes[0] != (Tally{"GET /r/{repo}/file", 2}) {
		t.Errorf("cat causes = %+v, want the route first", cat.Causes)
	}
	// Seven calls in, but only six attributable: the unresolvable
	// render adds to no cause.
	total := 0
	for _, c := range cat.Causes {
		total += c.Count
	}
	if total != 3 {
		t.Errorf("cat causes total = %d, want 3 (the unnamed render is not guessed)", total)
	}
}

// Ties break by name so two refreshes that saw the same counts draw the
// same order.
func TestAttributionIsStableOnTies(t *testing.T) {
	subs := []Subprocess{
		{Verb: "ls", Repo: "beta", Reason: ReasonSync},
		{Verb: "ls", Repo: "alpha", Reason: ReasonIngest},
	}
	for i := 0; i < 5; i++ {
		got := AttributeVerbs(subs, nil, 4)
		if got[0].Repos[0].Name != "alpha" || got[0].Causes[0].Name != ReasonIngest {
			t.Fatalf("unstable order: %+v", got[0])
		}
	}
}