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 way | 1 | import assert from "node:assert/strict"; |
| 2 | import { test } from "node:test"; | |
| 3 | ||
| 4 | import { | |
| 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 | ||
| 17 | let next = 0; | |
| 18 | function 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 | ||
| 32 | test("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 | ||
| 38 | test("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 | ||
| 43 | test("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 | ||
| 53 | test("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 | ||
| 70 | test("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 | ||
| 78 | test("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 | ||
| 94 | test("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 | ||
| 106 | test("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 | ||
| 149 | test("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 | ||
| 161 | test("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 | ||
| 166 | test("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 | ||
| 175 | test("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 | ||
| 182 | test("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 | ||
| 188 | test("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 | }); |