@elench/testkit 0.1.55 → 0.1.57
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 +81 -0
- package/lib/bundler/index.mjs +1 -1
- package/lib/bundler/index.test.mjs +29 -0
- package/lib/cli/args.mjs +2 -2
- package/lib/cli/args.test.mjs +8 -2
- package/lib/cli/command-helpers.mjs +5 -1
- package/lib/cli/commands/discover.mjs +80 -0
- package/lib/cli/commands/run.mjs +2 -2
- package/lib/cli/entrypoint.mjs +3 -1
- package/lib/cli/presentation/colors.mjs +32 -0
- package/lib/cli/presentation/discovery-reporter.mjs +166 -0
- package/lib/cli/viewer.mjs +30 -0
- package/lib/config/discovery.mjs +107 -45
- package/lib/config/discovery.test.mjs +83 -1
- package/lib/config/index.mjs +21 -3
- package/lib/discovery/index.d.ts +121 -0
- package/lib/discovery/index.mjs +540 -0
- package/lib/discovery/index.test.mjs +182 -0
- package/lib/history/index.d.ts +46 -0
- package/lib/history/index.mjs +166 -0
- package/lib/history/index.test.mjs +115 -0
- package/lib/index.d.ts +58 -0
- package/lib/index.mjs +3 -0
- package/lib/package.test.mjs +5 -0
- package/lib/runner/default-runtime-runner.mjs +4 -1
- package/lib/runner/orchestrator.mjs +21 -8
- package/lib/runner/planning.mjs +1 -1
- package/lib/runner/reporting.mjs +6 -0
- package/lib/runner/reporting.test.mjs +5 -0
- package/lib/runner/suite-selection.mjs +4 -4
- package/lib/runner/suite-selection.test.mjs +9 -2
- package/lib/runner/worker-loop.mjs +1 -1
- package/lib/runtime-src/k6/checks.js +9 -0
- package/lib/runtime-src/k6/scenario-runtime.js +234 -0
- package/lib/runtime-src/k6/scenario-suite.js +179 -0
- package/package.json +5 -1
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { loadConfigContext, resolveProductDir } from "../config/index.mjs";
|
|
3
|
+
import { discoverProject } from "../config/discovery.mjs";
|
|
4
|
+
import { loadTestkitSetup } from "../config/setup-loader.mjs";
|
|
5
|
+
import { historyFilePath, loadHistory, summarizeHistoryForFiles } from "../history/index.mjs";
|
|
6
|
+
import {
|
|
7
|
+
matchesSelectedTypes,
|
|
8
|
+
matchesSuiteSelectors,
|
|
9
|
+
normalizeTypeValues,
|
|
10
|
+
parseSuiteSelectors,
|
|
11
|
+
suiteSelectionType,
|
|
12
|
+
} from "../runner/suite-selection.mjs";
|
|
13
|
+
|
|
14
|
+
const DISCOVERY_SCHEMA_VERSION = 1;
|
|
15
|
+
|
|
16
|
+
export async function discoverTests(options = {}) {
|
|
17
|
+
const productDir = resolveProductDir(process.cwd(), options.dir);
|
|
18
|
+
const filters = normalizeDiscoveryFilters(options);
|
|
19
|
+
const setupContext = await loadSetupContext(productDir, filters.diagnosticsMode);
|
|
20
|
+
const baseResult = {
|
|
21
|
+
schemaVersion: DISCOVERY_SCHEMA_VERSION,
|
|
22
|
+
source: "testkit-discovery",
|
|
23
|
+
product: {
|
|
24
|
+
name: path.basename(productDir),
|
|
25
|
+
directory: productDir,
|
|
26
|
+
},
|
|
27
|
+
setupFile: setupContext.setupFile ? path.relative(productDir, setupContext.setupFile) || path.basename(setupContext.setupFile) : null,
|
|
28
|
+
filters: {
|
|
29
|
+
service: filters.serviceFilter,
|
|
30
|
+
types: filters.typeValues,
|
|
31
|
+
suiteSelectors: filters.suiteSelectors.map((selector) => selector.raw),
|
|
32
|
+
fileNames: filters.fileNames,
|
|
33
|
+
runnableOnly: filters.runnableOnly,
|
|
34
|
+
diagnostics: filters.diagnosticsMode,
|
|
35
|
+
},
|
|
36
|
+
services: [],
|
|
37
|
+
suites: [],
|
|
38
|
+
files: [],
|
|
39
|
+
diagnostics: [...setupContext.diagnostics],
|
|
40
|
+
summary: emptySummary(),
|
|
41
|
+
history: {
|
|
42
|
+
available: false,
|
|
43
|
+
path: normalizePath(path.relative(productDir, historyFilePath(productDir))),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!setupContext.setup) {
|
|
48
|
+
return finalizeDiscoveryResult(baseResult);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const rawDiscovery = discoverProject(productDir, setupContext.setup.services || {}, {
|
|
52
|
+
strict: filters.diagnosticsMode === "error",
|
|
53
|
+
});
|
|
54
|
+
baseResult.diagnostics.push(...rawDiscovery.diagnostics);
|
|
55
|
+
validateRequestedService(filters.serviceFilter, setupContext.setup.services || {}, rawDiscovery);
|
|
56
|
+
|
|
57
|
+
let configContext = null;
|
|
58
|
+
try {
|
|
59
|
+
configContext = await loadConfigContext({
|
|
60
|
+
dir: productDir,
|
|
61
|
+
setupContext: {
|
|
62
|
+
setup: setupContext.setup,
|
|
63
|
+
setupFile: setupContext.setupFile,
|
|
64
|
+
},
|
|
65
|
+
discoveryOptions: {
|
|
66
|
+
strict: filters.diagnosticsMode === "error",
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (filters.diagnosticsMode === "error") throw error;
|
|
71
|
+
baseResult.diagnostics.push({
|
|
72
|
+
code: "config_invalid",
|
|
73
|
+
severity: "error",
|
|
74
|
+
message: formatErrorMessage(error),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (configContext) {
|
|
79
|
+
const resolved = buildResolvedDiscovery({
|
|
80
|
+
configs: configContext.configs,
|
|
81
|
+
filters,
|
|
82
|
+
});
|
|
83
|
+
return finalizeDiscoveryResult(
|
|
84
|
+
{
|
|
85
|
+
...baseResult,
|
|
86
|
+
services: resolved.services,
|
|
87
|
+
suites: resolved.suites,
|
|
88
|
+
files: resolved.files,
|
|
89
|
+
},
|
|
90
|
+
productDir
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const rawOnly = buildRawDiscovery({
|
|
95
|
+
rawDiscovery,
|
|
96
|
+
explicitServices: setupContext.setup.services || {},
|
|
97
|
+
filters,
|
|
98
|
+
});
|
|
99
|
+
return finalizeDiscoveryResult(
|
|
100
|
+
{
|
|
101
|
+
...baseResult,
|
|
102
|
+
services: rawOnly.services,
|
|
103
|
+
suites: rawOnly.suites,
|
|
104
|
+
files: rawOnly.files,
|
|
105
|
+
},
|
|
106
|
+
productDir
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function formatSelectionTypeLabel(type) {
|
|
111
|
+
if (type === "int") return "Integration";
|
|
112
|
+
if (type === "e2e") return "E2E";
|
|
113
|
+
if (type === "scenario") return "Scenario";
|
|
114
|
+
if (type === "dal") return "DAL";
|
|
115
|
+
if (type === "load") return "Load";
|
|
116
|
+
if (type === "pw") return "Playwright";
|
|
117
|
+
return type;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function formatDisplayName(value) {
|
|
121
|
+
return String(value || "")
|
|
122
|
+
.split(/[-_/]+/g)
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.map((part) => part.replace(/^\w/, (char) => char.toUpperCase()))
|
|
125
|
+
.join(" ");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function loadSetupContext(productDir, diagnosticsMode) {
|
|
129
|
+
try {
|
|
130
|
+
const { setup, setupFile } = await loadTestkitSetup(productDir);
|
|
131
|
+
return {
|
|
132
|
+
setup,
|
|
133
|
+
setupFile,
|
|
134
|
+
diagnostics: [],
|
|
135
|
+
};
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (diagnosticsMode === "error") throw error;
|
|
138
|
+
return {
|
|
139
|
+
setup: null,
|
|
140
|
+
setupFile: null,
|
|
141
|
+
diagnostics: [
|
|
142
|
+
{
|
|
143
|
+
code: "setup_invalid",
|
|
144
|
+
severity: "error",
|
|
145
|
+
message: formatErrorMessage(error),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function buildResolvedDiscovery({ configs, filters }) {
|
|
153
|
+
const services = [];
|
|
154
|
+
const suites = [];
|
|
155
|
+
const files = [];
|
|
156
|
+
|
|
157
|
+
for (const config of configs) {
|
|
158
|
+
if (filters.serviceFilter && config.name !== filters.serviceFilter) continue;
|
|
159
|
+
|
|
160
|
+
const suiteEntries = [];
|
|
161
|
+
const fileEntries = [];
|
|
162
|
+
for (const [internalType, discoveredSuites] of Object.entries(config.suites || {})) {
|
|
163
|
+
for (const suite of discoveredSuites || []) {
|
|
164
|
+
const suiteEntriesForConfig = buildResolvedSuiteEntries(config, suite, internalType, filters);
|
|
165
|
+
if (!suiteEntriesForConfig) continue;
|
|
166
|
+
suiteEntries.push(suiteEntriesForConfig.suiteEntry);
|
|
167
|
+
fileEntries.push(...suiteEntriesForConfig.fileEntries);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
services.push(buildServiceEntry(config, suiteEntries, fileEntries));
|
|
172
|
+
suites.push(...suiteEntries);
|
|
173
|
+
files.push(...fileEntries);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
services: services.sort((left, right) => left.name.localeCompare(right.name)),
|
|
178
|
+
suites: suites.sort(compareSuiteEntries),
|
|
179
|
+
files: files.sort(compareFileEntries),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function buildResolvedSuiteEntries(config, suite, internalType, filters) {
|
|
184
|
+
const framework = suite.framework || "k6";
|
|
185
|
+
const selectionType = suiteSelectionType(internalType, framework);
|
|
186
|
+
if (!matchesSelectedTypes(selectionType, filters.typeValues)) return null;
|
|
187
|
+
if (!matchesSuiteSelectors(selectionType, suite.name, filters.suiteSelectors)) return null;
|
|
188
|
+
|
|
189
|
+
const suiteSkipReason = findSuiteSkipReason(config, selectionType, suite.name);
|
|
190
|
+
const suiteLocks = findSuiteLocks(config, selectionType, suite.name);
|
|
191
|
+
const visibleFiles = [];
|
|
192
|
+
|
|
193
|
+
for (const filePath of suite.files || []) {
|
|
194
|
+
if (filters.fileNameSet.size > 0 && !filters.fileNameSet.has(filePath)) continue;
|
|
195
|
+
const fileLocks = config.testkit.requirements?.fileLocksByPath?.get(filePath) || [];
|
|
196
|
+
const skipReason = config.testkit.skip?.fileReasonByPath?.get(filePath) || suiteSkipReason || null;
|
|
197
|
+
const skipped = Boolean(skipReason);
|
|
198
|
+
if (filters.runnableOnly && skipped) continue;
|
|
199
|
+
visibleFiles.push({
|
|
200
|
+
id: buildFileId(config.name, selectionType, filePath),
|
|
201
|
+
path: filePath,
|
|
202
|
+
displayName: fileDisplayName(filePath),
|
|
203
|
+
service: config.name,
|
|
204
|
+
suiteName: suite.name,
|
|
205
|
+
groupLabel: formatDisplayName(suite.name),
|
|
206
|
+
selectionType,
|
|
207
|
+
internalType,
|
|
208
|
+
framework,
|
|
209
|
+
skipped,
|
|
210
|
+
skipReason,
|
|
211
|
+
locks: [...new Set([...suiteLocks, ...fileLocks])].sort(),
|
|
212
|
+
dependsOn: [...(config.testkit.dependsOn || [])],
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (visibleFiles.length === 0) return null;
|
|
217
|
+
const skippedFileCount = visibleFiles.filter((entry) => entry.skipped).length;
|
|
218
|
+
const suiteEntry = {
|
|
219
|
+
id: buildSuiteId(config.name, selectionType, suite.name, framework),
|
|
220
|
+
service: config.name,
|
|
221
|
+
name: suite.name,
|
|
222
|
+
displayName: formatDisplayName(suite.name),
|
|
223
|
+
selectionType,
|
|
224
|
+
internalType,
|
|
225
|
+
framework,
|
|
226
|
+
groupLabel: formatDisplayName(suite.name),
|
|
227
|
+
fileCount: visibleFiles.length,
|
|
228
|
+
activeFileCount: visibleFiles.length - skippedFileCount,
|
|
229
|
+
skippedFileCount,
|
|
230
|
+
dependsOn: [...(config.testkit.dependsOn || [])],
|
|
231
|
+
locks: [...new Set(visibleFiles.flatMap((entry) => entry.locks || []))].sort(),
|
|
232
|
+
filePaths: visibleFiles.map((entry) => entry.path),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
suiteEntry,
|
|
237
|
+
fileEntries: visibleFiles,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function buildRawDiscovery({ rawDiscovery, explicitServices, filters }) {
|
|
242
|
+
const servicesByName = new Map();
|
|
243
|
+
const suitesById = new Map();
|
|
244
|
+
const files = [];
|
|
245
|
+
|
|
246
|
+
for (const [name, config] of Object.entries(explicitServices || {})) {
|
|
247
|
+
if (filters.serviceFilter && name !== filters.serviceFilter) continue;
|
|
248
|
+
servicesByName.set(name, {
|
|
249
|
+
name,
|
|
250
|
+
discovered: Boolean(rawDiscovery.services?.[name]),
|
|
251
|
+
localCwd: config?.local?.cwd || rawDiscovery.services?.[name]?.inferredLocalCwd || ".",
|
|
252
|
+
dependsOn: [...(config?.dependsOn || [])],
|
|
253
|
+
suiteCount: 0,
|
|
254
|
+
fileCount: 0,
|
|
255
|
+
activeFileCount: 0,
|
|
256
|
+
skippedFileCount: 0,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const [name, service] of Object.entries(rawDiscovery.services || {})) {
|
|
261
|
+
if (filters.serviceFilter && name !== filters.serviceFilter) continue;
|
|
262
|
+
servicesByName.set(name, {
|
|
263
|
+
name,
|
|
264
|
+
discovered: true,
|
|
265
|
+
localCwd: servicesByName.get(name)?.localCwd || service.inferredLocalCwd || ".",
|
|
266
|
+
dependsOn: [...(explicitServices[name]?.dependsOn || [])],
|
|
267
|
+
suiteCount: servicesByName.get(name)?.suiteCount || 0,
|
|
268
|
+
fileCount: servicesByName.get(name)?.fileCount || 0,
|
|
269
|
+
activeFileCount: servicesByName.get(name)?.activeFileCount || 0,
|
|
270
|
+
skippedFileCount: servicesByName.get(name)?.skippedFileCount || 0,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for (const entry of rawDiscovery.files || []) {
|
|
275
|
+
const selectionType = suiteSelectionType(entry.type, entry.framework);
|
|
276
|
+
if (!matchesSelectedTypes(selectionType, filters.typeValues)) continue;
|
|
277
|
+
if (!matchesSuiteSelectors(selectionType, entry.suiteName, filters.suiteSelectors)) continue;
|
|
278
|
+
if (filters.serviceFilter && entry.serviceName !== filters.serviceFilter) continue;
|
|
279
|
+
if (filters.fileNameSet.size > 0 && !filters.fileNameSet.has(entry.filePath)) continue;
|
|
280
|
+
|
|
281
|
+
const fileEntry = {
|
|
282
|
+
id: buildFileId(entry.serviceName, selectionType, entry.filePath),
|
|
283
|
+
path: entry.filePath,
|
|
284
|
+
displayName: fileDisplayName(entry.filePath),
|
|
285
|
+
service: entry.serviceName,
|
|
286
|
+
suiteName: entry.suiteName,
|
|
287
|
+
groupLabel: formatDisplayName(entry.suiteName),
|
|
288
|
+
selectionType,
|
|
289
|
+
internalType: entry.type,
|
|
290
|
+
framework: entry.framework,
|
|
291
|
+
skipped: false,
|
|
292
|
+
skipReason: null,
|
|
293
|
+
locks: [],
|
|
294
|
+
dependsOn: [...(explicitServices[entry.serviceName]?.dependsOn || [])],
|
|
295
|
+
};
|
|
296
|
+
files.push(fileEntry);
|
|
297
|
+
|
|
298
|
+
const suiteId = buildSuiteId(entry.serviceName, selectionType, entry.suiteName, entry.framework);
|
|
299
|
+
const existingSuite = suitesById.get(suiteId);
|
|
300
|
+
if (existingSuite) {
|
|
301
|
+
existingSuite.filePaths.push(entry.filePath);
|
|
302
|
+
existingSuite.fileCount += 1;
|
|
303
|
+
existingSuite.activeFileCount += 1;
|
|
304
|
+
} else {
|
|
305
|
+
suitesById.set(suiteId, {
|
|
306
|
+
id: suiteId,
|
|
307
|
+
service: entry.serviceName,
|
|
308
|
+
name: entry.suiteName,
|
|
309
|
+
displayName: formatDisplayName(entry.suiteName),
|
|
310
|
+
selectionType,
|
|
311
|
+
internalType: entry.type,
|
|
312
|
+
framework: entry.framework,
|
|
313
|
+
groupLabel: formatDisplayName(entry.suiteName),
|
|
314
|
+
fileCount: 1,
|
|
315
|
+
activeFileCount: 1,
|
|
316
|
+
skippedFileCount: 0,
|
|
317
|
+
dependsOn: [...(explicitServices[entry.serviceName]?.dependsOn || [])],
|
|
318
|
+
locks: [],
|
|
319
|
+
filePaths: [entry.filePath],
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
for (const suite of suitesById.values()) {
|
|
325
|
+
const service = servicesByName.get(suite.service) || {
|
|
326
|
+
name: suite.service,
|
|
327
|
+
discovered: true,
|
|
328
|
+
localCwd: rawDiscovery.services?.[suite.service]?.inferredLocalCwd || ".",
|
|
329
|
+
dependsOn: [...(explicitServices[suite.service]?.dependsOn || [])],
|
|
330
|
+
suiteCount: 0,
|
|
331
|
+
fileCount: 0,
|
|
332
|
+
activeFileCount: 0,
|
|
333
|
+
skippedFileCount: 0,
|
|
334
|
+
};
|
|
335
|
+
service.suiteCount += 1;
|
|
336
|
+
service.fileCount += suite.fileCount;
|
|
337
|
+
service.activeFileCount += suite.activeFileCount;
|
|
338
|
+
service.skippedFileCount += suite.skippedFileCount;
|
|
339
|
+
servicesByName.set(suite.service, service);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
services: [...servicesByName.values()].sort((left, right) => left.name.localeCompare(right.name)),
|
|
344
|
+
suites: [...suitesById.values()].sort(compareSuiteEntries),
|
|
345
|
+
files: files.sort(compareFileEntries),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function buildServiceEntry(config, suiteEntries, fileEntries) {
|
|
350
|
+
return {
|
|
351
|
+
name: config.name,
|
|
352
|
+
discovered: true,
|
|
353
|
+
localCwd: config.testkit.local?.cwd || ".",
|
|
354
|
+
dependsOn: [...(config.testkit.dependsOn || [])],
|
|
355
|
+
suiteCount: suiteEntries.length,
|
|
356
|
+
fileCount: fileEntries.length,
|
|
357
|
+
activeFileCount: fileEntries.filter((entry) => !entry.skipped).length,
|
|
358
|
+
skippedFileCount: fileEntries.filter((entry) => entry.skipped).length,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function finalizeDiscoveryResult(result, productDir) {
|
|
363
|
+
const history = loadHistory(productDir);
|
|
364
|
+
const historyById = summarizeHistoryForFiles(history, result.files);
|
|
365
|
+
const files = result.files.map((entry) => {
|
|
366
|
+
const historyEntry = historyById.get(entry.id);
|
|
367
|
+
if (!historyEntry) return entry;
|
|
368
|
+
return {
|
|
369
|
+
...entry,
|
|
370
|
+
history: historyEntry,
|
|
371
|
+
};
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
...result,
|
|
376
|
+
files,
|
|
377
|
+
diagnostics: result.diagnostics.sort(compareDiagnostics),
|
|
378
|
+
summary: buildSummary(result.services, result.suites, files, result.diagnostics),
|
|
379
|
+
history: {
|
|
380
|
+
...result.history,
|
|
381
|
+
available: Object.keys(history.tests || {}).length > 0,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildSummary(services, suites, files, diagnostics) {
|
|
387
|
+
const byService = {};
|
|
388
|
+
const byType = {};
|
|
389
|
+
|
|
390
|
+
for (const file of files) {
|
|
391
|
+
byService[file.service] = (byService[file.service] || 0) + 1;
|
|
392
|
+
byType[file.selectionType] = (byType[file.selectionType] || 0) + 1;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
services: services.length,
|
|
397
|
+
suites: suites.length,
|
|
398
|
+
files: files.length,
|
|
399
|
+
activeFiles: files.filter((entry) => !entry.skipped).length,
|
|
400
|
+
skippedFiles: files.filter((entry) => entry.skipped).length,
|
|
401
|
+
diagnostics: {
|
|
402
|
+
errors: diagnostics.filter((entry) => entry.severity === "error").length,
|
|
403
|
+
warnings: diagnostics.filter((entry) => entry.severity === "warning").length,
|
|
404
|
+
},
|
|
405
|
+
byService,
|
|
406
|
+
byType,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function validateRequestedService(serviceFilter, explicitServices, rawDiscovery) {
|
|
411
|
+
if (!serviceFilter) return;
|
|
412
|
+
const available = new Set([
|
|
413
|
+
...Object.keys(explicitServices || {}),
|
|
414
|
+
...Object.keys(rawDiscovery.services || {}),
|
|
415
|
+
...Object.keys(rawDiscovery.suitesByService || {}),
|
|
416
|
+
]);
|
|
417
|
+
if (available.has(serviceFilter)) return;
|
|
418
|
+
const formatted = [...available].sort((left, right) => left.localeCompare(right)).join(", ");
|
|
419
|
+
throw new Error(
|
|
420
|
+
formatted.length > 0
|
|
421
|
+
? `Service "${serviceFilter}" not found. Available: ${formatted}`
|
|
422
|
+
: `Service "${serviceFilter}" not found.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function normalizeDiscoveryFilters(options) {
|
|
427
|
+
const typeValues = normalizeTypeValues(normalizeMultiValue(options.type));
|
|
428
|
+
const suiteSelectors = parseSuiteSelectors(normalizeMultiValue(options.suite));
|
|
429
|
+
const fileNames = normalizeMultiValue(options.file).map(normalizePath);
|
|
430
|
+
return {
|
|
431
|
+
typeValues,
|
|
432
|
+
suiteSelectors,
|
|
433
|
+
fileNames,
|
|
434
|
+
fileNameSet: new Set(fileNames),
|
|
435
|
+
serviceFilter: options.service || null,
|
|
436
|
+
runnableOnly: Boolean(options.runnableOnly),
|
|
437
|
+
diagnosticsMode: options.diagnostics === "error" ? "error" : "report",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function findSuiteSkipReason(config, selectionType, suiteName) {
|
|
442
|
+
for (const rule of config.testkit.skip?.suites || []) {
|
|
443
|
+
if (matchesSuiteSelectors(selectionType, suiteName, [rule.selector])) {
|
|
444
|
+
return rule.reason;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return null;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function findSuiteLocks(config, selectionType, suiteName) {
|
|
451
|
+
const locks = new Set();
|
|
452
|
+
for (const rule of config.testkit.requirements?.suites || []) {
|
|
453
|
+
if (matchesSuiteSelectors(selectionType, suiteName, [rule.selector])) {
|
|
454
|
+
for (const lockName of rule.locks || []) {
|
|
455
|
+
locks.add(lockName);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return [...locks].sort();
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function normalizeMultiValue(value) {
|
|
463
|
+
if (Array.isArray(value)) {
|
|
464
|
+
return value.filter((entry) => entry != null).map((entry) => String(entry));
|
|
465
|
+
}
|
|
466
|
+
if (value == null || value === "") return [];
|
|
467
|
+
return [String(value)];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function normalizePath(filePath) {
|
|
471
|
+
return String(filePath).split(path.sep).join("/").replace(/^\.\/+/, "");
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function fileDisplayName(filePath) {
|
|
475
|
+
const base = path.posix
|
|
476
|
+
.basename(filePath)
|
|
477
|
+
.replace(/(\.int|\.e2e|\.scenario|\.dal|\.load|\.pw)\.testkit\.ts$/, "");
|
|
478
|
+
return formatDisplayName(base);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function buildSuiteId(serviceName, selectionType, suiteName, framework) {
|
|
482
|
+
return [serviceName, selectionType, framework, suiteName].join("|");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function buildFileId(serviceName, selectionType, filePath) {
|
|
486
|
+
return [serviceName, selectionType, normalizePath(filePath)].join("|");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function compareSuiteEntries(left, right) {
|
|
490
|
+
return (
|
|
491
|
+
left.service.localeCompare(right.service) ||
|
|
492
|
+
left.selectionType.localeCompare(right.selectionType) ||
|
|
493
|
+
left.name.localeCompare(right.name) ||
|
|
494
|
+
left.framework.localeCompare(right.framework)
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function compareFileEntries(left, right) {
|
|
499
|
+
return (
|
|
500
|
+
left.service.localeCompare(right.service) ||
|
|
501
|
+
left.selectionType.localeCompare(right.selectionType) ||
|
|
502
|
+
left.suiteName.localeCompare(right.suiteName) ||
|
|
503
|
+
left.path.localeCompare(right.path)
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function compareDiagnostics(left, right) {
|
|
508
|
+
return (
|
|
509
|
+
severityRank(left.severity) - severityRank(right.severity) ||
|
|
510
|
+
String(left.code || "").localeCompare(String(right.code || "")) ||
|
|
511
|
+
String(left.path || "").localeCompare(String(right.path || ""))
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function severityRank(value) {
|
|
516
|
+
if (value === "error") return 1;
|
|
517
|
+
if (value === "warning") return 2;
|
|
518
|
+
return 3;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function emptySummary() {
|
|
522
|
+
return {
|
|
523
|
+
services: 0,
|
|
524
|
+
suites: 0,
|
|
525
|
+
files: 0,
|
|
526
|
+
activeFiles: 0,
|
|
527
|
+
skippedFiles: 0,
|
|
528
|
+
diagnostics: {
|
|
529
|
+
errors: 0,
|
|
530
|
+
warnings: 0,
|
|
531
|
+
},
|
|
532
|
+
byService: {},
|
|
533
|
+
byType: {},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function formatErrorMessage(error) {
|
|
538
|
+
if (error instanceof Error) return error.message;
|
|
539
|
+
return String(error);
|
|
540
|
+
}
|