@elench/testkit 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -1
- package/lib/config/index.mjs +21 -5
- package/lib/database/index.mjs +105 -12
- package/lib/database/index.test.mjs +95 -0
- package/lib/runner/processes.mjs +16 -2
- package/lib/runner/processes.test.mjs +21 -0
- package/lib/runner/results.mjs +2 -1
- package/lib/runner/results.test.mjs +61 -0
- package/lib/runner/runtime-manager.mjs +34 -0
- package/lib/runner/runtime-manager.test.mjs +46 -0
- package/lib/runner/runtime-preparation.mjs +14 -0
- package/lib/runner/services.mjs +11 -1
- package/lib/runner/template-step-module-runner.mjs +25 -0
- package/lib/runner/template-steps.mjs +54 -45
- package/lib/runner/worker-loop.mjs +21 -0
- package/lib/setup/index.d.ts +14 -0
- package/lib/setup/index.mjs +12 -5
- package/lib/setup/index.test.mjs +34 -0
- package/lib/toolchains/index.mjs +565 -0
- package/lib/toolchains/index.test.mjs +168 -0
- package/lib/toolchains/semver.mjs +222 -0
- package/package.json +1 -1
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { execa } from "execa";
|
|
5
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
6
|
+
import {
|
|
7
|
+
applyToolchainEnv,
|
|
8
|
+
normalizeRuntimeToolchain,
|
|
9
|
+
normalizeToolchainRegistry,
|
|
10
|
+
resolveConfiguredToolchain,
|
|
11
|
+
} from "./index.mjs";
|
|
12
|
+
|
|
13
|
+
const tempDirs = [];
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
while (tempDirs.length > 0) {
|
|
17
|
+
fs.rmSync(tempDirs.pop(), { recursive: true, force: true });
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function makeTempDir(prefix) {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
23
|
+
tempDirs.push(dir);
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function writeJson(filePath, value) {
|
|
28
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
29
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("toolchains", () => {
|
|
33
|
+
it("normalizes named toolchain profiles and runtime references", () => {
|
|
34
|
+
const registry = normalizeToolchainRegistry({
|
|
35
|
+
frontend: {
|
|
36
|
+
cwd: "frontend",
|
|
37
|
+
install: "download",
|
|
38
|
+
node: "20.19.5",
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(normalizeRuntimeToolchain("frontend", "Service \"web\" runtime.toolchain", registry)).toEqual(
|
|
43
|
+
expect.objectContaining({
|
|
44
|
+
kind: "node",
|
|
45
|
+
cwd: "frontend",
|
|
46
|
+
install: "download",
|
|
47
|
+
node: "20.19.5",
|
|
48
|
+
refName: "frontend",
|
|
49
|
+
})
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("fails clearly when host versions do not satisfy a require-host toolchain", async () => {
|
|
54
|
+
const productDir = makeTempDir("testkit-toolchain-host-");
|
|
55
|
+
writeJson(path.join(productDir, "package.json"), {
|
|
56
|
+
name: "toolchain-host-product",
|
|
57
|
+
engines: {
|
|
58
|
+
node: ">=20.19.5 <21",
|
|
59
|
+
npm: ">=10.8.2 <11",
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const config = {
|
|
64
|
+
name: "frontend",
|
|
65
|
+
productDir,
|
|
66
|
+
testkit: {
|
|
67
|
+
runtime: {
|
|
68
|
+
toolchain: {
|
|
69
|
+
kind: "node",
|
|
70
|
+
cwd: ".",
|
|
71
|
+
detect: "auto",
|
|
72
|
+
install: "require-host",
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
await expect(
|
|
79
|
+
resolveConfiguredToolchain(config, {
|
|
80
|
+
hostNodeVersion: "18.19.0",
|
|
81
|
+
hostNpmVersion: "10.8.2",
|
|
82
|
+
})
|
|
83
|
+
).rejects.toThrow("requires Node >=20.19.5 <21");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("provisions a downloaded Node toolchain from a deterministic range minimum", async () => {
|
|
87
|
+
const productDir = makeTempDir("testkit-toolchain-download-");
|
|
88
|
+
writeJson(path.join(productDir, "package.json"), {
|
|
89
|
+
name: "toolchain-download-product",
|
|
90
|
+
engines: {
|
|
91
|
+
node: ">=20.19.5 <21",
|
|
92
|
+
npm: ">=10.8.2 <11",
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const archiveBuffer = await buildFakeNodeArchive();
|
|
97
|
+
const config = {
|
|
98
|
+
name: "frontend",
|
|
99
|
+
productDir,
|
|
100
|
+
testkit: {
|
|
101
|
+
runtime: {
|
|
102
|
+
toolchain: {
|
|
103
|
+
kind: "node",
|
|
104
|
+
cwd: ".",
|
|
105
|
+
detect: "auto",
|
|
106
|
+
install: "download",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const resolved = await resolveConfiguredToolchain(config, {
|
|
113
|
+
fetchImpl: async () => ({
|
|
114
|
+
ok: true,
|
|
115
|
+
status: 200,
|
|
116
|
+
arrayBuffer: async () => archiveBuffer,
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
expect(resolved).toMatchObject({
|
|
121
|
+
kind: "node",
|
|
122
|
+
install: "download",
|
|
123
|
+
nodeVersion: "20.19.5",
|
|
124
|
+
npmVersion: "10.8.2",
|
|
125
|
+
nodeSource: "package.json#engines.node",
|
|
126
|
+
npmSource: "package.json#engines.npm",
|
|
127
|
+
});
|
|
128
|
+
expect(fs.existsSync(resolved.nodeExecutable)).toBe(true);
|
|
129
|
+
expect(applyToolchainEnv({ PATH: "/usr/bin" }, resolved).PATH).toContain(
|
|
130
|
+
path.dirname(resolved.nodeExecutable)
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
async function buildFakeNodeArchive() {
|
|
136
|
+
const root = makeTempDir("testkit-node-archive-root-");
|
|
137
|
+
const platform = process.platform === "darwin" ? "darwin" : process.platform;
|
|
138
|
+
const arch = process.arch;
|
|
139
|
+
const bundleDir = path.join(root, `node-v20.19.5-${platform}-${arch}`);
|
|
140
|
+
const binDir = path.join(bundleDir, "bin");
|
|
141
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
142
|
+
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(binDir, "node"),
|
|
145
|
+
[
|
|
146
|
+
"#!/usr/bin/env bash",
|
|
147
|
+
'if [[ \"$1\" == *\"template-step-module-runner.mjs\" ]]; then',
|
|
148
|
+
" shift",
|
|
149
|
+
" exec node \"$@\"",
|
|
150
|
+
"fi",
|
|
151
|
+
'if [[ \"$1\" == *\"npm-cli.js\" ]]; then',
|
|
152
|
+
" shift",
|
|
153
|
+
" exec node \"$@\"",
|
|
154
|
+
"fi",
|
|
155
|
+
"exec node \"$@\"",
|
|
156
|
+
].join("\n"),
|
|
157
|
+
{ mode: 0o755 }
|
|
158
|
+
);
|
|
159
|
+
fs.writeFileSync(
|
|
160
|
+
path.join(binDir, "npm"),
|
|
161
|
+
['#!/usr/bin/env bash', 'echo "10.8.2"'].join("\n"),
|
|
162
|
+
{ mode: 0o755 }
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const archivePath = path.join(root, "node-v20.19.5-linux-x64.tar.gz");
|
|
166
|
+
await execa("tar", ["-czf", archivePath, "-C", root, path.basename(bundleDir)]);
|
|
167
|
+
return fs.readFileSync(archivePath);
|
|
168
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
export function normalizeSemver(input) {
|
|
2
|
+
const parsed = parseSemver(input);
|
|
3
|
+
return formatSemver(parsed);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function isExactSemver(input) {
|
|
7
|
+
try {
|
|
8
|
+
parseSemver(input);
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function parseSemver(input) {
|
|
16
|
+
const raw = String(input || "").trim().replace(/^v/i, "");
|
|
17
|
+
const match = raw.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/);
|
|
18
|
+
if (!match) {
|
|
19
|
+
throw new Error(`Invalid semver "${input}"`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
major: Number.parseInt(match[1], 10),
|
|
24
|
+
minor: Number.parseInt(match[2] || "0", 10),
|
|
25
|
+
patch: Number.parseInt(match[3] || "0", 10),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function compareSemver(left, right) {
|
|
30
|
+
const a = typeof left === "string" ? parseSemver(left) : left;
|
|
31
|
+
const b = typeof right === "string" ? parseSemver(right) : right;
|
|
32
|
+
|
|
33
|
+
if (a.major !== b.major) return a.major - b.major;
|
|
34
|
+
if (a.minor !== b.minor) return a.minor - b.minor;
|
|
35
|
+
return a.patch - b.patch;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatSemver(version) {
|
|
39
|
+
const parsed = typeof version === "string" ? parseSemver(version) : version;
|
|
40
|
+
return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function satisfiesSemver(version, spec) {
|
|
44
|
+
const normalizedSpec = String(spec || "").trim();
|
|
45
|
+
if (!normalizedSpec) return true;
|
|
46
|
+
const parsedVersion = typeof version === "string" ? parseSemver(version) : version;
|
|
47
|
+
|
|
48
|
+
for (const alternative of normalizedSpec.split("||").map((part) => part.trim()).filter(Boolean)) {
|
|
49
|
+
if (satisfiesSemverAlternative(parsedVersion, alternative)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deriveSemverMinimum(spec) {
|
|
58
|
+
const normalizedSpec = String(spec || "").trim();
|
|
59
|
+
if (!normalizedSpec) return null;
|
|
60
|
+
|
|
61
|
+
const alternatives = normalizedSpec
|
|
62
|
+
.split("||")
|
|
63
|
+
.map((part) => part.trim())
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((part) => deriveAlternativeMinimum(part))
|
|
66
|
+
.filter(Boolean);
|
|
67
|
+
|
|
68
|
+
if (alternatives.length === 0) return null;
|
|
69
|
+
alternatives.sort(compareSemver);
|
|
70
|
+
return formatSemver(alternatives[0]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function satisfiesSemverAlternative(version, spec) {
|
|
74
|
+
const comparators = expandSpecToComparators(spec);
|
|
75
|
+
return comparators.every((comparator) => compareWithComparator(version, comparator));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function deriveAlternativeMinimum(spec) {
|
|
79
|
+
const comparators = expandSpecToComparators(spec);
|
|
80
|
+
if (comparators.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
let lowerBound = null;
|
|
83
|
+
for (const comparator of comparators) {
|
|
84
|
+
if (comparator.operator === "=" || comparator.operator === ">=") {
|
|
85
|
+
lowerBound = maxSemver(lowerBound, comparator.version);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (comparator.operator === ">") {
|
|
89
|
+
lowerBound = maxSemver(lowerBound, incrementPatch(comparator.version));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const candidate = lowerBound || { major: 0, minor: 0, patch: 0 };
|
|
94
|
+
return comparators.every((comparator) => compareWithComparator(candidate, comparator))
|
|
95
|
+
? candidate
|
|
96
|
+
: null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function expandSpecToComparators(spec) {
|
|
100
|
+
const trimmed = String(spec || "").trim();
|
|
101
|
+
if (!trimmed) return [];
|
|
102
|
+
|
|
103
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
104
|
+
const comparators = [];
|
|
105
|
+
for (const part of parts) {
|
|
106
|
+
comparators.push(...expandComparator(part));
|
|
107
|
+
}
|
|
108
|
+
return comparators;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function expandComparator(part) {
|
|
112
|
+
const token = String(part || "").trim();
|
|
113
|
+
if (!token) return [];
|
|
114
|
+
|
|
115
|
+
const simple = token.match(/^(<=|>=|<|>|=)?\s*(.+)$/);
|
|
116
|
+
if (!simple) {
|
|
117
|
+
throw new Error(`Unsupported semver comparator "${token}"`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const operator = simple[1] || "";
|
|
121
|
+
const rhs = simple[2];
|
|
122
|
+
if (operator) {
|
|
123
|
+
return [{ operator: operator === "" ? "=" : operator, version: parseSemver(rhs) }];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (rhs.startsWith("^")) {
|
|
127
|
+
const base = parseSemver(rhs.slice(1));
|
|
128
|
+
return [
|
|
129
|
+
{ operator: ">=", version: base },
|
|
130
|
+
{ operator: "<", version: nextCaretUpperBound(base) },
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (rhs.startsWith("~")) {
|
|
135
|
+
const base = parseSemver(rhs.slice(1));
|
|
136
|
+
return [
|
|
137
|
+
{ operator: ">=", version: base },
|
|
138
|
+
{ operator: "<", version: { major: base.major, minor: base.minor + 1, patch: 0 } },
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (/^\d+\.(?:x|\*)$/i.test(rhs)) {
|
|
143
|
+
const [major] = rhs.split(".");
|
|
144
|
+
const base = { major: Number.parseInt(major, 10), minor: 0, patch: 0 };
|
|
145
|
+
return [
|
|
146
|
+
{ operator: ">=", version: base },
|
|
147
|
+
{ operator: "<", version: { major: base.major + 1, minor: 0, patch: 0 } },
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (/^\d+\.\d+\.(?:x|\*)$/i.test(rhs)) {
|
|
152
|
+
const [major, minor] = rhs.split(".");
|
|
153
|
+
const base = {
|
|
154
|
+
major: Number.parseInt(major, 10),
|
|
155
|
+
minor: Number.parseInt(minor, 10),
|
|
156
|
+
patch: 0,
|
|
157
|
+
};
|
|
158
|
+
return [
|
|
159
|
+
{ operator: ">=", version: base },
|
|
160
|
+
{ operator: "<", version: { major: base.major, minor: base.minor + 1, patch: 0 } },
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (/^\d+$/i.test(rhs)) {
|
|
165
|
+
const major = Number.parseInt(rhs, 10);
|
|
166
|
+
return [
|
|
167
|
+
{ operator: ">=", version: { major, minor: 0, patch: 0 } },
|
|
168
|
+
{ operator: "<", version: { major: major + 1, minor: 0, patch: 0 } },
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (/^\d+\.\d+$/i.test(rhs)) {
|
|
173
|
+
const [major, minor] = rhs.split(".").map((value) => Number.parseInt(value, 10));
|
|
174
|
+
return [
|
|
175
|
+
{ operator: ">=", version: { major, minor, patch: 0 } },
|
|
176
|
+
{ operator: "<", version: { major, minor: minor + 1, patch: 0 } },
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return [{ operator: "=", version: parseSemver(rhs) }];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function compareWithComparator(version, comparator) {
|
|
184
|
+
const comparison = compareSemver(version, comparator.version);
|
|
185
|
+
switch (comparator.operator) {
|
|
186
|
+
case "=":
|
|
187
|
+
return comparison === 0;
|
|
188
|
+
case ">":
|
|
189
|
+
return comparison > 0;
|
|
190
|
+
case ">=":
|
|
191
|
+
return comparison >= 0;
|
|
192
|
+
case "<":
|
|
193
|
+
return comparison < 0;
|
|
194
|
+
case "<=":
|
|
195
|
+
return comparison <= 0;
|
|
196
|
+
default:
|
|
197
|
+
throw new Error(`Unsupported semver operator "${comparator.operator}"`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function nextCaretUpperBound(base) {
|
|
202
|
+
if (base.major > 0) {
|
|
203
|
+
return { major: base.major + 1, minor: 0, patch: 0 };
|
|
204
|
+
}
|
|
205
|
+
if (base.minor > 0) {
|
|
206
|
+
return { major: 0, minor: base.minor + 1, patch: 0 };
|
|
207
|
+
}
|
|
208
|
+
return { major: 0, minor: 0, patch: base.patch + 1 };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function incrementPatch(version) {
|
|
212
|
+
return {
|
|
213
|
+
major: version.major,
|
|
214
|
+
minor: version.minor,
|
|
215
|
+
patch: version.patch + 1,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function maxSemver(left, right) {
|
|
220
|
+
if (!left) return right;
|
|
221
|
+
return compareSemver(left, right) >= 0 ? left : right;
|
|
222
|
+
}
|