package web
import (
"regexp"
"strings"
"testing"
)
type laneRow = struct {
Name string `json:"name"`
Bound int `json:"bound"`
Running int `json:"running"`
Admitted int `json:"admitted"`
Refused int `json:"refused"`
WaitedMs int64 `json:"waitedMs"`
Waiting int64 `json:"waiting"`
Queue int `json:"queue"`
Items []laneItem `json:"items"`
}
type laneItem = struct {
Verb string `json:"verb"`
Subject string `json:"subject"`
AgeMs int64 `json:"ageMs"`
Running bool `json:"running"`
}
func laneLine(t *testing.T, lanes ...laneRow) string {
t.Helper()
var d pageData
d.Ops.BudgetMs = 500
d.Ops.Lanes = lanes
var buf strings.Builder
if err := noHooks.template(opsPage).ExecuteTemplate(&buf, "content", d); err != nil {
t.Fatal(err)
}
m := regexp.MustCompile(`(?s)<p class="ops-phase-l">(.*?)</p>`).FindStringSubmatch(buf.String())
if m == nil {
t.Fatal("FAIL: the ops page drew no lane line")
}
return strings.Join(strings.Fields(regexp.MustCompile(`<[^>]*>`).ReplaceAllString(m[1], "")), " ")
}
// A saturated lane has to read as a diagnosis. All slots busy AND the
// queue full is capacity; all slots busy with an empty queue is a slot
// held too long. Running and refused alone cannot tell those apart,
// which is what left ten thousand "a store could not be ingested" rows
// unactionable.
func TestASaturatedLaneReadsAsADiagnosis(t *testing.T) {
got := laneLine(t, laneRow{Name: "ingest", Bound: 3, Running: 3,
Waiting: 22, Queue: 22, Admitted: 4201, Refused: 9871})
for _, want := range []string{"ingest", "3/3 running", "22 of 22 queued", "4,201 admitted", "9,871 told busy"} {
if !strings.Contains(got, want) {
t.Errorf("FAIL: the line is missing %q: %s", want, got)
}
}
}
// Exact figures, not rounded ones: this is the page an operator reads
// to pick a limit, and "9.9K told busy" is not a number you can set a
// queue from.
func TestTheLaneCountsAreExact(t *testing.T) {
got := laneLine(t, laneRow{Name: "ingest", Bound: 3, Admitted: 4201, Refused: 9871})
if strings.Contains(got, "K told busy") || strings.Contains(got, "K admitted") {
t.Errorf("FAIL: the counts are rounded: %s", got)
}
}
// A quiet lane says zero rather than leaving the word dangling. count
// renders a zero as nothing — right for a nav badge, and here it read
// "archive builds 0/2 running, admitted".
func TestAQuietLaneStillSaysZero(t *testing.T) {
got := laneLine(t, laneRow{Name: "archive builds", Bound: 2})
if !strings.Contains(got, "0 admitted") {
t.Errorf("FAIL: a lane that admitted nothing reads %q", got)
}
// A lane that never refused anybody does not mention refusals.
if strings.Contains(got, "told busy") {
t.Errorf("FAIL: a quiet lane reports refusals: %s", got)
}
}
// The queue depth is stated wherever a lane queues at all: "0 of 8
// queued" is how a reader learns the queue exists. A lane that does not
// queue says nothing, because there is no depth to report.
func TestDepthIsShownWhereverThereIsAQueue(t *testing.T) {
if got := laneLine(t, laneRow{Name: "bulk", Bound: 2, Queue: 8}); !strings.Contains(got, "0 of 8 queued") {
t.Errorf("FAIL: an idle queue is not stated: %s", got)
}
if got := laneLine(t, laneRow{Name: "store subprocesses", Bound: 8, Admitted: 483}); strings.Contains(got, "queued") {
t.Errorf("FAIL: a lane with no queue claims one: %s", got)
}
}
// heldList returns the in-flight lines the ops page drew, one string
// per item, tags stripped.
func heldList(t *testing.T, lanes ...laneRow) []string {
t.Helper()
var d pageData
d.Ops.BudgetMs = 500
d.Ops.Lanes = lanes
var buf strings.Builder
if err := noHooks.template(opsPage).ExecuteTemplate(&buf, "content", d); err != nil {
t.Fatal(err)
}
var out []string
for _, m := range regexp.MustCompile(`(?s)<li class="ops-held-i[^"]*"[^>]*>(.*?)</li>`).FindAllStringSubmatch(buf.String(), -1) {
out = append(out, strings.Join(strings.Fields(regexp.MustCompile(`<[^>]*>`).ReplaceAllString(m[1], "")), " "))
}
return out
}
// The counters say a lane refused nine thousand callers; only the list
// says WHICH units it is holding. Verb, subject and age, because the
// question a saturated lane raises is "what is in there" and none of
// the three answers it alone.
func TestTheLanePanelNamesWhatItIsHolding(t *testing.T) {
got := heldList(t, laneRow{Name: "ingest", Bound: 3, Running: 1, Queue: 22, Waiting: 1,
Items: []laneItem{
{Verb: "ingest", Subject: "pkgsrc", AgeMs: 94_000, Running: true},
{Verb: "first ingest", Subject: "tcl", AgeMs: 1200},
}})
if len(got) != 2 {
t.Fatalf("FAIL: drew %d held lines, want 2: %q", len(got), got)
}
for _, want := range []string{"ingest", "pkgsrc", "running", "1m 34s"} {
if !strings.Contains(got[0], want) {
t.Errorf("FAIL: the running line is missing %q: %s", want, got[0])
}
}
for _, want := range []string{"first ingest", "tcl", "queued", "1s"} {
if !strings.Contains(got[1], want) {
t.Errorf("FAIL: the queued line is missing %q: %s", want, got[1])
}
}
}
// A queued unit is drawn differently from a running one. They are not
// the same fact: one is work in progress, the other is work the lane
// has not started, and an operator reading a saturated lane is looking
// for exactly that boundary.
func TestQueuedAndRunningAreToldApart(t *testing.T) {
var d pageData
d.Ops.BudgetMs = 500
d.Ops.Lanes = []laneRow{{Name: "ingest", Bound: 1, Running: 1, Queue: 4, Waiting: 1,
Items: []laneItem{{Verb: "ingest", Subject: "a", Running: true}, {Verb: "ingest", Subject: "b"}}}}
var buf strings.Builder
if err := noHooks.template(opsPage).ExecuteTemplate(&buf, "content", d); err != nil {
t.Fatal(err)
}
body := buf.String()
for _, want := range []string{`data-running="true"`, `data-running="false"`, "ops-held-q"} {
if !strings.Contains(body, want) {
t.Errorf("FAIL: the panel does not mark %q", want)
}
}
}
// An idle lane draws no list at all. An empty <ul> is a promise of
// something to read, and a page of them would bury the one lane that
// actually has something in it.
func TestAnIdleLaneDrawsNoHeldList(t *testing.T) {
if got := heldList(t, laneRow{Name: "bulk", Bound: 2, Queue: 8}); len(got) != 0 {
t.Errorf("FAIL: an idle lane drew %q", got)
}
}
// held is the only place the age is rendered, and the units are chosen
// so the subject beside them stays readable.
func TestHeldChoosesTheUnitAReaderActsOn(t *testing.T) {
for _, tc := range []struct {
ms int64
want string
}{{0, "0ms"}, {412, "412ms"}, {1200, "1s"}, {59_999, "59s"}, {60_000, "1m"}, {94_000, "1m 34s"}, {600_000, "10m"}} {
if got := held(tc.ms); got != tc.want {
t.Errorf("FAIL: held(%d) = %q, want %q", tc.ms, got, tc.want)
}
}
}
// Rebuilds run on slots of their own, outside every lane, and the ops
// page lists them as one more row. Their queue has no cap, so its depth
// is stated without one, and each lane carries its numbers as data.
func TestRebuildSlotsReadAsALaneWithAnUncappedQueue(t *testing.T) {
var d pageData
d.Ops.BudgetMs = 500
d.Ops.Lanes = []laneRow{{Name: "rebuild", Bound: 2, Running: 2, Waiting: 3, Admitted: 7}}
var buf strings.Builder
if err := noHooks.template(opsPage).ExecuteTemplate(&buf, "content", d); err != nil {
t.Fatal(err)
}
html := buf.String()
if !regexp.MustCompile(`data-hook="lane" data-lane="rebuild" data-running="2" data-queued="3"`).MatchString(html) {
t.Errorf("FAIL: the rebuild row carries no lane hook with its numbers")
}
if got := laneLine(t, d.Ops.Lanes...); !strings.Contains(got, "3 queued") {
t.Errorf("FAIL: an uncapped queue's depth is not stated: %s", got)
} else {
t.Log("ok: the rebuild slots read as a lane with their queue depth")
}
}