@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
|
@@ -3,6 +3,7 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { resolveServiceCwd } from "../config/index.mjs";
|
|
5
5
|
import { appendFileToHash, appendInputToHash } from "../database/fingerprint.mjs";
|
|
6
|
+
import { announceResolvedToolchain, resolveConfiguredToolchain } from "../toolchains/index.mjs";
|
|
6
7
|
import { readDatabaseUrl } from "./state-io.mjs";
|
|
7
8
|
import { buildExecutionEnv } from "./template.mjs";
|
|
8
9
|
import {
|
|
@@ -42,6 +43,7 @@ export async function prepareRuntimeService(config) {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
try {
|
|
46
|
+
await announceResolvedToolchain(config, await resolveConfiguredToolchain(config));
|
|
45
47
|
await runConfiguredSteps({
|
|
46
48
|
config,
|
|
47
49
|
steps: prepare.steps,
|
|
@@ -61,10 +63,22 @@ export async function prepareRuntimeService(config) {
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export async function computeRuntimePrepareFingerprint(config) {
|
|
66
|
+
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
64
67
|
const hash = crypto.createHash("sha256");
|
|
65
68
|
hash.update(
|
|
66
69
|
JSON.stringify({
|
|
67
70
|
prepare: config.testkit.runtime.prepare || null,
|
|
71
|
+
toolchain: resolvedToolchain
|
|
72
|
+
? {
|
|
73
|
+
kind: resolvedToolchain.kind,
|
|
74
|
+
install: resolvedToolchain.install,
|
|
75
|
+
nodeVersion: resolvedToolchain.nodeVersion,
|
|
76
|
+
npmVersion: resolvedToolchain.npmVersion,
|
|
77
|
+
nodeSource: resolvedToolchain.nodeSource,
|
|
78
|
+
npmSource: resolvedToolchain.npmSource,
|
|
79
|
+
fingerprint: resolvedToolchain.fingerprint,
|
|
80
|
+
}
|
|
81
|
+
: null,
|
|
68
82
|
serviceEnv: config.testkit.serviceEnv || {},
|
|
69
83
|
local: config.testkit.local
|
|
70
84
|
? {
|
package/lib/runner/services.mjs
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { resolveServiceCwd } from "../config/index.mjs";
|
|
2
|
+
import {
|
|
3
|
+
announceResolvedToolchain,
|
|
4
|
+
applyToolchainEnv,
|
|
5
|
+
resolveConfiguredToolchain,
|
|
6
|
+
} from "../toolchains/index.mjs";
|
|
2
7
|
import { buildExecutionEnv, numericPortFromUrl } from "./template.mjs";
|
|
3
8
|
import { DEFAULT_READY_TIMEOUT_MS, assertLocalServicePortsAvailable, isPortInUse, waitForReady } from "./readiness.mjs";
|
|
4
9
|
import { killChildProcess, pipeOutput, startDetachedCommand, stopChildProcess, sleep } from "./processes.mjs";
|
|
@@ -23,7 +28,12 @@ export async function startLocalServices(runtimeConfigs, lifecycle) {
|
|
|
23
28
|
|
|
24
29
|
export async function startLocalService(config, lifecycle) {
|
|
25
30
|
const cwd = resolveServiceCwd(config.productDir, config.testkit.local.cwd);
|
|
26
|
-
const
|
|
31
|
+
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
32
|
+
await announceResolvedToolchain(config, resolvedToolchain);
|
|
33
|
+
const env = applyToolchainEnv(
|
|
34
|
+
buildExecutionEnv(config, config.testkit.local.env, process.env),
|
|
35
|
+
resolvedToolchain
|
|
36
|
+
);
|
|
27
37
|
const port = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
|
|
28
38
|
if (port) {
|
|
29
39
|
env.PORT = String(port);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import { pathToFileURL } from "url";
|
|
3
|
+
|
|
4
|
+
const [, , moduleFile, exportName, contextFile] = process.argv;
|
|
5
|
+
|
|
6
|
+
if (!moduleFile || !exportName || !contextFile) {
|
|
7
|
+
console.error("Usage: node template-step-module-runner.mjs <module-file> <export-name> <context-file>");
|
|
8
|
+
process.exit(1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const context = JSON.parse(fs.readFileSync(contextFile, "utf8"));
|
|
13
|
+
const moduleRef = await import(pathToFileURL(moduleFile).href);
|
|
14
|
+
const fn = moduleRef[exportName];
|
|
15
|
+
if (typeof fn !== "function") {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Template module step "${moduleFile}#${exportName}" did not export a function named "${exportName}"`
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
await fn(context);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error(error?.stack || error?.message || String(error));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
@@ -3,22 +3,35 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { build } from "esbuild";
|
|
5
5
|
import { execa, execaCommand } from "execa";
|
|
6
|
-
import { fileURLToPath
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
7
|
import { resolveServiceCwd } from "../config/index.mjs";
|
|
8
|
+
import {
|
|
9
|
+
announceResolvedToolchain,
|
|
10
|
+
applyToolchainEnv,
|
|
11
|
+
resolveConfiguredToolchain,
|
|
12
|
+
} from "../toolchains/index.mjs";
|
|
8
13
|
|
|
9
14
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
10
15
|
const ROOT_ENTRY = path.join(PACKAGE_ROOT, "lib", "index.mjs");
|
|
11
16
|
const SETUP_ENTRY = path.join(PACKAGE_ROOT, "lib", "setup", "index.mjs");
|
|
12
17
|
const RUNTIME_ENTRY = path.join(PACKAGE_ROOT, "lib", "runtime", "index.mjs");
|
|
13
18
|
const KNOWN_FAILURES_ENTRY = path.join(PACKAGE_ROOT, "lib", "known-failures", "index.mjs");
|
|
19
|
+
const MODULE_RUNNER_ENTRY = path.join(
|
|
20
|
+
PACKAGE_ROOT,
|
|
21
|
+
"lib",
|
|
22
|
+
"runner",
|
|
23
|
+
"template-step-module-runner.mjs"
|
|
24
|
+
);
|
|
14
25
|
|
|
15
26
|
export async function runConfiguredSteps({ config, steps = [], env, labelPrefix }) {
|
|
16
27
|
if (steps.length === 0) return;
|
|
28
|
+
const resolvedToolchain = await resolveConfiguredToolchain(config);
|
|
29
|
+
await announceResolvedToolchain(config, resolvedToolchain);
|
|
17
30
|
|
|
18
31
|
for (const [index, step] of steps.entries()) {
|
|
19
32
|
const label = `${labelPrefix}:${config.name}:${index + 1}`;
|
|
20
33
|
console.log(`\n── ${label} ──`);
|
|
21
|
-
await runConfiguredStep(config, step, env);
|
|
34
|
+
await runConfiguredStep(config, step, env, resolvedToolchain);
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
|
|
@@ -51,11 +64,14 @@ export function resolveConfiguredPath(productDir, stepCwd, targetPath) {
|
|
|
51
64
|
return path.resolve(resolveConfiguredCwd(productDir, stepCwd), targetPath);
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
async function runConfiguredStep(config, step, env) {
|
|
67
|
+
async function runConfiguredStep(config, step, env, resolvedToolchain) {
|
|
68
|
+
const runtimeEnv = applyToolchainEnv(env, resolvedToolchain);
|
|
69
|
+
const cwd = resolveConfiguredCwd(config.productDir, step.cwd);
|
|
70
|
+
|
|
55
71
|
if (step.kind === "command") {
|
|
56
72
|
await execaCommand(step.cmd, {
|
|
57
|
-
cwd
|
|
58
|
-
env,
|
|
73
|
+
cwd,
|
|
74
|
+
env: runtimeEnv,
|
|
59
75
|
stdio: "inherit",
|
|
60
76
|
shell: true,
|
|
61
77
|
});
|
|
@@ -66,7 +82,7 @@ async function runConfiguredStep(config, step, env) {
|
|
|
66
82
|
await execa(
|
|
67
83
|
"psql",
|
|
68
84
|
[
|
|
69
|
-
|
|
85
|
+
runtimeEnv.DATABASE_URL,
|
|
70
86
|
"-v",
|
|
71
87
|
"ON_ERROR_STOP=1",
|
|
72
88
|
"-X",
|
|
@@ -74,8 +90,8 @@ async function runConfiguredStep(config, step, env) {
|
|
|
74
90
|
resolveConfiguredPath(config.productDir, step.cwd, step.path),
|
|
75
91
|
],
|
|
76
92
|
{
|
|
77
|
-
cwd
|
|
78
|
-
env,
|
|
93
|
+
cwd,
|
|
94
|
+
env: runtimeEnv,
|
|
79
95
|
stdio: "inherit",
|
|
80
96
|
}
|
|
81
97
|
);
|
|
@@ -83,34 +99,41 @@ async function runConfiguredStep(config, step, env) {
|
|
|
83
99
|
}
|
|
84
100
|
|
|
85
101
|
if (step.kind === "module") {
|
|
86
|
-
const
|
|
102
|
+
const bundledModule = await bundleConfiguredModule(config.productDir, step);
|
|
87
103
|
const { exportName } = parseModuleSpecifier(step.specifier);
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
104
|
+
const context = {
|
|
105
|
+
databaseUrl: runtimeEnv.DATABASE_URL || null,
|
|
106
|
+
productDir: config.productDir,
|
|
107
|
+
cwd,
|
|
108
|
+
serviceName: config.name,
|
|
109
|
+
env: { ...runtimeEnv },
|
|
110
|
+
runtimeId: config.runtimeId || null,
|
|
111
|
+
stateDir: config.stateDir,
|
|
112
|
+
prepareDir: config.testkit.prepareDir || null,
|
|
113
|
+
};
|
|
114
|
+
const contextPath = `${bundledModule.outputFile}.context.json`;
|
|
115
|
+
fs.writeFileSync(contextPath, JSON.stringify(context));
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await execa(
|
|
119
|
+
resolvedToolchain?.nodeExecutable || process.execPath,
|
|
120
|
+
[MODULE_RUNNER_ENTRY, bundledModule.outputFile, exportName, contextPath],
|
|
121
|
+
{
|
|
122
|
+
cwd,
|
|
123
|
+
env: runtimeEnv,
|
|
124
|
+
stdio: "inherit",
|
|
125
|
+
}
|
|
92
126
|
);
|
|
127
|
+
} finally {
|
|
128
|
+
fs.rmSync(contextPath, { force: true });
|
|
93
129
|
}
|
|
94
|
-
|
|
95
|
-
await withProcessContext(resolveConfiguredCwd(config.productDir, step.cwd), env, async () => {
|
|
96
|
-
await fn({
|
|
97
|
-
databaseUrl: env.DATABASE_URL || null,
|
|
98
|
-
productDir: config.productDir,
|
|
99
|
-
cwd: resolveConfiguredCwd(config.productDir, step.cwd),
|
|
100
|
-
serviceName: config.name,
|
|
101
|
-
env: { ...env },
|
|
102
|
-
runtimeId: config.runtimeId || null,
|
|
103
|
-
stateDir: config.stateDir,
|
|
104
|
-
prepareDir: config.testkit.prepareDir || null,
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
130
|
return;
|
|
108
131
|
}
|
|
109
132
|
|
|
110
133
|
throw new Error(`Unsupported template step kind "${step.kind}"`);
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
async function
|
|
136
|
+
async function bundleConfiguredModule(productDir, step) {
|
|
114
137
|
const { modulePath } = parseModuleSpecifier(step.specifier);
|
|
115
138
|
const absoluteModulePath = resolveConfiguredPath(productDir, step.cwd, modulePath);
|
|
116
139
|
const bundleDir = path.join(productDir, ".testkit", "_template-steps");
|
|
@@ -135,7 +158,10 @@ async function loadConfiguredModule(productDir, step) {
|
|
|
135
158
|
plugins: [testkitAliasPlugin()],
|
|
136
159
|
});
|
|
137
160
|
|
|
138
|
-
return
|
|
161
|
+
return {
|
|
162
|
+
outputFile,
|
|
163
|
+
cacheKey,
|
|
164
|
+
};
|
|
139
165
|
}
|
|
140
166
|
|
|
141
167
|
function buildModuleCacheKey(modulePath) {
|
|
@@ -172,20 +198,3 @@ function parseModuleSpecifier(specifier) {
|
|
|
172
198
|
exportName: exportName || "default",
|
|
173
199
|
};
|
|
174
200
|
}
|
|
175
|
-
|
|
176
|
-
async function withProcessContext(cwd, env, fn) {
|
|
177
|
-
const previousCwd = process.cwd();
|
|
178
|
-
const previousEnv = process.env;
|
|
179
|
-
process.chdir(cwd);
|
|
180
|
-
process.env = {
|
|
181
|
-
...previousEnv,
|
|
182
|
-
...env,
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
return await fn();
|
|
187
|
-
} finally {
|
|
188
|
-
process.chdir(previousCwd);
|
|
189
|
-
process.env = previousEnv;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
@@ -36,6 +36,27 @@ export async function runWorker(
|
|
|
36
36
|
runtimeManager.canAcquire(candidate)
|
|
37
37
|
);
|
|
38
38
|
if (!task) {
|
|
39
|
+
const blockedTasks = runtimeManager.drainFailedTasks(queue);
|
|
40
|
+
if (blockedTasks.length > 0) {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const recordedGraphs = new Set();
|
|
43
|
+
for (const blocked of blockedTasks) {
|
|
44
|
+
recordTaskOutcome(trackers, blocked.task, {
|
|
45
|
+
failed: false,
|
|
46
|
+
status: "not_run",
|
|
47
|
+
reason: blocked.reason,
|
|
48
|
+
durationMs: 0,
|
|
49
|
+
startedAt: now,
|
|
50
|
+
finishedAt: now,
|
|
51
|
+
error: null,
|
|
52
|
+
});
|
|
53
|
+
if (!recordedGraphs.has(blocked.graph.key)) {
|
|
54
|
+
recordGraphError(trackers, blocked.graph, blocked.message, now);
|
|
55
|
+
recordedGraphs.add(blocked.graph.key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
39
60
|
if (queue.length === 0) break;
|
|
40
61
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
41
62
|
continue;
|
package/lib/setup/index.d.ts
CHANGED
|
@@ -62,8 +62,20 @@ export interface RuntimeConfig {
|
|
|
62
62
|
inputs?: string[];
|
|
63
63
|
steps?: TemplateLifecycleStepConfig[];
|
|
64
64
|
};
|
|
65
|
+
toolchain?: string | NodeToolchainConfig;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
export interface NodeToolchainConfig {
|
|
69
|
+
kind?: "node";
|
|
70
|
+
cwd?: string;
|
|
71
|
+
detect?: "auto" | "off";
|
|
72
|
+
install?: "require-host" | "download";
|
|
73
|
+
node?: string;
|
|
74
|
+
npm?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type ToolchainConfig = NodeToolchainConfig;
|
|
78
|
+
|
|
67
79
|
export interface SuiteRequirementRule {
|
|
68
80
|
selector: string;
|
|
69
81
|
locks?: string[];
|
|
@@ -124,6 +136,7 @@ export interface TestkitSetup {
|
|
|
124
136
|
issueValidation?: KnownFailureIssueValidationConfig;
|
|
125
137
|
};
|
|
126
138
|
services?: Record<string, ServiceConfig>;
|
|
139
|
+
toolchains?: Record<string, ToolchainConfig>;
|
|
127
140
|
telemetry?: {
|
|
128
141
|
enabled?: boolean;
|
|
129
142
|
endpoint?: string;
|
|
@@ -148,6 +161,7 @@ export declare function moduleStep(
|
|
|
148
161
|
specifier: string,
|
|
149
162
|
options?: Omit<TemplateModuleStepConfig, "kind" | "specifier">
|
|
150
163
|
): TemplateModuleStepConfig;
|
|
164
|
+
export declare function nodeToolchain(options?: NodeToolchainConfig): NodeToolchainConfig;
|
|
151
165
|
export declare function goService(options: ServiceConfig["local"] & {
|
|
152
166
|
command?: string;
|
|
153
167
|
entrypoint?: string;
|
package/lib/setup/index.mjs
CHANGED
|
@@ -56,6 +56,13 @@ export function moduleStep(specifier, options = {}) {
|
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
export function nodeToolchain(options = {}) {
|
|
60
|
+
return {
|
|
61
|
+
kind: "node",
|
|
62
|
+
...options,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
export function goService(options = {}) {
|
|
60
67
|
const cwd = options.cwd || ".";
|
|
61
68
|
const port = requiredNumber(options.port, "goService port");
|
|
@@ -85,7 +92,7 @@ export function nextService(options = {}) {
|
|
|
85
92
|
...service(options),
|
|
86
93
|
local: {
|
|
87
94
|
cwd,
|
|
88
|
-
start: options.start || "
|
|
95
|
+
start: options.start || "./node_modules/.bin/next dev -p {port}",
|
|
89
96
|
port,
|
|
90
97
|
baseUrl,
|
|
91
98
|
readyUrl: options.readyUrl || baseUrl,
|
|
@@ -105,7 +112,7 @@ export function tsxService(options = {}) {
|
|
|
105
112
|
...service(options),
|
|
106
113
|
local: {
|
|
107
114
|
cwd,
|
|
108
|
-
start: options.start ||
|
|
115
|
+
start: options.start || `./node_modules/.bin/tsx watch ${entry}`,
|
|
109
116
|
port,
|
|
110
117
|
baseUrl,
|
|
111
118
|
readyUrl: options.readyUrl || `${baseUrl}${options.readyPath || "/health"}`,
|
|
@@ -196,12 +203,12 @@ export {
|
|
|
196
203
|
|
|
197
204
|
function defaultGoStartCommand(options) {
|
|
198
205
|
if (options.command) {
|
|
199
|
-
return `
|
|
206
|
+
return `go run ${options.command}`;
|
|
200
207
|
}
|
|
201
208
|
if (options.entrypoint) {
|
|
202
|
-
return `
|
|
209
|
+
return `go run ${options.entrypoint}`;
|
|
203
210
|
}
|
|
204
|
-
return "
|
|
211
|
+
return "go run ./cmd/server";
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
function requiredNumber(value, label) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { goService, nextService, nodeToolchain, tsxService } from "./index.mjs";
|
|
3
|
+
|
|
4
|
+
describe("setup helpers", () => {
|
|
5
|
+
it("emits plain next start commands without an exec prefix", () => {
|
|
6
|
+
const config = nextService({ port: 3000 });
|
|
7
|
+
|
|
8
|
+
expect(config.local.start).toBe("./node_modules/.bin/next dev -p {port}");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("emits plain tsx start commands without an exec prefix", () => {
|
|
12
|
+
const config = tsxService({ port: 3000, entry: "src/server.ts" });
|
|
13
|
+
|
|
14
|
+
expect(config.local.start).toBe("./node_modules/.bin/tsx watch src/server.ts");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("emits plain go start commands without an exec prefix", () => {
|
|
18
|
+
expect(goService({ port: 3000 }).local.start).toBe("go run ./cmd/server");
|
|
19
|
+
expect(goService({ port: 3000, entrypoint: "./cmd/api" }).local.start).toBe(
|
|
20
|
+
"go run ./cmd/api"
|
|
21
|
+
);
|
|
22
|
+
expect(goService({ port: 3000, command: "./cmd/worker" }).local.start).toBe(
|
|
23
|
+
"go run ./cmd/worker"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("builds node toolchain profiles with a node kind", () => {
|
|
28
|
+
expect(nodeToolchain({ node: "20.19.5", install: "download" })).toEqual({
|
|
29
|
+
kind: "node",
|
|
30
|
+
node: "20.19.5",
|
|
31
|
+
install: "download",
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|