@midscene/test 1.15.4-beta-20261013090705.0 → 1.15.4
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/{1~214.mjs → 1~4.mjs} +96 -174
- package/dist/es/1~4.mjs.map +1 -0
- package/dist/es/cli/index.mjs +741 -1910
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +1 -20
- 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 +70 -1265
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/{0~160.js → 0~593.js} +95 -173
- package/dist/lib/0~593.js.map +1 -0
- package/dist/lib/cli/index.js +740 -1927
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +1 -20
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +0 -3
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +70 -1265
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/templates/manifest.json +60 -0
- package/dist/templates/test/android/.env.example +15 -0
- package/dist/templates/test/android/README.md +19 -0
- package/dist/templates/test/android/README.zh.md +11 -0
- package/dist/templates/test/android/cases/todo.yaml +26 -0
- package/dist/templates/test/android/gitignore +4 -0
- package/dist/templates/test/android/midscene.config.ts +69 -0
- package/dist/templates/test/android/package.json +20 -0
- package/dist/templates/test/android/tsconfig.json +12 -0
- package/dist/templates/test/computer/.env.example +17 -0
- package/dist/templates/test/computer/README.md +19 -0
- package/dist/templates/test/computer/README.zh.md +11 -0
- package/dist/templates/test/computer/cases/todo.yaml +31 -0
- package/dist/templates/test/computer/gitignore +4 -0
- package/dist/templates/test/computer/midscene.config.ts +74 -0
- package/dist/templates/test/computer/package.json +20 -0
- package/dist/templates/test/computer/tsconfig.json +12 -0
- package/dist/templates/test/harmony/.env.example +15 -0
- package/dist/templates/test/harmony/README.md +19 -0
- package/dist/templates/test/harmony/README.zh.md +11 -0
- package/dist/templates/test/harmony/cases/todo.yaml +26 -0
- package/dist/templates/test/harmony/gitignore +4 -0
- package/dist/templates/test/harmony/midscene.config.ts +69 -0
- package/dist/templates/test/harmony/package.json +20 -0
- package/dist/templates/test/harmony/tsconfig.json +12 -0
- package/dist/templates/test/ios/.env.example +18 -0
- package/dist/templates/test/ios/README.md +19 -0
- package/dist/templates/test/ios/README.zh.md +11 -0
- package/dist/templates/test/ios/cases/todo.yaml +26 -0
- package/dist/templates/test/ios/gitignore +4 -0
- package/dist/templates/test/ios/midscene.config.ts +95 -0
- package/dist/templates/test/ios/package.json +20 -0
- package/dist/templates/test/ios/tsconfig.json +12 -0
- package/dist/templates/test/web/.env.example +14 -0
- package/dist/templates/test/web/README.md +19 -0
- package/dist/templates/test/web/README.zh.md +13 -0
- package/dist/templates/test/web/cases/todo.yaml +62 -0
- package/dist/templates/test/web/gitignore +4 -0
- package/dist/templates/test/web/midscene.config.ts +56 -0
- package/dist/templates/test/web/nodes/todo.ts +215 -0
- package/dist/templates/test/web/package.json +21 -0
- package/dist/templates/test/web/tsconfig.json +12 -0
- package/dist/types/cli/index.d.ts +0 -556
- package/dist/types/index.d.ts +0 -20
- package/dist/types/midscene/index.d.ts +2 -6
- package/package.json +16 -15
- package/dist/es/1~214.mjs.map +0 -1
- package/dist/lib/0~160.js.map +0 -1
package/dist/es/test-cli.mjs
CHANGED
|
@@ -20,7 +20,9 @@ var __webpack_modules__ = {
|
|
|
20
20
|
module.exports = __rspack_external_node_path_c5b9b54f;
|
|
21
21
|
},
|
|
22
22
|
"./package.json" (module) {
|
|
23
|
-
module.exports =
|
|
23
|
+
module.exports = {
|
|
24
|
+
rE: "1.15.4"
|
|
25
|
+
};
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
var __webpack_module_cache__ = {};
|
|
@@ -267,7 +269,7 @@ const validatePatterns = (value, field, label = 'files', allowEmptyInclude = fal
|
|
|
267
269
|
return pattern;
|
|
268
270
|
});
|
|
269
271
|
};
|
|
270
|
-
const
|
|
272
|
+
const test_project_validateTestFileSelection = (value, label = 'files')=>{
|
|
271
273
|
if (void 0 === value) return;
|
|
272
274
|
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
273
275
|
rejectUnknownKeys(value, [
|
|
@@ -391,7 +393,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
391
393
|
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
392
394
|
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
393
395
|
names.add(candidate.name);
|
|
394
|
-
const files =
|
|
396
|
+
const files = test_project_validateTestFileSelection(candidate.files, `${label}.files`);
|
|
395
397
|
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
396
398
|
const localNodes = new registry_NodeRegistry(candidate.nodes);
|
|
397
399
|
const nodes = new registry_NodeRegistry([
|
|
@@ -421,27 +423,12 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
421
423
|
const validateTestOptions = (value)=>{
|
|
422
424
|
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
423
425
|
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')
|
|
430
|
-
executionUnit,
|
|
431
|
-
executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
|
|
429
|
+
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
|
|
432
430
|
});
|
|
433
431
|
};
|
|
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
|
-
};
|
|
445
432
|
const validateOutputPath = (value, fallback, label)=>{
|
|
446
433
|
if (void 0 === value) return fallback;
|
|
447
434
|
if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
|
|
@@ -470,22 +457,15 @@ const validateTestProjectDefinition = (definition)=>{
|
|
|
470
457
|
'projects',
|
|
471
458
|
'test',
|
|
472
459
|
'output',
|
|
473
|
-
'nodes'
|
|
474
|
-
'executor'
|
|
460
|
+
'nodes'
|
|
475
461
|
], 'root');
|
|
476
462
|
const nodes = new registry_NodeRegistry(definition.nodes);
|
|
477
463
|
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".');
|
|
481
464
|
return {
|
|
482
465
|
...resolvedProjects,
|
|
483
|
-
test,
|
|
466
|
+
test: validateTestOptions(definition.test),
|
|
484
467
|
output: validateOutput(definition.output),
|
|
485
468
|
nodes,
|
|
486
|
-
...executor ? {
|
|
487
|
-
executor
|
|
488
|
-
} : {},
|
|
489
469
|
resolveNode: (name)=>nodes.get(name)
|
|
490
470
|
};
|
|
491
471
|
};
|
|
@@ -530,7 +510,6 @@ async function loadTestProject(configPath) {
|
|
|
530
510
|
}
|
|
531
511
|
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
532
512
|
}
|
|
533
|
-
const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
534
513
|
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>createHash('sha256').update(JSON.stringify([
|
|
535
514
|
projectId,
|
|
536
515
|
sourcePath,
|
|
@@ -750,15 +729,10 @@ function collectWorkflowDocument(source, options) {
|
|
|
750
729
|
caseIndex
|
|
751
730
|
});
|
|
752
731
|
collect_rejectUnknownKeys(definition, [
|
|
753
|
-
'id',
|
|
754
732
|
'name',
|
|
755
733
|
'tags',
|
|
756
|
-
'resources',
|
|
757
734
|
'steps'
|
|
758
735
|
], `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
|
-
});
|
|
762
736
|
if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
|
|
763
737
|
caseIndex
|
|
764
738
|
});
|
|
@@ -769,13 +743,6 @@ function collectWorkflowDocument(source, options) {
|
|
|
769
743
|
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.`, {
|
|
770
744
|
caseIndex
|
|
771
745
|
});
|
|
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
|
-
});
|
|
779
746
|
const steps = normalize_normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
780
747
|
if (!options.resolveNode(normalized.node)) throw new WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
781
748
|
caseIndex,
|
|
@@ -784,7 +751,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
784
751
|
});
|
|
785
752
|
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
786
753
|
});
|
|
787
|
-
const caseId =
|
|
754
|
+
const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
|
|
788
755
|
if (ids.has(caseId)) throw new WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
789
756
|
caseId
|
|
790
757
|
});
|
|
@@ -795,14 +762,8 @@ function collectWorkflowDocument(source, options) {
|
|
|
795
762
|
sourcePath,
|
|
796
763
|
caseIndex,
|
|
797
764
|
definition: {
|
|
798
|
-
...'string' == typeof definition.id ? {
|
|
799
|
-
id: definition.id
|
|
800
|
-
} : {},
|
|
801
765
|
name: definition.name,
|
|
802
766
|
tags: tags,
|
|
803
|
-
...resources.length > 0 ? {
|
|
804
|
-
resources: resources
|
|
805
|
-
} : {},
|
|
806
767
|
steps
|
|
807
768
|
}
|
|
808
769
|
};
|
|
@@ -942,38 +903,6 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
942
903
|
...outcome.notRunReason ? {
|
|
943
904
|
notRunReason: outcome.notRunReason
|
|
944
905
|
} : {},
|
|
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
|
-
} : {},
|
|
977
906
|
...outcome.attempts ? {
|
|
978
907
|
attempts: outcome.attempts.map((attempt)=>({
|
|
979
908
|
attemptId: attempt.runId,
|
|
@@ -1051,27 +980,6 @@ const DEFAULT_TEST_FILE_SELECTION = {
|
|
|
1051
980
|
]
|
|
1052
981
|
};
|
|
1053
982
|
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
|
-
};
|
|
1075
983
|
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
1076
984
|
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
1077
985
|
const match = (patterns)=>globSync(patterns, {
|
|
@@ -1096,11 +1004,6 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
|
1096
1004
|
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
1097
1005
|
});
|
|
1098
1006
|
};
|
|
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
|
-
};
|
|
1104
1007
|
const discoverTestConfig = (projectRoot)=>{
|
|
1105
1008
|
const root = (0, external_node_path_.resolve)(projectRoot);
|
|
1106
1009
|
const candidates = (0, external_node_fs_.readdirSync)(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
|
|
@@ -1131,14 +1034,8 @@ const matchesTags = (tags, selection)=>{
|
|
|
1131
1034
|
if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
|
|
1132
1035
|
return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
|
|
1133
1036
|
};
|
|
1134
|
-
const filterDocumentCases = (document, project
|
|
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
|
-
});
|
|
1037
|
+
const filterDocumentCases = (document, project)=>{
|
|
1038
|
+
const cases = document.cases.filter((item)=>matchesTags(item.definition.tags ?? [], project.tags));
|
|
1142
1039
|
const filtered = document.cases.length - cases.length;
|
|
1143
1040
|
return 0 === cases.length ? {
|
|
1144
1041
|
filtered
|
|
@@ -1163,9 +1060,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1163
1060
|
const platform = options.platform?.trim();
|
|
1164
1061
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
1165
1062
|
const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_.resolve)(options.prerequisiteFile) : void 0;
|
|
1166
|
-
const
|
|
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;
|
|
1063
|
+
const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
1169
1064
|
const files = [
|
|
1170
1065
|
...prerequisiteFile ? [
|
|
1171
1066
|
prerequisiteFile
|
|
@@ -1188,7 +1083,6 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1188
1083
|
const invocations = [];
|
|
1189
1084
|
const plans = [];
|
|
1190
1085
|
let filteredCaseCount = 0;
|
|
1191
|
-
const availableCaseIds = new Set();
|
|
1192
1086
|
if (0 === sources.length) {
|
|
1193
1087
|
const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
1194
1088
|
projectName: project.name,
|
|
@@ -1219,16 +1113,10 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1219
1113
|
variables: project.variables,
|
|
1220
1114
|
env: process.env
|
|
1221
1115
|
});
|
|
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);
|
|
1228
1116
|
const filtered = source.absolutePath === prerequisiteFile ? {
|
|
1229
1117
|
document,
|
|
1230
1118
|
filtered: 0
|
|
1231
|
-
} : filterDocumentCases(document, project
|
|
1119
|
+
} : filterDocumentCases(document, project);
|
|
1232
1120
|
const plan = buildTestRunReportPlan(document, {
|
|
1233
1121
|
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
1234
1122
|
defaultTimeoutMs,
|
|
@@ -1276,42 +1164,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1276
1164
|
plans,
|
|
1277
1165
|
collectionErrors,
|
|
1278
1166
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
1279
|
-
filteredCaseCount
|
|
1280
|
-
availableCaseIds: [
|
|
1281
|
-
...availableCaseIds
|
|
1282
|
-
]
|
|
1167
|
+
filteredCaseCount
|
|
1283
1168
|
};
|
|
1284
1169
|
}
|
|
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
|
-
};
|
|
1315
1170
|
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
1316
1171
|
const createTestRunId = (date = new Date(), uuid = randomUUID())=>{
|
|
1317
1172
|
const timestamp = [
|
|
@@ -1349,7 +1204,6 @@ async function prepareTestRun(options) {
|
|
|
1349
1204
|
const startedAt = new Date();
|
|
1350
1205
|
const runId = createTestRunId(startedAt);
|
|
1351
1206
|
const input = resolveRunInput(options);
|
|
1352
|
-
const selection = validateRunSelection(options);
|
|
1353
1207
|
const configPath = options.configPath ? (0, external_node_path_.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
1354
1208
|
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}\`.`);
|
|
1355
1209
|
if (options.configPath && !(0, external_node_fs_.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
@@ -1363,20 +1217,11 @@ async function prepareTestRun(options) {
|
|
|
1363
1217
|
});
|
|
1364
1218
|
(0, external_node_fs_.mkdirSync)(runDir);
|
|
1365
1219
|
const projects = [];
|
|
1366
|
-
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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.');
|
|
1220
|
+
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
|
|
1221
|
+
files: [
|
|
1222
|
+
input.singleFile
|
|
1223
|
+
]
|
|
1224
|
+
} : void 0));
|
|
1380
1225
|
const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
|
|
1381
1226
|
return {
|
|
1382
1227
|
startedAt,
|
|
@@ -1472,1062 +1317,20 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1472
1317
|
}));
|
|
1473
1318
|
return output;
|
|
1474
1319
|
}
|
|
1475
|
-
const
|
|
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
|
-
});
|
|
1320
|
+
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1485
1321
|
const formatStep = (info)=>{
|
|
1486
|
-
const position =
|
|
1322
|
+
const position = stepPosition(info);
|
|
1487
1323
|
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
1488
1324
|
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
1489
1325
|
};
|
|
1490
|
-
const formatStepResult = (info, result
|
|
1326
|
+
const formatStepResult = (info, result)=>{
|
|
1327
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1491
1328
|
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1492
1329
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1493
1330
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1494
1331
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
1495
1332
|
return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
1496
1333
|
};
|
|
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
|
-
};
|
|
2531
1334
|
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
2532
1335
|
...attempt.beforeEach,
|
|
2533
1336
|
...attempt.steps,
|
|
@@ -2573,7 +1376,7 @@ async function executeDocumentInvocation(options) {
|
|
|
2573
1376
|
},
|
|
2574
1377
|
onStepResult: async (info, result)=>{
|
|
2575
1378
|
await invocation.bindings.onStepResult?.(info, result);
|
|
2576
|
-
options.onProgress(formatStepResult(info, result
|
|
1379
|
+
options.onProgress(formatStepResult(info, result));
|
|
2577
1380
|
},
|
|
2578
1381
|
onCaseResult: async (attempt)=>{
|
|
2579
1382
|
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
@@ -2691,6 +1494,16 @@ function createOperationJournal(runDir) {
|
|
|
2691
1494
|
}
|
|
2692
1495
|
};
|
|
2693
1496
|
}
|
|
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
|
+
});
|
|
2694
1507
|
const summarize = (projects)=>{
|
|
2695
1508
|
const healthCases = projects.flatMap((project)=>{
|
|
2696
1509
|
const documentAttemptByRunId = new Map(project.documents.map((document)=>[
|
|
@@ -2724,6 +1537,12 @@ const summarize = (projects)=>{
|
|
|
2724
1537
|
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
2725
1538
|
};
|
|
2726
1539
|
};
|
|
1540
|
+
const latestById = (items, id)=>[
|
|
1541
|
+
...new Map(items.map((item)=>[
|
|
1542
|
+
id(item),
|
|
1543
|
+
item
|
|
1544
|
+
])).values()
|
|
1545
|
+
];
|
|
2727
1546
|
const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
|
|
2728
1547
|
async function runPreparedTestProject(plan) {
|
|
2729
1548
|
const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
|
|
@@ -2736,7 +1555,7 @@ async function runPreparedTestProject(plan) {
|
|
|
2736
1555
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
2737
1556
|
const journal = createOperationJournal(runDir);
|
|
2738
1557
|
const rootController = new AbortController();
|
|
2739
|
-
setMaxListeners(Math.max(10,
|
|
1558
|
+
setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
2740
1559
|
const handleSignal = (signal)=>{
|
|
2741
1560
|
rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
|
|
2742
1561
|
};
|
|
@@ -2778,6 +1597,31 @@ async function runPreparedTestProject(plan) {
|
|
|
2778
1597
|
const { project } = prepared;
|
|
2779
1598
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
2780
1599
|
};
|
|
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
|
+
};
|
|
2781
1625
|
const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
|
|
2782
1626
|
announceProject(prepared, projectIndex);
|
|
2783
1627
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
@@ -2878,7 +1722,7 @@ async function runPreparedTestProject(plan) {
|
|
|
2878
1722
|
};
|
|
2879
1723
|
const projectResults = new Array(preparedProjects.length);
|
|
2880
1724
|
try {
|
|
2881
|
-
|
|
1725
|
+
await runConcurrentJobs(preparedProjects, {
|
|
2882
1726
|
concurrency: effectiveConcurrency,
|
|
2883
1727
|
shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
|
|
2884
1728
|
}, async (prepared, index)=>{
|
|
@@ -2889,20 +1733,6 @@ async function runPreparedTestProject(plan) {
|
|
|
2889
1733
|
throw error;
|
|
2890
1734
|
}
|
|
2891
1735
|
});
|
|
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
|
-
}
|
|
2906
1736
|
} catch (error) {
|
|
2907
1737
|
recordInfrastructureError(error);
|
|
2908
1738
|
} finally{
|
|
@@ -3015,10 +1845,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
3015
1845
|
let configPath;
|
|
3016
1846
|
let resultDir;
|
|
3017
1847
|
const projectNames = [];
|
|
3018
|
-
const paths = [];
|
|
3019
|
-
const caseIds = [];
|
|
3020
|
-
const includeTags = [];
|
|
3021
|
-
const excludeTags = [];
|
|
3022
1848
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
3023
1849
|
const token = args[index];
|
|
3024
1850
|
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
@@ -3029,22 +1855,17 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
3029
1855
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
3030
1856
|
continue;
|
|
3031
1857
|
}
|
|
3032
|
-
if ('--config' === arg || '--result-dir' === arg || '--project' === arg
|
|
1858
|
+
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
3033
1859
|
const value = inlineValue ?? args[index + 1];
|
|
3034
1860
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
3035
1861
|
if ('--config' === arg) configPath = value;
|
|
3036
1862
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
3037
|
-
else
|
|
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);
|
|
1863
|
+
else projectNames.push(value);
|
|
3042
1864
|
if (void 0 === inlineValue) index += 1;
|
|
3043
1865
|
} else throw new Error(`Unknown option: ${token}`);
|
|
3044
1866
|
}
|
|
3045
1867
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
3046
1868
|
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.');
|
|
3048
1869
|
return {
|
|
3049
1870
|
...command ? {
|
|
3050
1871
|
command
|
|
@@ -3055,22 +1876,6 @@ const parseTestCliArgs = (args, cwd = process.cwd())=>{
|
|
|
3055
1876
|
resultDir,
|
|
3056
1877
|
...projectNames.length > 0 ? {
|
|
3057
1878
|
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
|
-
}
|
|
3074
1879
|
} : {}
|
|
3075
1880
|
};
|
|
3076
1881
|
};
|
|
@@ -3226,7 +2031,7 @@ async function runTestCli(args, io = defaultCliIO) {
|
|
|
3226
2031
|
});
|
|
3227
2032
|
}
|
|
3228
2033
|
if ('create' === args[0]) {
|
|
3229
|
-
const { runCreateCommand } = await __webpack_require__.e("
|
|
2034
|
+
const { runCreateCommand } = await __webpack_require__.e("4").then(__webpack_require__.bind(__webpack_require__, "./src/cli/create-command.ts"));
|
|
3230
2035
|
await runCreateCommand(args.slice(1), io);
|
|
3231
2036
|
return 0;
|
|
3232
2037
|
}
|