Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

149 lines · 5.1 KBRaw History
1import { describe, expect, it } from "vitest";
2import { expandMatrix, MatrixError } from "../src/index.js";
3
4describe("expandMatrix", () => {
5 it("no matrix yields one empty combination", () => {
6 expect(expandMatrix(undefined)).toEqual([{}]);
7 expect(expandMatrix(null)).toEqual([{}]);
8 });
9
10 it("single dimension", () => {
11 expect(expandMatrix({ version: [10, 12, 14] })).toEqual([{ version: 10 }, { version: 12 }, { version: 14 }]);
12 });
13
14 it("cartesian product with first key outermost", () => {
15 expect(expandMatrix({ os: ["ubuntu-latest", "windows-latest"], version: [10, 12, 14] })).toEqual([
16 { os: "ubuntu-latest", version: 10 },
17 { os: "ubuntu-latest", version: 12 },
18 { os: "ubuntu-latest", version: 14 },
19 { os: "windows-latest", version: 10 },
20 { os: "windows-latest", version: 12 },
21 { os: "windows-latest", version: 14 },
22 ]);
23 });
24
25 it("include example from the GitHub docs", () => {
26 const out = expandMatrix({
27 fruit: ["apple", "pear"],
28 animal: ["cat", "dog"],
29 include: [
30 { color: "green" },
31 { color: "pink", animal: "cat" },
32 { fruit: "apple", shape: "circle" },
33 { fruit: "banana" },
34 { fruit: "banana", animal: "cat" },
35 ],
36 });
37 expect(out).toEqual([
38 { fruit: "apple", animal: "cat", color: "pink", shape: "circle" },
39 { fruit: "apple", animal: "dog", color: "green", shape: "circle" },
40 { fruit: "pear", animal: "cat", color: "pink" },
41 { fruit: "pear", animal: "dog", color: "green" },
42 { fruit: "banana" },
43 { fruit: "banana", animal: "cat" },
44 ]);
45 });
46
47 it("expanding configurations example from the docs", () => {
48 const out = expandMatrix({
49 os: ["windows-latest", "ubuntu-latest"],
50 node: [14, 16],
51 include: [{ os: "windows-latest", node: 16, npm: 6 }],
52 });
53 expect(out).toEqual([
54 { os: "windows-latest", node: 14 },
55 { os: "windows-latest", node: 16, npm: 6 },
56 { os: "ubuntu-latest", node: 14 },
57 { os: "ubuntu-latest", node: 16 },
58 ]);
59 });
60
61 it("include-only matrix", () => {
62 expect(
63 expandMatrix({
64 include: [
65 { site: "production", datacenter: "site-a" },
66 { site: "staging", datacenter: "site-b" },
67 ],
68 }),
69 ).toEqual([
70 { site: "production", datacenter: "site-a" },
71 { site: "staging", datacenter: "site-b" },
72 ]);
73 });
74
75 it("exclude with partial match (docs example)", () => {
76 const out = expandMatrix({
77 os: ["macos-latest", "windows-latest"],
78 version: [12, 14, 16],
79 environment: ["staging", "production"],
80 exclude: [
81 { os: "macos-latest", version: 12, environment: "production" },
82 { os: "windows-latest", version: 16 },
83 ],
84 });
85 expect(out).toHaveLength(12 - 1 - 2);
86 expect(out).not.toContainEqual({ os: "macos-latest", version: 12, environment: "production" });
87 expect(out.filter((c) => c.os === "windows-latest" && c.version === 16)).toEqual([]);
88 expect(out).toContainEqual({ os: "macos-latest", version: 12, environment: "staging" });
89 });
90
91 it("exclude is applied before include, and include can re-add", () => {
92 const out = expandMatrix({
93 os: ["linux", "mac"],
94 exclude: [{ os: "mac" }],
95 include: [{ os: "mac", experimental: true }],
96 });
97 expect(out).toEqual([{ os: "linux" }, { os: "mac", experimental: true }]);
98 });
99
100 it("exclude everything leaves no combinations", () => {
101 expect(expandMatrix({ os: ["a"], exclude: [{ os: "a" }] })).toEqual([]);
102 });
103
104 it("object-valued vectors and partial object exclude", () => {
105 const out = expandMatrix({
106 cfg: [
107 { name: "a", flags: "-O2" },
108 { name: "b", flags: "-O0" },
109 ],
110 arch: ["x64", "arm64"],
111 exclude: [{ cfg: { name: "b" }, arch: "arm64" }],
112 });
113 expect(out).toEqual([
114 { cfg: { name: "a", flags: "-O2" }, arch: "x64" },
115 { cfg: { name: "a", flags: "-O2" }, arch: "arm64" },
116 { cfg: { name: "b", flags: "-O0" }, arch: "x64" },
117 ]);
118 });
119
120 it("include matching values loosely ('18' vs 18)", () => {
121 expect(expandMatrix({ node: [18, 20], include: [{ node: "18", lts: true }] })).toEqual([
122 { node: 18, lts: true },
123 { node: 20 },
124 ]);
125 });
126
127 it("evaluates a fromJSON matrix expression with ctx", () => {
128 const ctx = { needs: { setup: { outputs: { matrix: '{"pkg":["a","b"],"include":[{"pkg":"a","extra":1}]}' } } } };
129 expect(expandMatrix("${{ fromJSON(needs.setup.outputs.matrix) }}", ctx)).toEqual([
130 { pkg: "a", extra: 1 },
131 { pkg: "b" },
132 ]);
133 });
134
135 it("evaluates expression vectors and include lists", () => {
136 const ctx = { inputs: { versions: "[1,2]", extra: '[{"v":3}]' } };
137 expect(expandMatrix({ v: "${{ fromJSON(inputs.versions) }}", include: "${{ fromJSON(inputs.extra) }}" }, ctx)).toEqual([
138 { v: 1 },
139 { v: 2 },
140 { v: 3 },
141 ]);
142 });
143
144 it("errors on invalid shapes", () => {
145 expect(() => expandMatrix({ os: "ubuntu" })).toThrow(MatrixError);
146 expect(() => expandMatrix({ include: ["x"] })).toThrow(/include\[0\]/);
147 expect(() => expandMatrix([1, 2])).toThrow(/mapping/);
148 });
149});