@midscene/test 1.16.1 → 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 +342 -83
- 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 +1532 -134
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +340 -81
- 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 +1523 -131
- 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 +1 -1
- package/dist/templates/test/android/README.zh.md +1 -1
- package/dist/templates/test/computer/.env.example +1 -1
- package/dist/templates/test/computer/README.md +1 -1
- package/dist/templates/test/computer/README.zh.md +1 -1
- package/dist/templates/test/harmony/.env.example +1 -1
- package/dist/templates/test/harmony/README.md +1 -1
- package/dist/templates/test/harmony/README.zh.md +1 -1
- package/dist/templates/test/ios/.env.example +1 -1
- package/dist/templates/test/ios/README.md +1 -1
- package/dist/templates/test/ios/README.zh.md +1 -1
- package/dist/templates/test/web/.env.example +1 -1
- package/dist/templates/test/web/README.md +1 -1
- package/dist/templates/test/web/README.zh.md +1 -1
- package/dist/types/cli/index.d.ts +75 -0
- package/dist/types/index.d.ts +3 -0
- 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
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, getPendingScopeOperations, getResourceCleanupCompletion, isFatalDeviceError, isReportSecretKey, isResourceRecoveryError, normalizeSteps, observeResourceOperations, runDocumentAttempts, runWorkflowDocument, sanitizeReportError, sanitizeReportText, trackResourceOperation, waitForScopeOperations } from "@midscene/core/internal/test-runner";
|
|
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,7 +9,9 @@ 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;
|
|
@@ -360,11 +362,13 @@ function parseWorkflowSource(source) {
|
|
|
360
362
|
start: state.position + sourceOffset,
|
|
361
363
|
end: 0,
|
|
362
364
|
value: null,
|
|
363
|
-
children: []
|
|
365
|
+
children: [],
|
|
366
|
+
kind: null
|
|
364
367
|
});
|
|
365
368
|
const node = stack.pop();
|
|
366
369
|
node.end = state.position + sourceOffset;
|
|
367
370
|
node.value = state.result;
|
|
371
|
+
node.kind = state.kind;
|
|
368
372
|
stack.at(-1)?.children.push(node);
|
|
369
373
|
nodes.push(node);
|
|
370
374
|
if ('mapping' !== state.kind) return;
|
|
@@ -394,6 +398,36 @@ function parseWorkflowSource(source) {
|
|
|
394
398
|
const value = sanitizeReportText(redacted, 65536);
|
|
395
399
|
return {
|
|
396
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
|
+
},
|
|
397
431
|
sourceYaml: {
|
|
398
432
|
value,
|
|
399
433
|
...redactedPaths.size ? {
|
|
@@ -506,6 +540,18 @@ function collectWorkflowDocument(source, options) {
|
|
|
506
540
|
const sourcePath = source.sourcePath.split(sep).join('/');
|
|
507
541
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
508
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
|
+
},
|
|
509
555
|
templateInput: step.input,
|
|
510
556
|
input: resolveWorkflowVariables(step.input, {
|
|
511
557
|
variables: options.variables,
|
|
@@ -680,6 +726,9 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
680
726
|
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
681
727
|
} : {},
|
|
682
728
|
summary: result.summary,
|
|
729
|
+
...result.metrics ? {
|
|
730
|
+
metrics: result.metrics
|
|
731
|
+
} : {},
|
|
683
732
|
projects: result.projects.map((project)=>({
|
|
684
733
|
projectId: project.projectId,
|
|
685
734
|
name: project.name,
|
|
@@ -1081,7 +1130,8 @@ async function prepareTestRun(options) {
|
|
|
1081
1130
|
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1082
1131
|
reportEnabled: true,
|
|
1083
1132
|
publications: [],
|
|
1084
|
-
onProgress: options.onProgress ?? (()=>{})
|
|
1133
|
+
onProgress: options.onProgress ?? (()=>{}),
|
|
1134
|
+
onEvent: options.onEvent
|
|
1085
1135
|
};
|
|
1086
1136
|
}
|
|
1087
1137
|
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
@@ -1159,6 +1209,38 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1159
1209
|
}));
|
|
1160
1210
|
return output;
|
|
1161
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
|
+
}
|
|
1162
1244
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1163
1245
|
const formatStep = (info)=>{
|
|
1164
1246
|
const position = stepPosition(info);
|
|
@@ -1205,69 +1287,121 @@ async function executeDocumentInvocation(options) {
|
|
|
1205
1287
|
const documentRetry = 'document' === invocation.retry.scope;
|
|
1206
1288
|
const attempts = [];
|
|
1207
1289
|
const fileSetup = invocation.bindings.documentSetup;
|
|
1208
|
-
const documentSetup =
|
|
1290
|
+
const documentSetup = fileSetup ? {
|
|
1209
1291
|
...fileSetup,
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
}
|
|
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
|
+
});
|
|
1217
1315
|
}
|
|
1218
|
-
} :
|
|
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
|
+
});
|
|
1219
1324
|
await runDocumentAttempts({
|
|
1220
1325
|
retry: documentRetry ? invocation.retry.count : 0,
|
|
1221
1326
|
signal: options.signal,
|
|
1222
1327
|
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1223
1328
|
}, async (documentAttemptIndex)=>{
|
|
1224
1329
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1225
|
-
const execution = await
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
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)=>{
|
|
1271
1405
|
if (error instanceof WorkflowExecutionFailure) {
|
|
1272
1406
|
const partial = error.result;
|
|
1273
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)));
|
|
@@ -1337,6 +1471,11 @@ async function executeDocumentInvocation(options) {
|
|
|
1337
1471
|
shouldStop: options.shouldBail()
|
|
1338
1472
|
});
|
|
1339
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
|
+
});
|
|
1340
1479
|
attempts.push(execution);
|
|
1341
1480
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1342
1481
|
...outcome,
|
|
@@ -1371,7 +1510,18 @@ async function executeDocumentInvocation(options) {
|
|
|
1371
1510
|
});
|
|
1372
1511
|
const final = attempts.at(-1);
|
|
1373
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
|
+
});
|
|
1374
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
|
+
});
|
|
1375
1525
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1376
1526
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1377
1527
|
}
|
|
@@ -1411,11 +1561,45 @@ async function runPreparedFiles(options) {
|
|
|
1411
1561
|
code: 'UNRESOLVED_FILE_RESOURCES'
|
|
1412
1562
|
}));
|
|
1413
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
|
+
};
|
|
1414
1586
|
const finishProject = async (state)=>{
|
|
1415
1587
|
if (!state.runtime || state.finished) return;
|
|
1416
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
|
+
}
|
|
1417
1598
|
const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
|
|
1418
|
-
state.lifecycle = await
|
|
1599
|
+
state.lifecycle = await runWithConsoleLogScope({
|
|
1600
|
+
projectName: state.prepared.project.name,
|
|
1601
|
+
phase: 'teardown'
|
|
1602
|
+
}, ()=>state.runtime.finish(failed ? 'failed' : 'success'));
|
|
1419
1603
|
const releaseProject = ()=>{
|
|
1420
1604
|
occupiedProjects--;
|
|
1421
1605
|
wake?.();
|
|
@@ -1429,7 +1613,11 @@ async function runPreparedFiles(options) {
|
|
|
1429
1613
|
options.onInfrastructureError(error);
|
|
1430
1614
|
releaseProject();
|
|
1431
1615
|
});
|
|
1432
|
-
} else
|
|
1616
|
+
} else {
|
|
1617
|
+
releaseProject();
|
|
1618
|
+
finalizeProject(state);
|
|
1619
|
+
options.onProjectEnd?.(state);
|
|
1620
|
+
}
|
|
1433
1621
|
};
|
|
1434
1622
|
const admit = ()=>{
|
|
1435
1623
|
for(let offset = 0; offset < states.length; offset++){
|
|
@@ -1470,7 +1658,20 @@ async function runPreparedFiles(options) {
|
|
|
1470
1658
|
});
|
|
1471
1659
|
setMaxListeners(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
|
|
1472
1660
|
options.announceProject(prepared, index);
|
|
1473
|
-
|
|
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
|
+
});
|
|
1474
1675
|
state.initializing = false;
|
|
1475
1676
|
if (!state.runtime.canRun) {
|
|
1476
1677
|
state.fatal = true;
|
|
@@ -1498,8 +1699,9 @@ async function runPreparedFiles(options) {
|
|
|
1498
1699
|
}));
|
|
1499
1700
|
},
|
|
1500
1701
|
addFailedCases: options.addFailedCases,
|
|
1501
|
-
onProgress: (message)=>options.onProgress(prepared, message),
|
|
1702
|
+
onProgress: (message)=>options.onProgress(prepared, options.maxConcurrentFiles > 1 ? ` [${invocation.document.sourcePath}]${message}` : message),
|
|
1502
1703
|
sinks: state,
|
|
1704
|
+
onEvent: options.onEvent,
|
|
1503
1705
|
isolatedContext: void 0 !== project.fileSetup,
|
|
1504
1706
|
isProjectResourceFailure: isSharedResourceFailure,
|
|
1505
1707
|
setupTimeoutMs: options.setupTimeoutMs,
|
|
@@ -1578,28 +1780,40 @@ async function runPreparedFiles(options) {
|
|
|
1578
1780
|
for (const state of states){
|
|
1579
1781
|
const { prepared } = state;
|
|
1580
1782
|
if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
|
|
1581
|
-
|
|
1582
|
-
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
1583
|
-
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
1584
|
-
documentId: document.documentId,
|
|
1585
|
-
caseId: item.caseId,
|
|
1586
|
-
projectName: prepared.project.name,
|
|
1587
|
-
name: item.definition.name,
|
|
1588
|
-
sourcePath: item.sourcePath,
|
|
1589
|
-
caseIndex: item.caseIndex,
|
|
1590
|
-
status: 'not-run',
|
|
1591
|
-
notRunReason: reason
|
|
1592
|
-
});
|
|
1593
|
-
const order = new Map(prepared.documents.map((document, i)=>[
|
|
1594
|
-
document.documentId,
|
|
1595
|
-
i
|
|
1596
|
-
]));
|
|
1597
|
-
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1598
|
-
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1599
|
-
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
1783
|
+
finalizeProject(state);
|
|
1600
1784
|
}
|
|
1601
1785
|
return states;
|
|
1602
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
|
+
}
|
|
1603
1817
|
function createOperationJournal(runDir) {
|
|
1604
1818
|
const path = join(runDir, 'operations.jsonl');
|
|
1605
1819
|
writeFileSync(path, '', {
|
|
@@ -1668,6 +1882,32 @@ async function runPreparedTestProject(plan) {
|
|
|
1668
1882
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
1669
1883
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
1670
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
|
+
});
|
|
1671
1911
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1672
1912
|
const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? 1 / 0, Math.max(1, totalDocuments));
|
|
1673
1913
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
@@ -1770,7 +2010,12 @@ async function runPreparedTestProject(plan) {
|
|
|
1770
2010
|
shouldBail: bailReached,
|
|
1771
2011
|
addFailedCases,
|
|
1772
2012
|
announceProject,
|
|
1773
|
-
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
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
|
+
})),
|
|
1774
2019
|
onInfrastructureError: recordInfrastructureError
|
|
1775
2020
|
});
|
|
1776
2021
|
states.forEach((state, index)=>{
|
|
@@ -1851,6 +2096,10 @@ async function runPreparedTestProject(plan) {
|
|
|
1851
2096
|
buildRunnerDump: (index)=>{
|
|
1852
2097
|
reportIndex = index;
|
|
1853
2098
|
const dump = buildTestRunReportDump(completedResult, index);
|
|
2099
|
+
completedResult = {
|
|
2100
|
+
...completedResult,
|
|
2101
|
+
metrics: dump.metrics
|
|
2102
|
+
};
|
|
1854
2103
|
const warn = getDebug('test-runner:report-assembler', {
|
|
1855
2104
|
console: true
|
|
1856
2105
|
});
|
|
@@ -1868,6 +2117,16 @@ async function runPreparedTestProject(plan) {
|
|
|
1868
2117
|
await publication.publish(completedResult);
|
|
1869
2118
|
});
|
|
1870
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
|
+
}
|
|
1871
2130
|
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1872
2131
|
completedResult = {
|
|
1873
2132
|
...completedResult,
|