@elench/testkit 0.1.17 → 0.1.19
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 +76 -16
- package/bin/testkit.mjs +1 -1
- package/lib/bundler/index.mjs +95 -0
- package/lib/bundler/index.test.mjs +79 -0
- package/lib/cli/args.mjs +57 -0
- package/lib/cli/args.test.mjs +62 -0
- package/lib/cli/index.mjs +114 -0
- package/lib/config/index.mjs +294 -0
- package/lib/config/index.test.mjs +12 -0
- package/lib/config/model.mjs +422 -0
- package/lib/config/model.test.mjs +193 -0
- package/lib/database/fingerprint.mjs +61 -0
- package/lib/database/fingerprint.test.mjs +93 -0
- package/lib/{database.mjs → database/index.mjs} +45 -160
- package/lib/database/naming.mjs +47 -0
- package/lib/database/naming.test.mjs +39 -0
- package/lib/database/state.mjs +52 -0
- package/lib/database/state.test.mjs +66 -0
- package/lib/index.mjs +1 -0
- package/lib/k6/checks.mjs +1 -0
- package/lib/k6/dal-suite.mjs +1 -0
- package/lib/k6/dal.mjs +1 -0
- package/lib/k6/http.mjs +1 -0
- package/lib/k6/index.mjs +30 -0
- package/lib/k6/suite.mjs +1 -0
- package/lib/reporters/playwright.mjs +125 -0
- package/lib/reporters/playwright.test.mjs +73 -0
- package/lib/{runner.mjs → runner/index.mjs} +252 -835
- package/lib/runner/metadata.mjs +55 -0
- package/lib/runner/metadata.test.mjs +52 -0
- package/lib/runner/planning.mjs +270 -0
- package/lib/runner/planning.test.mjs +127 -0
- package/lib/runner/results.mjs +285 -0
- package/lib/runner/results.test.mjs +144 -0
- package/lib/runner/state.mjs +71 -0
- package/lib/runner/state.test.mjs +64 -0
- package/lib/runner/template.mjs +320 -0
- package/lib/runner/template.test.mjs +150 -0
- package/lib/runtime/index.mjs +191 -0
- package/lib/runtime-src/k6/checks.js +39 -0
- package/lib/runtime-src/k6/dal-suite.js +33 -0
- package/lib/runtime-src/k6/dal.js +32 -0
- package/lib/runtime-src/k6/http.js +134 -0
- package/lib/runtime-src/k6/suite.js +55 -0
- package/lib/telemetry/index.mjs +43 -0
- package/lib/timing/index.mjs +73 -0
- package/lib/timing/index.test.mjs +64 -0
- package/package.json +18 -3
- package/infra/neon-down.sh +0 -18
- package/infra/neon-up.sh +0 -124
- package/lib/cli.mjs +0 -132
- package/lib/config.mjs +0 -666
- package/lib/exec.mjs +0 -20
|
@@ -3,7 +3,8 @@ import path from "path";
|
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import net from "net";
|
|
5
5
|
import { execa, execaCommand } from "execa";
|
|
6
|
-
import {
|
|
6
|
+
import { bundleK6File } from "../bundler/index.mjs";
|
|
7
|
+
import { resolveDalBinary, resolveServiceCwd } from "../config/index.mjs";
|
|
7
8
|
import {
|
|
8
9
|
cleanupOrphanedLocalInfrastructure,
|
|
9
10
|
destroyRuntimeDatabase,
|
|
@@ -11,28 +12,102 @@ import {
|
|
|
11
12
|
isDatabaseStateDir,
|
|
12
13
|
prepareDatabaseRuntime,
|
|
13
14
|
showServiceDatabaseStatus,
|
|
14
|
-
} from "
|
|
15
|
+
} from "../database/index.mjs";
|
|
16
|
+
import {
|
|
17
|
+
batchNeedsLocalRuntime as batchNeedsLocalRuntimeModel,
|
|
18
|
+
buildGraphDirName as buildGraphDirNameModel,
|
|
19
|
+
buildRuntimeGraphs as buildRuntimeGraphsModel,
|
|
20
|
+
buildTaskQueue as buildTaskQueueModel,
|
|
21
|
+
claimNextBatch as claimNextBatchModel,
|
|
22
|
+
collectSuites as collectSuitesModel,
|
|
23
|
+
compareGraphsForAssignment as compareGraphsForAssignmentModel,
|
|
24
|
+
isRuntimeSuperset as isRuntimeSupersetModel,
|
|
25
|
+
orderedTypes as orderedTypesModel,
|
|
26
|
+
resolveRuntimeConfigs as resolveRuntimeConfigsModel,
|
|
27
|
+
applyShard as applyShardModel,
|
|
28
|
+
} from "./planning.mjs";
|
|
29
|
+
import {
|
|
30
|
+
buildExecutionEnv as buildExecutionEnvModel,
|
|
31
|
+
buildPlaywrightEnv as buildPlaywrightEnvModel,
|
|
32
|
+
buildPortMap as buildPortMapModel,
|
|
33
|
+
finalizeString as finalizeStringModel,
|
|
34
|
+
getWorkerServiceStateDir as getWorkerServiceStateDirModel,
|
|
35
|
+
normalizeSocketHost as normalizeSocketHostModel,
|
|
36
|
+
numericPortFromUrl as numericPortFromUrlModel,
|
|
37
|
+
resolveRuntimeUrl as resolveRuntimeUrlModel,
|
|
38
|
+
resolveServiceStateDir as resolveServiceStateDirModel,
|
|
39
|
+
resolveTemplateString as resolveTemplateStringModel,
|
|
40
|
+
resolveWorkerConfig as resolveWorkerConfigModel,
|
|
41
|
+
resolveWorkerRuntimeConfigs as resolveWorkerRuntimeConfigsModel,
|
|
42
|
+
rewriteUrlPort as rewriteUrlPortModel,
|
|
43
|
+
socketFromUrl as socketFromUrlModel,
|
|
44
|
+
} from "./template.mjs";
|
|
45
|
+
import {
|
|
46
|
+
addTrackerError as addTrackerErrorModel,
|
|
47
|
+
buildRunArtifact as buildRunArtifactModel,
|
|
48
|
+
buildServiceTrackers as buildServiceTrackersModel,
|
|
49
|
+
finalizeServiceResult as finalizeServiceResultModel,
|
|
50
|
+
formatDuration as formatDurationModel,
|
|
51
|
+
formatError as formatErrorModel,
|
|
52
|
+
formatServiceSummary as formatServiceSummaryModel,
|
|
53
|
+
longestServiceName as longestServiceNameModel,
|
|
54
|
+
recordGraphError as recordGraphErrorModel,
|
|
55
|
+
recordTaskOutcome as recordTaskOutcomeModel,
|
|
56
|
+
summarizeDbBackend as summarizeDbBackendModel,
|
|
57
|
+
} from "./results.mjs";
|
|
58
|
+
import {
|
|
59
|
+
applyTimingUpdates,
|
|
60
|
+
buildTimingKey as buildTimingKeyModel,
|
|
61
|
+
createEmptyTimings,
|
|
62
|
+
estimateTaskDuration as estimateTaskDurationModel,
|
|
63
|
+
normalizeTimings,
|
|
64
|
+
} from "../timing/index.mjs";
|
|
65
|
+
import {
|
|
66
|
+
choosePlaywrightFinalResult as choosePlaywrightFinalResultModel,
|
|
67
|
+
collectPlaywrightSpec as collectPlaywrightSpecModel,
|
|
68
|
+
extractPlaywrightFailure as extractPlaywrightFailureModel,
|
|
69
|
+
extractReporterFile as extractReporterFileModel,
|
|
70
|
+
firstLine as firstLineModel,
|
|
71
|
+
formatPlaywrightReporterError as formatPlaywrightReporterErrorModel,
|
|
72
|
+
isPlaywrightPassingStatus as isPlaywrightPassingStatusModel,
|
|
73
|
+
normalizeReportedFile as normalizeReportedFileModel,
|
|
74
|
+
parsePlaywrightJsonResults as parsePlaywrightJsonResultsModel,
|
|
75
|
+
visitPlaywrightSuites as visitPlaywrightSuitesModel,
|
|
76
|
+
} from "../reporters/playwright.mjs";
|
|
77
|
+
import {
|
|
78
|
+
collectGitMetadata as collectGitMetadataModel,
|
|
79
|
+
readPackageMetadata as readPackageMetadataModel,
|
|
80
|
+
safeHostname as safeHostnameModel,
|
|
81
|
+
safeUsername as safeUsernameModel,
|
|
82
|
+
} from "./metadata.mjs";
|
|
83
|
+
import {
|
|
84
|
+
findGraphDirsForService as findGraphDirsForServiceModel,
|
|
85
|
+
findRuntimeStateDirs as findRuntimeStateDirsModel,
|
|
86
|
+
normalizePathSeparators as normalizePathSeparatorsModel,
|
|
87
|
+
readGraphMetadata as readGraphMetadataModel,
|
|
88
|
+
writeGraphMetadata as writeGraphMetadataModel,
|
|
89
|
+
} from "./state.mjs";
|
|
90
|
+
import { uploadTelemetryArtifact } from "../telemetry/index.mjs";
|
|
15
91
|
|
|
16
|
-
const TYPE_ORDER = ["dal", "integration", "e2e", "load"];
|
|
17
92
|
const HTTP_K6_TYPES = new Set(["integration", "e2e", "load"]);
|
|
18
93
|
const DEFAULT_READY_TIMEOUT_MS = 120_000;
|
|
19
|
-
const PORT_STRIDE = 100;
|
|
20
94
|
const TIMINGS_FILENAME = "timings.json";
|
|
21
|
-
const GRAPH_METADATA = "graph.json";
|
|
22
95
|
|
|
23
96
|
export async function runAll(configs, suiteType, suiteNames, opts, allConfigs = configs) {
|
|
24
97
|
const configMap = new Map(allConfigs.map((config) => [config.name, config]));
|
|
25
98
|
const startedAt = Date.now();
|
|
99
|
+
const telemetry = configs[0]?.telemetry || null;
|
|
26
100
|
const servicePlans = collectServicePlans(configs, configMap, suiteType, suiteNames, opts);
|
|
27
101
|
const trackers = buildServiceTrackers(servicePlans, startedAt);
|
|
28
102
|
const executedPlans = servicePlans.filter((plan) => !plan.skipped);
|
|
103
|
+
let workerCount = 0;
|
|
29
104
|
|
|
30
105
|
if (executedPlans.length > 0) {
|
|
31
106
|
const productDir = executedPlans[0].config.productDir;
|
|
32
107
|
const timings = loadTimings(productDir);
|
|
33
108
|
const graphs = buildRuntimeGraphs(executedPlans);
|
|
34
109
|
const queue = buildTaskQueue(executedPlans, graphs, timings);
|
|
35
|
-
|
|
110
|
+
workerCount = Math.max(1, Math.min(opts.jobs || 1, queue.length));
|
|
36
111
|
const graphByKey = new Map(graphs.map((graph) => [graph.key, graph]));
|
|
37
112
|
const workers = Array.from({ length: workerCount }, (_unused, index) =>
|
|
38
113
|
createWorker(index + 1, productDir)
|
|
@@ -61,8 +136,24 @@ export async function runAll(configs, suiteType, suiteNames, opts, allConfigs =
|
|
|
61
136
|
const results = configs.map((config) =>
|
|
62
137
|
finalizeServiceResult(trackers.get(config.name), startedAt, finishedAt)
|
|
63
138
|
);
|
|
139
|
+
const artifact = buildRunArtifact({
|
|
140
|
+
productDir: configs[0]?.productDir || process.cwd(),
|
|
141
|
+
results,
|
|
142
|
+
startedAt,
|
|
143
|
+
finishedAt,
|
|
144
|
+
requestedJobs: opts.jobs || 1,
|
|
145
|
+
workerCount,
|
|
146
|
+
suiteType,
|
|
147
|
+
suiteNames,
|
|
148
|
+
framework: opts.framework || "all",
|
|
149
|
+
shard: opts.shard || null,
|
|
150
|
+
serviceFilter: configs.length === 1 ? configs[0].name : null,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
writeRunArtifact(configs[0]?.productDir || process.cwd(), artifact);
|
|
64
154
|
|
|
65
155
|
printRunSummary(results, finishedAt - startedAt);
|
|
156
|
+
await reportTelemetry(telemetry, artifact);
|
|
66
157
|
if (results.some((result) => result.failed)) process.exit(1);
|
|
67
158
|
}
|
|
68
159
|
|
|
@@ -141,161 +232,15 @@ function collectServicePlans(configs, configMap, suiteType, suiteNames, opts) {
|
|
|
141
232
|
}
|
|
142
233
|
|
|
143
234
|
function buildServiceTrackers(servicePlans, startedAt) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
for (const plan of servicePlans) {
|
|
147
|
-
if (plan.skipped) {
|
|
148
|
-
trackers.set(plan.config.name, {
|
|
149
|
-
name: plan.config.name,
|
|
150
|
-
skipped: true,
|
|
151
|
-
suiteCount: 0,
|
|
152
|
-
suites: [],
|
|
153
|
-
suitesByKey: new Map(),
|
|
154
|
-
errors: [],
|
|
155
|
-
errorSet: new Set(),
|
|
156
|
-
startedAt,
|
|
157
|
-
firstTaskAt: null,
|
|
158
|
-
lastTaskAt: null,
|
|
159
|
-
});
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const suites = plan.suites.map((suite) => ({
|
|
164
|
-
key: `${suite.type}:${suite.name}`,
|
|
165
|
-
name: suite.name,
|
|
166
|
-
type: suite.type,
|
|
167
|
-
framework: suite.framework,
|
|
168
|
-
orderIndex: suite.orderIndex,
|
|
169
|
-
fileCount: suite.files.length,
|
|
170
|
-
completedFileCount: 0,
|
|
171
|
-
failedFiles: [],
|
|
172
|
-
failedFileSet: new Set(),
|
|
173
|
-
durationMs: 0,
|
|
174
|
-
error: null,
|
|
175
|
-
}));
|
|
176
|
-
|
|
177
|
-
trackers.set(plan.config.name, {
|
|
178
|
-
name: plan.config.name,
|
|
179
|
-
skipped: false,
|
|
180
|
-
suiteCount: suites.length,
|
|
181
|
-
suites,
|
|
182
|
-
suitesByKey: new Map(suites.map((suite) => [suite.key, suite])),
|
|
183
|
-
errors: [],
|
|
184
|
-
errorSet: new Set(),
|
|
185
|
-
startedAt,
|
|
186
|
-
firstTaskAt: null,
|
|
187
|
-
lastTaskAt: null,
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return trackers;
|
|
235
|
+
return buildServiceTrackersModel(servicePlans, startedAt);
|
|
192
236
|
}
|
|
193
237
|
|
|
194
238
|
function buildRuntimeGraphs(servicePlans) {
|
|
195
|
-
|
|
196
|
-
const uniqueGraphs = [];
|
|
197
|
-
const graphByRuntimeKey = new Map();
|
|
198
|
-
|
|
199
|
-
for (const plan of executed) {
|
|
200
|
-
if (graphByRuntimeKey.has(plan.runtimeKey)) {
|
|
201
|
-
graphByRuntimeKey.get(plan.runtimeKey).exactTargets.push(plan.config.name);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const graph = {
|
|
206
|
-
key: plan.runtimeKey,
|
|
207
|
-
runtimeNames: plan.runtimeNames,
|
|
208
|
-
runtimeConfigs: plan.runtimeConfigs,
|
|
209
|
-
exactTargets: [plan.config.name],
|
|
210
|
-
assignedTargets: [],
|
|
211
|
-
dirName: null,
|
|
212
|
-
rootConfig: null,
|
|
213
|
-
};
|
|
214
|
-
uniqueGraphs.push(graph);
|
|
215
|
-
graphByRuntimeKey.set(plan.runtimeKey, graph);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const maximalGraphs = uniqueGraphs.filter(
|
|
219
|
-
(graph) =>
|
|
220
|
-
!uniqueGraphs.some(
|
|
221
|
-
(other) =>
|
|
222
|
-
other.key !== graph.key &&
|
|
223
|
-
isRuntimeSuperset(other.runtimeNames, graph.runtimeNames)
|
|
224
|
-
)
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
for (const plan of executed) {
|
|
228
|
-
const compatible = maximalGraphs.filter((graph) =>
|
|
229
|
-
isRuntimeSuperset(graph.runtimeNames, plan.runtimeNames)
|
|
230
|
-
);
|
|
231
|
-
if (compatible.length === 0) {
|
|
232
|
-
throw new Error(`No runtime graph found for service "${plan.config.name}"`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const assigned = compatible.sort(compareGraphsForAssignment)[0];
|
|
236
|
-
plan.assignedGraphKey = assigned.key;
|
|
237
|
-
assigned.assignedTargets.push(plan.config.name);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
for (const graph of maximalGraphs) {
|
|
241
|
-
const rootName = [...graph.exactTargets].sort()[0];
|
|
242
|
-
const rootPlan = executed.find((plan) => plan.config.name === rootName);
|
|
243
|
-
if (!rootPlan) {
|
|
244
|
-
throw new Error(`Missing root plan for graph "${graph.key}"`);
|
|
245
|
-
}
|
|
246
|
-
graph.rootConfig = rootPlan.config;
|
|
247
|
-
graph.dirName = buildGraphDirName(graph.runtimeNames);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
return maximalGraphs.sort((a, b) => a.dirName.localeCompare(b.dirName));
|
|
239
|
+
return buildRuntimeGraphsModel(servicePlans);
|
|
251
240
|
}
|
|
252
241
|
|
|
253
242
|
function buildTaskQueue(servicePlans, graphs, timings) {
|
|
254
|
-
|
|
255
|
-
const tasks = [];
|
|
256
|
-
let nextId = 1;
|
|
257
|
-
|
|
258
|
-
for (const plan of servicePlans) {
|
|
259
|
-
if (plan.skipped) continue;
|
|
260
|
-
|
|
261
|
-
const graph = graphByKey.get(plan.assignedGraphKey);
|
|
262
|
-
if (!graph) {
|
|
263
|
-
throw new Error(`Assigned graph "${plan.assignedGraphKey}" not found for ${plan.config.name}`);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
for (const suite of plan.suites) {
|
|
267
|
-
for (const file of suite.files) {
|
|
268
|
-
const timingKey = buildTimingKey(plan.config.name, suite, file);
|
|
269
|
-
tasks.push({
|
|
270
|
-
id: nextId,
|
|
271
|
-
graphKey: graph.key,
|
|
272
|
-
targetName: plan.config.name,
|
|
273
|
-
serviceName: plan.config.name,
|
|
274
|
-
suiteKey: `${suite.type}:${suite.name}`,
|
|
275
|
-
suiteName: suite.name,
|
|
276
|
-
type: suite.type,
|
|
277
|
-
framework: suite.framework,
|
|
278
|
-
orderIndex: suite.orderIndex,
|
|
279
|
-
file,
|
|
280
|
-
timingKey,
|
|
281
|
-
estimatedDurationMs: estimateTaskDuration(timings, timingKey, suite),
|
|
282
|
-
maxBatchSize:
|
|
283
|
-
suite.framework === "playwright"
|
|
284
|
-
? Number.POSITIVE_INFINITY
|
|
285
|
-
: suite.maxFileConcurrency || 1,
|
|
286
|
-
});
|
|
287
|
-
nextId += 1;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return tasks.sort(
|
|
293
|
-
(a, b) =>
|
|
294
|
-
b.estimatedDurationMs - a.estimatedDurationMs ||
|
|
295
|
-
a.serviceName.localeCompare(b.serviceName) ||
|
|
296
|
-
a.suiteKey.localeCompare(b.suiteKey) ||
|
|
297
|
-
a.file.localeCompare(b.file)
|
|
298
|
-
);
|
|
243
|
+
return buildTaskQueueModel(servicePlans, graphs, timings);
|
|
299
244
|
}
|
|
300
245
|
|
|
301
246
|
function createWorker(workerId, productDir) {
|
|
@@ -352,58 +297,7 @@ async function runWorker(worker, queue, graphByKey, trackers, timingUpdates) {
|
|
|
352
297
|
}
|
|
353
298
|
|
|
354
299
|
function claimNextBatch(queue, preferredGraphKey) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
let index = -1;
|
|
358
|
-
if (preferredGraphKey) {
|
|
359
|
-
index = queue.findIndex((task) => task.graphKey === preferredGraphKey);
|
|
360
|
-
}
|
|
361
|
-
if (index === -1) index = 0;
|
|
362
|
-
|
|
363
|
-
const seed = queue.splice(index, 1)[0];
|
|
364
|
-
const tasks = [seed];
|
|
365
|
-
|
|
366
|
-
if (seed.framework === "playwright") {
|
|
367
|
-
for (let cursor = queue.length - 1; cursor >= 0; cursor -= 1) {
|
|
368
|
-
const candidate = queue[cursor];
|
|
369
|
-
if (
|
|
370
|
-
candidate.framework === "playwright" &&
|
|
371
|
-
candidate.graphKey === seed.graphKey &&
|
|
372
|
-
candidate.targetName === seed.targetName
|
|
373
|
-
) {
|
|
374
|
-
tasks.push(candidate);
|
|
375
|
-
queue.splice(cursor, 1);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
} else if (seed.maxBatchSize > 1) {
|
|
379
|
-
for (let cursor = queue.length - 1; cursor >= 0 && tasks.length < seed.maxBatchSize; cursor -= 1) {
|
|
380
|
-
const candidate = queue[cursor];
|
|
381
|
-
if (
|
|
382
|
-
candidate.framework === seed.framework &&
|
|
383
|
-
candidate.type === seed.type &&
|
|
384
|
-
candidate.graphKey === seed.graphKey &&
|
|
385
|
-
candidate.targetName === seed.targetName &&
|
|
386
|
-
candidate.suiteKey === seed.suiteKey
|
|
387
|
-
) {
|
|
388
|
-
tasks.push(candidate);
|
|
389
|
-
queue.splice(cursor, 1);
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
tasks.sort(
|
|
395
|
-
(a, b) =>
|
|
396
|
-
a.orderIndex - b.orderIndex ||
|
|
397
|
-
a.file.localeCompare(b.file)
|
|
398
|
-
);
|
|
399
|
-
|
|
400
|
-
return {
|
|
401
|
-
graphKey: seed.graphKey,
|
|
402
|
-
targetName: seed.targetName,
|
|
403
|
-
framework: seed.framework,
|
|
404
|
-
type: seed.type,
|
|
405
|
-
tasks,
|
|
406
|
-
};
|
|
300
|
+
return claimNextBatchModel(queue, preferredGraphKey);
|
|
407
301
|
}
|
|
408
302
|
|
|
409
303
|
async function ensureWorkerGraph(worker, batch, graphByKey) {
|
|
@@ -627,10 +521,15 @@ async function runHttpK6Batch(targetConfig, batch) {
|
|
|
627
521
|
|
|
628
522
|
async function runHttpK6Task(targetConfig, task, baseUrl) {
|
|
629
523
|
const absFile = path.join(targetConfig.productDir, task.file);
|
|
524
|
+
const bundledFile = await bundleK6File({
|
|
525
|
+
productDir: targetConfig.productDir,
|
|
526
|
+
serviceName: targetConfig.name,
|
|
527
|
+
sourceFile: absFile,
|
|
528
|
+
});
|
|
630
529
|
console.log(`·· ${targetConfig.workerLabel}:${task.suiteName} → ${task.file}`);
|
|
631
530
|
const startedAt = Date.now();
|
|
632
531
|
try {
|
|
633
|
-
await execa("k6", ["run", "--address", "127.0.0.1:0", "-e", `BASE_URL=${baseUrl}`,
|
|
532
|
+
await execa("k6", ["run", "--address", "127.0.0.1:0", "-e", `BASE_URL=${baseUrl}`, bundledFile], {
|
|
634
533
|
cwd: targetConfig.productDir,
|
|
635
534
|
env: buildExecutionEnv(targetConfig),
|
|
636
535
|
stdio: "inherit",
|
|
@@ -669,12 +568,17 @@ async function runDalBatch(targetConfig, batch) {
|
|
|
669
568
|
async function runDalTask(targetConfig, task, databaseUrl) {
|
|
670
569
|
const absFile = path.join(targetConfig.productDir, task.file);
|
|
671
570
|
const k6Binary = resolveDalBinary();
|
|
571
|
+
const bundledFile = await bundleK6File({
|
|
572
|
+
productDir: targetConfig.productDir,
|
|
573
|
+
serviceName: targetConfig.name,
|
|
574
|
+
sourceFile: absFile,
|
|
575
|
+
});
|
|
672
576
|
console.log(`·· ${targetConfig.workerLabel}:${task.suiteName} → ${task.file}`);
|
|
673
577
|
const startedAt = Date.now();
|
|
674
578
|
try {
|
|
675
579
|
await execa(
|
|
676
580
|
k6Binary,
|
|
677
|
-
["run", "--address", "127.0.0.1:0", "-e", `DATABASE_URL=${databaseUrl}`,
|
|
581
|
+
["run", "--address", "127.0.0.1:0", "-e", `DATABASE_URL=${databaseUrl}`, bundledFile],
|
|
678
582
|
{
|
|
679
583
|
cwd: targetConfig.productDir,
|
|
680
584
|
env: buildExecutionEnv(targetConfig),
|
|
@@ -812,168 +716,31 @@ async function waitForReady({ name, url, timeoutMs, process }) {
|
|
|
812
716
|
}
|
|
813
717
|
|
|
814
718
|
function batchNeedsLocalRuntime(batch) {
|
|
815
|
-
return batch
|
|
719
|
+
return batchNeedsLocalRuntimeModel(batch);
|
|
816
720
|
}
|
|
817
721
|
|
|
818
722
|
function resolveRuntimeConfigs(targetConfig, configMap) {
|
|
819
|
-
|
|
820
|
-
const visiting = new Set();
|
|
821
|
-
const seen = new Set();
|
|
822
|
-
|
|
823
|
-
const visit = (config) => {
|
|
824
|
-
if (seen.has(config.name)) return;
|
|
825
|
-
if (visiting.has(config.name)) {
|
|
826
|
-
throw new Error(`Dependency cycle detected involving "${config.name}"`);
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
visiting.add(config.name);
|
|
830
|
-
for (const depName of config.testkit.dependsOn || []) {
|
|
831
|
-
const dep = configMap.get(depName);
|
|
832
|
-
if (!dep) {
|
|
833
|
-
throw new Error(`Service "${config.name}" depends on unknown service "${depName}"`);
|
|
834
|
-
}
|
|
835
|
-
visit(dep);
|
|
836
|
-
}
|
|
837
|
-
visiting.delete(config.name);
|
|
838
|
-
seen.add(config.name);
|
|
839
|
-
ordered.push(config);
|
|
840
|
-
};
|
|
841
|
-
|
|
842
|
-
visit(targetConfig);
|
|
843
|
-
return ordered;
|
|
723
|
+
return resolveRuntimeConfigsModel(targetConfig, configMap);
|
|
844
724
|
}
|
|
845
725
|
|
|
846
726
|
function collectSuites(config, suiteType, suiteNames, frameworkFilter) {
|
|
847
|
-
|
|
848
|
-
suiteType === "all"
|
|
849
|
-
? orderedTypes(Object.keys(config.suites))
|
|
850
|
-
: [suiteType === "int" ? "integration" : suiteType];
|
|
851
|
-
|
|
852
|
-
const selectedNames = new Set(suiteNames);
|
|
853
|
-
const suites = [];
|
|
854
|
-
let orderIndex = 0;
|
|
855
|
-
|
|
856
|
-
for (const type of types) {
|
|
857
|
-
for (const suite of config.suites[type] || []) {
|
|
858
|
-
const framework = suite.framework || "k6";
|
|
859
|
-
if (selectedNames.size > 0 && !selectedNames.has(suite.name)) continue;
|
|
860
|
-
if (frameworkFilter && frameworkFilter !== "all" && framework !== frameworkFilter) continue;
|
|
861
|
-
|
|
862
|
-
suites.push({
|
|
863
|
-
...suite,
|
|
864
|
-
framework,
|
|
865
|
-
type,
|
|
866
|
-
orderIndex,
|
|
867
|
-
sortKey: `${type}:${suite.name}`,
|
|
868
|
-
weight:
|
|
869
|
-
suite.testkit?.weight ||
|
|
870
|
-
(framework === "playwright"
|
|
871
|
-
? Math.max(2, suite.files.length)
|
|
872
|
-
: Math.max(1, suite.files.length)),
|
|
873
|
-
maxFileConcurrency:
|
|
874
|
-
framework === "k6" ? suite.testkit?.maxFileConcurrency || 1 : 1,
|
|
875
|
-
});
|
|
876
|
-
orderIndex += 1;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
return suites;
|
|
727
|
+
return collectSuitesModel(config, suiteType, suiteNames, frameworkFilter);
|
|
881
728
|
}
|
|
882
729
|
|
|
883
730
|
function applyShard(suites, shard) {
|
|
884
|
-
|
|
885
|
-
return suites.filter((unused, index) => index % shard.total === shard.index - 1);
|
|
731
|
+
return applyShardModel(suites, shard);
|
|
886
732
|
}
|
|
887
733
|
|
|
888
734
|
function orderedTypes(types) {
|
|
889
|
-
|
|
890
|
-
for (const known of TYPE_ORDER) {
|
|
891
|
-
if (types.includes(known)) ordered.push(known);
|
|
892
|
-
}
|
|
893
|
-
for (const type of types) {
|
|
894
|
-
if (!ordered.includes(type)) ordered.push(type);
|
|
895
|
-
}
|
|
896
|
-
return ordered;
|
|
735
|
+
return orderedTypesModel(types);
|
|
897
736
|
}
|
|
898
737
|
|
|
899
738
|
function resolveWorkerRuntimeConfigs(targetConfig, runtimeConfigs, workerId, workerStateDir) {
|
|
900
|
-
|
|
901
|
-
const baseUrlByService = new Map();
|
|
902
|
-
const readyUrlByService = new Map();
|
|
903
|
-
|
|
904
|
-
for (const config of runtimeConfigs) {
|
|
905
|
-
if (!config.testkit.local) continue;
|
|
906
|
-
baseUrlByService.set(
|
|
907
|
-
config.name,
|
|
908
|
-
resolveRuntimeUrl(config.testkit.local.baseUrl, config.name, targetConfig, workerId, {
|
|
909
|
-
workerStateDir,
|
|
910
|
-
portMap,
|
|
911
|
-
baseUrlByService,
|
|
912
|
-
readyUrlByService,
|
|
913
|
-
})
|
|
914
|
-
);
|
|
915
|
-
readyUrlByService.set(
|
|
916
|
-
config.name,
|
|
917
|
-
resolveRuntimeUrl(config.testkit.local.readyUrl, config.name, targetConfig, workerId, {
|
|
918
|
-
workerStateDir,
|
|
919
|
-
portMap,
|
|
920
|
-
baseUrlByService,
|
|
921
|
-
readyUrlByService,
|
|
922
|
-
})
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const urlMappings = [];
|
|
927
|
-
for (const config of runtimeConfigs) {
|
|
928
|
-
if (!config.testkit.local) continue;
|
|
929
|
-
const resolvedBaseUrl = baseUrlByService.get(config.name);
|
|
930
|
-
const resolvedReadyUrl = readyUrlByService.get(config.name);
|
|
931
|
-
if (resolvedBaseUrl) {
|
|
932
|
-
urlMappings.push([config.testkit.local.baseUrl, resolvedBaseUrl]);
|
|
933
|
-
}
|
|
934
|
-
if (resolvedReadyUrl) {
|
|
935
|
-
urlMappings.push([config.testkit.local.readyUrl, resolvedReadyUrl]);
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
return runtimeConfigs.map((config) =>
|
|
940
|
-
resolveWorkerConfig(
|
|
941
|
-
config,
|
|
942
|
-
targetConfig,
|
|
943
|
-
workerId,
|
|
944
|
-
workerStateDir,
|
|
945
|
-
portMap,
|
|
946
|
-
baseUrlByService,
|
|
947
|
-
readyUrlByService,
|
|
948
|
-
urlMappings
|
|
949
|
-
)
|
|
950
|
-
);
|
|
739
|
+
return resolveWorkerRuntimeConfigsModel(targetConfig, runtimeConfigs, workerId, workerStateDir);
|
|
951
740
|
}
|
|
952
741
|
|
|
953
742
|
function buildPortMap(runtimeConfigs, workerId) {
|
|
954
|
-
|
|
955
|
-
const seen = new Map();
|
|
956
|
-
const offset = PORT_STRIDE * (workerId - 1);
|
|
957
|
-
|
|
958
|
-
for (const config of runtimeConfigs) {
|
|
959
|
-
if (!config.testkit.local) continue;
|
|
960
|
-
|
|
961
|
-
const basePort = config.testkit.local.port || numericPortFromUrl(config.testkit.local.baseUrl);
|
|
962
|
-
if (!basePort) continue;
|
|
963
|
-
|
|
964
|
-
const actualPort = basePort + offset;
|
|
965
|
-
const existing = seen.get(actualPort);
|
|
966
|
-
if (existing) {
|
|
967
|
-
throw new Error(
|
|
968
|
-
`Worker port collision: services "${existing}" and "${config.name}" both resolve to ${actualPort}. ` +
|
|
969
|
-
`Assign distinct local.port/baseUrl ports in testkit.config.json.`
|
|
970
|
-
);
|
|
971
|
-
}
|
|
972
|
-
seen.set(actualPort, config.name);
|
|
973
|
-
portMap.set(config.name, actualPort);
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return portMap;
|
|
743
|
+
return buildPortMapModel(runtimeConfigs, workerId);
|
|
977
744
|
}
|
|
978
745
|
|
|
979
746
|
function resolveWorkerConfig(
|
|
@@ -986,210 +753,48 @@ function resolveWorkerConfig(
|
|
|
986
753
|
readyUrlByService,
|
|
987
754
|
urlMappings
|
|
988
755
|
) {
|
|
989
|
-
|
|
990
|
-
|
|
756
|
+
return resolveWorkerConfigModel(
|
|
757
|
+
config,
|
|
758
|
+
targetConfig,
|
|
991
759
|
workerId,
|
|
992
|
-
|
|
993
|
-
targetName: targetConfig.name,
|
|
994
|
-
serviceStateDir: stateDir,
|
|
760
|
+
workerStateDir,
|
|
995
761
|
portMap,
|
|
996
762
|
baseUrlByService,
|
|
997
763
|
readyUrlByService,
|
|
998
|
-
urlMappings
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
const database = config.testkit.database
|
|
1002
|
-
? {
|
|
1003
|
-
...config.testkit.database,
|
|
1004
|
-
branchName:
|
|
1005
|
-
config.testkit.database.provider === "neon" &&
|
|
1006
|
-
config.testkit.database.branchName !== undefined
|
|
1007
|
-
? finalizeString(config.testkit.database.branchName, context)
|
|
1008
|
-
: config.testkit.database.provider === "neon"
|
|
1009
|
-
? `${targetConfig.name}-${config.name}-w${workerId}-testkit`
|
|
1010
|
-
: undefined,
|
|
1011
|
-
}
|
|
1012
|
-
: undefined;
|
|
1013
|
-
|
|
1014
|
-
const migrate = config.testkit.migrate
|
|
1015
|
-
? {
|
|
1016
|
-
...config.testkit.migrate,
|
|
1017
|
-
cmd: finalizeString(config.testkit.migrate.cmd, context),
|
|
1018
|
-
cwd:
|
|
1019
|
-
config.testkit.migrate.cwd !== undefined
|
|
1020
|
-
? finalizeString(config.testkit.migrate.cwd, context)
|
|
1021
|
-
: config.testkit.migrate.cwd,
|
|
1022
|
-
}
|
|
1023
|
-
: undefined;
|
|
1024
|
-
|
|
1025
|
-
const seed = config.testkit.seed
|
|
1026
|
-
? {
|
|
1027
|
-
...config.testkit.seed,
|
|
1028
|
-
cmd: finalizeString(config.testkit.seed.cmd, context),
|
|
1029
|
-
cwd:
|
|
1030
|
-
config.testkit.seed.cwd !== undefined
|
|
1031
|
-
? finalizeString(config.testkit.seed.cwd, context)
|
|
1032
|
-
: config.testkit.seed.cwd,
|
|
1033
|
-
}
|
|
1034
|
-
: undefined;
|
|
1035
|
-
|
|
1036
|
-
const local = config.testkit.local
|
|
1037
|
-
? {
|
|
1038
|
-
...config.testkit.local,
|
|
1039
|
-
start: finalizeString(config.testkit.local.start, context),
|
|
1040
|
-
cwd:
|
|
1041
|
-
config.testkit.local.cwd !== undefined
|
|
1042
|
-
? finalizeString(config.testkit.local.cwd, context)
|
|
1043
|
-
: config.testkit.local.cwd,
|
|
1044
|
-
port: portMap.get(config.name) || config.testkit.local.port,
|
|
1045
|
-
baseUrl: baseUrlByService.get(config.name),
|
|
1046
|
-
readyUrl: readyUrlByService.get(config.name),
|
|
1047
|
-
env: Object.fromEntries(
|
|
1048
|
-
Object.entries(config.testkit.local.env || {}).map(([key, value]) => [
|
|
1049
|
-
key,
|
|
1050
|
-
finalizeString(String(value), context),
|
|
1051
|
-
])
|
|
1052
|
-
),
|
|
1053
|
-
}
|
|
1054
|
-
: undefined;
|
|
1055
|
-
|
|
1056
|
-
return {
|
|
1057
|
-
...config,
|
|
1058
|
-
stateDir,
|
|
1059
|
-
workerId,
|
|
1060
|
-
workerLabel: `w${workerId}`,
|
|
1061
|
-
targetName: targetConfig.name,
|
|
1062
|
-
testkit: {
|
|
1063
|
-
...config.testkit,
|
|
1064
|
-
database,
|
|
1065
|
-
migrate,
|
|
1066
|
-
seed,
|
|
1067
|
-
local,
|
|
1068
|
-
},
|
|
1069
|
-
};
|
|
764
|
+
urlMappings
|
|
765
|
+
);
|
|
1070
766
|
}
|
|
1071
767
|
|
|
1072
768
|
function resolveServiceStateDir(workerStateDir, targetName, config) {
|
|
1073
|
-
|
|
1074
|
-
return getWorkerServiceStateDir(workerStateDir, targetName, dbSource);
|
|
769
|
+
return resolveServiceStateDirModel(workerStateDir, targetName, config);
|
|
1075
770
|
}
|
|
1076
771
|
|
|
1077
772
|
function getWorkerServiceStateDir(workerStateDir, targetName, serviceName) {
|
|
1078
|
-
|
|
1079
|
-
return workerStateDir;
|
|
1080
|
-
}
|
|
1081
|
-
return path.join(workerStateDir, "deps", serviceName);
|
|
773
|
+
return getWorkerServiceStateDirModel(workerStateDir, targetName, serviceName);
|
|
1082
774
|
}
|
|
1083
775
|
|
|
1084
776
|
function buildExecutionEnv(config, extraEnv = {}) {
|
|
1085
|
-
return
|
|
1086
|
-
...process.env,
|
|
1087
|
-
...(config.testkit.serviceEnv || {}),
|
|
1088
|
-
...extraEnv,
|
|
1089
|
-
};
|
|
777
|
+
return buildExecutionEnvModel(config, extraEnv, process.env);
|
|
1090
778
|
}
|
|
1091
779
|
|
|
1092
780
|
function buildPlaywrightEnv(config, baseUrl) {
|
|
1093
|
-
return
|
|
1094
|
-
BASE_URL: baseUrl,
|
|
1095
|
-
PLAYWRIGHT_HTML_OPEN: "never",
|
|
1096
|
-
PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS:
|
|
1097
|
-
process.env.PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS || "1",
|
|
1098
|
-
TESTKIT_MANAGED_SERVERS: "1",
|
|
1099
|
-
TESTKIT_WORKER_ID: String(config.workerId),
|
|
1100
|
-
});
|
|
781
|
+
return buildPlaywrightEnvModel(config, baseUrl, process.env);
|
|
1101
782
|
}
|
|
1102
783
|
|
|
1103
784
|
function recordTaskOutcome(trackers, task, outcome) {
|
|
1104
|
-
|
|
1105
|
-
if (!tracker || tracker.skipped) return;
|
|
1106
|
-
|
|
1107
|
-
const finishedAt = Date.now();
|
|
1108
|
-
if (!tracker.firstTaskAt) tracker.firstTaskAt = finishedAt;
|
|
1109
|
-
tracker.lastTaskAt = finishedAt;
|
|
1110
|
-
|
|
1111
|
-
const suite = tracker.suitesByKey.get(task.suiteKey);
|
|
1112
|
-
if (!suite) return;
|
|
1113
|
-
|
|
1114
|
-
suite.completedFileCount += 1;
|
|
1115
|
-
suite.durationMs += outcome.durationMs;
|
|
1116
|
-
if (outcome.failed && !suite.failedFileSet.has(task.file)) {
|
|
1117
|
-
suite.failedFileSet.add(task.file);
|
|
1118
|
-
suite.failedFiles.push(task.file);
|
|
1119
|
-
}
|
|
1120
|
-
if (outcome.error && !suite.error) {
|
|
1121
|
-
suite.error = outcome.error;
|
|
1122
|
-
}
|
|
785
|
+
return recordTaskOutcomeModel(trackers, task, outcome);
|
|
1123
786
|
}
|
|
1124
787
|
|
|
1125
788
|
function recordGraphError(trackers, graph, message) {
|
|
1126
|
-
|
|
1127
|
-
for (const targetName of targetNames) {
|
|
1128
|
-
const tracker = trackers.get(targetName);
|
|
1129
|
-
if (tracker && !tracker.skipped) {
|
|
1130
|
-
addTrackerError(tracker, message);
|
|
1131
|
-
tracker.lastTaskAt = Date.now();
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
789
|
+
return recordGraphErrorModel(trackers, graph, message);
|
|
1134
790
|
}
|
|
1135
791
|
|
|
1136
792
|
function addTrackerError(tracker, message) {
|
|
1137
|
-
|
|
1138
|
-
tracker.errorSet.add(message);
|
|
1139
|
-
tracker.errors.push(message);
|
|
793
|
+
return addTrackerErrorModel(tracker, message);
|
|
1140
794
|
}
|
|
1141
795
|
|
|
1142
796
|
function finalizeServiceResult(tracker, startedAt, finishedAt) {
|
|
1143
|
-
|
|
1144
|
-
return {
|
|
1145
|
-
name: tracker?.name || "unknown",
|
|
1146
|
-
failed: false,
|
|
1147
|
-
skipped: true,
|
|
1148
|
-
suiteCount: 0,
|
|
1149
|
-
completedSuiteCount: 0,
|
|
1150
|
-
failedSuiteCount: 0,
|
|
1151
|
-
durationMs: 0,
|
|
1152
|
-
suites: [],
|
|
1153
|
-
errors: [],
|
|
1154
|
-
};
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const suites = [...tracker.suites].sort(
|
|
1158
|
-
(a, b) => a.orderIndex - b.orderIndex || a.name.localeCompare(b.name)
|
|
1159
|
-
);
|
|
1160
|
-
const completedSuiteCount = suites.filter(
|
|
1161
|
-
(suite) => suite.completedFileCount === suite.fileCount
|
|
1162
|
-
).length;
|
|
1163
|
-
const failedSuiteCount = suites.filter((suite) => suite.failedFiles.length > 0).length;
|
|
1164
|
-
const accumulatedDurationMs = suites.reduce((sum, suite) => sum + suite.durationMs, 0);
|
|
1165
|
-
const durationMs =
|
|
1166
|
-
tracker.firstTaskAt && tracker.lastTaskAt
|
|
1167
|
-
? Math.max(tracker.lastTaskAt - tracker.firstTaskAt, accumulatedDurationMs)
|
|
1168
|
-
: Math.max(finishedAt - startedAt, accumulatedDurationMs);
|
|
1169
|
-
|
|
1170
|
-
return {
|
|
1171
|
-
name: tracker.name,
|
|
1172
|
-
failed:
|
|
1173
|
-
failedSuiteCount > 0 ||
|
|
1174
|
-
tracker.errors.length > 0 ||
|
|
1175
|
-
completedSuiteCount < tracker.suiteCount,
|
|
1176
|
-
skipped: false,
|
|
1177
|
-
suiteCount: tracker.suiteCount,
|
|
1178
|
-
completedSuiteCount,
|
|
1179
|
-
failedSuiteCount,
|
|
1180
|
-
durationMs,
|
|
1181
|
-
suites: suites.map((suite) => ({
|
|
1182
|
-
name: suite.name,
|
|
1183
|
-
type: suite.type,
|
|
1184
|
-
framework: suite.framework,
|
|
1185
|
-
failed: suite.failedFiles.length > 0,
|
|
1186
|
-
fileCount: suite.fileCount,
|
|
1187
|
-
failedFiles: suite.failedFiles,
|
|
1188
|
-
durationMs: suite.durationMs,
|
|
1189
|
-
error: suite.error,
|
|
1190
|
-
})),
|
|
1191
|
-
errors: tracker.errors,
|
|
1192
|
-
};
|
|
797
|
+
return finalizeServiceResultModel(tracker, startedAt, finishedAt);
|
|
1193
798
|
}
|
|
1194
799
|
|
|
1195
800
|
function printRunSummary(results, durationMs) {
|
|
@@ -1251,86 +856,121 @@ function printRunSummary(results, durationMs) {
|
|
|
1251
856
|
console.log("\nResult: PASSED");
|
|
1252
857
|
}
|
|
1253
858
|
|
|
859
|
+
async function reportTelemetry(telemetry, artifact) {
|
|
860
|
+
if (!telemetry?.enabled) return;
|
|
861
|
+
|
|
862
|
+
try {
|
|
863
|
+
const outcome = await uploadTelemetryArtifact(telemetry, artifact);
|
|
864
|
+
if (outcome?.ok) {
|
|
865
|
+
console.log("Telemetry: uploaded run artifact");
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (outcome?.reason === "missing-token") {
|
|
869
|
+
console.log(
|
|
870
|
+
`Telemetry: skipped upload because ${telemetry.tokenEnv || "configured token env"} is not set`
|
|
871
|
+
);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (outcome?.reason && !outcome.skipped) return;
|
|
875
|
+
} catch (error) {
|
|
876
|
+
console.log(`Telemetry: upload failed (${formatError(error)})`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
1254
880
|
function longestServiceName(results) {
|
|
1255
|
-
return results
|
|
881
|
+
return longestServiceNameModel(results);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function buildRunArtifact({
|
|
885
|
+
productDir,
|
|
886
|
+
results,
|
|
887
|
+
startedAt,
|
|
888
|
+
finishedAt,
|
|
889
|
+
requestedJobs,
|
|
890
|
+
workerCount,
|
|
891
|
+
suiteType,
|
|
892
|
+
suiteNames,
|
|
893
|
+
framework,
|
|
894
|
+
shard,
|
|
895
|
+
serviceFilter,
|
|
896
|
+
}) {
|
|
897
|
+
return buildRunArtifactModel({
|
|
898
|
+
productDir,
|
|
899
|
+
results,
|
|
900
|
+
startedAt,
|
|
901
|
+
finishedAt,
|
|
902
|
+
requestedJobs,
|
|
903
|
+
workerCount,
|
|
904
|
+
suiteType,
|
|
905
|
+
suiteNames,
|
|
906
|
+
framework,
|
|
907
|
+
shard,
|
|
908
|
+
serviceFilter,
|
|
909
|
+
metadata: {
|
|
910
|
+
git: collectGitMetadata(productDir),
|
|
911
|
+
host: {
|
|
912
|
+
hostname: safeHostname(),
|
|
913
|
+
username: safeUsername(),
|
|
914
|
+
},
|
|
915
|
+
testkitVersion: readPackageMetadata().version,
|
|
916
|
+
},
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
function writeRunArtifact(productDir, artifact) {
|
|
921
|
+
const resultsDir = path.join(productDir, ".testkit", "results");
|
|
922
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
923
|
+
fs.writeFileSync(path.join(resultsDir, "latest.json"), JSON.stringify(artifact, null, 2));
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function summarizeDbBackend(results) {
|
|
927
|
+
return summarizeDbBackendModel(results);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function collectGitMetadata(productDir) {
|
|
931
|
+
return collectGitMetadataModel(productDir);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function readPackageMetadata() {
|
|
935
|
+
return readPackageMetadataModel();
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function safeHostname() {
|
|
939
|
+
return safeHostnameModel();
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function safeUsername() {
|
|
943
|
+
return safeUsernameModel();
|
|
1256
944
|
}
|
|
1257
945
|
|
|
1258
946
|
function formatDuration(durationMs) {
|
|
1259
|
-
|
|
1260
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1261
|
-
const seconds = totalSeconds % 60;
|
|
1262
|
-
if (minutes === 0) return `${seconds}s`;
|
|
1263
|
-
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
947
|
+
return formatDurationModel(durationMs);
|
|
1264
948
|
}
|
|
1265
949
|
|
|
1266
950
|
function formatServiceSummary(result) {
|
|
1267
|
-
|
|
1268
|
-
const notRun = result.suiteCount - result.completedSuiteCount;
|
|
1269
|
-
let detail = `${passedSuites}/${result.suiteCount} suites passed`;
|
|
1270
|
-
if (notRun > 0) {
|
|
1271
|
-
detail += `, ${notRun} not run`;
|
|
1272
|
-
}
|
|
1273
|
-
return detail;
|
|
951
|
+
return formatServiceSummaryModel(result);
|
|
1274
952
|
}
|
|
1275
953
|
|
|
1276
954
|
function formatError(error) {
|
|
1277
|
-
|
|
1278
|
-
return String(error);
|
|
955
|
+
return formatErrorModel(error);
|
|
1279
956
|
}
|
|
1280
957
|
|
|
1281
958
|
function loadTimings(productDir) {
|
|
1282
959
|
const filePath = path.join(productDir, ".testkit", TIMINGS_FILENAME);
|
|
1283
960
|
if (!fs.existsSync(filePath)) {
|
|
1284
|
-
return
|
|
1285
|
-
version: 1,
|
|
1286
|
-
files: {},
|
|
1287
|
-
};
|
|
961
|
+
return createEmptyTimings();
|
|
1288
962
|
}
|
|
1289
963
|
|
|
1290
964
|
try {
|
|
1291
|
-
|
|
1292
|
-
return {
|
|
1293
|
-
version: 1,
|
|
1294
|
-
files: parsed.files && typeof parsed.files === "object" ? parsed.files : {},
|
|
1295
|
-
};
|
|
965
|
+
return normalizeTimings(JSON.parse(fs.readFileSync(filePath, "utf8")));
|
|
1296
966
|
} catch {
|
|
1297
|
-
return
|
|
1298
|
-
version: 1,
|
|
1299
|
-
files: {},
|
|
1300
|
-
};
|
|
967
|
+
return createEmptyTimings();
|
|
1301
968
|
}
|
|
1302
969
|
}
|
|
1303
970
|
|
|
1304
971
|
function saveTimings(productDir, timings, updates) {
|
|
1305
972
|
if (updates.length === 0) return;
|
|
1306
|
-
|
|
1307
|
-
const next = {
|
|
1308
|
-
version: 1,
|
|
1309
|
-
files: { ...timings.files },
|
|
1310
|
-
};
|
|
1311
|
-
|
|
1312
|
-
for (const update of updates) {
|
|
1313
|
-
const existing = next.files[update.key];
|
|
1314
|
-
if (!existing) {
|
|
1315
|
-
next.files[update.key] = {
|
|
1316
|
-
durationMs: Math.max(1, Math.round(update.durationMs)),
|
|
1317
|
-
runs: 1,
|
|
1318
|
-
updatedAt: new Date().toISOString(),
|
|
1319
|
-
};
|
|
1320
|
-
continue;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
const runs = Number(existing.runs || 0) + 1;
|
|
1324
|
-
const durationMs = Math.max(
|
|
1325
|
-
1,
|
|
1326
|
-
Math.round(((existing.durationMs || update.durationMs) * (runs - 1) + update.durationMs) / runs)
|
|
1327
|
-
);
|
|
1328
|
-
next.files[update.key] = {
|
|
1329
|
-
durationMs,
|
|
1330
|
-
runs,
|
|
1331
|
-
updatedAt: new Date().toISOString(),
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
973
|
+
const next = applyTimingUpdates(timings, updates);
|
|
1334
974
|
|
|
1335
975
|
const rootDir = path.join(productDir, ".testkit");
|
|
1336
976
|
fs.mkdirSync(rootDir, { recursive: true });
|
|
@@ -1341,140 +981,51 @@ function saveTimings(productDir, timings, updates) {
|
|
|
1341
981
|
}
|
|
1342
982
|
|
|
1343
983
|
function estimateTaskDuration(timings, timingKey, suite) {
|
|
1344
|
-
|
|
1345
|
-
if (cached?.durationMs) return cached.durationMs;
|
|
1346
|
-
|
|
1347
|
-
const base =
|
|
1348
|
-
suite.framework === "playwright"
|
|
1349
|
-
? 20_000
|
|
1350
|
-
: suite.type === "dal"
|
|
1351
|
-
? 4_000
|
|
1352
|
-
: 8_000;
|
|
1353
|
-
return Math.max(1_000, Math.round((base * suite.weight) / Math.max(1, suite.files.length)));
|
|
984
|
+
return estimateTaskDurationModel(timings, timingKey, suite);
|
|
1354
985
|
}
|
|
1355
986
|
|
|
1356
987
|
function buildTimingKey(serviceName, suite, file) {
|
|
1357
|
-
return
|
|
1358
|
-
serviceName,
|
|
1359
|
-
suite.framework,
|
|
1360
|
-
suite.type,
|
|
1361
|
-
normalizePathSeparators(file),
|
|
1362
|
-
].join("|");
|
|
988
|
+
return buildTimingKeyModel(serviceName, suite, file);
|
|
1363
989
|
}
|
|
1364
990
|
|
|
1365
991
|
function parsePlaywrightJsonResults(stdout, cwd) {
|
|
1366
|
-
|
|
1367
|
-
return { fileResults: new Map(), errors: [] };
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
let parsed;
|
|
1371
|
-
try {
|
|
1372
|
-
parsed = JSON.parse(stdout);
|
|
1373
|
-
} catch (error) {
|
|
1374
|
-
return {
|
|
1375
|
-
fileResults: new Map(),
|
|
1376
|
-
errors: [`Could not parse Playwright JSON output: ${formatError(error)}`],
|
|
1377
|
-
};
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
const fileResults = new Map();
|
|
1381
|
-
visitPlaywrightSuites(parsed.suites || [], null, fileResults, cwd);
|
|
1382
|
-
return {
|
|
1383
|
-
fileResults,
|
|
1384
|
-
errors: (parsed.errors || []).map(formatPlaywrightReporterError).filter(Boolean),
|
|
1385
|
-
};
|
|
992
|
+
return parsePlaywrightJsonResultsModel(stdout, cwd);
|
|
1386
993
|
}
|
|
1387
994
|
|
|
1388
995
|
function visitPlaywrightSuites(suites, inheritedFile, fileResults, cwd) {
|
|
1389
|
-
|
|
1390
|
-
const suiteFile = normalizeReportedFile(extractReporterFile(suite) || inheritedFile, cwd);
|
|
1391
|
-
for (const child of suite.suites || []) {
|
|
1392
|
-
visitPlaywrightSuites([child], suiteFile, fileResults, cwd);
|
|
1393
|
-
}
|
|
1394
|
-
for (const spec of suite.specs || []) {
|
|
1395
|
-
collectPlaywrightSpec(spec, suiteFile, fileResults, cwd);
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
996
|
+
return visitPlaywrightSuitesModel(suites, inheritedFile, fileResults, cwd);
|
|
1398
997
|
}
|
|
1399
998
|
|
|
1400
999
|
function collectPlaywrightSpec(spec, inheritedFile, fileResults, cwd) {
|
|
1401
|
-
|
|
1402
|
-
if (!file) return;
|
|
1403
|
-
|
|
1404
|
-
const current = fileResults.get(file) || {
|
|
1405
|
-
failed: false,
|
|
1406
|
-
error: null,
|
|
1407
|
-
durationMs: 0,
|
|
1408
|
-
};
|
|
1409
|
-
|
|
1410
|
-
for (const test of spec.tests || []) {
|
|
1411
|
-
const results = Array.isArray(test.results) ? test.results : [];
|
|
1412
|
-
current.durationMs += results.reduce(
|
|
1413
|
-
(sum, result) => sum + Number(result?.duration || 0),
|
|
1414
|
-
0
|
|
1415
|
-
);
|
|
1416
|
-
|
|
1417
|
-
const final = choosePlaywrightFinalResult(results);
|
|
1418
|
-
const failed =
|
|
1419
|
-
test.outcome === "unexpected" ||
|
|
1420
|
-
!isPlaywrightPassingStatus(final?.status);
|
|
1421
|
-
|
|
1422
|
-
if (failed) {
|
|
1423
|
-
current.failed = true;
|
|
1424
|
-
current.error ||= extractPlaywrightFailure(final, spec, test);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
fileResults.set(file, current);
|
|
1000
|
+
return collectPlaywrightSpecModel(spec, inheritedFile, fileResults, cwd);
|
|
1429
1001
|
}
|
|
1430
1002
|
|
|
1431
1003
|
function choosePlaywrightFinalResult(results) {
|
|
1432
|
-
|
|
1433
|
-
return results[results.length - 1];
|
|
1004
|
+
return choosePlaywrightFinalResultModel(results);
|
|
1434
1005
|
}
|
|
1435
1006
|
|
|
1436
1007
|
function isPlaywrightPassingStatus(status) {
|
|
1437
|
-
return
|
|
1008
|
+
return isPlaywrightPassingStatusModel(status);
|
|
1438
1009
|
}
|
|
1439
1010
|
|
|
1440
1011
|
function extractPlaywrightFailure(finalResult, spec, test) {
|
|
1441
|
-
|
|
1442
|
-
finalResult?.error?.message ||
|
|
1443
|
-
finalResult?.error?.value ||
|
|
1444
|
-
finalResult?.error?.stack;
|
|
1445
|
-
if (fromResult) return firstLine(fromResult);
|
|
1446
|
-
|
|
1447
|
-
const fromTest = test?.errors?.[0]?.message;
|
|
1448
|
-
if (fromTest) return firstLine(fromTest);
|
|
1449
|
-
|
|
1450
|
-
return firstLine(spec?.title || "Playwright test failed");
|
|
1012
|
+
return extractPlaywrightFailureModel(finalResult, spec, test);
|
|
1451
1013
|
}
|
|
1452
1014
|
|
|
1453
1015
|
function formatPlaywrightReporterError(error) {
|
|
1454
|
-
|
|
1455
|
-
if (typeof error === "string") return firstLine(error);
|
|
1456
|
-
if (typeof error.message === "string") return firstLine(error.message);
|
|
1457
|
-
if (typeof error.value === "string") return firstLine(error.value);
|
|
1458
|
-
return null;
|
|
1016
|
+
return formatPlaywrightReporterErrorModel(error);
|
|
1459
1017
|
}
|
|
1460
1018
|
|
|
1461
1019
|
function extractReporterFile(node) {
|
|
1462
|
-
|
|
1463
|
-
if (typeof node.file === "string" && node.file.length > 0) return node.file;
|
|
1464
|
-
if (node.location && typeof node.location.file === "string" && node.location.file.length > 0) {
|
|
1465
|
-
return node.location.file;
|
|
1466
|
-
}
|
|
1467
|
-
return null;
|
|
1020
|
+
return extractReporterFileModel(node);
|
|
1468
1021
|
}
|
|
1469
1022
|
|
|
1470
1023
|
function normalizeReportedFile(filePath, cwd) {
|
|
1471
|
-
|
|
1472
|
-
const absolute = path.isAbsolute(filePath) ? filePath : path.join(cwd, filePath);
|
|
1473
|
-
return normalizePathSeparators(path.relative(cwd, absolute));
|
|
1024
|
+
return normalizeReportedFileModel(filePath, cwd);
|
|
1474
1025
|
}
|
|
1475
1026
|
|
|
1476
1027
|
function firstLine(value) {
|
|
1477
|
-
return
|
|
1028
|
+
return firstLineModel(value);
|
|
1478
1029
|
}
|
|
1479
1030
|
|
|
1480
1031
|
function printBufferedOutput(output, prefix) {
|
|
@@ -1486,95 +1037,23 @@ function printBufferedOutput(output, prefix) {
|
|
|
1486
1037
|
}
|
|
1487
1038
|
|
|
1488
1039
|
function resolveRuntimeUrl(rawUrl, serviceName, targetConfig, workerId, context) {
|
|
1489
|
-
|
|
1490
|
-
...context,
|
|
1491
|
-
targetName: targetConfig.name,
|
|
1492
|
-
workerId,
|
|
1493
|
-
serviceName,
|
|
1494
|
-
});
|
|
1495
|
-
const actualPort = context.portMap.get(serviceName);
|
|
1496
|
-
return actualPort ? rewriteUrlPort(resolved, actualPort) : resolved;
|
|
1040
|
+
return resolveRuntimeUrlModel(rawUrl, serviceName, targetConfig, workerId, context);
|
|
1497
1041
|
}
|
|
1498
1042
|
|
|
1499
1043
|
function finalizeString(value, context) {
|
|
1500
|
-
|
|
1501
|
-
for (const [source, destination] of context.urlMappings || []) {
|
|
1502
|
-
if (source && destination && source !== destination) {
|
|
1503
|
-
resolved = resolved.split(source).join(destination);
|
|
1504
|
-
}
|
|
1505
|
-
}
|
|
1506
|
-
return resolved;
|
|
1044
|
+
return finalizeStringModel(value, context);
|
|
1507
1045
|
}
|
|
1508
1046
|
|
|
1509
1047
|
function resolveTemplateString(value, context) {
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
return value.replace(/\{([a-zA-Z]+)(?::([a-zA-Z0-9_-]+))?\}/g, (_match, token, arg) => {
|
|
1513
|
-
switch (token) {
|
|
1514
|
-
case "worker":
|
|
1515
|
-
return String(context.workerId);
|
|
1516
|
-
case "target":
|
|
1517
|
-
return context.targetName;
|
|
1518
|
-
case "service":
|
|
1519
|
-
return context.serviceName;
|
|
1520
|
-
case "stateDir":
|
|
1521
|
-
return context.serviceStateDir;
|
|
1522
|
-
case "port": {
|
|
1523
|
-
const serviceName = arg || context.serviceName;
|
|
1524
|
-
const port = context.portMap.get(serviceName);
|
|
1525
|
-
if (!port) {
|
|
1526
|
-
throw new Error(`Unknown port placeholder for service "${serviceName}"`);
|
|
1527
|
-
}
|
|
1528
|
-
return String(port);
|
|
1529
|
-
}
|
|
1530
|
-
case "baseUrl": {
|
|
1531
|
-
const serviceName = arg || context.serviceName;
|
|
1532
|
-
const baseUrl = context.baseUrlByService.get(serviceName);
|
|
1533
|
-
if (!baseUrl) {
|
|
1534
|
-
throw new Error(`Unknown baseUrl placeholder for service "${serviceName}"`);
|
|
1535
|
-
}
|
|
1536
|
-
return baseUrl;
|
|
1537
|
-
}
|
|
1538
|
-
case "readyUrl": {
|
|
1539
|
-
const serviceName = arg || context.serviceName;
|
|
1540
|
-
const readyUrl = context.readyUrlByService.get(serviceName);
|
|
1541
|
-
if (!readyUrl) {
|
|
1542
|
-
throw new Error(`Unknown readyUrl placeholder for service "${serviceName}"`);
|
|
1543
|
-
}
|
|
1544
|
-
return readyUrl;
|
|
1545
|
-
}
|
|
1546
|
-
default:
|
|
1547
|
-
throw new Error(`Unsupported template token "{${token}${arg ? `:${arg}` : ""}}"`);
|
|
1548
|
-
}
|
|
1549
|
-
});
|
|
1048
|
+
return resolveTemplateStringModel(value, context);
|
|
1550
1049
|
}
|
|
1551
1050
|
|
|
1552
1051
|
function rewriteUrlPort(rawUrl, port) {
|
|
1553
|
-
|
|
1554
|
-
const original = new URL(rawUrl);
|
|
1555
|
-
if (!original.port) return rawUrl;
|
|
1556
|
-
|
|
1557
|
-
const rewritten = new URL(rawUrl);
|
|
1558
|
-
rewritten.port = String(port);
|
|
1559
|
-
|
|
1560
|
-
let next = rewritten.toString();
|
|
1561
|
-
if (!rawUrl.endsWith("/") && original.pathname === "/" && next.endsWith("/")) {
|
|
1562
|
-
next = next.slice(0, -1);
|
|
1563
|
-
}
|
|
1564
|
-
return next;
|
|
1565
|
-
} catch {
|
|
1566
|
-
return rawUrl;
|
|
1567
|
-
}
|
|
1052
|
+
return rewriteUrlPortModel(rawUrl, port);
|
|
1568
1053
|
}
|
|
1569
1054
|
|
|
1570
1055
|
function numericPortFromUrl(rawUrl) {
|
|
1571
|
-
|
|
1572
|
-
const url = new URL(rawUrl);
|
|
1573
|
-
const port = Number(url.port);
|
|
1574
|
-
return Number.isInteger(port) && port > 0 ? port : null;
|
|
1575
|
-
} catch {
|
|
1576
|
-
return null;
|
|
1577
|
-
}
|
|
1056
|
+
return numericPortFromUrlModel(rawUrl);
|
|
1578
1057
|
}
|
|
1579
1058
|
|
|
1580
1059
|
async function assertLocalServicePortsAvailable(config) {
|
|
@@ -1599,22 +1078,11 @@ async function assertLocalServicePortsAvailable(config) {
|
|
|
1599
1078
|
}
|
|
1600
1079
|
|
|
1601
1080
|
function socketFromUrl(rawUrl) {
|
|
1602
|
-
|
|
1603
|
-
const url = new URL(rawUrl);
|
|
1604
|
-
const port = Number(url.port);
|
|
1605
|
-
if (!Number.isInteger(port) || port <= 0) return null;
|
|
1606
|
-
|
|
1607
|
-
const host = normalizeSocketHost(url.hostname);
|
|
1608
|
-
return host ? { host, port } : null;
|
|
1609
|
-
} catch {
|
|
1610
|
-
return null;
|
|
1611
|
-
}
|
|
1081
|
+
return socketFromUrlModel(rawUrl);
|
|
1612
1082
|
}
|
|
1613
1083
|
|
|
1614
1084
|
function normalizeSocketHost(hostname) {
|
|
1615
|
-
|
|
1616
|
-
if (hostname === "[::1]") return "::1";
|
|
1617
|
-
return hostname;
|
|
1085
|
+
return normalizeSocketHostModel(hostname);
|
|
1618
1086
|
}
|
|
1619
1087
|
|
|
1620
1088
|
async function isPortInUse({ host, port }) {
|
|
@@ -1728,82 +1196,31 @@ function sleep(ms) {
|
|
|
1728
1196
|
}
|
|
1729
1197
|
|
|
1730
1198
|
function findRuntimeStateDirs(rootDir) {
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
const visit = (dir) => {
|
|
1734
|
-
if (!fs.existsSync(dir)) return;
|
|
1735
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1736
|
-
if (isDatabaseStateDir(dir)) {
|
|
1737
|
-
found.push(dir);
|
|
1738
|
-
}
|
|
1739
|
-
|
|
1740
|
-
for (const entry of entries) {
|
|
1741
|
-
if (entry.isDirectory()) {
|
|
1742
|
-
visit(path.join(dir, entry.name));
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
};
|
|
1746
|
-
|
|
1747
|
-
visit(rootDir);
|
|
1748
|
-
return found.sort((a, b) => b.length - a.length);
|
|
1199
|
+
return findRuntimeStateDirsModel(rootDir, isDatabaseStateDir);
|
|
1749
1200
|
}
|
|
1750
1201
|
|
|
1751
1202
|
function findGraphDirsForService(productDir, serviceName) {
|
|
1752
|
-
|
|
1753
|
-
if (!fs.existsSync(graphsRoot)) return [];
|
|
1754
|
-
|
|
1755
|
-
const matches = [];
|
|
1756
|
-
for (const entry of fs.readdirSync(graphsRoot, { withFileTypes: true })) {
|
|
1757
|
-
if (!entry.isDirectory()) continue;
|
|
1758
|
-
const graphDir = path.join(graphsRoot, entry.name);
|
|
1759
|
-
const metadata = readGraphMetadata(graphDir);
|
|
1760
|
-
if (!metadata) continue;
|
|
1761
|
-
if ((metadata.runtimeServices || []).includes(serviceName)) {
|
|
1762
|
-
matches.push(graphDir);
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
return matches.sort();
|
|
1203
|
+
return findGraphDirsForServiceModel(productDir, serviceName);
|
|
1767
1204
|
}
|
|
1768
1205
|
|
|
1769
1206
|
function writeGraphMetadata(graphDir, graph) {
|
|
1770
|
-
|
|
1771
|
-
const metadata = {
|
|
1772
|
-
runtimeServices: graph.runtimeNames,
|
|
1773
|
-
assignedTargets: [...graph.assignedTargets].sort(),
|
|
1774
|
-
rootService: graph.rootConfig.name,
|
|
1775
|
-
};
|
|
1776
|
-
fs.writeFileSync(
|
|
1777
|
-
path.join(graphDir, GRAPH_METADATA),
|
|
1778
|
-
JSON.stringify(metadata, null, 2)
|
|
1779
|
-
);
|
|
1207
|
+
return writeGraphMetadataModel(graphDir, graph);
|
|
1780
1208
|
}
|
|
1781
1209
|
|
|
1782
1210
|
function readGraphMetadata(graphDir) {
|
|
1783
|
-
|
|
1784
|
-
if (!fs.existsSync(metadataPath)) return null;
|
|
1785
|
-
|
|
1786
|
-
try {
|
|
1787
|
-
return JSON.parse(fs.readFileSync(metadataPath, "utf8"));
|
|
1788
|
-
} catch {
|
|
1789
|
-
return null;
|
|
1790
|
-
}
|
|
1211
|
+
return readGraphMetadataModel(graphDir);
|
|
1791
1212
|
}
|
|
1792
1213
|
|
|
1793
1214
|
function isRuntimeSuperset(candidate, target) {
|
|
1794
|
-
return
|
|
1215
|
+
return isRuntimeSupersetModel(candidate, target);
|
|
1795
1216
|
}
|
|
1796
1217
|
|
|
1797
1218
|
function compareGraphsForAssignment(left, right) {
|
|
1798
|
-
|
|
1799
|
-
return left.runtimeNames.length - right.runtimeNames.length;
|
|
1800
|
-
}
|
|
1801
|
-
return left.key.localeCompare(right.key);
|
|
1219
|
+
return compareGraphsForAssignmentModel(left, right);
|
|
1802
1220
|
}
|
|
1803
1221
|
|
|
1804
1222
|
function buildGraphDirName(runtimeNames) {
|
|
1805
|
-
|
|
1806
|
-
return slug.length > 0 ? slug : "graph";
|
|
1223
|
+
return buildGraphDirNameModel(runtimeNames);
|
|
1807
1224
|
}
|
|
1808
1225
|
|
|
1809
1226
|
function slugSegment(value) {
|
|
@@ -1811,5 +1228,5 @@ function slugSegment(value) {
|
|
|
1811
1228
|
}
|
|
1812
1229
|
|
|
1813
1230
|
function normalizePathSeparators(filePath) {
|
|
1814
|
-
return filePath
|
|
1231
|
+
return normalizePathSeparatorsModel(filePath);
|
|
1815
1232
|
}
|