@midscene/test 1.15.4-beta-20261013090705.0 → 1.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/{1~214.mjs → 1~4.mjs} +96 -174
- package/dist/es/1~4.mjs.map +1 -0
- package/dist/es/cli/index.mjs +741 -1910
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +1 -20
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/midscene/index.mjs +2 -2
- package/dist/es/midscene/index.mjs.map +1 -1
- package/dist/es/test-cli.mjs +70 -1265
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/{0~160.js → 0~593.js} +95 -173
- package/dist/lib/0~593.js.map +1 -0
- package/dist/lib/cli/index.js +740 -1927
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +1 -20
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +0 -3
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +70 -1265
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/templates/manifest.json +60 -0
- package/dist/templates/test/android/.env.example +15 -0
- package/dist/templates/test/android/README.md +19 -0
- package/dist/templates/test/android/README.zh.md +11 -0
- package/dist/templates/test/android/cases/todo.yaml +26 -0
- package/dist/templates/test/android/gitignore +4 -0
- package/dist/templates/test/android/midscene.config.ts +69 -0
- package/dist/templates/test/android/package.json +20 -0
- package/dist/templates/test/android/tsconfig.json +12 -0
- package/dist/templates/test/computer/.env.example +17 -0
- package/dist/templates/test/computer/README.md +19 -0
- package/dist/templates/test/computer/README.zh.md +11 -0
- package/dist/templates/test/computer/cases/todo.yaml +31 -0
- package/dist/templates/test/computer/gitignore +4 -0
- package/dist/templates/test/computer/midscene.config.ts +74 -0
- package/dist/templates/test/computer/package.json +20 -0
- package/dist/templates/test/computer/tsconfig.json +12 -0
- package/dist/templates/test/harmony/.env.example +15 -0
- package/dist/templates/test/harmony/README.md +19 -0
- package/dist/templates/test/harmony/README.zh.md +11 -0
- package/dist/templates/test/harmony/cases/todo.yaml +26 -0
- package/dist/templates/test/harmony/gitignore +4 -0
- package/dist/templates/test/harmony/midscene.config.ts +69 -0
- package/dist/templates/test/harmony/package.json +20 -0
- package/dist/templates/test/harmony/tsconfig.json +12 -0
- package/dist/templates/test/ios/.env.example +18 -0
- package/dist/templates/test/ios/README.md +19 -0
- package/dist/templates/test/ios/README.zh.md +11 -0
- package/dist/templates/test/ios/cases/todo.yaml +26 -0
- package/dist/templates/test/ios/gitignore +4 -0
- package/dist/templates/test/ios/midscene.config.ts +95 -0
- package/dist/templates/test/ios/package.json +20 -0
- package/dist/templates/test/ios/tsconfig.json +12 -0
- package/dist/templates/test/web/.env.example +14 -0
- package/dist/templates/test/web/README.md +19 -0
- package/dist/templates/test/web/README.zh.md +13 -0
- package/dist/templates/test/web/cases/todo.yaml +62 -0
- package/dist/templates/test/web/gitignore +4 -0
- package/dist/templates/test/web/midscene.config.ts +56 -0
- package/dist/templates/test/web/nodes/todo.ts +215 -0
- package/dist/templates/test/web/package.json +21 -0
- package/dist/templates/test/web/tsconfig.json +12 -0
- package/dist/types/cli/index.d.ts +0 -556
- package/dist/types/index.d.ts +0 -20
- package/dist/types/midscene/index.d.ts +2 -6
- package/package.json +16 -15
- package/dist/es/1~214.mjs.map +0 -1
- package/dist/lib/0~160.js.map +0 -1
package/dist/lib/cli/index.js
CHANGED
|
@@ -25,1293 +25,751 @@ var __webpack_exports__ = {};
|
|
|
25
25
|
__webpack_require__.r(__webpack_exports__);
|
|
26
26
|
__webpack_require__.d(__webpack_exports__, {
|
|
27
27
|
DEFAULT_TEST_FILE_SELECTION: ()=>DEFAULT_TEST_FILE_SELECTION,
|
|
28
|
-
TestExecutorError: ()=>TestExecutorError,
|
|
29
28
|
createTestRunId: ()=>createTestRunId,
|
|
30
|
-
defineTestExecutor: ()=>defineTestExecutor,
|
|
31
|
-
discoverTestConfig: ()=>discoverTestConfig,
|
|
32
29
|
runTestProject: ()=>runTestProject,
|
|
33
|
-
|
|
34
|
-
discoverTestFiles: ()=>discoverTestFiles,
|
|
35
|
-
defineTestProject: ()=>defineTestProject,
|
|
36
|
-
localTestExecutor: ()=>localTestExecutor,
|
|
37
|
-
isTestExecutorError: ()=>isTestExecutorError,
|
|
38
|
-
assertTestCaseTaskRunResult: ()=>assertTestCaseTaskRunResult,
|
|
39
|
-
defineProjectSetup: ()=>defineProjectSetup,
|
|
30
|
+
discoverTestConfig: ()=>discoverTestConfig,
|
|
40
31
|
validateTestFileSelection: ()=>validateTestFileSelection,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const json = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
|
|
46
|
-
v4_namespaceObject.z["null"](),
|
|
47
|
-
v4_namespaceObject.z.string(),
|
|
48
|
-
v4_namespaceObject.z.number().finite(),
|
|
49
|
-
v4_namespaceObject.z.boolean(),
|
|
50
|
-
v4_namespaceObject.z.array(json),
|
|
51
|
-
v4_namespaceObject.z.record(v4_namespaceObject.z.string(), json)
|
|
52
|
-
]));
|
|
53
|
-
const time = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)));
|
|
54
|
-
const executor_runtime_schema_count = v4_namespaceObject.z.number().int().nonnegative();
|
|
55
|
-
const executor_runtime_schema_value = v4_namespaceObject.z.strictObject({
|
|
56
|
-
value: json,
|
|
57
|
-
truncatedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional(),
|
|
58
|
-
redactedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional()
|
|
59
|
-
});
|
|
60
|
-
const reportErrorSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.strictObject({
|
|
61
|
-
format: v4_namespaceObject.z.literal('midscene-error').optional(),
|
|
62
|
-
name: v4_namespaceObject.z.string(),
|
|
63
|
-
message: v4_namespaceObject.z.string(),
|
|
64
|
-
code: v4_namespaceObject.z.string().optional(),
|
|
65
|
-
details: executor_runtime_schema_value.optional(),
|
|
66
|
-
cause: reportErrorSchema.optional(),
|
|
67
|
-
errors: v4_namespaceObject.z.array(reportErrorSchema).optional(),
|
|
68
|
-
truncated: v4_namespaceObject.z.boolean().optional()
|
|
69
|
-
}));
|
|
70
|
-
const executor_runtime_schema_operation = v4_namespaceObject.z.strictObject({
|
|
71
|
-
operationId: v4_namespaceObject.z.string().min(1),
|
|
72
|
-
scopeId: v4_namespaceObject.z.string().optional(),
|
|
73
|
-
stepId: v4_namespaceObject.z.string().optional(),
|
|
74
|
-
node: v4_namespaceObject.z.string().optional()
|
|
75
|
-
});
|
|
76
|
-
const cleanupSchema = v4_namespaceObject.z.strictObject({
|
|
77
|
-
status: v4_namespaceObject.z["enum"]([
|
|
78
|
-
'not-required',
|
|
79
|
-
'completed',
|
|
80
|
-
'pending',
|
|
81
|
-
'failed'
|
|
82
|
-
]),
|
|
83
|
-
capturedAt: time,
|
|
84
|
-
operationId: v4_namespaceObject.z.string().optional(),
|
|
85
|
-
blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional(),
|
|
86
|
-
error: reportErrorSchema.optional()
|
|
87
|
-
});
|
|
88
|
-
const pendingOperationsSchema = v4_namespaceObject.z.array(executor_runtime_schema_operation);
|
|
89
|
-
const collectionStatus = v4_namespaceObject.z["enum"]([
|
|
90
|
-
'complete',
|
|
91
|
-
'partial',
|
|
92
|
-
'unknown'
|
|
93
|
-
]);
|
|
94
|
-
const metricsSchema = v4_namespaceObject.z.strictObject({
|
|
95
|
-
modelCallCount: executor_runtime_schema_count,
|
|
96
|
-
modelTimeMs: v4_namespaceObject.z.number().finite().nonnegative(),
|
|
97
|
-
promptTokens: executor_runtime_schema_count,
|
|
98
|
-
cachedInputTokens: executor_runtime_schema_count,
|
|
99
|
-
completionTokens: executor_runtime_schema_count,
|
|
100
|
-
totalTokens: executor_runtime_schema_count,
|
|
101
|
-
collection: v4_namespaceObject.z.strictObject({
|
|
102
|
-
status: collectionStatus,
|
|
103
|
-
expectedExecutionCount: executor_runtime_schema_count,
|
|
104
|
-
capturedExecutionCount: executor_runtime_schema_count,
|
|
105
|
-
reasons: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
|
|
106
|
-
fields: v4_namespaceObject.z.strictObject({
|
|
107
|
-
modelCallCount: collectionStatus,
|
|
108
|
-
modelTimeMs: collectionStatus,
|
|
109
|
-
totalTokens: collectionStatus
|
|
110
|
-
}).optional()
|
|
111
|
-
}).optional()
|
|
112
|
-
});
|
|
113
|
-
const operationsSchema = v4_namespaceObject.z.strictObject({
|
|
114
|
-
capturedAt: time,
|
|
115
|
-
events: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
116
|
-
operationId: v4_namespaceObject.z.string().min(1),
|
|
117
|
-
operationKind: v4_namespaceObject.z["enum"]([
|
|
118
|
-
'node',
|
|
119
|
-
'resource',
|
|
120
|
-
'acquisition',
|
|
121
|
-
'cleanup',
|
|
122
|
-
'readiness'
|
|
123
|
-
]),
|
|
124
|
-
kind: v4_namespaceObject.z["enum"]([
|
|
125
|
-
'started',
|
|
126
|
-
'cancel-requested',
|
|
127
|
-
'settled',
|
|
128
|
-
'runner-returned',
|
|
129
|
-
'cleanup-deferred'
|
|
130
|
-
]),
|
|
131
|
-
at: time,
|
|
132
|
-
stepId: v4_namespaceObject.z.string().optional(),
|
|
133
|
-
scopeId: v4_namespaceObject.z.string().optional(),
|
|
134
|
-
node: v4_namespaceObject.z.string().optional(),
|
|
135
|
-
status: v4_namespaceObject.z["enum"]([
|
|
136
|
-
'success',
|
|
137
|
-
'failed',
|
|
138
|
-
'not-run'
|
|
139
|
-
]).optional(),
|
|
140
|
-
pending: v4_namespaceObject.z.boolean().optional(),
|
|
141
|
-
error: reportErrorSchema.optional(),
|
|
142
|
-
blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional()
|
|
143
|
-
}))
|
|
144
|
-
});
|
|
145
|
-
const runtimeResultFields = {
|
|
146
|
-
executionStatus: v4_namespaceObject.z["enum"]([
|
|
147
|
-
'success',
|
|
148
|
-
'failed',
|
|
149
|
-
'not-run'
|
|
150
|
-
]).optional(),
|
|
151
|
-
phases: v4_namespaceObject.z.partialRecord(v4_namespaceObject.z["enum"]([
|
|
152
|
-
'beforeAll',
|
|
153
|
-
'beforeEach',
|
|
154
|
-
'steps',
|
|
155
|
-
'afterEach',
|
|
156
|
-
'afterAll'
|
|
157
|
-
]), v4_namespaceObject.z.strictObject({
|
|
158
|
-
status: v4_namespaceObject.z["enum"]([
|
|
159
|
-
'success',
|
|
160
|
-
'failed',
|
|
161
|
-
'not-run',
|
|
162
|
-
'not-required'
|
|
163
|
-
]),
|
|
164
|
-
plannedStepCount: executor_runtime_schema_count,
|
|
165
|
-
executedStepCount: executor_runtime_schema_count
|
|
166
|
-
})).optional(),
|
|
167
|
-
cleanup: cleanupSchema.optional(),
|
|
168
|
-
pendingOperations: pendingOperationsSchema.optional(),
|
|
169
|
-
retryDecision: v4_namespaceObject.z.strictObject({
|
|
170
|
-
scope: v4_namespaceObject.z["enum"]([
|
|
171
|
-
'case',
|
|
172
|
-
'document'
|
|
173
|
-
]),
|
|
174
|
-
retryCount: executor_runtime_schema_count,
|
|
175
|
-
attemptIndex: executor_runtime_schema_count,
|
|
176
|
-
decision: v4_namespaceObject.z["enum"]([
|
|
177
|
-
'retry',
|
|
178
|
-
'stop'
|
|
179
|
-
]),
|
|
180
|
-
reason: v4_namespaceObject.z["enum"]([
|
|
181
|
-
'passed',
|
|
182
|
-
'attempt-failed',
|
|
183
|
-
'budget-exhausted',
|
|
184
|
-
'cleanup-pending',
|
|
185
|
-
'cleanup-failed',
|
|
186
|
-
'operation-pending',
|
|
187
|
-
'fatal-error',
|
|
188
|
-
'interrupted',
|
|
189
|
-
'bail',
|
|
190
|
-
'infrastructure-error'
|
|
191
|
-
])
|
|
192
|
-
}).optional()
|
|
193
|
-
};
|
|
194
|
-
const plannedStep = v4_namespaceObject.z.strictObject({
|
|
195
|
-
id: v4_namespaceObject.z.string(),
|
|
196
|
-
phase: v4_namespaceObject.z["enum"]([
|
|
197
|
-
'beforeAll',
|
|
198
|
-
'beforeEach',
|
|
199
|
-
'steps',
|
|
200
|
-
'afterEach',
|
|
201
|
-
'afterAll'
|
|
202
|
-
]),
|
|
203
|
-
stepIndex: executor_runtime_schema_count,
|
|
204
|
-
node: v4_namespaceObject.z.string(),
|
|
205
|
-
title: v4_namespaceObject.z.string().optional(),
|
|
206
|
-
input: executor_runtime_schema_value,
|
|
207
|
-
inputMode: v4_namespaceObject.z["enum"]([
|
|
208
|
-
'template',
|
|
209
|
-
'resolved'
|
|
210
|
-
]),
|
|
211
|
-
timeoutMs: v4_namespaceObject.z.number().positive().optional(),
|
|
212
|
-
continueOnError: v4_namespaceObject.z.boolean()
|
|
213
|
-
});
|
|
214
|
-
const documentPlanSchema = v4_namespaceObject.z.strictObject({
|
|
215
|
-
documentId: v4_namespaceObject.z.string(),
|
|
216
|
-
sourcePath: v4_namespaceObject.z.string(),
|
|
217
|
-
sourceYaml: executor_runtime_schema_value.optional(),
|
|
218
|
-
retry: v4_namespaceObject.z.strictObject({
|
|
219
|
-
scope: v4_namespaceObject.z["enum"]([
|
|
220
|
-
'case',
|
|
221
|
-
'document'
|
|
222
|
-
]),
|
|
223
|
-
count: executor_runtime_schema_count
|
|
224
|
-
}),
|
|
225
|
-
beforeAll: v4_namespaceObject.z.array(plannedStep),
|
|
226
|
-
afterAll: v4_namespaceObject.z.array(plannedStep),
|
|
227
|
-
cases: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
228
|
-
caseId: v4_namespaceObject.z.string(),
|
|
229
|
-
name: v4_namespaceObject.z.string(),
|
|
230
|
-
caseIndex: executor_runtime_schema_count,
|
|
231
|
-
selected: v4_namespaceObject.z.boolean(),
|
|
232
|
-
tags: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
|
|
233
|
-
onFailure: v4_namespaceObject.z["enum"]([
|
|
234
|
-
'continue',
|
|
235
|
-
'stop-document'
|
|
236
|
-
]).optional(),
|
|
237
|
-
beforeEach: v4_namespaceObject.z.array(plannedStep),
|
|
238
|
-
steps: v4_namespaceObject.z.array(plannedStep),
|
|
239
|
-
afterEach: v4_namespaceObject.z.array(plannedStep)
|
|
240
|
-
}))
|
|
241
|
-
});
|
|
242
|
-
function _define_property(obj, key, value) {
|
|
243
|
-
if (key in obj) Object.defineProperty(obj, key, {
|
|
244
|
-
value: value,
|
|
245
|
-
enumerable: true,
|
|
246
|
-
configurable: true,
|
|
247
|
-
writable: true
|
|
248
|
-
});
|
|
249
|
-
else obj[key] = value;
|
|
250
|
-
return obj;
|
|
251
|
-
}
|
|
252
|
-
const executorErrorKey = Symbol.for('@midscene/test/executor-error');
|
|
253
|
-
let _executorErrorKey = executorErrorKey;
|
|
254
|
-
class TestExecutorError extends Error {
|
|
255
|
-
constructor(message, options = {}){
|
|
256
|
-
super(message, {
|
|
257
|
-
cause: options.cause
|
|
258
|
-
}), _define_property(this, _executorErrorKey, true), _define_property(this, "kind", void 0), _define_property(this, "retryable", void 0);
|
|
259
|
-
this.name = 'TestExecutorError';
|
|
260
|
-
this.kind = options.kind ?? 'unknown';
|
|
261
|
-
this.retryable = options.retryable ?? false;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function isTestExecutorError(value) {
|
|
265
|
-
return value instanceof Error && true === Reflect.get(value, executorErrorKey) && [
|
|
266
|
-
'provision',
|
|
267
|
-
'transport',
|
|
268
|
-
'cleanup',
|
|
269
|
-
'report',
|
|
270
|
-
'unknown'
|
|
271
|
-
].includes(Reflect.get(value, 'kind')) && 'boolean' == typeof Reflect.get(value, 'retryable');
|
|
272
|
-
}
|
|
273
|
-
const defineTestExecutor = (executor)=>executor;
|
|
274
|
-
const localTestExecutor = Object.freeze({
|
|
275
|
-
name: 'local',
|
|
276
|
-
execute: (_task, context)=>context.runLocal()
|
|
277
|
-
});
|
|
278
|
-
const jsonValueSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
|
|
279
|
-
v4_namespaceObject.z["null"](),
|
|
280
|
-
v4_namespaceObject.z.string(),
|
|
281
|
-
v4_namespaceObject.z.number().finite(),
|
|
282
|
-
v4_namespaceObject.z.boolean(),
|
|
283
|
-
v4_namespaceObject.z.array(jsonValueSchema),
|
|
284
|
-
v4_namespaceObject.z.record(v4_namespaceObject.z.string(), jsonValueSchema)
|
|
285
|
-
]));
|
|
286
|
-
const timestampSchema = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
|
|
287
|
-
message: 'Expected an ISO-compatible timestamp.'
|
|
288
|
-
});
|
|
289
|
-
const errorSchema = v4_namespaceObject.z.strictObject({
|
|
290
|
-
format: v4_namespaceObject.z.literal('midscene-error').optional(),
|
|
291
|
-
cause: reportErrorSchema.optional(),
|
|
292
|
-
name: v4_namespaceObject.z.string().min(1),
|
|
293
|
-
message: v4_namespaceObject.z.string(),
|
|
294
|
-
code: v4_namespaceObject.z.string().min(1),
|
|
295
|
-
details: jsonValueSchema.optional()
|
|
296
|
-
});
|
|
297
|
-
const stepBaseSchema = v4_namespaceObject.z.strictObject({
|
|
298
|
-
phase: v4_namespaceObject.z["enum"]([
|
|
299
|
-
'beforeAll',
|
|
300
|
-
'beforeEach',
|
|
301
|
-
'steps',
|
|
302
|
-
'afterEach',
|
|
303
|
-
'afterAll'
|
|
304
|
-
]),
|
|
305
|
-
stepIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
306
|
-
node: v4_namespaceObject.z.string().min(1),
|
|
307
|
-
input: jsonValueSchema,
|
|
308
|
-
meta: v4_namespaceObject.z.strictObject({
|
|
309
|
-
timeoutMs: v4_namespaceObject.z.number().positive().finite().optional(),
|
|
310
|
-
continueOnError: v4_namespaceObject.z.boolean()
|
|
311
|
-
}),
|
|
312
|
-
continuedAfterError: v4_namespaceObject.z.boolean(),
|
|
313
|
-
waitForPreviousOperationMs: v4_namespaceObject.z.number().nonnegative().finite().optional(),
|
|
314
|
-
operations: operationsSchema.optional(),
|
|
315
|
-
output: v4_namespaceObject.z.strictObject({
|
|
316
|
-
summary: v4_namespaceObject.z.string().optional(),
|
|
317
|
-
data: jsonValueSchema.optional()
|
|
318
|
-
}).optional(),
|
|
319
|
-
error: errorSchema.optional(),
|
|
320
|
-
report: v4_namespaceObject.z.strictObject({
|
|
321
|
-
traces: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
322
|
-
type: v4_namespaceObject.z.literal('midscene-execution'),
|
|
323
|
-
executionId: v4_namespaceObject.z.string().min(1),
|
|
324
|
-
metrics: metricsSchema.optional()
|
|
325
|
-
})),
|
|
326
|
-
collectionErrors: v4_namespaceObject.z.array(errorSchema).optional()
|
|
327
|
-
}).optional()
|
|
328
|
-
});
|
|
329
|
-
const stepSchema = v4_namespaceObject.z.discriminatedUnion('status', [
|
|
330
|
-
stepBaseSchema.extend({
|
|
331
|
-
status: v4_namespaceObject.z["enum"]([
|
|
332
|
-
'success',
|
|
333
|
-
'failed'
|
|
334
|
-
]),
|
|
335
|
-
startedAt: timestampSchema,
|
|
336
|
-
endedAt: timestampSchema,
|
|
337
|
-
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
338
|
-
}),
|
|
339
|
-
stepBaseSchema.extend({
|
|
340
|
-
status: v4_namespaceObject.z.literal('not-run'),
|
|
341
|
-
notRunReason: v4_namespaceObject.z.string().min(1),
|
|
342
|
-
blockedBy: v4_namespaceObject.z.string().optional()
|
|
343
|
-
})
|
|
344
|
-
]);
|
|
345
|
-
const validatedStepSchema = stepSchema.superRefine((step, context)=>{
|
|
346
|
-
if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
|
|
347
|
-
code: 'custom',
|
|
348
|
-
message: 'Successful steps cannot contain failure evidence.'
|
|
349
|
-
});
|
|
350
|
-
if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
|
|
351
|
-
code: 'custom',
|
|
352
|
-
message: 'Continuation requires continueOnError.'
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
const attemptSchema = v4_namespaceObject.z.strictObject({
|
|
356
|
-
...runtimeResultFields,
|
|
357
|
-
caseId: v4_namespaceObject.z.string().min(1),
|
|
358
|
-
runId: v4_namespaceObject.z.string().min(1),
|
|
359
|
-
projectName: v4_namespaceObject.z.string().min(1),
|
|
360
|
-
attemptIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
361
|
-
name: v4_namespaceObject.z.string(),
|
|
362
|
-
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
363
|
-
caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
364
|
-
status: v4_namespaceObject.z["enum"]([
|
|
365
|
-
'success',
|
|
366
|
-
'failed'
|
|
367
|
-
]),
|
|
368
|
-
beforeEach: v4_namespaceObject.z.array(validatedStepSchema),
|
|
369
|
-
steps: v4_namespaceObject.z.array(validatedStepSchema),
|
|
370
|
-
afterEach: v4_namespaceObject.z.array(validatedStepSchema),
|
|
371
|
-
executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
372
|
-
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
373
|
-
reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional(),
|
|
374
|
-
startedAt: timestampSchema,
|
|
375
|
-
endedAt: timestampSchema,
|
|
376
|
-
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
377
|
-
});
|
|
378
|
-
const notRunReasonSchema = v4_namespaceObject.z["enum"]([
|
|
379
|
-
'document-start-failed',
|
|
380
|
-
'project-preflight-failed',
|
|
381
|
-
'project-setup-failed',
|
|
382
|
-
'interrupted',
|
|
383
|
-
'bail',
|
|
384
|
-
'fatal-error',
|
|
385
|
-
'executor-failed',
|
|
386
|
-
'cleanup-pending',
|
|
387
|
-
'cleanup-failed',
|
|
388
|
-
'operation-pending'
|
|
389
|
-
]);
|
|
390
|
-
const resultSchema = v4_namespaceObject.z.strictObject({
|
|
391
|
-
case: v4_namespaceObject.z.strictObject({
|
|
392
|
-
documentId: v4_namespaceObject.z.string().min(1),
|
|
393
|
-
caseId: v4_namespaceObject.z.string().min(1),
|
|
394
|
-
projectName: v4_namespaceObject.z.string().min(1),
|
|
395
|
-
name: v4_namespaceObject.z.string(),
|
|
396
|
-
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
397
|
-
caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
398
|
-
status: v4_namespaceObject.z["enum"]([
|
|
399
|
-
'success',
|
|
400
|
-
'failed',
|
|
401
|
-
'not-run'
|
|
402
|
-
]),
|
|
403
|
-
run: attemptSchema.optional(),
|
|
404
|
-
attempts: v4_namespaceObject.z.array(attemptSchema).optional(),
|
|
405
|
-
notRunReason: notRunReasonSchema.optional()
|
|
406
|
-
}),
|
|
407
|
-
document: v4_namespaceObject.z.strictObject({
|
|
408
|
-
...runtimeResultFields,
|
|
409
|
-
plan: documentPlanSchema.optional(),
|
|
410
|
-
attemptIndex: v4_namespaceObject.z.number().int().nonnegative().optional(),
|
|
411
|
-
hostErrors: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
412
|
-
phase: v4_namespaceObject.z["enum"]([
|
|
413
|
-
'setup',
|
|
414
|
-
'execution',
|
|
415
|
-
'cleanup',
|
|
416
|
-
'report',
|
|
417
|
-
'publication',
|
|
418
|
-
'observer'
|
|
419
|
-
]),
|
|
420
|
-
error: reportErrorSchema
|
|
421
|
-
})).optional(),
|
|
422
|
-
documentId: v4_namespaceObject.z.string().min(1),
|
|
423
|
-
documentRunId: v4_namespaceObject.z.string().min(1),
|
|
424
|
-
projectId: v4_namespaceObject.z.string().min(1),
|
|
425
|
-
projectName: v4_namespaceObject.z.string().min(1),
|
|
426
|
-
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
427
|
-
status: v4_namespaceObject.z["enum"]([
|
|
428
|
-
'success',
|
|
429
|
-
'failed'
|
|
430
|
-
]),
|
|
431
|
-
startedAt: timestampSchema,
|
|
432
|
-
endedAt: timestampSchema,
|
|
433
|
-
durationMs: v4_namespaceObject.z.number().nonnegative().finite(),
|
|
434
|
-
beforeAll: v4_namespaceObject.z.array(validatedStepSchema),
|
|
435
|
-
afterAll: v4_namespaceObject.z.array(validatedStepSchema),
|
|
436
|
-
executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
437
|
-
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
438
|
-
reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional()
|
|
439
|
-
}).optional(),
|
|
440
|
-
lifecycle: v4_namespaceObject.z.strictObject({
|
|
441
|
-
cleanup: cleanupSchema.optional(),
|
|
442
|
-
pendingOperations: pendingOperationsSchema.optional(),
|
|
443
|
-
projectName: v4_namespaceObject.z.string().min(1),
|
|
444
|
-
status: v4_namespaceObject.z["enum"]([
|
|
445
|
-
'success',
|
|
446
|
-
'failed'
|
|
447
|
-
]),
|
|
448
|
-
setupError: errorSchema.optional(),
|
|
449
|
-
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
450
|
-
startedAt: timestampSchema,
|
|
451
|
-
endedAt: timestampSchema,
|
|
452
|
-
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
453
|
-
}).optional(),
|
|
454
|
-
artifacts: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
455
|
-
name: v4_namespaceObject.z.string().min(1),
|
|
456
|
-
uri: v4_namespaceObject.z.string().min(1),
|
|
457
|
-
mediaType: v4_namespaceObject.z.string().min(1).optional()
|
|
458
|
-
})).optional(),
|
|
459
|
-
metadata: v4_namespaceObject.z.record(v4_namespaceObject.z.string(), v4_namespaceObject.z.union([
|
|
460
|
-
v4_namespaceObject.z.string(),
|
|
461
|
-
v4_namespaceObject.z.number().finite(),
|
|
462
|
-
v4_namespaceObject.z.boolean(),
|
|
463
|
-
v4_namespaceObject.z["null"]()
|
|
464
|
-
])).optional()
|
|
32
|
+
defineProjectSetup: ()=>defineProjectSetup,
|
|
33
|
+
discoverTestFiles: ()=>discoverTestFiles,
|
|
34
|
+
loadTestProject: ()=>loadTestProject,
|
|
35
|
+
defineTestProject: ()=>defineTestProject
|
|
465
36
|
});
|
|
466
|
-
const attemptMatchesTask = (task, attempt)=>attempt.caseId === task.caseId && attempt.projectName === task.projectName && attempt.name === task.caseName && attempt.sourcePath === task.sourcePath && attempt.caseIndex === task.caseIndex && isSafeResultPathSegment(attempt.runId);
|
|
467
|
-
const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
|
|
468
|
-
'completed',
|
|
469
|
-
'not-required'
|
|
470
|
-
].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
|
|
471
|
-
function assertTestCaseTaskRunResult(task, value) {
|
|
472
|
-
const parsed = resultSchema.safeParse(value);
|
|
473
|
-
if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${v4_namespaceObject.z.prettifyError(parsed.error)}`);
|
|
474
|
-
const outcome = parsed.data.case;
|
|
475
|
-
if (outcome.caseId !== task.caseId || outcome.documentId !== task.documentId || outcome.projectName !== task.projectName || outcome.name !== task.caseName || outcome.sourcePath !== task.sourcePath || outcome.caseIndex !== task.caseIndex) throw new Error(`Executor returned a result that does not match task ${task.caseId}.`);
|
|
476
|
-
const attempts = outcome.attempts ?? [];
|
|
477
|
-
for (const attempt of attempts){
|
|
478
|
-
const steps = [
|
|
479
|
-
...attempt.beforeEach,
|
|
480
|
-
...attempt.steps,
|
|
481
|
-
...attempt.afterEach
|
|
482
|
-
];
|
|
483
|
-
if ('success' === attempt.status && (0 === attempt.steps.length || (attempt.executionErrors?.length ?? 0) > 0 || steps.some((step)=>'success' !== step.status) || hasUnfinishedWork(attempt) || (attempt.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful attempt for ${task.caseId}.`);
|
|
484
|
-
}
|
|
485
|
-
if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
|
|
486
|
-
if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
|
|
487
|
-
if (attempts.some((attempt, index)=>attempt.attemptIndex !== index) || attempts.length > task.retry + 1) throw new TypeError(`Executor returned a non-sequential attempt history for ${task.caseId}.`);
|
|
488
|
-
const finalAttempt = attempts.at(-1);
|
|
489
|
-
if ('not-run' === outcome.status) {
|
|
490
|
-
if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
|
|
491
|
-
} else if (void 0 !== outcome.notRunReason || !outcome.run || !finalAttempt || finalAttempt.status !== outcome.status || outcome.run.runId !== finalAttempt.runId || JSON.stringify(outcome.run) !== JSON.stringify(finalAttempt) || attempts.slice(0, -1).some((attempt)=>'failed' !== attempt.status)) throw new TypeError(`Executor returned an inconsistent ${outcome.status} result for ${task.caseId}.`);
|
|
492
|
-
const reportRefs = [
|
|
493
|
-
...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
|
|
494
|
-
...parsed.data.document?.reportRefs ?? []
|
|
495
|
-
];
|
|
496
|
-
if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
|
|
497
|
-
const document = parsed.data.document;
|
|
498
|
-
if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
|
|
499
|
-
...document.beforeAll,
|
|
500
|
-
...document.afterAll
|
|
501
|
-
].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
|
|
502
|
-
const lifecycle = parsed.data.lifecycle;
|
|
503
|
-
if (lifecycle?.status === 'success' && (hasUnfinishedWork(lifecycle) || lifecycle.setupError || (lifecycle.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful lifecycle for ${task.caseId}.`);
|
|
504
|
-
if (document && (document.documentId !== task.documentId || document.projectId !== task.projectId || document.projectName !== task.projectName || document.sourcePath !== task.sourcePath || !isSafeResultPathSegment(document.documentRunId))) throw new Error(`Executor returned a document that does not match task ${task.caseId}.`);
|
|
505
|
-
if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
|
|
506
|
-
}
|
|
507
|
-
const external_node_fs_namespaceObject = require("node:fs");
|
|
508
37
|
const external_node_path_namespaceObject = require("node:path");
|
|
38
|
+
const external_node_url_namespaceObject = require("node:url");
|
|
39
|
+
const api_namespaceObject = require("tsx/cjs/api");
|
|
509
40
|
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
const
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
521
|
-
projectId,
|
|
522
|
-
sourcePath,
|
|
523
|
-
...invocationIndex ? [
|
|
524
|
-
invocationIndex
|
|
525
|
-
] : []
|
|
526
|
-
])).digest('hex');
|
|
527
|
-
const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
|
|
528
|
-
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
529
|
-
function parseWorkflowSource(source) {
|
|
530
|
-
const stack = [];
|
|
531
|
-
const nodes = [];
|
|
532
|
-
const sensitiveNodes = new Set();
|
|
533
|
-
const sensitiveValues = new Set();
|
|
534
|
-
const redactedPaths = new Set();
|
|
535
|
-
const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
|
|
536
|
-
const parsed = (0, external_js_yaml_namespaceObject.load)(source, {
|
|
537
|
-
schema: external_js_yaml_namespaceObject.JSON_SCHEMA,
|
|
538
|
-
listener (event, state) {
|
|
539
|
-
if ('open' === event) return void stack.push({
|
|
540
|
-
start: state.position + sourceOffset,
|
|
541
|
-
end: 0,
|
|
542
|
-
value: null,
|
|
543
|
-
children: []
|
|
544
|
-
});
|
|
545
|
-
const node = stack.pop();
|
|
546
|
-
node.end = state.position + sourceOffset;
|
|
547
|
-
node.value = state.result;
|
|
548
|
-
stack.at(-1)?.children.push(node);
|
|
549
|
-
nodes.push(node);
|
|
550
|
-
if ('mapping' !== state.kind) return;
|
|
551
|
-
for(let index = 0; index < node.children.length; index += 2){
|
|
552
|
-
const key = node.children[index].value;
|
|
553
|
-
const value = node.children[index + 1];
|
|
554
|
-
if ('string' == typeof key && (0, test_runner_namespaceObject.isReportSecretKey)(key) && value) {
|
|
555
|
-
sensitiveNodes.add(value);
|
|
556
|
-
if (null !== value.value) sensitiveValues.add(value.value);
|
|
557
|
-
redactedPaths.add(`$..[${JSON.stringify(key)}]`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
41
|
+
const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
|
|
42
|
+
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
43
|
+
const defineTestProject = (definition)=>definition;
|
|
44
|
+
const defineProjectSetup = (definition)=>definition;
|
|
45
|
+
const getDefaultExport = (loaded, absolutePath)=>{
|
|
46
|
+
if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
|
|
47
|
+
const defaultExport = loaded.default;
|
|
48
|
+
if ('object' == typeof defaultExport && null !== defaultExport && '__esModule' in defaultExport) {
|
|
49
|
+
if ('default' in defaultExport) return defaultExport.default;
|
|
50
|
+
throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
|
|
560
51
|
}
|
|
561
|
-
|
|
562
|
-
const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
|
|
563
|
-
let offset = 0;
|
|
564
|
-
let redacted = '';
|
|
565
|
-
for (const range of ranges){
|
|
566
|
-
if (range.start < offset) continue;
|
|
567
|
-
const text = source.slice(range.start, range.end);
|
|
568
|
-
const leading = text.match(/^\s*/)?.[0] ?? '';
|
|
569
|
-
const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
|
|
570
|
-
redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
|
|
571
|
-
offset = range.end;
|
|
52
|
+
return defaultExport;
|
|
572
53
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
54
|
+
throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
|
|
55
|
+
};
|
|
56
|
+
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
57
|
+
const rejectUnknownKeys = (value, allowedKeys, label)=>{
|
|
58
|
+
const allowed = new Set(allowedKeys);
|
|
59
|
+
const unknown = Object.keys(value).find((key)=>!allowed.has(key));
|
|
60
|
+
if (unknown) throw new TypeError(`Midscene config ${label}.${unknown} is not supported.`);
|
|
61
|
+
};
|
|
62
|
+
const isAbsolutePattern = (pattern)=>(0, external_node_path_namespaceObject.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith('\\\\');
|
|
63
|
+
const validatePatterns = (value, field, label = 'files', allowEmptyInclude = false)=>{
|
|
64
|
+
if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
|
|
65
|
+
if ('include' === field && 0 === value.length && !allowEmptyInclude) throw new TypeError(`Midscene config ${label}.include must be a non-empty array.`);
|
|
66
|
+
return value.map((pattern, index)=>{
|
|
67
|
+
if ('string' != typeof pattern || 0 === pattern.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
|
|
68
|
+
if (isAbsolutePattern(pattern)) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be relative to the project root.`);
|
|
69
|
+
if (pattern.split(/[\\/]/).includes('..')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must not contain a ".." path segment.`);
|
|
70
|
+
if (pattern.includes('\\')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must use POSIX path separators (/).`);
|
|
71
|
+
if (pattern.startsWith('!')) {
|
|
72
|
+
if ('include' === field) throw new TypeError(`Midscene config ${label}.include[${index}] must not be a negated pattern. Use files.exclude instead.`);
|
|
73
|
+
throw new TypeError(`Midscene config ${label}.exclude[${index}] must not be a negated pattern.`);
|
|
589
74
|
}
|
|
590
|
-
|
|
591
|
-
}
|
|
592
|
-
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
593
|
-
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
594
|
-
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
595
|
-
const missingVariable = (kind, name, options)=>{
|
|
596
|
-
throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
|
|
597
|
-
...options.location,
|
|
598
|
-
variable: name,
|
|
599
|
-
variableKind: kind
|
|
75
|
+
return pattern;
|
|
600
76
|
});
|
|
601
77
|
};
|
|
602
|
-
const
|
|
603
|
-
if (
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
78
|
+
const validateTestFileSelection = (value, label = 'files')=>{
|
|
79
|
+
if (void 0 === value) return;
|
|
80
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
81
|
+
rejectUnknownKeys(value, [
|
|
82
|
+
'include',
|
|
83
|
+
'exclude'
|
|
84
|
+
], label);
|
|
85
|
+
const include = validatePatterns(value.include, 'include', label);
|
|
86
|
+
const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
include: Object.freeze(include),
|
|
89
|
+
...exclude ? {
|
|
90
|
+
exclude: Object.freeze(exclude)
|
|
91
|
+
} : {}
|
|
92
|
+
});
|
|
610
93
|
};
|
|
611
|
-
const
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
94
|
+
const validateTagList = (value, field, label)=>{
|
|
95
|
+
if (void 0 === value) return [];
|
|
96
|
+
if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
|
|
97
|
+
return value.map((tag, index)=>{
|
|
98
|
+
if ('string' != typeof tag || 0 === tag.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
|
|
99
|
+
return tag;
|
|
616
100
|
});
|
|
617
|
-
return String(value);
|
|
618
101
|
};
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
102
|
+
const validateTagSelection = (value, label)=>{
|
|
103
|
+
if (void 0 === value) return Object.freeze({
|
|
104
|
+
include: [],
|
|
105
|
+
exclude: []
|
|
106
|
+
});
|
|
107
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
108
|
+
return Object.freeze({
|
|
109
|
+
include: Object.freeze(validateTagList(value.include, 'include', label)),
|
|
110
|
+
exclude: Object.freeze(validateTagList(value.exclude, 'exclude', label))
|
|
111
|
+
});
|
|
625
112
|
};
|
|
626
|
-
|
|
627
|
-
if ('string' == typeof value
|
|
628
|
-
if (
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
113
|
+
function assertJsonValue(value, path, seen) {
|
|
114
|
+
if (null === value || 'string' == typeof value || 'boolean' == typeof value) return;
|
|
115
|
+
if ('number' == typeof value) {
|
|
116
|
+
if (Number.isFinite(value)) return;
|
|
117
|
+
throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
118
|
+
}
|
|
119
|
+
if ('object' != typeof value || void 0 === value) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
120
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
121
|
+
if (seen.has(value)) throw new TypeError(`Midscene config ${path} must not contain cycles.`);
|
|
122
|
+
seen.add(value);
|
|
123
|
+
if (Array.isArray(value)) value.forEach((item, index)=>assertJsonValue(item, `${path}[${index}]`, seen));
|
|
124
|
+
else for (const [key, child] of Object.entries(value))assertJsonValue(child, `${path}.${key}`, seen);
|
|
125
|
+
seen.delete(value);
|
|
126
|
+
}
|
|
127
|
+
const deepFreezeJson = (value)=>{
|
|
128
|
+
if ('object' == typeof value && null !== value) {
|
|
129
|
+
for (const child of Object.values(value))deepFreezeJson(child);
|
|
130
|
+
Object.freeze(value);
|
|
131
|
+
}
|
|
633
132
|
return value;
|
|
634
133
|
};
|
|
635
|
-
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
writable: true
|
|
641
|
-
});
|
|
642
|
-
else obj[key] = value;
|
|
643
|
-
return obj;
|
|
644
|
-
}
|
|
645
|
-
const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
646
|
-
class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
|
|
647
|
-
constructor(actualFormat, sourcePath){
|
|
648
|
-
super('legacy' === actualFormat ? `Legacy tasks/flow YAML is not supported by midscene-test: ${sourcePath}. Run it with \`midscene ${sourcePath}\`, or migrate the file to cases/steps.` : 'legacy-batch' === actualFormat ? `Legacy YAML batch configuration is not a midscene-test workflow document: ${sourcePath}. Run it with \`midscene --config ${sourcePath}\`, or migrate it to a native project config.` : `A workflow file cannot mix legacy tasks/flow with native cases/steps or lifecycle hooks: ${sourcePath}. Keep the formats in separate files and run each file with its matching command.`, {
|
|
649
|
-
sourcePath,
|
|
650
|
-
actualFormat,
|
|
651
|
-
expectedFormat: 'native'
|
|
652
|
-
}), collect_define_property(this, "actualFormat", void 0);
|
|
653
|
-
this.actualFormat = actualFormat;
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
const rejectUnknownKeys = (value, allowed, label)=>{
|
|
657
|
-
const allowedSet = new Set(allowed);
|
|
658
|
-
const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
|
|
659
|
-
if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
|
|
660
|
-
field: unknown[0]
|
|
661
|
-
});
|
|
134
|
+
const validateVariables = (value, label)=>{
|
|
135
|
+
if (void 0 === value) return Object.freeze({});
|
|
136
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
137
|
+
assertJsonValue(value, label, new Set());
|
|
138
|
+
return deepFreezeJson(value);
|
|
662
139
|
};
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
140
|
+
const validateSetup = (value, label)=>{
|
|
141
|
+
if (void 0 === value) return;
|
|
142
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
143
|
+
rejectUnknownKeys(value, [
|
|
144
|
+
'name',
|
|
145
|
+
'setup'
|
|
146
|
+
], label);
|
|
147
|
+
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
148
|
+
if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
|
|
149
|
+
return value;
|
|
150
|
+
};
|
|
151
|
+
const validatePositiveInteger = (value, fallback, label)=>{
|
|
152
|
+
if (void 0 === value) return fallback;
|
|
153
|
+
if (!Number.isInteger(value) || value <= 0) throw new TypeError(`Midscene config ${label} must be a positive integer.`);
|
|
154
|
+
return value;
|
|
155
|
+
};
|
|
156
|
+
const validateNonNegativeInteger = (value, fallback, label)=>{
|
|
157
|
+
if (void 0 === value) return fallback;
|
|
158
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
|
|
159
|
+
return value;
|
|
160
|
+
};
|
|
161
|
+
const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
162
|
+
if (void 0 === value) {
|
|
163
|
+
const setup = validateSetup(defaultSetup, 'setup');
|
|
164
|
+
return {
|
|
165
|
+
hasExplicitProjects: false,
|
|
166
|
+
projects: Object.freeze([
|
|
167
|
+
Object.freeze({
|
|
168
|
+
projectId: projectIdFromIndex(0),
|
|
169
|
+
name: 'default',
|
|
170
|
+
...setup ? {
|
|
171
|
+
setup
|
|
172
|
+
} : {},
|
|
173
|
+
tags: Object.freeze({
|
|
174
|
+
include: [],
|
|
175
|
+
exclude: []
|
|
176
|
+
}),
|
|
177
|
+
retry: 0,
|
|
178
|
+
variables: Object.freeze({}),
|
|
179
|
+
nodes: new NodeRegistry(globalNodes.definitions())
|
|
180
|
+
})
|
|
181
|
+
])
|
|
182
|
+
};
|
|
673
183
|
}
|
|
674
|
-
|
|
675
|
-
if (!
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
'cases',
|
|
682
|
-
'afterEach',
|
|
683
|
-
'afterAll'
|
|
684
|
-
], 'Workflow document');
|
|
685
|
-
if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
|
|
686
|
-
const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
|
|
687
|
-
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
688
|
-
...step,
|
|
689
|
-
templateInput: step.input,
|
|
690
|
-
input: resolveWorkflowVariables(step.input, {
|
|
691
|
-
variables: options.variables,
|
|
692
|
-
env: options.env ?? process.env,
|
|
693
|
-
location: {
|
|
694
|
-
projectName: source.projectName ?? source.projectId,
|
|
695
|
-
sourcePath,
|
|
696
|
-
phase,
|
|
697
|
-
stepIndex,
|
|
698
|
-
...void 0 === caseIndex ? {} : {
|
|
699
|
-
caseIndex
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
})
|
|
703
|
-
});
|
|
704
|
-
const normalizeLifecycle = (phase)=>{
|
|
705
|
-
const value = parsed[phase];
|
|
706
|
-
if (void 0 === value) return [];
|
|
707
|
-
if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
|
|
708
|
-
phase
|
|
709
|
-
});
|
|
710
|
-
return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
|
|
711
|
-
const resolved = options.resolveNode(normalized.node);
|
|
712
|
-
if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
713
|
-
phase,
|
|
714
|
-
stepIndex,
|
|
715
|
-
node: normalized.node
|
|
716
|
-
});
|
|
717
|
-
return resolveStepVariables(normalized, phase, stepIndex);
|
|
718
|
-
});
|
|
719
|
-
};
|
|
720
|
-
const lifecycle = {
|
|
721
|
-
beforeAll: normalizeLifecycle('beforeAll'),
|
|
722
|
-
beforeEach: normalizeLifecycle('beforeEach'),
|
|
723
|
-
afterEach: normalizeLifecycle('afterEach'),
|
|
724
|
-
afterAll: normalizeLifecycle('afterAll')
|
|
725
|
-
};
|
|
726
|
-
const ids = new Set();
|
|
727
|
-
const cases = parsed.cases.map((definition, caseIndex)=>{
|
|
728
|
-
if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
|
|
729
|
-
caseIndex
|
|
730
|
-
});
|
|
731
|
-
rejectUnknownKeys(definition, [
|
|
732
|
-
'id',
|
|
184
|
+
if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
|
|
185
|
+
if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
|
|
186
|
+
const names = new Set();
|
|
187
|
+
const projects = value.map((candidate, index)=>{
|
|
188
|
+
const label = `projects[${index}]`;
|
|
189
|
+
if (!isRecord(candidate)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
190
|
+
rejectUnknownKeys(candidate, [
|
|
733
191
|
'name',
|
|
192
|
+
'setup',
|
|
193
|
+
'nodes',
|
|
194
|
+
'files',
|
|
734
195
|
'tags',
|
|
735
|
-
'
|
|
736
|
-
'
|
|
737
|
-
],
|
|
738
|
-
if (
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
caseIndex,
|
|
761
|
-
stepIndex,
|
|
762
|
-
node: normalized.node
|
|
763
|
-
});
|
|
764
|
-
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
765
|
-
});
|
|
766
|
-
const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
|
|
767
|
-
if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
768
|
-
caseId
|
|
769
|
-
});
|
|
770
|
-
ids.add(caseId);
|
|
771
|
-
return {
|
|
772
|
-
caseId,
|
|
773
|
-
projectId: source.projectId,
|
|
774
|
-
sourcePath,
|
|
775
|
-
caseIndex,
|
|
776
|
-
definition: {
|
|
777
|
-
...'string' == typeof definition.id ? {
|
|
778
|
-
id: definition.id
|
|
779
|
-
} : {},
|
|
780
|
-
name: definition.name,
|
|
781
|
-
tags: tags,
|
|
782
|
-
...resources.length > 0 ? {
|
|
783
|
-
resources: resources
|
|
784
|
-
} : {},
|
|
785
|
-
steps
|
|
196
|
+
'retry',
|
|
197
|
+
'variables'
|
|
198
|
+
], label);
|
|
199
|
+
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
200
|
+
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
201
|
+
names.add(candidate.name);
|
|
202
|
+
const files = validateTestFileSelection(candidate.files, `${label}.files`);
|
|
203
|
+
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
204
|
+
const localNodes = new NodeRegistry(candidate.nodes);
|
|
205
|
+
const nodes = new NodeRegistry([
|
|
206
|
+
...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
|
|
207
|
+
...localNodes.definitions()
|
|
208
|
+
]);
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
projectId: projectIdFromIndex(index),
|
|
211
|
+
name: candidate.name,
|
|
212
|
+
nodes,
|
|
213
|
+
...files ? {
|
|
214
|
+
files
|
|
215
|
+
} : {},
|
|
216
|
+
tags: validateTagSelection(candidate.tags, `${label}.tags`),
|
|
217
|
+
retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
|
|
218
|
+
variables: validateVariables(candidate.variables, `${label}.variables`),
|
|
219
|
+
...void 0 === candidate.setup ? {} : {
|
|
220
|
+
setup: validateSetup(candidate.setup, `${label}.setup`)
|
|
786
221
|
}
|
|
787
|
-
};
|
|
222
|
+
});
|
|
788
223
|
});
|
|
789
224
|
return {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
sourcePath,
|
|
793
|
-
sourceYaml,
|
|
794
|
-
lifecycle,
|
|
795
|
-
cases
|
|
225
|
+
projects: Object.freeze(projects),
|
|
226
|
+
hasExplicitProjects: true
|
|
796
227
|
};
|
|
797
|
-
}
|
|
798
|
-
const promises_namespaceObject = require("node:fs/promises");
|
|
799
|
-
const writeJson = async (path, value)=>{
|
|
800
|
-
try {
|
|
801
|
-
await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(path), {
|
|
802
|
-
recursive: true
|
|
803
|
-
});
|
|
804
|
-
await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
|
|
805
|
-
} catch (error) {
|
|
806
|
-
throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
|
|
807
|
-
}
|
|
808
228
|
};
|
|
809
|
-
const
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
}))
|
|
229
|
+
const validateTestOptions = (value)=>{
|
|
230
|
+
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
231
|
+
const candidate = value ?? {};
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
|
|
234
|
+
bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
|
|
235
|
+
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout')
|
|
817
236
|
});
|
|
818
|
-
const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
|
|
819
|
-
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
820
|
-
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
821
|
-
const collectionErrorPath = (projectId, sourcePath)=>{
|
|
822
|
-
const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
823
|
-
projectId,
|
|
824
|
-
sourcePath
|
|
825
|
-
])).digest('hex');
|
|
826
|
-
return toPosix((0, external_node_path_namespaceObject.join)(projectId, 'collection-errors', `${id}.json`));
|
|
827
237
|
};
|
|
828
|
-
const
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
|
|
833
|
-
const { runId: attemptId, ...persistedResult } = result;
|
|
834
|
-
return writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
|
|
835
|
-
...persistedResult,
|
|
836
|
-
attemptId
|
|
837
|
-
});
|
|
238
|
+
const validateOutputPath = (value, fallback, label)=>{
|
|
239
|
+
if (void 0 === value) return fallback;
|
|
240
|
+
if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
|
|
241
|
+
return value;
|
|
838
242
|
};
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
243
|
+
const validateOutput = (value)=>{
|
|
244
|
+
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config output must be an object.');
|
|
245
|
+
const candidate = value ?? {};
|
|
246
|
+
rejectUnknownKeys(candidate, [
|
|
247
|
+
'reportDir'
|
|
248
|
+
], 'output');
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
|
|
842
251
|
});
|
|
843
|
-
const relativeToSummary = (summaryPath, target)=>{
|
|
844
|
-
const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
|
|
845
|
-
return toPosix(value || '.');
|
|
846
252
|
};
|
|
847
|
-
const
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
path: relativeToSummary(result.summaryPath, result.operationLog.path)
|
|
872
|
-
}
|
|
873
|
-
} : {},
|
|
874
|
-
reportDir: relativeToSummary(result.summaryPath, result.reportDir),
|
|
875
|
-
...result.reportPath ? {
|
|
876
|
-
report: relativeToSummary(result.summaryPath, result.reportPath)
|
|
877
|
-
} : {},
|
|
878
|
-
...result.markdownPath ? {
|
|
879
|
-
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
880
|
-
} : {},
|
|
881
|
-
summary: result.summary,
|
|
882
|
-
projects: result.projects.map((project)=>({
|
|
883
|
-
projectId: project.projectId,
|
|
884
|
-
name: project.name,
|
|
885
|
-
status: project.status,
|
|
886
|
-
retry: project.retry,
|
|
887
|
-
fileSelection: project.fileSelection,
|
|
888
|
-
tagSelection: project.tagSelection,
|
|
889
|
-
sourceCount: project.sourceCount,
|
|
890
|
-
selectedCaseCount: project.selectedCaseCount,
|
|
891
|
-
filteredCaseCount: project.filteredCaseCount,
|
|
892
|
-
...project.lifecycle ? {
|
|
893
|
-
lifecycle: {
|
|
894
|
-
status: project.lifecycle.status,
|
|
895
|
-
startedAt: project.lifecycle.startedAt,
|
|
896
|
-
endedAt: project.lifecycle.endedAt,
|
|
897
|
-
durationMs: project.lifecycle.durationMs,
|
|
898
|
-
...project.lifecycle.cleanup ? {
|
|
899
|
-
cleanup: project.lifecycle.cleanup
|
|
900
|
-
} : {},
|
|
901
|
-
...project.lifecycle.pendingOperations ? {
|
|
902
|
-
pendingOperations: project.lifecycle.pendingOperations
|
|
903
|
-
} : {},
|
|
904
|
-
...project.lifecycle.setupError ? {
|
|
905
|
-
setupError: errorJson(project.lifecycle.setupError)
|
|
906
|
-
} : {},
|
|
907
|
-
...project.lifecycle.teardownErrors ? {
|
|
908
|
-
teardownErrors: project.lifecycle.teardownErrors.map(errorJson)
|
|
909
|
-
} : {}
|
|
910
|
-
}
|
|
911
|
-
} : {},
|
|
912
|
-
cases: project.cases.map((outcome)=>({
|
|
913
|
-
...outcome.documentRunId ? {
|
|
914
|
-
documentRunId: outcome.documentRunId
|
|
915
|
-
} : {},
|
|
916
|
-
documentId: outcome.documentId,
|
|
917
|
-
caseId: outcome.caseId,
|
|
918
|
-
sourcePath: outcome.sourcePath,
|
|
919
|
-
caseIndex: outcome.caseIndex,
|
|
920
|
-
name: outcome.name,
|
|
921
|
-
status: outcome.status,
|
|
922
|
-
...outcome.notRunReason ? {
|
|
923
|
-
notRunReason: outcome.notRunReason
|
|
924
|
-
} : {},
|
|
925
|
-
...outcome.execution ? {
|
|
926
|
-
execution: {
|
|
927
|
-
executor: outcome.execution.executor,
|
|
928
|
-
resources: outcome.execution.resources,
|
|
929
|
-
...void 0 === outcome.execution.attempts ? {} : {
|
|
930
|
-
attempts: outcome.execution.attempts
|
|
931
|
-
},
|
|
932
|
-
...outcome.execution.failure ? {
|
|
933
|
-
failure: outcome.execution.failure
|
|
934
|
-
} : {},
|
|
935
|
-
...outcome.execution.artifacts ? {
|
|
936
|
-
artifacts: outcome.execution.artifacts
|
|
937
|
-
} : {},
|
|
938
|
-
...outcome.execution.metadata ? {
|
|
939
|
-
metadata: outcome.execution.metadata
|
|
940
|
-
} : {},
|
|
941
|
-
...outcome.execution.lifecycle ? {
|
|
942
|
-
lifecycle: {
|
|
943
|
-
status: outcome.execution.lifecycle.status,
|
|
944
|
-
startedAt: outcome.execution.lifecycle.startedAt,
|
|
945
|
-
endedAt: outcome.execution.lifecycle.endedAt,
|
|
946
|
-
durationMs: outcome.execution.lifecycle.durationMs,
|
|
947
|
-
...outcome.execution.lifecycle.setupError ? {
|
|
948
|
-
setupError: errorJson(outcome.execution.lifecycle.setupError)
|
|
949
|
-
} : {},
|
|
950
|
-
...outcome.execution.lifecycle.teardownErrors ? {
|
|
951
|
-
teardownErrors: outcome.execution.lifecycle.teardownErrors.map(errorJson)
|
|
952
|
-
} : {}
|
|
953
|
-
}
|
|
954
|
-
} : {}
|
|
955
|
-
}
|
|
956
|
-
} : {},
|
|
957
|
-
...outcome.attempts ? {
|
|
958
|
-
attempts: outcome.attempts.map((attempt)=>({
|
|
959
|
-
attemptId: attempt.runId,
|
|
960
|
-
attemptIndex: attempt.attemptIndex,
|
|
961
|
-
status: attempt.status,
|
|
962
|
-
...attempt.cleanup ? {
|
|
963
|
-
cleanup: attempt.cleanup
|
|
964
|
-
} : {},
|
|
965
|
-
...attempt.pendingOperations ? {
|
|
966
|
-
pendingOperations: attempt.pendingOperations
|
|
967
|
-
} : {},
|
|
968
|
-
...attempt.executionStatus ? {
|
|
969
|
-
executionStatus: attempt.executionStatus
|
|
970
|
-
} : {},
|
|
971
|
-
...attempt.retryDecision ? {
|
|
972
|
-
retryDecision: attempt.retryDecision
|
|
973
|
-
} : {},
|
|
974
|
-
resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
|
|
975
|
-
...attempt.reportPaths?.length ? {
|
|
976
|
-
reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
977
|
-
} : {}
|
|
978
|
-
}))
|
|
979
|
-
} : {}
|
|
980
|
-
})),
|
|
981
|
-
documents: project.documents.map((document)=>({
|
|
982
|
-
documentId: document.documentId,
|
|
983
|
-
...void 0 === document.attemptIndex ? {} : {
|
|
984
|
-
attemptIndex: document.attemptIndex,
|
|
985
|
-
documentRunId: document.documentRunId
|
|
986
|
-
},
|
|
987
|
-
sourcePath: document.sourcePath,
|
|
988
|
-
status: document.status,
|
|
989
|
-
...document.cleanup ? {
|
|
990
|
-
cleanup: document.cleanup
|
|
991
|
-
} : {},
|
|
992
|
-
...document.pendingOperations ? {
|
|
993
|
-
pendingOperations: document.pendingOperations
|
|
994
|
-
} : {},
|
|
995
|
-
...document.executionStatus ? {
|
|
996
|
-
executionStatus: document.executionStatus
|
|
997
|
-
} : {},
|
|
998
|
-
...document.retryDecision ? {
|
|
999
|
-
retryDecision: document.retryDecision
|
|
1000
|
-
} : {},
|
|
1001
|
-
resultFile: fact(workflowDocumentResultPath(document)),
|
|
1002
|
-
...document.reportPaths?.length ? {
|
|
1003
|
-
reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
1004
|
-
} : {}
|
|
1005
|
-
})),
|
|
1006
|
-
collectionErrors: project.collectionErrors.map((error)=>({
|
|
1007
|
-
sourcePath: error.sourcePath,
|
|
1008
|
-
errorFile: fact(collectionErrorPath(error.projectId, error.sourcePath))
|
|
1009
|
-
}))
|
|
1010
|
-
}))
|
|
1011
|
-
});
|
|
253
|
+
const validateTestProjectDefinition = (definition)=>{
|
|
254
|
+
if (!isRecord(definition)) throw new TypeError('Midscene config must default export an object.');
|
|
255
|
+
if (void 0 !== definition.nodes && !Array.isArray(definition.nodes)) throw new TypeError('Midscene config nodes must be an array.');
|
|
256
|
+
if ('setupWorkflow' in definition) throw new TypeError('Midscene config setupWorkflow is no longer supported. Use setup or projects[].setup instead.');
|
|
257
|
+
if ('setupDocument' in definition) throw new TypeError('Midscene config setupDocument is not supported. Use setup or projects[].setup for resources, and beforeAll/afterAll for document lifecycle steps.');
|
|
258
|
+
if ('root' in definition) throw new TypeError('Midscene config root is not supported. Pass the Test Project directory to midscene-test instead.');
|
|
259
|
+
if ('files' in definition) throw new TypeError('Midscene config files is not supported at the root. Move it to projects[].files.');
|
|
260
|
+
if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
|
|
261
|
+
rejectUnknownKeys(definition, [
|
|
262
|
+
'setup',
|
|
263
|
+
'projects',
|
|
264
|
+
'test',
|
|
265
|
+
'output',
|
|
266
|
+
'nodes'
|
|
267
|
+
], 'root');
|
|
268
|
+
const nodes = new NodeRegistry(definition.nodes);
|
|
269
|
+
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
|
|
270
|
+
return {
|
|
271
|
+
...resolvedProjects,
|
|
272
|
+
test: validateTestOptions(definition.test),
|
|
273
|
+
output: validateOutput(definition.output),
|
|
274
|
+
nodes,
|
|
275
|
+
resolveNode: (name)=>nodes.get(name)
|
|
276
|
+
};
|
|
1012
277
|
};
|
|
1013
|
-
const
|
|
1014
|
-
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
const defineTestProject = (definition)=>definition;
|
|
1018
|
-
const defineProjectSetup = (definition)=>definition;
|
|
1019
|
-
const getDefaultExport = (loaded, absolutePath)=>{
|
|
1020
|
-
if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
|
|
1021
|
-
const defaultExport = loaded.default;
|
|
1022
|
-
if ('object' == typeof defaultExport && null !== defaultExport && '__esModule' in defaultExport) {
|
|
1023
|
-
if ('default' in defaultExport) return defaultExport.default;
|
|
1024
|
-
throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
|
|
1025
|
-
}
|
|
1026
|
-
return defaultExport;
|
|
278
|
+
const assertSupportedConfig = (absolutePath)=>{
|
|
279
|
+
if (!absolutePath.endsWith('.ts') && !absolutePath.endsWith('.mjs')) {
|
|
280
|
+
const extension = absolutePath.match(/(\.[^./\\]+)$/)?.[1] ?? '(none)';
|
|
281
|
+
throw new TypeError(`Unsupported Midscene config extension: ${extension}. Supported extensions: .ts, .mjs.`);
|
|
1027
282
|
}
|
|
1028
|
-
|
|
283
|
+
return absolutePath.endsWith('.mjs') ? '.mjs' : '.ts';
|
|
1029
284
|
};
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
const
|
|
1033
|
-
|
|
1034
|
-
if (unknown) throw new TypeError(`Midscene config ${label}.${unknown} is not supported.`);
|
|
285
|
+
const canRetryWithCjsLoader = (error)=>{
|
|
286
|
+
if (!(error instanceof Error)) return false;
|
|
287
|
+
const errorCode = error.code;
|
|
288
|
+
return error instanceof SyntaxError && /^Unexpected (?:identifier|reserved word|token)/.test(error.message) || 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' === errorCode || 'ERR_UNKNOWN_FILE_EXTENSION' === errorCode && /^Unknown file extension\b/.test(error.message);
|
|
1035
289
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
if ('include' === field && 0 === value.length && !allowEmptyInclude) throw new TypeError(`Midscene config ${label}.include must be a non-empty array.`);
|
|
1040
|
-
return value.map((pattern, index)=>{
|
|
1041
|
-
if ('string' != typeof pattern || 0 === pattern.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
|
|
1042
|
-
if (isAbsolutePattern(pattern)) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be relative to the project root.`);
|
|
1043
|
-
if (pattern.split(/[\\/]/).includes('..')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must not contain a ".." path segment.`);
|
|
1044
|
-
if (pattern.includes('\\')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must use POSIX path separators (/).`);
|
|
1045
|
-
if (pattern.startsWith('!')) {
|
|
1046
|
-
if ('include' === field) throw new TypeError(`Midscene config ${label}.include[${index}] must not be a negated pattern. Use files.exclude instead.`);
|
|
1047
|
-
throw new TypeError(`Midscene config ${label}.exclude[${index}] must not be a negated pattern.`);
|
|
1048
|
-
}
|
|
1049
|
-
return pattern;
|
|
290
|
+
async function loadTestProject(configPath) {
|
|
291
|
+
if (!configPath) return validateTestProjectDefinition({
|
|
292
|
+
nodes: []
|
|
1050
293
|
});
|
|
1051
|
-
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
294
|
+
const absolutePath = (0, external_node_path_namespaceObject.resolve)(configPath);
|
|
295
|
+
const extension = assertSupportedConfig(absolutePath);
|
|
296
|
+
let loaded;
|
|
297
|
+
try {
|
|
298
|
+
if ('.mjs' === extension) loaded = await import((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href);
|
|
299
|
+
else {
|
|
300
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
301
|
+
loaded = await tsImport((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href, {
|
|
302
|
+
parentURL: (0, external_node_url_namespaceObject.pathToFileURL)(`${(0, external_node_path_namespaceObject.dirname)(absolutePath)}${external_node_path_namespaceObject.sep}`).href,
|
|
303
|
+
tsconfig: false
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
try {
|
|
308
|
+
if ('.mjs' === extension || !canRetryWithCjsLoader(error)) throw error;
|
|
309
|
+
loaded = (0, api_namespaceObject.require)(absolutePath, (0, external_node_url_namespaceObject.pathToFileURL)(absolutePath));
|
|
310
|
+
} catch (fallbackError) {
|
|
311
|
+
const message = fallbackError instanceof Error ? `: ${fallbackError.message}` : '';
|
|
312
|
+
throw new Error(`Failed to load Midscene config "${absolutePath}"${message}`, {
|
|
313
|
+
cause: fallbackError
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
318
|
+
}
|
|
319
|
+
const external_node_fs_namespaceObject = require("node:fs");
|
|
320
|
+
const external_tinyglobby_namespaceObject = require("tinyglobby");
|
|
321
|
+
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
322
|
+
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
323
|
+
projectId,
|
|
324
|
+
sourcePath,
|
|
325
|
+
caseIndex,
|
|
326
|
+
...invocationIndex ? [
|
|
327
|
+
invocationIndex
|
|
328
|
+
] : []
|
|
329
|
+
])).digest('hex');
|
|
330
|
+
const createDocumentInvocationId = (projectId, sourcePath, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
331
|
+
projectId,
|
|
332
|
+
sourcePath,
|
|
333
|
+
...invocationIndex ? [
|
|
334
|
+
invocationIndex
|
|
335
|
+
] : []
|
|
336
|
+
])).digest('hex');
|
|
337
|
+
const normalizeSteps = test_runner_namespaceObject.normalizeSteps;
|
|
338
|
+
const external_js_yaml_namespaceObject = require("js-yaml");
|
|
339
|
+
function parseWorkflowSource(source) {
|
|
340
|
+
const stack = [];
|
|
341
|
+
const nodes = [];
|
|
342
|
+
const sensitiveNodes = new Set();
|
|
343
|
+
const sensitiveValues = new Set();
|
|
344
|
+
const redactedPaths = new Set();
|
|
345
|
+
const sourceOffset = 0xfeff === source.charCodeAt(0) ? 1 : 0;
|
|
346
|
+
const parsed = (0, external_js_yaml_namespaceObject.load)(source, {
|
|
347
|
+
schema: external_js_yaml_namespaceObject.JSON_SCHEMA,
|
|
348
|
+
listener (event, state) {
|
|
349
|
+
if ('open' === event) return void stack.push({
|
|
350
|
+
start: state.position + sourceOffset,
|
|
351
|
+
end: 0,
|
|
352
|
+
value: null,
|
|
353
|
+
children: []
|
|
354
|
+
});
|
|
355
|
+
const node = stack.pop();
|
|
356
|
+
node.end = state.position + sourceOffset;
|
|
357
|
+
node.value = state.result;
|
|
358
|
+
stack.at(-1)?.children.push(node);
|
|
359
|
+
nodes.push(node);
|
|
360
|
+
if ('mapping' !== state.kind) return;
|
|
361
|
+
for(let index = 0; index < node.children.length; index += 2){
|
|
362
|
+
const key = node.children[index].value;
|
|
363
|
+
const value = node.children[index + 1];
|
|
364
|
+
if ('string' == typeof key && (0, test_runner_namespaceObject.isReportSecretKey)(key) && value) {
|
|
365
|
+
sensitiveNodes.add(value);
|
|
366
|
+
if (null !== value.value) sensitiveValues.add(value.value);
|
|
367
|
+
redactedPaths.add(`$..[${JSON.stringify(key)}]`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
1066
371
|
});
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
372
|
+
const ranges = nodes.filter((node)=>sensitiveNodes.has(node) || sensitiveValues.has(node.value)).sort((left, right)=>left.start - right.start || right.end - left.end);
|
|
373
|
+
let offset = 0;
|
|
374
|
+
let redacted = '';
|
|
375
|
+
for (const range of ranges){
|
|
376
|
+
if (range.start < offset) continue;
|
|
377
|
+
const text = source.slice(range.start, range.end);
|
|
378
|
+
const leading = text.match(/^\s*/)?.[0] ?? '';
|
|
379
|
+
const trailing = text.slice(leading.length).match(/\s*$/)?.[0] ?? '';
|
|
380
|
+
redacted += `${source.slice(offset, range.start)}${leading}"[REDACTED]"${trailing}`;
|
|
381
|
+
offset = range.end;
|
|
382
|
+
}
|
|
383
|
+
redacted += source.slice(offset);
|
|
384
|
+
const value = (0, test_runner_namespaceObject.sanitizeReportText)(redacted, 65536);
|
|
385
|
+
return {
|
|
386
|
+
parsed,
|
|
387
|
+
sourceYaml: {
|
|
388
|
+
value,
|
|
389
|
+
...redactedPaths.size ? {
|
|
390
|
+
redactedPaths: [
|
|
391
|
+
...redactedPaths
|
|
392
|
+
]
|
|
393
|
+
} : {},
|
|
394
|
+
...value !== redacted ? {
|
|
395
|
+
truncatedPaths: [
|
|
396
|
+
'$'
|
|
397
|
+
]
|
|
398
|
+
} : {}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const interpolationExpression = /\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}|\$\{([^{}]+)\}/g;
|
|
403
|
+
const exactEnvExpression = /^\$\{\{([A-Za-z_][A-Za-z0-9_]*)\}\}$/;
|
|
404
|
+
const exactProjectExpression = /^\$\{([^{}]+)\}$/;
|
|
405
|
+
const missingVariable = (kind, name, options)=>{
|
|
406
|
+
throw new test_runner_namespaceObject.WorkflowParseError(`Undefined ${kind} variable "${name}" in ${options.location.sourcePath}.`, {
|
|
407
|
+
...options.location,
|
|
408
|
+
variable: name,
|
|
409
|
+
variableKind: kind
|
|
1074
410
|
});
|
|
1075
411
|
};
|
|
1076
|
-
const
|
|
1077
|
-
if (
|
|
1078
|
-
|
|
1079
|
-
exclude: []
|
|
1080
|
-
});
|
|
1081
|
-
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1082
|
-
return Object.freeze({
|
|
1083
|
-
include: Object.freeze(validateTagList(value.include, 'include', label)),
|
|
1084
|
-
exclude: Object.freeze(validateTagList(value.exclude, 'exclude', label))
|
|
1085
|
-
});
|
|
412
|
+
const projectVariable = (name, options)=>{
|
|
413
|
+
if (!Object.hasOwn(options.variables ?? {}, name)) return missingVariable('project', name, options);
|
|
414
|
+
return options.variables[name];
|
|
1086
415
|
};
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (
|
|
1090
|
-
if (Number.isFinite(value)) return;
|
|
1091
|
-
throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1092
|
-
}
|
|
1093
|
-
if ('object' != typeof value || void 0 === value) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1094
|
-
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1095
|
-
if (seen.has(value)) throw new TypeError(`Midscene config ${path} must not contain cycles.`);
|
|
1096
|
-
seen.add(value);
|
|
1097
|
-
if (Array.isArray(value)) value.forEach((item, index)=>assertJsonValue(item, `${path}[${index}]`, seen));
|
|
1098
|
-
else for (const [key, child] of Object.entries(value))assertJsonValue(child, `${path}.${key}`, seen);
|
|
1099
|
-
seen.delete(value);
|
|
1100
|
-
}
|
|
1101
|
-
const deepFreezeJson = (value)=>{
|
|
1102
|
-
if ('object' == typeof value && null !== value) {
|
|
1103
|
-
for (const child of Object.values(value))deepFreezeJson(child);
|
|
1104
|
-
Object.freeze(value);
|
|
1105
|
-
}
|
|
416
|
+
const environmentVariable = (name, options)=>{
|
|
417
|
+
const value = options.env?.[name];
|
|
418
|
+
if (void 0 === value) return missingVariable('environment', name, options);
|
|
1106
419
|
return value;
|
|
1107
420
|
};
|
|
1108
|
-
const
|
|
1109
|
-
if (
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
421
|
+
const interpolationValue = (name, value, options)=>{
|
|
422
|
+
if ('object' == typeof value && null !== value) throw new test_runner_namespaceObject.WorkflowParseError(`Project variable "${name}" must be a primitive when embedded in a string.`, {
|
|
423
|
+
...options.location,
|
|
424
|
+
variable: name,
|
|
425
|
+
variableKind: 'project'
|
|
426
|
+
});
|
|
427
|
+
return String(value);
|
|
1113
428
|
};
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
], label);
|
|
1121
|
-
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
1122
|
-
if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
|
|
1123
|
-
return value;
|
|
429
|
+
const resolveString = (value, options)=>{
|
|
430
|
+
const exactEnv = exactEnvExpression.exec(value);
|
|
431
|
+
if (exactEnv) return environmentVariable(exactEnv[1], options);
|
|
432
|
+
const exactProject = exactProjectExpression.exec(value);
|
|
433
|
+
if (exactProject) return projectVariable(exactProject[1], options);
|
|
434
|
+
return value.replace(interpolationExpression, (_match, environmentName, projectName)=>environmentName ? environmentVariable(environmentName, options) : interpolationValue(projectName, projectVariable(projectName, options), options));
|
|
1124
435
|
};
|
|
1125
|
-
const
|
|
1126
|
-
if (
|
|
1127
|
-
if (
|
|
436
|
+
const resolveWorkflowVariables = (value, options)=>{
|
|
437
|
+
if ('string' == typeof value) return resolveString(value, options);
|
|
438
|
+
if (Array.isArray(value)) return value.map((child)=>resolveWorkflowVariables(child, options));
|
|
439
|
+
if ('object' == typeof value && null !== value) return Object.fromEntries(Object.entries(value).map(([key, child])=>[
|
|
440
|
+
key,
|
|
441
|
+
resolveWorkflowVariables(child, options)
|
|
442
|
+
]));
|
|
1128
443
|
return value;
|
|
1129
444
|
};
|
|
1130
|
-
|
|
1131
|
-
if (
|
|
1132
|
-
|
|
1133
|
-
|
|
445
|
+
function _define_property(obj, key, value) {
|
|
446
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
447
|
+
value: value,
|
|
448
|
+
enumerable: true,
|
|
449
|
+
configurable: true,
|
|
450
|
+
writable: true
|
|
451
|
+
});
|
|
452
|
+
else obj[key] = value;
|
|
453
|
+
return obj;
|
|
454
|
+
}
|
|
455
|
+
const isMapping = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
456
|
+
class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowParseError {
|
|
457
|
+
constructor(actualFormat, sourcePath){
|
|
458
|
+
super('legacy' === actualFormat ? `Legacy tasks/flow YAML is not supported by midscene-test: ${sourcePath}. Run it with \`midscene ${sourcePath}\`, or migrate the file to cases/steps.` : 'legacy-batch' === actualFormat ? `Legacy YAML batch configuration is not a midscene-test workflow document: ${sourcePath}. Run it with \`midscene --config ${sourcePath}\`, or migrate it to a native project config.` : `A workflow file cannot mix legacy tasks/flow with native cases/steps or lifecycle hooks: ${sourcePath}. Keep the formats in separate files and run each file with its matching command.`, {
|
|
459
|
+
sourcePath,
|
|
460
|
+
actualFormat,
|
|
461
|
+
expectedFormat: 'native'
|
|
462
|
+
}), _define_property(this, "actualFormat", void 0);
|
|
463
|
+
this.actualFormat = actualFormat;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const collect_rejectUnknownKeys = (value, allowed, label)=>{
|
|
467
|
+
const allowedSet = new Set(allowed);
|
|
468
|
+
const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
|
|
469
|
+
if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
|
|
470
|
+
field: unknown[0]
|
|
471
|
+
});
|
|
1134
472
|
};
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
setup
|
|
1146
|
-
} : {},
|
|
1147
|
-
tags: Object.freeze({
|
|
1148
|
-
include: [],
|
|
1149
|
-
exclude: []
|
|
1150
|
-
}),
|
|
1151
|
-
retry: 0,
|
|
1152
|
-
variables: Object.freeze({}),
|
|
1153
|
-
nodes: new NodeRegistry(globalNodes.definitions())
|
|
1154
|
-
})
|
|
1155
|
-
])
|
|
1156
|
-
};
|
|
473
|
+
const createCaseId = (projectId, sourcePath, caseIndex)=>createCaseInvocationId(projectId, sourcePath, caseIndex);
|
|
474
|
+
const createWorkflowDocumentId = (projectId, sourcePath)=>createDocumentInvocationId(projectId, sourcePath);
|
|
475
|
+
function collectWorkflowDocument(source, options) {
|
|
476
|
+
let snapshot;
|
|
477
|
+
try {
|
|
478
|
+
snapshot = parseWorkflowSource((0, external_node_fs_namespaceObject.readFileSync)(source.absolutePath, 'utf8'));
|
|
479
|
+
} catch (error) {
|
|
480
|
+
throw new test_runner_namespaceObject.WorkflowParseError(`Failed to parse workflow document "${source.sourcePath}".`, {
|
|
481
|
+
sourcePath: source.sourcePath
|
|
482
|
+
}, error);
|
|
1157
483
|
}
|
|
1158
|
-
|
|
1159
|
-
if (!
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
484
|
+
const { parsed, sourceYaml } = snapshot;
|
|
485
|
+
if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
|
|
486
|
+
const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
|
|
487
|
+
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
488
|
+
collect_rejectUnknownKeys(parsed, [
|
|
489
|
+
'beforeAll',
|
|
490
|
+
'beforeEach',
|
|
491
|
+
'cases',
|
|
492
|
+
'afterEach',
|
|
493
|
+
'afterAll'
|
|
494
|
+
], 'Workflow document');
|
|
495
|
+
if (!Array.isArray(parsed.cases) || 0 === parsed.cases.length) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must contain a non-empty cases array.');
|
|
496
|
+
const sourcePath = source.sourcePath.split(external_node_path_namespaceObject.sep).join('/');
|
|
497
|
+
const resolveStepVariables = (step, phase, stepIndex, caseIndex)=>({
|
|
498
|
+
...step,
|
|
499
|
+
templateInput: step.input,
|
|
500
|
+
input: resolveWorkflowVariables(step.input, {
|
|
501
|
+
variables: options.variables,
|
|
502
|
+
env: options.env ?? process.env,
|
|
503
|
+
location: {
|
|
504
|
+
projectName: source.projectName ?? source.projectId,
|
|
505
|
+
sourcePath,
|
|
506
|
+
phase,
|
|
507
|
+
stepIndex,
|
|
508
|
+
...void 0 === caseIndex ? {} : {
|
|
509
|
+
caseIndex
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
const normalizeLifecycle = (phase)=>{
|
|
515
|
+
const value = parsed[phase];
|
|
516
|
+
if (void 0 === value) return [];
|
|
517
|
+
if (!Array.isArray(value)) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} must be an array.`, {
|
|
518
|
+
phase
|
|
519
|
+
});
|
|
520
|
+
return normalizeSteps(value, options.resolveNode).map((normalized, stepIndex)=>{
|
|
521
|
+
const resolved = options.resolveNode(normalized.node);
|
|
522
|
+
if (!resolved) throw new test_runner_namespaceObject.WorkflowParseError(`Workflow document ${phase} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
523
|
+
phase,
|
|
524
|
+
stepIndex,
|
|
525
|
+
node: normalized.node
|
|
526
|
+
});
|
|
527
|
+
return resolveStepVariables(normalized, phase, stepIndex);
|
|
528
|
+
});
|
|
529
|
+
};
|
|
530
|
+
const lifecycle = {
|
|
531
|
+
beforeAll: normalizeLifecycle('beforeAll'),
|
|
532
|
+
beforeEach: normalizeLifecycle('beforeEach'),
|
|
533
|
+
afterEach: normalizeLifecycle('afterEach'),
|
|
534
|
+
afterAll: normalizeLifecycle('afterAll')
|
|
535
|
+
};
|
|
536
|
+
const ids = new Set();
|
|
537
|
+
const cases = parsed.cases.map((definition, caseIndex)=>{
|
|
538
|
+
if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
|
|
539
|
+
caseIndex
|
|
540
|
+
});
|
|
541
|
+
collect_rejectUnknownKeys(definition, [
|
|
1165
542
|
'name',
|
|
1166
|
-
'setup',
|
|
1167
|
-
'nodes',
|
|
1168
|
-
'files',
|
|
1169
543
|
'tags',
|
|
1170
|
-
'
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
...void 0 === candidate.setup ? {} : {
|
|
1194
|
-
setup: validateSetup(candidate.setup, `${label}.setup`)
|
|
1195
|
-
}
|
|
544
|
+
'steps'
|
|
545
|
+
], `Case ${caseIndex + 1}`);
|
|
546
|
+
if ('string' != typeof definition.name || 0 === definition.name.trim().length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} name must be a non-empty string.`, {
|
|
547
|
+
caseIndex
|
|
548
|
+
});
|
|
549
|
+
if (!Array.isArray(definition.steps) || 0 === definition.steps.length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} steps must be a non-empty array.`, {
|
|
550
|
+
caseIndex
|
|
551
|
+
});
|
|
552
|
+
const tags = definition.tags ?? [];
|
|
553
|
+
if (!Array.isArray(tags) || tags.some((tag)=>'string' != typeof tag || 0 === tag.trim().length)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} tags must be an array of non-empty strings.`, {
|
|
554
|
+
caseIndex
|
|
555
|
+
});
|
|
556
|
+
const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
557
|
+
if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
558
|
+
caseIndex,
|
|
559
|
+
stepIndex,
|
|
560
|
+
node: normalized.node
|
|
561
|
+
});
|
|
562
|
+
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
563
|
+
});
|
|
564
|
+
const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
|
|
565
|
+
if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
566
|
+
caseId
|
|
1196
567
|
});
|
|
568
|
+
ids.add(caseId);
|
|
569
|
+
return {
|
|
570
|
+
caseId,
|
|
571
|
+
projectId: source.projectId,
|
|
572
|
+
sourcePath,
|
|
573
|
+
caseIndex,
|
|
574
|
+
definition: {
|
|
575
|
+
name: definition.name,
|
|
576
|
+
tags: tags,
|
|
577
|
+
steps
|
|
578
|
+
}
|
|
579
|
+
};
|
|
1197
580
|
});
|
|
1198
581
|
return {
|
|
1199
|
-
|
|
1200
|
-
|
|
582
|
+
documentId: createWorkflowDocumentId(source.projectId, sourcePath),
|
|
583
|
+
projectId: source.projectId,
|
|
584
|
+
sourcePath,
|
|
585
|
+
sourceYaml,
|
|
586
|
+
lifecycle,
|
|
587
|
+
cases
|
|
1201
588
|
};
|
|
589
|
+
}
|
|
590
|
+
const promises_namespaceObject = require("node:fs/promises");
|
|
591
|
+
const writeJson = async (path, value)=>{
|
|
592
|
+
try {
|
|
593
|
+
await (0, promises_namespaceObject.mkdir)((0, external_node_path_namespaceObject.dirname)(path), {
|
|
594
|
+
recursive: true
|
|
595
|
+
});
|
|
596
|
+
await (0, promises_namespaceObject.writeFile)(path, JSON.stringify(value, null, 2));
|
|
597
|
+
} catch (error) {
|
|
598
|
+
throw new test_runner_namespaceObject.WorkflowPublicationError('write-result', path, error);
|
|
599
|
+
}
|
|
1202
600
|
};
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout'),
|
|
1212
|
-
executionUnit,
|
|
1213
|
-
executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
|
|
601
|
+
const writeTestRunPlan = (runDir, projects)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, 'plan.json'), {
|
|
602
|
+
schemaVersion: 1,
|
|
603
|
+
createdAt: new Date().toISOString(),
|
|
604
|
+
projects: projects.map((item)=>({
|
|
605
|
+
projectId: item.project.projectId,
|
|
606
|
+
name: item.project.name,
|
|
607
|
+
documents: item.plans ?? []
|
|
608
|
+
}))
|
|
1214
609
|
});
|
|
610
|
+
const toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
|
|
611
|
+
const caseAttemptResultPath = (projectId, documentId, result)=>toPosix((0, external_node_path_namespaceObject.join)(projectId, 'documents', documentId, 'cases', result.caseId, `${result.runId}.json`));
|
|
612
|
+
const workflowDocumentResultPath = (result)=>toPosix((0, external_node_path_namespaceObject.join)(result.projectId, 'documents', result.documentId, void 0 === result.attemptIndex ? 'document.json' : `${result.documentRunId}.json`));
|
|
613
|
+
const collectionErrorPath = (projectId, sourcePath)=>{
|
|
614
|
+
const id = (0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
615
|
+
projectId,
|
|
616
|
+
sourcePath
|
|
617
|
+
])).digest('hex');
|
|
618
|
+
return toPosix((0, external_node_path_namespaceObject.join)(projectId, 'collection-errors', `${id}.json`));
|
|
1215
619
|
};
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
test_project_rejectUnknownKeys(value, [
|
|
1220
|
-
'name',
|
|
1221
|
-
'execute'
|
|
1222
|
-
], 'executor');
|
|
1223
|
-
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError('Midscene config executor.name must be non-empty.');
|
|
1224
|
-
if ('function' != typeof value.execute) throw new TypeError('Midscene config executor.execute must be a function.');
|
|
1225
|
-
return Object.freeze(value);
|
|
620
|
+
const writeWorkflowDocumentResult = (runDir, result)=>{
|
|
621
|
+
const { documentRunId: _documentRunId, ...persistedResult } = result;
|
|
622
|
+
return writeJson((0, external_node_path_namespaceObject.join)(runDir, workflowDocumentResultPath(result)), persistedResult);
|
|
1226
623
|
};
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
624
|
+
const writeCaseAttemptResult = (runDir, projectId, documentId, result)=>{
|
|
625
|
+
const { runId: attemptId, ...persistedResult } = result;
|
|
626
|
+
return writeJson((0, external_node_path_namespaceObject.join)(runDir, caseAttemptResultPath(projectId, documentId, result)), {
|
|
627
|
+
...persistedResult,
|
|
628
|
+
attemptId
|
|
629
|
+
});
|
|
1231
630
|
};
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
test_project_rejectUnknownKeys(candidate, [
|
|
1236
|
-
'reportDir'
|
|
1237
|
-
], 'output');
|
|
1238
|
-
return Object.freeze({
|
|
1239
|
-
reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
|
|
631
|
+
const writeCollectionError = (runDir, collectionError)=>writeJson((0, external_node_path_namespaceObject.join)(runDir, collectionErrorPath(collectionError.projectId, collectionError.sourcePath)), {
|
|
632
|
+
kind: 'collection-error',
|
|
633
|
+
...collectionError
|
|
1240
634
|
});
|
|
635
|
+
const relativeToSummary = (summaryPath, target)=>{
|
|
636
|
+
const value = (0, external_node_path_namespaceObject.relative)((0, external_node_path_namespaceObject.dirname)(summaryPath), target);
|
|
637
|
+
return toPosix(value || '.');
|
|
1241
638
|
};
|
|
1242
|
-
const
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
'executor'
|
|
1257
|
-
], 'root');
|
|
1258
|
-
const nodes = new NodeRegistry(definition.nodes);
|
|
1259
|
-
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
|
|
1260
|
-
const test = validateTestOptions(definition.test);
|
|
1261
|
-
const executor = validateTestExecutor(definition.executor);
|
|
1262
|
-
if (executor && 'case' !== test.executionUnit) throw new TypeError('Midscene config executor requires test.executionUnit to be "case".');
|
|
1263
|
-
return {
|
|
1264
|
-
...resolvedProjects,
|
|
1265
|
-
test,
|
|
1266
|
-
output: validateOutput(definition.output),
|
|
1267
|
-
nodes,
|
|
1268
|
-
...executor ? {
|
|
1269
|
-
executor
|
|
639
|
+
const errorJson = test_runner_namespaceObject.sanitizeReportError;
|
|
640
|
+
const writeTestProjectRunResult = (options)=>{
|
|
641
|
+
const { result } = options;
|
|
642
|
+
const fact = (path)=>toPosix(path);
|
|
643
|
+
return writeJson(result.summaryPath, {
|
|
644
|
+
schemaVersion: result.schemaVersion,
|
|
645
|
+
runId: result.runId,
|
|
646
|
+
startedAt: result.startedAt,
|
|
647
|
+
endedAt: result.endedAt,
|
|
648
|
+
durationMs: result.durationMs,
|
|
649
|
+
status: result.status,
|
|
650
|
+
exitCode: result.exitCode,
|
|
651
|
+
...result.errors?.length ? {
|
|
652
|
+
errors: result.errors.map(errorJson)
|
|
1270
653
|
} : {},
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
654
|
+
projectRoot: options.projectRoot,
|
|
655
|
+
...options.configPath ? {
|
|
656
|
+
configPath: options.configPath
|
|
657
|
+
} : {},
|
|
658
|
+
factsRoot: '.',
|
|
659
|
+
plan: 'plan.json',
|
|
660
|
+
...result.operationLog ? {
|
|
661
|
+
operationLog: {
|
|
662
|
+
...result.operationLog,
|
|
663
|
+
path: relativeToSummary(result.summaryPath, result.operationLog.path)
|
|
664
|
+
}
|
|
665
|
+
} : {},
|
|
666
|
+
reportDir: relativeToSummary(result.summaryPath, result.reportDir),
|
|
667
|
+
...result.reportPath ? {
|
|
668
|
+
report: relativeToSummary(result.summaryPath, result.reportPath)
|
|
669
|
+
} : {},
|
|
670
|
+
...result.markdownPath ? {
|
|
671
|
+
markdown: relativeToSummary(result.summaryPath, result.markdownPath)
|
|
672
|
+
} : {},
|
|
673
|
+
summary: result.summary,
|
|
674
|
+
projects: result.projects.map((project)=>({
|
|
675
|
+
projectId: project.projectId,
|
|
676
|
+
name: project.name,
|
|
677
|
+
status: project.status,
|
|
678
|
+
retry: project.retry,
|
|
679
|
+
fileSelection: project.fileSelection,
|
|
680
|
+
tagSelection: project.tagSelection,
|
|
681
|
+
sourceCount: project.sourceCount,
|
|
682
|
+
selectedCaseCount: project.selectedCaseCount,
|
|
683
|
+
filteredCaseCount: project.filteredCaseCount,
|
|
684
|
+
...project.lifecycle ? {
|
|
685
|
+
lifecycle: {
|
|
686
|
+
status: project.lifecycle.status,
|
|
687
|
+
startedAt: project.lifecycle.startedAt,
|
|
688
|
+
endedAt: project.lifecycle.endedAt,
|
|
689
|
+
durationMs: project.lifecycle.durationMs,
|
|
690
|
+
...project.lifecycle.cleanup ? {
|
|
691
|
+
cleanup: project.lifecycle.cleanup
|
|
692
|
+
} : {},
|
|
693
|
+
...project.lifecycle.pendingOperations ? {
|
|
694
|
+
pendingOperations: project.lifecycle.pendingOperations
|
|
695
|
+
} : {},
|
|
696
|
+
...project.lifecycle.setupError ? {
|
|
697
|
+
setupError: errorJson(project.lifecycle.setupError)
|
|
698
|
+
} : {},
|
|
699
|
+
...project.lifecycle.teardownErrors ? {
|
|
700
|
+
teardownErrors: project.lifecycle.teardownErrors.map(errorJson)
|
|
701
|
+
} : {}
|
|
702
|
+
}
|
|
703
|
+
} : {},
|
|
704
|
+
cases: project.cases.map((outcome)=>({
|
|
705
|
+
...outcome.documentRunId ? {
|
|
706
|
+
documentRunId: outcome.documentRunId
|
|
707
|
+
} : {},
|
|
708
|
+
documentId: outcome.documentId,
|
|
709
|
+
caseId: outcome.caseId,
|
|
710
|
+
sourcePath: outcome.sourcePath,
|
|
711
|
+
caseIndex: outcome.caseIndex,
|
|
712
|
+
name: outcome.name,
|
|
713
|
+
status: outcome.status,
|
|
714
|
+
...outcome.notRunReason ? {
|
|
715
|
+
notRunReason: outcome.notRunReason
|
|
716
|
+
} : {},
|
|
717
|
+
...outcome.attempts ? {
|
|
718
|
+
attempts: outcome.attempts.map((attempt)=>({
|
|
719
|
+
attemptId: attempt.runId,
|
|
720
|
+
attemptIndex: attempt.attemptIndex,
|
|
721
|
+
status: attempt.status,
|
|
722
|
+
...attempt.cleanup ? {
|
|
723
|
+
cleanup: attempt.cleanup
|
|
724
|
+
} : {},
|
|
725
|
+
...attempt.pendingOperations ? {
|
|
726
|
+
pendingOperations: attempt.pendingOperations
|
|
727
|
+
} : {},
|
|
728
|
+
...attempt.executionStatus ? {
|
|
729
|
+
executionStatus: attempt.executionStatus
|
|
730
|
+
} : {},
|
|
731
|
+
...attempt.retryDecision ? {
|
|
732
|
+
retryDecision: attempt.retryDecision
|
|
733
|
+
} : {},
|
|
734
|
+
resultFile: fact(caseAttemptResultPath(project.projectId, outcome.documentId, attempt)),
|
|
735
|
+
...attempt.reportPaths?.length ? {
|
|
736
|
+
reports: attempt.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
737
|
+
} : {}
|
|
738
|
+
}))
|
|
739
|
+
} : {}
|
|
740
|
+
})),
|
|
741
|
+
documents: project.documents.map((document)=>({
|
|
742
|
+
documentId: document.documentId,
|
|
743
|
+
...void 0 === document.attemptIndex ? {} : {
|
|
744
|
+
attemptIndex: document.attemptIndex,
|
|
745
|
+
documentRunId: document.documentRunId
|
|
746
|
+
},
|
|
747
|
+
sourcePath: document.sourcePath,
|
|
748
|
+
status: document.status,
|
|
749
|
+
...document.cleanup ? {
|
|
750
|
+
cleanup: document.cleanup
|
|
751
|
+
} : {},
|
|
752
|
+
...document.pendingOperations ? {
|
|
753
|
+
pendingOperations: document.pendingOperations
|
|
754
|
+
} : {},
|
|
755
|
+
...document.executionStatus ? {
|
|
756
|
+
executionStatus: document.executionStatus
|
|
757
|
+
} : {},
|
|
758
|
+
...document.retryDecision ? {
|
|
759
|
+
retryDecision: document.retryDecision
|
|
760
|
+
} : {},
|
|
761
|
+
resultFile: fact(workflowDocumentResultPath(document)),
|
|
762
|
+
...document.reportPaths?.length ? {
|
|
763
|
+
reports: document.reportPaths.map((path)=>relativeToSummary(result.summaryPath, path))
|
|
764
|
+
} : {}
|
|
765
|
+
})),
|
|
766
|
+
collectionErrors: project.collectionErrors.map((error)=>({
|
|
767
|
+
sourcePath: error.sourcePath,
|
|
768
|
+
errorFile: fact(collectionErrorPath(error.projectId, error.sourcePath))
|
|
769
|
+
}))
|
|
770
|
+
}))
|
|
1289
771
|
});
|
|
1290
|
-
|
|
1291
|
-
const extension = assertSupportedConfig(absolutePath);
|
|
1292
|
-
let loaded;
|
|
1293
|
-
try {
|
|
1294
|
-
if ('.mjs' === extension) loaded = await import((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href);
|
|
1295
|
-
else {
|
|
1296
|
-
const { tsImport } = await import("tsx/esm/api");
|
|
1297
|
-
loaded = await tsImport((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href, {
|
|
1298
|
-
parentURL: (0, external_node_url_namespaceObject.pathToFileURL)(`${(0, external_node_path_namespaceObject.dirname)(absolutePath)}${external_node_path_namespaceObject.sep}`).href,
|
|
1299
|
-
tsconfig: false
|
|
1300
|
-
});
|
|
1301
|
-
}
|
|
1302
|
-
} catch (error) {
|
|
1303
|
-
try {
|
|
1304
|
-
if ('.mjs' === extension || !canRetryWithCjsLoader(error)) throw error;
|
|
1305
|
-
loaded = (0, api_namespaceObject.require)(absolutePath, (0, external_node_url_namespaceObject.pathToFileURL)(absolutePath));
|
|
1306
|
-
} catch (fallbackError) {
|
|
1307
|
-
const message = fallbackError instanceof Error ? `: ${fallbackError.message}` : '';
|
|
1308
|
-
throw new Error(`Failed to load Midscene config "${absolutePath}"${message}`, {
|
|
1309
|
-
cause: fallbackError
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
1314
|
-
}
|
|
772
|
+
};
|
|
1315
773
|
const CONFIG_NAMES = [
|
|
1316
774
|
'midscene.config.ts',
|
|
1317
775
|
'midscene.config.mjs'
|
|
@@ -1333,27 +791,6 @@ const DEFAULT_TEST_FILE_SELECTION = {
|
|
|
1333
791
|
]
|
|
1334
792
|
};
|
|
1335
793
|
const project_preparation_toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
|
|
1336
|
-
const discoverSelectedTestFiles = (projectRoot, paths)=>{
|
|
1337
|
-
const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
|
|
1338
|
-
const files = new Set();
|
|
1339
|
-
for (const requestedPath of paths){
|
|
1340
|
-
if ('string' != typeof requestedPath || 0 === requestedPath.trim().length) throw new TypeError('Test selection paths must be non-empty strings.');
|
|
1341
|
-
const absolutePath = (0, external_node_path_namespaceObject.resolve)(root, requestedPath);
|
|
1342
|
-
const relativePath = (0, external_node_path_namespaceObject.relative)(root, absolutePath);
|
|
1343
|
-
if ('..' === relativePath || relativePath.startsWith(`..${external_node_path_namespaceObject.sep}`) || (0, external_node_path_namespaceObject.isAbsolute)(relativePath)) throw new Error(`Test selection path is outside the project: ${requestedPath}`);
|
|
1344
|
-
if (!(0, external_node_fs_namespaceObject.existsSync)(absolutePath)) throw new Error(`Test selection path does not exist: ${requestedPath}`);
|
|
1345
|
-
if ((0, external_node_fs_namespaceObject.statSync)(absolutePath).isDirectory()) for (const file of discoverTestFiles(absolutePath))files.add(file);
|
|
1346
|
-
else if (/\.ya?ml$/i.test(absolutePath)) files.add(absolutePath);
|
|
1347
|
-
else throw new Error(`Test selection file must be YAML: ${requestedPath}`);
|
|
1348
|
-
}
|
|
1349
|
-
return [
|
|
1350
|
-
...files
|
|
1351
|
-
].sort((a, b)=>{
|
|
1352
|
-
const relativeA = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, a));
|
|
1353
|
-
const relativeB = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, b));
|
|
1354
|
-
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
1355
|
-
});
|
|
1356
|
-
};
|
|
1357
794
|
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
1358
795
|
const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
|
|
1359
796
|
const match = (patterns)=>(0, external_tinyglobby_namespaceObject.globSync)(patterns, {
|
|
@@ -1413,14 +850,8 @@ const matchesTags = (tags, selection)=>{
|
|
|
1413
850
|
if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
|
|
1414
851
|
return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
|
|
1415
852
|
};
|
|
1416
|
-
const filterDocumentCases = (document, project
|
|
1417
|
-
const cases = document.cases.filter((item)=>
|
|
1418
|
-
const tags = item.definition.tags ?? [];
|
|
1419
|
-
return matchesTags(tags, project.tags) && matchesTags(tags, {
|
|
1420
|
-
include: selection?.tags?.include ?? [],
|
|
1421
|
-
exclude: selection?.tags?.exclude ?? []
|
|
1422
|
-
}) && (!selection?.caseIds?.length || selection.caseIds.includes(item.caseId));
|
|
1423
|
-
});
|
|
853
|
+
const filterDocumentCases = (document, project)=>{
|
|
854
|
+
const cases = document.cases.filter((item)=>matchesTags(item.definition.tags ?? [], project.tags));
|
|
1424
855
|
const filtered = document.cases.length - cases.length;
|
|
1425
856
|
return 0 === cases.length ? {
|
|
1426
857
|
filtered
|
|
@@ -1445,9 +876,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1445
876
|
const platform = options.platform?.trim();
|
|
1446
877
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
1447
878
|
const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_namespaceObject.resolve)(options.prerequisiteFile) : void 0;
|
|
1448
|
-
const
|
|
1449
|
-
const selectedFiles = options.selection?.paths?.length ? new Set(discoverSelectedTestFiles(projectRoot, options.selection.paths)) : void 0;
|
|
1450
|
-
const mainFiles = selectedFiles ? projectFiles.filter((file)=>selectedFiles.has(file)) : projectFiles;
|
|
879
|
+
const mainFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
1451
880
|
const files = [
|
|
1452
881
|
...prerequisiteFile ? [
|
|
1453
882
|
prerequisiteFile
|
|
@@ -1470,7 +899,6 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1470
899
|
const invocations = [];
|
|
1471
900
|
const plans = [];
|
|
1472
901
|
let filteredCaseCount = 0;
|
|
1473
|
-
const availableCaseIds = new Set();
|
|
1474
902
|
if (0 === sources.length) {
|
|
1475
903
|
const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
1476
904
|
projectName: project.name,
|
|
@@ -1501,16 +929,10 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1501
929
|
variables: project.variables,
|
|
1502
930
|
env: process.env
|
|
1503
931
|
});
|
|
1504
|
-
const duplicate = document.cases.find((item)=>availableCaseIds.has(item.caseId));
|
|
1505
|
-
if (duplicate) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision in project "${project.name}": ${duplicate.caseId}.`, {
|
|
1506
|
-
caseId: duplicate.caseId,
|
|
1507
|
-
projectName: project.name
|
|
1508
|
-
});
|
|
1509
|
-
for (const item of document.cases)availableCaseIds.add(item.caseId);
|
|
1510
932
|
const filtered = source.absolutePath === prerequisiteFile ? {
|
|
1511
933
|
document,
|
|
1512
934
|
filtered: 0
|
|
1513
|
-
} : filterDocumentCases(document, project
|
|
935
|
+
} : filterDocumentCases(document, project);
|
|
1514
936
|
const plan = (0, test_runner_namespaceObject.buildTestRunReportPlan)(document, {
|
|
1515
937
|
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
1516
938
|
defaultTimeoutMs,
|
|
@@ -1558,42 +980,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
1558
980
|
plans,
|
|
1559
981
|
collectionErrors,
|
|
1560
982
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
1561
|
-
filteredCaseCount
|
|
1562
|
-
availableCaseIds: [
|
|
1563
|
-
...availableCaseIds
|
|
1564
|
-
]
|
|
983
|
+
filteredCaseCount
|
|
1565
984
|
};
|
|
1566
985
|
}
|
|
1567
|
-
const validateUniqueStrings = (values, label)=>{
|
|
1568
|
-
if (void 0 === values) return;
|
|
1569
|
-
if (!Array.isArray(values) || values.some((value)=>'string' != typeof value || 0 === value.trim().length)) throw new TypeError(`${label} must be an array of non-empty strings.`);
|
|
1570
|
-
if (new Set(values).size !== values.length) throw new TypeError(`${label} must not contain duplicates.`);
|
|
1571
|
-
return values;
|
|
1572
|
-
};
|
|
1573
|
-
const validateRunSelection = (options)=>{
|
|
1574
|
-
const paths = validateUniqueStrings(options.paths, 'Test selection paths');
|
|
1575
|
-
const caseIds = validateUniqueStrings(options.caseIds, 'Test selection caseIds');
|
|
1576
|
-
const include = validateUniqueStrings(options.tags?.include, 'Test selection tags.include');
|
|
1577
|
-
const exclude = validateUniqueStrings(options.tags?.exclude, 'Test selection tags.exclude');
|
|
1578
|
-
return {
|
|
1579
|
-
...paths ? {
|
|
1580
|
-
paths
|
|
1581
|
-
} : {},
|
|
1582
|
-
...caseIds ? {
|
|
1583
|
-
caseIds
|
|
1584
|
-
} : {},
|
|
1585
|
-
...include || exclude ? {
|
|
1586
|
-
tags: {
|
|
1587
|
-
...include ? {
|
|
1588
|
-
include
|
|
1589
|
-
} : {},
|
|
1590
|
-
...exclude ? {
|
|
1591
|
-
exclude
|
|
1592
|
-
} : {}
|
|
1593
|
-
}
|
|
1594
|
-
} : {}
|
|
1595
|
-
};
|
|
1596
|
-
};
|
|
1597
986
|
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
1598
987
|
const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
|
|
1599
988
|
const timestamp = [
|
|
@@ -1631,7 +1020,6 @@ async function prepareTestRun(options) {
|
|
|
1631
1020
|
const startedAt = new Date();
|
|
1632
1021
|
const runId = createTestRunId(startedAt);
|
|
1633
1022
|
const input = resolveRunInput(options);
|
|
1634
|
-
const selection = validateRunSelection(options);
|
|
1635
1023
|
const configPath = options.configPath ? (0, external_node_path_namespaceObject.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
1636
1024
|
if (options.configPath && /\.ya?ml$/i.test(configPath)) throw new Error(`midscene-test --config only accepts a native TypeScript or JavaScript project config: ${configPath}. Run legacy batch YAML with \`midscene --config ${options.configPath}\`.`);
|
|
1637
1025
|
if (options.configPath && !(0, external_node_fs_namespaceObject.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
@@ -1645,20 +1033,11 @@ async function prepareTestRun(options) {
|
|
|
1645
1033
|
});
|
|
1646
1034
|
(0, external_node_fs_namespaceObject.mkdirSync)(runDir);
|
|
1647
1035
|
const projects = [];
|
|
1648
|
-
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
]
|
|
1654
|
-
} : {}
|
|
1655
|
-
}));
|
|
1656
|
-
if (selection.caseIds?.length) {
|
|
1657
|
-
const availableCaseIds = new Set(projects.flatMap((project)=>project.availableCaseIds ?? []));
|
|
1658
|
-
const unknown = selection.caseIds.find((id)=>!availableCaseIds.has(id));
|
|
1659
|
-
if (unknown) throw new Error(`Unknown Midscene case id: ${unknown}`);
|
|
1660
|
-
}
|
|
1661
|
-
if ((selection.paths?.length || selection.caseIds?.length || selection.tags?.include?.length || selection.tags?.exclude?.length) && projects.every((project)=>0 === project.selectedCaseCount && 0 === project.collectionErrors.length)) throw new Error('No Midscene cases matched the requested selection.');
|
|
1036
|
+
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, input.singleFile ? {
|
|
1037
|
+
files: [
|
|
1038
|
+
input.singleFile
|
|
1039
|
+
]
|
|
1040
|
+
} : void 0));
|
|
1662
1041
|
const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
|
|
1663
1042
|
return {
|
|
1664
1043
|
startedAt,
|
|
@@ -1757,601 +1136,20 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1757
1136
|
}));
|
|
1758
1137
|
return output;
|
|
1759
1138
|
}
|
|
1760
|
-
const
|
|
1761
|
-
documentId,
|
|
1762
|
-
caseId: collectedCase.caseId,
|
|
1763
|
-
projectName,
|
|
1764
|
-
name: collectedCase.definition.name,
|
|
1765
|
-
sourcePath: collectedCase.sourcePath,
|
|
1766
|
-
caseIndex: collectedCase.caseIndex,
|
|
1767
|
-
status: 'not-run',
|
|
1768
|
-
notRunReason: reason
|
|
1769
|
-
});
|
|
1139
|
+
const stepPosition = (info)=>'case' === info.scope ? info.case : info.document;
|
|
1770
1140
|
const formatStep = (info)=>{
|
|
1771
|
-
const position =
|
|
1141
|
+
const position = stepPosition(info);
|
|
1772
1142
|
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
1773
1143
|
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
1774
1144
|
};
|
|
1775
|
-
const formatStepResult = (info, result
|
|
1145
|
+
const formatStepResult = (info, result)=>{
|
|
1146
|
+
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1776
1147
|
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1777
1148
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1778
1149
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1779
1150
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
1780
1151
|
return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
1781
1152
|
};
|
|
1782
|
-
const latestById = (items, id)=>[
|
|
1783
|
-
...new Map(items.map((item)=>[
|
|
1784
|
-
id(item),
|
|
1785
|
-
item
|
|
1786
|
-
])).values()
|
|
1787
|
-
];
|
|
1788
|
-
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
1789
|
-
const { project } = prepared;
|
|
1790
|
-
const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status || item.execution?.lifecycle?.status === 'failed') || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
1791
|
-
return {
|
|
1792
|
-
projectId: project.projectId,
|
|
1793
|
-
name: project.name,
|
|
1794
|
-
...prepared.platform ? {
|
|
1795
|
-
platform: prepared.platform
|
|
1796
|
-
} : {},
|
|
1797
|
-
status: projectFailed ? 'failed' : 'success',
|
|
1798
|
-
retry: project.retry,
|
|
1799
|
-
fileSelection: prepared.fileSelection,
|
|
1800
|
-
tagSelection: project.tags,
|
|
1801
|
-
sourceCount: prepared.sources.length,
|
|
1802
|
-
selectedCaseCount: prepared.selectedCaseCount,
|
|
1803
|
-
filteredCaseCount: prepared.filteredCaseCount,
|
|
1804
|
-
...lifecycle ? {
|
|
1805
|
-
lifecycle
|
|
1806
|
-
} : {},
|
|
1807
|
-
cases,
|
|
1808
|
-
plans: prepared.plans,
|
|
1809
|
-
documents,
|
|
1810
|
-
collectionErrors: prepared.collectionErrors
|
|
1811
|
-
};
|
|
1812
|
-
};
|
|
1813
|
-
function createExecutorReports(outputDir) {
|
|
1814
|
-
const paths = new Map();
|
|
1815
|
-
const sources = new Map();
|
|
1816
|
-
return {
|
|
1817
|
-
materializeSource (source) {
|
|
1818
|
-
if (!source.dumpPath) {
|
|
1819
|
-
const reference = this.materialize(source.sourcePath);
|
|
1820
|
-
sources.set(reference, {
|
|
1821
|
-
sourcePath: this.resolve(reference),
|
|
1822
|
-
collection: source.collection
|
|
1823
|
-
});
|
|
1824
|
-
return reference;
|
|
1825
|
-
}
|
|
1826
|
-
const dump = (0, external_node_fs_namespaceObject.realpathSync)(source.dumpPath);
|
|
1827
|
-
const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1828
|
-
const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
|
|
1829
|
-
(0, external_node_fs_namespaceObject.mkdirSync)(destination, {
|
|
1830
|
-
recursive: true
|
|
1831
|
-
});
|
|
1832
|
-
const dumpPath = (0, external_node_path_namespaceObject.join)(destination, 'snapshot.json');
|
|
1833
|
-
(0, external_node_fs_namespaceObject.cpSync)(dump, dumpPath);
|
|
1834
|
-
const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(dump), 'screenshots');
|
|
1835
|
-
if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
|
|
1836
|
-
recursive: true,
|
|
1837
|
-
dereference: true
|
|
1838
|
-
});
|
|
1839
|
-
const sourcePath = (0, external_node_path_namespaceObject.join)(destination, 'index.html');
|
|
1840
|
-
sources.set(reference, {
|
|
1841
|
-
sourcePath,
|
|
1842
|
-
dumpPath,
|
|
1843
|
-
collection: source.collection
|
|
1844
|
-
});
|
|
1845
|
-
paths.set(reference, sourcePath);
|
|
1846
|
-
return reference;
|
|
1847
|
-
},
|
|
1848
|
-
resolveSource (reference) {
|
|
1849
|
-
return sources.get(reference);
|
|
1850
|
-
},
|
|
1851
|
-
materialize (sourcePath) {
|
|
1852
|
-
const source = (0, external_node_fs_namespaceObject.realpathSync)(sourcePath);
|
|
1853
|
-
const entry = (0, external_node_fs_namespaceObject.statSync)(source).isDirectory() ? (0, external_node_path_namespaceObject.join)(source, 'index.html') : source;
|
|
1854
|
-
if (!(0, external_node_fs_namespaceObject.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
|
|
1855
|
-
kind: 'report'
|
|
1856
|
-
});
|
|
1857
|
-
const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1858
|
-
const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
|
|
1859
|
-
(0, external_node_fs_namespaceObject.mkdirSync)(destination, {
|
|
1860
|
-
recursive: true
|
|
1861
|
-
});
|
|
1862
|
-
const isDirectoryReport = 'index.html' === (0, external_node_path_namespaceObject.basename)(entry);
|
|
1863
|
-
const reportPath = (0, external_node_path_namespaceObject.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
|
|
1864
|
-
(0, external_node_fs_namespaceObject.cpSync)(isDirectoryReport ? (0, external_node_path_namespaceObject.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
|
|
1865
|
-
recursive: isDirectoryReport,
|
|
1866
|
-
dereference: true
|
|
1867
|
-
});
|
|
1868
|
-
if (!isDirectoryReport) {
|
|
1869
|
-
const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(entry), 'screenshots');
|
|
1870
|
-
if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
|
|
1871
|
-
recursive: true,
|
|
1872
|
-
dereference: true
|
|
1873
|
-
});
|
|
1874
|
-
}
|
|
1875
|
-
paths.set(reference, reportPath);
|
|
1876
|
-
return reference;
|
|
1877
|
-
},
|
|
1878
|
-
resolve (reference) {
|
|
1879
|
-
const reportPath = paths.get(reference);
|
|
1880
|
-
if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
|
|
1881
|
-
kind: 'report'
|
|
1882
|
-
});
|
|
1883
|
-
return reportPath;
|
|
1884
|
-
}
|
|
1885
|
-
};
|
|
1886
|
-
}
|
|
1887
|
-
async function runTaskPool(options) {
|
|
1888
|
-
if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
|
|
1889
|
-
const pending = options.tasks.map((_, index)=>index);
|
|
1890
|
-
const results = new Array(options.tasks.length);
|
|
1891
|
-
const activeResources = new Set();
|
|
1892
|
-
let active = 0;
|
|
1893
|
-
let firstError;
|
|
1894
|
-
let hasError = false;
|
|
1895
|
-
return await new Promise((resolve, reject)=>{
|
|
1896
|
-
let settled = false;
|
|
1897
|
-
const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
|
|
1898
|
-
const finish = ()=>{
|
|
1899
|
-
if (settled) return;
|
|
1900
|
-
settled = true;
|
|
1901
|
-
options.signal?.removeEventListener('abort', pump);
|
|
1902
|
-
if (hasError) reject(firstError);
|
|
1903
|
-
else resolve(results);
|
|
1904
|
-
};
|
|
1905
|
-
const release = (resources)=>{
|
|
1906
|
-
for (const resource of resources)activeResources.delete(resource);
|
|
1907
|
-
};
|
|
1908
|
-
const claimRunnable = ()=>{
|
|
1909
|
-
const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
|
|
1910
|
-
if (pendingIndex < 0) return;
|
|
1911
|
-
const [taskIndex] = pending.splice(pendingIndex, 1);
|
|
1912
|
-
return taskIndex;
|
|
1913
|
-
};
|
|
1914
|
-
function pump() {
|
|
1915
|
-
if (settled) return;
|
|
1916
|
-
while(active < options.maxConcurrency && !stopRequested()){
|
|
1917
|
-
const taskIndex = claimRunnable();
|
|
1918
|
-
if (void 0 === taskIndex) break;
|
|
1919
|
-
const task = options.tasks[taskIndex];
|
|
1920
|
-
for (const resource of task.resources)activeResources.add(resource);
|
|
1921
|
-
active += 1;
|
|
1922
|
-
Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
|
|
1923
|
-
results[taskIndex] = result;
|
|
1924
|
-
}).catch((error)=>{
|
|
1925
|
-
if (!hasError) {
|
|
1926
|
-
hasError = true;
|
|
1927
|
-
firstError = error;
|
|
1928
|
-
}
|
|
1929
|
-
}).finally(()=>{
|
|
1930
|
-
release(task.resources);
|
|
1931
|
-
active -= 1;
|
|
1932
|
-
pump();
|
|
1933
|
-
});
|
|
1934
|
-
}
|
|
1935
|
-
if (0 === active && (0 === pending.length || stopRequested())) finish();
|
|
1936
|
-
}
|
|
1937
|
-
options.signal?.addEventListener('abort', pump, {
|
|
1938
|
-
once: true
|
|
1939
|
-
});
|
|
1940
|
-
pump();
|
|
1941
|
-
});
|
|
1942
|
-
}
|
|
1943
|
-
const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
|
|
1944
|
-
const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
|
|
1945
|
-
const documentId = isolatedDocumentId(document, collectedCase);
|
|
1946
|
-
return {
|
|
1947
|
-
projectIndex,
|
|
1948
|
-
prepared,
|
|
1949
|
-
document,
|
|
1950
|
-
collectedCase,
|
|
1951
|
-
task: Object.freeze({
|
|
1952
|
-
taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
|
|
1953
|
-
projectId: prepared.project.projectId,
|
|
1954
|
-
projectName: prepared.project.name,
|
|
1955
|
-
documentId,
|
|
1956
|
-
sourcePath: collectedCase.sourcePath,
|
|
1957
|
-
caseId: collectedCase.caseId,
|
|
1958
|
-
caseName: collectedCase.definition.name,
|
|
1959
|
-
caseIndex: collectedCase.caseIndex,
|
|
1960
|
-
tags: Object.freeze([
|
|
1961
|
-
...collectedCase.definition.tags ?? []
|
|
1962
|
-
]),
|
|
1963
|
-
resources: Object.freeze([
|
|
1964
|
-
...collectedCase.definition.resources ?? []
|
|
1965
|
-
]),
|
|
1966
|
-
retry: prepared.project.retry
|
|
1967
|
-
})
|
|
1968
|
-
};
|
|
1969
|
-
})));
|
|
1970
|
-
const reviveError = (error)=>{
|
|
1971
|
-
const revived = new test_runner_namespaceObject.WorkflowError(error.message, {
|
|
1972
|
-
code: error.code,
|
|
1973
|
-
details: error.details,
|
|
1974
|
-
cause: error.cause
|
|
1975
|
-
});
|
|
1976
|
-
revived.name = error.name;
|
|
1977
|
-
return revived;
|
|
1978
|
-
};
|
|
1979
|
-
const reviveStep = (step)=>{
|
|
1980
|
-
const { error, output, report, ...base } = step;
|
|
1981
|
-
return {
|
|
1982
|
-
...base,
|
|
1983
|
-
input: step.input,
|
|
1984
|
-
meta: {
|
|
1985
|
-
...step.meta
|
|
1986
|
-
},
|
|
1987
|
-
...output ? {
|
|
1988
|
-
output: {
|
|
1989
|
-
...output
|
|
1990
|
-
}
|
|
1991
|
-
} : {},
|
|
1992
|
-
...error ? {
|
|
1993
|
-
error: reviveError(error)
|
|
1994
|
-
} : {},
|
|
1995
|
-
...report ? {
|
|
1996
|
-
report: {
|
|
1997
|
-
traces: report.traces.map((trace)=>({
|
|
1998
|
-
...trace
|
|
1999
|
-
})),
|
|
2000
|
-
...report.collectionErrors ? {
|
|
2001
|
-
collectionErrors: report.collectionErrors.map(reviveError)
|
|
2002
|
-
} : {}
|
|
2003
|
-
}
|
|
2004
|
-
} : {}
|
|
2005
|
-
};
|
|
2006
|
-
};
|
|
2007
|
-
const reviveAttempt = (attempt)=>{
|
|
2008
|
-
const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
|
|
2009
|
-
return {
|
|
2010
|
-
...base,
|
|
2011
|
-
beforeEach: beforeEach.map(reviveStep),
|
|
2012
|
-
steps: steps.map(reviveStep),
|
|
2013
|
-
afterEach: afterEach.map(reviveStep),
|
|
2014
|
-
...executionErrors ? {
|
|
2015
|
-
executionErrors: executionErrors.map(reviveError)
|
|
2016
|
-
} : {},
|
|
2017
|
-
...teardownErrors ? {
|
|
2018
|
-
teardownErrors: teardownErrors.map(reviveError)
|
|
2019
|
-
} : {}
|
|
2020
|
-
};
|
|
2021
|
-
};
|
|
2022
|
-
const reviveDocument = (document)=>{
|
|
2023
|
-
const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
|
|
2024
|
-
return {
|
|
2025
|
-
...base,
|
|
2026
|
-
...hostErrors ? {
|
|
2027
|
-
hostErrors: hostErrors.map((error)=>({
|
|
2028
|
-
...error
|
|
2029
|
-
}))
|
|
2030
|
-
} : {},
|
|
2031
|
-
beforeAll: beforeAll.map(reviveStep),
|
|
2032
|
-
afterAll: afterAll.map(reviveStep),
|
|
2033
|
-
...executionErrors ? {
|
|
2034
|
-
executionErrors: executionErrors.map(reviveError)
|
|
2035
|
-
} : {},
|
|
2036
|
-
...teardownErrors ? {
|
|
2037
|
-
teardownErrors: teardownErrors.map(reviveError)
|
|
2038
|
-
} : {}
|
|
2039
|
-
};
|
|
2040
|
-
};
|
|
2041
|
-
const reviveLifecycle = (lifecycle)=>{
|
|
2042
|
-
const { setupError, teardownErrors, ...base } = lifecycle;
|
|
2043
|
-
return {
|
|
2044
|
-
...base,
|
|
2045
|
-
...setupError ? {
|
|
2046
|
-
setupError: reviveError(setupError)
|
|
2047
|
-
} : {},
|
|
2048
|
-
...teardownErrors ? {
|
|
2049
|
-
teardownErrors: teardownErrors.map(reviveError)
|
|
2050
|
-
} : {}
|
|
2051
|
-
};
|
|
2052
|
-
};
|
|
2053
|
-
const toTransportResult = (result, materializeReport, materializeSource)=>{
|
|
2054
|
-
const transportReports = (scope)=>[
|
|
2055
|
-
...(scope.reportSources ?? []).map(materializeSource),
|
|
2056
|
-
...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
|
|
2057
|
-
];
|
|
2058
|
-
const mapStep = (step)=>{
|
|
2059
|
-
if (!step.report) return step;
|
|
2060
|
-
const { sources: _sources, ...report } = step.report;
|
|
2061
|
-
return {
|
|
2062
|
-
...step,
|
|
2063
|
-
report
|
|
2064
|
-
};
|
|
2065
|
-
};
|
|
2066
|
-
const mapAttempt = (attempt)=>{
|
|
2067
|
-
const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
|
|
2068
|
-
const reportRefs = transportReports(attempt);
|
|
2069
|
-
return {
|
|
2070
|
-
...transportAttempt,
|
|
2071
|
-
beforeEach: attempt.beforeEach.map(mapStep),
|
|
2072
|
-
steps: attempt.steps.map(mapStep),
|
|
2073
|
-
afterEach: attempt.afterEach.map(mapStep),
|
|
2074
|
-
...reportRefs.length ? {
|
|
2075
|
-
reportRefs
|
|
2076
|
-
} : {}
|
|
2077
|
-
};
|
|
2078
|
-
};
|
|
2079
|
-
const attempts = result.case.attempts?.map(mapAttempt);
|
|
2080
|
-
const run = attempts?.at(-1);
|
|
2081
|
-
const { run: _run, attempts: _attempts, ...transportCase } = result.case;
|
|
2082
|
-
const document = result.document ? (()=>{
|
|
2083
|
-
const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
|
|
2084
|
-
const reportRefs = transportReports(result.document);
|
|
2085
|
-
return {
|
|
2086
|
-
...transportDocument,
|
|
2087
|
-
...result.document.hostErrors ? {
|
|
2088
|
-
hostErrors: result.document.hostErrors.map(({ phase, error })=>({
|
|
2089
|
-
phase,
|
|
2090
|
-
error: (0, test_runner_namespaceObject.sanitizeReportError)(error)
|
|
2091
|
-
}))
|
|
2092
|
-
} : {},
|
|
2093
|
-
beforeAll: result.document.beforeAll.map(mapStep),
|
|
2094
|
-
afterAll: result.document.afterAll.map(mapStep),
|
|
2095
|
-
...reportRefs.length ? {
|
|
2096
|
-
reportRefs
|
|
2097
|
-
} : {}
|
|
2098
|
-
};
|
|
2099
|
-
})() : void 0;
|
|
2100
|
-
return JSON.parse(JSON.stringify({
|
|
2101
|
-
case: {
|
|
2102
|
-
...transportCase,
|
|
2103
|
-
...run ? {
|
|
2104
|
-
run
|
|
2105
|
-
} : {},
|
|
2106
|
-
...attempts ? {
|
|
2107
|
-
attempts
|
|
2108
|
-
} : {}
|
|
2109
|
-
},
|
|
2110
|
-
...document ? {
|
|
2111
|
-
document
|
|
2112
|
-
} : {},
|
|
2113
|
-
...result.lifecycle ? {
|
|
2114
|
-
lifecycle: result.lifecycle
|
|
2115
|
-
} : {}
|
|
2116
|
-
}));
|
|
2117
|
-
};
|
|
2118
|
-
const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
|
|
2119
|
-
const resolveReports = (refs)=>{
|
|
2120
|
-
const reportPaths = [];
|
|
2121
|
-
const reportSources = [];
|
|
2122
|
-
for (const ref of refs ?? []){
|
|
2123
|
-
const path = resolveReportReference(ref);
|
|
2124
|
-
const source = resolveSource(ref);
|
|
2125
|
-
if (source) reportSources.push(source);
|
|
2126
|
-
else reportPaths.push(path);
|
|
2127
|
-
}
|
|
2128
|
-
return {
|
|
2129
|
-
...reportPaths.length ? {
|
|
2130
|
-
reportPaths
|
|
2131
|
-
} : {},
|
|
2132
|
-
...reportSources.length ? {
|
|
2133
|
-
reportSources
|
|
2134
|
-
} : {}
|
|
2135
|
-
};
|
|
2136
|
-
};
|
|
2137
|
-
const attempts = result.case.attempts?.map((attempt)=>({
|
|
2138
|
-
...reviveAttempt(attempt),
|
|
2139
|
-
...resolveReports(attempt.reportRefs)
|
|
2140
|
-
}));
|
|
2141
|
-
const run = attempts?.at(-1);
|
|
2142
|
-
const { run: _run, attempts: _attempts, ...caseBase } = result.case;
|
|
2143
|
-
return {
|
|
2144
|
-
case: {
|
|
2145
|
-
...caseBase,
|
|
2146
|
-
...run ? {
|
|
2147
|
-
run
|
|
2148
|
-
} : {},
|
|
2149
|
-
...attempts ? {
|
|
2150
|
-
attempts
|
|
2151
|
-
} : {}
|
|
2152
|
-
},
|
|
2153
|
-
...result.document ? {
|
|
2154
|
-
document: {
|
|
2155
|
-
...reviveDocument(result.document),
|
|
2156
|
-
...resolveReports(result.document.reportRefs)
|
|
2157
|
-
}
|
|
2158
|
-
} : {},
|
|
2159
|
-
...result.lifecycle ? {
|
|
2160
|
-
lifecycle: reviveLifecycle(result.lifecycle)
|
|
2161
|
-
} : {}
|
|
2162
|
-
};
|
|
2163
|
-
};
|
|
2164
|
-
const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
|
|
2165
|
-
const { prepared, document, collectedCase, task } = preparedTask;
|
|
2166
|
-
const { project } = prepared;
|
|
2167
|
-
const isolatedDocument = {
|
|
2168
|
-
...document,
|
|
2169
|
-
documentId: task.documentId,
|
|
2170
|
-
cases: [
|
|
2171
|
-
collectedCase
|
|
2172
|
-
]
|
|
2173
|
-
};
|
|
2174
|
-
const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
|
|
2175
|
-
project,
|
|
2176
|
-
setup: project.setup,
|
|
2177
|
-
signal: options.signal
|
|
2178
|
-
});
|
|
2179
|
-
let lifecycle;
|
|
2180
|
-
let execution;
|
|
2181
|
-
let executionError;
|
|
2182
|
-
try {
|
|
2183
|
-
await runtime.start();
|
|
2184
|
-
if (runtime.canRun) execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(isolatedDocument, {
|
|
2185
|
-
resolveNode: project.nodes.require.bind(project.nodes),
|
|
2186
|
-
project,
|
|
2187
|
-
projectContext: runtime.context,
|
|
2188
|
-
retry: project.retry,
|
|
2189
|
-
signal: runtime.signal,
|
|
2190
|
-
defaultTimeoutMs: options.test.testTimeout,
|
|
2191
|
-
shouldStop: ()=>options.signal.aborted,
|
|
2192
|
-
stopReason: ()=>'interrupted',
|
|
2193
|
-
isFatalError: (run)=>[
|
|
2194
|
-
...run.beforeEach,
|
|
2195
|
-
...run.steps,
|
|
2196
|
-
...run.afterEach
|
|
2197
|
-
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (run.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError),
|
|
2198
|
-
onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
|
|
2199
|
-
onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
|
|
2200
|
-
onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
|
|
2201
|
-
});
|
|
2202
|
-
} catch (error) {
|
|
2203
|
-
executionError = error;
|
|
2204
|
-
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) execution = error.result;
|
|
2205
|
-
} finally{
|
|
2206
|
-
const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
|
|
2207
|
-
lifecycle = await runtime.finish(failed ? 'failed' : 'success');
|
|
2208
|
-
}
|
|
2209
|
-
if (executionError) {
|
|
2210
|
-
if (!execution) throw executionError;
|
|
2211
|
-
options.onInfrastructureError(executionError);
|
|
2212
|
-
}
|
|
2213
|
-
if (!execution) return toTransportResult({
|
|
2214
|
-
case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
|
|
2215
|
-
lifecycle
|
|
2216
|
-
}, materializeReport, materializeSource);
|
|
2217
|
-
const outcome = execution.cases[0];
|
|
2218
|
-
if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
|
|
2219
|
-
return toTransportResult({
|
|
2220
|
-
case: {
|
|
2221
|
-
...outcome,
|
|
2222
|
-
documentId: task.documentId
|
|
2223
|
-
},
|
|
2224
|
-
document: execution.document,
|
|
2225
|
-
lifecycle
|
|
2226
|
-
}, materializeReport, materializeSource);
|
|
2227
|
-
};
|
|
2228
|
-
const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
|
|
2229
|
-
const { prepared, task } = preparedTask;
|
|
2230
|
-
const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
|
|
2231
|
-
taskProgress('started');
|
|
2232
|
-
const outputDir = (0, external_node_path_namespaceObject.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
|
|
2233
|
-
(0, external_node_fs_namespaceObject.mkdirSync)(outputDir, {
|
|
2234
|
-
recursive: true
|
|
2235
|
-
});
|
|
2236
|
-
const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
|
|
2237
|
-
if (prepared.collectionErrors.length > 0) return {
|
|
2238
|
-
case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
|
|
2239
|
-
};
|
|
2240
|
-
const executor = options.executor ?? localTestExecutor;
|
|
2241
|
-
let result;
|
|
2242
|
-
let executorFailure;
|
|
2243
|
-
let executorAttempts = 0;
|
|
2244
|
-
for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
|
|
2245
|
-
executorAttempts = attemptIndex + 1;
|
|
2246
|
-
try {
|
|
2247
|
-
result = await executor.execute(task, {
|
|
2248
|
-
signal: options.signal,
|
|
2249
|
-
onProgress: taskProgress,
|
|
2250
|
-
outputDir,
|
|
2251
|
-
materializeReport,
|
|
2252
|
-
runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
|
|
2253
|
-
});
|
|
2254
|
-
executorFailure = void 0;
|
|
2255
|
-
break;
|
|
2256
|
-
} catch (error) {
|
|
2257
|
-
if (!isTestExecutorError(error)) throw error;
|
|
2258
|
-
executorFailure = error;
|
|
2259
|
-
if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
|
|
2260
|
-
taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
|
|
2261
|
-
}
|
|
2262
|
-
}
|
|
2263
|
-
if (!result) {
|
|
2264
|
-
if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
|
|
2265
|
-
taskProgress(`executor-failed: ${executorFailure.message}`);
|
|
2266
|
-
return {
|
|
2267
|
-
case: {
|
|
2268
|
-
...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
|
|
2269
|
-
execution: {
|
|
2270
|
-
executor: executor.name,
|
|
2271
|
-
resources: task.resources,
|
|
2272
|
-
attempts: executorAttempts,
|
|
2273
|
-
failure: {
|
|
2274
|
-
kind: executorFailure.kind,
|
|
2275
|
-
message: executorFailure.message,
|
|
2276
|
-
retryable: executorFailure.retryable
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
};
|
|
2281
|
-
}
|
|
2282
|
-
assertTestCaseTaskRunResult(task, result);
|
|
2283
|
-
const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
|
|
2284
|
-
const completed = {
|
|
2285
|
-
...internalResult,
|
|
2286
|
-
case: {
|
|
2287
|
-
...internalResult.case,
|
|
2288
|
-
execution: {
|
|
2289
|
-
executor: executor.name,
|
|
2290
|
-
resources: task.resources,
|
|
2291
|
-
attempts: executorAttempts,
|
|
2292
|
-
...internalResult.lifecycle ? {
|
|
2293
|
-
lifecycle: internalResult.lifecycle
|
|
2294
|
-
} : {},
|
|
2295
|
-
...result.artifacts ? {
|
|
2296
|
-
artifacts: result.artifacts
|
|
2297
|
-
} : {},
|
|
2298
|
-
...result.metadata ? {
|
|
2299
|
-
metadata: result.metadata
|
|
2300
|
-
} : {}
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
};
|
|
2304
|
-
onCompleted(completed);
|
|
2305
|
-
for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
|
|
2306
|
-
if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
|
|
2307
|
-
taskProgress(internalResult.case.status);
|
|
2308
|
-
return completed;
|
|
2309
|
-
};
|
|
2310
|
-
const runCaseExecution = async (options)=>{
|
|
2311
|
-
const tasks = prepareCaseTasks(options.projects);
|
|
2312
|
-
let failedCaseCount = 0;
|
|
2313
|
-
const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
|
|
2314
|
-
const results = new Array(tasks.length);
|
|
2315
|
-
if (tasks.length > 0) try {
|
|
2316
|
-
await runTaskPool({
|
|
2317
|
-
tasks: tasks.map((preparedTask)=>preparedTask.task),
|
|
2318
|
-
maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
|
|
2319
|
-
signal: options.signal,
|
|
2320
|
-
shouldStop: bailReached,
|
|
2321
|
-
run: async (_task, taskIndex)=>{
|
|
2322
|
-
try {
|
|
2323
|
-
const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
|
|
2324
|
-
results[taskIndex] = completed;
|
|
2325
|
-
});
|
|
2326
|
-
results[taskIndex] = result;
|
|
2327
|
-
if ('failed' === result.case.status) failedCaseCount += 1;
|
|
2328
|
-
return result;
|
|
2329
|
-
} catch (error) {
|
|
2330
|
-
options.onInfrastructureError(error);
|
|
2331
|
-
throw error;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
});
|
|
2335
|
-
} catch (error) {
|
|
2336
|
-
options.onInfrastructureError(error);
|
|
2337
|
-
}
|
|
2338
|
-
return options.projects.map((prepared, projectIndex)=>{
|
|
2339
|
-
const projectTasks = tasks.map((task, taskIndex)=>({
|
|
2340
|
-
task,
|
|
2341
|
-
result: results[taskIndex]
|
|
2342
|
-
})).filter(({ task })=>task.projectIndex === projectIndex);
|
|
2343
|
-
const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
|
|
2344
|
-
const cases = projectTasks.map(({ task, result })=>{
|
|
2345
|
-
if (result) return result.case;
|
|
2346
|
-
if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
|
|
2347
|
-
return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
|
|
2348
|
-
});
|
|
2349
|
-
const documents = projectTasks.flatMap(({ result })=>result?.document ? [
|
|
2350
|
-
result.document
|
|
2351
|
-
] : []);
|
|
2352
|
-
return buildProjectResult(prepared, cases, documents);
|
|
2353
|
-
});
|
|
2354
|
-
};
|
|
2355
1153
|
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
2356
1154
|
...attempt.beforeEach,
|
|
2357
1155
|
...attempt.steps,
|
|
@@ -2397,7 +1195,7 @@ async function executeDocumentInvocation(options) {
|
|
|
2397
1195
|
},
|
|
2398
1196
|
onStepResult: async (info, result)=>{
|
|
2399
1197
|
await invocation.bindings.onStepResult?.(info, result);
|
|
2400
|
-
options.onProgress(formatStepResult(info, result
|
|
1198
|
+
options.onProgress(formatStepResult(info, result));
|
|
2401
1199
|
},
|
|
2402
1200
|
onCaseResult: async (attempt)=>{
|
|
2403
1201
|
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
@@ -2515,6 +1313,16 @@ function createOperationJournal(runDir) {
|
|
|
2515
1313
|
}
|
|
2516
1314
|
};
|
|
2517
1315
|
}
|
|
1316
|
+
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1317
|
+
documentId,
|
|
1318
|
+
caseId: collectedCase.caseId,
|
|
1319
|
+
projectName,
|
|
1320
|
+
name: collectedCase.definition.name,
|
|
1321
|
+
sourcePath: collectedCase.sourcePath,
|
|
1322
|
+
caseIndex: collectedCase.caseIndex,
|
|
1323
|
+
status: 'not-run',
|
|
1324
|
+
notRunReason: reason
|
|
1325
|
+
});
|
|
2518
1326
|
const summarize = (projects)=>{
|
|
2519
1327
|
const healthCases = projects.flatMap((project)=>{
|
|
2520
1328
|
const documentAttemptByRunId = new Map(project.documents.map((document)=>[
|
|
@@ -2548,6 +1356,12 @@ const summarize = (projects)=>{
|
|
|
2548
1356
|
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
2549
1357
|
};
|
|
2550
1358
|
};
|
|
1359
|
+
const latestById = (items, id)=>[
|
|
1360
|
+
...new Map(items.map((item)=>[
|
|
1361
|
+
id(item),
|
|
1362
|
+
item
|
|
1363
|
+
])).values()
|
|
1364
|
+
];
|
|
2551
1365
|
const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
|
|
2552
1366
|
async function runPreparedTestProject(plan) {
|
|
2553
1367
|
const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
|
|
@@ -2560,7 +1374,7 @@ async function runPreparedTestProject(plan) {
|
|
|
2560
1374
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
2561
1375
|
const journal = createOperationJournal(runDir);
|
|
2562
1376
|
const rootController = new AbortController();
|
|
2563
|
-
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10,
|
|
1377
|
+
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
2564
1378
|
const handleSignal = (signal)=>{
|
|
2565
1379
|
rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
|
|
2566
1380
|
};
|
|
@@ -2602,6 +1416,31 @@ async function runPreparedTestProject(plan) {
|
|
|
2602
1416
|
const { project } = prepared;
|
|
2603
1417
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
2604
1418
|
};
|
|
1419
|
+
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
1420
|
+
const { project } = prepared;
|
|
1421
|
+
const projectFailed = prepared.collectionErrors.length > 0 || latestById(cases, (item)=>item.caseId).some((item)=>'success' !== item.status) || latestById(documents, (item)=>item.documentId).some((item)=>'failed' === item.status) || lifecycle?.status === 'failed';
|
|
1422
|
+
return {
|
|
1423
|
+
projectId: project.projectId,
|
|
1424
|
+
name: project.name,
|
|
1425
|
+
...prepared.platform ? {
|
|
1426
|
+
platform: prepared.platform
|
|
1427
|
+
} : {},
|
|
1428
|
+
status: projectFailed ? 'failed' : 'success',
|
|
1429
|
+
retry: project.retry,
|
|
1430
|
+
fileSelection: prepared.fileSelection,
|
|
1431
|
+
tagSelection: project.tags,
|
|
1432
|
+
sourceCount: prepared.sources.length,
|
|
1433
|
+
selectedCaseCount: prepared.selectedCaseCount,
|
|
1434
|
+
filteredCaseCount: prepared.filteredCaseCount,
|
|
1435
|
+
...lifecycle ? {
|
|
1436
|
+
lifecycle
|
|
1437
|
+
} : {},
|
|
1438
|
+
cases,
|
|
1439
|
+
plans: prepared.plans,
|
|
1440
|
+
documents,
|
|
1441
|
+
collectionErrors: prepared.collectionErrors
|
|
1442
|
+
};
|
|
1443
|
+
};
|
|
2605
1444
|
const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
|
|
2606
1445
|
announceProject(prepared, projectIndex);
|
|
2607
1446
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
@@ -2702,7 +1541,7 @@ async function runPreparedTestProject(plan) {
|
|
|
2702
1541
|
};
|
|
2703
1542
|
const projectResults = new Array(preparedProjects.length);
|
|
2704
1543
|
try {
|
|
2705
|
-
|
|
1544
|
+
await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
|
|
2706
1545
|
concurrency: effectiveConcurrency,
|
|
2707
1546
|
shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
|
|
2708
1547
|
}, async (prepared, index)=>{
|
|
@@ -2713,20 +1552,6 @@ async function runPreparedTestProject(plan) {
|
|
|
2713
1552
|
throw error;
|
|
2714
1553
|
}
|
|
2715
1554
|
});
|
|
2716
|
-
else {
|
|
2717
|
-
const results = await runCaseExecution({
|
|
2718
|
-
projects: preparedProjects,
|
|
2719
|
-
test: definition.test,
|
|
2720
|
-
executor: definition.executor,
|
|
2721
|
-
runDir,
|
|
2722
|
-
signal: rootController.signal,
|
|
2723
|
-
progress,
|
|
2724
|
-
onInfrastructureError: recordInfrastructureError
|
|
2725
|
-
});
|
|
2726
|
-
results.forEach((result, index)=>{
|
|
2727
|
-
projectResults[index] = result;
|
|
2728
|
-
});
|
|
2729
|
-
}
|
|
2730
1555
|
} catch (error) {
|
|
2731
1556
|
recordInfrastructureError(error);
|
|
2732
1557
|
} finally{
|
|
@@ -2833,34 +1658,22 @@ async function runTestProject(options = {}) {
|
|
|
2833
1658
|
return runPreparedTestProject(await prepareTestRun(options));
|
|
2834
1659
|
}
|
|
2835
1660
|
exports.DEFAULT_TEST_FILE_SELECTION = __webpack_exports__.DEFAULT_TEST_FILE_SELECTION;
|
|
2836
|
-
exports.TestExecutorError = __webpack_exports__.TestExecutorError;
|
|
2837
|
-
exports.assertTestCaseTaskRunResult = __webpack_exports__.assertTestCaseTaskRunResult;
|
|
2838
1661
|
exports.createTestRunId = __webpack_exports__.createTestRunId;
|
|
2839
1662
|
exports.defineProjectSetup = __webpack_exports__.defineProjectSetup;
|
|
2840
|
-
exports.defineTestExecutor = __webpack_exports__.defineTestExecutor;
|
|
2841
1663
|
exports.defineTestProject = __webpack_exports__.defineTestProject;
|
|
2842
|
-
exports.discoverSelectedTestFiles = __webpack_exports__.discoverSelectedTestFiles;
|
|
2843
1664
|
exports.discoverTestConfig = __webpack_exports__.discoverTestConfig;
|
|
2844
1665
|
exports.discoverTestFiles = __webpack_exports__.discoverTestFiles;
|
|
2845
|
-
exports.isTestExecutorError = __webpack_exports__.isTestExecutorError;
|
|
2846
1666
|
exports.loadTestProject = __webpack_exports__.loadTestProject;
|
|
2847
|
-
exports.localTestExecutor = __webpack_exports__.localTestExecutor;
|
|
2848
1667
|
exports.runTestProject = __webpack_exports__.runTestProject;
|
|
2849
1668
|
exports.validateTestFileSelection = __webpack_exports__.validateTestFileSelection;
|
|
2850
1669
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
2851
1670
|
"DEFAULT_TEST_FILE_SELECTION",
|
|
2852
|
-
"TestExecutorError",
|
|
2853
|
-
"assertTestCaseTaskRunResult",
|
|
2854
1671
|
"createTestRunId",
|
|
2855
1672
|
"defineProjectSetup",
|
|
2856
|
-
"defineTestExecutor",
|
|
2857
1673
|
"defineTestProject",
|
|
2858
|
-
"discoverSelectedTestFiles",
|
|
2859
1674
|
"discoverTestConfig",
|
|
2860
1675
|
"discoverTestFiles",
|
|
2861
|
-
"isTestExecutorError",
|
|
2862
1676
|
"loadTestProject",
|
|
2863
|
-
"localTestExecutor",
|
|
2864
1677
|
"runTestProject",
|
|
2865
1678
|
"validateTestFileSelection"
|
|
2866
1679
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|