Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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