Skip to content
200 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

A run opens on its summary: what triggered it, its status, duration and artifacts, a graph of its jobs (what needs what, matrices folded, called workflows boxed, deploys with their address), annotations, and each job's summary; the job list groups the same way1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import {
5 GRAPH,
6 type GraphJob,
7 baseName,
8 columns,
9 connector,
10 crossings,
11 groupJobs,
12 layoutRun,
13 orderColumns,
14 standingOf,
15} from "./run-graph.ts";
16
17let next = 0;
18function job(key: string, needs: string[] = [], more: Partial<GraphJob> = {}): GraphJob {
19 return {
20 id: `job_${next++}`,
21 key,
22 name: key,
23 needs,
24 status: "completed",
25 conclusion: "success",
26 startedAt: null,
27 finishedAt: null,
28 ...more,
29 };
30}
31
32test("columns go by the longest path of needs", () => {
33 const units = groupJobs([job("lint"), job("build"), job("test", ["build"]), job("deploy", ["build", "test"])]);
34 const column = columns(units);
35 assert.deepEqual(Object.fromEntries(column), { lint: 0, build: 0, test: 1, deploy: 2 });
36});
37
38test("a need of a job not in the run is left out", () => {
39 const column = columns(groupJobs([job("a", ["gone"]), job("b", ["a"])]));
40 assert.deepEqual(Object.fromEntries(column), { a: 0, b: 1 });
41});
42
43test("a cycle cannot loop the layout", () => {
44 const jobs = [job("a", ["b"]), job("b", ["a"]), job("c", ["c"])];
45 const column = columns(groupJobs(jobs));
46 for (const value of column.values()) assert.ok(Number.isFinite(value) && value >= 0);
47 const layout = layoutRun(jobs);
48 // Never a connector going back a column.
49 assert.ok(layout.edges.length <= 1);
50 for (const edge of layout.edges) assert.ok(column.get(edge.from)! < column.get(edge.to)!);
51});
52
53test("ordering a column by its needs takes out a crossing", () => {
54 // As listed: a, b over x, y, with x needing b and y needing a: they cross.
55 const units = groupJobs([job("a"), job("b"), job("x", ["b"]), job("y", ["a"])]);
56 const column = columns(units);
57 const edges: [string, string][] = [
58 ["b", "x"],
59 ["a", "y"],
60 ];
61 assert.equal(crossings([["a", "b"], ["x", "y"]], edges), 1);
62 const order = orderColumns(units, column);
63 assert.deepEqual(order, [
64 ["a", "b"],
65 ["y", "x"],
66 ]);
67 assert.equal(crossings(order, edges), 0);
68});
69
70test("ordering keeps the order as listed when nothing crosses", () => {
71 const units = groupJobs([job("a"), job("b"), job("x", ["a"]), job("y", ["b"])]);
72 assert.deepEqual(orderColumns(units, columns(units)), [
73 ["a", "b"],
74 ["x", "y"],
75 ]);
76});
77
78test("a matrix's jobs are one node, named without their combination", () => {
79 const units = groupJobs([
80 job("build"),
81 job("test", ["build"], { name: "test (ubuntu-latest, 20)" }),
82 job("test", ["build"], { name: "test (ubuntu-latest, 22)" }),
83 job("test", ["build"], { name: "test (macos-latest, 22)" }),
84 ]);
85 assert.equal(units.length, 2);
86 const matrix = units[1]!;
87 assert.equal(matrix.kind, "matrix");
88 assert.equal(matrix.label, "test");
89 assert.equal(matrix.kind === "matrix" && matrix.jobs.length, 3);
90 assert.equal(baseName("test (a, b)"), "test");
91 assert.equal(baseName("plain"), "plain");
92});
93
94test("an expanded matrix is taller, and its connectors still meet its first line", () => {
95 const jobs = [job("build"), job("test", ["build"], { name: "test (1)" }), job("test", ["build"], { name: "test (2)" })];
96 const closed = layoutRun(jobs);
97 const open = layoutRun(jobs, new Set(["test"]));
98 const node = (layout: typeof closed) => layout.nodes.find((n) => n.unit.key === "test")!;
99 assert.equal(node(closed).h, GRAPH.nodeHeight);
100 assert.equal(node(open).h, GRAPH.nodeHeight + 2 * GRAPH.rowHeight + 6);
101 assert.ok(node(open).expanded);
102 assert.equal(open.edges.length, 1);
103 assert.equal(open.edges[0]!.path, closed.edges[0]!.path);
104});
105
106test("a called workflow's jobs sit in a box under the job calling it", () => {
107 const jobs = [
108 job("build", [], { name: "Build", uses: "./.g1t/workflows/build.yml" }),
109 job("build/compile", [], { name: "Build / compile" }),
110 job("build/package", ["build/compile"], { name: "Build / package" }),
111 job("deploy", ["build"]),
112 ];
113 const units = groupJobs(jobs);
114 assert.deepEqual(
115 units.map((unit) => [unit.kind, unit.key]),
116 [
117 ["call", "build"],
118 ["job", "deploy"],
119 ],
120 );
121 const call = units[0]!;
122 assert.ok(call.kind === "call");
123 assert.equal(call.uses, "./.g1t/workflows/build.yml");
124 assert.deepEqual(
125 call.units.map((unit) => unit.label),
126 ["compile", "package"],
127 );
128
129 const layout = layoutRun(jobs);
130 const box = layout.groups[0]!;
131 const compile = layout.nodes.find((n) => n.unit.key === "build/compile")!;
132 const pack = layout.nodes.find((n) => n.unit.key === "build/package")!;
133 const deploy = layout.nodes.find((n) => n.unit.key === "deploy")!;
134 // Inside the box, one after the other; the job needing the caller after the box.
135 assert.ok(compile.x >= box.x && compile.x + compile.w <= box.x + box.w);
136 assert.ok(pack.x > compile.x && pack.x + pack.w <= box.x + box.w);
137 assert.ok(compile.y >= box.y + GRAPH.groupHead);
138 assert.ok(deploy.x >= box.x + box.w + GRAPH.columnGap);
139 assert.deepEqual(
140 layout.edges.map((edge) => [edge.from, edge.to]).sort(),
141 [
142 ["build", "deploy"],
143 ["build/compile", "build/package"],
144 ],
145 );
146 assert.equal(layout.width, deploy.x + deploy.w + GRAPH.margin);
147});
148
149test("a connector's colour follows the run: red from a failure, moving into a running job", () => {
150 const layout = layoutRun([
151 job("a", [], { conclusion: "failure" }),
152 job("b", ["a"], { conclusion: "skipped" }),
153 job("c"),
154 job("d", ["c"], { status: "in_progress", conclusion: null }),
155 job("e", ["c"], { status: "waiting", conclusion: null }),
156 ]);
157 const state = Object.fromEntries(layout.edges.map((edge) => [`${edge.from}>${edge.to}`, edge.state]));
158 assert.deepEqual(state, { "a>b": "failed", "c>d": "active", "c>e": "idle" });
159});
160
161test("a deployment's address makes its node taller", () => {
162 const layout = layoutRun([job("deploy", [], { environment: "production", environmentUrl: "https://g1t.page" })]);
163 assert.equal(layout.nodes[0]!.h, GRAPH.nodeHeight + GRAPH.urlHeight);
164});
165
166test("several jobs stand together as running, then as the worst ending", () => {
167 assert.deepEqual(standingOf([job("a"), job("b", [], { status: "calling", conclusion: null })]), { status: "in_progress", conclusion: null });
168 assert.deepEqual(standingOf([job("a"), job("b", [], { conclusion: "failure" }), job("c", [], { conclusion: "cancelled" })]), {
169 status: "completed",
170 conclusion: "failure",
171 });
172 assert.deepEqual(standingOf([job("a", [], { conclusion: "skipped" })]), { status: "completed", conclusion: "skipped" });
173});
174
175test("connectors run straight on one row, and turn with rounded corners between rows", () => {
176 assert.equal(connector(0, 10, 50, 10), "M0 10H50");
177 const turn = connector(0, 10, 100, 70);
178 assert.match(turn, /^M0 10H\d+Q/);
179 assert.ok(turn.endsWith("H100"));
180});
181
182test("an empty run lays out as nothing", () => {
183 const layout = layoutRun([]);
184 assert.equal(layout.nodes.length, 0);
185 assert.equal(layout.edges.length, 0);
186});
187
188test("a matrix whose jobs deploy to one address shows it once", () => {
189 const url = "https://g1t.sh";
190 const shared = layoutRun([
191 job("core", [], { name: "core (a)", environmentUrl: url }),
192 job("core", [], { name: "core (b)", environmentUrl: url }),
193 ]);
194 assert.equal(shared.nodes[0]!.h, GRAPH.nodeHeight + GRAPH.urlHeight);
195 const mixed = layoutRun([
196 job("core", [], { name: "core (a)", environmentUrl: url }),
197 job("core", [], { name: "core (b)", environmentUrl: null }),
198 ]);
199 assert.equal(mixed.nodes[0]!.h, GRAPH.nodeHeight);
200});