@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/lib/test-cli.js
CHANGED
|
@@ -29,7 +29,7 @@ var __webpack_modules__ = {
|
|
|
29
29
|
},
|
|
30
30
|
"./package.json" (module) {
|
|
31
31
|
module.exports = {
|
|
32
|
-
rE: "1.16.
|
|
32
|
+
rE: "1.16.2"
|
|
33
33
|
};
|
|
34
34
|
}
|
|
35
35
|
};
|
|
@@ -93,6 +93,7 @@ var __webpack_exports__ = {};
|
|
|
93
93
|
var external_node_fs_ = __webpack_require__("node:fs");
|
|
94
94
|
var external_node_path_ = __webpack_require__("node:path");
|
|
95
95
|
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
96
|
+
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
96
97
|
var package_0 = __webpack_require__("./package.json");
|
|
97
98
|
const external_node_util_namespaceObject = require("node:util");
|
|
98
99
|
const formatCliError = (error)=>'string' == typeof error ? error : (0, external_node_util_namespaceObject.inspect)(error, {
|
|
@@ -100,6 +101,1021 @@ var __webpack_exports__ = {};
|
|
|
100
101
|
depth: null,
|
|
101
102
|
customInspect: false
|
|
102
103
|
});
|
|
104
|
+
function formatConciseError(error) {
|
|
105
|
+
const seen = new Set();
|
|
106
|
+
const messages = [];
|
|
107
|
+
let current = error;
|
|
108
|
+
while(current instanceof Error && !seen.has(current)){
|
|
109
|
+
seen.add(current);
|
|
110
|
+
if ('NODE_EXECUTION_ERROR' === current.code && void 0 !== current.cause) {
|
|
111
|
+
current = current.cause;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (current.message && !messages.includes(current.message)) messages.push(current.message);
|
|
115
|
+
if (current instanceof AggregateError) {
|
|
116
|
+
for (const nested of current.errors)if (!(nested === current || seen.has(nested))) {
|
|
117
|
+
seen.add(nested);
|
|
118
|
+
messages.push(nested instanceof Error ? nested.message : String(nested));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
current = current.cause;
|
|
122
|
+
}
|
|
123
|
+
if (!messages.length) return current instanceof Error ? current.message : formatCliError(current);
|
|
124
|
+
return messages.join('\nCaused by: ');
|
|
125
|
+
}
|
|
126
|
+
var external_chalk_ = __webpack_require__("chalk");
|
|
127
|
+
var external_chalk_default = /*#__PURE__*/ __webpack_require__.n(external_chalk_);
|
|
128
|
+
const external_string_width_namespaceObject = require("string-width");
|
|
129
|
+
var external_string_width_default = /*#__PURE__*/ __webpack_require__.n(external_string_width_namespaceObject);
|
|
130
|
+
const external_node_url_namespaceObject = require("node:url");
|
|
131
|
+
const oneLine = (value)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(value).replace(/\s+/g, ' ').trim();
|
|
132
|
+
const shorten = (value, length)=>{
|
|
133
|
+
const text = oneLine(value);
|
|
134
|
+
return text.length <= length ? text : `${text.slice(0, length - 1)}…`;
|
|
135
|
+
};
|
|
136
|
+
function firstBusinessFrame(error, projectRoot) {
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
const frames = [];
|
|
139
|
+
const visit = (value, depth = 0)=>{
|
|
140
|
+
if (!value || 'object' != typeof value || seen.has(value) || depth > 12) return;
|
|
141
|
+
seen.add(value);
|
|
142
|
+
const detail = value;
|
|
143
|
+
visit(detail.cause, depth + 1);
|
|
144
|
+
if (Array.isArray(detail.errors)) for (const child of detail.errors.slice(0, 10))visit(child, depth + 1);
|
|
145
|
+
if ('string' != typeof detail.stack) return;
|
|
146
|
+
for (const line of detail.stack.split('\n')){
|
|
147
|
+
if (!line.trim().startsWith('at ')) continue;
|
|
148
|
+
let location = line.trim().slice(3).replace(/^async /, '');
|
|
149
|
+
if (location.endsWith(')')) location = location.slice(location.lastIndexOf('(') + 1, -1);
|
|
150
|
+
const match = location.match(/^(.*):(\d+):(\d+)$/);
|
|
151
|
+
if (!match) continue;
|
|
152
|
+
let file = match[1];
|
|
153
|
+
if (file.startsWith('file://')) try {
|
|
154
|
+
file = (0, external_node_url_namespaceObject.fileURLToPath)(file);
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const normalized = file.replace(/\\/g, '/');
|
|
159
|
+
if (!(normalized.startsWith('node:') || normalized.includes('/node_modules/') || /(?:^|\/)packages\/(?:core|test|shared|web-integration)\/(?:dist|src)\//.test(normalized)) && /\.[cm]?[jt]sx?$/.test(normalized)) frames.push(`${file}:${match[2]}:${match[3]}`);
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
visit(error);
|
|
163
|
+
const chosen = frames[0];
|
|
164
|
+
if (!chosen || !projectRoot) return chosen;
|
|
165
|
+
const match = chosen.match(/^(.*):(\d+):(\d+)$/);
|
|
166
|
+
const file = match[1];
|
|
167
|
+
const paths = /^[A-Za-z]:[\\/]/.test(file) ? external_node_path_.win32 : {
|
|
168
|
+
isAbsolute: external_node_path_.isAbsolute,
|
|
169
|
+
relative: external_node_path_.relative
|
|
170
|
+
};
|
|
171
|
+
const display = paths.isAbsolute(file) ? paths.relative(projectRoot, file) : file;
|
|
172
|
+
return `${display.replace(/\\/g, '/')}:${match[2]}:${match[3]}`;
|
|
173
|
+
}
|
|
174
|
+
function formatFailureInput(input) {
|
|
175
|
+
if (void 0 === input) return [];
|
|
176
|
+
const value = (0, test_runner_namespaceObject.sanitizeReportValue)(input).value;
|
|
177
|
+
const render = (value)=>shorten((0, test_runner_namespaceObject.sanitizeReportError)({
|
|
178
|
+
message: 'string' == typeof value ? value : (0, external_node_util_namespaceObject.inspect)(value, {
|
|
179
|
+
colors: false,
|
|
180
|
+
depth: 2,
|
|
181
|
+
maxArrayLength: 4,
|
|
182
|
+
maxStringLength: 180,
|
|
183
|
+
breakLength: 1 / 0,
|
|
184
|
+
customInspect: false,
|
|
185
|
+
getters: false
|
|
186
|
+
})
|
|
187
|
+
}).message, 160);
|
|
188
|
+
if (value && 'object' == typeof value && !Array.isArray(value)) {
|
|
189
|
+
const entries = Object.entries(value);
|
|
190
|
+
const rows = entries.slice(0, 4).map(([key, value])=>`${shorten(key, 48)}: ${render(value)}`);
|
|
191
|
+
if (entries.length > 4) rows.push(`… ${entries.length - 4} more fields (see Summary)`);
|
|
192
|
+
return rows;
|
|
193
|
+
}
|
|
194
|
+
return [
|
|
195
|
+
render(value)
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
function formatFailureDetails(source, error, context = {}) {
|
|
199
|
+
const title = context.projectName ? `[${oneLine(context.projectName)}] ${oneLine(context.caseName ?? context.sourcePath ?? context.phase ?? source)}` : oneLine(source);
|
|
200
|
+
const body = [];
|
|
201
|
+
const field = (label, value)=>body.push(` ${label.padEnd(9)}${value}`);
|
|
202
|
+
const step = context.step;
|
|
203
|
+
if (step) {
|
|
204
|
+
const phase = 'steps' === step.phase ? '' : `${step.phase} `;
|
|
205
|
+
const index = step.stepIndex + 1;
|
|
206
|
+
const position = void 0 === context.stepCount ? String(index) : `${index}/${Math.max(index, context.stepCount)}`;
|
|
207
|
+
field('Step', `${phase}${position} · ${oneLine(step.node)}`);
|
|
208
|
+
} else if (context.phase) field('Phase', oneLine(context.phase));
|
|
209
|
+
if (context.sourcePath) {
|
|
210
|
+
const location = step?.meta.sourceLocation;
|
|
211
|
+
field('Location', `${oneLine(context.sourcePath)}${location ? `:${location.line}` : ''}`);
|
|
212
|
+
}
|
|
213
|
+
for (const [label, attempt] of [
|
|
214
|
+
[
|
|
215
|
+
'Attempt',
|
|
216
|
+
context.attempt
|
|
217
|
+
],
|
|
218
|
+
[
|
|
219
|
+
'File try',
|
|
220
|
+
context.fileAttempt
|
|
221
|
+
]
|
|
222
|
+
])if (attempt && (attempt.total > 1 || attempt.index > 0)) field(label, `${attempt.index + 1}/${attempt.total}`);
|
|
223
|
+
if (body.length) body.push('');
|
|
224
|
+
const safeMessage = (0, test_runner_namespaceObject.sanitizeReportError)({
|
|
225
|
+
message: formatConciseError(error)
|
|
226
|
+
}).message;
|
|
227
|
+
const lines = (0, external_node_util_namespaceObject.stripVTControlCharacters)(safeMessage).split(/\r?\n/);
|
|
228
|
+
body.push(` Error: ${shorten(lines[0], 320)}`, ...lines.slice(1, 6).map((line)=>` ${shorten(line, 320)}`));
|
|
229
|
+
if (lines.length > 6) body.push(' … (use --reporter verbose for details)');
|
|
230
|
+
const input = step ? formatFailureInput(step.input) : [];
|
|
231
|
+
if (input.length) {
|
|
232
|
+
body.push('');
|
|
233
|
+
input.forEach((line, index)=>field(0 === index ? 'Input' : '', line));
|
|
234
|
+
}
|
|
235
|
+
const frame = firstBusinessFrame(error, context.projectRoot);
|
|
236
|
+
if (frame) body.push('', ` at ${oneLine(frame)}`);
|
|
237
|
+
return {
|
|
238
|
+
title,
|
|
239
|
+
body
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
var env_ = __webpack_require__("@midscene/shared/env");
|
|
243
|
+
function _define_property(obj, key, value) {
|
|
244
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
245
|
+
value: value,
|
|
246
|
+
enumerable: true,
|
|
247
|
+
configurable: true,
|
|
248
|
+
writable: true
|
|
249
|
+
});
|
|
250
|
+
else obj[key] = value;
|
|
251
|
+
return obj;
|
|
252
|
+
}
|
|
253
|
+
function configuredModelNames() {
|
|
254
|
+
const env = env_.globalConfigManager.getAllEnvConfig();
|
|
255
|
+
const defaultName = env.MIDSCENE_MODEL_NAME;
|
|
256
|
+
const models = new Map();
|
|
257
|
+
for (const [intent, name] of [
|
|
258
|
+
[
|
|
259
|
+
'default',
|
|
260
|
+
defaultName
|
|
261
|
+
],
|
|
262
|
+
[
|
|
263
|
+
'planning',
|
|
264
|
+
env.MIDSCENE_PLANNING_MODEL_NAME || defaultName
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
'insight',
|
|
268
|
+
env.MIDSCENE_INSIGHT_MODEL_NAME || defaultName
|
|
269
|
+
]
|
|
270
|
+
]){
|
|
271
|
+
if (!name) continue;
|
|
272
|
+
const intents = models.get(name) ?? [];
|
|
273
|
+
intents.push(intent);
|
|
274
|
+
models.set(name, intents);
|
|
275
|
+
}
|
|
276
|
+
return [
|
|
277
|
+
...models
|
|
278
|
+
].map(([name, intents])=>({
|
|
279
|
+
name,
|
|
280
|
+
intents
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
class CliModelUsage {
|
|
284
|
+
add(step) {
|
|
285
|
+
for (const trace of step.report?.traces ?? [])if (trace.metrics) {
|
|
286
|
+
this.executions.set(trace.executionId, trace.metrics);
|
|
287
|
+
this.missing.delete(trace.executionId);
|
|
288
|
+
} else if (!this.executions.has(trace.executionId)) this.missing.add(trace.executionId);
|
|
289
|
+
}
|
|
290
|
+
snapshot() {
|
|
291
|
+
if (!this.executions.size && !this.missing.size) return;
|
|
292
|
+
return (0, test_runner_namespaceObject.sumTestRunReportMetrics)([
|
|
293
|
+
...this.executions.values()
|
|
294
|
+
], this.missing.size ? [
|
|
295
|
+
'Some execution usage has not been recorded.'
|
|
296
|
+
] : []);
|
|
297
|
+
}
|
|
298
|
+
constructor(){
|
|
299
|
+
_define_property(this, "executions", new Map());
|
|
300
|
+
_define_property(this, "missing", new Set());
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function formatTokenCount(value) {
|
|
304
|
+
if (value < 1000) return String(Math.round(value));
|
|
305
|
+
if (value < 1000000) return `${(value / 1000).toFixed(1)}k`;
|
|
306
|
+
return `${(value / 1000000).toFixed(1)}m`;
|
|
307
|
+
}
|
|
308
|
+
function formatModelUsage(metrics) {
|
|
309
|
+
if (!metrics) return [
|
|
310
|
+
'—'
|
|
311
|
+
];
|
|
312
|
+
if (!metrics.models?.length) {
|
|
313
|
+
const name = metrics.modelCallCount || metrics.collection?.status === 'unknown' ? 'unknown' : '—';
|
|
314
|
+
return [
|
|
315
|
+
`${name} · ${formatTokenUsage(metrics)}`
|
|
316
|
+
];
|
|
317
|
+
}
|
|
318
|
+
return metrics.models.map((model)=>{
|
|
319
|
+
const name = (0, external_node_util_namespaceObject.stripVTControlCharacters)(model.modelName).replace(/\s+/g, ' ').trim() || 'unknown';
|
|
320
|
+
const usage = formatTokenUsage({
|
|
321
|
+
...metrics,
|
|
322
|
+
modelCallCount: model.callCount,
|
|
323
|
+
promptTokens: model.promptTokens,
|
|
324
|
+
cachedInputTokens: model.cachedInputTokens,
|
|
325
|
+
completionTokens: model.completionTokens,
|
|
326
|
+
totalTokens: model.totalTokens
|
|
327
|
+
});
|
|
328
|
+
return `${name} · ${usage}`;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
function formatTokenUsage(metrics, includeCached = true) {
|
|
332
|
+
if (!metrics) return '—';
|
|
333
|
+
const state = (field)=>metrics.collection?.fields?.[field] ?? metrics.collection?.status ?? 'complete';
|
|
334
|
+
if ('unknown' === state('totalTokens')) return '— (usage unavailable)';
|
|
335
|
+
if (!metrics.modelCallCount && !metrics.totalTokens && 'complete' === state('totalTokens')) return '0 (no model calls)';
|
|
336
|
+
const count = (field)=>'unknown' === state(field) ? '—' : `${'partial' === state(field) ? '≥' : ''}${formatTokenCount(metrics[field])}`;
|
|
337
|
+
return [
|
|
338
|
+
`${count('totalTokens')} total${'partial' === state('totalTokens') ? ' (partial)' : ''}`,
|
|
339
|
+
`${count('promptTokens')} input${includeCached && metrics.cachedInputTokens ? ` (${formatTokenCount(metrics.cachedInputTokens)} cached)` : ''}`,
|
|
340
|
+
`${count('completionTokens')} output`
|
|
341
|
+
].join(' · ');
|
|
342
|
+
}
|
|
343
|
+
function terminal_progress_define_property(obj, key, value) {
|
|
344
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
345
|
+
value: value,
|
|
346
|
+
enumerable: true,
|
|
347
|
+
configurable: true,
|
|
348
|
+
writable: true
|
|
349
|
+
});
|
|
350
|
+
else obj[key] = value;
|
|
351
|
+
return obj;
|
|
352
|
+
}
|
|
353
|
+
const graphemes = new Intl.Segmenter(void 0, {
|
|
354
|
+
granularity: 'grapheme'
|
|
355
|
+
});
|
|
356
|
+
const SYNC_START = '\u001b[?2026h';
|
|
357
|
+
const SYNC_END = '\u001b[?2026l';
|
|
358
|
+
const truncateTerminalLine = (text, columns)=>{
|
|
359
|
+
if (columns <= 0) return '';
|
|
360
|
+
if (external_string_width_default()(text) <= columns) return text;
|
|
361
|
+
let output = '';
|
|
362
|
+
let width = 0;
|
|
363
|
+
for (const { segment } of graphemes.segment((0, external_node_util_namespaceObject.stripVTControlCharacters)(text))){
|
|
364
|
+
width += external_string_width_default()(segment);
|
|
365
|
+
if (width > columns - 1) break;
|
|
366
|
+
output += segment;
|
|
367
|
+
}
|
|
368
|
+
return `${output}…`;
|
|
369
|
+
};
|
|
370
|
+
class TerminalProgress {
|
|
371
|
+
erase() {
|
|
372
|
+
if (!this.lines.length) return '';
|
|
373
|
+
const columns = Math.max(1, this.io.columns ?? 100);
|
|
374
|
+
const occupiedRows = this.lines.reduce((sum, line)=>sum + Math.max(1, Math.ceil(external_string_width_default()(line) / columns)), 0);
|
|
375
|
+
const rows = Math.min(occupiedRows, Math.max(0, (this.io.rows ?? 24) - 1));
|
|
376
|
+
this.lines = [];
|
|
377
|
+
return `\r${rows ? `\u001b[${rows}A` : ''}\u001b[0J`;
|
|
378
|
+
}
|
|
379
|
+
frame(previousHeight = 0) {
|
|
380
|
+
const availableRows = Math.max(0, (this.io.rows ?? 24) - 2);
|
|
381
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
382
|
+
if (!availableRows || !columns) return '';
|
|
383
|
+
const footer = this.footer.slice(0, availableRows);
|
|
384
|
+
const budget = availableRows - footer.length;
|
|
385
|
+
const overflow = this.content.length > budget;
|
|
386
|
+
const visible = Math.max(0, budget - (overflow ? 1 : 0));
|
|
387
|
+
const body = this.content.slice(0, visible);
|
|
388
|
+
if (overflow && budget > 0) body.push(` … ${this.content.length - visible} more rows`);
|
|
389
|
+
const lines = [
|
|
390
|
+
...body,
|
|
391
|
+
...footer
|
|
392
|
+
].map((line)=>truncateTerminalLine(line, columns));
|
|
393
|
+
const blanks = Math.max(0, Math.min(previousHeight, availableRows) - lines.length);
|
|
394
|
+
this.lines = [
|
|
395
|
+
...Array.from({
|
|
396
|
+
length: blanks
|
|
397
|
+
}, ()=>''),
|
|
398
|
+
...lines
|
|
399
|
+
];
|
|
400
|
+
return this.lines.length ? `${this.lines.join('\n')}\n` : '';
|
|
401
|
+
}
|
|
402
|
+
render(content, footer) {
|
|
403
|
+
this.update(content, footer);
|
|
404
|
+
const previousHeight = this.lines.length;
|
|
405
|
+
const erase = this.erase();
|
|
406
|
+
this.io.write?.(`${SYNC_START}${erase}${this.frame(previousHeight)}${SYNC_END}`);
|
|
407
|
+
}
|
|
408
|
+
update(content, footer) {
|
|
409
|
+
this.content = content;
|
|
410
|
+
this.footer = footer;
|
|
411
|
+
}
|
|
412
|
+
output(message, write) {
|
|
413
|
+
this.io.write?.(SYNC_START);
|
|
414
|
+
try {
|
|
415
|
+
const erase = this.erase();
|
|
416
|
+
if (erase) this.io.write?.(erase);
|
|
417
|
+
write(message.endsWith('\n') ? message : `${message}\n`);
|
|
418
|
+
const frame = this.frame();
|
|
419
|
+
if (frame) this.io.write?.(frame);
|
|
420
|
+
} finally{
|
|
421
|
+
this.io.write?.(SYNC_END);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
clear() {
|
|
425
|
+
const erase = this.erase();
|
|
426
|
+
this.content = [];
|
|
427
|
+
this.footer = [];
|
|
428
|
+
if (erase) this.io.write?.(`${SYNC_START}${erase}${SYNC_END}`);
|
|
429
|
+
}
|
|
430
|
+
constructor(io){
|
|
431
|
+
terminal_progress_define_property(this, "io", void 0);
|
|
432
|
+
terminal_progress_define_property(this, "lines", void 0);
|
|
433
|
+
terminal_progress_define_property(this, "content", void 0);
|
|
434
|
+
terminal_progress_define_property(this, "footer", void 0);
|
|
435
|
+
this.io = io;
|
|
436
|
+
this.lines = [];
|
|
437
|
+
this.content = [];
|
|
438
|
+
this.footer = [];
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const warning_format_oneLine = (text)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(text).replace(/\s+/g, ' ').trim();
|
|
442
|
+
function formatWarningScope(scope) {
|
|
443
|
+
if (!scope) return '';
|
|
444
|
+
const parts = [
|
|
445
|
+
`[${warning_format_oneLine(scope.projectName)}]`
|
|
446
|
+
];
|
|
447
|
+
if (scope.sourcePath) parts.push(warning_format_oneLine(scope.sourcePath));
|
|
448
|
+
if (scope.caseName) parts.push(warning_format_oneLine(scope.caseName));
|
|
449
|
+
if (scope.phase) {
|
|
450
|
+
const phase = 'steps' === scope.phase ? 'step' : scope.phase;
|
|
451
|
+
parts.push(`${phase}${void 0 === scope.stepIndex ? '' : `[${scope.stepIndex + 1}]`}${scope.node ? ` ${warning_format_oneLine(scope.node)}` : ''}`);
|
|
452
|
+
}
|
|
453
|
+
return `${parts.join(' › ')} · `;
|
|
454
|
+
}
|
|
455
|
+
function errorMessage(error) {
|
|
456
|
+
const seen = new Set();
|
|
457
|
+
let current = error;
|
|
458
|
+
let message = '';
|
|
459
|
+
let status;
|
|
460
|
+
while(current && 'object' == typeof current && !seen.has(current)){
|
|
461
|
+
seen.add(current);
|
|
462
|
+
const detail = current;
|
|
463
|
+
if ('string' == typeof detail.message) message = detail.message;
|
|
464
|
+
if ('number' == typeof detail.status) status = detail.status;
|
|
465
|
+
current = detail.cause;
|
|
466
|
+
}
|
|
467
|
+
const firstLine = warning_format_oneLine((message || String(error)).split(/\r?\n/)[0]);
|
|
468
|
+
return status && !firstLine.startsWith(String(status)) ? `${status}: ${firstLine}` : firstLine;
|
|
469
|
+
}
|
|
470
|
+
function formatWarning(entry) {
|
|
471
|
+
const details = entry.warning;
|
|
472
|
+
let message;
|
|
473
|
+
if (details?.kind === 'model-request-retry') message = `model request retry ${details.attempt + 1}/${details.maxAttempts} in ${details.delayMs / 1000}s (${warning_format_oneLine(details.modelName)}): ${errorMessage(details.error)}`;
|
|
474
|
+
else {
|
|
475
|
+
const error = entry.args?.find((arg)=>arg instanceof Error);
|
|
476
|
+
const prefix = 'string' == typeof entry.args?.[0] ? warning_format_oneLine(entry.args[0].split(/\r?\n/)[0]).replace(/:\s*$/, '') : '';
|
|
477
|
+
message = error ? `${prefix ? `${prefix}: ` : ''}${errorMessage(error)}` : warning_format_oneLine(entry.message.split(/\r?\n/)[0]);
|
|
478
|
+
message = message.replace(/^\[midscene:warning\]\s*/, '');
|
|
479
|
+
}
|
|
480
|
+
if (message.startsWith('waitForNetworkIdle is skipped for Playwright.')) message = 'Playwright: post-action network-idle wait skipped.';
|
|
481
|
+
return message.length > 240 ? `${message.slice(0, 239)}…` : message;
|
|
482
|
+
}
|
|
483
|
+
function warningKey(entry, message) {
|
|
484
|
+
const details = entry.warning;
|
|
485
|
+
if (details?.kind === 'model-request-retry') return `${entry.topic}\n${JSON.stringify(entry.scope)}\n${details.callId}\n${details.attempt}`;
|
|
486
|
+
const scope = entry.args?.some((arg)=>arg instanceof Error) ? JSON.stringify(entry.scope) : '';
|
|
487
|
+
return `${entry.topic}\n${scope}\n${message}`;
|
|
488
|
+
}
|
|
489
|
+
function terminal_reporter_define_property(obj, key, value) {
|
|
490
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
491
|
+
value: value,
|
|
492
|
+
enumerable: true,
|
|
493
|
+
configurable: true,
|
|
494
|
+
writable: true
|
|
495
|
+
});
|
|
496
|
+
else obj[key] = value;
|
|
497
|
+
return obj;
|
|
498
|
+
}
|
|
499
|
+
const terminal_reporter_oneLine = (value)=>(0, external_node_util_namespaceObject.stripVTControlCharacters)(value).replace(/\s+/g, ' ').trim();
|
|
500
|
+
const formatDuration = (ms)=>{
|
|
501
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
502
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
|
|
503
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor(ms % 60000 / 1000)}s`;
|
|
504
|
+
};
|
|
505
|
+
const identity = (project, path, name)=>`[${terminal_reporter_oneLine(project)}] ${terminal_reporter_oneLine(path)}${name ? ` › ${terminal_reporter_oneLine(name)}` : ''}`;
|
|
506
|
+
const projectPhaseLabel = (phase)=>'setup' === phase ? 'setting up' : 'cleaning up';
|
|
507
|
+
const fileStatus = (status, cases)=>{
|
|
508
|
+
if ('failed' === status || cases.some((item)=>'failed' === item.status)) return 'failed';
|
|
509
|
+
if (cases.length && cases.every((item)=>'not-run' === item.status)) return 'not-run';
|
|
510
|
+
if (cases.some((item)=>'not-run' === item.status)) return 'failed';
|
|
511
|
+
return status ?? (cases.length ? 'success' : 'not-run');
|
|
512
|
+
};
|
|
513
|
+
class TerminalReporter {
|
|
514
|
+
get interactive() {
|
|
515
|
+
return !!this.io.isTTY && !!this.io.write && 'default' === this.mode;
|
|
516
|
+
}
|
|
517
|
+
log(message) {
|
|
518
|
+
if (this.interactive) this.output(`${message}\n`, (text)=>this.io.write(text));
|
|
519
|
+
else this.io.log(message);
|
|
520
|
+
this.lastOutput = Date.now();
|
|
521
|
+
}
|
|
522
|
+
output(message, write) {
|
|
523
|
+
this.renderProgress(false);
|
|
524
|
+
this.progressView.output(message, write);
|
|
525
|
+
this.lastOutput = Date.now();
|
|
526
|
+
}
|
|
527
|
+
progress(message) {
|
|
528
|
+
if ('verbose' === this.mode) this.log(message);
|
|
529
|
+
}
|
|
530
|
+
console(entry) {
|
|
531
|
+
if ('report' === entry.type) {
|
|
532
|
+
if ('verbose' === this.mode) this.log(entry.message);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const scope = formatWarningScope(entry.scope);
|
|
536
|
+
if ('verbose' === this.mode) {
|
|
537
|
+
const kind = entry.warning?.kind === 'model-request-retry' ? 'model request retry · ' : entry.warning?.kind === 'model-request-detail' ? 'model request error · ' : '';
|
|
538
|
+
this.log(`WARN ${scope}${kind}${entry.message}`);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (entry.warning?.kind === 'model-request-detail') return;
|
|
542
|
+
const message = formatWarning(entry);
|
|
543
|
+
const key = warningKey(entry, message);
|
|
544
|
+
if (this.warnings.has(key)) return;
|
|
545
|
+
this.warnings.set(key, {
|
|
546
|
+
scope,
|
|
547
|
+
message
|
|
548
|
+
});
|
|
549
|
+
if (this.interactive) {
|
|
550
|
+
let attached = false;
|
|
551
|
+
if (entry.warning?.kind === 'model-request-retry' && entry.scope) {
|
|
552
|
+
const owner = entry.scope;
|
|
553
|
+
for (const [id, task] of this.active)if (task.file.projectName === owner.projectName && (!owner.sourcePath || task.file.sourcePath === owner.sourcePath) && (!owner.caseId || id === owner.caseId) && (!owner.runId || task.runId === owner.runId) && (!owner.phase || task.phase === owner.phase) && (void 0 === owner.stepIndex || task.stepIndex === owner.stepIndex)) {
|
|
554
|
+
task.warning = message;
|
|
555
|
+
attached = true;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (!attached) this.latestNotice = message.startsWith('Playwright:') ? 'Playwright: network-idle wait skipped.' : `${scope}${message}`;
|
|
559
|
+
this.schedule();
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
this.log(`${this.color.yellow('WARN')} ${this.color.dim(scope)}${message}`);
|
|
563
|
+
}
|
|
564
|
+
file(projectName, sourcePath, total = 0) {
|
|
565
|
+
const key = `${projectName}\0${sourcePath}`;
|
|
566
|
+
let file = this.files.get(key);
|
|
567
|
+
if (!file) {
|
|
568
|
+
file = {
|
|
569
|
+
projectName,
|
|
570
|
+
sourcePath,
|
|
571
|
+
total,
|
|
572
|
+
outcomes: new Map(),
|
|
573
|
+
attemptOutcomes: new Map(),
|
|
574
|
+
started: false,
|
|
575
|
+
printed: false,
|
|
576
|
+
durationMs: 0,
|
|
577
|
+
retries: 0,
|
|
578
|
+
completionOrder: 0
|
|
579
|
+
};
|
|
580
|
+
this.files.set(key, file);
|
|
581
|
+
}
|
|
582
|
+
file.total = Math.max(file.total, total);
|
|
583
|
+
return file;
|
|
584
|
+
}
|
|
585
|
+
event(event) {
|
|
586
|
+
if ('run-start' === event.type) {
|
|
587
|
+
this.total = event.cases;
|
|
588
|
+
this.totalFiles = event.documents;
|
|
589
|
+
this.startedAt = event.startedAt ? Date.parse(event.startedAt) : Date.now();
|
|
590
|
+
for (const file of event.files ?? [])this.file(file.projectName, file.sourcePath, file.cases);
|
|
591
|
+
const c = this.color;
|
|
592
|
+
this.log(`\n ${c.bgGreen.black.bold(' RUN ')} ${c.green(`midscene-test v${package_0.rE}`)} ${c.dim(terminal_reporter_oneLine(event.projectRoot ?? process.cwd()))}`);
|
|
593
|
+
this.log(` ${c.dim(`${event.projects} projects · ${event.documents} files · ${event.cases} cases`)}`);
|
|
594
|
+
if (void 0 !== event.maxConcurrentProjects && void 0 !== event.maxConcurrentFiles) this.log(` ${c.dim(`Concurrency ${event.maxConcurrentProjects ?? 'unlimited'} projects · ${event.maxConcurrentFiles ?? 'unlimited'} files (global limits)`)}`);
|
|
595
|
+
if (event.configPath) this.log(` ${c.dim(`Config ${terminal_reporter_oneLine(event.configPath)}`)}`);
|
|
596
|
+
for (const model of event.configuredModels ?? [])this.log(` ${c.dim('Models (env)')} ${terminal_reporter_oneLine(model.name)} ${c.dim(`· ${model.intents.map(terminal_reporter_oneLine).join(', ')}`)}`);
|
|
597
|
+
this.log('');
|
|
598
|
+
if ('default' === this.mode) {
|
|
599
|
+
this.timer = setInterval(()=>this.tick(), this.interactive ? 1000 : 30000);
|
|
600
|
+
this.timer.unref();
|
|
601
|
+
if (this.interactive) this.removeResizeListener = this.io.onResize?.(()=>this.renderProgress());
|
|
602
|
+
this.renderProgress();
|
|
603
|
+
}
|
|
604
|
+
} else if ('project-end' === event.type) {
|
|
605
|
+
this.completeProject(event.project);
|
|
606
|
+
this.schedule();
|
|
607
|
+
} else if ('project-phase' === event.type) {
|
|
608
|
+
if ('running' === event.phase) this.projectPhases.delete(event.projectName);
|
|
609
|
+
else this.projectPhases.set(event.projectName, {
|
|
610
|
+
phase: event.phase,
|
|
611
|
+
since: Date.now()
|
|
612
|
+
});
|
|
613
|
+
this.schedule();
|
|
614
|
+
} else if ('document-start' === event.type) {
|
|
615
|
+
this.file(event.projectName, event.sourcePath, event.cases).started = true;
|
|
616
|
+
this.schedule();
|
|
617
|
+
} else if ('document-end' === event.type) {
|
|
618
|
+
const file = this.file(event.document.projectName, event.document.sourcePath, event.cases.length);
|
|
619
|
+
file.status = fileStatus(event.document.status, event.cases);
|
|
620
|
+
file.started = true;
|
|
621
|
+
file.completionOrder = ++this.completionOrder;
|
|
622
|
+
file.durationMs = event.durationMs;
|
|
623
|
+
file.retries = event.document.attemptIndex ?? 0;
|
|
624
|
+
file.outcomes = new Map(event.cases.map((outcome)=>[
|
|
625
|
+
outcome.caseId,
|
|
626
|
+
outcome
|
|
627
|
+
]));
|
|
628
|
+
file.attemptOutcomes.clear();
|
|
629
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
630
|
+
if (!this.interactive) this.printFile(file);
|
|
631
|
+
this.schedule();
|
|
632
|
+
} else if ('step-start' === event.type) this.startStep(event.projectName, event.info);
|
|
633
|
+
else if ('step-end' === event.type) {
|
|
634
|
+
if (event.result) this.usage.add(event.result);
|
|
635
|
+
const key = 'case' === event.info.scope ? event.info.case.caseId : event.info.document.documentId;
|
|
636
|
+
const task = this.active.get(key);
|
|
637
|
+
if (task) if ('document' === event.info.scope) this.active.delete(key);
|
|
638
|
+
else {
|
|
639
|
+
task.step = void 0;
|
|
640
|
+
task.warning = void 0;
|
|
641
|
+
}
|
|
642
|
+
this.schedule();
|
|
643
|
+
} else if ('case-attempt-end' === event.type) this.attempts.set(event.attempt.caseId, {
|
|
644
|
+
run: event.attempt,
|
|
645
|
+
max: event.maxAttempts
|
|
646
|
+
});
|
|
647
|
+
else if ('case-end' === event.type || 'case-progress' === event.type) {
|
|
648
|
+
const file = this.file(event.outcome.projectName, event.outcome.sourcePath);
|
|
649
|
+
if ('case-progress' === event.type) {
|
|
650
|
+
file.attemptOutcomes.set(event.outcome.caseId, event.outcome);
|
|
651
|
+
file.retries = Math.max(file.retries, event.documentAttemptIndex);
|
|
652
|
+
} else {
|
|
653
|
+
file.attemptOutcomes.delete(event.outcome.caseId);
|
|
654
|
+
file.outcomes.set(event.outcome.caseId, event.outcome);
|
|
655
|
+
file.retries = Math.max(file.retries, event.documentRetries ?? 0);
|
|
656
|
+
}
|
|
657
|
+
this.active.delete(event.outcome.caseId);
|
|
658
|
+
this.schedule();
|
|
659
|
+
} else if ('document-retry' === event.type) {
|
|
660
|
+
const file = this.file(event.document.projectName, event.document.sourcePath);
|
|
661
|
+
file.outcomes.clear();
|
|
662
|
+
file.attemptOutcomes.clear();
|
|
663
|
+
file.status = void 0;
|
|
664
|
+
file.completionOrder = 0;
|
|
665
|
+
for (const [key, task] of this.active)if (task.file === file) this.active.delete(key);
|
|
666
|
+
this.log(this.color.yellow(` ↻ ${identity(file.projectName, file.sourcePath)} · file retry ${(event.document.attemptIndex ?? 0) + 2}/${event.maxAttempts}`));
|
|
667
|
+
this.schedule();
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
startStep(projectName, info) {
|
|
671
|
+
const position = 'case' === info.scope ? info.case : info.document;
|
|
672
|
+
const file = this.file(projectName, position.sourcePath);
|
|
673
|
+
file.started = true;
|
|
674
|
+
const key = 'case' === info.scope ? info.case.caseId : info.document.documentId;
|
|
675
|
+
const attempt = 'case' === info.scope ? info.case.attemptIndex : 0;
|
|
676
|
+
const previous = this.attempts.get(key);
|
|
677
|
+
if (previous && previous.run.attemptIndex < attempt) {
|
|
678
|
+
this.attempts.delete(key);
|
|
679
|
+
if ('default' === this.mode && !this.interactive) this.log(this.color.yellow(` ↻ ${identity(projectName, position.sourcePath, 'case' === info.scope ? info.case.name : void 0)} · case retry · attempt ${attempt + 1}/${previous.max}`));
|
|
680
|
+
}
|
|
681
|
+
const existing = this.active.get(key);
|
|
682
|
+
const now = Date.now();
|
|
683
|
+
this.active.set(key, {
|
|
684
|
+
file,
|
|
685
|
+
name: 'case' === info.scope ? info.case.name : void 0,
|
|
686
|
+
step: `${'steps' === position.phase ? 'step' : position.phase} ${position.stepIndex + 1}/${info.stepCount} · ${terminal_reporter_oneLine(info.node)}`,
|
|
687
|
+
since: existing?.attempt === attempt ? existing.since : now,
|
|
688
|
+
attempt,
|
|
689
|
+
maxAttempts: previous?.max ?? existing?.maxAttempts,
|
|
690
|
+
runId: 'case' === info.scope ? info.case.runId : info.document.documentRunId,
|
|
691
|
+
phase: position.phase,
|
|
692
|
+
stepIndex: position.stepIndex,
|
|
693
|
+
caseIndex: 'case' === info.scope ? info.case.caseIndex : void 0
|
|
694
|
+
});
|
|
695
|
+
this.schedule();
|
|
696
|
+
}
|
|
697
|
+
schedule() {
|
|
698
|
+
if (!this.interactive || !this.timer || this.scheduled) return;
|
|
699
|
+
this.scheduled = setTimeout(()=>{
|
|
700
|
+
this.scheduled = void 0;
|
|
701
|
+
this.renderProgress();
|
|
702
|
+
}, 100);
|
|
703
|
+
this.scheduled.unref();
|
|
704
|
+
}
|
|
705
|
+
counts(items, total) {
|
|
706
|
+
const failed = items.filter((item)=>'failed' === item.status).length;
|
|
707
|
+
const passed = items.filter((item)=>'success' === item.status).length;
|
|
708
|
+
const notRun = items.filter((item)=>'not-run' === item.status).length;
|
|
709
|
+
return this.stateCounts(passed, failed, notRun, total);
|
|
710
|
+
}
|
|
711
|
+
stateCounts(passed, failed, notRun, total) {
|
|
712
|
+
const c = this.color;
|
|
713
|
+
return [
|
|
714
|
+
failed ? c.red.bold(`${failed} failed`) : '',
|
|
715
|
+
c.green.bold(`${passed} passed`),
|
|
716
|
+
notRun ? c.yellow(`${notRun} not run`) : ''
|
|
717
|
+
].filter(Boolean).join(c.dim(' | ')) + c.dim(` (${total})`);
|
|
718
|
+
}
|
|
719
|
+
footer(durationMs = Date.now() - this.startedAt, result) {
|
|
720
|
+
const files = [
|
|
721
|
+
...this.files.values()
|
|
722
|
+
];
|
|
723
|
+
const cases = files.flatMap((file)=>[
|
|
724
|
+
...file.outcomes.values()
|
|
725
|
+
]);
|
|
726
|
+
const c = this.color;
|
|
727
|
+
const metrics = result ? this.finalMetrics : this.usage.snapshot();
|
|
728
|
+
return [
|
|
729
|
+
` ${c.dim('Test Files')} ${this.counts(files, this.totalFiles)}`,
|
|
730
|
+
` ${c.dim(' Cases')} ${result ? this.stateCounts(result.summary.passed, result.summary.failed, result.summary.notRun, result.summary.total) : this.counts(cases, this.total)}`,
|
|
731
|
+
` ${c.dim(' Start at')} ${new Date(this.startedAt).toLocaleTimeString('en-GB', {
|
|
732
|
+
hour12: false
|
|
733
|
+
})}`,
|
|
734
|
+
` ${c.dim(' Duration')} ${formatDuration(durationMs)}`,
|
|
735
|
+
...formatModelUsage(metrics).map((model, index)=>` ${c.dim(index ? ' ' : ' Models:')} ${model}`),
|
|
736
|
+
...!result || this.warnings.size ? [
|
|
737
|
+
` ${c.dim(' Warnings')} ${this.warnings.size ? c.yellow(String(this.warnings.size)) : c.dim('0')}${!result && this.latestNotice ? c.dim(` · ${this.latestNotice}`) : ''}`
|
|
738
|
+
] : []
|
|
739
|
+
];
|
|
740
|
+
}
|
|
741
|
+
renderProgress(render = true) {
|
|
742
|
+
if (!this.interactive || !this.timer) return;
|
|
743
|
+
const projects = new Map();
|
|
744
|
+
const files = [
|
|
745
|
+
...this.files.values()
|
|
746
|
+
].filter((file)=>file.started && !file.printed).sort((a, b)=>a.projectName.localeCompare(b.projectName) || a.sourcePath.localeCompare(b.sourcePath));
|
|
747
|
+
for (const file of files){
|
|
748
|
+
const group = projects.get(file.projectName) ?? [];
|
|
749
|
+
group.push(file);
|
|
750
|
+
projects.set(file.projectName, group);
|
|
751
|
+
}
|
|
752
|
+
for (const projectName of this.projectPhases.keys())if (!projects.has(projectName)) projects.set(projectName, []);
|
|
753
|
+
const footer = this.footer();
|
|
754
|
+
const budget = Math.max(0, (this.io.rows ?? 24) - 2 - footer.length - 1);
|
|
755
|
+
let lines = this.liveTree(projects, 'full');
|
|
756
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'recent');
|
|
757
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'compact');
|
|
758
|
+
if (lines.length > budget) lines = this.liveTree(projects, 'active');
|
|
759
|
+
if (lines.length) lines.push('');
|
|
760
|
+
if (render) this.progressView.render(lines, footer);
|
|
761
|
+
else this.progressView.update(lines, footer);
|
|
762
|
+
}
|
|
763
|
+
liveTree(projects, mode) {
|
|
764
|
+
const c = this.color;
|
|
765
|
+
const lines = [];
|
|
766
|
+
const groups = [
|
|
767
|
+
...projects
|
|
768
|
+
];
|
|
769
|
+
const isActive = ([projectName, files])=>this.projectPhases.has(projectName) || files.some((file)=>!file.status);
|
|
770
|
+
if ('full' !== mode) groups.sort((a, b)=>Number(isActive(b)) - Number(isActive(a)));
|
|
771
|
+
for (const [projectName, files] of groups){
|
|
772
|
+
const phase = this.projectPhases.get(projectName);
|
|
773
|
+
const running = files.filter((file)=>!file.status);
|
|
774
|
+
const completed = files.filter((file)=>file.status);
|
|
775
|
+
const recent = [
|
|
776
|
+
...completed
|
|
777
|
+
].sort((a, b)=>b.completionOrder - a.completionOrder).slice(0, 'recent' === mode ? 1 : 0);
|
|
778
|
+
const visible = 'full' === mode ? files : [
|
|
779
|
+
...running,
|
|
780
|
+
...recent
|
|
781
|
+
];
|
|
782
|
+
const hidden = completed.filter((file)=>!visible.includes(file));
|
|
783
|
+
const hiddenFailures = hidden.filter((file)=>'failed' === file.status).length;
|
|
784
|
+
const details = [
|
|
785
|
+
phase ? c.yellow(`${projectPhaseLabel(phase.phase)} ${formatDuration(Date.now() - phase.since)}`) : '',
|
|
786
|
+
c.yellow(`${running.length} ${1 === running.length ? 'file' : 'files'} running`),
|
|
787
|
+
completed.length ? c.dim(`${completed.length} completed`) : '',
|
|
788
|
+
'active' === mode && hiddenFailures ? c.red(`${hiddenFailures} failed`) : ''
|
|
789
|
+
].filter(Boolean);
|
|
790
|
+
lines.push(this.liveRow(` ${c.yellow('❯')} ${c.bold(terminal_reporter_oneLine(projectName))}`, c.dim(' · ') + details.join(c.dim(' · '))));
|
|
791
|
+
for (const [fileIndex, file] of visible.entries()){
|
|
792
|
+
const lastFile = fileIndex === visible.length - 1 && (!hidden.length || 'active' === mode);
|
|
793
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
794
|
+
const outcomes = [
|
|
795
|
+
...new Map([
|
|
796
|
+
...file.attemptOutcomes,
|
|
797
|
+
...file.outcomes
|
|
798
|
+
]).values()
|
|
799
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
800
|
+
const failedCases = outcomes.filter((outcome)=>'failed' === outcome.status).length;
|
|
801
|
+
lines.push(this.liveRow(` ${c.dim(lastFile ? '└─' : '├─')} ${file.status ? `${this.statusSymbol(file.status)} ` : ''}${terminal_reporter_oneLine(file.sourcePath)}`, ` ${c.dim(`${outcomes.length}/${file.total} cases`)}${file.status ? c.dim(` ${formatDuration(file.durationMs)}`) : ''}${'active' === mode && failedCases ? c.red(` · ${failedCases} failed`) : ''}`));
|
|
802
|
+
const tasks = [
|
|
803
|
+
...this.active.values()
|
|
804
|
+
].filter((task)=>task.file === file);
|
|
805
|
+
const shown = 'full' === mode ? outcomes : 'recent' === mode ? outcomes.slice(-1) : [];
|
|
806
|
+
const children = [
|
|
807
|
+
...tasks.map((task)=>({
|
|
808
|
+
task
|
|
809
|
+
})),
|
|
810
|
+
...shown.map((outcome)=>({
|
|
811
|
+
outcome
|
|
812
|
+
}))
|
|
813
|
+
];
|
|
814
|
+
if ('full' === mode) children.sort((a, b)=>this.liveChildIndex(a) - this.liveChildIndex(b));
|
|
815
|
+
const hiddenCases = outcomes.filter((outcome)=>!shown.includes(outcome));
|
|
816
|
+
if (hiddenCases.length && 'active' !== mode) {
|
|
817
|
+
const failed = hiddenCases.filter((outcome)=>'failed' === outcome.status).length;
|
|
818
|
+
children.push({
|
|
819
|
+
summary: `… ${hiddenCases.length} completed ${1 === hiddenCases.length ? 'case' : 'cases'}${failed ? ` · ${failed} failed` : ''}`,
|
|
820
|
+
failed: failed > 0
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
for (const [index, child] of children.entries()){
|
|
824
|
+
const last = index === children.length - 1;
|
|
825
|
+
const prefix = `${stem}${c.dim(last ? '└─' : '├─')} `;
|
|
826
|
+
if ('outcome' in child) {
|
|
827
|
+
lines.push(this.liveRow(`${prefix}${this.formatCaseResult(child.outcome, file.retries)}`, ''));
|
|
828
|
+
continue;
|
|
829
|
+
}
|
|
830
|
+
if ('summary' in child) {
|
|
831
|
+
lines.push(this.liveRow(`${prefix}${child.failed ? c.red(child.summary) : c.dim(child.summary)}`, ''));
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const { task } = child;
|
|
835
|
+
const hint = task.name && task.step ? truncateTerminalLine(` · ${task.step}`, Math.floor(Math.max(0, (this.io.columns ?? 100) - 1) / 2)) : '';
|
|
836
|
+
const retry = task.attempt ? ` · attempt ${task.attempt + 1}${task.maxAttempts ? `/${task.maxAttempts}` : ''}` : '';
|
|
837
|
+
lines.push(this.liveRow(`${prefix}${terminal_reporter_oneLine(task.name ?? task.step ?? 'file lifecycle')}`, `${c.dim(hint)} ${c.yellow(formatDuration(Date.now() - task.since))}${c.yellow(retry)}`));
|
|
838
|
+
if (task.warning) lines.push(this.liveRow(`${stem}${last ? ' ' : `${c.dim('│')} `}${c.dim('└─')} ${c.yellow(`⚠ ${task.warning}`)}`, ''));
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (hidden.length && 'active' !== mode) {
|
|
842
|
+
const summary = `… ${hidden.length} completed ${1 === hidden.length ? 'file' : 'files'}${hiddenFailures ? ` · ${hiddenFailures} failed` : ''}`;
|
|
843
|
+
lines.push(this.liveRow(` ${c.dim('└─')} ${hiddenFailures ? c.red(summary) : c.dim(summary)}`, ''));
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return lines;
|
|
847
|
+
}
|
|
848
|
+
liveChildIndex(child) {
|
|
849
|
+
if ('outcome' in child) return child.outcome.caseIndex;
|
|
850
|
+
if ('summary' in child) return Number.MAX_SAFE_INTEGER;
|
|
851
|
+
return child.task.caseIndex ?? ('beforeAll' === child.task.phase ? -1 : Number.MAX_SAFE_INTEGER);
|
|
852
|
+
}
|
|
853
|
+
liveRow(label, suffix) {
|
|
854
|
+
const columns = Math.max(0, (this.io.columns ?? 100) - 1);
|
|
855
|
+
const tail = truncateTerminalLine(suffix, columns);
|
|
856
|
+
return truncateTerminalLine(label, columns - external_string_width_default()(tail)) + tail;
|
|
857
|
+
}
|
|
858
|
+
tick() {
|
|
859
|
+
if (this.interactive) return void this.renderProgress();
|
|
860
|
+
if ((this.active.size || this.projectPhases.size) && Date.now() - this.lastOutput >= 30000) this.log(`… Still running: ${[
|
|
861
|
+
...[
|
|
862
|
+
...this.projectPhases
|
|
863
|
+
].map(([projectName, phase])=>`[${terminal_reporter_oneLine(projectName)}] · ${projectPhaseLabel(phase.phase)} (${formatDuration(Date.now() - phase.since)})`),
|
|
864
|
+
...[
|
|
865
|
+
...this.active.values()
|
|
866
|
+
].map((task)=>`${identity(task.file.projectName, task.file.sourcePath, task.name)} · ${task.step ?? 'between steps'} (${formatDuration(Date.now() - task.since)})`)
|
|
867
|
+
].join(' | ')}`);
|
|
868
|
+
}
|
|
869
|
+
formatCaseResult(outcome, documentRetries) {
|
|
870
|
+
const c = this.color;
|
|
871
|
+
const attempts = outcome.attempts ?? (outcome.run ? [
|
|
872
|
+
outcome.run
|
|
873
|
+
] : []);
|
|
874
|
+
const retries = Math.max(attempts.length - 1, outcome.run?.attemptIndex ?? 0);
|
|
875
|
+
const duration = formatDuration(attempts.reduce((sum, attempt)=>sum + attempt.durationMs, 0));
|
|
876
|
+
const symbol = 'success' === outcome.status ? c.green('✓') : 'failed' === outcome.status ? c.red('×') : c.yellow('–');
|
|
877
|
+
const detail = 'not-run' === outcome.status ? c.dim(` · ${outcome.notRunReason ?? 'not run'}`) : ` ${c.dim(duration)}${retries ? c.yellow(` · ${'success' === outcome.status ? 'passed after retry' : 'failed after retries'} (${retries + 1} attempts)`) : documentRetries ? c.yellow(` · file retried ${documentRetries}x`) : ''}`;
|
|
878
|
+
return `${symbol} ${terminal_reporter_oneLine(outcome.name)}${detail}`;
|
|
879
|
+
}
|
|
880
|
+
statusSymbol(status) {
|
|
881
|
+
return 'failed' === status ? this.color.red('×') : 'not-run' === status ? this.color.yellow('–') : this.color.green('✓');
|
|
882
|
+
}
|
|
883
|
+
resultFileLines(file, lastFile) {
|
|
884
|
+
const c = this.color;
|
|
885
|
+
const stem = lastFile ? ' ' : ` ${c.dim('│')} `;
|
|
886
|
+
const cases = [
|
|
887
|
+
...file.outcomes.values()
|
|
888
|
+
].sort((a, b)=>a.caseIndex - b.caseIndex);
|
|
889
|
+
const failed = cases.filter((item)=>'failed' === item.status).length;
|
|
890
|
+
const notRun = cases.filter((item)=>'not-run' === item.status).length;
|
|
891
|
+
const stats = `(${cases.length} cases${failed ? ` | ${failed} failed` : ''}${notRun ? ` | ${notRun} not run` : ''})`;
|
|
892
|
+
return [
|
|
893
|
+
` ${c.dim(lastFile ? '└─' : '├─')} ${this.statusSymbol(file.status)} ${c.bold(terminal_reporter_oneLine(file.sourcePath))} ${c.dim(stats)} ${c.dim(formatDuration(file.durationMs))}`,
|
|
894
|
+
...cases.map((item, caseIndex)=>`${stem}${c.dim(caseIndex === cases.length - 1 ? '└─' : '├─')} ${this.formatCaseResult(item, file.retries)}`)
|
|
895
|
+
];
|
|
896
|
+
}
|
|
897
|
+
printFile(file) {
|
|
898
|
+
if (file.printed) return;
|
|
899
|
+
file.printed = true;
|
|
900
|
+
this.log([
|
|
901
|
+
'',
|
|
902
|
+
` ${this.color.bold(terminal_reporter_oneLine(file.projectName))}`,
|
|
903
|
+
...this.resultFileLines(file, true)
|
|
904
|
+
].join('\n'));
|
|
905
|
+
}
|
|
906
|
+
printProject(projectName, files, failed) {
|
|
907
|
+
if (!files.length || files.every((file)=>file.printed)) return;
|
|
908
|
+
const c = this.color;
|
|
909
|
+
const status = failed || files.some((file)=>'failed' === file.status) ? 'failed' : files.every((file)=>'not-run' === file.status) ? 'not-run' : files.some((file)=>'not-run' === file.status) ? 'failed' : 'success';
|
|
910
|
+
const lines = [
|
|
911
|
+
'',
|
|
912
|
+
` ${this.statusSymbol(status)} ${c.bold(terminal_reporter_oneLine(projectName))} ${c.dim(`(${files.length} ${1 === files.length ? 'file' : 'files'})`)}`
|
|
913
|
+
];
|
|
914
|
+
for (const [index, file] of files.entries())if (!file.printed) {
|
|
915
|
+
file.printed = true;
|
|
916
|
+
lines.push(...this.resultFileLines(file, index === files.length - 1));
|
|
917
|
+
}
|
|
918
|
+
this.log(lines.join('\n'));
|
|
919
|
+
}
|
|
920
|
+
failure(source, error, context) {
|
|
921
|
+
this.failures.push({
|
|
922
|
+
source,
|
|
923
|
+
error,
|
|
924
|
+
context
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
completeProject(project) {
|
|
928
|
+
this.projectPhases.delete(project.name);
|
|
929
|
+
const projectFiles = new Set();
|
|
930
|
+
const documents = new Map(project.documents.map((document)=>[
|
|
931
|
+
document.documentId,
|
|
932
|
+
document
|
|
933
|
+
]));
|
|
934
|
+
const resultSources = new Set([
|
|
935
|
+
...project.cases.map((outcome)=>outcome.sourcePath),
|
|
936
|
+
...project.documents.map((document)=>document.sourcePath),
|
|
937
|
+
...project.collectionErrors.map((error)=>error.sourcePath)
|
|
938
|
+
]);
|
|
939
|
+
for (const plan of project.plans ?? []){
|
|
940
|
+
const selected = plan.cases.filter((item)=>item.selected).length;
|
|
941
|
+
if (selected || resultSources.has(plan.sourcePath)) projectFiles.add(this.file(project.name, plan.sourcePath, selected));
|
|
942
|
+
}
|
|
943
|
+
for (const outcome of project.cases)this.file(project.name, outcome.sourcePath).outcomes.set(outcome.caseId, outcome);
|
|
944
|
+
for (const document of documents.values()){
|
|
945
|
+
const file = this.file(project.name, document.sourcePath);
|
|
946
|
+
file.status = document.status;
|
|
947
|
+
file.retries = document.attemptIndex ?? 0;
|
|
948
|
+
file.durationMs = project.documents.filter((item)=>item.documentId === document.documentId).reduce((sum, item)=>sum + item.durationMs, 0);
|
|
949
|
+
}
|
|
950
|
+
for (const error of project.collectionErrors)this.file(project.name, error.sourcePath).status = 'failed';
|
|
951
|
+
for (const file of this.files.values())if (file.projectName === project.name) {
|
|
952
|
+
file.attemptOutcomes.clear();
|
|
953
|
+
const cases = [
|
|
954
|
+
...file.outcomes.values()
|
|
955
|
+
];
|
|
956
|
+
file.status = fileStatus(file.status, cases);
|
|
957
|
+
projectFiles.add(file);
|
|
958
|
+
}
|
|
959
|
+
for (const [key, task] of this.active)if (task.file.projectName === project.name) this.active.delete(key);
|
|
960
|
+
if (this.interactive) this.printProject(project.name, [
|
|
961
|
+
...projectFiles
|
|
962
|
+
], 'failed' === project.status);
|
|
963
|
+
else for (const file of projectFiles)this.printFile(file);
|
|
964
|
+
}
|
|
965
|
+
finish(result) {
|
|
966
|
+
this.dispose();
|
|
967
|
+
this.finalMetrics = result.metrics ?? this.usage.snapshot();
|
|
968
|
+
if (result.startedAt) this.startedAt = Date.parse(result.startedAt);
|
|
969
|
+
for (const project of result.projects)this.completeProject(project);
|
|
970
|
+
this.total = result.summary.total;
|
|
971
|
+
this.totalFiles = this.files.size;
|
|
972
|
+
}
|
|
973
|
+
sectionHeading(label, kind) {
|
|
974
|
+
const c = this.color;
|
|
975
|
+
const width = Math.max(0, (this.io.columns ?? 80) - 2);
|
|
976
|
+
const title = truncateTerminalLine(` ${label} `, width);
|
|
977
|
+
const padding = Math.max(0, width - external_string_width_default()(title));
|
|
978
|
+
const left = Math.floor(padding / 2);
|
|
979
|
+
const line = 'warning' === kind ? c.yellow : c.red;
|
|
980
|
+
const badge = 'warning' === kind ? c.bgYellow.black.bold : c.bgRed.white.bold;
|
|
981
|
+
this.log(`\n ${line('─'.repeat(left))}${badge(title)}${line('─'.repeat(padding - left))}\n`);
|
|
982
|
+
}
|
|
983
|
+
summary(result) {
|
|
984
|
+
const c = this.color;
|
|
985
|
+
if (this.interactive && this.warnings.size) {
|
|
986
|
+
this.sectionHeading(`Warnings ${this.warnings.size}`, 'warning');
|
|
987
|
+
for (const warning of [
|
|
988
|
+
...this.warnings.values()
|
|
989
|
+
].slice(0, 5))this.log(` ${c.yellow('WARN')} ${c.dim(warning.scope)}${warning.message}`);
|
|
990
|
+
if (this.warnings.size > 5) this.log(c.dim(` … ${this.warnings.size - 5} more warnings; see debug logs or --reporter verbose.`));
|
|
991
|
+
}
|
|
992
|
+
if (this.failures.length) {
|
|
993
|
+
this.sectionHeading(`Failures ${this.failures.length}`, 'failure');
|
|
994
|
+
for (const failure of this.failures){
|
|
995
|
+
if ('default' === this.mode) {
|
|
996
|
+
const detail = formatFailureDetails(failure.source, failure.error, failure.context);
|
|
997
|
+
const body = detail.body.map((line)=>{
|
|
998
|
+
if (line.startsWith(' Error:')) return c.red(line);
|
|
999
|
+
if (line.startsWith(' at ')) return c.dim(line);
|
|
1000
|
+
return line.replace(/^ {2}(?:Step|Location|Attempt|File try|Phase|Input)\s+/, (label)=>c.dim(label));
|
|
1001
|
+
}).join('\n');
|
|
1002
|
+
const block = `${c.bgRed.white.bold(' FAIL ')} ${c.bold(detail.title)}\n${body}\n`;
|
|
1003
|
+
if (this.interactive) this.log(block);
|
|
1004
|
+
else this.io.error(`FAIL ${detail.title}\n${detail.body.join('\n')}\n`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
const message = formatCliError(failure.error);
|
|
1008
|
+
this.io.error(`FAIL midscene-test: ${failure.source}:\n${message.split('\n').map((line)=>` ${line}`).join('\n')}\n`);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
this.log(`\n${this.footer(result.durationMs, result).join('\n')}`);
|
|
1012
|
+
this.log('');
|
|
1013
|
+
this.failures = [];
|
|
1014
|
+
}
|
|
1015
|
+
dispose() {
|
|
1016
|
+
if (this.timer) clearInterval(this.timer);
|
|
1017
|
+
if (this.scheduled) clearTimeout(this.scheduled);
|
|
1018
|
+
this.timer = void 0;
|
|
1019
|
+
this.scheduled = void 0;
|
|
1020
|
+
this.removeResizeListener?.();
|
|
1021
|
+
this.removeResizeListener = void 0;
|
|
1022
|
+
this.progressView.clear();
|
|
1023
|
+
}
|
|
1024
|
+
constructor(io, mode = 'default'){
|
|
1025
|
+
terminal_reporter_define_property(this, "io", void 0);
|
|
1026
|
+
terminal_reporter_define_property(this, "mode", void 0);
|
|
1027
|
+
terminal_reporter_define_property(this, "total", void 0);
|
|
1028
|
+
terminal_reporter_define_property(this, "completionOrder", void 0);
|
|
1029
|
+
terminal_reporter_define_property(this, "totalFiles", void 0);
|
|
1030
|
+
terminal_reporter_define_property(this, "files", void 0);
|
|
1031
|
+
terminal_reporter_define_property(this, "warnings", void 0);
|
|
1032
|
+
terminal_reporter_define_property(this, "latestNotice", void 0);
|
|
1033
|
+
terminal_reporter_define_property(this, "usage", void 0);
|
|
1034
|
+
terminal_reporter_define_property(this, "finalMetrics", void 0);
|
|
1035
|
+
terminal_reporter_define_property(this, "attempts", void 0);
|
|
1036
|
+
terminal_reporter_define_property(this, "active", void 0);
|
|
1037
|
+
terminal_reporter_define_property(this, "projectPhases", void 0);
|
|
1038
|
+
terminal_reporter_define_property(this, "failures", void 0);
|
|
1039
|
+
terminal_reporter_define_property(this, "lastOutput", void 0);
|
|
1040
|
+
terminal_reporter_define_property(this, "startedAt", void 0);
|
|
1041
|
+
terminal_reporter_define_property(this, "timer", void 0);
|
|
1042
|
+
terminal_reporter_define_property(this, "scheduled", void 0);
|
|
1043
|
+
terminal_reporter_define_property(this, "progressView", void 0);
|
|
1044
|
+
terminal_reporter_define_property(this, "removeResizeListener", void 0);
|
|
1045
|
+
terminal_reporter_define_property(this, "color", void 0);
|
|
1046
|
+
this.io = io;
|
|
1047
|
+
this.mode = mode;
|
|
1048
|
+
this.total = 0;
|
|
1049
|
+
this.completionOrder = 0;
|
|
1050
|
+
this.totalFiles = 0;
|
|
1051
|
+
this.files = new Map();
|
|
1052
|
+
this.warnings = new Map();
|
|
1053
|
+
this.usage = new CliModelUsage();
|
|
1054
|
+
this.attempts = new Map();
|
|
1055
|
+
this.active = new Map();
|
|
1056
|
+
this.projectPhases = new Map();
|
|
1057
|
+
this.failures = [];
|
|
1058
|
+
this.lastOutput = Date.now();
|
|
1059
|
+
this.startedAt = Date.now();
|
|
1060
|
+
this.progressView = new TerminalProgress(io);
|
|
1061
|
+
this.color = new (external_chalk_default()).Instance({
|
|
1062
|
+
level: io.isTTY && !process.env.NO_COLOR ? 1 : 0
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const escapeData = (text)=>text.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
1067
|
+
const escapeProperty = (text)=>escapeData(text).replace(/:/g, '%3A').replace(/,/g, '%2C');
|
|
1068
|
+
const markdown = (text)=>text.replace(/[&<>`\r\n]/g, (char)=>({
|
|
1069
|
+
'&': '&',
|
|
1070
|
+
'<': '<',
|
|
1071
|
+
'>': '>',
|
|
1072
|
+
'`': '`',
|
|
1073
|
+
'\r': ' ',
|
|
1074
|
+
'\n': ' '
|
|
1075
|
+
})[char]);
|
|
1076
|
+
function githubFailureAnnotation(source, message, file) {
|
|
1077
|
+
if ('true' !== process.env.GITHUB_ACTIONS) return;
|
|
1078
|
+
const workspace = process.env.GITHUB_WORKSPACE ?? process.cwd();
|
|
1079
|
+
const path = file ? (0, external_node_path_.relative)(workspace, (0, external_node_path_.resolve)(file)).split(external_node_path_.sep).join('/') : void 0;
|
|
1080
|
+
return `::error ${path ? `file=${escapeProperty(path)},` : ''}title=${escapeProperty(source)}::${escapeData(message)}`;
|
|
1081
|
+
}
|
|
1082
|
+
function writeGitHubSummary(result) {
|
|
1083
|
+
const output = process.env.GITHUB_STEP_SUMMARY;
|
|
1084
|
+
if ('true' !== process.env.GITHUB_ACTIONS || !output) return;
|
|
1085
|
+
const failedCases = [
|
|
1086
|
+
...new Map(result.cases.map((item)=>[
|
|
1087
|
+
item.caseId,
|
|
1088
|
+
item
|
|
1089
|
+
])).values()
|
|
1090
|
+
].filter((item)=>'failed' === item.status);
|
|
1091
|
+
const summary = [
|
|
1092
|
+
'## Midscene Test',
|
|
1093
|
+
'',
|
|
1094
|
+
'| Passed | Failed | Not run | Passed after retry | Duration |',
|
|
1095
|
+
'| ---: | ---: | ---: | ---: | ---: |',
|
|
1096
|
+
`| ${result.summary.passed} | ${result.summary.failed} | ${result.summary.notRun} | ${result.summary.passedAfterRetry} | ${formatDuration(result.durationMs)} |`,
|
|
1097
|
+
'',
|
|
1098
|
+
'Passed after retry is included in Passed.',
|
|
1099
|
+
'',
|
|
1100
|
+
...failedCases.map((item)=>`- Failed: **${markdown(item.name)}** — ${markdown(item.projectName)}/\`${markdown(item.sourcePath)}\``),
|
|
1101
|
+
...result.summary.collectionErrors ? [
|
|
1102
|
+
`- Collection errors: ${result.summary.collectionErrors}`
|
|
1103
|
+
] : [],
|
|
1104
|
+
...result.summary.documentFailures ? [
|
|
1105
|
+
`- Failed files: ${result.summary.documentFailures}`
|
|
1106
|
+
] : [],
|
|
1107
|
+
...result.summary.projectFailures ? [
|
|
1108
|
+
`- Failed projects: ${result.summary.projectFailures}`
|
|
1109
|
+
] : [],
|
|
1110
|
+
'',
|
|
1111
|
+
...result.reportPath ? [
|
|
1112
|
+
`Report artifact entry: \`${markdown(result.reportPath)}\``
|
|
1113
|
+
] : [],
|
|
1114
|
+
`Structured results: \`${markdown(result.summaryPath)}\``,
|
|
1115
|
+
''
|
|
1116
|
+
].join('\n');
|
|
1117
|
+
(0, external_node_fs_.appendFileSync)(output, `${summary}\n`);
|
|
1118
|
+
}
|
|
103
1119
|
const v4_namespaceObject = require("zod/v4");
|
|
104
1120
|
const GENERATED_COMMENT = '<!-- Generated by `midscene-test nodes`. Do not edit directly. -->';
|
|
105
1121
|
const sortNodesForSpec = (nodes)=>{
|
|
@@ -233,7 +1249,32 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
233
1249
|
warnings
|
|
234
1250
|
};
|
|
235
1251
|
};
|
|
236
|
-
|
|
1252
|
+
function captureTerminalOutput(output, streams = [
|
|
1253
|
+
process.stdout,
|
|
1254
|
+
process.stderr
|
|
1255
|
+
]) {
|
|
1256
|
+
const cleanups = [];
|
|
1257
|
+
for (const stream of streams){
|
|
1258
|
+
const original = stream.write;
|
|
1259
|
+
const forward = original.bind(stream);
|
|
1260
|
+
const intercepted = (chunk, encoding, callback)=>{
|
|
1261
|
+
const done = 'function' == typeof encoding ? encoding : callback;
|
|
1262
|
+
const message = 'string' == typeof chunk ? chunk : Buffer.from(chunk).toString();
|
|
1263
|
+
let accepted = true;
|
|
1264
|
+
output(message, (text)=>{
|
|
1265
|
+
accepted = forward(text, done);
|
|
1266
|
+
});
|
|
1267
|
+
return accepted;
|
|
1268
|
+
};
|
|
1269
|
+
stream.write = intercepted;
|
|
1270
|
+
cleanups.push(()=>{
|
|
1271
|
+
if (stream.write === intercepted) stream.write = original;
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
return ()=>{
|
|
1275
|
+
for (const cleanup of cleanups)cleanup();
|
|
1276
|
+
};
|
|
1277
|
+
}
|
|
237
1278
|
const api_namespaceObject = require("tsx/cjs/api");
|
|
238
1279
|
const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
|
|
239
1280
|
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
@@ -353,9 +1394,19 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
353
1394
|
if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
|
|
354
1395
|
return value;
|
|
355
1396
|
};
|
|
356
|
-
const
|
|
1397
|
+
const validateConcurrency = (value, label)=>{
|
|
1398
|
+
if (null === value) return null;
|
|
1399
|
+
return validatePositiveInteger(value, 1, label);
|
|
1400
|
+
};
|
|
1401
|
+
const validateFileConcurrency = (maxConcurrentFiles, fileSetup, label)=>{
|
|
1402
|
+
if (maxConcurrentFiles > 1 && !fileSetup) throw new TypeError(`Midscene config ${label}.fileSetup is required when the effective maxConcurrentFiles is greater than 1.`);
|
|
1403
|
+
};
|
|
1404
|
+
const validateExecutionProjects = (value, defaultSetup, defaultFileSetup, globalNodes, test)=>{
|
|
357
1405
|
if (void 0 === value) {
|
|
358
1406
|
const setup = validateSetup(defaultSetup, 'setup');
|
|
1407
|
+
const fileSetup = validateSetup(defaultFileSetup, 'fileSetup');
|
|
1408
|
+
const maxConcurrentFiles = test.maxConcurrentFiles ?? 1 / 0;
|
|
1409
|
+
validateFileConcurrency(maxConcurrentFiles, fileSetup, 'default');
|
|
359
1410
|
return {
|
|
360
1411
|
hasExplicitProjects: false,
|
|
361
1412
|
projects: Object.freeze([
|
|
@@ -365,6 +1416,10 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
365
1416
|
...setup ? {
|
|
366
1417
|
setup
|
|
367
1418
|
} : {},
|
|
1419
|
+
...fileSetup ? {
|
|
1420
|
+
fileSetup
|
|
1421
|
+
} : {},
|
|
1422
|
+
maxConcurrentFiles,
|
|
368
1423
|
tags: Object.freeze({
|
|
369
1424
|
include: [],
|
|
370
1425
|
exclude: []
|
|
@@ -377,6 +1432,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
377
1432
|
};
|
|
378
1433
|
}
|
|
379
1434
|
if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
|
|
1435
|
+
if (void 0 !== defaultFileSetup) throw new TypeError('Midscene config fileSetup cannot be used together with projects. Move fileSetup to projects[].fileSetup.');
|
|
380
1436
|
if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
|
|
381
1437
|
const names = new Set();
|
|
382
1438
|
const projects = value.map((candidate, index)=>{
|
|
@@ -385,6 +1441,8 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
385
1441
|
rejectUnknownKeys(candidate, [
|
|
386
1442
|
'name',
|
|
387
1443
|
'setup',
|
|
1444
|
+
'fileSetup',
|
|
1445
|
+
'maxConcurrentFiles',
|
|
388
1446
|
'nodes',
|
|
389
1447
|
'files',
|
|
390
1448
|
'tags',
|
|
@@ -401,10 +1459,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
401
1459
|
...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
|
|
402
1460
|
...localNodes.definitions()
|
|
403
1461
|
]);
|
|
1462
|
+
const fileSetup = validateSetup(candidate.fileSetup, `${label}.fileSetup`);
|
|
1463
|
+
const globalFileLimit = test.maxConcurrentFiles ?? 1 / 0;
|
|
1464
|
+
const maxConcurrentFiles = Math.min(globalFileLimit, validatePositiveInteger(candidate.maxConcurrentFiles, globalFileLimit, `${label}.maxConcurrentFiles`));
|
|
1465
|
+
validateFileConcurrency(maxConcurrentFiles, fileSetup, label);
|
|
404
1466
|
return Object.freeze({
|
|
405
1467
|
projectId: projectIdFromIndex(index),
|
|
406
1468
|
name: candidate.name,
|
|
407
1469
|
nodes,
|
|
1470
|
+
...fileSetup ? {
|
|
1471
|
+
fileSetup
|
|
1472
|
+
} : {},
|
|
1473
|
+
maxConcurrentFiles,
|
|
408
1474
|
...files ? {
|
|
409
1475
|
files
|
|
410
1476
|
} : {},
|
|
@@ -421,11 +1487,25 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
421
1487
|
hasExplicitProjects: true
|
|
422
1488
|
};
|
|
423
1489
|
};
|
|
424
|
-
const validateTestOptions = (value)=>{
|
|
1490
|
+
const validateTestOptions = (value, hasExplicitProjects)=>{
|
|
425
1491
|
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
426
1492
|
const candidate = value ?? {};
|
|
1493
|
+
if (void 0 !== candidate.maxConcurrency) {
|
|
1494
|
+
const limit = 'number' == typeof candidate.maxConcurrency && Number.isInteger(candidate.maxConcurrency) && candidate.maxConcurrency > 0 ? candidate.maxConcurrency : 1;
|
|
1495
|
+
throw new TypeError([
|
|
1496
|
+
'Midscene config test.maxConcurrency is no longer supported.',
|
|
1497
|
+
'It controlled project concurrency, not file concurrency. Remove it and use:',
|
|
1498
|
+
` test: { maxConcurrentProjects: ${limit}, maxConcurrentFiles: ${hasExplicitProjects ? limit : 1} }`,
|
|
1499
|
+
hasExplicitProjects ? ' Set projects[].maxConcurrentFiles to 1 to keep files sequential within each project.' : ' The implicit default project keeps running files sequentially.',
|
|
1500
|
+
hasExplicitProjects ? 'For parallel files, configure fileSetup and raise both test.maxConcurrentFiles and projects[].maxConcurrentFiles.' : 'For parallel files, configure fileSetup and raise test.maxConcurrentFiles.',
|
|
1501
|
+
'Migration guide: https://midscenejs.com/midscene-test/configuration.html#existing-configurations'
|
|
1502
|
+
].join('\n'));
|
|
1503
|
+
}
|
|
427
1504
|
return Object.freeze({
|
|
428
|
-
|
|
1505
|
+
maxConcurrentFiles: validateConcurrency(candidate.maxConcurrentFiles, 'test.maxConcurrentFiles'),
|
|
1506
|
+
maxConcurrentProjects: validateConcurrency(candidate.maxConcurrentProjects, 'test.maxConcurrentProjects'),
|
|
1507
|
+
setupTimeout: validatePositiveInteger(candidate.setupTimeout, 120000, 'test.setupTimeout'),
|
|
1508
|
+
teardownTimeout: validatePositiveInteger(candidate.teardownTimeout, 30000, 'test.teardownTimeout'),
|
|
429
1509
|
bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
|
|
430
1510
|
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
|
|
431
1511
|
});
|
|
@@ -455,16 +1535,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
455
1535
|
if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
|
|
456
1536
|
rejectUnknownKeys(definition, [
|
|
457
1537
|
'setup',
|
|
1538
|
+
'fileSetup',
|
|
458
1539
|
'projects',
|
|
459
1540
|
'test',
|
|
460
1541
|
'output',
|
|
461
1542
|
'nodes'
|
|
462
1543
|
], 'root');
|
|
463
1544
|
const nodes = new NodeRegistry(definition.nodes);
|
|
464
|
-
const
|
|
1545
|
+
const test = validateTestOptions(definition.test, void 0 !== definition.projects);
|
|
1546
|
+
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, definition.fileSetup, nodes, test);
|
|
465
1547
|
return {
|
|
466
1548
|
...resolvedProjects,
|
|
467
|
-
test
|
|
1549
|
+
test,
|
|
468
1550
|
output: validateOutput(definition.output),
|
|
469
1551
|
nodes,
|
|
470
1552
|
resolveNode: (name)=>nodes.get(name)
|
|
@@ -544,11 +1626,13 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
544
1626
|
start: state.position + sourceOffset,
|
|
545
1627
|
end: 0,
|
|
546
1628
|
value: null,
|
|
547
|
-
children: []
|
|
1629
|
+
children: [],
|
|
1630
|
+
kind: null
|
|
548
1631
|
});
|
|
549
1632
|
const node = stack.pop();
|
|
550
1633
|
node.end = state.position + sourceOffset;
|
|
551
1634
|
node.value = state.result;
|
|
1635
|
+
node.kind = state.kind;
|
|
552
1636
|
stack.at(-1)?.children.push(node);
|
|
553
1637
|
nodes.push(node);
|
|
554
1638
|
if ('mapping' !== state.kind) return;
|
|
@@ -578,6 +1662,36 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
578
1662
|
const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
|
|
579
1663
|
return {
|
|
580
1664
|
parsed,
|
|
1665
|
+
locate (path) {
|
|
1666
|
+
let node = nodes.at(-1);
|
|
1667
|
+
for (const part of path){
|
|
1668
|
+
if (!node) return;
|
|
1669
|
+
while(1 === node.children.length && node.children[0].value === node.value)node = node.children[0];
|
|
1670
|
+
if ('number' == typeof part && 'sequence' === node.kind) node = node.children[part];
|
|
1671
|
+
else {
|
|
1672
|
+
if ('string' != typeof part || 'mapping' !== node.kind) return;
|
|
1673
|
+
const children = node.children;
|
|
1674
|
+
node = void 0;
|
|
1675
|
+
for(let index = 0; index < children.length; index += 2)if (children[index].value === part) {
|
|
1676
|
+
node = children[index + 1];
|
|
1677
|
+
break;
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (!node) return;
|
|
1682
|
+
let start = node.start;
|
|
1683
|
+
while(start < node.end)if (/\s/.test(source[start])) start++;
|
|
1684
|
+
else if ('#' === source[start]) {
|
|
1685
|
+
const end = source.indexOf('\n', start);
|
|
1686
|
+
if (end < 0) return;
|
|
1687
|
+
start = end + 1;
|
|
1688
|
+
} else break;
|
|
1689
|
+
const prefix = source.slice(0, start).split(/\r\n|\r|\n/);
|
|
1690
|
+
return {
|
|
1691
|
+
line: prefix.length,
|
|
1692
|
+
column: prefix.at(-1).length + 1
|
|
1693
|
+
};
|
|
1694
|
+
},
|
|
581
1695
|
sourceYaml: {
|
|
582
1696
|
value,
|
|
583
1697
|
...redactedPaths.size ? {
|
|
@@ -636,7 +1750,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
636
1750
|
]));
|
|
637
1751
|
return value;
|
|
638
1752
|
};
|
|
639
|
-
function
|
|
1753
|
+
function collect_define_property(obj, key, value) {
|
|
640
1754
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
641
1755
|
value: value,
|
|
642
1756
|
enumerable: true,
|
|
@@ -653,7 +1767,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
653
1767
|
sourcePath,
|
|
654
1768
|
actualFormat,
|
|
655
1769
|
expectedFormat: 'native'
|
|
656
|
-
}),
|
|
1770
|
+
}), collect_define_property(this, "actualFormat", void 0);
|
|
657
1771
|
this.actualFormat = actualFormat;
|
|
658
1772
|
}
|
|
659
1773
|
}
|
|
@@ -690,6 +1804,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
690
1804
|
const sourcePath = source.sourcePath.split(external_node_path_.sep).join('/');
|
|
691
1805
|
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
692
1806
|
...step,
|
|
1807
|
+
meta: {
|
|
1808
|
+
...step.meta,
|
|
1809
|
+
sourceLocation: snapshot.locate('steps' === phase ? [
|
|
1810
|
+
'cases',
|
|
1811
|
+
caseIndex,
|
|
1812
|
+
'steps',
|
|
1813
|
+
stepIndex
|
|
1814
|
+
] : [
|
|
1815
|
+
phase,
|
|
1816
|
+
stepIndex
|
|
1817
|
+
])
|
|
1818
|
+
},
|
|
693
1819
|
templateInput: step.input,
|
|
694
1820
|
input: resolveWorkflowVariables(step.input, {
|
|
695
1821
|
variables: options.variables,
|
|
@@ -865,6 +1991,9 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
865
1991
|
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
866
1992
|
} : {},
|
|
867
1993
|
summary: result.summary,
|
|
1994
|
+
...result.metrics ? {
|
|
1995
|
+
metrics: result.metrics
|
|
1996
|
+
} : {},
|
|
868
1997
|
projects: result.projects.map((project)=>({
|
|
869
1998
|
projectId: project.projectId,
|
|
870
1999
|
name: project.name,
|
|
@@ -1142,7 +2271,24 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1142
2271
|
defaultTimeoutMs,
|
|
1143
2272
|
reportEnabled: true,
|
|
1144
2273
|
bindings: {
|
|
1145
|
-
resolveNode: project.nodes.require.bind(project.nodes)
|
|
2274
|
+
resolveNode: project.nodes.require.bind(project.nodes),
|
|
2275
|
+
...project.fileSetup ? {
|
|
2276
|
+
documentSetup: {
|
|
2277
|
+
name: project.fileSetup.name,
|
|
2278
|
+
setup ({ projectContext, document, signal, onTeardown }) {
|
|
2279
|
+
return project.fileSetup.setup({
|
|
2280
|
+
project,
|
|
2281
|
+
projectContext,
|
|
2282
|
+
file: document,
|
|
2283
|
+
env: Object.freeze({
|
|
2284
|
+
...process.env
|
|
2285
|
+
}),
|
|
2286
|
+
signal,
|
|
2287
|
+
onTeardown
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
} : {}
|
|
1146
2292
|
}
|
|
1147
2293
|
});
|
|
1148
2294
|
} catch (error) {
|
|
@@ -1244,14 +2390,14 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1244
2390
|
preflightScope: hasFormatMismatch ? 'run' : 'project',
|
|
1245
2391
|
reportEnabled: true,
|
|
1246
2392
|
publications: [],
|
|
1247
|
-
onProgress: options.onProgress ?? (()=>{})
|
|
2393
|
+
onProgress: options.onProgress ?? (()=>{}),
|
|
2394
|
+
onEvent: options.onEvent
|
|
1248
2395
|
};
|
|
1249
2396
|
}
|
|
1250
2397
|
const external_node_events_namespaceObject = require("node:events");
|
|
1251
2398
|
const report_namespaceObject = require("@midscene/core/report");
|
|
1252
|
-
const logger_namespaceObject = require("@midscene/shared/logger");
|
|
1253
2399
|
const result_markdown_text = (value)=>value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll('|', '|').replaceAll('[', '[').replaceAll(']', ']').replaceAll('*', '*').replaceAll('_', '_').replaceAll('`', '`').replaceAll('\\', '\').replace(/[\r\n]+/g, ' ');
|
|
1254
|
-
const
|
|
2400
|
+
const result_markdown_duration = (ms)=>{
|
|
1255
2401
|
const seconds = Math.round(ms / 1000);
|
|
1256
2402
|
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
1257
2403
|
};
|
|
@@ -1262,7 +2408,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1262
2408
|
const lines = [
|
|
1263
2409
|
'## Midscene Test',
|
|
1264
2410
|
'',
|
|
1265
|
-
`${outcome} · ${
|
|
2411
|
+
`${outcome} · ${result_markdown_duration(dump.durationMs)}`,
|
|
1266
2412
|
''
|
|
1267
2413
|
];
|
|
1268
2414
|
if (summary.filtered) lines.push(`**Filtered:** ${summary.filtered}`, '');
|
|
@@ -1325,6 +2471,38 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1325
2471
|
}));
|
|
1326
2472
|
return output;
|
|
1327
2473
|
}
|
|
2474
|
+
function scopeNodeConsole(node) {
|
|
2475
|
+
return {
|
|
2476
|
+
...node,
|
|
2477
|
+
execute (ctx) {
|
|
2478
|
+
const position = 'case' === ctx.scope ? ctx.case : ctx.document;
|
|
2479
|
+
const scope = {
|
|
2480
|
+
projectName: position.projectName,
|
|
2481
|
+
sourcePath: position.sourcePath,
|
|
2482
|
+
...'case' === ctx.scope ? {
|
|
2483
|
+
caseId: ctx.case.caseId,
|
|
2484
|
+
caseName: ctx.case.name,
|
|
2485
|
+
runId: ctx.case.runId
|
|
2486
|
+
} : {
|
|
2487
|
+
runId: ctx.document.documentRunId
|
|
2488
|
+
},
|
|
2489
|
+
phase: position.phase,
|
|
2490
|
+
stepIndex: position.stepIndex,
|
|
2491
|
+
node: node.name
|
|
2492
|
+
};
|
|
2493
|
+
return (0, logger_namespaceObject.runWithConsoleLogScope)(scope, ()=>node.execute({
|
|
2494
|
+
...ctx,
|
|
2495
|
+
onTeardown (teardown) {
|
|
2496
|
+
ctx.onTeardown(()=>(0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2497
|
+
...scope,
|
|
2498
|
+
phase: 'teardown',
|
|
2499
|
+
stepIndex: void 0
|
|
2500
|
+
}, teardown));
|
|
2501
|
+
}
|
|
2502
|
+
}));
|
|
2503
|
+
}
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
1328
2506
|
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1329
2507
|
const formatStep = (info)=>{
|
|
1330
2508
|
const position = stepPosition(info);
|
|
@@ -1348,59 +2526,149 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1348
2526
|
...result.beforeAll,
|
|
1349
2527
|
...result.afterAll
|
|
1350
2528
|
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (result.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError) || !!result.executionErrors?.some(test_runner_namespaceObject.isResourceRecoveryError);
|
|
2529
|
+
const isSharedResourceFailure = (error)=>{
|
|
2530
|
+
const seen = new Set();
|
|
2531
|
+
const pending = [
|
|
2532
|
+
error
|
|
2533
|
+
];
|
|
2534
|
+
while(pending.length && seen.size < 50){
|
|
2535
|
+
const item = pending.pop();
|
|
2536
|
+
if ('object' != typeof item || null === item || seen.has(item)) continue;
|
|
2537
|
+
seen.add(item);
|
|
2538
|
+
const failure = item;
|
|
2539
|
+
if ('PROJECT_RESOURCE_ERROR' === failure.code) return true;
|
|
2540
|
+
if (failure.cause) pending.push(failure.cause);
|
|
2541
|
+
if (Array.isArray(failure.errors)) pending.push(...failure.errors.slice(0, 50));
|
|
2542
|
+
}
|
|
2543
|
+
return false;
|
|
2544
|
+
};
|
|
1351
2545
|
async function executeDocumentInvocation(options) {
|
|
1352
2546
|
const { invocation, project, sinks } = options;
|
|
2547
|
+
const isProjectResourceFailure = options.isProjectResourceFailure ?? (()=>false);
|
|
1353
2548
|
const { document } = invocation;
|
|
1354
2549
|
const documentRetry = 'document' === invocation.retry.scope;
|
|
1355
2550
|
const attempts = [];
|
|
2551
|
+
const fileSetup = invocation.bindings.documentSetup;
|
|
2552
|
+
const documentSetup = fileSetup ? {
|
|
2553
|
+
...fileSetup,
|
|
2554
|
+
setup (ctx) {
|
|
2555
|
+
const scope = {
|
|
2556
|
+
projectName: project.name,
|
|
2557
|
+
sourcePath: document.sourcePath,
|
|
2558
|
+
runId: ctx.document.documentRunId,
|
|
2559
|
+
phase: 'fileSetup'
|
|
2560
|
+
};
|
|
2561
|
+
return (0, logger_namespaceObject.runWithConsoleLogScope)(scope, async ()=>{
|
|
2562
|
+
try {
|
|
2563
|
+
return await fileSetup.setup({
|
|
2564
|
+
...ctx,
|
|
2565
|
+
onTeardown (teardown) {
|
|
2566
|
+
ctx.onTeardown(()=>(0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2567
|
+
...scope,
|
|
2568
|
+
phase: 'teardown'
|
|
2569
|
+
}, teardown));
|
|
2570
|
+
}
|
|
2571
|
+
});
|
|
2572
|
+
} catch (error) {
|
|
2573
|
+
if (isProjectResourceFailure(error)) options.markProjectFatal();
|
|
2574
|
+
throw error;
|
|
2575
|
+
}
|
|
2576
|
+
});
|
|
2577
|
+
}
|
|
2578
|
+
} : void 0;
|
|
2579
|
+
options.onEvent?.({
|
|
2580
|
+
type: 'document-start',
|
|
2581
|
+
documentId: document.documentId,
|
|
2582
|
+
projectName: project.name,
|
|
2583
|
+
sourcePath: document.sourcePath,
|
|
2584
|
+
cases: document.cases.length
|
|
2585
|
+
});
|
|
1356
2586
|
await (0, test_runner_namespaceObject.runDocumentAttempts)({
|
|
1357
2587
|
retry: documentRetry ? invocation.retry.count : 0,
|
|
1358
2588
|
signal: options.signal,
|
|
1359
2589
|
shouldStop: ()=>options.shouldBail() || options.isProjectFatal()
|
|
1360
2590
|
}, async (documentAttemptIndex)=>{
|
|
1361
2591
|
if (documentRetry) options.onProgress(` file attempt ${documentAttemptIndex + 1}/${invocation.retry.count + 1}: ${document.sourcePath}`);
|
|
1362
|
-
const execution = await (0,
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
options.
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
2592
|
+
const execution = await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2593
|
+
projectName: project.name,
|
|
2594
|
+
sourcePath: document.sourcePath
|
|
2595
|
+
}, ()=>(0, test_runner_namespaceObject.runWorkflowDocument)(document, {
|
|
2596
|
+
plan: invocation.plan,
|
|
2597
|
+
documentAttemptIndex: documentRetry ? documentAttemptIndex : void 0,
|
|
2598
|
+
...invocation.bindings,
|
|
2599
|
+
resolveNode: (name)=>scopeNodeConsole(invocation.bindings.resolveNode(name)),
|
|
2600
|
+
documentSetup,
|
|
2601
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
2602
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
2603
|
+
project,
|
|
2604
|
+
projectContext: options.projectContext,
|
|
2605
|
+
retry: documentRetry ? 0 : invocation.retry.count,
|
|
2606
|
+
signal: options.signal,
|
|
2607
|
+
defaultTimeoutMs: invocation.defaultTimeoutMs,
|
|
2608
|
+
shouldStop: ()=>options.rootSignal.aborted || options.shouldBail() || options.isProjectFatal(),
|
|
2609
|
+
stopReason: ()=>options.rootSignal.aborted ? 'interrupted' : options.isProjectFatal() ? 'fatal-error' : 'bail',
|
|
2610
|
+
isFatalError: (run)=>[
|
|
2611
|
+
...run.beforeEach,
|
|
2612
|
+
...run.steps,
|
|
2613
|
+
...run.afterEach
|
|
2614
|
+
].some((step)=>step.error && ((0, test_runner_namespaceObject.isFatalDeviceError)(step.error) || isProjectResourceFailure(step.error))) || (run.teardownErrors ?? []).some((error)=>(0, test_runner_namespaceObject.isFatalDeviceError)(error) || isProjectResourceFailure(error)),
|
|
2615
|
+
onCaseStart: (collectedCase)=>{
|
|
2616
|
+
options.onProgress(` [case ${collectedCase.caseIndex + 1}/${document.cases.length}] ${collectedCase.definition.name}`);
|
|
2617
|
+
},
|
|
2618
|
+
onStepStart: (info)=>{
|
|
2619
|
+
options.onEvent?.({
|
|
2620
|
+
type: 'step-start',
|
|
2621
|
+
projectName: project.name,
|
|
2622
|
+
info
|
|
2623
|
+
});
|
|
2624
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
2625
|
+
options.onProgress(`${indent}→ ${formatStep(info)}`);
|
|
2626
|
+
},
|
|
2627
|
+
onStepResult: async (info, result)=>{
|
|
2628
|
+
if (isProjectResourceFailure(result.error)) options.markProjectFatal();
|
|
2629
|
+
await invocation.bindings.onStepResult?.(info, result);
|
|
2630
|
+
options.onProgress(formatStepResult(info, result));
|
|
2631
|
+
options.onEvent?.({
|
|
2632
|
+
type: 'step-end',
|
|
2633
|
+
projectName: project.name,
|
|
2634
|
+
info,
|
|
2635
|
+
result
|
|
2636
|
+
});
|
|
2637
|
+
},
|
|
2638
|
+
onCaseResult: async (attempt)=>{
|
|
2639
|
+
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
2640
|
+
options.onProgress(` ${'success' === attempt.status ? '✓' : '✗'} case attempt ${attempt.attemptIndex + 1}/${documentRetry ? 1 : invocation.retry.count + 1}: ${attempt.name} (${attempt.durationMs} ms)`);
|
|
2641
|
+
if (!documentRetry) options.onEvent?.({
|
|
2642
|
+
type: 'case-attempt-end',
|
|
2643
|
+
attempt,
|
|
2644
|
+
maxAttempts: invocation.retry.count + 1
|
|
2645
|
+
});
|
|
2646
|
+
},
|
|
2647
|
+
onCaseOutcome: (outcome)=>{
|
|
2648
|
+
if (documentRetry) options.onEvent?.({
|
|
2649
|
+
type: 'case-progress',
|
|
2650
|
+
outcome,
|
|
2651
|
+
documentAttemptIndex
|
|
2652
|
+
});
|
|
2653
|
+
else options.onEvent?.({
|
|
2654
|
+
type: 'case-end',
|
|
2655
|
+
outcome
|
|
2656
|
+
});
|
|
2657
|
+
if (outcome.attempts?.some((attempt)=>[
|
|
2658
|
+
...attempt.beforeEach,
|
|
2659
|
+
...attempt.steps,
|
|
2660
|
+
...attempt.afterEach
|
|
2661
|
+
].some((step)=>isProjectResourceFailure(step.error)) || attempt.teardownErrors?.some(isProjectResourceFailure))) options.markProjectFatal();
|
|
2662
|
+
if (!options.isolatedContext && caseHasFatalError(outcome)) options.markProjectFatal();
|
|
2663
|
+
if (!documentRetry && 'failed' === outcome.status) options.addFailedCases(1);
|
|
2664
|
+
},
|
|
2665
|
+
onDocumentResult: options.onDocumentResult
|
|
2666
|
+
})).catch(async (error)=>{
|
|
1399
2667
|
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) {
|
|
1400
2668
|
const partial = error.result;
|
|
1401
2669
|
const resourceOnly = error.errors.length > 0 && error.errors.every((failure)=>(0, test_runner_namespaceObject.isResourceRecoveryError)(failure) || partial.document.hostErrors?.some((host)=>'cleanup' === host.phase && (host.error === failure || host.error instanceof Error && host.error.cause === failure)));
|
|
1402
2670
|
if (resourceOnly) {
|
|
1403
|
-
if (error.errors.some(test_runner_namespaceObject.isResourceRecoveryError)) options.markProjectFatal();
|
|
2671
|
+
if (!options.isolatedContext && error.errors.some(test_runner_namespaceObject.isResourceRecoveryError)) options.markProjectFatal();
|
|
1404
2672
|
return partial;
|
|
1405
2673
|
}
|
|
1406
2674
|
if (documentRetry) partial.document.retryDecision = (0, test_runner_namespaceObject.decideRetry)({
|
|
@@ -1429,7 +2697,17 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1429
2697
|
}
|
|
1430
2698
|
throw error;
|
|
1431
2699
|
});
|
|
1432
|
-
if (documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
2700
|
+
if (!options.isolatedContext && documentHasFatalError(execution.document)) options.markProjectFatal();
|
|
2701
|
+
if ([
|
|
2702
|
+
...execution.document.beforeAll,
|
|
2703
|
+
...execution.document.afterAll,
|
|
2704
|
+
...(execution.document.hostErrors ?? []).map(({ error })=>({
|
|
2705
|
+
error
|
|
2706
|
+
})),
|
|
2707
|
+
...(execution.document.teardownErrors ?? []).map((error)=>({
|
|
2708
|
+
error
|
|
2709
|
+
}))
|
|
2710
|
+
].some(({ error })=>isProjectResourceFailure(error))) options.markProjectFatal();
|
|
1433
2711
|
const owners = [
|
|
1434
2712
|
execution.document,
|
|
1435
2713
|
...execution.cases.flatMap((outcome)=>outcome.attempts ?? [])
|
|
@@ -1437,7 +2715,10 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1437
2715
|
const cleanupPending = owners.some((owner)=>owner.cleanup?.status === 'pending');
|
|
1438
2716
|
const operationPending = owners.some((owner)=>!!owner.pendingOperations?.length);
|
|
1439
2717
|
const cleanupFailed = owners.some((owner)=>owner.cleanup?.status === 'failed' || !!owner.teardownErrors?.length || 'hostErrors' in owner && owner.hostErrors?.some(({ phase })=>'cleanup' === phase));
|
|
1440
|
-
if (cleanupPending ||
|
|
2718
|
+
if (cleanupPending || operationPending) {
|
|
2719
|
+
options.markProjectFatal();
|
|
2720
|
+
options.onUnresolvedResources?.();
|
|
2721
|
+
} else if (cleanupFailed && !options.isolatedContext) options.markProjectFatal();
|
|
1441
2722
|
const failed = 'failed' === execution.document.status || execution.cases.some((item)=>'success' !== item.status);
|
|
1442
2723
|
const decision = (0, test_runner_namespaceObject.decideRetry)({
|
|
1443
2724
|
scope: 'document',
|
|
@@ -1452,6 +2733,11 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1452
2733
|
shouldStop: options.shouldBail()
|
|
1453
2734
|
});
|
|
1454
2735
|
if (documentRetry) execution.document.retryDecision = decision;
|
|
2736
|
+
if (documentRetry && 'retry' === decision.decision) options.onEvent?.({
|
|
2737
|
+
type: 'document-retry',
|
|
2738
|
+
document: execution.document,
|
|
2739
|
+
maxAttempts: invocation.retry.count + 1
|
|
2740
|
+
});
|
|
1455
2741
|
attempts.push(execution);
|
|
1456
2742
|
sinks.cases.push(...execution.cases.map((outcome)=>({
|
|
1457
2743
|
...outcome,
|
|
@@ -1486,10 +2772,280 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1486
2772
|
});
|
|
1487
2773
|
const final = attempts.at(-1);
|
|
1488
2774
|
if (!final) return;
|
|
2775
|
+
if (documentRetry) for (const outcome of final.cases)options.onEvent?.({
|
|
2776
|
+
type: 'case-end',
|
|
2777
|
+
outcome,
|
|
2778
|
+
documentRetries: attempts.length - 1
|
|
2779
|
+
});
|
|
1489
2780
|
const failedCases = final.cases.filter((item)=>'failed' === item.status).length;
|
|
2781
|
+
options.onEvent?.({
|
|
2782
|
+
type: 'document-end',
|
|
2783
|
+
document: final.document,
|
|
2784
|
+
cases: final.cases,
|
|
2785
|
+
durationMs: attempts.reduce((total, attempt)=>total + attempt.document.durationMs, 0)
|
|
2786
|
+
});
|
|
1490
2787
|
if (documentRetry && failedCases) options.addFailedCases(failedCases);
|
|
1491
2788
|
else if ('failed' === final.document.status && 0 === failedCases) options.addFailedCases(Math.max(1, failedCases));
|
|
1492
2789
|
}
|
|
2790
|
+
async function runPreparedFiles(options) {
|
|
2791
|
+
const states = options.projects.map((prepared)=>({
|
|
2792
|
+
prepared,
|
|
2793
|
+
cases: [],
|
|
2794
|
+
documents: [],
|
|
2795
|
+
cursor: 0,
|
|
2796
|
+
active: 0,
|
|
2797
|
+
initializing: false,
|
|
2798
|
+
finished: false,
|
|
2799
|
+
fatal: false,
|
|
2800
|
+
setupFailed: false
|
|
2801
|
+
}));
|
|
2802
|
+
let nextProject = 0;
|
|
2803
|
+
let resourcesUnresolved = false;
|
|
2804
|
+
let occupied = 0;
|
|
2805
|
+
let occupiedProjects = 0;
|
|
2806
|
+
const cleanupQueue = [];
|
|
2807
|
+
const release = ()=>{
|
|
2808
|
+
occupied--;
|
|
2809
|
+
while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
|
|
2810
|
+
occupied++;
|
|
2811
|
+
cleanupQueue.shift()();
|
|
2812
|
+
}
|
|
2813
|
+
wake?.();
|
|
2814
|
+
};
|
|
2815
|
+
let wake;
|
|
2816
|
+
const running = new Set();
|
|
2817
|
+
const stopped = (state)=>options.signal.aborted || options.shouldBail() || state.fatal;
|
|
2818
|
+
const invalid = (state)=>options.preflightFailed || state.prepared.collectionErrors.length > 0;
|
|
2819
|
+
const unresolved = ()=>{
|
|
2820
|
+
if (resourcesUnresolved) return;
|
|
2821
|
+
resourcesUnresolved = true;
|
|
2822
|
+
options.onInfrastructureError(new test_runner_namespaceObject.WorkflowError('Resources are still running or cleanup is pending. No new test files will start.', {
|
|
2823
|
+
code: 'UNRESOLVED_FILE_RESOURCES'
|
|
2824
|
+
}));
|
|
2825
|
+
};
|
|
2826
|
+
const finalizeProject = (state)=>{
|
|
2827
|
+
const { prepared } = state;
|
|
2828
|
+
const completed = new Set(state.cases.map((item)=>item.caseId));
|
|
2829
|
+
const reason = invalid(state) ? 'project-preflight-failed' : state.setupFailed ? 'project-setup-failed' : options.signal.aborted ? 'interrupted' : state.fatal ? 'fatal-error' : 'bail';
|
|
2830
|
+
for (const document of prepared.documents)for (const item of document.cases)if (!completed.has(item.caseId)) state.cases.push({
|
|
2831
|
+
documentId: document.documentId,
|
|
2832
|
+
caseId: item.caseId,
|
|
2833
|
+
projectName: prepared.project.name,
|
|
2834
|
+
name: item.definition.name,
|
|
2835
|
+
sourcePath: item.sourcePath,
|
|
2836
|
+
caseIndex: item.caseIndex,
|
|
2837
|
+
status: 'not-run',
|
|
2838
|
+
notRunReason: reason
|
|
2839
|
+
});
|
|
2840
|
+
const order = new Map(prepared.documents.map((document, i)=>[
|
|
2841
|
+
document.documentId,
|
|
2842
|
+
i
|
|
2843
|
+
]));
|
|
2844
|
+
const position = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
2845
|
+
state.cases.sort((a, b)=>position(a.documentId) - position(b.documentId) || a.caseIndex - b.caseIndex);
|
|
2846
|
+
state.documents.sort((a, b)=>position(a.documentId) - position(b.documentId));
|
|
2847
|
+
};
|
|
2848
|
+
const finishProject = async (state)=>{
|
|
2849
|
+
if (!state.runtime || state.finished) return;
|
|
2850
|
+
state.finished = true;
|
|
2851
|
+
try {
|
|
2852
|
+
options.onEvent?.({
|
|
2853
|
+
type: 'project-phase',
|
|
2854
|
+
projectName: state.prepared.project.name,
|
|
2855
|
+
phase: 'teardown'
|
|
2856
|
+
});
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
options.onInfrastructureError(error);
|
|
2859
|
+
}
|
|
2860
|
+
const failed = state.fatal || options.signal.aborted || state.cases.some((item)=>'success' !== item.status) || state.documents.some((item)=>'success' !== item.status);
|
|
2861
|
+
state.lifecycle = await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2862
|
+
projectName: state.prepared.project.name,
|
|
2863
|
+
phase: 'teardown'
|
|
2864
|
+
}, ()=>state.runtime.finish(failed ? 'failed' : 'success'));
|
|
2865
|
+
const releaseProject = ()=>{
|
|
2866
|
+
occupiedProjects--;
|
|
2867
|
+
wake?.();
|
|
2868
|
+
};
|
|
2869
|
+
if (state.lifecycle.cleanup?.status === 'pending' || state.lifecycle.pendingOperations?.length) {
|
|
2870
|
+
unresolved();
|
|
2871
|
+
Promise.all([
|
|
2872
|
+
(0, test_runner_namespaceObject.waitForScopeOperations)(state.runtime.signal),
|
|
2873
|
+
(0, test_runner_namespaceObject.getResourceCleanupCompletion)(state.runtime.signal)
|
|
2874
|
+
]).then(releaseProject, (error)=>{
|
|
2875
|
+
options.onInfrastructureError(error);
|
|
2876
|
+
releaseProject();
|
|
2877
|
+
});
|
|
2878
|
+
} else {
|
|
2879
|
+
releaseProject();
|
|
2880
|
+
finalizeProject(state);
|
|
2881
|
+
options.onProjectEnd?.(state);
|
|
2882
|
+
}
|
|
2883
|
+
};
|
|
2884
|
+
const admit = ()=>{
|
|
2885
|
+
for(let offset = 0; offset < states.length; offset++){
|
|
2886
|
+
const index = (nextProject + offset) % states.length;
|
|
2887
|
+
const state = states[index];
|
|
2888
|
+
if (invalid(state) || state.finished || state.initializing) continue;
|
|
2889
|
+
if (!state.runtime && occupiedProjects >= options.maxConcurrentProjects) continue;
|
|
2890
|
+
const cleanupOnly = !!state.runtime && 0 === state.active && (stopped(state) || state.cursor === state.prepared.invocations.length);
|
|
2891
|
+
if (!cleanupOnly && (stopped(state) || state.cursor >= state.prepared.invocations.length || state.active >= (state.prepared.project.maxConcurrentFiles ?? options.maxConcurrentFiles))) continue;
|
|
2892
|
+
nextProject = (index + 1) % states.length;
|
|
2893
|
+
occupied++;
|
|
2894
|
+
state.active++;
|
|
2895
|
+
if (!state.runtime && !cleanupOnly) {
|
|
2896
|
+
occupiedProjects++;
|
|
2897
|
+
state.initializing = true;
|
|
2898
|
+
}
|
|
2899
|
+
const invocationIndex = cleanupOnly ? -1 : state.cursor++;
|
|
2900
|
+
return {
|
|
2901
|
+
state,
|
|
2902
|
+
index,
|
|
2903
|
+
invocationIndex
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
};
|
|
2907
|
+
const run = async ({ state, index, invocationIndex })=>{
|
|
2908
|
+
const { prepared } = state;
|
|
2909
|
+
const { project } = prepared;
|
|
2910
|
+
let fileResourcesPending = false;
|
|
2911
|
+
try {
|
|
2912
|
+
if (invocationIndex < 0) return void await finishProject(state);
|
|
2913
|
+
if (!state.runtime) {
|
|
2914
|
+
state.runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
|
|
2915
|
+
project,
|
|
2916
|
+
setup: project.setup,
|
|
2917
|
+
signal: options.signal,
|
|
2918
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
2919
|
+
teardownTimeoutMs: options.teardownTimeoutMs
|
|
2920
|
+
});
|
|
2921
|
+
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, Math.min(prepared.invocations.length, project.maxConcurrentFiles ?? options.maxConcurrentFiles) + 1), state.runtime.signal);
|
|
2922
|
+
options.announceProject(prepared, index);
|
|
2923
|
+
options.onEvent?.({
|
|
2924
|
+
type: 'project-phase',
|
|
2925
|
+
projectName: project.name,
|
|
2926
|
+
phase: 'setup'
|
|
2927
|
+
});
|
|
2928
|
+
await (0, logger_namespaceObject.runWithConsoleLogScope)({
|
|
2929
|
+
projectName: project.name,
|
|
2930
|
+
phase: 'setup'
|
|
2931
|
+
}, ()=>state.runtime.start());
|
|
2932
|
+
options.onEvent?.({
|
|
2933
|
+
type: 'project-phase',
|
|
2934
|
+
projectName: project.name,
|
|
2935
|
+
phase: 'running'
|
|
2936
|
+
});
|
|
2937
|
+
state.initializing = false;
|
|
2938
|
+
if (!state.runtime.canRun) {
|
|
2939
|
+
state.fatal = true;
|
|
2940
|
+
state.setupFailed = true;
|
|
2941
|
+
}
|
|
2942
|
+
wake?.();
|
|
2943
|
+
}
|
|
2944
|
+
if (stopped(state)) return;
|
|
2945
|
+
const invocation = prepared.invocations[invocationIndex];
|
|
2946
|
+
options.onProgress(prepared, ` [document ${invocationIndex + 1}/${prepared.invocations.length}] ${invocation.document.sourcePath}`);
|
|
2947
|
+
await executeDocumentInvocation({
|
|
2948
|
+
invocation,
|
|
2949
|
+
project,
|
|
2950
|
+
projectContext: state.runtime.context,
|
|
2951
|
+
runDir: options.runDir,
|
|
2952
|
+
signal: state.runtime.signal,
|
|
2953
|
+
rootSignal: options.signal,
|
|
2954
|
+
shouldBail: options.shouldBail,
|
|
2955
|
+
isProjectFatal: ()=>state.fatal,
|
|
2956
|
+
markProjectFatal: ()=>{
|
|
2957
|
+
if (state.fatal) return;
|
|
2958
|
+
state.fatal = true;
|
|
2959
|
+
state.runtime?.abort(new test_runner_namespaceObject.WorkflowError(`Project "${project.name}" shared execution environment is unavailable.`, {
|
|
2960
|
+
code: 'PROJECT_RESOURCE_ERROR'
|
|
2961
|
+
}));
|
|
2962
|
+
},
|
|
2963
|
+
addFailedCases: options.addFailedCases,
|
|
2964
|
+
onProgress: (message)=>options.onProgress(prepared, options.maxConcurrentFiles > 1 ? ` [${invocation.document.sourcePath}]${message}` : message),
|
|
2965
|
+
sinks: state,
|
|
2966
|
+
onEvent: options.onEvent,
|
|
2967
|
+
isolatedContext: void 0 !== project.fileSetup,
|
|
2968
|
+
isProjectResourceFailure: isSharedResourceFailure,
|
|
2969
|
+
setupTimeoutMs: options.setupTimeoutMs,
|
|
2970
|
+
teardownTimeoutMs: options.teardownTimeoutMs,
|
|
2971
|
+
onUnresolvedResources: ()=>{
|
|
2972
|
+
fileResourcesPending = true;
|
|
2973
|
+
unresolved();
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
} catch (error) {
|
|
2977
|
+
state.fatal = true;
|
|
2978
|
+
fileResourcesPending = !!state.runtime && (0, test_runner_namespaceObject.getPendingScopeOperations)(state.runtime.signal).length > 0;
|
|
2979
|
+
options.onInfrastructureError(error);
|
|
2980
|
+
} finally{
|
|
2981
|
+
state.initializing = false;
|
|
2982
|
+
try {
|
|
2983
|
+
if (1 === state.active && (stopped(state) || state.cursor === prepared.invocations.length)) await finishProject(state);
|
|
2984
|
+
} catch (error) {
|
|
2985
|
+
options.onInfrastructureError(error);
|
|
2986
|
+
}
|
|
2987
|
+
state.active--;
|
|
2988
|
+
if (state.runtime && (fileResourcesPending || state.lifecycle?.cleanup?.status === 'pending')) (0, test_runner_namespaceObject.waitForScopeOperations)(state.runtime.signal).then(release, (error)=>{
|
|
2989
|
+
options.onInfrastructureError(error);
|
|
2990
|
+
release();
|
|
2991
|
+
});
|
|
2992
|
+
else release();
|
|
2993
|
+
wake?.();
|
|
2994
|
+
}
|
|
2995
|
+
};
|
|
2996
|
+
while(true){
|
|
2997
|
+
const changed = new Promise((resolve)=>{
|
|
2998
|
+
wake = resolve;
|
|
2999
|
+
});
|
|
3000
|
+
while(occupied < options.maxConcurrentFiles){
|
|
3001
|
+
const job = admit();
|
|
3002
|
+
if (!job) break;
|
|
3003
|
+
const task = run(job);
|
|
3004
|
+
running.add(task);
|
|
3005
|
+
const settled = ()=>{
|
|
3006
|
+
running.delete(task);
|
|
3007
|
+
wake?.();
|
|
3008
|
+
};
|
|
3009
|
+
task.then(settled, (error)=>{
|
|
3010
|
+
options.onInfrastructureError(error);
|
|
3011
|
+
settled();
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
if (!running.size) break;
|
|
3015
|
+
await changed;
|
|
3016
|
+
}
|
|
3017
|
+
wake = void 0;
|
|
3018
|
+
await Promise.all(states.map(async (state)=>{
|
|
3019
|
+
if (!state.runtime || state.finished) return;
|
|
3020
|
+
const admission = new Promise((resolve)=>{
|
|
3021
|
+
cleanupQueue.push(resolve);
|
|
3022
|
+
});
|
|
3023
|
+
(0, test_runner_namespaceObject.trackResourceOperation)({}, admission, state.runtime.signal);
|
|
3024
|
+
while(occupied < options.maxConcurrentFiles && cleanupQueue.length){
|
|
3025
|
+
occupied++;
|
|
3026
|
+
cleanupQueue.shift()();
|
|
3027
|
+
}
|
|
3028
|
+
try {
|
|
3029
|
+
await finishProject(state);
|
|
3030
|
+
} catch (error) {
|
|
3031
|
+
options.onInfrastructureError(error);
|
|
3032
|
+
} finally{
|
|
3033
|
+
Promise.all([
|
|
3034
|
+
admission,
|
|
3035
|
+
(0, test_runner_namespaceObject.getResourceCleanupCompletion)(state.runtime.signal)
|
|
3036
|
+
]).then(release, (error)=>{
|
|
3037
|
+
options.onInfrastructureError(error);
|
|
3038
|
+
release();
|
|
3039
|
+
});
|
|
3040
|
+
}
|
|
3041
|
+
}));
|
|
3042
|
+
for (const state of states){
|
|
3043
|
+
const { prepared } = state;
|
|
3044
|
+
if (!state.runtime) options.announceProject(prepared, states.indexOf(state));
|
|
3045
|
+
finalizeProject(state);
|
|
3046
|
+
}
|
|
3047
|
+
return states;
|
|
3048
|
+
}
|
|
1493
3049
|
function createOperationJournal(runDir) {
|
|
1494
3050
|
const path = (0, external_node_path_.join)(runDir, 'operations.jsonl');
|
|
1495
3051
|
(0, external_node_fs_.writeFileSync)(path, '', {
|
|
@@ -1558,8 +3114,34 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1558
3114
|
const totalDocuments = preparedProjects.reduce((total, prepared)=>total + prepared.documents.length, 0);
|
|
1559
3115
|
const totalCases = preparedProjects.reduce((total, prepared)=>total + prepared.selectedCaseCount, 0);
|
|
1560
3116
|
const totalErrors = preparedProjects.reduce((total, prepared)=>total + prepared.collectionErrors.length, 0);
|
|
3117
|
+
plan.onEvent?.({
|
|
3118
|
+
type: 'run-start',
|
|
3119
|
+
configuredModels: configuredModelNames(),
|
|
3120
|
+
projects: preparedProjects.length,
|
|
3121
|
+
documents: totalDocuments + totalErrors,
|
|
3122
|
+
cases: totalCases,
|
|
3123
|
+
projectRoot,
|
|
3124
|
+
configPath,
|
|
3125
|
+
startedAt: startedAt.toISOString(),
|
|
3126
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects,
|
|
3127
|
+
maxConcurrentFiles: definition.test.maxConcurrentFiles,
|
|
3128
|
+
files: preparedProjects.flatMap((prepared)=>[
|
|
3129
|
+
...prepared.documents.map((document)=>({
|
|
3130
|
+
documentId: document.documentId,
|
|
3131
|
+
projectName: prepared.project.name,
|
|
3132
|
+
sourcePath: document.sourcePath,
|
|
3133
|
+
cases: document.cases.length
|
|
3134
|
+
})),
|
|
3135
|
+
...prepared.collectionErrors.map((error)=>({
|
|
3136
|
+
documentId: error.sourcePath,
|
|
3137
|
+
projectName: prepared.project.name,
|
|
3138
|
+
sourcePath: error.sourcePath,
|
|
3139
|
+
cases: 0
|
|
3140
|
+
}))
|
|
3141
|
+
])
|
|
3142
|
+
});
|
|
1561
3143
|
progress(`midscene-test: preflighted ${preparedProjects.length} projects, ${totalDocuments} documents, ${totalCases} cases, ${totalErrors} collection errors`);
|
|
1562
|
-
const effectiveConcurrency = Math.min(definition.test.
|
|
3144
|
+
const effectiveConcurrency = Math.min(definition.test.maxConcurrentFiles ?? 1 / 0, Math.max(1, totalDocuments));
|
|
1563
3145
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
1564
3146
|
const journal = createOperationJournal(runDir);
|
|
1565
3147
|
const rootController = new AbortController();
|
|
@@ -1573,6 +3155,18 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1573
3155
|
process.on('SIGTERM', sigterm);
|
|
1574
3156
|
let failedCaseCount = 0;
|
|
1575
3157
|
const bailReached = ()=>definition.test.bail > 0 && failedCaseCount >= definition.test.bail;
|
|
3158
|
+
let shutdownDeadline;
|
|
3159
|
+
const beginShutdown = ()=>{
|
|
3160
|
+
shutdownDeadline ??= Date.now() + definition.test.teardownTimeout;
|
|
3161
|
+
};
|
|
3162
|
+
rootController.signal.addEventListener('abort', beginShutdown, {
|
|
3163
|
+
once: true
|
|
3164
|
+
});
|
|
3165
|
+
const addFailedCases = (count)=>{
|
|
3166
|
+
failedCaseCount += count;
|
|
3167
|
+
if (bailReached()) beginShutdown();
|
|
3168
|
+
};
|
|
3169
|
+
const teardownTimeoutMs = ()=>Math.max(0, Math.min(definition.test.teardownTimeout, void 0 === shutdownDeadline ? 1 / 0 : shutdownDeadline - Date.now()));
|
|
1576
3170
|
let hasInfrastructureError = false;
|
|
1577
3171
|
const infrastructureErrors = [];
|
|
1578
3172
|
const recordInfrastructureError = (error)=>{
|
|
@@ -1634,112 +3228,30 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1634
3228
|
announceProject(prepared, projectIndex);
|
|
1635
3229
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
1636
3230
|
};
|
|
1637
|
-
const runPreparedProject = async (prepared, projectIndex)=>{
|
|
1638
|
-
const { project } = prepared;
|
|
1639
|
-
announceProject(prepared, projectIndex);
|
|
1640
|
-
const projectProgress = (message)=>runInfrastructureCallback(()=>progress(effectiveConcurrency > 1 ? `[${project.name}]${message}` : message));
|
|
1641
|
-
const cases = [];
|
|
1642
|
-
const documents = [];
|
|
1643
|
-
let lifecycle;
|
|
1644
|
-
let projectFatal = false;
|
|
1645
|
-
if (prepared.collectionErrors.length > 0 || 'run' === plan.preflightScope && totalErrors > 0) cases.push(...notRunSuite(prepared, 'project-preflight-failed'));
|
|
1646
|
-
else if (rootController.signal.aborted) cases.push(...notRunSuite(prepared, 'interrupted'));
|
|
1647
|
-
else if (bailReached()) cases.push(...notRunSuite(prepared, 'bail'));
|
|
1648
|
-
else {
|
|
1649
|
-
const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
|
|
1650
|
-
project,
|
|
1651
|
-
setup: project.setup,
|
|
1652
|
-
signal: rootController.signal
|
|
1653
|
-
});
|
|
1654
|
-
let hasProjectExecutionError = false;
|
|
1655
|
-
try {
|
|
1656
|
-
await runtime.start();
|
|
1657
|
-
if (runtime.canRun) {
|
|
1658
|
-
const runDocument = async (invocation, documentIndex)=>{
|
|
1659
|
-
const { document } = invocation;
|
|
1660
|
-
if (rootController.signal.aborted || bailReached() || projectFatal) {
|
|
1661
|
-
const reason = rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1662
|
-
cases.push(...document.cases.map((item)=>asNotRun(document.documentId, item, project.name, reason)));
|
|
1663
|
-
return;
|
|
1664
|
-
}
|
|
1665
|
-
projectProgress(` [document ${documentIndex + 1}/${prepared.documents.length}] ${document.sourcePath}`);
|
|
1666
|
-
await executeDocumentInvocation({
|
|
1667
|
-
invocation,
|
|
1668
|
-
project,
|
|
1669
|
-
projectContext: runtime.context,
|
|
1670
|
-
runDir,
|
|
1671
|
-
signal: runtime.signal,
|
|
1672
|
-
rootSignal: rootController.signal,
|
|
1673
|
-
shouldBail: bailReached,
|
|
1674
|
-
isProjectFatal: ()=>projectFatal,
|
|
1675
|
-
markProjectFatal: ()=>{
|
|
1676
|
-
projectFatal = true;
|
|
1677
|
-
},
|
|
1678
|
-
addFailedCases: (count)=>{
|
|
1679
|
-
failedCaseCount += count;
|
|
1680
|
-
},
|
|
1681
|
-
onProgress: projectProgress,
|
|
1682
|
-
sinks: {
|
|
1683
|
-
cases,
|
|
1684
|
-
documents
|
|
1685
|
-
}
|
|
1686
|
-
});
|
|
1687
|
-
};
|
|
1688
|
-
let prerequisiteFailed = false;
|
|
1689
|
-
const prerequisite = prepared.invocations.find((invocation)=>invocation.document.documentId === prepared.prerequisiteDocumentId);
|
|
1690
|
-
const prerequisiteCount = prerequisite ? 1 : 0;
|
|
1691
|
-
if (prerequisite) {
|
|
1692
|
-
await runDocument(prerequisite, 0);
|
|
1693
|
-
prerequisiteFailed = documents.at(-1)?.status === 'failed' || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status);
|
|
1694
|
-
}
|
|
1695
|
-
if (!prerequisiteFailed) await (0, test_runner_namespaceObject.runConcurrentJobs)(prepared.invocations.filter((invocation)=>invocation !== prerequisite), {
|
|
1696
|
-
concurrency: prepared.documentConcurrency,
|
|
1697
|
-
shouldStop: ()=>rootController.signal.aborted || bailReached() || projectFatal
|
|
1698
|
-
}, async (invocation, index)=>{
|
|
1699
|
-
try {
|
|
1700
|
-
await runDocument(invocation, index + prerequisiteCount);
|
|
1701
|
-
} catch (error) {
|
|
1702
|
-
recordInfrastructureError(error);
|
|
1703
|
-
throw error;
|
|
1704
|
-
}
|
|
1705
|
-
});
|
|
1706
|
-
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1707
|
-
const reason = prerequisiteFailed ? 'project-setup-failed' : rootController.signal.aborted ? 'interrupted' : projectFatal ? 'fatal-error' : 'bail';
|
|
1708
|
-
cases.push(...notRunSuite(prepared, reason).filter((item)=>!completed.has(item.caseId)));
|
|
1709
|
-
} else cases.push(...notRunSuite(prepared, rootController.signal.aborted ? 'interrupted' : 'project-setup-failed'));
|
|
1710
|
-
} catch (error) {
|
|
1711
|
-
hasProjectExecutionError = true;
|
|
1712
|
-
recordInfrastructureError(error);
|
|
1713
|
-
} finally{
|
|
1714
|
-
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;
|
|
1715
|
-
lifecycle = await runtime.finish(hasFailure ? 'failed' : 'success');
|
|
1716
|
-
}
|
|
1717
|
-
if (hasProjectExecutionError) {
|
|
1718
|
-
const completed = new Set(cases.map((item)=>item.caseId));
|
|
1719
|
-
cases.push(...notRunSuite(prepared, 'interrupted').filter((item)=>!completed.has(item.caseId)));
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
const order = new Map(prepared.documents.map((document, index)=>[
|
|
1723
|
-
document.documentId,
|
|
1724
|
-
index
|
|
1725
|
-
]));
|
|
1726
|
-
const documentOrder = (id)=>order.get(id) ?? Number.MAX_SAFE_INTEGER;
|
|
1727
|
-
cases.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || a.caseIndex - b.caseIndex);
|
|
1728
|
-
documents.sort((a, b)=>documentOrder(a.documentId) - documentOrder(b.documentId) || (a.attemptIndex ?? 0) - (b.attemptIndex ?? 0));
|
|
1729
|
-
return buildProjectResult(prepared, cases, documents, lifecycle);
|
|
1730
|
-
};
|
|
1731
3231
|
const projectResults = new Array(preparedProjects.length);
|
|
1732
3232
|
try {
|
|
1733
|
-
await (
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
3233
|
+
const states = await runPreparedFiles({
|
|
3234
|
+
projects: preparedProjects,
|
|
3235
|
+
maxConcurrentFiles: effectiveConcurrency,
|
|
3236
|
+
maxConcurrentProjects: definition.test.maxConcurrentProjects ?? 1 / 0,
|
|
3237
|
+
setupTimeoutMs: definition.test.setupTimeout,
|
|
3238
|
+
teardownTimeoutMs,
|
|
3239
|
+
signal: rootController.signal,
|
|
3240
|
+
runDir,
|
|
3241
|
+
preflightFailed: 'run' === plan.preflightScope && totalErrors > 0,
|
|
3242
|
+
shouldBail: bailReached,
|
|
3243
|
+
addFailedCases,
|
|
3244
|
+
announceProject,
|
|
3245
|
+
onProgress: (prepared, message)=>runInfrastructureCallback(()=>progress(preparedProjects.length > 1 || effectiveConcurrency > 1 ? `[${prepared.project.name}]${message}` : message)),
|
|
3246
|
+
onEvent: (event)=>runInfrastructureCallback(()=>plan.onEvent?.(event)),
|
|
3247
|
+
onProjectEnd: (state)=>runInfrastructureCallback(()=>plan.onEvent?.({
|
|
3248
|
+
type: 'project-end',
|
|
3249
|
+
project: buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle)
|
|
3250
|
+
})),
|
|
3251
|
+
onInfrastructureError: recordInfrastructureError
|
|
3252
|
+
});
|
|
3253
|
+
states.forEach((state, index)=>{
|
|
3254
|
+
projectResults[index] = buildProjectResult(state.prepared, state.cases, state.documents, state.lifecycle);
|
|
1743
3255
|
});
|
|
1744
3256
|
} catch (error) {
|
|
1745
3257
|
recordInfrastructureError(error);
|
|
@@ -1816,6 +3328,10 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1816
3328
|
buildRunnerDump: (index)=>{
|
|
1817
3329
|
reportIndex = index;
|
|
1818
3330
|
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, index);
|
|
3331
|
+
completedResult = {
|
|
3332
|
+
...completedResult,
|
|
3333
|
+
metrics: dump.metrics
|
|
3334
|
+
};
|
|
1819
3335
|
const warn = (0, logger_namespaceObject.getDebug)('test-runner:report-assembler', {
|
|
1820
3336
|
console: true
|
|
1821
3337
|
});
|
|
@@ -1833,6 +3349,16 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1833
3349
|
await publication.publish(completedResult);
|
|
1834
3350
|
});
|
|
1835
3351
|
await publish('write-result', (0, external_node_path_.join)((0, external_node_path_.dirname)(summaryPath), 'result.md'), async ()=>{
|
|
3352
|
+
if (!completedResult.metrics) {
|
|
3353
|
+
const dump = (0, test_runner_namespaceObject.buildTestRunReportDump)(completedResult, reportIndex ?? {
|
|
3354
|
+
sources: [],
|
|
3355
|
+
metrics: (0, test_runner_namespaceObject.sumTestRunReportMetrics)([])
|
|
3356
|
+
});
|
|
3357
|
+
completedResult = {
|
|
3358
|
+
...completedResult,
|
|
3359
|
+
metrics: dump.metrics
|
|
3360
|
+
};
|
|
3361
|
+
}
|
|
1836
3362
|
const markdownPath = await writeTestResultMarkdown(completedResult, reportIndex);
|
|
1837
3363
|
completedResult = {
|
|
1838
3364
|
...completedResult,
|
|
@@ -1853,6 +3379,7 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1853
3379
|
let configPath;
|
|
1854
3380
|
let resultDir;
|
|
1855
3381
|
const projectNames = [];
|
|
3382
|
+
let reporter;
|
|
1856
3383
|
for(let index = commandOffset; index < args.length; index += 1){
|
|
1857
3384
|
const token = args[index];
|
|
1858
3385
|
const equal = token.startsWith('-') ? token.indexOf('=') : -1;
|
|
@@ -1863,16 +3390,21 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1863
3390
|
projectRoot = (0, external_node_path_.resolve)(cwd, arg);
|
|
1864
3391
|
continue;
|
|
1865
3392
|
}
|
|
1866
|
-
if ('--config' === arg || '--result-dir' === arg || '--project' === arg) {
|
|
3393
|
+
if ('--config' === arg || '--result-dir' === arg || '--project' === arg || '--reporter' === arg) {
|
|
1867
3394
|
const value = inlineValue ?? args[index + 1];
|
|
1868
3395
|
if (!value || value.startsWith('--')) throw new Error(`${arg} requires a value.`);
|
|
1869
3396
|
if ('--config' === arg) configPath = value;
|
|
1870
3397
|
else if ('--result-dir' === arg) resultDir = (0, external_node_path_.resolve)(cwd, value);
|
|
1871
|
-
else projectNames.push(value);
|
|
3398
|
+
else if ('--project' === arg) projectNames.push(value);
|
|
3399
|
+
else {
|
|
3400
|
+
if ('default' !== value && 'verbose' !== value) throw new Error(`Unknown reporter: ${value}. Expected default or verbose.`);
|
|
3401
|
+
reporter = value;
|
|
3402
|
+
}
|
|
1872
3403
|
if (void 0 === inlineValue) index += 1;
|
|
1873
3404
|
} else throw new Error(`Unknown option: ${token}`);
|
|
1874
3405
|
}
|
|
1875
3406
|
if ('nodes' === command && resultDir) throw new Error('--result-dir is not supported by nodes.');
|
|
3407
|
+
if ('nodes' === command && reporter) throw new Error('--reporter is not supported by nodes.');
|
|
1876
3408
|
if ('nodes' === command && projectNames.length > 1) throw new Error('nodes accepts only one --project name.');
|
|
1877
3409
|
return {
|
|
1878
3410
|
...command ? {
|
|
@@ -1884,13 +3416,27 @@ Each Node's returned \`data\` is saved on that Step's result. Results are not na
|
|
|
1884
3416
|
resultDir,
|
|
1885
3417
|
...projectNames.length > 0 ? {
|
|
1886
3418
|
projectNames
|
|
3419
|
+
} : {},
|
|
3420
|
+
...reporter ? {
|
|
3421
|
+
reporter
|
|
1887
3422
|
} : {}
|
|
1888
3423
|
};
|
|
1889
3424
|
};
|
|
1890
3425
|
const defaultCliIO = {
|
|
1891
3426
|
log: console.log,
|
|
1892
3427
|
error: console.error,
|
|
1893
|
-
write:
|
|
3428
|
+
write: process.stdout.write.bind(process.stdout),
|
|
3429
|
+
isTTY: Boolean(process.stdout.isTTY && !process.env.CI),
|
|
3430
|
+
get columns () {
|
|
3431
|
+
return process.stdout.columns;
|
|
3432
|
+
},
|
|
3433
|
+
get rows () {
|
|
3434
|
+
return process.stdout.rows;
|
|
3435
|
+
},
|
|
3436
|
+
onResize (listener) {
|
|
3437
|
+
process.stdout.on('resize', listener);
|
|
3438
|
+
return ()=>process.stdout.off('resize', listener);
|
|
3439
|
+
}
|
|
1894
3440
|
};
|
|
1895
3441
|
const testCliHelp = `Midscene Test: run native cases/steps Test projects.
|
|
1896
3442
|
|
|
@@ -1905,6 +3451,7 @@ Options:
|
|
|
1905
3451
|
--config <path> Native TypeScript or JavaScript Test config
|
|
1906
3452
|
--project <name> Select a native execution Project (repeatable)
|
|
1907
3453
|
--result-dir <path> Store Test result files in this directory
|
|
3454
|
+
--reporter <default|verbose> Concise Case results or full step diagnostics
|
|
1908
3455
|
--help, -h Show this help
|
|
1909
3456
|
--version Show the package version
|
|
1910
3457
|
|
|
@@ -1965,11 +3512,17 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
1965
3512
|
io.log(`\nNode Spec generated: ${specPath}`);
|
|
1966
3513
|
}
|
|
1967
3514
|
};
|
|
1968
|
-
const reportTestProjectResult = (result, io, infrastructureErrors = result.errors ?? [])=>{
|
|
3515
|
+
const reportTestProjectResult = (result, io, reporter, projectRoot, infrastructureErrors = result.errors ?? [])=>{
|
|
3516
|
+
reporter.finish(result);
|
|
1969
3517
|
const reportedErrors = new Set();
|
|
1970
|
-
const reportError = (source, error)=>{
|
|
3518
|
+
const reportError = (source, error, sourcePath, context)=>{
|
|
1971
3519
|
if (error instanceof Error && reportedErrors.has(error)) return;
|
|
1972
|
-
|
|
3520
|
+
reporter.failure(source, error, {
|
|
3521
|
+
projectRoot,
|
|
3522
|
+
...context
|
|
3523
|
+
});
|
|
3524
|
+
const annotation = githubFailureAnnotation(source, formatConciseError(error), sourcePath ? (0, external_node_path_.resolve)(projectRoot, sourcePath) : void 0);
|
|
3525
|
+
if (annotation) io.log(annotation);
|
|
1973
3526
|
const pending = [
|
|
1974
3527
|
error
|
|
1975
3528
|
];
|
|
@@ -1981,7 +3534,33 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
1981
3534
|
if (current instanceof AggregateError) pending.push(...current.errors);
|
|
1982
3535
|
}
|
|
1983
3536
|
};
|
|
1984
|
-
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error
|
|
3537
|
+
for (const failure of result.collectionErrors)reportError(`${failure.projectName}/${failure.sourcePath}`, failure.error, failure.sourcePath, {
|
|
3538
|
+
projectName: failure.projectName,
|
|
3539
|
+
sourcePath: failure.sourcePath,
|
|
3540
|
+
phase: 'collection'
|
|
3541
|
+
});
|
|
3542
|
+
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
3543
|
+
document.documentId,
|
|
3544
|
+
document
|
|
3545
|
+
]));
|
|
3546
|
+
const documentContext = (projectName, documentId, sourcePath)=>{
|
|
3547
|
+
const project = result.projects.find((project)=>project.name === projectName);
|
|
3548
|
+
const document = finalDocuments.get(documentId);
|
|
3549
|
+
const plan = document?.plan ?? project?.plans?.find((plan)=>plan.documentId === documentId);
|
|
3550
|
+
const fileIndex = document?.attemptIndex ?? 0;
|
|
3551
|
+
return {
|
|
3552
|
+
plan,
|
|
3553
|
+
projectName,
|
|
3554
|
+
sourcePath,
|
|
3555
|
+
...plan?.retry.scope === 'document' ? {
|
|
3556
|
+
fileAttempt: {
|
|
3557
|
+
index: fileIndex,
|
|
3558
|
+
total: Math.max(fileIndex + 1, plan.retry.count + 1)
|
|
3559
|
+
}
|
|
3560
|
+
} : {},
|
|
3561
|
+
caseAttempts: plan?.retry.scope === 'document' ? 1 : (plan?.retry.count ?? project?.retry ?? 0) + 1
|
|
3562
|
+
};
|
|
3563
|
+
};
|
|
1985
3564
|
const finalCases = new Map(result.cases.map((outcome)=>[
|
|
1986
3565
|
outcome.caseId,
|
|
1987
3566
|
outcome
|
|
@@ -1990,43 +3569,101 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
1990
3569
|
if ('failed' !== outcome.status || !outcome.run) continue;
|
|
1991
3570
|
const run = outcome.run;
|
|
1992
3571
|
const source = `${run.projectName}/${run.sourcePath} / ${run.name}`;
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
...
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
3572
|
+
const { caseAttempts, plan, ...scope } = documentContext(run.projectName, outcome.documentId, run.sourcePath);
|
|
3573
|
+
const context = {
|
|
3574
|
+
...scope,
|
|
3575
|
+
caseName: run.name,
|
|
3576
|
+
attempt: {
|
|
3577
|
+
index: run.attemptIndex,
|
|
3578
|
+
total: Math.max(caseAttempts, run.attemptIndex + 1)
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
const casePlan = plan?.cases.find((item)=>item.caseId === outcome.caseId);
|
|
3582
|
+
for (const [phase, steps] of [
|
|
3583
|
+
[
|
|
3584
|
+
'beforeEach',
|
|
3585
|
+
run.beforeEach
|
|
3586
|
+
],
|
|
3587
|
+
[
|
|
3588
|
+
'steps',
|
|
3589
|
+
run.steps
|
|
3590
|
+
],
|
|
3591
|
+
[
|
|
3592
|
+
'afterEach',
|
|
3593
|
+
run.afterEach
|
|
3594
|
+
]
|
|
3595
|
+
])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}${run.attemptIndex ? ` (failed after ${run.attemptIndex + 1} attempts)` : ''}`, step.error, run.sourcePath, {
|
|
3596
|
+
...context,
|
|
3597
|
+
step,
|
|
3598
|
+
stepCount: casePlan?.[phase].length
|
|
3599
|
+
});
|
|
3600
|
+
for (const [phase, errors] of [
|
|
3601
|
+
[
|
|
3602
|
+
'execution',
|
|
3603
|
+
run.executionErrors
|
|
3604
|
+
],
|
|
3605
|
+
[
|
|
3606
|
+
'teardown',
|
|
3607
|
+
run.teardownErrors
|
|
3608
|
+
]
|
|
3609
|
+
])for (const error of errors ?? [])reportError(source, error, run.sourcePath, {
|
|
3610
|
+
...context,
|
|
3611
|
+
phase
|
|
3612
|
+
});
|
|
2002
3613
|
}
|
|
2003
|
-
const finalDocuments = new Map(result.documents.map((document)=>[
|
|
2004
|
-
document.documentId,
|
|
2005
|
-
document
|
|
2006
|
-
]));
|
|
2007
3614
|
for (const document of finalDocuments.values()){
|
|
2008
3615
|
if ('failed' !== document.status) continue;
|
|
2009
3616
|
const source = `${document.projectName}/${document.sourcePath}`;
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
3617
|
+
const { caseAttempts: _, plan, ...context } = documentContext(document.projectName, document.documentId, document.sourcePath);
|
|
3618
|
+
for (const [phase, steps] of [
|
|
3619
|
+
[
|
|
3620
|
+
'beforeAll',
|
|
3621
|
+
document.beforeAll
|
|
3622
|
+
],
|
|
3623
|
+
[
|
|
3624
|
+
'afterAll',
|
|
3625
|
+
document.afterAll
|
|
3626
|
+
]
|
|
3627
|
+
])for (const step of steps)if (step.error) reportError(`${source} / ${step.phase}[${step.stepIndex + 1}] ${step.node}`, step.error, document.sourcePath, {
|
|
3628
|
+
...context,
|
|
3629
|
+
step,
|
|
3630
|
+
stepCount: plan?.[phase].length
|
|
3631
|
+
});
|
|
3632
|
+
for (const [phase, errors] of [
|
|
3633
|
+
[
|
|
3634
|
+
'execution',
|
|
3635
|
+
document.executionErrors
|
|
3636
|
+
],
|
|
3637
|
+
[
|
|
3638
|
+
'teardown',
|
|
3639
|
+
document.teardownErrors
|
|
3640
|
+
]
|
|
3641
|
+
])for (const error of errors ?? [])reportError(source, error, document.sourcePath, {
|
|
3642
|
+
...context,
|
|
3643
|
+
phase
|
|
3644
|
+
});
|
|
3645
|
+
for (const failure of document.hostErrors ?? [])reportError(`${source} / host ${failure.phase}`, failure.error, document.sourcePath, {
|
|
3646
|
+
...context,
|
|
3647
|
+
phase: `host ${failure.phase}`
|
|
3648
|
+
});
|
|
2019
3649
|
}
|
|
2020
3650
|
for (const project of result.projects){
|
|
2021
|
-
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError
|
|
2022
|
-
|
|
3651
|
+
if (project.lifecycle?.setupError) reportError(`${project.name} / setup`, project.lifecycle.setupError, void 0, {
|
|
3652
|
+
projectName: project.name,
|
|
3653
|
+
phase: 'setup'
|
|
3654
|
+
});
|
|
3655
|
+
for (const error of project.lifecycle?.teardownErrors ?? [])reportError(`${project.name} / teardown`, error, void 0, {
|
|
3656
|
+
projectName: project.name,
|
|
3657
|
+
phase: 'teardown'
|
|
3658
|
+
});
|
|
2023
3659
|
}
|
|
2024
3660
|
for (const error of infrastructureErrors)reportError('infrastructure', error);
|
|
2025
|
-
|
|
2026
|
-
io.log(`Results: ${result.resultDir}`);
|
|
3661
|
+
reporter.summary(result);
|
|
3662
|
+
if ('verbose' === reporter.mode) io.log(`Results: ${result.resultDir}`);
|
|
2027
3663
|
io.log(`Summary: ${result.summaryPath}`);
|
|
2028
|
-
if (result.markdownPath) io.log(`Markdown: ${result.markdownPath}`);
|
|
3664
|
+
if (result.markdownPath && 'verbose' === reporter.mode) io.log(`Markdown: ${result.markdownPath}`);
|
|
2029
3665
|
if (result.reportPath) io.log(`Report: ${result.reportPath}`);
|
|
3666
|
+
writeGitHubSummary(result);
|
|
2030
3667
|
return result.exitCode;
|
|
2031
3668
|
};
|
|
2032
3669
|
async function runTestCli(args, io = defaultCliIO) {
|
|
@@ -2056,18 +3693,32 @@ Legacy tasks/flow YAML and its CLI options belong to the midscene command.
|
|
|
2056
3693
|
await runNodesCommand(options, io);
|
|
2057
3694
|
return 0;
|
|
2058
3695
|
}
|
|
2059
|
-
|
|
3696
|
+
const reporter = new TerminalReporter(io, options.reporter);
|
|
3697
|
+
const restoreOutput = io === defaultCliIO && io.isTTY && 'verbose' !== options.reporter ? captureTerminalOutput((message, write)=>reporter.output(message, write)) : void 0;
|
|
3698
|
+
const projectRoot = (0, external_node_fs_.realpathSync)(options.projectRoot && (0, external_node_fs_.existsSync)(options.projectRoot) && (0, external_node_fs_.statSync)(options.projectRoot).isFile() ? (0, external_node_path_.dirname)(options.projectRoot) : options.projectRoot ?? options.cwd);
|
|
2060
3699
|
try {
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
3700
|
+
return await (0, logger_namespaceObject.runWithConsoleLogHandler)((entry)=>reporter.console(entry), async ()=>{
|
|
3701
|
+
let result;
|
|
3702
|
+
try {
|
|
3703
|
+
result = await runTestProject({
|
|
3704
|
+
...options,
|
|
3705
|
+
onProgress: (message)=>reporter.progress(message),
|
|
3706
|
+
onEvent: (event)=>reporter.event(event)
|
|
3707
|
+
});
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
if (!(error instanceof test_runner_namespaceObject.WorkflowExecutionFailure)) throw error;
|
|
3710
|
+
reportTestProjectResult(error.result, io, reporter, projectRoot, error.errors);
|
|
3711
|
+
return 1;
|
|
3712
|
+
}
|
|
3713
|
+
return reportTestProjectResult(result, io, reporter, projectRoot);
|
|
2064
3714
|
});
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
3715
|
+
} finally{
|
|
3716
|
+
try {
|
|
3717
|
+
reporter.dispose();
|
|
3718
|
+
} finally{
|
|
3719
|
+
restoreOutput?.();
|
|
3720
|
+
}
|
|
2069
3721
|
}
|
|
2070
|
-
return reportTestProjectResult(result, io);
|
|
2071
3722
|
} catch (error) {
|
|
2072
3723
|
io.error(formatCliError(error));
|
|
2073
3724
|
return 1;
|