OXYGENHUB

Coverage / src/flowview.ts 94.6%

140 of 148 lines run, 16 of 16 functions, 59 of 70 branches · run #24 on main · run missed

10×
// OxygenFlow in the editor, without VS Code: how runs and jobs look (icon,
20×
// colour, labels) and which runs just finished. Shared by the Flow view, the
30×
// status bar item and the tests.
41×
5
import type { FlowJob, FlowRun, TestCase } from "./hubapi";
61×
70×
/** A codicon and a theme colour id for a run's or job's status. */
8
export interface Look {
9
  /** Codicon id; `~spin` makes it turn. */
10
  icon: string;
11
  /** A theme colour id, e.g. `testing.iconPassed`. */
12
  color: string;
13
  /** A short word for screen readers and tooltips. */
14
  word: string;
15
}
161×
171×
export function look(status: string): Look {
181×
  switch (status) {
191×
    case "passed":
201×
      return { icon: "pass-filled", color: "testing.iconPassed", word: "passed" };
211×
    case "failed":
221×
      return { icon: "error", color: "testing.iconFailed", word: "failed" };
231×
    case "error":
241×
      return { icon: "warning", color: "testing.iconErrored", word: "could not run" };
251×
    case "running":
261×
      return { icon: "sync~spin", color: "oxygenhub.flowRunning", word: "running" };
271×
    case "queued":
281×
      return { icon: "clock", color: "testing.iconQueued", word: "queued" };
291×
    case "skipped":
301×
      return { icon: "debug-step-over", color: "testing.iconSkipped", word: "skipped" };
311×
    default:
321×
      return { icon: "circle-large-outline", color: "descriptionForeground", word: status };
331×
  }
341×
}
351×
361×
export function live(status: string): boolean {
378×
  return status === "queued" || status === "running";
384×
}
394×
404×
/** "3.2 s", "4 min 10 s". */
414×
export function duration(ms: number): string {
424×
  const s = Math.max(0, ms) / 1000;
434×
  if (s < 60) {
442×
    return `${s.toFixed(1)} s`;
452×
  }
464×
  return `${Math.floor(s / 60)} min ${Math.floor(s % 60)} s`;
474×
}
481×
491×
/** "just now", "5 minutes ago", as the hub says it. */
501×
export function ago(ms: number, now = Date.now()): string {
514×
  const s = Math.max(0, Math.floor((now - ms) / 1000));
524×
  const unit = (n: number, w: string) => `${n} ${w}${n === 1 ? "" : "s"} ago`;
534×
  if (s < 45) {
541×
    return "just now";
551×
  }
564×
  if (s < 90) {
570×
    return "a minute ago";
580×
  }
594×
  if (s < 2700) {
601×
    return unit(Math.ceil(s / 60), "minute");
611×
  }
624×
  if (s < 5400) {
630×
    return "an hour ago";
640×
  }
651×
  if (s < 79200) {
661×
    return unit(Math.ceil(s / 3600), "hour");
671×
  }
684×
  return unit(Math.ceil(s / 86400), "day");
694×
}
701×
711×
/** What a run ran on: its bookmark, or the change for a run on one change. */
721×
export function subject(run: FlowRun): string {
732×
  return run.bookmark || `change ${(run.heads[0] ?? "").slice(0, 12)}`;
742×
}
752×
762×
/** "#12 main". */
772×
export function runLabel(run: FlowRun): string {
782×
  return `#${run.number} ${subject(run)}`;
792×
}
801×
811×
/** "passed in 2 min 3 s · push by andy · 5 minutes ago". */
821×
export function runDescription(run: FlowRun, now = Date.now()): string {
832×
  const took = run.finished_ms ? ` in ${duration(run.finished_ms - run.created_ms)}` : "";
842×
  const queued = run.status === "queued" && run.queue_position ? ` (#${run.queue_position} in line)` : "";
852×
  const trigger = run.trigger.startsWith("rerun:") ? `re-run of #${run.trigger.slice(6)}` : run.trigger;
862×
  return `${look(run.status).word}${took}${queued} · ${trigger} by ${run.actor} · ${ago(run.created_ms, now)}`;
871×
}
881×
891×
/**
901×
 * Runs that were live at the last look and are finished now, so the editor
911×
 * can say so once. `before` maps a run's key to its last status.
921×
 */
931×
export function newlyFinished(before: Map<string, string>, runs: { key: string; run: FlowRun }[]): { key: string; run: FlowRun }[] {
941×
  return runs.filter(({ key, run }) => {
954×
    const was = before.get(key);
964×
    return was !== undefined && live(was) && !live(run.status);
971×
  });
983×
}
993×
1003×
/** Whether a job waits on someone's approval before it runs (oxygenflow#57). */
1013×
export function waitingApproval(job: FlowJob): boolean {
1023×
  return job.approval === "waiting" && job.status === "queued";
1033×
}
1041×
1051×
/** MiB as the hub's pages say it: "900 MiB", "2.1 GiB". */
1062×
function mib(n: number): string {
1072×
  return n >= 1024 ? `${(n / 1024).toFixed(1)} GiB` : `${n} MiB`;
1082×
}
1092×
1102×
/**
1112×
 * Where a job's time went (#348): how long it waited for a runner, each
1122×
 * phase its runner reported, and its peak disk and memory. Empty when the
1132×
 * hub reported none of it.
1142×
 */
1152×
export function jobTimings(job: FlowJob): string[] {
1162×
  const out: string[] = [];
1172×
  if (job.queued_ms && job.started_ms) {
1181×
    out.push(`waited ${duration(job.started_ms - job.queued_ms)}`);
1191×
  }
1202×
  for (const p of job.phases ?? []) {
1212×
    out.push(`${p.name} ${p.ms < 1000 ? `${p.ms} ms` : duration(p.ms)}`);
1222×
  }
1232×
  const peaks = [job.peak_disk_mib ? `disk ${mib(job.peak_disk_mib)}` : "", job.peak_memory_mib ? `memory ${mib(job.peak_memory_mib)}` : ""].filter(Boolean);
1242×
  if (peaks.length) {
1251×
    out.push(`peak ${peaks.join(", ")}`);
1261×
  }
1272×
  return out;
1282×
}
1291×
1301×
/** A test report's cases by suite, in the order they came, for the Testing view. */
1311×
export function casesBySuite(cases: TestCase[]): Map<string, TestCase[]> {
1321×
  const out = new Map<string, TestCase[]>();
1331×
  for (const c of cases) {
1344×
    const suite = c.suite || c.classname || "tests";
1354×
    out.set(suite, [...(out.get(suite) ?? []), c]);
1364×
  }
1371×
  return out;
1381×
}
1391×
1401×
/** A case's id in the Testing view: stable across reports, so its history lines up. */
1411×
export function caseId(repo: string, c: TestCase): string {
1421×
  return `${repo}::${c.suite || c.classname || "tests"}::${c.classname}::${c.name}`;
1431×
}
1441×
1451×
/** What VS Code should show for a case's status: passed, failed, errored or skipped. */
1461×
export function caseOutcome(status: string): "passed" | "failed" | "errored" | "skipped" {
1474×
  return status === "passed" ? "passed" : status === "failed" ? "failed" : status === "error" ? "errored" : "skipped";
1484×
}