@midscene/test 1.15.2-beta-20261012100626.0 → 1.15.2
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 +235 -125
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +72 -102
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +54 -30
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +237 -126
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +233 -123
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +70 -106
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +53 -29
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +236 -125
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +10 -6
- package/dist/types/index.d.ts +1 -85
- package/dist/types/midscene/index.d.ts +3 -1
- package/package.json +13 -13
package/dist/es/test-cli.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as __rspack_external_node_fs_5ea92f0c from "node:fs";
|
|
2
2
|
import * as __rspack_external_node_path_c5b9b54f from "node:path";
|
|
3
|
-
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, isFatalDeviceError, normalizeSteps, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument } from "@midscene/core/internal/test-runner";
|
|
3
|
+
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runConcurrentJobs, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText } from "@midscene/core/internal/test-runner";
|
|
4
4
|
import { inspect } from "node:util";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
7
7
|
import { require as api_require } from "tsx/cjs/api";
|
|
8
8
|
import { globSync } from "tinyglobby";
|
|
9
|
-
import { JSON_SCHEMA, load } from "js-yaml";
|
|
10
9
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
+
import { JSON_SCHEMA, load } from "js-yaml";
|
|
11
11
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
12
12
|
import { setMaxListeners } from "node:events";
|
|
13
13
|
import { TestRunReportAssembler, calculateTestRunHealth } from "@midscene/core/report";
|
|
@@ -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.2"
|
|
25
|
+
};
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
var __webpack_module_cache__ = {};
|
|
@@ -524,6 +526,69 @@ const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=
|
|
|
524
526
|
] : []
|
|
525
527
|
])).digest('hex');
|
|
526
528
|
const normalize_normalizeSteps = normalizeSteps;
|
|
529
|
+
function parseWorkflowSource(source) {
|
|
530
|
+
const stack = [];
|
|
531
|
+
const nodes = [];
|
|
532
|
+
const sensitiveNodes = new Set();
|
|
533
|
+
const sensitiveValues = new Set();
|
|
534
|
+
const redactedPaths = new Set();
|
|
535
|
+
const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
|
|
536
|
+
const parsed = load(source, {
|
|
537
|
+
schema: JSON_SCHEMA,
|
|
538
|
+
listener (event, state) {
|
|
539
|
+
if ('open' === event) return void stack.push({
|
|
540
|
+
start: state.position + sourceOffset,
|
|
541
|
+
end: 0,
|
|
542
|
+
value: null,
|
|
543
|
+
children: []
|
|
544
|
+
});
|
|
545
|
+
const node = stack.pop();
|
|
546
|
+
node.end = state.position + sourceOffset;
|
|
547
|
+
node.value = state.result;
|
|
548
|
+
stack.at(-1)?.children.push(node);
|
|
549
|
+
nodes.push(node);
|
|
550
|
+
if ('mapping' !== state.kind) return;
|
|
551
|
+
for(let index = 0; index < node.children.length; index += 2){
|
|
552
|
+
const key = node.children[index].value;
|
|
553
|
+
const value = node.children[index + 1];
|
|
554
|
+
if ('string' == typeof key && isReportSecretKey(key) && value) {
|
|
555
|
+
sensitiveNodes.add(value);
|
|
556
|
+
if (null !== value.value) sensitiveValues.add(value.value);
|
|
557
|
+
redactedPaths.add(`$..[${JSON.stringify(key)}]`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
|
|
563
|
+
let offset = 0;
|
|
564
|
+
let redacted = '';
|
|
565
|
+
for (const range of ranges){
|
|
566
|
+
if (range.start < offset) continue;
|
|
567
|
+
const text = source.slice(range.start, range.end);
|
|
568
|
+
const leading = text.match(/^\s*/)?.[0] ?? '';
|
|
569
|
+
const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
|
|
570
|
+
redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
|
|
571
|
+
offset = range.end;
|
|
572
|
+
}
|
|
573
|
+
redacted += source.slice(offset);
|
|
574
|
+
const value = sanitizeReportText(redacted, 65536);
|
|
575
|
+
return {
|
|
576
|
+
parsed,
|
|
577
|
+
sourceYaml: {
|
|
578
|
+
value,
|
|
579
|
+
...redactedPaths.size ? {
|
|
580
|
+
redactedPaths: [
|
|
581
|
+
...redactedPaths
|
|
582
|
+
]
|
|
583
|
+
} : {},
|
|
584
|
+
...value !== redacted ? {
|
|
585
|
+
truncatedPaths: [
|
|
586
|
+
'$'
|
|
587
|
+
]
|
|
588
|
+
} : {}
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
}
|
|
527
592
|
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
528
593
|
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
529
594
|
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
@@ -598,16 +663,15 @@ const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
|
598
663
|
const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
|
|
599
664
|
const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
|
|
600
665
|
function collectWorkflowDocument(source, options) {
|
|
601
|
-
let
|
|
666
|
+
let snapshot;
|
|
602
667
|
try {
|
|
603
|
-
|
|
604
|
-
schema: JSON_SCHEMA
|
|
605
|
-
});
|
|
668
|
+
snapshot = parseWorkflowSource((0, external_node_fs_.readFileSync)(source.absolutePath, 'utf8'));
|
|
606
669
|
} catch (error) {
|
|
607
670
|
throw new WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
608
671
|
sourcePath: source.sourcePath
|
|
609
672
|
}, error);
|
|
610
673
|
}
|
|
674
|
+
const { parsed, sourceYaml } = snapshot;
|
|
611
675
|
if (!isMapping(parsed)) throw new WorkflowParseError('Workflow document must be a mapping.');
|
|
612
676
|
const format = classifyWorkflowDocumentFormat(parsed);
|
|
613
677
|
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
@@ -622,6 +686,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
622
686
|
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
623
687
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
624
688
|
...step,
|
|
689
|
+
templateInput: step.input,
|
|
625
690
|
input: resolveWorkflowVariables(step.input, {
|
|
626
691
|
variables: options.variables,
|
|
627
692
|
env: options.env ?? process.env,
|
|
@@ -707,6 +772,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
707
772
|
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
708
773
|
projectId: source.projectId,
|
|
709
774
|
sourcePath,
|
|
775
|
+
sourceYaml,
|
|
710
776
|
lifecycle,
|
|
711
777
|
cases
|
|
712
778
|
};
|
|
@@ -721,6 +787,15 @@ const writeJson = async (path, value)=>{
|
|
|
721
787
|
throw new WorkflowPublicationError('write-result', path, error);
|
|
722
788
|
}
|
|
723
789
|
};
|
|
790
|
+
const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_.join)(runDir, 'plan.json'), {
|
|
791
|
+
schemaVersion: 1,
|
|
792
|
+
createdAt: new Date().toISOString(),
|
|
793
|
+
projects: projects.map((item)=>({
|
|
794
|
+
projectId: item.project.projectId,
|
|
795
|
+
name: item.project.name,
|
|
796
|
+
documents: item.plans ?? []
|
|
797
|
+
}))
|
|
798
|
+
});
|
|
724
799
|
const toPosix = (value)=>value.split(external_node_path_.sep).join('/');
|
|
725
800
|
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
726
801
|
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
@@ -750,7 +825,7 @@ const relativeToSummary = (summaryPath, target)=>{
|
|
|
750
825
|
const value = (0, external_node_path_.relative)((0, external_node_path_.dirname)(summaryPath), target);
|
|
751
826
|
return toPosix(value || '.');
|
|
752
827
|
};
|
|
753
|
-
const errorJson =
|
|
828
|
+
const errorJson = sanitizeReportError;
|
|
754
829
|
const writeTestProjectRunResult = (options)=>{
|
|
755
830
|
const { result } = options;
|
|
756
831
|
const fact = (path)=>toPosix(path);
|
|
@@ -770,13 +845,17 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
770
845
|
configPath: options.configPath
|
|
771
846
|
} : {},
|
|
772
847
|
factsRoot: '.',
|
|
848
|
+
plan: 'plan.json',
|
|
849
|
+
...result.operationLog ? {
|
|
850
|
+
operationLog: {
|
|
851
|
+
...result.operationLog,
|
|
852
|
+
path: relativeToSummary(result.summaryPath, result.operationLog.path)
|
|
853
|
+
}
|
|
854
|
+
} : {},
|
|
773
855
|
reportDir: relativeToSummary(result.summaryPath, result.reportDir),
|
|
774
856
|
...result.reportPath ? {
|
|
775
857
|
report: relativeToSummary(result.summaryPath, result.reportPath)
|
|
776
858
|
} : {},
|
|
777
|
-
...result.markdownPath ? {
|
|
778
|
-
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
779
|
-
} : {},
|
|
780
859
|
summary: result.summary,
|
|
781
860
|
projects: result.projects.map((project)=>({
|
|
782
861
|
projectId: project.projectId,
|
|
@@ -794,6 +873,12 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
794
873
|
startedAt: project.lifecycle.startedAt,
|
|
795
874
|
endedAt: project.lifecycle.endedAt,
|
|
796
875
|
durationMs: project.lifecycle.durationMs,
|
|
876
|
+
...project.lifecycle.cleanup ? {
|
|
877
|
+
cleanup: project.lifecycle.cleanup
|
|
878
|
+
} : {},
|
|
879
|
+
...project.lifecycle.pendingOperations ? {
|
|
880
|
+
pendingOperations: project.lifecycle.pendingOperations
|
|
881
|
+
} : {},
|
|
797
882
|
...project.lifecycle.setupError ? {
|
|
798
883
|
setupError: errorJson(project.lifecycle.setupError)
|
|
799
884
|
} : {},
|
|
@@ -820,6 +905,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
820
905
|
attemptId: attempt.runId,
|
|
821
906
|
attemptIndex: attempt.attemptIndex,
|
|
822
907
|
status: attempt.status,
|
|
908
|
+
...attempt.cleanup ? {
|
|
909
|
+
cleanup: attempt.cleanup
|
|
910
|
+
} : {},
|
|
911
|
+
...attempt.pendingOperations ? {
|
|
912
|
+
pendingOperations: attempt.pendingOperations
|
|
913
|
+
} : {},
|
|
914
|
+
...attempt.executionStatus ? {
|
|
915
|
+
executionStatus: attempt.executionStatus
|
|
916
|
+
} : {},
|
|
917
|
+
...attempt.retryDecision ? {
|
|
918
|
+
retryDecision: attempt.retryDecision
|
|
919
|
+
} : {},
|
|
823
920
|
resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
|
|
824
921
|
...attempt.reportPaths?.length ? {
|
|
825
922
|
reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -835,6 +932,18 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
835
932
|
},
|
|
836
933
|
sourcePath: document.sourcePath,
|
|
837
934
|
status: document.status,
|
|
935
|
+
...document.cleanup ? {
|
|
936
|
+
cleanup: document.cleanup
|
|
937
|
+
} : {},
|
|
938
|
+
...document.pendingOperations ? {
|
|
939
|
+
pendingOperations: document.pendingOperations
|
|
940
|
+
} : {},
|
|
941
|
+
...document.executionStatus ? {
|
|
942
|
+
executionStatus: document.executionStatus
|
|
943
|
+
} : {},
|
|
944
|
+
...document.retryDecision ? {
|
|
945
|
+
retryDecision: document.retryDecision
|
|
946
|
+
} : {},
|
|
838
947
|
resultFile: fact(workflowDocumentResultPath(document)),
|
|
839
948
|
...document.reportPaths?.length ? {
|
|
840
949
|
reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
@@ -969,6 +1078,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
969
1078
|
});
|
|
970
1079
|
const collectionErrors = [];
|
|
971
1080
|
const invocations = [];
|
|
1081
|
+
const plans = [];
|
|
972
1082
|
let filteredCaseCount = 0;
|
|
973
1083
|
if (0 === sources.length) {
|
|
974
1084
|
const error = asCollectionError(project.projectId, project.name, '<project>', new WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
@@ -982,7 +1092,17 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
982
1092
|
const collected = await options.collectDocument?.(source, project);
|
|
983
1093
|
if (collected) {
|
|
984
1094
|
filteredCaseCount += collected.filteredCaseCount;
|
|
985
|
-
if (collected.invocation)
|
|
1095
|
+
if (collected.invocation) {
|
|
1096
|
+
const plan = buildTestRunReportPlan(collected.invocation.document, {
|
|
1097
|
+
defaultTimeoutMs: collected.invocation.defaultTimeoutMs,
|
|
1098
|
+
retry: collected.invocation.retry
|
|
1099
|
+
});
|
|
1100
|
+
invocations.push({
|
|
1101
|
+
...collected.invocation,
|
|
1102
|
+
plan
|
|
1103
|
+
});
|
|
1104
|
+
plans.push(plan);
|
|
1105
|
+
}
|
|
986
1106
|
continue;
|
|
987
1107
|
}
|
|
988
1108
|
const document = collectWorkflowDocument(source, {
|
|
@@ -994,8 +1114,18 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
994
1114
|
document,
|
|
995
1115
|
filtered: 0
|
|
996
1116
|
} : filterDocumentCases(document, project);
|
|
1117
|
+
const plan = buildTestRunReportPlan(document, {
|
|
1118
|
+
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
1119
|
+
defaultTimeoutMs,
|
|
1120
|
+
retry: {
|
|
1121
|
+
scope: 'case',
|
|
1122
|
+
count: project.retry
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
plans.push(plan);
|
|
997
1126
|
filteredCaseCount += filtered.filtered;
|
|
998
1127
|
if (filtered.document) invocations.push({
|
|
1128
|
+
plan,
|
|
999
1129
|
document: filtered.document,
|
|
1000
1130
|
retry: {
|
|
1001
1131
|
scope: 'case',
|
|
@@ -1028,6 +1158,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1028
1158
|
sources,
|
|
1029
1159
|
documents,
|
|
1030
1160
|
invocations,
|
|
1161
|
+
plans,
|
|
1031
1162
|
collectionErrors,
|
|
1032
1163
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
1033
1164
|
filteredCaseCount
|
|
@@ -1108,99 +1239,6 @@ async function prepareTestRun(options) {
|
|
|
1108
1239
|
onProgress: options.onProgress ?? (()=>{})
|
|
1109
1240
|
};
|
|
1110
1241
|
}
|
|
1111
|
-
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
1112
|
-
const duration = (ms)=>`${(ms / 1000).toFixed(1)}s`;
|
|
1113
|
-
const href = (from, target)=>(0, external_node_path_.relative)((0, external_node_path_.dirname)(from), target).split(external_node_path_.sep).map(encodeURIComponent).join('/');
|
|
1114
|
-
function renderTestResultMarkdown(dump, options = {}) {
|
|
1115
|
-
const { summary } = dump;
|
|
1116
|
-
const lines = [
|
|
1117
|
-
`## Midscene Test · ${'success' === dump.status ? 'passed' : 'failed'}`,
|
|
1118
|
-
'',
|
|
1119
|
-
`**Cases:** ${summary.total} total · ${summary.passed} passed · ${summary.failed} failed · ${summary.notRun} not run`,
|
|
1120
|
-
'',
|
|
1121
|
-
`**Duration:** ${duration(dump.durationMs)} · **Filtered:** ${summary.filtered}`,
|
|
1122
|
-
'',
|
|
1123
|
-
`**Infrastructure:** ${summary.collectionErrors} collection errors · ${summary.documentFailures} document failures · ${summary.projectFailures} project failures`,
|
|
1124
|
-
'',
|
|
1125
|
-
...options.reportHref ? [
|
|
1126
|
-
`[Open the Midscene Test report](<${options.reportHref}>)`,
|
|
1127
|
-
''
|
|
1128
|
-
] : []
|
|
1129
|
-
];
|
|
1130
|
-
const rows = [];
|
|
1131
|
-
const errors = [];
|
|
1132
|
-
const recordSteps = (label, steps)=>{
|
|
1133
|
-
for (const step of steps)if ('failed' === step.status) errors.push(`${label} / ${step.node}: ${step.error?.message ?? 'Step failed'}`);
|
|
1134
|
-
};
|
|
1135
|
-
for (const project of dump.projects){
|
|
1136
|
-
if (project.lifecycle?.setupError) errors.push(`${project.name}: ${project.lifecycle.setupError.message}`);
|
|
1137
|
-
for (const error of project.lifecycle?.teardownErrors ?? [])errors.push(`${project.name}: ${error.message}`);
|
|
1138
|
-
for (const error of project.collectionErrors)errors.push(`${project.name} / ${error.sourcePath}: ${error.error.message}`);
|
|
1139
|
-
for (const document of project.documents){
|
|
1140
|
-
recordSteps(`${project.name} / ${document.sourcePath}`, [
|
|
1141
|
-
...document.beforeAll,
|
|
1142
|
-
...document.afterAll
|
|
1143
|
-
]);
|
|
1144
|
-
for (const error of document.teardownErrors ?? [])errors.push(`${document.sourcePath}: ${error.message}`);
|
|
1145
|
-
for (const failure of document.hostErrors ?? [])errors.push(`${document.sourcePath} / ${failure.phase}: ${failure.error.message}`);
|
|
1146
|
-
for (const testCase of document.cases){
|
|
1147
|
-
const attempt = testCase.attempts.at(-1);
|
|
1148
|
-
const steps = attempt ? [
|
|
1149
|
-
...attempt.beforeEach,
|
|
1150
|
-
...attempt.steps,
|
|
1151
|
-
...attempt.afterEach
|
|
1152
|
-
] : [];
|
|
1153
|
-
const failedSteps = steps.filter((step)=>'failed' === step.status);
|
|
1154
|
-
const step = failedSteps.at(-1) ?? [
|
|
1155
|
-
...steps
|
|
1156
|
-
].reverse().find((item)=>item.agentDetails?.length) ?? steps.at(-1);
|
|
1157
|
-
const route = new URLSearchParams({
|
|
1158
|
-
'runner-page': 'case',
|
|
1159
|
-
'runner-project': project.projectId,
|
|
1160
|
-
'runner-case': testCase.caseId,
|
|
1161
|
-
...step ? {
|
|
1162
|
-
'runner-step': step.id
|
|
1163
|
-
} : {}
|
|
1164
|
-
});
|
|
1165
|
-
const target = options.reportHref ? `${options.reportHref}#${route}` : void 0;
|
|
1166
|
-
const name = target ? `[${result_markdown_text(testCase.name)}](<${target}>)` : result_markdown_text(testCase.name);
|
|
1167
|
-
rows.push({
|
|
1168
|
-
failed: 'success' !== testCase.status,
|
|
1169
|
-
row: `- ${name} — ${testCase.status}`
|
|
1170
|
-
});
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
for (const diagnostic of dump.diagnostics ?? [])if ('error' === diagnostic.level) errors.push(diagnostic.message);
|
|
1175
|
-
if (errors.length) lines.push('### Execution errors', '', ...errors.map((error)=>`- ${result_markdown_text(error)}`), '');
|
|
1176
|
-
lines.push('### Cases', '');
|
|
1177
|
-
lines.push(...rows.filter((row)=>row.failed).map((row)=>row.row));
|
|
1178
|
-
const passed = rows.filter((row)=>!row.failed);
|
|
1179
|
-
if (passed.length) lines.push(...passed.map((row)=>row.row));
|
|
1180
|
-
if (!rows.length) lines.push('', 'No cases were reported.');
|
|
1181
|
-
return `${lines.join('\n')}\n`;
|
|
1182
|
-
}
|
|
1183
|
-
const emptyIndex = ()=>({
|
|
1184
|
-
sources: [],
|
|
1185
|
-
metrics: {
|
|
1186
|
-
modelCallCount: 0,
|
|
1187
|
-
modelTimeMs: 0,
|
|
1188
|
-
promptTokens: 0,
|
|
1189
|
-
cachedInputTokens: 0,
|
|
1190
|
-
completionTokens: 0,
|
|
1191
|
-
totalTokens: 0
|
|
1192
|
-
}
|
|
1193
|
-
});
|
|
1194
|
-
async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
1195
|
-
const output = (0, external_node_path_.join)((0, external_node_path_.dirname)(result.summaryPath), 'result.md');
|
|
1196
|
-
const dump = buildTestRunReportDump(result, index);
|
|
1197
|
-
await writeFile(output, renderTestResultMarkdown(dump, {
|
|
1198
|
-
...result.reportPath ? {
|
|
1199
|
-
reportHref: href(output, result.reportPath)
|
|
1200
|
-
} : {}
|
|
1201
|
-
}));
|
|
1202
|
-
return output;
|
|
1203
|
-
}
|
|
1204
1242
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1205
1243
|
const formatStep = (info)=>{
|
|
1206
1244
|
const position = stepPosition(info);
|
|
@@ -1209,6 +1247,7 @@ const formatStep = (info)=>{
|
|
|
1209
1247
|
};
|
|
1210
1248
|
const formatStepResult = (info, result)=>{
|
|
1211
1249
|
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1250
|
+
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1212
1251
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1213
1252
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1214
1253
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
@@ -1218,11 +1257,11 @@ const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
|
1218
1257
|
...attempt.beforeEach,
|
|
1219
1258
|
...attempt.steps,
|
|
1220
1259
|
...attempt.afterEach
|
|
1221
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError));
|
|
1260
|
+
].some((step)=>step.error && isFatalDeviceError(step.error)) || (attempt.teardownErrors ?? []).some(isFatalDeviceError) || !!attempt.executionErrors?.some(isResourceRecoveryError));
|
|
1222
1261
|
const documentHasFatalError = (result)=>[
|
|
1223
1262
|
...result.beforeAll,
|
|
1224
1263
|
...result.afterAll
|
|
1225
|
-
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError);
|
|
1264
|
+
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError) || !!result.executionErrors?.some(isResourceRecoveryError);
|
|
1226
1265
|
async function executeDocumentInvocation(options) {
|
|
1227
1266
|
const { invocation, project, sinks } = options;
|
|
1228
1267
|
const { document } = invocation;
|
|
@@ -1235,6 +1274,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1235
1274
|
}, async (documentAttemptIndex)=>{
|
|
1236
1275
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1237
1276
|
const execution = await runWorkflowDocument(document, {
|
|
1277
|
+
plan: invocation.plan,
|
|
1238
1278
|
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
1239
1279
|
...invocation.bindings,
|
|
1240
1280
|
project,
|
|
@@ -1272,6 +1312,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1272
1312
|
}).catch(async (error)=>{
|
|
1273
1313
|
if (error instanceof WorkflowExecutionFailure) {
|
|
1274
1314
|
const partial = error.result;
|
|
1315
|
+
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>isResourceRecoveryError(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
1316
|
+
if (resourceOnly) {
|
|
1317
|
+
if (error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
|
|
1318
|
+
return partial;
|
|
1319
|
+
}
|
|
1320
|
+
if (documentRetry) partial.document.retryDecision = decideRetry({
|
|
1321
|
+
scope: 'document',
|
|
1322
|
+
retryCount: invocation.retry.count,
|
|
1323
|
+
attemptIndex: documentAttemptIndex,
|
|
1324
|
+
status: 'failed',
|
|
1325
|
+
infrastructureError: true
|
|
1326
|
+
});
|
|
1275
1327
|
attempts.push(partial);
|
|
1276
1328
|
sinks.documents.push(partial.document);
|
|
1277
1329
|
sinks.cases.push(...partial.cases.map((outcome)=>({
|
|
@@ -1280,6 +1332,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1280
1332
|
documentRunId: partial.document.documentRunId
|
|
1281
1333
|
})));
|
|
1282
1334
|
try {
|
|
1335
|
+
for (const outcome of partial.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1283
1336
|
await writeWorkflowDocumentResult(options.runDir, partial.document);
|
|
1284
1337
|
} catch (publicationError) {
|
|
1285
1338
|
throw new WorkflowExecutionFailure(partial, [
|
|
@@ -1290,6 +1343,29 @@ async function executeDocumentInvocation(options) {
|
|
|
1290
1343
|
}
|
|
1291
1344
|
throw error;
|
|
1292
1345
|
});
|
|
1346
|
+
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1347
|
+
const owners = [
|
|
1348
|
+
execution.document,
|
|
1349
|
+
...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
|
|
1350
|
+
];
|
|
1351
|
+
const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
|
|
1352
|
+
const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
|
|
1353
|
+
const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
|
|
1354
|
+
if (cleanupPending || cleanupFailed || operationPending) options.markProjectFatal();
|
|
1355
|
+
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1356
|
+
const decision = decideRetry({
|
|
1357
|
+
scope: 'document',
|
|
1358
|
+
retryCount: invocation.retry.count,
|
|
1359
|
+
attemptIndex: documentAttemptIndex,
|
|
1360
|
+
status: failed ? 'failed' : 'success',
|
|
1361
|
+
cleanupPending,
|
|
1362
|
+
cleanupFailed,
|
|
1363
|
+
operationPending,
|
|
1364
|
+
fatalError: options.isProjectFatal(),
|
|
1365
|
+
interrupted: options.signal.aborted || options.rootSignal.aborted,
|
|
1366
|
+
shouldStop: options.shouldBail()
|
|
1367
|
+
});
|
|
1368
|
+
if (documentRetry) execution.document.retryDecision = decision;
|
|
1293
1369
|
attempts.push(execution);
|
|
1294
1370
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1295
1371
|
...outcome,
|
|
@@ -1297,15 +1373,30 @@ async function executeDocumentInvocation(options) {
|
|
|
1297
1373
|
documentRunId: execution.document.documentRunId
|
|
1298
1374
|
})));
|
|
1299
1375
|
sinks.documents.push(execution.document);
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1376
|
+
try {
|
|
1377
|
+
for (const outcome of execution.cases)for (const attempt of outcome.attempts ?? [])await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1378
|
+
await writeWorkflowDocumentResult(options.runDir, execution.document);
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
execution.document.status = 'failed';
|
|
1381
|
+
execution.document.hostErrors = [
|
|
1382
|
+
...execution.document.hostErrors ?? [],
|
|
1383
|
+
{
|
|
1384
|
+
phase: 'publication',
|
|
1385
|
+
error
|
|
1386
|
+
}
|
|
1387
|
+
];
|
|
1388
|
+
if (documentRetry) execution.document.retryDecision = decideRetry({
|
|
1389
|
+
scope: 'document',
|
|
1390
|
+
retryCount: invocation.retry.count,
|
|
1391
|
+
attemptIndex: documentAttemptIndex,
|
|
1392
|
+
status: 'failed',
|
|
1393
|
+
infrastructureError: true
|
|
1394
|
+
});
|
|
1395
|
+
throw new WorkflowExecutionFailure(execution, [
|
|
1396
|
+
error
|
|
1397
|
+
]);
|
|
1398
|
+
}
|
|
1399
|
+
return 'retry' === decision.decision ? 'failed' : void 0;
|
|
1309
1400
|
});
|
|
1310
1401
|
const final = attempts.at(-1);
|
|
1311
1402
|
if (!final) return;
|
|
@@ -1313,6 +1404,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1313
1404
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1314
1405
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1315
1406
|
}
|
|
1407
|
+
function createOperationJournal(runDir) {
|
|
1408
|
+
const path = (0, external_node_path_.join)(runDir, 'operations.jsonl');
|
|
1409
|
+
(0, external_node_fs_.writeFileSync)(path, '', {
|
|
1410
|
+
flag: 'wx'
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
path,
|
|
1414
|
+
append (event) {
|
|
1415
|
+
(0, external_node_fs_.appendFileSync)(path, `${JSON.stringify(event)}\n`);
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1316
1419
|
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1317
1420
|
documentId,
|
|
1318
1421
|
caseId: collectedCase.caseId,
|
|
@@ -1371,6 +1474,8 @@ async function runPreparedTestProject(plan) {
|
|
|
1371
1474
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
1372
1475
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1373
1476
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrency, preparedProjects.length);
|
|
1477
|
+
await writeTestRunPlan(runDir, preparedProjects);
|
|
1478
|
+
const journal = createOperationJournal(runDir);
|
|
1374
1479
|
const rootController = new AbortController();
|
|
1375
1480
|
setMaxListeners(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
1376
1481
|
const handleSignal = (signal)=>{
|
|
@@ -1400,6 +1505,16 @@ async function runPreparedTestProject(plan) {
|
|
|
1400
1505
|
throw error;
|
|
1401
1506
|
}
|
|
1402
1507
|
};
|
|
1508
|
+
const stopObservingOperations = observeResourceOperations(rootController.signal, (event)=>{
|
|
1509
|
+
try {
|
|
1510
|
+
journal.append(event);
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
recordInfrastructureError(new WorkflowPublicationError('write-result', journal.path, error));
|
|
1513
|
+
getDebug('test-runner:operations', {
|
|
1514
|
+
console: true
|
|
1515
|
+
})(`Could not persist operation event: ${String(error)}`);
|
|
1516
|
+
}
|
|
1517
|
+
});
|
|
1403
1518
|
const announceProject = (prepared, projectIndex)=>{
|
|
1404
1519
|
const { project } = prepared;
|
|
1405
1520
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
@@ -1424,6 +1539,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1424
1539
|
lifecycle
|
|
1425
1540
|
} : {},
|
|
1426
1541
|
cases,
|
|
1542
|
+
plans: prepared.plans,
|
|
1427
1543
|
documents,
|
|
1428
1544
|
collectionErrors: prepared.collectionErrors
|
|
1429
1545
|
};
|
|
@@ -1542,6 +1658,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1542
1658
|
} catch (error) {
|
|
1543
1659
|
recordInfrastructureError(error);
|
|
1544
1660
|
} finally{
|
|
1661
|
+
stopObservingOperations();
|
|
1545
1662
|
process.off('SIGINT', sigint);
|
|
1546
1663
|
process.off('SIGTERM', sigterm);
|
|
1547
1664
|
}
|
|
@@ -1557,6 +1674,10 @@ async function runPreparedTestProject(plan) {
|
|
|
1557
1674
|
const endedAt = new Date();
|
|
1558
1675
|
const result = {
|
|
1559
1676
|
schemaVersion: 3,
|
|
1677
|
+
operationLog: {
|
|
1678
|
+
path: journal.path,
|
|
1679
|
+
snapshotAt: endedAt.toISOString()
|
|
1680
|
+
},
|
|
1560
1681
|
runId,
|
|
1561
1682
|
startedAt: startedAt.toISOString(),
|
|
1562
1683
|
endedAt: endedAt.toISOString(),
|
|
@@ -1576,7 +1697,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1576
1697
|
} : {}
|
|
1577
1698
|
};
|
|
1578
1699
|
let completedResult = result;
|
|
1579
|
-
let reportIndex;
|
|
1580
1700
|
const publish = async (operation, path, callback)=>{
|
|
1581
1701
|
try {
|
|
1582
1702
|
await callback();
|
|
@@ -1607,7 +1727,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1607
1727
|
overwrite: false,
|
|
1608
1728
|
sources: collectTestRunReportSources(completedResult),
|
|
1609
1729
|
buildRunnerDump: (index)=>{
|
|
1610
|
-
reportIndex = index;
|
|
1611
1730
|
const dump = buildTestRunReportDump(completedResult, index);
|
|
1612
1731
|
const warn = getDebug('test-runner:report-assembler', {
|
|
1613
1732
|
console: true
|
|
@@ -1625,13 +1744,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1625
1744
|
for (const publication of plan.publications)await publish(publication.operation, publication.path, async ()=>{
|
|
1626
1745
|
await publication.publish(completedResult);
|
|
1627
1746
|
});
|
|
1628
|
-
await publish('write-result', (0, external_node_path_.join)((0, external_node_path_.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
1629
|
-
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1630
|
-
completedResult = {
|
|
1631
|
-
...completedResult,
|
|
1632
|
-
markdownPath
|
|
1633
|
-
};
|
|
1634
|
-
});
|
|
1635
1747
|
await writeSummary();
|
|
1636
1748
|
if (hasInfrastructureError) throw new WorkflowExecutionFailure(completedResult, infrastructureErrors);
|
|
1637
1749
|
return completedResult;
|
|
@@ -1818,7 +1930,6 @@ const reportTestProjectResult = (result, io, infrastructureErrors = result.error
|
|
|
1818
1930
|
io.log(`midscene-test: ${result.summary.passed}/${result.summary.total} cases passed, ${result.summary.failed} failed, ${result.summary.notRun} not run`);
|
|
1819
1931
|
io.log(`Results: ${result.resultDir}`);
|
|
1820
1932
|
io.log(`Summary: ${result.summaryPath}`);
|
|
1821
|
-
if (result.markdownPath) io.log(`Markdown: ${result.markdownPath}`);
|
|
1822
1933
|
if (result.reportPath) io.log(`Report: ${result.reportPath}`);
|
|
1823
1934
|
return result.exitCode;
|
|
1824
1935
|
};
|