/**
* The page behind the two selection tiles. The tiles carry one number each,
* which is all a dashboard glanced at from across a room can hold; everything a
* person wants once that number has caught their eye is here, at full width and
* with nothing abbreviated: how close each of a pull request's lanes is to its
* budget, whether the cost model the lanes are packed by still describes what
* they spend, and every test held back from those lanes together with the
* measurement that held it back.
*
* Following the dashboard's values (README.md): it reports on the system. It
* names tests, never the people who wrote or touched them.
*/
import {
type CalibrationHealth,
type Manifest,
type ManifestEntry,
type TestIdentity,
testIdentityKey,
} from "@commonfabric/test-support/records";
import { type LivePageContent, livePageResponse } from "./live-page.ts";
import {
compactSpan,
escapeHtml,
groupDigits,
} from "./lib.ts";
import { STATUS_EDGE, STATUS_WASH } from "./palette.ts";
import {
collectionSub,
sharedTestSelection,
} from "./test-selection-history.ts";
import {
corpusSeconds,
FLAKE_EXCLUSION_FALLBACK,
FLAKE_WINDOW_FALLBACK_DAYS,
flakyCount,
laneBudgetOf,
laneTestCount,
type ManifestReader,
numberDial,
selectedCount,
selectedSeconds,
shareLabel,
} from "./test-selection-manifest.ts";
import { statusLayer } from "./theme.ts";
import { humanDuration } from "./tile-render-values.ts";
/** The fragment the flaky tests tile links to. */
export const FLAKY_SECTION_ID = "flaky";
/** The fragment the tests no lane can hold are listed under. */
export const UNSCHEDULABLE_SECTION_ID = "unschedulable";
/** The fragment the cost model's health is under. */
export const HEALTH_SECTION_ID = "cost-model";
/** Where the page lives, and what both tiles link to. */
export const TEST_SELECTION_PATH = "/test-selection";
const STYLES = `
.summary{display:flex;flex-wrap:wrap;gap:10px 34px;background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:12px 16px;margin-bottom:4px}
.summary div{display:flex;flex-direction:column;gap:2px}
.summary dt{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-subtle)}
.summary dd{margin:0;font-size:18px;font-weight:600;color:var(--text);font-variant-numeric:tabular-nums}
.lead{font-size:12px;color:var(--text-muted);margin:0 0 8px}
.lane{display:flex;flex-wrap:wrap;align-items:center;gap:6px 14px;background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:8px 14px;margin-bottom:7px}
.lane.over{border-color:${statusLayer("bad", STATUS_EDGE.bad)};background:${
statusLayer("bad", STATUS_WASH.bad)
}}
.lane b{font-size:13px;font-weight:600;flex:none;min-width:58px}
.lane .fill{flex:1 1 90px;order:1;height:8px;border-radius:4px;background:var(--surface-deep);overflow:hidden}
@media(max-width:560px){.lane .fill{flex-basis:100%;order:2}}
.lane .fill span{display:block;height:100%;background:var(--accent)}
.lane.over .fill span{background:var(--status-bad)}
.lane .num{flex:none;font-size:13px;color:var(--text-muted);font-variant-numeric:tabular-nums}
table{border-collapse:collapse;width:100%;font-size:13px}
th{text-align:left;font-weight:600;color:var(--text-subtle);font-size:11px;letter-spacing:.06em;text-transform:uppercase;white-space:nowrap;padding:0 12px 5px 0}
td{padding:5px 12px 5px 0;border-top:1px solid var(--divider);color:var(--text-secondary);vertical-align:top}
td.measure{font-variant-numeric:tabular-nums;white-space:nowrap;color:var(--text)}
td.name{width:99%;word-break:break-word}
.alarm{background:${statusLayer("bad", STATUS_WASH.bad)};border:1px solid ${
statusLayer("bad", STATUS_EDGE.bad)
};border-radius:10px;padding:8px 14px;margin:0 0 8px;font-size:13px;color:var(--text)}
.alarm ul{margin:4px 0 0;padding-left:18px}
.scroll{overflow-x:auto}
tr.over td{color:var(--status-bad)}
td .was{color:var(--text-faint)}
td .variant{color:var(--text-faint)}`;
/** One second count, to the precision a page of them stays readable at. */
const seconds = (value: number): string => `${value.toFixed(0)}s`;
/** A flake rate as the percentage the publisher measured. */
const percent = (rate: number): string => `${(rate * 100).toFixed(1)}%`;
/** An ISO 8601 time cut to the minute, which is the precision a reader wants. */
function minutePrecision(at: string): string {
return `${at.slice(0, 16).replace("T", " ")} UTC`;
}
/** The cells naming one test: its kind, its member, and its leaf name. */
function identityCells(test: TestIdentity): string {
const variant = test.v === undefined
? ""
: ` (${escapeHtml(test.v)})`;
return `
${escapeHtml(test.k)} | ${escapeHtml(test.s)} | ` +
`${escapeHtml(test.n)}${variant} | `;
}
/** One test named in a table, under the table's own measurement of it. */
interface TestRow {
test: TestIdentity;
/** What the leading column says, absent where nothing measured it. */
measure?: string;
}
/** A section listing tests, or nothing at all when the list is empty. */
function testSection(section: {
heading: string;
id?: string;
lead: string;
/** The leading column's heading. */
measure: string;
rows: TestRow[];
}): string {
if (section.rows.length === 0) return "";
// A row carrying no measurement leaves a gap in the table rather than
// shifting it.
const body = section.rows.map((row) =>
`| ${
escapeHtml(row.measure ?? "—")
} | ${identityCells(row.test)}
`
).join("");
const id = section.id === undefined ? "" : ` id="${section.id}"`;
return `${escapeHtml(section.heading)} · ${section.rows.length}
${escapeHtml(section.lead)}
| ${
escapeHtml(section.measure)
} | kind | member | test |
${body}
`;
}
/**
* What the flake share counts. The share is over runs rather than over
* failures, so a test that passes reliably reads as small however many of
* its rare failures were flakes, and a disagreement weighs less the more
* runs have followed it, so a test that has settled since reads lower
* than one that has just started. The counts beside it are what let a reader weigh it,
* since one disagreement in two runs and a thousand in two thousand are
* the same ratio and not the same claim.
*/
function flakeLead(manifest: Manifest): string {
const days = numberDial(
manifest.dials,
"FLAKE_WINDOW_DAYS",
FLAKE_WINDOW_FALLBACK_DAYS,
);
const exclusion = numberDial(
manifest.dials,
"FLAKE_EXCLUSION_RATE",
FLAKE_EXCLUSION_FALLBACK,
);
return `The share of the runs each test took part in over the last ${days} ` +
"days that it was seen disagreeing with itself over: the same commit " +
"both passing and failing, with nothing between the two runs but " +
"chance. A test that is deterministic cannot do that, so nothing is " +
"charged against the count and one disagreement among two runs reads " +
"as the half it is; a disagreement counts for less as the test goes " +
"on running without repeating it. " +
`Past ${percent(exclusion)} a test is held back from pull requests, ` +
"and the share falls again as it goes on running without disagreeing, " +
"so the exclusion reverses on its own. The counts are flat, so they " +
"are not what the share divides.";
}
/** The tests held back as flaky, worst-measured first. */
function flakyRows(manifest: Manifest): TestRow[] {
const scored = new Map(
manifest.entries.map((entry) => [testIdentityKey(entry.test), entry]),
);
return manifest.withheld
.filter((entry) => entry.reason === "flaky")
.map((entry) => ({
test: entry.test,
scored: scored.get(testIdentityKey(entry.test)),
}))
.sort((a, b) => (b.scored?.flakeRate ?? 0) - (a.scored?.flakeRate ?? 0))
.map(({ test, scored }) => ({
test,
...(scored === undefined ? {} : { measure: flakeMeasure(scored) }),
}));
}
/**
* One test's flake share with the counts it was taken from, where the
* manifest carries them. The share alone says how the exclusion was
* decided; the counts say how much is behind that decision.
*/
function flakeMeasure(entry: ManifestEntry): string {
const share = percent(entry.flakeRate);
const evidence = entry.flakeEvidence;
if (evidence === undefined) return share;
return `${share} · ${groupDigits(evidence.flakes)} in ${
groupDigits(evidence.runs)
}`;
}
/** The lanes a pull request would run, each against the budget it was packed to. */
function lanesSection(manifest: Manifest): string {
const budget = laneBudgetOf(manifest.dials);
const rows = manifest.lanes.map((lane) => {
const tests = laneTestCount(lane);
const over = lane.projectedSeconds > budget;
const filled = Math.min(100, (lane.projectedSeconds / budget) * 100);
return `Lane ${lane.lane}` +
`` +
`${seconds(lane.projectedSeconds)} of ${
seconds(budget)
}` +
`${groupDigits(tests)} tests
`;
}).join("");
return `Lanes
What each lane is projected to spend against the budget this manifest was packed to. The publisher packs these with nothing mandatory, so this is the run a pull request touching no test would get: a real one re-packs against its own diff, placing what that diff makes mandatory first and whatever the cost, and filling what is left of the budget from the same corpus.
${rows}`;
}
/** A figure, and what it was in the manifest before where that differs. */
function against(now: string, was: string | undefined): string {
return was === undefined || was === now
? escapeHtml(now)
: `${escapeHtml(now)} was ${escapeHtml(was)}`;
}
/**
* Whether the cost model this manifest was packed by still describes what
* lanes spend: what the publisher found broken, how many lanes ran past their
* bound, and each suite's charges against the manifest before and against
* what its batches spent. The suites a lane pays most to hold come first,
* since those are nearest to crowding everything else out.
*/
function healthSection(manifest: Manifest): string {
const health = manifest.health;
const heading = `Cost model
`;
if (health === undefined) {
return `${heading}
This manifest carries no figures about its cost model that this page can read.
`;
}
const budget = laneBudgetOf(manifest.dials);
const alarms = health.alarms.length === 0
? ""
: `The cost model is broken.${
health.alarms.map((alarm) => `- ${escapeHtml(alarm)}
`).join("")
}
`;
const rows = Object.entries(health.suites)
.sort(([a, x], [b, y]) => y.fixed - x.fixed || (a < b ? -1 : 1))
.map(([suite, figures]) => {
const was = health.previous?.suites[suite];
return ` budget ? ' class="over"' : ""}>` +
`| ${escapeHtml(suite)} | ` +
`${
against(
seconds(figures.fixed),
was === undefined ? undefined : seconds(was.fixed),
)
} | ` +
`${
against(
String(figures.tooLong),
was === undefined ? undefined : String(was.tooLong),
)
} | ` +
`${groupDigits(figures.batches)} | ` +
`${
figures.ratio === undefined ? "—" : figures.ratio.median.toFixed(2)
} | ` +
`${
figures.ratio === undefined ? "—" : figures.ratio.p90.toFixed(2)
} |
`;
}).join("");
return `${heading}
${escapeHtml(healthLead(health))}
${alarms}
`;
}
/**
* What the figures in the cost model's table are, and how the lanes it was
* measured against came out.
*/
function healthLead(health: CalibrationHealth): string {
const { observed, pastBound, projectedInside, overran } = health.lanes;
const lanes = observed === 0
? "No lane in the cost window recorded its work as a whole."
: `${groupDigits(pastBound)} of the ${
groupDigits(observed)
} lanes in the cost window ran past their bound, ${
groupDigits(overran)
} of them among the ${
groupDigits(projectedInside)
} the packer projected to finish inside it.`;
const previous = health.previous === undefined
? ""
: ` A grayed figure is the one in the manifest of ${
minutePrecision(health.previous.generatedAt)
}.`;
return `${lanes} "To hold" is what a lane pays before it runs anything of ` +
"a suite; past a lane's budget, nothing can share a lane with it. " +
'"Too long" counts its tests no lane can hold. The last two columns ' +
"are what its batches spent over what the packer charged for them, at " +
"the middle batch and at the ninetieth percentile, which the model is " +
`fitted to hold near one.${previous}`;
}
/** The counts the two tiles headline, spelled out. */
function summary(manifest: Manifest): string {
const selected = selectedSeconds(manifest);
const corpus = corpusSeconds(manifest);
const facts: Array<[string, string]> = [
["selected", shareLabel(selected, corpus)],
[
"test time",
`${humanDuration(selected * 1_000)} of ${
humanDuration(corpus * 1_000)
}`,
],
[
"tests",
`${groupDigits(selectedCount(manifest))} of ${
groupDigits(manifest.entries.length)
}`,
],
["lanes", String(manifest.lanes.length)],
["flaky", groupDigits(flakyCount(manifest))],
];
return `${
facts.map(([term, value]) =>
`- ${term}
- ${escapeHtml(value)}
`
).join("")
}
`;
}
/**
* The page's frame, which every state of it wears. Everything that changes
* from one manifest to the next is inside ``, which the page brings up
* to date as the server renders it again.
*/
function frame(head: string, body: string): LivePageContent {
return {
title: "Test selection",
styles: STYLES,
head,
body: `${body}
What a pull request runs, from the newest manifest the selection publisher wrote.
`,
};
}
/** The page for one manifest, or the page saying there is not one. */
export function testSelectionPage(
manifest: Manifest | undefined,
now = Date.now(),
): LivePageContent {
if (manifest === undefined) {
return frame(
"",
`No selection manifest has been published yet.
`,
);
}
const head = `commit ${escapeHtml(manifest.commit.slice(0, 7))} · generated ${
escapeHtml(minutePrecision(manifest.generatedAt))
}, ${compactSpan(now - Date.parse(manifest.generatedAt))} ago`;
return frame(
head,
`${summary(manifest)}
${lanesSection(manifest)}
${healthSection(manifest)}
${
testSection({
heading: "Held back as flaky",
id: FLAKY_SECTION_ID,
lead: flakeLead(manifest),
measure: "flake share",
rows: flakyRows(manifest),
})
}
${
testSection({
heading: "Too long for any lane",
id: UNSCHEDULABLE_SECTION_ID,
lead:
"One execution costs more than a whole lane's budget, so no packing can place them.",
measure: "cost",
rows: [...manifest.unschedulable]
.sort((a, b) => b.cost - a.cost)
.map((entry) => ({
test: entry.test,
measure: seconds(entry.cost),
})),
})
}`,
);
}
/**
* The page for a store that could not be read, which is a different thing
* from a store holding no manifest and says so.
*/
export function testSelectionUnavailable(reason: string): LivePageContent {
return frame(
"",
`The selection manifest could not be read: ${
escapeHtml(reason)
}.
`,
);
}
/** Serves the page against the manifest the tiles are already reading. */
export async function testSelectionResponse(
read: ManifestReader = sharedTestSelection.latest,
clock?: () => number,
): Promise {
try {
return livePageResponse(testSelectionPage(await read(), clock?.()));
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error("test selection page:", message);
return livePageResponse(
testSelectionUnavailable(collectionSub(error)),
503,
);
}
}