@elench/testkit 0.1.40 → 0.1.42
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 +27 -13
- package/bin/testkit.mjs +6 -1
- package/lib/cli/args.mjs +0 -4
- package/lib/cli/args.test.mjs +0 -5
- package/lib/cli/index.mjs +4 -11
- package/lib/config/index.mjs +78 -24
- package/lib/database/index.mjs +19 -7
- package/lib/database/naming.mjs +2 -2
- package/lib/database/naming.test.mjs +2 -2
- package/lib/runner/default-runtime-runner.mjs +52 -55
- package/lib/runner/execution-config.mjs +31 -70
- package/lib/runner/execution-config.test.mjs +30 -74
- package/lib/runner/formatting.mjs +0 -15
- package/lib/runner/formatting.test.mjs +0 -18
- package/lib/runner/lifecycle.mjs +106 -8
- package/lib/runner/orchestrator.mjs +16 -10
- package/lib/runner/planning.mjs +66 -138
- package/lib/runner/planning.test.mjs +101 -167
- package/lib/runner/playwright-config.mjs +13 -2
- package/lib/runner/playwright-config.test.mjs +26 -6
- package/lib/runner/playwright-runner.mjs +50 -56
- package/lib/runner/readiness.mjs +2 -2
- package/lib/runner/reporting.mjs +4 -3
- package/lib/runner/reporting.test.mjs +2 -5
- package/lib/runner/results.mjs +1 -1
- package/lib/runner/results.test.mjs +1 -1
- package/lib/runner/runtime-contexts.mjs +20 -24
- package/lib/runner/runtime-manager.mjs +228 -0
- package/lib/runner/runtime-manager.test.mjs +206 -0
- package/lib/runner/services.mjs +8 -6
- package/lib/runner/state.mjs +1 -2
- package/lib/runner/state.test.mjs +2 -4
- package/lib/runner/template.mjs +90 -60
- package/lib/runner/template.test.mjs +59 -27
- package/lib/runner/worker-loop.mjs +35 -32
- package/lib/setup/index.d.ts +15 -10
- package/package.json +1 -1
- package/lib/runner/stack-manager.mjs +0 -146
package/lib/runner/lifecycle.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import crypto from "crypto";
|
|
4
|
+
import { execFileSync } from "child_process";
|
|
4
5
|
import { destroyRuntimeDatabase, cleanupOrphanedLocalInfrastructure } from "../database/index.mjs";
|
|
5
6
|
|
|
6
7
|
const RUN_SCHEMA_VERSION = 1;
|
|
7
8
|
const RUNS_DIRNAME = path.join(".testkit", "_runs");
|
|
8
9
|
const TERMINATION_TIMEOUT_MS = 5_000;
|
|
10
|
+
const SHUTDOWN_HOLD_TIMEOUT_MS = 10_000;
|
|
9
11
|
|
|
10
12
|
export function createRunLifecycle(productDir) {
|
|
11
13
|
const runId = buildRunId();
|
|
@@ -21,10 +23,13 @@ export function createRunLifecycle(productDir) {
|
|
|
21
23
|
interruptReason: null,
|
|
22
24
|
services: [],
|
|
23
25
|
graphDirs: [],
|
|
24
|
-
|
|
26
|
+
runtimeDirs: [],
|
|
25
27
|
runtimeStateDirs: [],
|
|
26
28
|
};
|
|
27
29
|
const signalListeners = [];
|
|
30
|
+
const managedProcesses = new Set();
|
|
31
|
+
let shutdownHold = null;
|
|
32
|
+
let shutdownHoldTimeout = null;
|
|
28
33
|
|
|
29
34
|
function persist() {
|
|
30
35
|
fs.mkdirSync(getRunsDir(productDir), { recursive: true });
|
|
@@ -36,6 +41,24 @@ export function createRunLifecycle(productDir) {
|
|
|
36
41
|
persist();
|
|
37
42
|
}
|
|
38
43
|
|
|
44
|
+
function ensureShutdownHold() {
|
|
45
|
+
if (shutdownHold) return;
|
|
46
|
+
shutdownHold = setInterval(() => {}, 1_000);
|
|
47
|
+
shutdownHoldTimeout = setTimeout(() => {
|
|
48
|
+
releaseShutdownHold();
|
|
49
|
+
}, SHUTDOWN_HOLD_TIMEOUT_MS);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function releaseShutdownHold() {
|
|
53
|
+
if (!shutdownHold) return;
|
|
54
|
+
clearInterval(shutdownHold);
|
|
55
|
+
shutdownHold = null;
|
|
56
|
+
if (shutdownHoldTimeout) {
|
|
57
|
+
clearTimeout(shutdownHoldTimeout);
|
|
58
|
+
shutdownHoldTimeout = null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
39
62
|
const api = {
|
|
40
63
|
runId,
|
|
41
64
|
manifestPath,
|
|
@@ -55,9 +78,18 @@ export function createRunLifecycle(productDir) {
|
|
|
55
78
|
});
|
|
56
79
|
},
|
|
57
80
|
requestStop(reason = "interrupted") {
|
|
81
|
+
ensureShutdownHold();
|
|
58
82
|
if (!abortController.signal.aborted) {
|
|
59
83
|
abortController.abort(new Error(`testkit run interrupted (${reason})`));
|
|
60
84
|
}
|
|
85
|
+
for (const entry of managedProcesses) {
|
|
86
|
+
try {
|
|
87
|
+
entry.terminate?.();
|
|
88
|
+
} catch {
|
|
89
|
+
// Best-effort interruption only.
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
terminateDescendantProcesses(process.pid, "SIGINT");
|
|
61
93
|
mutate((draft) => {
|
|
62
94
|
draft.status = "interrupting";
|
|
63
95
|
draft.interruptReason = reason;
|
|
@@ -66,19 +98,34 @@ export function createRunLifecycle(productDir) {
|
|
|
66
98
|
trackGraphContext(context) {
|
|
67
99
|
mutate((draft) => {
|
|
68
100
|
pushUnique(draft.graphDirs, context.graphDir);
|
|
69
|
-
pushUnique(draft.
|
|
101
|
+
pushUnique(draft.runtimeDirs, context.runtimeDir);
|
|
70
102
|
for (const runtimeConfig of context.runtimeConfigs || []) {
|
|
71
103
|
if (runtimeConfig.stateDir) pushUnique(draft.runtimeStateDirs, runtimeConfig.stateDir);
|
|
72
104
|
}
|
|
73
105
|
});
|
|
74
106
|
},
|
|
75
|
-
|
|
107
|
+
registerProcess(child, terminate) {
|
|
108
|
+
if (!child) return;
|
|
109
|
+
managedProcesses.add({
|
|
110
|
+
child,
|
|
111
|
+
terminate,
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
unregisterProcess(childPid) {
|
|
115
|
+
for (const entry of managedProcesses) {
|
|
116
|
+
if (entry.child?.pid === childPid) {
|
|
117
|
+
managedProcesses.delete(entry);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
registerService(config, child, cwd, terminate) {
|
|
122
|
+
api.registerProcess(child, terminate);
|
|
76
123
|
const ports = collectConfigPorts(config);
|
|
77
124
|
mutate((draft) => {
|
|
78
125
|
draft.services = draft.services.filter((service) => service.pid !== child.pid);
|
|
79
126
|
draft.services.push({
|
|
80
127
|
serviceName: config.name,
|
|
81
|
-
|
|
128
|
+
runtimeLabel: config.runtimeLabel,
|
|
82
129
|
command: config.testkit.local?.start || null,
|
|
83
130
|
cwd,
|
|
84
131
|
pid: child.pid,
|
|
@@ -89,6 +136,7 @@ export function createRunLifecycle(productDir) {
|
|
|
89
136
|
});
|
|
90
137
|
},
|
|
91
138
|
unregisterService(childPid) {
|
|
139
|
+
api.unregisterProcess(childPid);
|
|
92
140
|
mutate((draft) => {
|
|
93
141
|
draft.services = draft.services.filter((service) => service.pid !== childPid);
|
|
94
142
|
});
|
|
@@ -116,6 +164,9 @@ export function createRunLifecycle(productDir) {
|
|
|
116
164
|
removeManifest() {
|
|
117
165
|
removeManifestFile(productDir, runId);
|
|
118
166
|
},
|
|
167
|
+
dispose() {
|
|
168
|
+
releaseShutdownHold();
|
|
169
|
+
},
|
|
119
170
|
};
|
|
120
171
|
|
|
121
172
|
return api;
|
|
@@ -195,8 +246,8 @@ export function findPortOwner(productDir, { host, port }) {
|
|
|
195
246
|
}
|
|
196
247
|
|
|
197
248
|
export function formatRunSummary(manifest) {
|
|
198
|
-
const
|
|
199
|
-
return `${manifest.runId} pid=${manifest.pid}${
|
|
249
|
+
const runtimeLabels = [...new Set((manifest.services || []).map((service) => service.runtimeLabel).filter(Boolean))];
|
|
250
|
+
return `${manifest.runId} pid=${manifest.pid}${runtimeLabels.length > 0 ? ` runtimes=${runtimeLabels.join(",")}` : ""}`;
|
|
200
251
|
}
|
|
201
252
|
|
|
202
253
|
export function isPidRunning(pid) {
|
|
@@ -220,8 +271,8 @@ async function cleanupRunManifest(productDir, manifest, { removeRuntimeState = f
|
|
|
220
271
|
await destroyRuntimeDatabase({ productDir, stateDir });
|
|
221
272
|
}
|
|
222
273
|
|
|
223
|
-
for (const
|
|
224
|
-
fs.rmSync(
|
|
274
|
+
for (const runtimeDir of [...new Set(manifest.runtimeDirs || [])].sort((a, b) => b.length - a.length)) {
|
|
275
|
+
fs.rmSync(runtimeDir, { recursive: true, force: true });
|
|
225
276
|
}
|
|
226
277
|
|
|
227
278
|
for (const graphDir of [...new Set(manifest.graphDirs || [])].sort((a, b) => b.length - a.length)) {
|
|
@@ -264,6 +315,53 @@ function killProcessGroup(pid, signal) {
|
|
|
264
315
|
}
|
|
265
316
|
}
|
|
266
317
|
|
|
318
|
+
function terminateDescendantProcesses(rootPid, signal) {
|
|
319
|
+
const descendants = listDescendantPids(rootPid);
|
|
320
|
+
for (const pid of descendants) {
|
|
321
|
+
try {
|
|
322
|
+
process.kill(pid, signal);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (error?.code !== "ESRCH") {
|
|
325
|
+
// Best-effort interruption only.
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function listDescendantPids(rootPid) {
|
|
332
|
+
try {
|
|
333
|
+
const output = execFileSync("ps", ["-eo", "pid=,ppid="], {
|
|
334
|
+
encoding: "utf8",
|
|
335
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
336
|
+
});
|
|
337
|
+
const childrenByParent = new Map();
|
|
338
|
+
for (const line of output.split(/\r?\n/)) {
|
|
339
|
+
const trimmed = line.trim();
|
|
340
|
+
if (!trimmed) continue;
|
|
341
|
+
const [pidRaw, parentRaw] = trimmed.split(/\s+/);
|
|
342
|
+
const pid = Number(pidRaw);
|
|
343
|
+
const parentPid = Number(parentRaw);
|
|
344
|
+
if (!Number.isInteger(pid) || !Number.isInteger(parentPid)) continue;
|
|
345
|
+
const siblings = childrenByParent.get(parentPid) || [];
|
|
346
|
+
siblings.push(pid);
|
|
347
|
+
childrenByParent.set(parentPid, siblings);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const descendants = [];
|
|
351
|
+
const stack = [...(childrenByParent.get(rootPid) || [])];
|
|
352
|
+
while (stack.length > 0) {
|
|
353
|
+
const pid = stack.pop();
|
|
354
|
+
descendants.push(pid);
|
|
355
|
+
for (const childPid of childrenByParent.get(pid) || []) {
|
|
356
|
+
stack.push(childPid);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return descendants;
|
|
360
|
+
} catch {
|
|
361
|
+
return [];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
267
365
|
async function waitForPidExit(pid, timeoutMs) {
|
|
268
366
|
const startedAt = Date.now();
|
|
269
367
|
while (Date.now() - startedAt < timeoutMs) {
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
applyShard,
|
|
3
3
|
buildRuntimeGraphs,
|
|
4
4
|
buildTaskQueue,
|
|
5
|
-
|
|
5
|
+
claimNextTask,
|
|
6
6
|
collectSuites,
|
|
7
7
|
resolveRuntimeConfigs,
|
|
8
8
|
} from "./planning.mjs";
|
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
safeUsername,
|
|
37
37
|
} from "./metadata.mjs";
|
|
38
38
|
import { resolveExecutionConfig } from "./execution-config.mjs";
|
|
39
|
-
import {
|
|
39
|
+
import { createRuntimeManager } from "./runtime-manager.mjs";
|
|
40
40
|
import { createWorker, runWorker } from "./worker-loop.mjs";
|
|
41
41
|
import { findUnmatchedRequestedFiles, isFullRunSelection } from "./selection.mjs";
|
|
42
42
|
import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
|
|
@@ -105,7 +105,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
105
105
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
106
106
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
107
107
|
let workerCount = 0;
|
|
108
|
-
let
|
|
108
|
+
let runtimeInstanceCount = 0;
|
|
109
|
+
let runtimeStats = [];
|
|
109
110
|
let exitCode = 0;
|
|
110
111
|
const lifecycle = createRunLifecycle(productDir);
|
|
111
112
|
lifecycle.markRunning();
|
|
@@ -119,14 +120,13 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
119
120
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
120
121
|
const queue = buildTaskQueue(executedPlans, graphs, timings);
|
|
121
122
|
workerCount = Math.max(1, Math.min(execution.workers, queue.length));
|
|
122
|
-
|
|
123
|
+
runtimeInstanceCount = graphs.reduce((sum, graph) => sum + graph.instanceCount, 0);
|
|
123
124
|
const workers = Array.from({ length: workerCount }, (_unused, index) =>
|
|
124
125
|
createWorker(index + 1, productDir)
|
|
125
126
|
);
|
|
126
|
-
const
|
|
127
|
+
const runtimeManager = createRuntimeManager({
|
|
127
128
|
productDir,
|
|
128
129
|
graphs,
|
|
129
|
-
execution,
|
|
130
130
|
lifecycle,
|
|
131
131
|
});
|
|
132
132
|
const timingUpdates = [];
|
|
@@ -137,11 +137,11 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
137
137
|
runWorker(
|
|
138
138
|
worker,
|
|
139
139
|
queue,
|
|
140
|
-
|
|
140
|
+
runtimeManager,
|
|
141
141
|
trackers,
|
|
142
142
|
timingUpdates,
|
|
143
143
|
lifecycle,
|
|
144
|
-
|
|
144
|
+
claimNextTask,
|
|
145
145
|
recordTaskOutcome,
|
|
146
146
|
recordGraphError
|
|
147
147
|
)
|
|
@@ -156,8 +156,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
|
+
runtimeStats = runtimeManager.getStats();
|
|
159
160
|
} finally {
|
|
160
|
-
await
|
|
161
|
+
await runtimeManager.cleanupAll();
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
saveTimings(productDir, timings, timingUpdates);
|
|
@@ -174,7 +175,8 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
174
175
|
finishedAt,
|
|
175
176
|
execution,
|
|
176
177
|
workerCount,
|
|
177
|
-
|
|
178
|
+
runtimeInstanceCount,
|
|
179
|
+
runtimeStats,
|
|
178
180
|
typeValues,
|
|
179
181
|
suiteSelectors,
|
|
180
182
|
fileNames: requestedFiles,
|
|
@@ -206,6 +208,9 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
206
208
|
if (results.some((result) => result.failed)) exitCode = 1;
|
|
207
209
|
if (lifecycle.isStopRequested()) exitCode = Math.max(exitCode, 130);
|
|
208
210
|
} finally {
|
|
211
|
+
if (lifecycle.isStopRequested()) {
|
|
212
|
+
exitCode = Math.max(exitCode, 130);
|
|
213
|
+
}
|
|
209
214
|
lifecycle.removeSignalHandlers();
|
|
210
215
|
lifecycle.markFinished(
|
|
211
216
|
exitCode === 0 ? "finished" : lifecycle.isStopRequested() ? "interrupted" : "failed"
|
|
@@ -213,6 +218,7 @@ export async function runAll(configs, typeValues, suiteSelectors, opts, allConfi
|
|
|
213
218
|
await cleanupRunById(productDir, lifecycle.runId);
|
|
214
219
|
await cleanupRuns(productDir, { includeActive: false });
|
|
215
220
|
lifecycle.removeManifest();
|
|
221
|
+
lifecycle.dispose();
|
|
216
222
|
process.exitCode = exitCode;
|
|
217
223
|
}
|
|
218
224
|
}
|
package/lib/runner/planning.mjs
CHANGED
|
@@ -4,12 +4,11 @@ import {
|
|
|
4
4
|
matchesSuiteSelectors,
|
|
5
5
|
suiteSelectionType,
|
|
6
6
|
} from "./suite-selection.mjs";
|
|
7
|
-
import { resolveBatchAccessMode, resolveBatchStackMode } from "./execution-config.mjs";
|
|
8
7
|
|
|
9
8
|
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
10
9
|
|
|
11
|
-
export function
|
|
12
|
-
return
|
|
10
|
+
export function taskNeedsLocalRuntime(task) {
|
|
11
|
+
return task.type !== "dal";
|
|
13
12
|
}
|
|
14
13
|
|
|
15
14
|
export function resolveRuntimeConfigs(targetConfig, configMap) {
|
|
@@ -78,10 +77,6 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
|
|
|
78
77
|
(framework === "playwright"
|
|
79
78
|
? Math.max(2, Math.max(1, files.length))
|
|
80
79
|
: Math.max(1, files.length)),
|
|
81
|
-
maxFileConcurrency:
|
|
82
|
-
framework === "k6" || framework === "playwright"
|
|
83
|
-
? suite.testkit?.maxFileConcurrency || 1
|
|
84
|
-
: 1,
|
|
85
80
|
totalFileCount: selectedSuiteFiles.length,
|
|
86
81
|
});
|
|
87
82
|
orderIndex += 1;
|
|
@@ -93,7 +88,7 @@ export function collectSuites(config, typeValues, suiteSelectors, fileNames = []
|
|
|
93
88
|
|
|
94
89
|
export function applyShard(suites, shard) {
|
|
95
90
|
if (!shard) return suites;
|
|
96
|
-
return suites.filter((
|
|
91
|
+
return suites.filter((_unused, index) => index % shard.total === shard.index - 1);
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
export function orderedTypes(types) {
|
|
@@ -109,12 +104,19 @@ export function orderedTypes(types) {
|
|
|
109
104
|
|
|
110
105
|
export function buildRuntimeGraphs(servicePlans) {
|
|
111
106
|
const executed = servicePlans.filter((plan) => !plan.skipped);
|
|
112
|
-
const
|
|
107
|
+
const graphs = [];
|
|
113
108
|
const graphByRuntimeKey = new Map();
|
|
114
109
|
|
|
115
110
|
for (const plan of executed) {
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
plan.assignedGraphKey = plan.runtimeKey;
|
|
112
|
+
const existing = graphByRuntimeKey.get(plan.runtimeKey);
|
|
113
|
+
if (existing) {
|
|
114
|
+
existing.targetNames.push(plan.config.name);
|
|
115
|
+
existing.instanceCount = Math.max(existing.instanceCount, plan.config.testkit.runtime.instances);
|
|
116
|
+
existing.maxConcurrentTasks = Math.min(
|
|
117
|
+
existing.maxConcurrentTasks,
|
|
118
|
+
resolveGraphMaxConcurrentTasks(plan.runtimeConfigs)
|
|
119
|
+
);
|
|
118
120
|
continue;
|
|
119
121
|
}
|
|
120
122
|
|
|
@@ -122,48 +124,23 @@ export function buildRuntimeGraphs(servicePlans) {
|
|
|
122
124
|
key: plan.runtimeKey,
|
|
123
125
|
runtimeNames: plan.runtimeNames,
|
|
124
126
|
runtimeConfigs: plan.runtimeConfigs,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
targetNames: [plan.config.name],
|
|
128
|
+
dirName: buildGraphDirName(plan.runtimeNames),
|
|
129
|
+
instanceCount: plan.config.testkit.runtime.instances,
|
|
130
|
+
maxConcurrentTasks: resolveGraphMaxConcurrentTasks(plan.runtimeConfigs),
|
|
129
131
|
};
|
|
130
|
-
|
|
132
|
+
graphs.push(graph);
|
|
131
133
|
graphByRuntimeKey.set(plan.runtimeKey, graph);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
!uniqueGraphs.some(
|
|
137
|
-
(other) =>
|
|
138
|
-
other.key !== graph.key &&
|
|
139
|
-
isRuntimeSuperset(other.runtimeNames, graph.runtimeNames)
|
|
140
|
-
)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
for (const plan of executed) {
|
|
144
|
-
const compatible = maximalGraphs.filter((graph) =>
|
|
145
|
-
isRuntimeSuperset(graph.runtimeNames, plan.runtimeNames)
|
|
146
|
-
);
|
|
147
|
-
if (compatible.length === 0) {
|
|
148
|
-
throw new Error(`No runtime graph found for service "${plan.config.name}"`);
|
|
149
|
-
}
|
|
136
|
+
const sortedGraphs = graphs.sort((left, right) => left.dirName.localeCompare(right.dirName));
|
|
137
|
+
const maxInstanceCount = Math.max(1, ...sortedGraphs.map((graph) => graph.instanceCount));
|
|
150
138
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
for (const graph of maximalGraphs) {
|
|
157
|
-
const rootName = [...graph.exactTargets].sort()[0];
|
|
158
|
-
const rootPlan = executed.find((plan) => plan.config.name === rootName);
|
|
159
|
-
if (!rootPlan) {
|
|
160
|
-
throw new Error(`Missing root plan for graph "${graph.key}"`);
|
|
161
|
-
}
|
|
162
|
-
graph.rootConfig = rootPlan.config;
|
|
163
|
-
graph.dirName = buildGraphDirName(graph.runtimeNames);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return maximalGraphs.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
139
|
+
return sortedGraphs.map((graph, index) => ({
|
|
140
|
+
...graph,
|
|
141
|
+
portNamespaceIndex: index,
|
|
142
|
+
portNamespaceStride: maxInstanceCount,
|
|
143
|
+
}));
|
|
167
144
|
}
|
|
168
145
|
|
|
169
146
|
export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
@@ -181,7 +158,6 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
181
158
|
|
|
182
159
|
for (const suite of plan.suites) {
|
|
183
160
|
for (const file of suite.files) {
|
|
184
|
-
const stackMode = resolveTaskStackMode(plan.config, plan.execution, suite, file);
|
|
185
161
|
const timingKey = buildTimingKey(plan.config.name, suite, file);
|
|
186
162
|
tasks.push({
|
|
187
163
|
id: nextId,
|
|
@@ -192,18 +168,12 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
192
168
|
suiteName: suite.name,
|
|
193
169
|
type: suite.type,
|
|
194
170
|
framework: suite.framework,
|
|
195
|
-
stackMode,
|
|
196
|
-
accessMode: resolveBatchAccessMode({
|
|
197
|
-
framework: suite.framework,
|
|
198
|
-
type: suite.type,
|
|
199
|
-
stackMode,
|
|
200
|
-
}),
|
|
201
171
|
orderIndex: suite.orderIndex,
|
|
202
172
|
file,
|
|
173
|
+
locks: resolveTaskLocks(plan.config, suite, file),
|
|
174
|
+
resourceCost: 1,
|
|
203
175
|
timingKey,
|
|
204
176
|
estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
|
|
205
|
-
maxBatchSize:
|
|
206
|
-
suite.maxFileConcurrency || 1,
|
|
207
177
|
});
|
|
208
178
|
nextId += 1;
|
|
209
179
|
}
|
|
@@ -219,68 +189,30 @@ export function buildTaskQueue(servicePlans, graphs, timings) {
|
|
|
219
189
|
);
|
|
220
190
|
}
|
|
221
191
|
|
|
222
|
-
export function
|
|
192
|
+
export function claimNextTask(queue, preferredGraphKey, isRunnable = () => true) {
|
|
223
193
|
if (queue.length === 0) return null;
|
|
224
194
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const tasks = [seed];
|
|
233
|
-
|
|
234
|
-
if (seed.maxBatchSize > 1) {
|
|
235
|
-
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
236
|
-
if (tasks.length >= seed.maxBatchSize) break;
|
|
237
|
-
const candidate = queue[cursor];
|
|
238
|
-
if (
|
|
239
|
-
candidate.framework === seed.framework &&
|
|
240
|
-
candidate.type === seed.type &&
|
|
241
|
-
candidate.graphKey === seed.graphKey &&
|
|
242
|
-
candidate.targetName === seed.targetName &&
|
|
243
|
-
candidate.suiteKey === seed.suiteKey &&
|
|
244
|
-
candidate.stackMode === seed.stackMode &&
|
|
245
|
-
candidate.accessMode === seed.accessMode
|
|
246
|
-
) {
|
|
247
|
-
tasks.push(candidate);
|
|
248
|
-
queue.splice(cursor, 1);
|
|
249
|
-
cursor -= 1;
|
|
250
|
-
}
|
|
195
|
+
const preferredIndexes = [];
|
|
196
|
+
const fallbackIndexes = [];
|
|
197
|
+
for (let index = 0; index < queue.length; index += 1) {
|
|
198
|
+
if (preferredGraphKey && queue[index].graphKey === preferredGraphKey) {
|
|
199
|
+
preferredIndexes.push(index);
|
|
200
|
+
} else {
|
|
201
|
+
fallbackIndexes.push(index);
|
|
251
202
|
}
|
|
252
203
|
}
|
|
253
204
|
|
|
254
|
-
|
|
255
|
-
(
|
|
256
|
-
|
|
257
|
-
a.file.localeCompare(b.file)
|
|
258
|
-
);
|
|
259
|
-
|
|
260
|
-
return {
|
|
261
|
-
graphKey: seed.graphKey,
|
|
262
|
-
targetName: seed.targetName,
|
|
263
|
-
framework: seed.framework,
|
|
264
|
-
type: seed.type,
|
|
265
|
-
stackMode: seed.stackMode,
|
|
266
|
-
accessMode: seed.accessMode,
|
|
267
|
-
tasks,
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
export function isRuntimeSuperset(candidate, target) {
|
|
272
|
-
return target.every((name) => candidate.includes(name));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function compareGraphsForAssignment(left, right) {
|
|
276
|
-
if (left.runtimeNames.length !== right.runtimeNames.length) {
|
|
277
|
-
return left.runtimeNames.length - right.runtimeNames.length;
|
|
205
|
+
for (const index of [...preferredIndexes, ...fallbackIndexes]) {
|
|
206
|
+
if (!isRunnable(queue[index])) continue;
|
|
207
|
+
return queue.splice(index, 1)[0];
|
|
278
208
|
}
|
|
279
|
-
|
|
209
|
+
|
|
210
|
+
return null;
|
|
280
211
|
}
|
|
281
212
|
|
|
282
|
-
function
|
|
283
|
-
|
|
213
|
+
export function buildGraphDirName(runtimeNames) {
|
|
214
|
+
const slug = runtimeNames.map(slugSegment).join("__");
|
|
215
|
+
return slug.length > 0 ? slug : "graph";
|
|
284
216
|
}
|
|
285
217
|
|
|
286
218
|
function applySkipRules(config, displayType, suiteName, files, opts = {}) {
|
|
@@ -324,40 +256,36 @@ function applySkipRules(config, displayType, suiteName, files, opts = {}) {
|
|
|
324
256
|
};
|
|
325
257
|
}
|
|
326
258
|
|
|
327
|
-
function
|
|
328
|
-
const
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
matchesSuiteSelectors(displayType,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
259
|
+
function resolveTaskLocks(config, suite, file) {
|
|
260
|
+
const locks = new Set();
|
|
261
|
+
const matchedSuiteRules = config.testkit.requirements?.suites || [];
|
|
262
|
+
for (const rule of matchedSuiteRules) {
|
|
263
|
+
if (matchesSuiteSelectors(suite.displayType, suite.name, [rule.selector])) {
|
|
264
|
+
for (const lockName of rule.locks || []) {
|
|
265
|
+
locks.add(lockName);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
335
269
|
|
|
336
|
-
function resolveTaskStackMode(config, execution, suite, file) {
|
|
337
|
-
const effectiveExecution = execution || config.testkit.execution;
|
|
338
270
|
const normalizedFile = normalizePathSeparators(file);
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
return resolveBatchStackMode(effectiveExecution.stackMode, fileOverride);
|
|
271
|
+
for (const lockName of config.testkit.requirements?.fileLocksByPath?.get(normalizedFile) || []) {
|
|
272
|
+
locks.add(lockName);
|
|
342
273
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
...config,
|
|
346
|
-
testkit: {
|
|
347
|
-
...config.testkit,
|
|
348
|
-
execution: effectiveExecution,
|
|
349
|
-
},
|
|
350
|
-
},
|
|
351
|
-
suite.displayType,
|
|
352
|
-
suite.name
|
|
353
|
-
);
|
|
274
|
+
|
|
275
|
+
return [...locks].sort();
|
|
354
276
|
}
|
|
355
277
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return slug.length > 0 ? slug : "graph";
|
|
278
|
+
function normalizePathSeparators(filePath) {
|
|
279
|
+
return String(filePath).split("\\").join("/");
|
|
359
280
|
}
|
|
360
281
|
|
|
361
282
|
function slugSegment(value) {
|
|
362
283
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "svc";
|
|
363
284
|
}
|
|
285
|
+
|
|
286
|
+
function resolveGraphMaxConcurrentTasks(runtimeConfigs) {
|
|
287
|
+
return runtimeConfigs.reduce(
|
|
288
|
+
(currentMin, config) => Math.min(currentMin, config.testkit.runtime.maxConcurrentTasks),
|
|
289
|
+
Number.POSITIVE_INFINITY
|
|
290
|
+
);
|
|
291
|
+
}
|