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