@midscene/test 1.15.3 → 1.15.4-beta-20261013090705.0
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/dist/es/cli/index.mjs +1519 -350
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +20 -1
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +2 -2
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +1264 -69
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +1530 -343
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +20 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +3 -0
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +1264 -69
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +556 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/midscene/index.d.ts +6 -2
- package/package.json +13 -13
package/dist/es/test-cli.mjs
CHANGED
|
@@ -20,9 +20,7 @@ var __webpack_modules__ = {
|
|
|
20
20
|
module.exports = __rspack_external_node_path_c5b9b54f;
|
|
21
21
|
},
|
|
22
22
|
"./package.json" (module) {
|
|
23
|
-
module.exports = {
|
|
24
|
-
rE: "1.15.3"
|
|
25
|
-
};
|
|
23
|
+
module.exports = JSON.parse('{"rE":"1.15.4-beta-20261013090705.0"}');
|
|
26
24
|
}
|
|
27
25
|
};
|
|
28
26
|
var __webpack_module_cache__ = {};
|
|
@@ -269,7 +267,7 @@ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = fal
|
|
|
269
267
|
return pattern;
|
|
270
268
|
});
|
|
271
269
|
};
|
|
272
|
-
const
|
|
270
|
+
const validateTestFileSelection = (value, label = 'files')=>{
|
|
273
271
|
if (void 0 === value) return;
|
|
274
272
|
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
275
273
|
rejectUnknownKeys(value, [
|
|
@@ -393,7 +391,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
393
391
|
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
394
392
|
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
395
393
|
names.add(candidate.name);
|
|
396
|
-
const files =
|
|
394
|
+
const files = validateTestFileSelection(candidate.files, `${label}.files`);
|
|
397
395
|
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
398
396
|
const localNodes = new registry_NodeRegistry(candidate.nodes);
|
|
399
397
|
const nodes = new registry_NodeRegistry([
|
|
@@ -423,12 +421,27 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
423
421
|
const validateTestOptions = (value)=>{
|
|
424
422
|
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
425
423
|
const candidate = value ?? {};
|
|
424
|
+
const executionUnit = candidate.executionUnit ?? 'project';
|
|
425
|
+
if ('project' !== executionUnit && 'case' !== executionUnit) throw new TypeError('Midscene config test.executionUnit must be "project" or "case".');
|
|
426
426
|
return Object.freeze({
|
|
427
427
|
maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
|
|
428
428
|
bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
|
|
429
|
-
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
|
|
429
|
+
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout'),
|
|
430
|
+
executionUnit,
|
|
431
|
+
executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
|
|
430
432
|
});
|
|
431
433
|
};
|
|
434
|
+
const validateTestExecutor = (value)=>{
|
|
435
|
+
if (void 0 === value) return;
|
|
436
|
+
if (!isRecord(value)) throw new TypeError('Midscene config executor must be an object.');
|
|
437
|
+
rejectUnknownKeys(value, [
|
|
438
|
+
'name',
|
|
439
|
+
'execute'
|
|
440
|
+
], 'executor');
|
|
441
|
+
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError('Midscene config executor.name must be non-empty.');
|
|
442
|
+
if ('function' != typeof value.execute) throw new TypeError('Midscene config executor.execute must be a function.');
|
|
443
|
+
return Object.freeze(value);
|
|
444
|
+
};
|
|
432
445
|
const validateOutputPath = (value, fallback, label)=>{
|
|
433
446
|
if (void 0 === value) return fallback;
|
|
434
447
|
if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
|
|
@@ -457,15 +470,22 @@ const validateTestProjectDefinition = (definition)=>{
|
|
|
457
470
|
'projects',
|
|
458
471
|
'test',
|
|
459
472
|
'output',
|
|
460
|
-
'nodes'
|
|
473
|
+
'nodes',
|
|
474
|
+
'executor'
|
|
461
475
|
], 'root');
|
|
462
476
|
const nodes = new registry_NodeRegistry(definition.nodes);
|
|
463
477
|
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
|
|
478
|
+
const test = validateTestOptions(definition.test);
|
|
479
|
+
const executor = validateTestExecutor(definition.executor);
|
|
480
|
+
if (executor && 'case' !== test.executionUnit) throw new TypeError('Midscene config executor requires test.executionUnit to be "case".');
|
|
464
481
|
return {
|
|
465
482
|
...resolvedProjects,
|
|
466
|
-
test
|
|
483
|
+
test,
|
|
467
484
|
output: validateOutput(definition.output),
|
|
468
485
|
nodes,
|
|
486
|
+
...executor ? {
|
|
487
|
+
executor
|
|
488
|
+
} : {},
|
|
469
489
|
resolveNode: (name)=>nodes.get(name)
|
|
470
490
|
};
|
|
471
491
|
};
|
|
@@ -510,6 +530,7 @@ async function loadTestProject(configPath) {
|
|
|
510
530
|
}
|
|
511
531
|
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
512
532
|
}
|
|
533
|
+
const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
513
534
|
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
|
|
514
535
|
projectId,
|
|
515
536
|
sourcePath,
|
|
@@ -729,10 +750,15 @@ function collectWorkflowDocument(source, options) {
|
|
|
729
750
|
caseIndex
|
|
730
751
|
});
|
|
731
752
|
collect_rejectUnknownKeys(definition, [
|
|
753
|
+
'id',
|
|
732
754
|
'name',
|
|
733
755
|
'tags',
|
|
756
|
+
'resources',
|
|
734
757
|
'steps'
|
|
735
758
|
], `Case ${caseIndex + 1}`);
|
|
759
|
+
if (void 0 !== definition.id && !isSafeResultPathSegment(definition.id)) throw new WorkflowParseError(`Case ${caseIndex + 1} id must start with an ASCII letter or number, contain only letters, numbers, dots, underscores, or hyphens, and be at most 128 characters.`, {
|
|
760
|
+
caseIndex
|
|
761
|
+
});
|
|
736
762
|
if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
|
|
737
763
|
caseIndex
|
|
738
764
|
});
|
|
@@ -743,6 +769,13 @@ function collectWorkflowDocument(source, options) {
|
|
|
743
769
|
if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
|
|
744
770
|
caseIndex
|
|
745
771
|
});
|
|
772
|
+
const resources = definition.resources ?? [];
|
|
773
|
+
if (!Array.isArray(resources) || resources.some((resource)=>'string' != typeof resource || 0 === resource.trim().length)) throw new WorkflowParseError(`Case ${caseIndex + 1} resources must be an array of non-empty strings.`, {
|
|
774
|
+
caseIndex
|
|
775
|
+
});
|
|
776
|
+
if (new Set(resources).size !== resources.length) throw new WorkflowParseError(`Case ${caseIndex + 1} resources must not contain duplicates.`, {
|
|
777
|
+
caseIndex
|
|
778
|
+
});
|
|
746
779
|
const steps = normalize_normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
747
780
|
if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
748
781
|
caseIndex,
|
|
@@ -751,7 +784,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
751
784
|
});
|
|
752
785
|
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
753
786
|
});
|
|
754
|
-
const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
|
|
787
|
+
const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
|
|
755
788
|
if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
756
789
|
caseId
|
|
757
790
|
});
|
|
@@ -762,8 +795,14 @@ function collectWorkflowDocument(source, options) {
|
|
|
762
795
|
sourcePath,
|
|
763
796
|
caseIndex,
|
|
764
797
|
definition: {
|
|
798
|
+
...'string' == typeof definition.id ? {
|
|
799
|
+
id: definition.id
|
|
800
|
+
} : {},
|
|
765
801
|
name: definition.name,
|
|
766
802
|
tags: tags,
|
|
803
|
+
...resources.length > 0 ? {
|
|
804
|
+
resources: resources
|
|
805
|
+
} : {},
|
|
767
806
|
steps
|
|
768
807
|
}
|
|
769
808
|
};
|
|
@@ -903,6 +942,38 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
903
942
|
...outcome.notRunReason ? {
|
|
904
943
|
notRunReason: outcome.notRunReason
|
|
905
944
|
} : {},
|
|
945
|
+
...outcome.execution ? {
|
|
946
|
+
execution: {
|
|
947
|
+
executor: outcome.execution.executor,
|
|
948
|
+
resources: outcome.execution.resources,
|
|
949
|
+
...void 0 === outcome.execution.attempts ? {} : {
|
|
950
|
+
attempts: outcome.execution.attempts
|
|
951
|
+
},
|
|
952
|
+
...outcome.execution.failure ? {
|
|
953
|
+
failure: outcome.execution.failure
|
|
954
|
+
} : {},
|
|
955
|
+
...outcome.execution.artifacts ? {
|
|
956
|
+
artifacts: outcome.execution.artifacts
|
|
957
|
+
} : {},
|
|
958
|
+
...outcome.execution.metadata ? {
|
|
959
|
+
metadata: outcome.execution.metadata
|
|
960
|
+
} : {},
|
|
961
|
+
...outcome.execution.lifecycle ? {
|
|
962
|
+
lifecycle: {
|
|
963
|
+
status: outcome.execution.lifecycle.status,
|
|
964
|
+
startedAt: outcome.execution.lifecycle.startedAt,
|
|
965
|
+
endedAt: outcome.execution.lifecycle.endedAt,
|
|
966
|
+
durationMs: outcome.execution.lifecycle.durationMs,
|
|
967
|
+
...outcome.execution.lifecycle.setupError ? {
|
|
968
|
+
setupError: errorJson(outcome.execution.lifecycle.setupError)
|
|
969
|
+
} : {},
|
|
970
|
+
...outcome.execution.lifecycle.teardownErrors ? {
|
|
971
|
+
teardownErrors: outcome.execution.lifecycle.teardownErrors.map(errorJson)
|
|
972
|
+
} : {}
|
|
973
|
+
}
|
|
974
|
+
} : {}
|
|
975
|
+
}
|
|
976
|
+
} : {},
|
|
906
977
|
...outcome.attempts ? {
|
|
907
978
|
attempts: outcome.attempts.map((attempt)=>({
|
|
908
979
|
attemptId: attempt.runId,
|
|
@@ -980,6 +1051,27 @@ const DEFAULT_TEST_FILE_SELECTION = {
|
|
|
980
1051
|
]
|
|
981
1052
|
};
|
|
982
1053
|
const project_preparation_toPosix = (value)=>value.split(external_node_path_.sep).join('/');
|
|
1054
|
+
const discoverSelectedTestFiles = (projectRoot, paths)=>{
|
|
1055
|
+
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
1056
|
+
const files = new Set();
|
|
1057
|
+
for (const requestedPath of paths){
|
|
1058
|
+
if ('string' != typeof requestedPath || 0 === requestedPath.trim().length) throw new TypeError('Test selection paths must be non-empty strings.');
|
|
1059
|
+
const absolutePath = (0, external_node_path_.resolve)(root, requestedPath);
|
|
1060
|
+
const relativePath = (0, external_node_path_.relative)(root, absolutePath);
|
|
1061
|
+
if ('..' === relativePath || relativePath.startsWith(`..${external_node_path_.sep}`) || (0, external_node_path_.isAbsolute)(relativePath)) throw new Error(`Test selection path is outside the project: ${requestedPath}`);
|
|
1062
|
+
if (!(0, external_node_fs_.existsSync)(absolutePath)) throw new Error(`Test selection path does not exist: ${requestedPath}`);
|
|
1063
|
+
if ((0, external_node_fs_.statSync)(absolutePath).isDirectory()) for (const file of discoverTestFiles(absolutePath))files.add(file);
|
|
1064
|
+
else if (/\.ya?ml$/i.test(absolutePath)) files.add(absolutePath);
|
|
1065
|
+
else throw new Error(`Test selection file must be YAML: ${requestedPath}`);
|
|
1066
|
+
}
|
|
1067
|
+
return [
|
|
1068
|
+
...files
|
|
1069
|
+
].sort((a, b)=>{
|
|
1070
|
+
const relativeA = project_preparation_toPosix((0, external_node_path_.relative)(root, a));
|
|
1071
|
+
const relativeB = project_preparation_toPosix((0, external_node_path_.relative)(root, b));
|
|
1072
|
+
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
983
1075
|
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
984
1076
|
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
985
1077
|
const match = (patterns)=>globSync(patterns, {
|
|
@@ -1004,6 +1096,11 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
|
1004
1096
|
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
1005
1097
|
});
|
|
1006
1098
|
};
|
|
1099
|
+
const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)=>{
|
|
1100
|
+
const normalized = validateTestFileSelection(selection);
|
|
1101
|
+
if (!normalized) throw new TypeError('Test file selection is required.');
|
|
1102
|
+
return discoverResolvedTestFiles(projectRoot, normalized);
|
|
1103
|
+
};
|
|
1007
1104
|
const discoverTestConfig = (projectRoot)=>{
|
|
1008
1105
|
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
1009
1106
|
const candidates = (0, external_node_fs_.readdirSync)(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
|
|
@@ -1034,8 +1131,14 @@ const matchesTags = (tags, selection)=>{
|
|
|
1034
1131
|
if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
|
|
1035
1132
|
return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
|
|
1036
1133
|
};
|
|
1037
|
-
const filterDocumentCases = (document, project)=>{
|
|
1038
|
-
const cases = document.cases.filter((item)=>
|
|
1134
|
+
const filterDocumentCases = (document, project, selection)=>{
|
|
1135
|
+
const cases = document.cases.filter((item)=>{
|
|
1136
|
+
const tags = item.definition.tags ?? [];
|
|
1137
|
+
return matchesTags(tags, project.tags) && matchesTags(tags, {
|
|
1138
|
+
include: selection?.tags?.include ?? [],
|
|
1139
|
+
exclude: selection?.tags?.exclude ?? []
|
|
1140
|
+
}) && (!selection?.caseIds?.length || selection.caseIds.includes(item.caseId));
|
|
1141
|
+
});
|
|
1039
1142
|
const filtered = document.cases.length - cases.length;
|
|
1040
1143
|
return 0 === cases.length ? {
|
|
1041
1144
|
filtered
|
|
@@ -1060,7 +1163,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1060
1163
|
const platform = options.platform?.trim();
|
|
1061
1164
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
1062
1165
|
const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_.resolve)(options.prerequisiteFile) : void 0;
|
|
1063
|
-
const
|
|
1166
|
+
const projectFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
1167
|
+
const selectedFiles = options.selection?.paths?.length ? new Set(discoverSelectedTestFiles(projectRoot, options.selection.paths)) : void 0;
|
|
1168
|
+
const mainFiles = selectedFiles ? projectFiles.filter((file)=>selectedFiles.has(file)) : projectFiles;
|
|
1064
1169
|
const files = [
|
|
1065
1170
|
...prerequisiteFile ? [
|
|
1066
1171
|
prerequisiteFile
|
|
@@ -1083,6 +1188,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1083
1188
|
const invocations = [];
|
|
1084
1189
|
const plans = [];
|
|
1085
1190
|
let filteredCaseCount = 0;
|
|
1191
|
+
const availableCaseIds = new Set();
|
|
1086
1192
|
if (0 === sources.length) {
|
|
1087
1193
|
const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
1088
1194
|
projectName: project.name,
|
|
@@ -1113,10 +1219,16 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1113
1219
|
variables: project.variables,
|
|
1114
1220
|
env: process.env
|
|
1115
1221
|
});
|
|
1222
|
+
const duplicate = document.cases.find((item)=>availableCaseIds.has(item.caseId));
|
|
1223
|
+
if (duplicate) throw new WorkflowParseError(`Case id collision in project "${project.name}": ${duplicate.caseId}.`, {
|
|
1224
|
+
caseId: duplicate.caseId,
|
|
1225
|
+
projectName: project.name
|
|
1226
|
+
});
|
|
1227
|
+
for (const item of document.cases)availableCaseIds.add(item.caseId);
|
|
1116
1228
|
const filtered = source.absolutePath === prerequisiteFile ? {
|
|
1117
1229
|
document,
|
|
1118
1230
|
filtered: 0
|
|
1119
|
-
} : filterDocumentCases(document, project);
|
|
1231
|
+
} : filterDocumentCases(document, project, options.selection);
|
|
1120
1232
|
const plan = buildTestRunReportPlan(document, {
|
|
1121
1233
|
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
1122
1234
|
defaultTimeoutMs,
|
|
@@ -1164,9 +1276,42 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1164
1276
|
plans,
|
|
1165
1277
|
collectionErrors,
|
|
1166
1278
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
1167
|
-
filteredCaseCount
|
|
1279
|
+
filteredCaseCount,
|
|
1280
|
+
availableCaseIds: [
|
|
1281
|
+
...availableCaseIds
|
|
1282
|
+
]
|
|
1168
1283
|
};
|
|
1169
1284
|
}
|
|
1285
|
+
const validateUniqueStrings = (values, label)=>{
|
|
1286
|
+
if (void 0 === values) return;
|
|
1287
|
+
if (!Array.isArray(values) || values.some((value)=>'string' != typeof value || 0 === value.trim().length)) throw new TypeError(`${label} must be an array of non-empty strings.`);
|
|
1288
|
+
if (new Set(values).size !== values.length) throw new TypeError(`${label} must not contain duplicates.`);
|
|
1289
|
+
return values;
|
|
1290
|
+
};
|
|
1291
|
+
const validateRunSelection = (options)=>{
|
|
1292
|
+
const paths = validateUniqueStrings(options.paths, 'Test selection paths');
|
|
1293
|
+
const caseIds = validateUniqueStrings(options.caseIds, 'Test selection caseIds');
|
|
1294
|
+
const include = validateUniqueStrings(options.tags?.include, 'Test selection tags.include');
|
|
1295
|
+
const exclude = validateUniqueStrings(options.tags?.exclude, 'Test selection tags.exclude');
|
|
1296
|
+
return {
|
|
1297
|
+
...paths ? {
|
|
1298
|
+
paths
|
|
1299
|
+
} : {},
|
|
1300
|
+
...caseIds ? {
|
|
1301
|
+
caseIds
|
|
1302
|
+
} : {},
|
|
1303
|
+
...include || exclude ? {
|
|
1304
|
+
tags: {
|
|
1305
|
+
...include ? {
|
|
1306
|
+
include
|
|
1307
|
+
} : {},
|
|
1308
|
+
...exclude ? {
|
|
1309
|
+
exclude
|
|
1310
|
+
} : {}
|
|
1311
|
+
}
|
|
1312
|
+
} : {}
|
|
1313
|
+
};
|
|
1314
|
+
};
|
|
1170
1315
|
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
1171
1316
|
const createTestRunId = (date = new Date(), uuid = randomUUID())=>{
|
|
1172
1317
|
const timestamp = [
|
|
@@ -1204,6 +1349,7 @@ async function prepareTestRun(options) {
|
|
|
1204
1349
|
const startedAt = new Date();
|
|
1205
1350
|
const runId = createTestRunId(startedAt);
|
|
1206
1351
|
const input = resolveRunInput(options);
|
|
1352
|
+
const selection = validateRunSelection(options);
|
|
1207
1353
|
const configPath = options.configPath ? (0, external_node_path_.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
1208
1354
|
if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test --config only accepts a native TypeScript or JavaScript project config: ${configPath}. Run legacy batch YAML with \`midscene --config ${options.configPath}\`.`);
|
|
1209
1355
|
if (options.configPath && !(0, external_node_fs_.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
@@ -1217,11 +1363,20 @@ async function prepareTestRun(options) {
|
|
|
1217
1363
|
});
|
|
1218
1364
|
(0, external_node_fs_.mkdirSync)(runDir);
|
|
1219
1365
|
const projects = [];
|
|
1220
|
-
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout,
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1366
|
+
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
|
|
1367
|
+
selection,
|
|
1368
|
+
...input.singleFile ? {
|
|
1369
|
+
files: [
|
|
1370
|
+
input.singleFile
|
|
1371
|
+
]
|
|
1372
|
+
} : {}
|
|
1373
|
+
}));
|
|
1374
|
+
if (selection.caseIds?.length) {
|
|
1375
|
+
const availableCaseIds = new Set(projects.flatMap((project)=>project.availableCaseIds ?? []));
|
|
1376
|
+
const unknown = selection.caseIds.find((id)=>!availableCaseIds.has(id));
|
|
1377
|
+
if (unknown) throw new Error(`Unknown Midscene case id: ${unknown}`);
|
|
1378
|
+
}
|
|
1379
|
+
if ((selection.paths?.length || selection.caseIds?.length || selection.tags?.include?.length || selection.tags?.exclude?.length) && projects.every((project)=>0 === project.selectedCaseCount && 0 === project.collectionErrors.length)) throw new Error('No Midscene cases matched the requested selection.');
|
|
1225
1380
|
const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
|
|
1226
1381
|
return {
|
|
1227
1382
|
startedAt,
|
|
@@ -1317,20 +1472,1062 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1317
1472
|
}));
|
|
1318
1473
|
return output;
|
|
1319
1474
|
}
|
|
1320
|
-
const
|
|
1475
|
+
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1476
|
+
documentId,
|
|
1477
|
+
caseId: collectedCase.caseId,
|
|
1478
|
+
projectName,
|
|
1479
|
+
name: collectedCase.definition.name,
|
|
1480
|
+
sourcePath: collectedCase.sourcePath,
|
|
1481
|
+
caseIndex: collectedCase.caseIndex,
|
|
1482
|
+
status: 'not-run',
|
|
1483
|
+
notRunReason: reason
|
|
1484
|
+
});
|
|
1321
1485
|
const formatStep = (info)=>{
|
|
1322
|
-
const position =
|
|
1486
|
+
const position = 'case' === info.scope ? info.case : info.document;
|
|
1323
1487
|
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
1324
1488
|
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
1325
1489
|
};
|
|
1326
|
-
const formatStepResult = (info, result)=>{
|
|
1327
|
-
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1490
|
+
const formatStepResult = (info, result, indent = '')=>{
|
|
1328
1491
|
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1329
1492
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1330
1493
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1331
1494
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
1332
1495
|
return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
1333
1496
|
};
|
|
1497
|
+
const latestById = (items, id)=>[
|
|
1498
|
+
...new Map(items.map((item)=>[
|
|
1499
|
+
id(item),
|
|
1500
|
+
item
|
|
1501
|
+
])).values()
|
|
1502
|
+
];
|
|
1503
|
+
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
1504
|
+
const { project } = prepared;
|
|
1505
|
+
const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status || item.execution?.lifecycle?.status === 'failed') || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
1506
|
+
return {
|
|
1507
|
+
projectId: project.projectId,
|
|
1508
|
+
name: project.name,
|
|
1509
|
+
...prepared.platform ? {
|
|
1510
|
+
platform: prepared.platform
|
|
1511
|
+
} : {},
|
|
1512
|
+
status: projectFailed ? 'failed' : 'success',
|
|
1513
|
+
retry: project.retry,
|
|
1514
|
+
fileSelection: prepared.fileSelection,
|
|
1515
|
+
tagSelection: project.tags,
|
|
1516
|
+
sourceCount: prepared.sources.length,
|
|
1517
|
+
selectedCaseCount: prepared.selectedCaseCount,
|
|
1518
|
+
filteredCaseCount: prepared.filteredCaseCount,
|
|
1519
|
+
...lifecycle ? {
|
|
1520
|
+
lifecycle
|
|
1521
|
+
} : {},
|
|
1522
|
+
cases,
|
|
1523
|
+
plans: prepared.plans,
|
|
1524
|
+
documents,
|
|
1525
|
+
collectionErrors: prepared.collectionErrors
|
|
1526
|
+
};
|
|
1527
|
+
};
|
|
1528
|
+
const json = z.lazy(()=>z.union([
|
|
1529
|
+
z["null"](),
|
|
1530
|
+
z.string(),
|
|
1531
|
+
z.number().finite(),
|
|
1532
|
+
z.boolean(),
|
|
1533
|
+
z.array(json),
|
|
1534
|
+
z.record(z.string(), json)
|
|
1535
|
+
]));
|
|
1536
|
+
const time = z.string().refine((value)=>Number.isFinite(Date.parse(value)));
|
|
1537
|
+
const executor_runtime_schema_count = z.number().int().nonnegative();
|
|
1538
|
+
const executor_runtime_schema_value = z.strictObject({
|
|
1539
|
+
value: json,
|
|
1540
|
+
truncatedPaths: z.array(z.string()).optional(),
|
|
1541
|
+
redactedPaths: z.array(z.string()).optional()
|
|
1542
|
+
});
|
|
1543
|
+
const reportErrorSchema = z.lazy(()=>z.strictObject({
|
|
1544
|
+
format: z.literal('midscene-error').optional(),
|
|
1545
|
+
name: z.string(),
|
|
1546
|
+
message: z.string(),
|
|
1547
|
+
code: z.string().optional(),
|
|
1548
|
+
details: executor_runtime_schema_value.optional(),
|
|
1549
|
+
cause: reportErrorSchema.optional(),
|
|
1550
|
+
errors: z.array(reportErrorSchema).optional(),
|
|
1551
|
+
truncated: z.boolean().optional()
|
|
1552
|
+
}));
|
|
1553
|
+
const executor_runtime_schema_operation = z.strictObject({
|
|
1554
|
+
operationId: z.string().min(1),
|
|
1555
|
+
scopeId: z.string().optional(),
|
|
1556
|
+
stepId: z.string().optional(),
|
|
1557
|
+
node: z.string().optional()
|
|
1558
|
+
});
|
|
1559
|
+
const cleanupSchema = z.strictObject({
|
|
1560
|
+
status: z["enum"]([
|
|
1561
|
+
'not-required',
|
|
1562
|
+
'completed',
|
|
1563
|
+
'pending',
|
|
1564
|
+
'failed'
|
|
1565
|
+
]),
|
|
1566
|
+
capturedAt: time,
|
|
1567
|
+
operationId: z.string().optional(),
|
|
1568
|
+
blockingOperations: z.array(executor_runtime_schema_operation).optional(),
|
|
1569
|
+
error: reportErrorSchema.optional()
|
|
1570
|
+
});
|
|
1571
|
+
const pendingOperationsSchema = z.array(executor_runtime_schema_operation);
|
|
1572
|
+
const collectionStatus = z["enum"]([
|
|
1573
|
+
'complete',
|
|
1574
|
+
'partial',
|
|
1575
|
+
'unknown'
|
|
1576
|
+
]);
|
|
1577
|
+
const metricsSchema = z.strictObject({
|
|
1578
|
+
modelCallCount: executor_runtime_schema_count,
|
|
1579
|
+
modelTimeMs: z.number().finite().nonnegative(),
|
|
1580
|
+
promptTokens: executor_runtime_schema_count,
|
|
1581
|
+
cachedInputTokens: executor_runtime_schema_count,
|
|
1582
|
+
completionTokens: executor_runtime_schema_count,
|
|
1583
|
+
totalTokens: executor_runtime_schema_count,
|
|
1584
|
+
collection: z.strictObject({
|
|
1585
|
+
status: collectionStatus,
|
|
1586
|
+
expectedExecutionCount: executor_runtime_schema_count,
|
|
1587
|
+
capturedExecutionCount: executor_runtime_schema_count,
|
|
1588
|
+
reasons: z.array(z.string()),
|
|
1589
|
+
fields: z.strictObject({
|
|
1590
|
+
modelCallCount: collectionStatus,
|
|
1591
|
+
modelTimeMs: collectionStatus,
|
|
1592
|
+
totalTokens: collectionStatus
|
|
1593
|
+
}).optional()
|
|
1594
|
+
}).optional()
|
|
1595
|
+
});
|
|
1596
|
+
const operationsSchema = z.strictObject({
|
|
1597
|
+
capturedAt: time,
|
|
1598
|
+
events: z.array(z.strictObject({
|
|
1599
|
+
operationId: z.string().min(1),
|
|
1600
|
+
operationKind: z["enum"]([
|
|
1601
|
+
'node',
|
|
1602
|
+
'resource',
|
|
1603
|
+
'acquisition',
|
|
1604
|
+
'cleanup',
|
|
1605
|
+
'readiness'
|
|
1606
|
+
]),
|
|
1607
|
+
kind: z["enum"]([
|
|
1608
|
+
'started',
|
|
1609
|
+
'cancel-requested',
|
|
1610
|
+
'settled',
|
|
1611
|
+
'runner-returned',
|
|
1612
|
+
'cleanup-deferred'
|
|
1613
|
+
]),
|
|
1614
|
+
at: time,
|
|
1615
|
+
stepId: z.string().optional(),
|
|
1616
|
+
scopeId: z.string().optional(),
|
|
1617
|
+
node: z.string().optional(),
|
|
1618
|
+
status: z["enum"]([
|
|
1619
|
+
'success',
|
|
1620
|
+
'failed',
|
|
1621
|
+
'not-run'
|
|
1622
|
+
]).optional(),
|
|
1623
|
+
pending: z.boolean().optional(),
|
|
1624
|
+
error: reportErrorSchema.optional(),
|
|
1625
|
+
blockingOperations: z.array(executor_runtime_schema_operation).optional()
|
|
1626
|
+
}))
|
|
1627
|
+
});
|
|
1628
|
+
const runtimeResultFields = {
|
|
1629
|
+
executionStatus: z["enum"]([
|
|
1630
|
+
'success',
|
|
1631
|
+
'failed',
|
|
1632
|
+
'not-run'
|
|
1633
|
+
]).optional(),
|
|
1634
|
+
phases: z.partialRecord(z["enum"]([
|
|
1635
|
+
'beforeAll',
|
|
1636
|
+
'beforeEach',
|
|
1637
|
+
'steps',
|
|
1638
|
+
'afterEach',
|
|
1639
|
+
'afterAll'
|
|
1640
|
+
]), z.strictObject({
|
|
1641
|
+
status: z["enum"]([
|
|
1642
|
+
'success',
|
|
1643
|
+
'failed',
|
|
1644
|
+
'not-run',
|
|
1645
|
+
'not-required'
|
|
1646
|
+
]),
|
|
1647
|
+
plannedStepCount: executor_runtime_schema_count,
|
|
1648
|
+
executedStepCount: executor_runtime_schema_count
|
|
1649
|
+
})).optional(),
|
|
1650
|
+
cleanup: cleanupSchema.optional(),
|
|
1651
|
+
pendingOperations: pendingOperationsSchema.optional(),
|
|
1652
|
+
retryDecision: z.strictObject({
|
|
1653
|
+
scope: z["enum"]([
|
|
1654
|
+
'case',
|
|
1655
|
+
'document'
|
|
1656
|
+
]),
|
|
1657
|
+
retryCount: executor_runtime_schema_count,
|
|
1658
|
+
attemptIndex: executor_runtime_schema_count,
|
|
1659
|
+
decision: z["enum"]([
|
|
1660
|
+
'retry',
|
|
1661
|
+
'stop'
|
|
1662
|
+
]),
|
|
1663
|
+
reason: z["enum"]([
|
|
1664
|
+
'passed',
|
|
1665
|
+
'attempt-failed',
|
|
1666
|
+
'budget-exhausted',
|
|
1667
|
+
'cleanup-pending',
|
|
1668
|
+
'cleanup-failed',
|
|
1669
|
+
'operation-pending',
|
|
1670
|
+
'fatal-error',
|
|
1671
|
+
'interrupted',
|
|
1672
|
+
'bail',
|
|
1673
|
+
'infrastructure-error'
|
|
1674
|
+
])
|
|
1675
|
+
}).optional()
|
|
1676
|
+
};
|
|
1677
|
+
const plannedStep = z.strictObject({
|
|
1678
|
+
id: z.string(),
|
|
1679
|
+
phase: z["enum"]([
|
|
1680
|
+
'beforeAll',
|
|
1681
|
+
'beforeEach',
|
|
1682
|
+
'steps',
|
|
1683
|
+
'afterEach',
|
|
1684
|
+
'afterAll'
|
|
1685
|
+
]),
|
|
1686
|
+
stepIndex: executor_runtime_schema_count,
|
|
1687
|
+
node: z.string(),
|
|
1688
|
+
title: z.string().optional(),
|
|
1689
|
+
input: executor_runtime_schema_value,
|
|
1690
|
+
inputMode: z["enum"]([
|
|
1691
|
+
'template',
|
|
1692
|
+
'resolved'
|
|
1693
|
+
]),
|
|
1694
|
+
timeoutMs: z.number().positive().optional(),
|
|
1695
|
+
continueOnError: z.boolean()
|
|
1696
|
+
});
|
|
1697
|
+
const documentPlanSchema = z.strictObject({
|
|
1698
|
+
documentId: z.string(),
|
|
1699
|
+
sourcePath: z.string(),
|
|
1700
|
+
sourceYaml: executor_runtime_schema_value.optional(),
|
|
1701
|
+
retry: z.strictObject({
|
|
1702
|
+
scope: z["enum"]([
|
|
1703
|
+
'case',
|
|
1704
|
+
'document'
|
|
1705
|
+
]),
|
|
1706
|
+
count: executor_runtime_schema_count
|
|
1707
|
+
}),
|
|
1708
|
+
beforeAll: z.array(plannedStep),
|
|
1709
|
+
afterAll: z.array(plannedStep),
|
|
1710
|
+
cases: z.array(z.strictObject({
|
|
1711
|
+
caseId: z.string(),
|
|
1712
|
+
name: z.string(),
|
|
1713
|
+
caseIndex: executor_runtime_schema_count,
|
|
1714
|
+
selected: z.boolean(),
|
|
1715
|
+
tags: z.array(z.string()),
|
|
1716
|
+
onFailure: z["enum"]([
|
|
1717
|
+
'continue',
|
|
1718
|
+
'stop-document'
|
|
1719
|
+
]).optional(),
|
|
1720
|
+
beforeEach: z.array(plannedStep),
|
|
1721
|
+
steps: z.array(plannedStep),
|
|
1722
|
+
afterEach: z.array(plannedStep)
|
|
1723
|
+
}))
|
|
1724
|
+
});
|
|
1725
|
+
function test_executor_define_property(obj, key, value) {
|
|
1726
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
1727
|
+
value: value,
|
|
1728
|
+
enumerable: true,
|
|
1729
|
+
configurable: true,
|
|
1730
|
+
writable: true
|
|
1731
|
+
});
|
|
1732
|
+
else obj[key] = value;
|
|
1733
|
+
return obj;
|
|
1734
|
+
}
|
|
1735
|
+
const executorErrorKey = Symbol.for('@midscene/test/executor-error');
|
|
1736
|
+
let _executorErrorKey = executorErrorKey;
|
|
1737
|
+
class TestExecutorError extends Error {
|
|
1738
|
+
constructor(message, options = {}){
|
|
1739
|
+
super(message, {
|
|
1740
|
+
cause: options.cause
|
|
1741
|
+
}), test_executor_define_property(this, _executorErrorKey, true), test_executor_define_property(this, "kind", void 0), test_executor_define_property(this, "retryable", void 0);
|
|
1742
|
+
this.name = 'TestExecutorError';
|
|
1743
|
+
this.kind = options.kind ?? 'unknown';
|
|
1744
|
+
this.retryable = options.retryable ?? false;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
function isTestExecutorError(value) {
|
|
1748
|
+
return value instanceof Error && true === Reflect.get(value, executorErrorKey) && [
|
|
1749
|
+
'provision',
|
|
1750
|
+
'transport',
|
|
1751
|
+
'cleanup',
|
|
1752
|
+
'report',
|
|
1753
|
+
'unknown'
|
|
1754
|
+
].includes(Reflect.get(value, 'kind')) && 'boolean' == typeof Reflect.get(value, 'retryable');
|
|
1755
|
+
}
|
|
1756
|
+
const localTestExecutor = Object.freeze({
|
|
1757
|
+
name: 'local',
|
|
1758
|
+
execute: (_task, context)=>context.runLocal()
|
|
1759
|
+
});
|
|
1760
|
+
const jsonValueSchema = z.lazy(()=>z.union([
|
|
1761
|
+
z["null"](),
|
|
1762
|
+
z.string(),
|
|
1763
|
+
z.number().finite(),
|
|
1764
|
+
z.boolean(),
|
|
1765
|
+
z.array(jsonValueSchema),
|
|
1766
|
+
z.record(z.string(), jsonValueSchema)
|
|
1767
|
+
]));
|
|
1768
|
+
const timestampSchema = z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
|
|
1769
|
+
message: 'Expected an ISO-compatible timestamp.'
|
|
1770
|
+
});
|
|
1771
|
+
const errorSchema = z.strictObject({
|
|
1772
|
+
format: z.literal('midscene-error').optional(),
|
|
1773
|
+
cause: reportErrorSchema.optional(),
|
|
1774
|
+
name: z.string().min(1),
|
|
1775
|
+
message: z.string(),
|
|
1776
|
+
code: z.string().min(1),
|
|
1777
|
+
details: jsonValueSchema.optional()
|
|
1778
|
+
});
|
|
1779
|
+
const stepBaseSchema = z.strictObject({
|
|
1780
|
+
phase: z["enum"]([
|
|
1781
|
+
'beforeAll',
|
|
1782
|
+
'beforeEach',
|
|
1783
|
+
'steps',
|
|
1784
|
+
'afterEach',
|
|
1785
|
+
'afterAll'
|
|
1786
|
+
]),
|
|
1787
|
+
stepIndex: z.number().int().nonnegative(),
|
|
1788
|
+
node: z.string().min(1),
|
|
1789
|
+
input: jsonValueSchema,
|
|
1790
|
+
meta: z.strictObject({
|
|
1791
|
+
timeoutMs: z.number().positive().finite().optional(),
|
|
1792
|
+
continueOnError: z.boolean()
|
|
1793
|
+
}),
|
|
1794
|
+
continuedAfterError: z.boolean(),
|
|
1795
|
+
waitForPreviousOperationMs: z.number().nonnegative().finite().optional(),
|
|
1796
|
+
operations: operationsSchema.optional(),
|
|
1797
|
+
output: z.strictObject({
|
|
1798
|
+
summary: z.string().optional(),
|
|
1799
|
+
data: jsonValueSchema.optional()
|
|
1800
|
+
}).optional(),
|
|
1801
|
+
error: errorSchema.optional(),
|
|
1802
|
+
report: z.strictObject({
|
|
1803
|
+
traces: z.array(z.strictObject({
|
|
1804
|
+
type: z.literal('midscene-execution'),
|
|
1805
|
+
executionId: z.string().min(1),
|
|
1806
|
+
metrics: metricsSchema.optional()
|
|
1807
|
+
})),
|
|
1808
|
+
collectionErrors: z.array(errorSchema).optional()
|
|
1809
|
+
}).optional()
|
|
1810
|
+
});
|
|
1811
|
+
const stepSchema = z.discriminatedUnion('status', [
|
|
1812
|
+
stepBaseSchema.extend({
|
|
1813
|
+
status: z["enum"]([
|
|
1814
|
+
'success',
|
|
1815
|
+
'failed'
|
|
1816
|
+
]),
|
|
1817
|
+
startedAt: timestampSchema,
|
|
1818
|
+
endedAt: timestampSchema,
|
|
1819
|
+
durationMs: z.number().nonnegative().finite()
|
|
1820
|
+
}),
|
|
1821
|
+
stepBaseSchema.extend({
|
|
1822
|
+
status: z.literal('not-run'),
|
|
1823
|
+
notRunReason: z.string().min(1),
|
|
1824
|
+
blockedBy: z.string().optional()
|
|
1825
|
+
})
|
|
1826
|
+
]);
|
|
1827
|
+
const validatedStepSchema = stepSchema.superRefine((step, context)=>{
|
|
1828
|
+
if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
|
|
1829
|
+
code: 'custom',
|
|
1830
|
+
message: 'Successful steps cannot contain failure evidence.'
|
|
1831
|
+
});
|
|
1832
|
+
if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
|
|
1833
|
+
code: 'custom',
|
|
1834
|
+
message: 'Continuation requires continueOnError.'
|
|
1835
|
+
});
|
|
1836
|
+
});
|
|
1837
|
+
const attemptSchema = z.strictObject({
|
|
1838
|
+
...runtimeResultFields,
|
|
1839
|
+
caseId: z.string().min(1),
|
|
1840
|
+
runId: z.string().min(1),
|
|
1841
|
+
projectName: z.string().min(1),
|
|
1842
|
+
attemptIndex: z.number().int().nonnegative(),
|
|
1843
|
+
name: z.string(),
|
|
1844
|
+
sourcePath: z.string().min(1),
|
|
1845
|
+
caseIndex: z.number().int().nonnegative(),
|
|
1846
|
+
status: z["enum"]([
|
|
1847
|
+
'success',
|
|
1848
|
+
'failed'
|
|
1849
|
+
]),
|
|
1850
|
+
beforeEach: z.array(validatedStepSchema),
|
|
1851
|
+
steps: z.array(validatedStepSchema),
|
|
1852
|
+
afterEach: z.array(validatedStepSchema),
|
|
1853
|
+
executionErrors: z.array(errorSchema).optional(),
|
|
1854
|
+
teardownErrors: z.array(errorSchema).optional(),
|
|
1855
|
+
reportRefs: z.array(z.string().min(1)).optional(),
|
|
1856
|
+
startedAt: timestampSchema,
|
|
1857
|
+
endedAt: timestampSchema,
|
|
1858
|
+
durationMs: z.number().nonnegative().finite()
|
|
1859
|
+
});
|
|
1860
|
+
const notRunReasonSchema = z["enum"]([
|
|
1861
|
+
'document-start-failed',
|
|
1862
|
+
'project-preflight-failed',
|
|
1863
|
+
'project-setup-failed',
|
|
1864
|
+
'interrupted',
|
|
1865
|
+
'bail',
|
|
1866
|
+
'fatal-error',
|
|
1867
|
+
'executor-failed',
|
|
1868
|
+
'cleanup-pending',
|
|
1869
|
+
'cleanup-failed',
|
|
1870
|
+
'operation-pending'
|
|
1871
|
+
]);
|
|
1872
|
+
const resultSchema = z.strictObject({
|
|
1873
|
+
case: z.strictObject({
|
|
1874
|
+
documentId: z.string().min(1),
|
|
1875
|
+
caseId: z.string().min(1),
|
|
1876
|
+
projectName: z.string().min(1),
|
|
1877
|
+
name: z.string(),
|
|
1878
|
+
sourcePath: z.string().min(1),
|
|
1879
|
+
caseIndex: z.number().int().nonnegative(),
|
|
1880
|
+
status: z["enum"]([
|
|
1881
|
+
'success',
|
|
1882
|
+
'failed',
|
|
1883
|
+
'not-run'
|
|
1884
|
+
]),
|
|
1885
|
+
run: attemptSchema.optional(),
|
|
1886
|
+
attempts: z.array(attemptSchema).optional(),
|
|
1887
|
+
notRunReason: notRunReasonSchema.optional()
|
|
1888
|
+
}),
|
|
1889
|
+
document: z.strictObject({
|
|
1890
|
+
...runtimeResultFields,
|
|
1891
|
+
plan: documentPlanSchema.optional(),
|
|
1892
|
+
attemptIndex: z.number().int().nonnegative().optional(),
|
|
1893
|
+
hostErrors: z.array(z.strictObject({
|
|
1894
|
+
phase: z["enum"]([
|
|
1895
|
+
'setup',
|
|
1896
|
+
'execution',
|
|
1897
|
+
'cleanup',
|
|
1898
|
+
'report',
|
|
1899
|
+
'publication',
|
|
1900
|
+
'observer'
|
|
1901
|
+
]),
|
|
1902
|
+
error: reportErrorSchema
|
|
1903
|
+
})).optional(),
|
|
1904
|
+
documentId: z.string().min(1),
|
|
1905
|
+
documentRunId: z.string().min(1),
|
|
1906
|
+
projectId: z.string().min(1),
|
|
1907
|
+
projectName: z.string().min(1),
|
|
1908
|
+
sourcePath: z.string().min(1),
|
|
1909
|
+
status: z["enum"]([
|
|
1910
|
+
'success',
|
|
1911
|
+
'failed'
|
|
1912
|
+
]),
|
|
1913
|
+
startedAt: timestampSchema,
|
|
1914
|
+
endedAt: timestampSchema,
|
|
1915
|
+
durationMs: z.number().nonnegative().finite(),
|
|
1916
|
+
beforeAll: z.array(validatedStepSchema),
|
|
1917
|
+
afterAll: z.array(validatedStepSchema),
|
|
1918
|
+
executionErrors: z.array(errorSchema).optional(),
|
|
1919
|
+
teardownErrors: z.array(errorSchema).optional(),
|
|
1920
|
+
reportRefs: z.array(z.string().min(1)).optional()
|
|
1921
|
+
}).optional(),
|
|
1922
|
+
lifecycle: z.strictObject({
|
|
1923
|
+
cleanup: cleanupSchema.optional(),
|
|
1924
|
+
pendingOperations: pendingOperationsSchema.optional(),
|
|
1925
|
+
projectName: z.string().min(1),
|
|
1926
|
+
status: z["enum"]([
|
|
1927
|
+
'success',
|
|
1928
|
+
'failed'
|
|
1929
|
+
]),
|
|
1930
|
+
setupError: errorSchema.optional(),
|
|
1931
|
+
teardownErrors: z.array(errorSchema).optional(),
|
|
1932
|
+
startedAt: timestampSchema,
|
|
1933
|
+
endedAt: timestampSchema,
|
|
1934
|
+
durationMs: z.number().nonnegative().finite()
|
|
1935
|
+
}).optional(),
|
|
1936
|
+
artifacts: z.array(z.strictObject({
|
|
1937
|
+
name: z.string().min(1),
|
|
1938
|
+
uri: z.string().min(1),
|
|
1939
|
+
mediaType: z.string().min(1).optional()
|
|
1940
|
+
})).optional(),
|
|
1941
|
+
metadata: z.record(z.string(), z.union([
|
|
1942
|
+
z.string(),
|
|
1943
|
+
z.number().finite(),
|
|
1944
|
+
z.boolean(),
|
|
1945
|
+
z["null"]()
|
|
1946
|
+
])).optional()
|
|
1947
|
+
});
|
|
1948
|
+
const attemptMatchesTask = (task, attempt)=>attempt.caseId === task.caseId && attempt.projectName === task.projectName && attempt.name === task.caseName && attempt.sourcePath === task.sourcePath && attempt.caseIndex === task.caseIndex && isSafeResultPathSegment(attempt.runId);
|
|
1949
|
+
const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
|
|
1950
|
+
'completed',
|
|
1951
|
+
'not-required'
|
|
1952
|
+
].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
|
|
1953
|
+
function assertTestCaseTaskRunResult(task, value) {
|
|
1954
|
+
const parsed = resultSchema.safeParse(value);
|
|
1955
|
+
if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${z.prettifyError(parsed.error)}`);
|
|
1956
|
+
const outcome = parsed.data.case;
|
|
1957
|
+
if (outcome.caseId !== task.caseId || outcome.documentId !== task.documentId || outcome.projectName !== task.projectName || outcome.name !== task.caseName || outcome.sourcePath !== task.sourcePath || outcome.caseIndex !== task.caseIndex) throw new Error(`Executor returned a result that does not match task ${task.caseId}.`);
|
|
1958
|
+
const attempts = outcome.attempts ?? [];
|
|
1959
|
+
for (const attempt of attempts){
|
|
1960
|
+
const steps = [
|
|
1961
|
+
...attempt.beforeEach,
|
|
1962
|
+
...attempt.steps,
|
|
1963
|
+
...attempt.afterEach
|
|
1964
|
+
];
|
|
1965
|
+
if ('success' === attempt.status && (0 === attempt.steps.length || (attempt.executionErrors?.length ?? 0) > 0 || steps.some((step)=>'success' !== step.status) || hasUnfinishedWork(attempt) || (attempt.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful attempt for ${task.caseId}.`);
|
|
1966
|
+
}
|
|
1967
|
+
if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
|
|
1968
|
+
if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
|
|
1969
|
+
if (attempts.some((attempt, index)=>attempt.attemptIndex !== index) || attempts.length > task.retry + 1) throw new TypeError(`Executor returned a non-sequential attempt history for ${task.caseId}.`);
|
|
1970
|
+
const finalAttempt = attempts.at(-1);
|
|
1971
|
+
if ('not-run' === outcome.status) {
|
|
1972
|
+
if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
|
|
1973
|
+
} else if (void 0 !== outcome.notRunReason || !outcome.run || !finalAttempt || finalAttempt.status !== outcome.status || outcome.run.runId !== finalAttempt.runId || JSON.stringify(outcome.run) !== JSON.stringify(finalAttempt) || attempts.slice(0, -1).some((attempt)=>'failed' !== attempt.status)) throw new TypeError(`Executor returned an inconsistent ${outcome.status} result for ${task.caseId}.`);
|
|
1974
|
+
const reportRefs = [
|
|
1975
|
+
...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
|
|
1976
|
+
...parsed.data.document?.reportRefs ?? []
|
|
1977
|
+
];
|
|
1978
|
+
if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
|
|
1979
|
+
const document = parsed.data.document;
|
|
1980
|
+
if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
|
|
1981
|
+
...document.beforeAll,
|
|
1982
|
+
...document.afterAll
|
|
1983
|
+
].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
|
|
1984
|
+
const lifecycle = parsed.data.lifecycle;
|
|
1985
|
+
if (lifecycle?.status === 'success' && (hasUnfinishedWork(lifecycle) || lifecycle.setupError || (lifecycle.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful lifecycle for ${task.caseId}.`);
|
|
1986
|
+
if (document && (document.documentId !== task.documentId || document.projectId !== task.projectId || document.projectName !== task.projectName || document.sourcePath !== task.sourcePath || !isSafeResultPathSegment(document.documentRunId))) throw new Error(`Executor returned a document that does not match task ${task.caseId}.`);
|
|
1987
|
+
if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
|
|
1988
|
+
}
|
|
1989
|
+
function createExecutorReports(outputDir) {
|
|
1990
|
+
const paths = new Map();
|
|
1991
|
+
const sources = new Map();
|
|
1992
|
+
return {
|
|
1993
|
+
materializeSource (source) {
|
|
1994
|
+
if (!source.dumpPath) {
|
|
1995
|
+
const reference = this.materialize(source.sourcePath);
|
|
1996
|
+
sources.set(reference, {
|
|
1997
|
+
sourcePath: this.resolve(reference),
|
|
1998
|
+
collection: source.collection
|
|
1999
|
+
});
|
|
2000
|
+
return reference;
|
|
2001
|
+
}
|
|
2002
|
+
const dump = (0, external_node_fs_.realpathSync)(source.dumpPath);
|
|
2003
|
+
const reference = randomUUID();
|
|
2004
|
+
const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
|
|
2005
|
+
(0, external_node_fs_.mkdirSync)(destination, {
|
|
2006
|
+
recursive: true
|
|
2007
|
+
});
|
|
2008
|
+
const dumpPath = (0, external_node_path_.join)(destination, 'snapshot.json');
|
|
2009
|
+
(0, external_node_fs_.cpSync)(dump, dumpPath);
|
|
2010
|
+
const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(dump), 'screenshots');
|
|
2011
|
+
if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
|
|
2012
|
+
recursive: true,
|
|
2013
|
+
dereference: true
|
|
2014
|
+
});
|
|
2015
|
+
const sourcePath = (0, external_node_path_.join)(destination, 'index.html');
|
|
2016
|
+
sources.set(reference, {
|
|
2017
|
+
sourcePath,
|
|
2018
|
+
dumpPath,
|
|
2019
|
+
collection: source.collection
|
|
2020
|
+
});
|
|
2021
|
+
paths.set(reference, sourcePath);
|
|
2022
|
+
return reference;
|
|
2023
|
+
},
|
|
2024
|
+
resolveSource (reference) {
|
|
2025
|
+
return sources.get(reference);
|
|
2026
|
+
},
|
|
2027
|
+
materialize (sourcePath) {
|
|
2028
|
+
const source = (0, external_node_fs_.realpathSync)(sourcePath);
|
|
2029
|
+
const entry = (0, external_node_fs_.statSync)(source).isDirectory() ? (0, external_node_path_.join)(source, 'index.html') : source;
|
|
2030
|
+
if (!(0, external_node_fs_.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
|
|
2031
|
+
kind: 'report'
|
|
2032
|
+
});
|
|
2033
|
+
const reference = randomUUID();
|
|
2034
|
+
const destination = (0, external_node_path_.join)(outputDir, 'reports', reference);
|
|
2035
|
+
(0, external_node_fs_.mkdirSync)(destination, {
|
|
2036
|
+
recursive: true
|
|
2037
|
+
});
|
|
2038
|
+
const isDirectoryReport = 'index.html' === (0, external_node_path_.basename)(entry);
|
|
2039
|
+
const reportPath = (0, external_node_path_.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
|
|
2040
|
+
(0, external_node_fs_.cpSync)(isDirectoryReport ? (0, external_node_path_.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
|
|
2041
|
+
recursive: isDirectoryReport,
|
|
2042
|
+
dereference: true
|
|
2043
|
+
});
|
|
2044
|
+
if (!isDirectoryReport) {
|
|
2045
|
+
const screenshots = (0, external_node_path_.join)((0, external_node_path_.dirname)(entry), 'screenshots');
|
|
2046
|
+
if ((0, external_node_fs_.existsSync)(screenshots)) (0, external_node_fs_.cpSync)(screenshots, (0, external_node_path_.join)(destination, 'screenshots'), {
|
|
2047
|
+
recursive: true,
|
|
2048
|
+
dereference: true
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
paths.set(reference, reportPath);
|
|
2052
|
+
return reference;
|
|
2053
|
+
},
|
|
2054
|
+
resolve (reference) {
|
|
2055
|
+
const reportPath = paths.get(reference);
|
|
2056
|
+
if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
|
|
2057
|
+
kind: 'report'
|
|
2058
|
+
});
|
|
2059
|
+
return reportPath;
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
async function runTaskPool(options) {
|
|
2064
|
+
if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
|
|
2065
|
+
const pending = options.tasks.map((_, index)=>index);
|
|
2066
|
+
const results = new Array(options.tasks.length);
|
|
2067
|
+
const activeResources = new Set();
|
|
2068
|
+
let active = 0;
|
|
2069
|
+
let firstError;
|
|
2070
|
+
let hasError = false;
|
|
2071
|
+
return await new Promise((resolve, reject)=>{
|
|
2072
|
+
let settled = false;
|
|
2073
|
+
const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
|
|
2074
|
+
const finish = ()=>{
|
|
2075
|
+
if (settled) return;
|
|
2076
|
+
settled = true;
|
|
2077
|
+
options.signal?.removeEventListener('abort', pump);
|
|
2078
|
+
if (hasError) reject(firstError);
|
|
2079
|
+
else resolve(results);
|
|
2080
|
+
};
|
|
2081
|
+
const release = (resources)=>{
|
|
2082
|
+
for (const resource of resources)activeResources.delete(resource);
|
|
2083
|
+
};
|
|
2084
|
+
const claimRunnable = ()=>{
|
|
2085
|
+
const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
|
|
2086
|
+
if (pendingIndex < 0) return;
|
|
2087
|
+
const [taskIndex] = pending.splice(pendingIndex, 1);
|
|
2088
|
+
return taskIndex;
|
|
2089
|
+
};
|
|
2090
|
+
function pump() {
|
|
2091
|
+
if (settled) return;
|
|
2092
|
+
while(active < options.maxConcurrency && !stopRequested()){
|
|
2093
|
+
const taskIndex = claimRunnable();
|
|
2094
|
+
if (void 0 === taskIndex) break;
|
|
2095
|
+
const task = options.tasks[taskIndex];
|
|
2096
|
+
for (const resource of task.resources)activeResources.add(resource);
|
|
2097
|
+
active += 1;
|
|
2098
|
+
Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
|
|
2099
|
+
results[taskIndex] = result;
|
|
2100
|
+
}).catch((error)=>{
|
|
2101
|
+
if (!hasError) {
|
|
2102
|
+
hasError = true;
|
|
2103
|
+
firstError = error;
|
|
2104
|
+
}
|
|
2105
|
+
}).finally(()=>{
|
|
2106
|
+
release(task.resources);
|
|
2107
|
+
active -= 1;
|
|
2108
|
+
pump();
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
if (0 === active && (0 === pending.length || stopRequested())) finish();
|
|
2112
|
+
}
|
|
2113
|
+
options.signal?.addEventListener('abort', pump, {
|
|
2114
|
+
once: true
|
|
2115
|
+
});
|
|
2116
|
+
pump();
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
|
|
2120
|
+
const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
|
|
2121
|
+
const documentId = isolatedDocumentId(document, collectedCase);
|
|
2122
|
+
return {
|
|
2123
|
+
projectIndex,
|
|
2124
|
+
prepared,
|
|
2125
|
+
document,
|
|
2126
|
+
collectedCase,
|
|
2127
|
+
task: Object.freeze({
|
|
2128
|
+
taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
|
|
2129
|
+
projectId: prepared.project.projectId,
|
|
2130
|
+
projectName: prepared.project.name,
|
|
2131
|
+
documentId,
|
|
2132
|
+
sourcePath: collectedCase.sourcePath,
|
|
2133
|
+
caseId: collectedCase.caseId,
|
|
2134
|
+
caseName: collectedCase.definition.name,
|
|
2135
|
+
caseIndex: collectedCase.caseIndex,
|
|
2136
|
+
tags: Object.freeze([
|
|
2137
|
+
...collectedCase.definition.tags ?? []
|
|
2138
|
+
]),
|
|
2139
|
+
resources: Object.freeze([
|
|
2140
|
+
...collectedCase.definition.resources ?? []
|
|
2141
|
+
]),
|
|
2142
|
+
retry: prepared.project.retry
|
|
2143
|
+
})
|
|
2144
|
+
};
|
|
2145
|
+
})));
|
|
2146
|
+
const reviveError = (error)=>{
|
|
2147
|
+
const revived = new WorkflowError(error.message, {
|
|
2148
|
+
code: error.code,
|
|
2149
|
+
details: error.details,
|
|
2150
|
+
cause: error.cause
|
|
2151
|
+
});
|
|
2152
|
+
revived.name = error.name;
|
|
2153
|
+
return revived;
|
|
2154
|
+
};
|
|
2155
|
+
const reviveStep = (step)=>{
|
|
2156
|
+
const { error, output, report, ...base } = step;
|
|
2157
|
+
return {
|
|
2158
|
+
...base,
|
|
2159
|
+
input: step.input,
|
|
2160
|
+
meta: {
|
|
2161
|
+
...step.meta
|
|
2162
|
+
},
|
|
2163
|
+
...output ? {
|
|
2164
|
+
output: {
|
|
2165
|
+
...output
|
|
2166
|
+
}
|
|
2167
|
+
} : {},
|
|
2168
|
+
...error ? {
|
|
2169
|
+
error: reviveError(error)
|
|
2170
|
+
} : {},
|
|
2171
|
+
...report ? {
|
|
2172
|
+
report: {
|
|
2173
|
+
traces: report.traces.map((trace)=>({
|
|
2174
|
+
...trace
|
|
2175
|
+
})),
|
|
2176
|
+
...report.collectionErrors ? {
|
|
2177
|
+
collectionErrors: report.collectionErrors.map(reviveError)
|
|
2178
|
+
} : {}
|
|
2179
|
+
}
|
|
2180
|
+
} : {}
|
|
2181
|
+
};
|
|
2182
|
+
};
|
|
2183
|
+
const reviveAttempt = (attempt)=>{
|
|
2184
|
+
const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
|
|
2185
|
+
return {
|
|
2186
|
+
...base,
|
|
2187
|
+
beforeEach: beforeEach.map(reviveStep),
|
|
2188
|
+
steps: steps.map(reviveStep),
|
|
2189
|
+
afterEach: afterEach.map(reviveStep),
|
|
2190
|
+
...executionErrors ? {
|
|
2191
|
+
executionErrors: executionErrors.map(reviveError)
|
|
2192
|
+
} : {},
|
|
2193
|
+
...teardownErrors ? {
|
|
2194
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2195
|
+
} : {}
|
|
2196
|
+
};
|
|
2197
|
+
};
|
|
2198
|
+
const reviveDocument = (document)=>{
|
|
2199
|
+
const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
|
|
2200
|
+
return {
|
|
2201
|
+
...base,
|
|
2202
|
+
...hostErrors ? {
|
|
2203
|
+
hostErrors: hostErrors.map((error)=>({
|
|
2204
|
+
...error
|
|
2205
|
+
}))
|
|
2206
|
+
} : {},
|
|
2207
|
+
beforeAll: beforeAll.map(reviveStep),
|
|
2208
|
+
afterAll: afterAll.map(reviveStep),
|
|
2209
|
+
...executionErrors ? {
|
|
2210
|
+
executionErrors: executionErrors.map(reviveError)
|
|
2211
|
+
} : {},
|
|
2212
|
+
...teardownErrors ? {
|
|
2213
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2214
|
+
} : {}
|
|
2215
|
+
};
|
|
2216
|
+
};
|
|
2217
|
+
const reviveLifecycle = (lifecycle)=>{
|
|
2218
|
+
const { setupError, teardownErrors, ...base } = lifecycle;
|
|
2219
|
+
return {
|
|
2220
|
+
...base,
|
|
2221
|
+
...setupError ? {
|
|
2222
|
+
setupError: reviveError(setupError)
|
|
2223
|
+
} : {},
|
|
2224
|
+
...teardownErrors ? {
|
|
2225
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2226
|
+
} : {}
|
|
2227
|
+
};
|
|
2228
|
+
};
|
|
2229
|
+
const toTransportResult = (result, materializeReport, materializeSource)=>{
|
|
2230
|
+
const transportReports = (scope)=>[
|
|
2231
|
+
...(scope.reportSources ?? []).map(materializeSource),
|
|
2232
|
+
...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
|
|
2233
|
+
];
|
|
2234
|
+
const mapStep = (step)=>{
|
|
2235
|
+
if (!step.report) return step;
|
|
2236
|
+
const { sources: _sources, ...report } = step.report;
|
|
2237
|
+
return {
|
|
2238
|
+
...step,
|
|
2239
|
+
report
|
|
2240
|
+
};
|
|
2241
|
+
};
|
|
2242
|
+
const mapAttempt = (attempt)=>{
|
|
2243
|
+
const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
|
|
2244
|
+
const reportRefs = transportReports(attempt);
|
|
2245
|
+
return {
|
|
2246
|
+
...transportAttempt,
|
|
2247
|
+
beforeEach: attempt.beforeEach.map(mapStep),
|
|
2248
|
+
steps: attempt.steps.map(mapStep),
|
|
2249
|
+
afterEach: attempt.afterEach.map(mapStep),
|
|
2250
|
+
...reportRefs.length ? {
|
|
2251
|
+
reportRefs
|
|
2252
|
+
} : {}
|
|
2253
|
+
};
|
|
2254
|
+
};
|
|
2255
|
+
const attempts = result.case.attempts?.map(mapAttempt);
|
|
2256
|
+
const run = attempts?.at(-1);
|
|
2257
|
+
const { run: _run, attempts: _attempts, ...transportCase } = result.case;
|
|
2258
|
+
const document = result.document ? (()=>{
|
|
2259
|
+
const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
|
|
2260
|
+
const reportRefs = transportReports(result.document);
|
|
2261
|
+
return {
|
|
2262
|
+
...transportDocument,
|
|
2263
|
+
...result.document.hostErrors ? {
|
|
2264
|
+
hostErrors: result.document.hostErrors.map(({ phase, error })=>({
|
|
2265
|
+
phase,
|
|
2266
|
+
error: sanitizeReportError(error)
|
|
2267
|
+
}))
|
|
2268
|
+
} : {},
|
|
2269
|
+
beforeAll: result.document.beforeAll.map(mapStep),
|
|
2270
|
+
afterAll: result.document.afterAll.map(mapStep),
|
|
2271
|
+
...reportRefs.length ? {
|
|
2272
|
+
reportRefs
|
|
2273
|
+
} : {}
|
|
2274
|
+
};
|
|
2275
|
+
})() : void 0;
|
|
2276
|
+
return JSON.parse(JSON.stringify({
|
|
2277
|
+
case: {
|
|
2278
|
+
...transportCase,
|
|
2279
|
+
...run ? {
|
|
2280
|
+
run
|
|
2281
|
+
} : {},
|
|
2282
|
+
...attempts ? {
|
|
2283
|
+
attempts
|
|
2284
|
+
} : {}
|
|
2285
|
+
},
|
|
2286
|
+
...document ? {
|
|
2287
|
+
document
|
|
2288
|
+
} : {},
|
|
2289
|
+
...result.lifecycle ? {
|
|
2290
|
+
lifecycle: result.lifecycle
|
|
2291
|
+
} : {}
|
|
2292
|
+
}));
|
|
2293
|
+
};
|
|
2294
|
+
const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
|
|
2295
|
+
const resolveReports = (refs)=>{
|
|
2296
|
+
const reportPaths = [];
|
|
2297
|
+
const reportSources = [];
|
|
2298
|
+
for (const ref of refs ?? []){
|
|
2299
|
+
const path = resolveReportReference(ref);
|
|
2300
|
+
const source = resolveSource(ref);
|
|
2301
|
+
if (source) reportSources.push(source);
|
|
2302
|
+
else reportPaths.push(path);
|
|
2303
|
+
}
|
|
2304
|
+
return {
|
|
2305
|
+
...reportPaths.length ? {
|
|
2306
|
+
reportPaths
|
|
2307
|
+
} : {},
|
|
2308
|
+
...reportSources.length ? {
|
|
2309
|
+
reportSources
|
|
2310
|
+
} : {}
|
|
2311
|
+
};
|
|
2312
|
+
};
|
|
2313
|
+
const attempts = result.case.attempts?.map((attempt)=>({
|
|
2314
|
+
...reviveAttempt(attempt),
|
|
2315
|
+
...resolveReports(attempt.reportRefs)
|
|
2316
|
+
}));
|
|
2317
|
+
const run = attempts?.at(-1);
|
|
2318
|
+
const { run: _run, attempts: _attempts, ...caseBase } = result.case;
|
|
2319
|
+
return {
|
|
2320
|
+
case: {
|
|
2321
|
+
...caseBase,
|
|
2322
|
+
...run ? {
|
|
2323
|
+
run
|
|
2324
|
+
} : {},
|
|
2325
|
+
...attempts ? {
|
|
2326
|
+
attempts
|
|
2327
|
+
} : {}
|
|
2328
|
+
},
|
|
2329
|
+
...result.document ? {
|
|
2330
|
+
document: {
|
|
2331
|
+
...reviveDocument(result.document),
|
|
2332
|
+
...resolveReports(result.document.reportRefs)
|
|
2333
|
+
}
|
|
2334
|
+
} : {},
|
|
2335
|
+
...result.lifecycle ? {
|
|
2336
|
+
lifecycle: reviveLifecycle(result.lifecycle)
|
|
2337
|
+
} : {}
|
|
2338
|
+
};
|
|
2339
|
+
};
|
|
2340
|
+
const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
|
|
2341
|
+
const { prepared, document, collectedCase, task } = preparedTask;
|
|
2342
|
+
const { project } = prepared;
|
|
2343
|
+
const isolatedDocument = {
|
|
2344
|
+
...document,
|
|
2345
|
+
documentId: task.documentId,
|
|
2346
|
+
cases: [
|
|
2347
|
+
collectedCase
|
|
2348
|
+
]
|
|
2349
|
+
};
|
|
2350
|
+
const runtime = createProjectRuntime({
|
|
2351
|
+
project,
|
|
2352
|
+
setup: project.setup,
|
|
2353
|
+
signal: options.signal
|
|
2354
|
+
});
|
|
2355
|
+
let lifecycle;
|
|
2356
|
+
let execution;
|
|
2357
|
+
let executionError;
|
|
2358
|
+
try {
|
|
2359
|
+
await runtime.start();
|
|
2360
|
+
if (runtime.canRun) execution = await runWorkflowDocument(isolatedDocument, {
|
|
2361
|
+
resolveNode: project.nodes.require.bind(project.nodes),
|
|
2362
|
+
project,
|
|
2363
|
+
projectContext: runtime.context,
|
|
2364
|
+
retry: project.retry,
|
|
2365
|
+
signal: runtime.signal,
|
|
2366
|
+
defaultTimeoutMs: options.test.testTimeout,
|
|
2367
|
+
shouldStop: ()=>options.signal.aborted,
|
|
2368
|
+
stopReason: ()=>'interrupted',
|
|
2369
|
+
isFatalError: (run)=>[
|
|
2370
|
+
...run.beforeEach,
|
|
2371
|
+
...run.steps,
|
|
2372
|
+
...run.afterEach
|
|
2373
|
+
].some((step)=>step.error && isFatalDeviceError(step.error)) || (run.teardownErrors ?? []).some(isFatalDeviceError),
|
|
2374
|
+
onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
|
|
2375
|
+
onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
|
|
2376
|
+
onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
|
|
2377
|
+
});
|
|
2378
|
+
} catch (error) {
|
|
2379
|
+
executionError = error;
|
|
2380
|
+
if (error instanceof WorkflowExecutionFailure) execution = error.result;
|
|
2381
|
+
} finally{
|
|
2382
|
+
const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
|
|
2383
|
+
lifecycle = await runtime.finish(failed ? 'failed' : 'success');
|
|
2384
|
+
}
|
|
2385
|
+
if (executionError) {
|
|
2386
|
+
if (!execution) throw executionError;
|
|
2387
|
+
options.onInfrastructureError(executionError);
|
|
2388
|
+
}
|
|
2389
|
+
if (!execution) return toTransportResult({
|
|
2390
|
+
case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
|
|
2391
|
+
lifecycle
|
|
2392
|
+
}, materializeReport, materializeSource);
|
|
2393
|
+
const outcome = execution.cases[0];
|
|
2394
|
+
if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
|
|
2395
|
+
return toTransportResult({
|
|
2396
|
+
case: {
|
|
2397
|
+
...outcome,
|
|
2398
|
+
documentId: task.documentId
|
|
2399
|
+
},
|
|
2400
|
+
document: execution.document,
|
|
2401
|
+
lifecycle
|
|
2402
|
+
}, materializeReport, materializeSource);
|
|
2403
|
+
};
|
|
2404
|
+
const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
|
|
2405
|
+
const { prepared, task } = preparedTask;
|
|
2406
|
+
const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
|
|
2407
|
+
taskProgress('started');
|
|
2408
|
+
const outputDir = (0, external_node_path_.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
|
|
2409
|
+
(0, external_node_fs_.mkdirSync)(outputDir, {
|
|
2410
|
+
recursive: true
|
|
2411
|
+
});
|
|
2412
|
+
const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
|
|
2413
|
+
if (prepared.collectionErrors.length > 0) return {
|
|
2414
|
+
case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
|
|
2415
|
+
};
|
|
2416
|
+
const executor = options.executor ?? localTestExecutor;
|
|
2417
|
+
let result;
|
|
2418
|
+
let executorFailure;
|
|
2419
|
+
let executorAttempts = 0;
|
|
2420
|
+
for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
|
|
2421
|
+
executorAttempts = attemptIndex + 1;
|
|
2422
|
+
try {
|
|
2423
|
+
result = await executor.execute(task, {
|
|
2424
|
+
signal: options.signal,
|
|
2425
|
+
onProgress: taskProgress,
|
|
2426
|
+
outputDir,
|
|
2427
|
+
materializeReport,
|
|
2428
|
+
runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
|
|
2429
|
+
});
|
|
2430
|
+
executorFailure = void 0;
|
|
2431
|
+
break;
|
|
2432
|
+
} catch (error) {
|
|
2433
|
+
if (!isTestExecutorError(error)) throw error;
|
|
2434
|
+
executorFailure = error;
|
|
2435
|
+
if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
|
|
2436
|
+
taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
if (!result) {
|
|
2440
|
+
if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
|
|
2441
|
+
taskProgress(`executor-failed: ${executorFailure.message}`);
|
|
2442
|
+
return {
|
|
2443
|
+
case: {
|
|
2444
|
+
...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
|
|
2445
|
+
execution: {
|
|
2446
|
+
executor: executor.name,
|
|
2447
|
+
resources: task.resources,
|
|
2448
|
+
attempts: executorAttempts,
|
|
2449
|
+
failure: {
|
|
2450
|
+
kind: executorFailure.kind,
|
|
2451
|
+
message: executorFailure.message,
|
|
2452
|
+
retryable: executorFailure.retryable
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
};
|
|
2457
|
+
}
|
|
2458
|
+
assertTestCaseTaskRunResult(task, result);
|
|
2459
|
+
const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
|
|
2460
|
+
const completed = {
|
|
2461
|
+
...internalResult,
|
|
2462
|
+
case: {
|
|
2463
|
+
...internalResult.case,
|
|
2464
|
+
execution: {
|
|
2465
|
+
executor: executor.name,
|
|
2466
|
+
resources: task.resources,
|
|
2467
|
+
attempts: executorAttempts,
|
|
2468
|
+
...internalResult.lifecycle ? {
|
|
2469
|
+
lifecycle: internalResult.lifecycle
|
|
2470
|
+
} : {},
|
|
2471
|
+
...result.artifacts ? {
|
|
2472
|
+
artifacts: result.artifacts
|
|
2473
|
+
} : {},
|
|
2474
|
+
...result.metadata ? {
|
|
2475
|
+
metadata: result.metadata
|
|
2476
|
+
} : {}
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
onCompleted(completed);
|
|
2481
|
+
for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
|
|
2482
|
+
if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
|
|
2483
|
+
taskProgress(internalResult.case.status);
|
|
2484
|
+
return completed;
|
|
2485
|
+
};
|
|
2486
|
+
const runCaseExecution = async (options)=>{
|
|
2487
|
+
const tasks = prepareCaseTasks(options.projects);
|
|
2488
|
+
let failedCaseCount = 0;
|
|
2489
|
+
const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
|
|
2490
|
+
const results = new Array(tasks.length);
|
|
2491
|
+
if (tasks.length > 0) try {
|
|
2492
|
+
await runTaskPool({
|
|
2493
|
+
tasks: tasks.map((preparedTask)=>preparedTask.task),
|
|
2494
|
+
maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
|
|
2495
|
+
signal: options.signal,
|
|
2496
|
+
shouldStop: bailReached,
|
|
2497
|
+
run: async (_task, taskIndex)=>{
|
|
2498
|
+
try {
|
|
2499
|
+
const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
|
|
2500
|
+
results[taskIndex] = completed;
|
|
2501
|
+
});
|
|
2502
|
+
results[taskIndex] = result;
|
|
2503
|
+
if ('failed' === result.case.status) failedCaseCount += 1;
|
|
2504
|
+
return result;
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
options.onInfrastructureError(error);
|
|
2507
|
+
throw error;
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
});
|
|
2511
|
+
} catch (error) {
|
|
2512
|
+
options.onInfrastructureError(error);
|
|
2513
|
+
}
|
|
2514
|
+
return options.projects.map((prepared, projectIndex)=>{
|
|
2515
|
+
const projectTasks = tasks.map((task, taskIndex)=>({
|
|
2516
|
+
task,
|
|
2517
|
+
result: results[taskIndex]
|
|
2518
|
+
})).filter(({ task })=>task.projectIndex === projectIndex);
|
|
2519
|
+
const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
|
|
2520
|
+
const cases = projectTasks.map(({ task, result })=>{
|
|
2521
|
+
if (result) return result.case;
|
|
2522
|
+
if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
|
|
2523
|
+
return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
|
|
2524
|
+
});
|
|
2525
|
+
const documents = projectTasks.flatMap(({ result })=>result?.document ? [
|
|
2526
|
+
result.document
|
|
2527
|
+
] : []);
|
|
2528
|
+
return buildProjectResult(prepared, cases, documents);
|
|
2529
|
+
});
|
|
2530
|
+
};
|
|
1334
2531
|
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
1335
2532
|
...attempt.beforeEach,
|
|
1336
2533
|
...attempt.steps,
|
|
@@ -1376,7 +2573,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1376
2573
|
},
|
|
1377
2574
|
onStepResult: async (info, result)=>{
|
|
1378
2575
|
await invocation.bindings.onStepResult?.(info, result);
|
|
1379
|
-
options.onProgress(formatStepResult(info, result));
|
|
2576
|
+
options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
|
|
1380
2577
|
},
|
|
1381
2578
|
onCaseResult: async (attempt)=>{
|
|
1382
2579
|
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
@@ -1494,16 +2691,6 @@ function createOperationJournal(runDir) {
|
|
|
1494
2691
|
}
|
|
1495
2692
|
};
|
|
1496
2693
|
}
|
|
1497
|
-
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1498
|
-
documentId,
|
|
1499
|
-
caseId: collectedCase.caseId,
|
|
1500
|
-
projectName,
|
|
1501
|
-
name: collectedCase.definition.name,
|
|
1502
|
-
sourcePath: collectedCase.sourcePath,
|
|
1503
|
-
caseIndex: collectedCase.caseIndex,
|
|
1504
|
-
status: 'not-run',
|
|
1505
|
-
notRunReason: reason
|
|
1506
|
-
});
|
|
1507
2694
|
const summarize = (projects)=>{
|
|
1508
2695
|
const healthCases = projects.flatMap((project)=>{
|
|
1509
2696
|
const documentAttemptByRunId = new Map(project.documents.map((document)=>[
|
|
@@ -1537,12 +2724,6 @@ const summarize = (projects)=>{
|
|
|
1537
2724
|
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
1538
2725
|
};
|
|
1539
2726
|
};
|
|
1540
|
-
const latestById = (items, id)=>[
|
|
1541
|
-
...new Map(items.map((item)=>[
|
|
1542
|
-
id(item),
|
|
1543
|
-
item
|
|
1544
|
-
])).values()
|
|
1545
|
-
];
|
|
1546
2727
|
const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
|
|
1547
2728
|
async function runPreparedTestProject(plan) {
|
|
1548
2729
|
const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
|
|
@@ -1555,7 +2736,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1555
2736
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
1556
2737
|
const journal = createOperationJournal(runDir);
|
|
1557
2738
|
const rootController = new AbortController();
|
|
1558
|
-
setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
2739
|
+
setMaxListeners(Math.max(10, ('case' === definition.test.executionUnit ? Math.min(definition.test.maxConcurrency, totalCases) : effectiveConcurrency) + 1), rootController.signal);
|
|
1559
2740
|
const handleSignal = (signal)=>{
|
|
1560
2741
|
rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
|
|
1561
2742
|
};
|
|
@@ -1597,31 +2778,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1597
2778
|
const { project } = prepared;
|
|
1598
2779
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
1599
2780
|
};
|
|
1600
|
-
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
1601
|
-
const { project } = prepared;
|
|
1602
|
-
const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
1603
|
-
return {
|
|
1604
|
-
projectId: project.projectId,
|
|
1605
|
-
name: project.name,
|
|
1606
|
-
...prepared.platform ? {
|
|
1607
|
-
platform: prepared.platform
|
|
1608
|
-
} : {},
|
|
1609
|
-
status: projectFailed ? 'failed' : 'success',
|
|
1610
|
-
retry: project.retry,
|
|
1611
|
-
fileSelection: prepared.fileSelection,
|
|
1612
|
-
tagSelection: project.tags,
|
|
1613
|
-
sourceCount: prepared.sources.length,
|
|
1614
|
-
selectedCaseCount: prepared.selectedCaseCount,
|
|
1615
|
-
filteredCaseCount: prepared.filteredCaseCount,
|
|
1616
|
-
...lifecycle ? {
|
|
1617
|
-
lifecycle
|
|
1618
|
-
} : {},
|
|
1619
|
-
cases,
|
|
1620
|
-
plans: prepared.plans,
|
|
1621
|
-
documents,
|
|
1622
|
-
collectionErrors: prepared.collectionErrors
|
|
1623
|
-
};
|
|
1624
|
-
};
|
|
1625
2781
|
const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
|
|
1626
2782
|
announceProject(prepared, projectIndex);
|
|
1627
2783
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
@@ -1722,7 +2878,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1722
2878
|
};
|
|
1723
2879
|
const projectResults = new Array(preparedProjects.length);
|
|
1724
2880
|
try {
|
|
1725
|
-
await runConcurrentJobs(preparedProjects, {
|
|
2881
|
+
if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await runConcurrentJobs(preparedProjects, {
|
|
1726
2882
|
concurrency: effectiveConcurrency,
|
|
1727
2883
|
shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
|
|
1728
2884
|
}, async (prepared, index)=>{
|
|
@@ -1733,6 +2889,20 @@ async function runPreparedTestProject(plan) {
|
|
|
1733
2889
|
throw error;
|
|
1734
2890
|
}
|
|
1735
2891
|
});
|
|
2892
|
+
else {
|
|
2893
|
+
const results = await runCaseExecution({
|
|
2894
|
+
projects: preparedProjects,
|
|
2895
|
+
test: definition.test,
|
|
2896
|
+
executor: definition.executor,
|
|
2897
|
+
runDir,
|
|
2898
|
+
signal: rootController.signal,
|
|
2899
|
+
progress,
|
|
2900
|
+
onInfrastructureError: recordInfrastructureError
|
|
2901
|
+
});
|
|
2902
|
+
results.forEach((result, index)=>{
|
|
2903
|
+
projectResults[index] = result;
|
|
2904
|
+
});
|
|
2905
|
+
}
|
|
1736
2906
|
} catch (error) {
|
|
1737
2907
|
recordInfrastructureError(error);
|
|
1738
2908
|
} finally{
|
|
@@ -1845,6 +3015,10 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
1845
3015
|
let configPath;
|
|
1846
3016
|
let resultDir;
|
|
1847
3017
|
const projectNames = [];
|
|
3018
|
+
const paths = [];
|
|
3019
|
+
const caseIds = [];
|
|
3020
|
+
const includeTags = [];
|
|
3021
|
+
const excludeTags = [];
|
|
1848
3022
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
1849
3023
|
const token = args[index];
|
|
1850
3024
|
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
@@ -1855,17 +3029,22 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
1855
3029
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
1856
3030
|
continue;
|
|
1857
3031
|
}
|
|
1858
|
-
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
3032
|
+
if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--file' === arg || '--case-id' === arg || '--tag' === arg || '--exclude-tag' === arg) {
|
|
1859
3033
|
const value = inlineValue ?? args[index + 1];
|
|
1860
3034
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
1861
3035
|
if ('--config' === arg) configPath = value;
|
|
1862
3036
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
1863
|
-
else projectNames.push(value);
|
|
3037
|
+
else if ('--project' === arg) projectNames.push(value);
|
|
3038
|
+
else if ('--file' === arg) paths.push(value);
|
|
3039
|
+
else if ('--case-id' === arg) caseIds.push(value);
|
|
3040
|
+
else if ('--tag' === arg) includeTags.push(value);
|
|
3041
|
+
else excludeTags.push(value);
|
|
1864
3042
|
if (void 0 === inlineValue) index += 1;
|
|
1865
3043
|
} else throw new Error(`Unknown option: ${token}`);
|
|
1866
3044
|
}
|
|
1867
3045
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
1868
3046
|
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
3047
|
+
if ('nodes' === command && (paths.length > 0 || caseIds.length > 0 || includeTags.length > 0 || excludeTags.length > 0)) throw new Error('nodes does not support --file, --case-id, --tag, or --exclude-tag.');
|
|
1869
3048
|
return {
|
|
1870
3049
|
...command ? {
|
|
1871
3050
|
command
|
|
@@ -1876,6 +3055,22 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
1876
3055
|
resultDir,
|
|
1877
3056
|
...projectNames.length > 0 ? {
|
|
1878
3057
|
projectNames
|
|
3058
|
+
} : {},
|
|
3059
|
+
...paths.length > 0 ? {
|
|
3060
|
+
paths
|
|
3061
|
+
} : {},
|
|
3062
|
+
...caseIds.length > 0 ? {
|
|
3063
|
+
caseIds
|
|
3064
|
+
} : {},
|
|
3065
|
+
...includeTags.length > 0 || excludeTags.length > 0 ? {
|
|
3066
|
+
tags: {
|
|
3067
|
+
...includeTags.length > 0 ? {
|
|
3068
|
+
include: includeTags
|
|
3069
|
+
} : {},
|
|
3070
|
+
...excludeTags.length > 0 ? {
|
|
3071
|
+
exclude: excludeTags
|
|
3072
|
+
} : {}
|
|
3073
|
+
}
|
|
1879
3074
|
} : {}
|
|
1880
3075
|
};
|
|
1881
3076
|
};
|