@midscene/test 1.16.0 → 1.16.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/{1~77.mjs → 1~731.mjs} +9 -9
- package/dist/es/1~731.mjs.map +1 -0
- package/dist/es/cli/index.mjs +686 -167
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +45 -1
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +1863 -206
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +677 -155
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +45 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/test-cli.js +1854 -203
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/templates/test/android/.env.example +1 -1
- package/dist/templates/test/android/README.md +3 -1
- package/dist/templates/test/android/README.zh.md +3 -1
- package/dist/templates/test/android/midscene.config.ts +28 -31
- package/dist/templates/test/computer/.env.example +1 -1
- package/dist/templates/test/computer/README.md +3 -1
- package/dist/templates/test/computer/README.zh.md +3 -1
- package/dist/templates/test/computer/midscene.config.ts +31 -34
- package/dist/templates/test/harmony/.env.example +1 -1
- package/dist/templates/test/harmony/README.md +3 -1
- package/dist/templates/test/harmony/README.zh.md +3 -1
- package/dist/templates/test/harmony/midscene.config.ts +28 -31
- package/dist/templates/test/ios/.env.example +1 -1
- package/dist/templates/test/ios/README.md +3 -1
- package/dist/templates/test/ios/README.zh.md +3 -1
- package/dist/templates/test/ios/midscene.config.ts +28 -31
- package/dist/templates/test/web/.env.example +1 -1
- package/dist/templates/test/web/README.md +17 -1
- package/dist/templates/test/web/README.zh.md +17 -1
- package/dist/templates/test/web/cases/todo.yaml +2 -0
- package/dist/templates/test/web/midscene.config.ts +34 -12
- package/dist/types/cli/index.d.ts +107 -11
- package/dist/types/index.d.ts +6 -3
- package/package.json +14 -13
- package/dist/es/1~77.mjs.map +0 -1
package/dist/es/cli/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
1
|
+
import { dirname, isAbsolute, join, relative, resolve as external_node_path_resolve, sep } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import { require as api_require } from "tsx/cjs/api";
|
|
4
|
-
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations,
|
|
4
|
+
import { NodeRegistry, WorkflowError, WorkflowExecutionFailure, WorkflowParseError, WorkflowPublicationError, asExecutionError, buildTestRunReportDump, buildTestRunReportPlan, classifyWorkflowDocumentFormat, collectTestRunReportSources, createProjectRuntime, decideRetry, getPendingScopeOperations, getResourceCleanupCompletion, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText, sumTestRunReportMetrics as test_runner_sumTestRunReportMetrics, trackResourceOperation, waitForScopeOperations } from "@midscene/core/internal/test-runner";
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { globSync } from "tinyglobby";
|
|
7
7
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -9,11 +9,14 @@ import { JSON_SCHEMA, load } from "js-yaml";
|
|
|
9
9
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
10
|
import { setMaxListeners } from "node:events";
|
|
11
11
|
import { TestRunReportAssembler, calculateTestRunHealth } from "@midscene/core/report";
|
|
12
|
-
import { getDebug } from "@midscene/shared/logger";
|
|
12
|
+
import { getDebug, runWithConsoleLogScope } from "@midscene/shared/logger";
|
|
13
|
+
import "node:util";
|
|
14
|
+
import { globalConfigManager } from "@midscene/shared/env";
|
|
13
15
|
const registry_NodeRegistry = NodeRegistry;
|
|
14
16
|
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
15
17
|
const defineTestProject = (definition)=>definition;
|
|
16
18
|
const defineProjectSetup = (definition)=>definition;
|
|
19
|
+
const defineFileSetup = (definition)=>definition;
|
|
17
20
|
const getDefaultExport = (loaded, absolutePath)=>{
|
|
18
21
|
if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
|
|
19
22
|
const defaultExport = loaded.default;
|
|
@@ -130,9 +133,19 @@ const validateNonNegativeInteger = (value, fallback, label)=>{
|
|
|
130
133
|
if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
|
|
131
134
|
return value;
|
|
132
135
|
};
|
|
133
|
-
const
|
|
136
|
+
const validateConcurrency = (value, label)=>{
|
|
137
|
+
if (null === value) return null;
|
|
138
|
+
return validatePositiveInteger(value, 1, label);
|
|
139
|
+
};
|
|
140
|
+
const validateFileConcurrency = (maxConcurrentFiles, fileSetup, label)=>{
|
|
141
|
+
if (maxConcurrentFiles > 1 && !fileSetup) throw new TypeError(`Midscene config ${label}.fileSetup is required when the effective maxConcurrentFiles is greater than 1.`);
|
|
142
|
+
};
|
|
143
|
+
const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, globalNodes, test)=>{
|
|
134
144
|
if (void 0 === value) {
|
|
135
145
|
const setup = validateSetup(defaultSetup, 'setup');
|
|
146
|
+
const fileSetup = validateSetup(defaultFileSetup, 'fileSetup');
|
|
147
|
+
const maxConcurrentFiles = test.maxConcurrentFiles ?? 1 / 0;
|
|
148
|
+
validateFileConcurrency(maxConcurrentFiles, fileSetup, 'default');
|
|
136
149
|
return {
|
|
137
150
|
hasExplicitProjects: false,
|
|
138
151
|
projects: Object.freeze([
|
|
@@ -142,6 +155,10 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
142
155
|
...setup ? {
|
|
143
156
|
setup
|
|
144
157
|
} : {},
|
|
158
|
+
...fileSetup ? {
|
|
159
|
+
fileSetup
|
|
160
|
+
} : {},
|
|
161
|
+
maxConcurrentFiles,
|
|
145
162
|
tags: Object.freeze({
|
|
146
163
|
include: [],
|
|
147
164
|
exclude: []
|
|
@@ -154,6 +171,7 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
154
171
|
};
|
|
155
172
|
}
|
|
156
173
|
if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
|
|
174
|
+
if (void 0 !== defaultFileSetup) throw new TypeError('Midscene config fileSetup cannot be used together with projects. Move fileSetup to projects[].fileSetup.');
|
|
157
175
|
if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
|
|
158
176
|
const names = new Set();
|
|
159
177
|
const projects = value.map((candidate, index)=>{
|
|
@@ -162,6 +180,8 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
162
180
|
rejectUnknownKeys(candidate, [
|
|
163
181
|
'name',
|
|
164
182
|
'setup',
|
|
183
|
+
'fileSetup',
|
|
184
|
+
'maxConcurrentFiles',
|
|
165
185
|
'nodes',
|
|
166
186
|
'files',
|
|
167
187
|
'tags',
|
|
@@ -178,10 +198,18 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
178
198
|
...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
|
|
179
199
|
...localNodes.definitions()
|
|
180
200
|
]);
|
|
201
|
+
const fileSetup = validateSetup(candidate.fileSetup, `${label}.fileSetup`);
|
|
202
|
+
const globalFileLimit = test.maxConcurrentFiles ?? 1 / 0;
|
|
203
|
+
const maxConcurrentFiles = Math.min(globalFileLimit, validatePositiveInteger(candidate.maxConcurrentFiles, globalFileLimit, `${label}.maxConcurrentFiles`));
|
|
204
|
+
validateFileConcurrency(maxConcurrentFiles, fileSetup, label);
|
|
181
205
|
return Object.freeze({
|
|
182
206
|
projectId: projectIdFromIndex(index),
|
|
183
207
|
name: candidate.name,
|
|
184
208
|
nodes,
|
|
209
|
+
...fileSetup ? {
|
|
210
|
+
fileSetup
|
|
211
|
+
} : {},
|
|
212
|
+
maxConcurrentFiles,
|
|
185
213
|
...files ? {
|
|
186
214
|
files
|
|
187
215
|
} : {},
|
|
@@ -198,11 +226,25 @@ const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
|
198
226
|
hasExplicitProjects: true
|
|
199
227
|
};
|
|
200
228
|
};
|
|
201
|
-
const validateTestOptions = (value)=>{
|
|
229
|
+
const validateTestOptions = (value, hasExplicitProjects)=>{
|
|
202
230
|
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
203
231
|
const candidate = value ?? {};
|
|
232
|
+
if (void 0 !== candidate.maxConcurrency) {
|
|
233
|
+
const limit = 'number' == typeof candidate.maxConcurrency && Number.isInteger(candidate.maxConcurrency) && candidate.maxConcurrency > 0 ? candidate.maxConcurrency : 1;
|
|
234
|
+
throw new TypeError([
|
|
235
|
+
'Midscene config test.maxConcurrency is no longer supported.',
|
|
236
|
+
'It controlled project concurrency, not file concurrency. Remove it and use:',
|
|
237
|
+
` test: { maxConcurrentProjects: ${limit}, maxConcurrentFiles: ${hasExplicitProjects ? limit : 1} }`,
|
|
238
|
+
hasExplicitProjects ? ' Set projects[].maxConcurrentFiles to 1 to keep files sequential within each project.' : ' The implicit default project keeps running files sequentially.',
|
|
239
|
+
hasExplicitProjects ? 'For parallel files, configure fileSetup and raise both test.maxConcurrentFiles and projects[].maxConcurrentFiles.' : 'For parallel files, configure fileSetup and raise test.maxConcurrentFiles.',
|
|
240
|
+
'Migration guide: https://midscenejs.com/midscene-test/configuration.html#existing-configurations'
|
|
241
|
+
].join('\n'));
|
|
242
|
+
}
|
|
204
243
|
return Object.freeze({
|
|
205
|
-
|
|
244
|
+
maxConcurrentFiles: validateConcurrency(candidate.maxConcurrentFiles, 'test.maxConcurrentFiles'),
|
|
245
|
+
maxConcurrentProjects: validateConcurrency(candidate.maxConcurrentProjects, 'test.maxConcurrentProjects'),
|
|
246
|
+
setupTimeout: validatePositiveInteger(candidate.setupTimeout, 120000, 'test.setupTimeout'),
|
|
247
|
+
teardownTimeout: validatePositiveInteger(candidate.teardownTimeout, 30000, 'test.teardownTimeout'),
|
|
206
248
|
bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
|
|
207
249
|
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
|
|
208
250
|
});
|
|
@@ -232,16 +274,18 @@ const validateTestProjectDefinition = (definition)=>{
|
|
|
232
274
|
if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
|
|
233
275
|
rejectUnknownKeys(definition, [
|
|
234
276
|
'setup',
|
|
277
|
+
'fileSetup',
|
|
235
278
|
'projects',
|
|
236
279
|
'test',
|
|
237
280
|
'output',
|
|
238
281
|
'nodes'
|
|
239
282
|
], 'root');
|
|
240
283
|
const nodes = new registry_NodeRegistry(definition.nodes);
|
|
241
|
-
const
|
|
284
|
+
const test = validateTestOptions(definition.test, void 0 !== definition.projects);
|
|
285
|
+
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, definition.fileSetup, nodes, test);
|
|
242
286
|
return {
|
|
243
287
|
...resolvedProjects,
|
|
244
|
-
test
|
|
288
|
+
test,
|
|
245
289
|
output: validateOutput(definition.output),
|
|
246
290
|
nodes,
|
|
247
291
|
resolveNode: (name)=>nodes.get(name)
|
|
@@ -263,7 +307,7 @@ async function loadTestProject(configPath) {
|
|
|
263
307
|
if (!configPath) return validateTestProjectDefinition({
|
|
264
308
|
nodes: []
|
|
265
309
|
});
|
|
266
|
-
const absolutePath =
|
|
310
|
+
const absolutePath = external_node_path_resolve(configPath);
|
|
267
311
|
const extension = assertSupportedConfig(absolutePath);
|
|
268
312
|
let loaded;
|
|
269
313
|
try {
|
|
@@ -318,11 +362,13 @@ function parseWorkflowSource(source) {
|
|
|
318
362
|
start: state.position + sourceOffset,
|
|
319
363
|
end: 0,
|
|
320
364
|
value: null,
|
|
321
|
-
children: []
|
|
365
|
+
children: [],
|
|
366
|
+
kind: null
|
|
322
367
|
});
|
|
323
368
|
const node = stack.pop();
|
|
324
369
|
node.end = state.position + sourceOffset;
|
|
325
370
|
node.value = state.result;
|
|
371
|
+
node.kind = state.kind;
|
|
326
372
|
stack.at(-1)?.children.push(node);
|
|
327
373
|
nodes.push(node);
|
|
328
374
|
if ('mapping' !== state.kind) return;
|
|
@@ -352,6 +398,36 @@ function parseWorkflowSource(source) {
|
|
|
352
398
|
const value = sanitizeReportText(redacted, 65536);
|
|
353
399
|
return {
|
|
354
400
|
parsed,
|
|
401
|
+
locate (path) {
|
|
402
|
+
let node = nodes.at(-1);
|
|
403
|
+
for (const part of path){
|
|
404
|
+
if (!node) return;
|
|
405
|
+
while(1 === node.children.length && node.children[0].value === node.value)node = node.children[0];
|
|
406
|
+
if ('number' == typeof part && 'sequence' === node.kind) node = node.children[part];
|
|
407
|
+
else {
|
|
408
|
+
if ('string' != typeof part || 'mapping' !== node.kind) return;
|
|
409
|
+
const children = node.children;
|
|
410
|
+
node = void 0;
|
|
411
|
+
for(let index = 0; index < children.length; index += 2)if (children[index].value === part) {
|
|
412
|
+
node = children[index + 1];
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (!node) return;
|
|
418
|
+
let start = node.start;
|
|
419
|
+
while(start < node.end)if (/\s/.test(source[start])) start++;
|
|
420
|
+
else if ('#' === source[start]) {
|
|
421
|
+
const end = source.indexOf('\n', start);
|
|
422
|
+
if (end < 0) return;
|
|
423
|
+
start = end + 1;
|
|
424
|
+
} else break;
|
|
425
|
+
const prefix = source.slice(0, start).split(/\r\n|\r|\n/);
|
|
426
|
+
return {
|
|
427
|
+
line: prefix.length,
|
|
428
|
+
column: prefix.at(-1).length + 1
|
|
429
|
+
};
|
|
430
|
+
},
|
|
355
431
|
sourceYaml: {
|
|
356
432
|
value,
|
|
357
433
|
...redactedPaths.size ? {
|
|
@@ -464,6 +540,18 @@ function collectWorkflowDocument(source, options) {
|
|
|
464
540
|
const sourcePath = source.sourcePath.split(sep).join('/');
|
|
465
541
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
466
542
|
...step,
|
|
543
|
+
meta: {
|
|
544
|
+
...step.meta,
|
|
545
|
+
sourceLocation: snapshot.locate('steps' === phase ? [
|
|
546
|
+
'cases',
|
|
547
|
+
caseIndex,
|
|
548
|
+
'steps',
|
|
549
|
+
stepIndex
|
|
550
|
+
] : [
|
|
551
|
+
phase,
|
|
552
|
+
stepIndex
|
|
553
|
+
])
|
|
554
|
+
},
|
|
467
555
|
templateInput: step.input,
|
|
468
556
|
input: resolveWorkflowVariables(step.input, {
|
|
469
557
|
variables: options.variables,
|
|
@@ -638,6 +726,9 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
638
726
|
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
639
727
|
} : {},
|
|
640
728
|
summary: result.summary,
|
|
729
|
+
...result.metrics ? {
|
|
730
|
+
metrics: result.metrics
|
|
731
|
+
} : {},
|
|
641
732
|
projects: result.projects.map((project)=>({
|
|
642
733
|
projectId: project.projectId,
|
|
643
734
|
name: project.name,
|
|
@@ -759,7 +850,7 @@ const DEFAULT_TEST_FILE_SELECTION = {
|
|
|
759
850
|
};
|
|
760
851
|
const project_preparation_toPosix = (value)=>value.split(sep).join('/');
|
|
761
852
|
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
762
|
-
const root =
|
|
853
|
+
const root = external_node_path_resolve(projectRoot);
|
|
763
854
|
const match = (patterns)=>globSync(patterns, {
|
|
764
855
|
absolute: true,
|
|
765
856
|
caseSensitiveMatch: false,
|
|
@@ -775,7 +866,7 @@ const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
|
775
866
|
}).filter((file)=>/\.ya?ml$/i.test(file));
|
|
776
867
|
const files = match(selection.include);
|
|
777
868
|
return [
|
|
778
|
-
...new Set(files.map((file)=>
|
|
869
|
+
...new Set(files.map((file)=>external_node_path_resolve(file)))
|
|
779
870
|
].sort((a, b)=>{
|
|
780
871
|
const relativeA = project_preparation_toPosix(relative(root, a));
|
|
781
872
|
const relativeB = project_preparation_toPosix(relative(root, b));
|
|
@@ -788,7 +879,7 @@ const discoverTestFiles = (projectRoot, selection = DEFAULT_TEST_FILE_SELECTION)
|
|
|
788
879
|
return discoverResolvedTestFiles(projectRoot, normalized);
|
|
789
880
|
};
|
|
790
881
|
const discoverTestConfig = (projectRoot)=>{
|
|
791
|
-
const root =
|
|
882
|
+
const root = external_node_path_resolve(projectRoot);
|
|
792
883
|
const candidates = readdirSync(root).filter((name)=>name.startsWith(CONFIG_PREFIX)).sort();
|
|
793
884
|
const supported = candidates.filter((name)=>CONFIG_NAMES.includes(name));
|
|
794
885
|
const unsupported = candidates.filter((name)=>!CONFIG_NAMES.includes(name));
|
|
@@ -842,7 +933,7 @@ const selectProjects = (projects, names)=>{
|
|
|
842
933
|
async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, options = {}) {
|
|
843
934
|
const platform = options.platform?.trim();
|
|
844
935
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
845
|
-
const prerequisiteFile = options.prerequisiteFile ?
|
|
936
|
+
const prerequisiteFile = options.prerequisiteFile ? external_node_path_resolve(options.prerequisiteFile) : void 0;
|
|
846
937
|
const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
847
938
|
const files = [
|
|
848
939
|
...prerequisiteFile ? [
|
|
@@ -920,7 +1011,24 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
920
1011
|
defaultTimeoutMs,
|
|
921
1012
|
reportEnabled: true,
|
|
922
1013
|
bindings: {
|
|
923
|
-
resolveNode: project.nodes.require.bind(project.nodes)
|
|
1014
|
+
resolveNode: project.nodes.require.bind(project.nodes),
|
|
1015
|
+
...project.fileSetup ? {
|
|
1016
|
+
documentSetup: {
|
|
1017
|
+
name: project.fileSetup.name,
|
|
1018
|
+
setup ({ projectContext, document, signal, onTeardown }) {
|
|
1019
|
+
return project.fileSetup.setup({
|
|
1020
|
+
project,
|
|
1021
|
+
projectContext,
|
|
1022
|
+
file: document,
|
|
1023
|
+
env: Object.freeze({
|
|
1024
|
+
...process.env
|
|
1025
|
+
}),
|
|
1026
|
+
signal,
|
|
1027
|
+
onTeardown
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
} : {}
|
|
924
1032
|
}
|
|
925
1033
|
});
|
|
926
1034
|
} catch (error) {
|
|
@@ -966,9 +1074,9 @@ const assertDirectory = (path, label)=>{
|
|
|
966
1074
|
if (!existsSync(path) || !statSync(path).isDirectory()) throw new Error(`${label} does not exist or is not a directory: ${path}`);
|
|
967
1075
|
};
|
|
968
1076
|
const resolveRunInput = (options)=>{
|
|
969
|
-
const cwd =
|
|
1077
|
+
const cwd = external_node_path_resolve(options.cwd ?? process.cwd());
|
|
970
1078
|
assertDirectory(cwd, 'Test working directory');
|
|
971
|
-
const inputPath = options.projectRoot ?
|
|
1079
|
+
const inputPath = options.projectRoot ? external_node_path_resolve(cwd, options.projectRoot) : void 0;
|
|
972
1080
|
const singleFile = inputPath && existsSync(inputPath) && statSync(inputPath).isFile() ? inputPath : void 0;
|
|
973
1081
|
if (singleFile && !/\.ya?ml$/i.test(singleFile)) throw new Error(`Test input must be a YAML file or project directory: ${singleFile}`);
|
|
974
1082
|
const cliProjectRoot = singleFile ? dirname(singleFile) : inputPath;
|
|
@@ -987,14 +1095,14 @@ async function prepareTestRun(options) {
|
|
|
987
1095
|
const startedAt = new Date();
|
|
988
1096
|
const runId = createTestRunId(startedAt);
|
|
989
1097
|
const input = resolveRunInput(options);
|
|
990
|
-
const configPath = options.configPath ?
|
|
1098
|
+
const configPath = options.configPath ? external_node_path_resolve(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
991
1099
|
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}\`.`);
|
|
992
1100
|
if (options.configPath && !existsSync(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
993
1101
|
const definition = await loadTestProject(configPath);
|
|
994
|
-
const resultDir = options.resultDir ?
|
|
1102
|
+
const resultDir = options.resultDir ? external_node_path_resolve(input.cwd, options.resultDir) : join(input.projectRoot, '.midscene', 'test-results');
|
|
995
1103
|
const runDir = join(resultDir, runId);
|
|
996
1104
|
const summaryPath = join(runDir, 'summary.json');
|
|
997
|
-
const reportDir =
|
|
1105
|
+
const reportDir = external_node_path_resolve(input.projectRoot, definition.output.reportDir);
|
|
998
1106
|
mkdirSync(resultDir, {
|
|
999
1107
|
recursive: true
|
|
1000
1108
|
});
|
|
@@ -1022,7 +1130,8 @@ async function prepareTestRun(options) {
|
|
|
1022
1130
|
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1023
1131
|
reportEnabled: true,
|
|
1024
1132
|
publications: [],
|
|
1025
|
-
onProgress: options.onProgress ?? (()=>{})
|
|
1133
|
+
onProgress: options.onProgress ?? (()=>{}),
|
|
1134
|
+
onEvent: options.onEvent
|
|
1026
1135
|
};
|
|
1027
1136
|
}
|
|
1028
1137
|
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
@@ -1100,6 +1209,38 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1100
1209
|
}));
|
|
1101
1210
|
return output;
|
|
1102
1211
|
}
|
|
1212
|
+
function scopeNodeConsole(node) {
|
|
1213
|
+
return {
|
|
1214
|
+
...node,
|
|
1215
|
+
execute (ctx) {
|
|
1216
|
+
const position = 'case' === ctx.scope ? ctx.case : ctx.document;
|
|
1217
|
+
const scope = {
|
|
1218
|
+
projectName: position.projectName,
|
|
1219
|
+
sourcePath: position.sourcePath,
|
|
1220
|
+
...'case' === ctx.scope ? {
|
|
1221
|
+
caseId: ctx.case.caseId,
|
|
1222
|
+
caseName: ctx.case.name,
|
|
1223
|
+
runId: ctx.case.runId
|
|
1224
|
+
} : {
|
|
1225
|
+
runId: ctx.document.documentRunId
|
|
1226
|
+
},
|
|
1227
|
+
phase: position.phase,
|
|
1228
|
+
stepIndex: position.stepIndex,
|
|
1229
|
+
node: node.name
|
|
1230
|
+
};
|
|
1231
|
+
return runWithConsoleLogScope(scope, ()=>node.execute({
|
|
1232
|
+
...ctx,
|
|
1233
|
+
onTeardown (teardown) {
|
|
1234
|
+
ctx.onTeardown(()=>runWithConsoleLogScope({
|
|
1235
|
+
...scope,
|
|
1236
|
+
phase: 'teardown',
|
|
1237
|
+
stepIndex: void 0
|
|
1238
|
+
}, teardown));
|
|
1239
|
+
}
|
|
1240
|
+
}));
|
|
1241
|
+
}
|
|
1242
|
+
};
|
|
1243
|
+
}
|
|
1103
1244
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1104
1245
|
const formatStep = (info)=>{
|
|
1105
1246
|
const position = stepPosition(info);
|
|
@@ -1123,59 +1264,149 @@ const documentHasFatalError = (result)=>[
|
|
|
1123
1264
|
...result.beforeAll,
|
|
1124
1265
|
...result.afterAll
|
|
1125
1266
|
].some((step)=>step.error && isFatalDeviceError(step.error)) || (result.teardownErrors ?? []).some(isFatalDeviceError) || !!result.executionErrors?.some(isResourceRecoveryError);
|
|
1267
|
+
const isSharedResourceFailure = (error)=>{
|
|
1268
|
+
const seen = new Set();
|
|
1269
|
+
const pending = [
|
|
1270
|
+
error
|
|
1271
|
+
];
|
|
1272
|
+
while(pending.length && seen.size < 50){
|
|
1273
|
+
const item = pending.pop();
|
|
1274
|
+
if ('object' != typeof item || null === item || seen.has(item)) continue;
|
|
1275
|
+
seen.add(item);
|
|
1276
|
+
const failure = item;
|
|
1277
|
+
if ('PROJECT_RESOURCE_ERROR' === failure.code) return true;
|
|
1278
|
+
if (failure.cause) pending.push(failure.cause);
|
|
1279
|
+
if (Array.isArray(failure.errors)) pending.push(...failure.errors.slice(0, 50));
|
|
1280
|
+
}
|
|
1281
|
+
return false;
|
|
1282
|
+
};
|
|
1126
1283
|
async function executeDocumentInvocation(options) {
|
|
1127
1284
|
const { invocation, project, sinks } = options;
|
|
1285
|
+
const isProjectResourceFailure = options.isProjectResourceFailure ?? (()=>false);
|
|
1128
1286
|
const { document } = invocation;
|
|
1129
1287
|
const documentRetry = 'document' === invocation.retry.scope;
|
|
1130
1288
|
const attempts = [];
|
|
1289
|
+
const fileSetup = invocation.bindings.documentSetup;
|
|
1290
|
+
const documentSetup = fileSetup ? {
|
|
1291
|
+
...fileSetup,
|
|
1292
|
+
setup (ctx) {
|
|
1293
|
+
const scope = {
|
|
1294
|
+
projectName: project.name,
|
|
1295
|
+
sourcePath: document.sourcePath,
|
|
1296
|
+
runId: ctx.document.documentRunId,
|
|
1297
|
+
phase: 'fileSetup'
|
|
1298
|
+
};
|
|
1299
|
+
return runWithConsoleLogScope(scope, async ()=>{
|
|
1300
|
+
try {
|
|
1301
|
+
return await fileSetup.setup({
|
|
1302
|
+
...ctx,
|
|
1303
|
+
onTeardown (teardown) {
|
|
1304
|
+
ctx.onTeardown(()=>runWithConsoleLogScope({
|
|
1305
|
+
...scope,
|
|
1306
|
+
phase: 'teardown'
|
|
1307
|
+
}, teardown));
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
if (isProjectResourceFailure(error)) options.markProjectFatal();
|
|
1312
|
+
throw error;
|
|
1313
|
+
}
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
} : void 0;
|
|
1317
|
+
options.onEvent?.({
|
|
1318
|
+
type: 'document-start',
|
|
1319
|
+
documentId: document.documentId,
|
|
1320
|
+
projectName: project.name,
|
|
1321
|
+
sourcePath: document.sourcePath,
|
|
1322
|
+
cases: document.cases.length
|
|
1323
|
+
});
|
|
1131
1324
|
await runDocumentAttempts({
|
|
1132
1325
|
retry: documentRetry ? invocation.retry.count : 0,
|
|
1133
1326
|
signal: options.signal,
|
|
1134
1327
|
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1135
1328
|
}, async (documentAttemptIndex)=>{
|
|
1136
1329
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1137
|
-
const execution = await
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
options.
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1330
|
+
const execution = await runWithConsoleLogScope({
|
|
1331
|
+
projectName: project.name,
|
|
1332
|
+
sourcePath: document.sourcePath
|
|
1333
|
+
}, ()=>runWorkflowDocument(document, {
|
|
1334
|
+
plan: invocation.plan,
|
|
1335
|
+
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
1336
|
+
...invocation.bindings,
|
|
1337
|
+
resolveNode: (name)=>scopeNodeConsole(invocation.bindings.resolveNode(name)),
|
|
1338
|
+
documentSetup,
|
|
1339
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
1340
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
1341
|
+
project,
|
|
1342
|
+
projectContext: options.projectContext,
|
|
1343
|
+
retry: documentRetry ? 0 : invocation.retry.count,
|
|
1344
|
+
signal: options.signal,
|
|
1345
|
+
defaultTimeoutMs: invocation.defaultTimeoutMs,
|
|
1346
|
+
shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
|
|
1347
|
+
stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
|
|
1348
|
+
isFatalError: (run)=>[
|
|
1349
|
+
...run.beforeEach,
|
|
1350
|
+
...run.steps,
|
|
1351
|
+
...run.afterEach
|
|
1352
|
+
].some((step)=>step.error && (isFatalDeviceError(step.error) || isProjectResourceFailure(step.error))) || (run.teardownErrors ?? []).some((error)=>isFatalDeviceError(error) || isProjectResourceFailure(error)),
|
|
1353
|
+
onCaseStart: (collectedCase)=>{
|
|
1354
|
+
options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
1355
|
+
},
|
|
1356
|
+
onStepStart: (info)=>{
|
|
1357
|
+
options.onEvent?.({
|
|
1358
|
+
type: 'step-start',
|
|
1359
|
+
projectName: project.name,
|
|
1360
|
+
info
|
|
1361
|
+
});
|
|
1362
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1363
|
+
options.onProgress(`${indent}→ ${formatStep(info)}`);
|
|
1364
|
+
},
|
|
1365
|
+
onStepResult: async (info, result)=>{
|
|
1366
|
+
if (isProjectResourceFailure(result.error)) options.markProjectFatal();
|
|
1367
|
+
await invocation.bindings.onStepResult?.(info, result);
|
|
1368
|
+
options.onProgress(formatStepResult(info, result));
|
|
1369
|
+
options.onEvent?.({
|
|
1370
|
+
type: 'step-end',
|
|
1371
|
+
projectName: project.name,
|
|
1372
|
+
info,
|
|
1373
|
+
result
|
|
1374
|
+
});
|
|
1375
|
+
},
|
|
1376
|
+
onCaseResult: async (attempt)=>{
|
|
1377
|
+
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
1378
|
+
options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} case attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
1379
|
+
if (!documentRetry) options.onEvent?.({
|
|
1380
|
+
type: 'case-attempt-end',
|
|
1381
|
+
attempt,
|
|
1382
|
+
maxAttempts: invocation.retry.count + 1
|
|
1383
|
+
});
|
|
1384
|
+
},
|
|
1385
|
+
onCaseOutcome: (outcome)=>{
|
|
1386
|
+
if (documentRetry) options.onEvent?.({
|
|
1387
|
+
type: 'case-progress',
|
|
1388
|
+
outcome,
|
|
1389
|
+
documentAttemptIndex
|
|
1390
|
+
});
|
|
1391
|
+
else options.onEvent?.({
|
|
1392
|
+
type: 'case-end',
|
|
1393
|
+
outcome
|
|
1394
|
+
});
|
|
1395
|
+
if (outcome.attempts?.some((attempt)=>[
|
|
1396
|
+
...attempt.beforeEach,
|
|
1397
|
+
...attempt.steps,
|
|
1398
|
+
...attempt.afterEach
|
|
1399
|
+
].some((step)=>isProjectResourceFailure(step.error)) || attempt.teardownErrors?.some(isProjectResourceFailure))) options.markProjectFatal();
|
|
1400
|
+
if (!options.isolatedContext && caseHasFatalError(outcome)) options.markProjectFatal();
|
|
1401
|
+
if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
|
|
1402
|
+
},
|
|
1403
|
+
onDocumentResult: options.onDocumentResult
|
|
1404
|
+
})).catch(async (error)=>{
|
|
1174
1405
|
if (error instanceof WorkflowExecutionFailure) {
|
|
1175
1406
|
const partial = error.result;
|
|
1176
1407
|
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)));
|
|
1177
1408
|
if (resourceOnly) {
|
|
1178
|
-
if (error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
|
|
1409
|
+
if (!options.isolatedContext && error.errors.some(isResourceRecoveryError)) options.markProjectFatal();
|
|
1179
1410
|
return partial;
|
|
1180
1411
|
}
|
|
1181
1412
|
if (documentRetry) partial.document.retryDecision = decideRetry({
|
|
@@ -1204,7 +1435,17 @@ async function executeDocumentInvocation(options) {
|
|
|
1204
1435
|
}
|
|
1205
1436
|
throw error;
|
|
1206
1437
|
});
|
|
1207
|
-
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1438
|
+
if (!options.isolatedContext && documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
1439
|
+
if ([
|
|
1440
|
+
...execution.document.beforeAll,
|
|
1441
|
+
...execution.document.afterAll,
|
|
1442
|
+
...(execution.document.hostErrors ?? []).map(({ error })=>({
|
|
1443
|
+
error
|
|
1444
|
+
})),
|
|
1445
|
+
...(execution.document.teardownErrors ?? []).map((error)=>({
|
|
1446
|
+
error
|
|
1447
|
+
}))
|
|
1448
|
+
].some(({ error })=>isProjectResourceFailure(error))) options.markProjectFatal();
|
|
1208
1449
|
const owners = [
|
|
1209
1450
|
execution.document,
|
|
1210
1451
|
...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
|
|
@@ -1212,7 +1453,10 @@ async function executeDocumentInvocation(options) {
|
|
|
1212
1453
|
const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
|
|
1213
1454
|
const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
|
|
1214
1455
|
const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
|
|
1215
|
-
if (cleanupPending ||
|
|
1456
|
+
if (cleanupPending || operationPending) {
|
|
1457
|
+
options.markProjectFatal();
|
|
1458
|
+
options.onUnresolvedResources?.();
|
|
1459
|
+
} else if (cleanupFailed && !options.isolatedContext) options.markProjectFatal();
|
|
1216
1460
|
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1217
1461
|
const decision = decideRetry({
|
|
1218
1462
|
scope: 'document',
|
|
@@ -1227,6 +1471,11 @@ async function executeDocumentInvocation(options) {
|
|
|
1227
1471
|
shouldStop: options.shouldBail()
|
|
1228
1472
|
});
|
|
1229
1473
|
if (documentRetry) execution.document.retryDecision = decision;
|
|
1474
|
+
if (documentRetry && 'retry' === decision.decision) options.onEvent?.({
|
|
1475
|
+
type: 'document-retry',
|
|
1476
|
+
document: execution.document,
|
|
1477
|
+
maxAttempts: invocation.retry.count + 1
|
|
1478
|
+
});
|
|
1230
1479
|
attempts.push(execution);
|
|
1231
1480
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1232
1481
|
...outcome,
|
|
@@ -1261,10 +1510,310 @@ async function executeDocumentInvocation(options) {
|
|
|
1261
1510
|
});
|
|
1262
1511
|
const final = attempts.at(-1);
|
|
1263
1512
|
if (!final) return;
|
|
1513
|
+
if (documentRetry) for (const outcome of final.cases)options.onEvent?.({
|
|
1514
|
+
type: 'case-end',
|
|
1515
|
+
outcome,
|
|
1516
|
+
documentRetries: attempts.length - 1
|
|
1517
|
+
});
|
|
1264
1518
|
const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
|
|
1519
|
+
options.onEvent?.({
|
|
1520
|
+
type: 'document-end',
|
|
1521
|
+
document: final.document,
|
|
1522
|
+
cases: final.cases,
|
|
1523
|
+
durationMs: attempts.reduce((total, attempt)=>total + attempt.document.durationMs, 0)
|
|
1524
|
+
});
|
|
1265
1525
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1266
1526
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1267
1527
|
}
|
|
1528
|
+
async function runPreparedFiles(options) {
|
|
1529
|
+
const states = options.projects.map((prepared)=>({
|
|
1530
|
+
prepared,
|
|
1531
|
+
cases: [],
|
|
1532
|
+
documents: [],
|
|
1533
|
+
cursor: 0,
|
|
1534
|
+
active: 0,
|
|
1535
|
+
initializing: false,
|
|
1536
|
+
finished: false,
|
|
1537
|
+
fatal: false,
|
|
1538
|
+
setupFailed: false
|
|
1539
|
+
}));
|
|
1540
|
+
let nextProject = 0;
|
|
1541
|
+
let resourcesUnresolved = false;
|
|
1542
|
+
let occupied = 0;
|
|
1543
|
+
let occupiedProjects = 0;
|
|
1544
|
+
const cleanupQueue = [];
|
|
1545
|
+
const release = ()=>{
|
|
1546
|
+
occupied--;
|
|
1547
|
+
while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
|
|
1548
|
+
occupied++;
|
|
1549
|
+
cleanupQueue.shift()();
|
|
1550
|
+
}
|
|
1551
|
+
wake?.();
|
|
1552
|
+
};
|
|
1553
|
+
let wake;
|
|
1554
|
+
const running = new Set();
|
|
1555
|
+
const stopped = (state)=>options.signal.aborted || options.shouldBail() || state.fatal;
|
|
1556
|
+
const invalid = (state)=>options.preflightFailed || state.prepared.collectionErrors.length > 0;
|
|
1557
|
+
const unresolved = ()=>{
|
|
1558
|
+
if (resourcesUnresolved) return;
|
|
1559
|
+
resourcesUnresolved = true;
|
|
1560
|
+
options.onInfrastructureError(new WorkflowError('Resources are still running or cleanup is pending. No new test files will start.', {
|
|
1561
|
+
code: 'UNRESOLVED_FILE_RESOURCES'
|
|
1562
|
+
}));
|
|
1563
|
+
};
|
|
1564
|
+
const finalizeProject = (state)=>{
|
|
1565
|
+
const { prepared } = state;
|
|
1566
|
+
const completed = new Set(state.cases.map((item)=>item.caseId));
|
|
1567
|
+
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
1568
|
+
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
1569
|
+
documentId: document.documentId,
|
|
1570
|
+
caseId: item.caseId,
|
|
1571
|
+
projectName: prepared.project.name,
|
|
1572
|
+
name: item.definition.name,
|
|
1573
|
+
sourcePath: item.sourcePath,
|
|
1574
|
+
caseIndex: item.caseIndex,
|
|
1575
|
+
status: 'not-run',
|
|
1576
|
+
notRunReason: reason
|
|
1577
|
+
});
|
|
1578
|
+
const order = new Map(prepared.documents.map((document, i)=>[
|
|
1579
|
+
document.documentId,
|
|
1580
|
+
i
|
|
1581
|
+
]));
|
|
1582
|
+
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1583
|
+
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1584
|
+
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
1585
|
+
};
|
|
1586
|
+
const finishProject = async (state)=>{
|
|
1587
|
+
if (!state.runtime || state.finished) return;
|
|
1588
|
+
state.finished = true;
|
|
1589
|
+
try {
|
|
1590
|
+
options.onEvent?.({
|
|
1591
|
+
type: 'project-phase',
|
|
1592
|
+
projectName: state.prepared.project.name,
|
|
1593
|
+
phase: 'teardown'
|
|
1594
|
+
});
|
|
1595
|
+
} catch (error) {
|
|
1596
|
+
options.onInfrastructureError(error);
|
|
1597
|
+
}
|
|
1598
|
+
const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
|
|
1599
|
+
state.lifecycle = await runWithConsoleLogScope({
|
|
1600
|
+
projectName: state.prepared.project.name,
|
|
1601
|
+
phase: 'teardown'
|
|
1602
|
+
}, ()=>state.runtime.finish(failed ? 'failed' : 'success'));
|
|
1603
|
+
const releaseProject = ()=>{
|
|
1604
|
+
occupiedProjects--;
|
|
1605
|
+
wake?.();
|
|
1606
|
+
};
|
|
1607
|
+
if (state.lifecycle.cleanup?.status === 'pending' || state.lifecycle.pendingOperations?.length) {
|
|
1608
|
+
unresolved();
|
|
1609
|
+
Promise.all([
|
|
1610
|
+
waitForScopeOperations(state.runtime.signal),
|
|
1611
|
+
getResourceCleanupCompletion(state.runtime.signal)
|
|
1612
|
+
]).then(releaseProject, (error)=>{
|
|
1613
|
+
options.onInfrastructureError(error);
|
|
1614
|
+
releaseProject();
|
|
1615
|
+
});
|
|
1616
|
+
} else {
|
|
1617
|
+
releaseProject();
|
|
1618
|
+
finalizeProject(state);
|
|
1619
|
+
options.onProjectEnd?.(state);
|
|
1620
|
+
}
|
|
1621
|
+
};
|
|
1622
|
+
const admit = ()=>{
|
|
1623
|
+
for(let offset = 0; offset < states.length; offset++){
|
|
1624
|
+
const index = (nextProject + offset) % states.length;
|
|
1625
|
+
const state = states[index];
|
|
1626
|
+
if (invalid(state) || state.finished || state.initializing) continue;
|
|
1627
|
+
if (!state.runtime && occupiedProjects >= options.maxConcurrentProjects) continue;
|
|
1628
|
+
const cleanupOnly = !!state.runtime && 0 === state.active && (stopped(state) || state.cursor === state.prepared.invocations.length);
|
|
1629
|
+
if (!cleanupOnly && (stopped(state) || state.cursor >= state.prepared.invocations.length || state.active >= (state.prepared.project.maxConcurrentFiles ?? options.maxConcurrentFiles))) continue;
|
|
1630
|
+
nextProject = (index + 1) % states.length;
|
|
1631
|
+
occupied++;
|
|
1632
|
+
state.active++;
|
|
1633
|
+
if (!state.runtime && !cleanupOnly) {
|
|
1634
|
+
occupiedProjects++;
|
|
1635
|
+
state.initializing = true;
|
|
1636
|
+
}
|
|
1637
|
+
const invocationIndex = cleanupOnly ? -1 : state.cursor++;
|
|
1638
|
+
return {
|
|
1639
|
+
state,
|
|
1640
|
+
index,
|
|
1641
|
+
invocationIndex
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
};
|
|
1645
|
+
const run = async ({ state, index, invocationIndex })=>{
|
|
1646
|
+
const { prepared } = state;
|
|
1647
|
+
const { project } = prepared;
|
|
1648
|
+
let fileResourcesPending = false;
|
|
1649
|
+
try {
|
|
1650
|
+
if (invocationIndex < 0) return void await finishProject(state);
|
|
1651
|
+
if (!state.runtime) {
|
|
1652
|
+
state.runtime = createProjectRuntime({
|
|
1653
|
+
project,
|
|
1654
|
+
setup: project.setup,
|
|
1655
|
+
signal: options.signal,
|
|
1656
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
1657
|
+
teardownTimeoutMs: options.teardownTimeoutMs
|
|
1658
|
+
});
|
|
1659
|
+
setMaxListeners(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
|
|
1660
|
+
options.announceProject(prepared, index);
|
|
1661
|
+
options.onEvent?.({
|
|
1662
|
+
type: 'project-phase',
|
|
1663
|
+
projectName: project.name,
|
|
1664
|
+
phase: 'setup'
|
|
1665
|
+
});
|
|
1666
|
+
await runWithConsoleLogScope({
|
|
1667
|
+
projectName: project.name,
|
|
1668
|
+
phase: 'setup'
|
|
1669
|
+
}, ()=>state.runtime.start());
|
|
1670
|
+
options.onEvent?.({
|
|
1671
|
+
type: 'project-phase',
|
|
1672
|
+
projectName: project.name,
|
|
1673
|
+
phase: 'running'
|
|
1674
|
+
});
|
|
1675
|
+
state.initializing = false;
|
|
1676
|
+
if (!state.runtime.canRun) {
|
|
1677
|
+
state.fatal = true;
|
|
1678
|
+
state.setupFailed = true;
|
|
1679
|
+
}
|
|
1680
|
+
wake?.();
|
|
1681
|
+
}
|
|
1682
|
+
if (stopped(state)) return;
|
|
1683
|
+
const invocation = prepared.invocations[invocationIndex];
|
|
1684
|
+
options.onProgress(prepared, ` [document ${invocationIndex + 1}/${prepared.invocations.length}] ${invocation.document.sourcePath}`);
|
|
1685
|
+
await executeDocumentInvocation({
|
|
1686
|
+
invocation,
|
|
1687
|
+
project,
|
|
1688
|
+
projectContext: state.runtime.context,
|
|
1689
|
+
runDir: options.runDir,
|
|
1690
|
+
signal: state.runtime.signal,
|
|
1691
|
+
rootSignal: options.signal,
|
|
1692
|
+
shouldBail: options.shouldBail,
|
|
1693
|
+
isProjectFatal: ()=>state.fatal,
|
|
1694
|
+
markProjectFatal: ()=>{
|
|
1695
|
+
if (state.fatal) return;
|
|
1696
|
+
state.fatal = true;
|
|
1697
|
+
state.runtime?.abort(new WorkflowError(`Project "${project.name}" shared execution environment is unavailable.`, {
|
|
1698
|
+
code: 'PROJECT_RESOURCE_ERROR'
|
|
1699
|
+
}));
|
|
1700
|
+
},
|
|
1701
|
+
addFailedCases: options.addFailedCases,
|
|
1702
|
+
onProgress: (message)=>options.onProgress(prepared, options.maxConcurrentFiles > 1 ? ` [${invocation.document.sourcePath}]${message}` : message),
|
|
1703
|
+
sinks: state,
|
|
1704
|
+
onEvent: options.onEvent,
|
|
1705
|
+
isolatedContext: void 0 !== project.fileSetup,
|
|
1706
|
+
isProjectResourceFailure: isSharedResourceFailure,
|
|
1707
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
1708
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
1709
|
+
onUnresolvedResources: ()=>{
|
|
1710
|
+
fileResourcesPending = true;
|
|
1711
|
+
unresolved();
|
|
1712
|
+
}
|
|
1713
|
+
});
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
state.fatal = true;
|
|
1716
|
+
fileResourcesPending = !!state.runtime && getPendingScopeOperations(state.runtime.signal).length > 0;
|
|
1717
|
+
options.onInfrastructureError(error);
|
|
1718
|
+
} finally{
|
|
1719
|
+
state.initializing = false;
|
|
1720
|
+
try {
|
|
1721
|
+
if (1 === state.active && (stopped(state) || state.cursor === prepared.invocations.length)) await finishProject(state);
|
|
1722
|
+
} catch (error) {
|
|
1723
|
+
options.onInfrastructureError(error);
|
|
1724
|
+
}
|
|
1725
|
+
state.active--;
|
|
1726
|
+
if (state.runtime && (fileResourcesPending || state.lifecycle?.cleanup?.status === 'pending')) waitForScopeOperations(state.runtime.signal).then(release, (error)=>{
|
|
1727
|
+
options.onInfrastructureError(error);
|
|
1728
|
+
release();
|
|
1729
|
+
});
|
|
1730
|
+
else release();
|
|
1731
|
+
wake?.();
|
|
1732
|
+
}
|
|
1733
|
+
};
|
|
1734
|
+
while(true){
|
|
1735
|
+
const changed = new Promise((resolve)=>{
|
|
1736
|
+
wake = resolve;
|
|
1737
|
+
});
|
|
1738
|
+
while(occupied < options.maxConcurrentFiles){
|
|
1739
|
+
const job = admit();
|
|
1740
|
+
if (!job) break;
|
|
1741
|
+
const task = run(job);
|
|
1742
|
+
running.add(task);
|
|
1743
|
+
const settled = ()=>{
|
|
1744
|
+
running.delete(task);
|
|
1745
|
+
wake?.();
|
|
1746
|
+
};
|
|
1747
|
+
task.then(settled, (error)=>{
|
|
1748
|
+
options.onInfrastructureError(error);
|
|
1749
|
+
settled();
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
if (!running.size) break;
|
|
1753
|
+
await changed;
|
|
1754
|
+
}
|
|
1755
|
+
wake = void 0;
|
|
1756
|
+
await Promise.all(states.map(async (state)=>{
|
|
1757
|
+
if (!state.runtime || state.finished) return;
|
|
1758
|
+
const admission = new Promise((resolve)=>{
|
|
1759
|
+
cleanupQueue.push(resolve);
|
|
1760
|
+
});
|
|
1761
|
+
trackResourceOperation({}, admission, state.runtime.signal);
|
|
1762
|
+
while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
|
|
1763
|
+
occupied++;
|
|
1764
|
+
cleanupQueue.shift()();
|
|
1765
|
+
}
|
|
1766
|
+
try {
|
|
1767
|
+
await finishProject(state);
|
|
1768
|
+
} catch (error) {
|
|
1769
|
+
options.onInfrastructureError(error);
|
|
1770
|
+
} finally{
|
|
1771
|
+
Promise.all([
|
|
1772
|
+
admission,
|
|
1773
|
+
getResourceCleanupCompletion(state.runtime.signal)
|
|
1774
|
+
]).then(release, (error)=>{
|
|
1775
|
+
options.onInfrastructureError(error);
|
|
1776
|
+
release();
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
}));
|
|
1780
|
+
for (const state of states){
|
|
1781
|
+
const { prepared } = state;
|
|
1782
|
+
if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
|
|
1783
|
+
finalizeProject(state);
|
|
1784
|
+
}
|
|
1785
|
+
return states;
|
|
1786
|
+
}
|
|
1787
|
+
function configuredModelNames() {
|
|
1788
|
+
const env = globalConfigManager.getAllEnvConfig();
|
|
1789
|
+
const defaultName = env.MIDSCENE_MODEL_NAME;
|
|
1790
|
+
const models = new Map();
|
|
1791
|
+
for (const [intent, name] of [
|
|
1792
|
+
[
|
|
1793
|
+
'default',
|
|
1794
|
+
defaultName
|
|
1795
|
+
],
|
|
1796
|
+
[
|
|
1797
|
+
'planning',
|
|
1798
|
+
env.MIDSCENE_PLANNING_MODEL_NAME || defaultName
|
|
1799
|
+
],
|
|
1800
|
+
[
|
|
1801
|
+
'insight',
|
|
1802
|
+
env.MIDSCENE_INSIGHT_MODEL_NAME || defaultName
|
|
1803
|
+
]
|
|
1804
|
+
]){
|
|
1805
|
+
if (!name) continue;
|
|
1806
|
+
const intents = models.get(name) ?? [];
|
|
1807
|
+
intents.push(intent);
|
|
1808
|
+
models.set(name, intents);
|
|
1809
|
+
}
|
|
1810
|
+
return [
|
|
1811
|
+
...models
|
|
1812
|
+
].map(([name, intents])=>({
|
|
1813
|
+
name,
|
|
1814
|
+
intents
|
|
1815
|
+
}));
|
|
1816
|
+
}
|
|
1268
1817
|
function createOperationJournal(runDir) {
|
|
1269
1818
|
const path = join(runDir, 'operations.jsonl');
|
|
1270
1819
|
writeFileSync(path, '', {
|
|
@@ -1333,8 +1882,34 @@ async function runPreparedTestProject(plan) {
|
|
|
1333
1882
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
1334
1883
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
1335
1884
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
1885
|
+
plan.onEvent?.({
|
|
1886
|
+
type: 'run-start',
|
|
1887
|
+
configuredModels: configuredModelNames(),
|
|
1888
|
+
projects: preparedProjects.length,
|
|
1889
|
+
documents: totalDocuments + totalErrors,
|
|
1890
|
+
cases: totalCases,
|
|
1891
|
+
projectRoot,
|
|
1892
|
+
configPath,
|
|
1893
|
+
startedAt: startedAt.toISOString(),
|
|
1894
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects,
|
|
1895
|
+
maxConcurrentFiles: definition.test.maxConcurrentFiles,
|
|
1896
|
+
files: preparedProjects.flatMap((prepared)=>[
|
|
1897
|
+
...prepared.documents.map((document)=>({
|
|
1898
|
+
documentId: document.documentId,
|
|
1899
|
+
projectName: prepared.project.name,
|
|
1900
|
+
sourcePath: document.sourcePath,
|
|
1901
|
+
cases: document.cases.length
|
|
1902
|
+
})),
|
|
1903
|
+
...prepared.collectionErrors.map((error)=>({
|
|
1904
|
+
documentId: error.sourcePath,
|
|
1905
|
+
projectName: prepared.project.name,
|
|
1906
|
+
sourcePath: error.sourcePath,
|
|
1907
|
+
cases: 0
|
|
1908
|
+
}))
|
|
1909
|
+
])
|
|
1910
|
+
});
|
|
1336
1911
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1337
|
-
const effectiveConcurrency = Math.min(definition.test.
|
|
1912
|
+
const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? 1 / 0, Math.max(1, totalDocuments));
|
|
1338
1913
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
1339
1914
|
const journal = createOperationJournal(runDir);
|
|
1340
1915
|
const rootController = new AbortController();
|
|
@@ -1348,6 +1923,18 @@ async function runPreparedTestProject(plan) {
|
|
|
1348
1923
|
process.on('SIGTERM', sigterm);
|
|
1349
1924
|
let failedCaseCount = 0;
|
|
1350
1925
|
const bailReached = ()=>definition.test.bail > 0 && failedCaseCount >= definition.test.bail;
|
|
1926
|
+
let shutdownDeadline;
|
|
1927
|
+
const beginShutdown = ()=>{
|
|
1928
|
+
shutdownDeadline ??= Date.now() + definition.test.teardownTimeout;
|
|
1929
|
+
};
|
|
1930
|
+
rootController.signal.addEventListener('abort', beginShutdown, {
|
|
1931
|
+
once: true
|
|
1932
|
+
});
|
|
1933
|
+
const addFailedCases = (count)=>{
|
|
1934
|
+
failedCaseCount += count;
|
|
1935
|
+
if (bailReached()) beginShutdown();
|
|
1936
|
+
};
|
|
1937
|
+
const teardownTimeoutMs = ()=>Math.max(0, Math.min(definition.test.teardownTimeout, void 0 === shutdownDeadline ? 1 / 0 : shutdownDeadline - Date.now()));
|
|
1351
1938
|
let hasInfrastructureError = false;
|
|
1352
1939
|
const infrastructureErrors = [];
|
|
1353
1940
|
const recordInfrastructureError = (error)=>{
|
|
@@ -1409,112 +1996,30 @@ async function runPreparedTestProject(plan) {
|
|
|
1409
1996
|
announceProject(prepared, projectIndex);
|
|
1410
1997
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
1411
1998
|
};
|
|
1412
|
-
const runPreparedProject = async (prepared, projectIndex)=>{
|
|
1413
|
-
const { project } = prepared;
|
|
1414
|
-
announceProject(prepared, projectIndex);
|
|
1415
|
-
const projectProgress = (message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${project.name}]${message}` : message));
|
|
1416
|
-
const cases = [];
|
|
1417
|
-
const documents = [];
|
|
1418
|
-
let lifecycle;
|
|
1419
|
-
let projectFatal = false;
|
|
1420
|
-
if (prepared.collectionErrors.length > 0 || 'run' === plan.preflightScope && totalErrors > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
|
|
1421
|
-
else if (rootController.signal.aborted) cases.push(...notRunSuite(prepared, 'interrupted'));
|
|
1422
|
-
else if (bailReached()) cases.push(...notRunSuite(prepared, 'bail'));
|
|
1423
|
-
else {
|
|
1424
|
-
const runtime = createProjectRuntime({
|
|
1425
|
-
project,
|
|
1426
|
-
setup: project.setup,
|
|
1427
|
-
signal: rootController.signal
|
|
1428
|
-
});
|
|
1429
|
-
let hasProjectExecutionError = false;
|
|
1430
|
-
try {
|
|
1431
|
-
await runtime.start();
|
|
1432
|
-
if (runtime.canRun) {
|
|
1433
|
-
const runDocument = async (invocation, documentIndex)=>{
|
|
1434
|
-
const { document } = invocation;
|
|
1435
|
-
if (rootController.signal.aborted || bailReached() || projectFatal) {
|
|
1436
|
-
const reason = rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1437
|
-
cases.push(...document.cases.map((item)=>asNotRun(document.documentId, item, project.name, reason)));
|
|
1438
|
-
return;
|
|
1439
|
-
}
|
|
1440
|
-
projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
|
|
1441
|
-
await executeDocumentInvocation({
|
|
1442
|
-
invocation,
|
|
1443
|
-
project,
|
|
1444
|
-
projectContext: runtime.context,
|
|
1445
|
-
runDir,
|
|
1446
|
-
signal: runtime.signal,
|
|
1447
|
-
rootSignal: rootController.signal,
|
|
1448
|
-
shouldBail: bailReached,
|
|
1449
|
-
isProjectFatal: ()=>projectFatal,
|
|
1450
|
-
markProjectFatal: ()=>{
|
|
1451
|
-
projectFatal = true;
|
|
1452
|
-
},
|
|
1453
|
-
addFailedCases: (count)=>{
|
|
1454
|
-
failedCaseCount += count;
|
|
1455
|
-
},
|
|
1456
|
-
onProgress: projectProgress,
|
|
1457
|
-
sinks: {
|
|
1458
|
-
cases,
|
|
1459
|
-
documents
|
|
1460
|
-
}
|
|
1461
|
-
});
|
|
1462
|
-
};
|
|
1463
|
-
let prerequisiteFailed = false;
|
|
1464
|
-
const prerequisite = prepared.invocations.find((invocation)=>invocation.document.documentId === prepared.prerequisiteDocumentId);
|
|
1465
|
-
const prerequisiteCount = prerequisite ? 1 : 0;
|
|
1466
|
-
if (prerequisite) {
|
|
1467
|
-
await runDocument(prerequisite, 0);
|
|
1468
|
-
prerequisiteFailed = documents.at(-1)?.status === 'failed' || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status);
|
|
1469
|
-
}
|
|
1470
|
-
if (!prerequisiteFailed) await runConcurrentJobs(prepared.invocations.filter((invocation)=>invocation !== prerequisite), {
|
|
1471
|
-
concurrency: prepared.documentConcurrency,
|
|
1472
|
-
shouldStop: ()=>rootController.signal.aborted || bailReached() || projectFatal
|
|
1473
|
-
}, async (invocation, index)=>{
|
|
1474
|
-
try {
|
|
1475
|
-
await runDocument(invocation, index + prerequisiteCount);
|
|
1476
|
-
} catch (error) {
|
|
1477
|
-
recordInfrastructureError(error);
|
|
1478
|
-
throw error;
|
|
1479
|
-
}
|
|
1480
|
-
});
|
|
1481
|
-
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1482
|
-
const reason = prerequisiteFailed ? 'project-setup-failed' : rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1483
|
-
cases.push(...notRunSuite(prepared, reason).filter((item)=>!completed.has(item.caseId)));
|
|
1484
|
-
} else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
|
|
1485
|
-
} catch (error) {
|
|
1486
|
-
hasProjectExecutionError = true;
|
|
1487
|
-
recordInfrastructureError(error);
|
|
1488
|
-
} finally{
|
|
1489
|
-
const hasFailure = hasProjectExecutionError || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || projectFatal || rootController.signal.aborted;
|
|
1490
|
-
lifecycle = await runtime.finish(hasFailure ? 'failed' : 'success');
|
|
1491
|
-
}
|
|
1492
|
-
if (hasProjectExecutionError) {
|
|
1493
|
-
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1494
|
-
cases.push(...notRunSuite(prepared, 'interrupted').filter((item)=>!completed.has(item.caseId)));
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
const order = new Map(prepared.documents.map((document, index)=>[
|
|
1498
|
-
document.documentId,
|
|
1499
|
-
index
|
|
1500
|
-
]));
|
|
1501
|
-
const documentOrder = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1502
|
-
cases.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1503
|
-
documents.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || (a.attemptIndex ?? 0) - (b.attemptIndex ?? 0));
|
|
1504
|
-
return buildProjectResult(prepared, cases, documents, lifecycle);
|
|
1505
|
-
};
|
|
1506
1999
|
const projectResults = new Array(preparedProjects.length);
|
|
1507
2000
|
try {
|
|
1508
|
-
await
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
2001
|
+
const states = await runPreparedFiles({
|
|
2002
|
+
projects: preparedProjects,
|
|
2003
|
+
maxConcurrentFiles: effectiveConcurrency,
|
|
2004
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects ?? 1 / 0,
|
|
2005
|
+
setupTimeoutMs: definition.test.setupTimeout,
|
|
2006
|
+
teardownTimeoutMs,
|
|
2007
|
+
signal: rootController.signal,
|
|
2008
|
+
runDir,
|
|
2009
|
+
preflightFailed: 'run' === plan.preflightScope && totalErrors > 0,
|
|
2010
|
+
shouldBail: bailReached,
|
|
2011
|
+
addFailedCases,
|
|
2012
|
+
announceProject,
|
|
2013
|
+
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(preparedProjects.length > 1 || effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
2014
|
+
onEvent: (event)=>runInfrastructureCallback(()=>plan.onEvent?.(event)),
|
|
2015
|
+
onProjectEnd: (state)=>runInfrastructureCallback(()=>plan.onEvent?.({
|
|
2016
|
+
type: 'project-end',
|
|
2017
|
+
project: buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle)
|
|
2018
|
+
})),
|
|
2019
|
+
onInfrastructureError: recordInfrastructureError
|
|
2020
|
+
});
|
|
2021
|
+
states.forEach((state, index)=>{
|
|
2022
|
+
projectResults[index] = buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle);
|
|
1518
2023
|
});
|
|
1519
2024
|
} catch (error) {
|
|
1520
2025
|
recordInfrastructureError(error);
|
|
@@ -1591,6 +2096,10 @@ async function runPreparedTestProject(plan) {
|
|
|
1591
2096
|
buildRunnerDump: (index)=>{
|
|
1592
2097
|
reportIndex = index;
|
|
1593
2098
|
const dump = buildTestRunReportDump(completedResult, index);
|
|
2099
|
+
completedResult = {
|
|
2100
|
+
...completedResult,
|
|
2101
|
+
metrics: dump.metrics
|
|
2102
|
+
};
|
|
1594
2103
|
const warn = getDebug('test-runner:report-assembler', {
|
|
1595
2104
|
console: true
|
|
1596
2105
|
});
|
|
@@ -1608,6 +2117,16 @@ async function runPreparedTestProject(plan) {
|
|
|
1608
2117
|
await publication.publish(completedResult);
|
|
1609
2118
|
});
|
|
1610
2119
|
await publish('write-result', join(dirname(summaryPath), 'result.md'), async ()=>{
|
|
2120
|
+
if (!completedResult.metrics) {
|
|
2121
|
+
const dump = buildTestRunReportDump(completedResult, reportIndex ?? {
|
|
2122
|
+
sources: [],
|
|
2123
|
+
metrics: test_runner_sumTestRunReportMetrics([])
|
|
2124
|
+
});
|
|
2125
|
+
completedResult = {
|
|
2126
|
+
...completedResult,
|
|
2127
|
+
metrics: dump.metrics
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
1611
2130
|
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1612
2131
|
completedResult = {
|
|
1613
2132
|
...completedResult,
|
|
@@ -1621,6 +2140,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1621
2140
|
async function runTestProject(options = {}) {
|
|
1622
2141
|
return runPreparedTestProject(await prepareTestRun(options));
|
|
1623
2142
|
}
|
|
1624
|
-
export { DEFAULT_TEST_FILE_SELECTION, createTestRunId, defineProjectSetup, defineTestProject, discoverTestConfig, discoverTestFiles, loadTestProject, runTestProject, validateTestFileSelection };
|
|
2143
|
+
export { DEFAULT_TEST_FILE_SELECTION, createTestRunId, defineFileSetup, defineProjectSetup, defineTestProject, discoverTestConfig, discoverTestFiles, loadTestProject, runTestProject, validateTestFileSelection };
|
|
1625
2144
|
|
|
1626
2145
|
//# sourceMappingURL=index.mjs.map
|