@midscene/test 1.15.3 → 1.15.4-beta-20261013083552.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/cli/index.mjs +1508 -350
- package/dist/es/cli/index.mjs.map +1 -1
- package/dist/es/index.mjs +20 -1
- 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 +1253 -69
- package/dist/es/test-cli.mjs.map +1 -1
- package/dist/lib/cli/index.js +1515 -342
- package/dist/lib/cli/index.js.map +1 -1
- package/dist/lib/index.js +20 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/midscene/index.js +3 -0
- package/dist/lib/midscene/index.js.map +1 -1
- package/dist/lib/test-cli.js +1253 -69
- package/dist/lib/test-cli.js.map +1 -1
- package/dist/types/cli/index.d.ts +550 -0
- package/dist/types/index.d.ts +20 -0
- package/dist/types/midscene/index.d.ts +6 -2
- package/package.json +13 -13
package/dist/lib/cli/index.js
CHANGED
|
@@ -25,298 +25,476 @@ 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,
|
|
28
29
|
createTestRunId: ()=>createTestRunId,
|
|
29
|
-
runTestProject: ()=>runTestProject,
|
|
30
|
-
discoverTestConfig: ()=>discoverTestConfig,
|
|
31
30
|
validateTestFileSelection: ()=>validateTestFileSelection,
|
|
31
|
+
defineTestExecutor: ()=>defineTestExecutor,
|
|
32
|
+
discoverTestConfig: ()=>discoverTestConfig,
|
|
33
|
+
runTestProject: ()=>runTestProject,
|
|
34
|
+
localTestExecutor: ()=>localTestExecutor,
|
|
35
|
+
assertTestCaseTaskRunResult: ()=>assertTestCaseTaskRunResult,
|
|
32
36
|
defineProjectSetup: ()=>defineProjectSetup,
|
|
37
|
+
discoverSelectedTestFiles: ()=>discoverSelectedTestFiles,
|
|
33
38
|
discoverTestFiles: ()=>discoverTestFiles,
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
defineTestProject: ()=>defineTestProject,
|
|
40
|
+
loadTestProject: ()=>loadTestProject
|
|
36
41
|
});
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
42
|
+
const v4_namespaceObject = require("zod/v4");
|
|
43
|
+
const isSafeResultPathSegment = (value)=>'string' == typeof value && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(value);
|
|
44
|
+
const json = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
|
|
45
|
+
v4_namespaceObject.z["null"](),
|
|
46
|
+
v4_namespaceObject.z.string(),
|
|
47
|
+
v4_namespaceObject.z.number().finite(),
|
|
48
|
+
v4_namespaceObject.z.boolean(),
|
|
49
|
+
v4_namespaceObject.z.array(json),
|
|
50
|
+
v4_namespaceObject.z.record(v4_namespaceObject.z.string(), json)
|
|
51
|
+
]));
|
|
52
|
+
const time = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)));
|
|
53
|
+
const executor_runtime_schema_count = v4_namespaceObject.z.number().int().nonnegative();
|
|
54
|
+
const executor_runtime_schema_value = v4_namespaceObject.z.strictObject({
|
|
55
|
+
value: json,
|
|
56
|
+
truncatedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional(),
|
|
57
|
+
redactedPaths: v4_namespaceObject.z.array(v4_namespaceObject.z.string()).optional()
|
|
58
|
+
});
|
|
59
|
+
const reportErrorSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.strictObject({
|
|
60
|
+
format: v4_namespaceObject.z.literal('midscene-error').optional(),
|
|
61
|
+
name: v4_namespaceObject.z.string(),
|
|
62
|
+
message: v4_namespaceObject.z.string(),
|
|
63
|
+
code: v4_namespaceObject.z.string().optional(),
|
|
64
|
+
details: executor_runtime_schema_value.optional(),
|
|
65
|
+
cause: reportErrorSchema.optional(),
|
|
66
|
+
errors: v4_namespaceObject.z.array(reportErrorSchema).optional(),
|
|
67
|
+
truncated: v4_namespaceObject.z.boolean().optional()
|
|
68
|
+
}));
|
|
69
|
+
const executor_runtime_schema_operation = v4_namespaceObject.z.strictObject({
|
|
70
|
+
operationId: v4_namespaceObject.z.string().min(1),
|
|
71
|
+
scopeId: v4_namespaceObject.z.string().optional(),
|
|
72
|
+
stepId: v4_namespaceObject.z.string().optional(),
|
|
73
|
+
node: v4_namespaceObject.z.string().optional()
|
|
74
|
+
});
|
|
75
|
+
const cleanupSchema = v4_namespaceObject.z.strictObject({
|
|
76
|
+
status: v4_namespaceObject.z["enum"]([
|
|
77
|
+
'not-required',
|
|
78
|
+
'completed',
|
|
79
|
+
'pending',
|
|
80
|
+
'failed'
|
|
81
|
+
]),
|
|
82
|
+
capturedAt: time,
|
|
83
|
+
operationId: v4_namespaceObject.z.string().optional(),
|
|
84
|
+
blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional(),
|
|
85
|
+
error: reportErrorSchema.optional()
|
|
86
|
+
});
|
|
87
|
+
const pendingOperationsSchema = v4_namespaceObject.z.array(executor_runtime_schema_operation);
|
|
88
|
+
const collectionStatus = v4_namespaceObject.z["enum"]([
|
|
89
|
+
'complete',
|
|
90
|
+
'partial',
|
|
91
|
+
'unknown'
|
|
92
|
+
]);
|
|
93
|
+
const metricsSchema = v4_namespaceObject.z.strictObject({
|
|
94
|
+
modelCallCount: executor_runtime_schema_count,
|
|
95
|
+
modelTimeMs: v4_namespaceObject.z.number().finite().nonnegative(),
|
|
96
|
+
promptTokens: executor_runtime_schema_count,
|
|
97
|
+
cachedInputTokens: executor_runtime_schema_count,
|
|
98
|
+
completionTokens: executor_runtime_schema_count,
|
|
99
|
+
totalTokens: executor_runtime_schema_count,
|
|
100
|
+
collection: v4_namespaceObject.z.strictObject({
|
|
101
|
+
status: collectionStatus,
|
|
102
|
+
expectedExecutionCount: executor_runtime_schema_count,
|
|
103
|
+
capturedExecutionCount: executor_runtime_schema_count,
|
|
104
|
+
reasons: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
|
|
105
|
+
fields: v4_namespaceObject.z.strictObject({
|
|
106
|
+
modelCallCount: collectionStatus,
|
|
107
|
+
modelTimeMs: collectionStatus,
|
|
108
|
+
totalTokens: collectionStatus
|
|
109
|
+
}).optional()
|
|
110
|
+
}).optional()
|
|
111
|
+
});
|
|
112
|
+
const operationsSchema = v4_namespaceObject.z.strictObject({
|
|
113
|
+
capturedAt: time,
|
|
114
|
+
events: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
115
|
+
operationId: v4_namespaceObject.z.string().min(1),
|
|
116
|
+
operationKind: v4_namespaceObject.z["enum"]([
|
|
117
|
+
'node',
|
|
118
|
+
'resource',
|
|
119
|
+
'acquisition',
|
|
120
|
+
'cleanup',
|
|
121
|
+
'readiness'
|
|
122
|
+
]),
|
|
123
|
+
kind: v4_namespaceObject.z["enum"]([
|
|
124
|
+
'started',
|
|
125
|
+
'cancel-requested',
|
|
126
|
+
'settled',
|
|
127
|
+
'runner-returned',
|
|
128
|
+
'cleanup-deferred'
|
|
129
|
+
]),
|
|
130
|
+
at: time,
|
|
131
|
+
stepId: v4_namespaceObject.z.string().optional(),
|
|
132
|
+
scopeId: v4_namespaceObject.z.string().optional(),
|
|
133
|
+
node: v4_namespaceObject.z.string().optional(),
|
|
134
|
+
status: v4_namespaceObject.z["enum"]([
|
|
135
|
+
'success',
|
|
136
|
+
'failed',
|
|
137
|
+
'not-run'
|
|
138
|
+
]).optional(),
|
|
139
|
+
pending: v4_namespaceObject.z.boolean().optional(),
|
|
140
|
+
error: reportErrorSchema.optional(),
|
|
141
|
+
blockingOperations: v4_namespaceObject.z.array(executor_runtime_schema_operation).optional()
|
|
142
|
+
}))
|
|
143
|
+
});
|
|
144
|
+
const runtimeResultFields = {
|
|
145
|
+
executionStatus: v4_namespaceObject.z["enum"]([
|
|
146
|
+
'success',
|
|
147
|
+
'failed',
|
|
148
|
+
'not-run'
|
|
149
|
+
]).optional(),
|
|
150
|
+
phases: v4_namespaceObject.z.partialRecord(v4_namespaceObject.z["enum"]([
|
|
151
|
+
'beforeAll',
|
|
152
|
+
'beforeEach',
|
|
153
|
+
'steps',
|
|
154
|
+
'afterEach',
|
|
155
|
+
'afterAll'
|
|
156
|
+
]), v4_namespaceObject.z.strictObject({
|
|
157
|
+
status: v4_namespaceObject.z["enum"]([
|
|
158
|
+
'success',
|
|
159
|
+
'failed',
|
|
160
|
+
'not-run',
|
|
161
|
+
'not-required'
|
|
162
|
+
]),
|
|
163
|
+
plannedStepCount: executor_runtime_schema_count,
|
|
164
|
+
executedStepCount: executor_runtime_schema_count
|
|
165
|
+
})).optional(),
|
|
166
|
+
cleanup: cleanupSchema.optional(),
|
|
167
|
+
pendingOperations: pendingOperationsSchema.optional(),
|
|
168
|
+
retryDecision: v4_namespaceObject.z.strictObject({
|
|
169
|
+
scope: v4_namespaceObject.z["enum"]([
|
|
170
|
+
'case',
|
|
171
|
+
'document'
|
|
172
|
+
]),
|
|
173
|
+
retryCount: executor_runtime_schema_count,
|
|
174
|
+
attemptIndex: executor_runtime_schema_count,
|
|
175
|
+
decision: v4_namespaceObject.z["enum"]([
|
|
176
|
+
'retry',
|
|
177
|
+
'stop'
|
|
178
|
+
]),
|
|
179
|
+
reason: v4_namespaceObject.z["enum"]([
|
|
180
|
+
'passed',
|
|
181
|
+
'attempt-failed',
|
|
182
|
+
'budget-exhausted',
|
|
183
|
+
'cleanup-pending',
|
|
184
|
+
'cleanup-failed',
|
|
185
|
+
'operation-pending',
|
|
186
|
+
'fatal-error',
|
|
187
|
+
'interrupted',
|
|
188
|
+
'bail',
|
|
189
|
+
'infrastructure-error'
|
|
190
|
+
])
|
|
191
|
+
}).optional()
|
|
101
192
|
};
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
193
|
+
const plannedStep = v4_namespaceObject.z.strictObject({
|
|
194
|
+
id: v4_namespaceObject.z.string(),
|
|
195
|
+
phase: v4_namespaceObject.z["enum"]([
|
|
196
|
+
'beforeAll',
|
|
197
|
+
'beforeEach',
|
|
198
|
+
'steps',
|
|
199
|
+
'afterEach',
|
|
200
|
+
'afterAll'
|
|
201
|
+
]),
|
|
202
|
+
stepIndex: executor_runtime_schema_count,
|
|
203
|
+
node: v4_namespaceObject.z.string(),
|
|
204
|
+
title: v4_namespaceObject.z.string().optional(),
|
|
205
|
+
input: executor_runtime_schema_value,
|
|
206
|
+
inputMode: v4_namespaceObject.z["enum"]([
|
|
207
|
+
'template',
|
|
208
|
+
'resolved'
|
|
209
|
+
]),
|
|
210
|
+
timeoutMs: v4_namespaceObject.z.number().positive().optional(),
|
|
211
|
+
continueOnError: v4_namespaceObject.z.boolean()
|
|
212
|
+
});
|
|
213
|
+
const documentPlanSchema = v4_namespaceObject.z.strictObject({
|
|
214
|
+
documentId: v4_namespaceObject.z.string(),
|
|
215
|
+
sourcePath: v4_namespaceObject.z.string(),
|
|
216
|
+
sourceYaml: executor_runtime_schema_value.optional(),
|
|
217
|
+
retry: v4_namespaceObject.z.strictObject({
|
|
218
|
+
scope: v4_namespaceObject.z["enum"]([
|
|
219
|
+
'case',
|
|
220
|
+
'document'
|
|
221
|
+
]),
|
|
222
|
+
count: executor_runtime_schema_count
|
|
223
|
+
}),
|
|
224
|
+
beforeAll: v4_namespaceObject.z.array(plannedStep),
|
|
225
|
+
afterAll: v4_namespaceObject.z.array(plannedStep),
|
|
226
|
+
cases: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
227
|
+
caseId: v4_namespaceObject.z.string(),
|
|
228
|
+
name: v4_namespaceObject.z.string(),
|
|
229
|
+
caseIndex: executor_runtime_schema_count,
|
|
230
|
+
selected: v4_namespaceObject.z.boolean(),
|
|
231
|
+
tags: v4_namespaceObject.z.array(v4_namespaceObject.z.string()),
|
|
232
|
+
onFailure: v4_namespaceObject.z["enum"]([
|
|
233
|
+
'continue',
|
|
234
|
+
'stop-document'
|
|
235
|
+
]).optional(),
|
|
236
|
+
beforeEach: v4_namespaceObject.z.array(plannedStep),
|
|
237
|
+
steps: v4_namespaceObject.z.array(plannedStep),
|
|
238
|
+
afterEach: v4_namespaceObject.z.array(plannedStep)
|
|
239
|
+
}))
|
|
240
|
+
});
|
|
241
|
+
function _define_property(obj, key, value) {
|
|
242
|
+
if (key in obj) Object.defineProperty(obj, key, {
|
|
243
|
+
value: value,
|
|
244
|
+
enumerable: true,
|
|
245
|
+
configurable: true,
|
|
246
|
+
writable: true
|
|
111
247
|
});
|
|
112
|
-
|
|
113
|
-
|
|
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);
|
|
248
|
+
else obj[key] = value;
|
|
249
|
+
return obj;
|
|
126
250
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
251
|
+
class TestExecutorError extends Error {
|
|
252
|
+
constructor(message, options = {}){
|
|
253
|
+
super(message, {
|
|
254
|
+
cause: options.cause
|
|
255
|
+
}), _define_property(this, "kind", void 0), _define_property(this, "retryable", void 0);
|
|
256
|
+
this.name = 'TestExecutorError';
|
|
257
|
+
this.kind = options.kind ?? 'unknown';
|
|
258
|
+
this.retryable = options.retryable ?? false;
|
|
131
259
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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`)
|
|
221
|
-
}
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
return {
|
|
225
|
-
projects: Object.freeze(projects),
|
|
226
|
-
hasExplicitProjects: true
|
|
227
|
-
};
|
|
228
|
-
};
|
|
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')
|
|
236
|
-
});
|
|
237
|
-
};
|
|
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;
|
|
242
|
-
};
|
|
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')
|
|
260
|
+
}
|
|
261
|
+
const defineTestExecutor = (executor)=>executor;
|
|
262
|
+
const localTestExecutor = Object.freeze({
|
|
263
|
+
name: 'local',
|
|
264
|
+
execute: (_task, context)=>context.runLocal()
|
|
265
|
+
});
|
|
266
|
+
const jsonValueSchema = v4_namespaceObject.z.lazy(()=>v4_namespaceObject.z.union([
|
|
267
|
+
v4_namespaceObject.z["null"](),
|
|
268
|
+
v4_namespaceObject.z.string(),
|
|
269
|
+
v4_namespaceObject.z.number().finite(),
|
|
270
|
+
v4_namespaceObject.z.boolean(),
|
|
271
|
+
v4_namespaceObject.z.array(jsonValueSchema),
|
|
272
|
+
v4_namespaceObject.z.record(v4_namespaceObject.z.string(), jsonValueSchema)
|
|
273
|
+
]));
|
|
274
|
+
const timestampSchema = v4_namespaceObject.z.string().refine((value)=>Number.isFinite(Date.parse(value)), {
|
|
275
|
+
message: 'Expected an ISO-compatible timestamp.'
|
|
276
|
+
});
|
|
277
|
+
const errorSchema = v4_namespaceObject.z.strictObject({
|
|
278
|
+
format: v4_namespaceObject.z.literal('midscene-error').optional(),
|
|
279
|
+
cause: reportErrorSchema.optional(),
|
|
280
|
+
name: v4_namespaceObject.z.string().min(1),
|
|
281
|
+
message: v4_namespaceObject.z.string(),
|
|
282
|
+
code: v4_namespaceObject.z.string().min(1),
|
|
283
|
+
details: jsonValueSchema.optional()
|
|
284
|
+
});
|
|
285
|
+
const stepBaseSchema = v4_namespaceObject.z.strictObject({
|
|
286
|
+
phase: v4_namespaceObject.z["enum"]([
|
|
287
|
+
'beforeAll',
|
|
288
|
+
'beforeEach',
|
|
289
|
+
'steps',
|
|
290
|
+
'afterEach',
|
|
291
|
+
'afterAll'
|
|
292
|
+
]),
|
|
293
|
+
stepIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
294
|
+
node: v4_namespaceObject.z.string().min(1),
|
|
295
|
+
input: jsonValueSchema,
|
|
296
|
+
meta: v4_namespaceObject.z.strictObject({
|
|
297
|
+
timeoutMs: v4_namespaceObject.z.number().positive().finite().optional(),
|
|
298
|
+
continueOnError: v4_namespaceObject.z.boolean()
|
|
299
|
+
}),
|
|
300
|
+
continuedAfterError: v4_namespaceObject.z.boolean(),
|
|
301
|
+
waitForPreviousOperationMs: v4_namespaceObject.z.number().nonnegative().finite().optional(),
|
|
302
|
+
operations: operationsSchema.optional(),
|
|
303
|
+
output: v4_namespaceObject.z.strictObject({
|
|
304
|
+
summary: v4_namespaceObject.z.string().optional(),
|
|
305
|
+
data: jsonValueSchema.optional()
|
|
306
|
+
}).optional(),
|
|
307
|
+
error: errorSchema.optional(),
|
|
308
|
+
report: v4_namespaceObject.z.strictObject({
|
|
309
|
+
traces: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
310
|
+
type: v4_namespaceObject.z.literal('midscene-execution'),
|
|
311
|
+
executionId: v4_namespaceObject.z.string().min(1),
|
|
312
|
+
metrics: metricsSchema.optional()
|
|
313
|
+
})),
|
|
314
|
+
collectionErrors: v4_namespaceObject.z.array(errorSchema).optional()
|
|
315
|
+
}).optional()
|
|
316
|
+
});
|
|
317
|
+
const stepSchema = v4_namespaceObject.z.discriminatedUnion('status', [
|
|
318
|
+
stepBaseSchema.extend({
|
|
319
|
+
status: v4_namespaceObject.z["enum"]([
|
|
320
|
+
'success',
|
|
321
|
+
'failed'
|
|
322
|
+
]),
|
|
323
|
+
startedAt: timestampSchema,
|
|
324
|
+
endedAt: timestampSchema,
|
|
325
|
+
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
326
|
+
}),
|
|
327
|
+
stepBaseSchema.extend({
|
|
328
|
+
status: v4_namespaceObject.z.literal('not-run'),
|
|
329
|
+
notRunReason: v4_namespaceObject.z.string().min(1),
|
|
330
|
+
blockedBy: v4_namespaceObject.z.string().optional()
|
|
331
|
+
})
|
|
332
|
+
]);
|
|
333
|
+
const validatedStepSchema = stepSchema.superRefine((step, context)=>{
|
|
334
|
+
if ('success' === step.status && (step.error || step.continuedAfterError)) context.addIssue({
|
|
335
|
+
code: 'custom',
|
|
336
|
+
message: 'Successful steps cannot contain failure evidence.'
|
|
251
337
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
};
|
|
277
|
-
};
|
|
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.`);
|
|
282
|
-
}
|
|
283
|
-
return absolutePath.endsWith('.mjs') ? '.mjs' : '.ts';
|
|
284
|
-
};
|
|
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);
|
|
289
|
-
};
|
|
290
|
-
async function loadTestProject(configPath) {
|
|
291
|
-
if (!configPath) return validateTestProjectDefinition({
|
|
292
|
-
nodes: []
|
|
338
|
+
if (step.continuedAfterError && !step.meta.continueOnError) context.addIssue({
|
|
339
|
+
code: 'custom',
|
|
340
|
+
message: 'Continuation requires continueOnError.'
|
|
293
341
|
});
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
342
|
+
});
|
|
343
|
+
const attemptSchema = v4_namespaceObject.z.strictObject({
|
|
344
|
+
...runtimeResultFields,
|
|
345
|
+
caseId: v4_namespaceObject.z.string().min(1),
|
|
346
|
+
runId: v4_namespaceObject.z.string().min(1),
|
|
347
|
+
projectName: v4_namespaceObject.z.string().min(1),
|
|
348
|
+
attemptIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
349
|
+
name: v4_namespaceObject.z.string(),
|
|
350
|
+
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
351
|
+
caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
352
|
+
status: v4_namespaceObject.z["enum"]([
|
|
353
|
+
'success',
|
|
354
|
+
'failed'
|
|
355
|
+
]),
|
|
356
|
+
beforeEach: v4_namespaceObject.z.array(validatedStepSchema),
|
|
357
|
+
steps: v4_namespaceObject.z.array(validatedStepSchema),
|
|
358
|
+
afterEach: v4_namespaceObject.z.array(validatedStepSchema),
|
|
359
|
+
executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
360
|
+
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
361
|
+
reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional(),
|
|
362
|
+
startedAt: timestampSchema,
|
|
363
|
+
endedAt: timestampSchema,
|
|
364
|
+
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
365
|
+
});
|
|
366
|
+
const notRunReasonSchema = v4_namespaceObject.z["enum"]([
|
|
367
|
+
'document-start-failed',
|
|
368
|
+
'project-preflight-failed',
|
|
369
|
+
'project-setup-failed',
|
|
370
|
+
'interrupted',
|
|
371
|
+
'bail',
|
|
372
|
+
'fatal-error',
|
|
373
|
+
'executor-failed',
|
|
374
|
+
'cleanup-pending',
|
|
375
|
+
'cleanup-failed',
|
|
376
|
+
'operation-pending'
|
|
377
|
+
]);
|
|
378
|
+
const resultSchema = v4_namespaceObject.z.strictObject({
|
|
379
|
+
case: v4_namespaceObject.z.strictObject({
|
|
380
|
+
documentId: v4_namespaceObject.z.string().min(1),
|
|
381
|
+
caseId: v4_namespaceObject.z.string().min(1),
|
|
382
|
+
projectName: v4_namespaceObject.z.string().min(1),
|
|
383
|
+
name: v4_namespaceObject.z.string(),
|
|
384
|
+
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
385
|
+
caseIndex: v4_namespaceObject.z.number().int().nonnegative(),
|
|
386
|
+
status: v4_namespaceObject.z["enum"]([
|
|
387
|
+
'success',
|
|
388
|
+
'failed',
|
|
389
|
+
'not-run'
|
|
390
|
+
]),
|
|
391
|
+
run: attemptSchema.optional(),
|
|
392
|
+
attempts: v4_namespaceObject.z.array(attemptSchema).optional(),
|
|
393
|
+
notRunReason: notRunReasonSchema.optional()
|
|
394
|
+
}),
|
|
395
|
+
document: v4_namespaceObject.z.strictObject({
|
|
396
|
+
...runtimeResultFields,
|
|
397
|
+
plan: documentPlanSchema.optional(),
|
|
398
|
+
attemptIndex: v4_namespaceObject.z.number().int().nonnegative().optional(),
|
|
399
|
+
hostErrors: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
400
|
+
phase: v4_namespaceObject.z["enum"]([
|
|
401
|
+
'setup',
|
|
402
|
+
'execution',
|
|
403
|
+
'cleanup',
|
|
404
|
+
'report',
|
|
405
|
+
'publication',
|
|
406
|
+
'observer'
|
|
407
|
+
]),
|
|
408
|
+
error: reportErrorSchema
|
|
409
|
+
})).optional(),
|
|
410
|
+
documentId: v4_namespaceObject.z.string().min(1),
|
|
411
|
+
documentRunId: v4_namespaceObject.z.string().min(1),
|
|
412
|
+
projectId: v4_namespaceObject.z.string().min(1),
|
|
413
|
+
projectName: v4_namespaceObject.z.string().min(1),
|
|
414
|
+
sourcePath: v4_namespaceObject.z.string().min(1),
|
|
415
|
+
status: v4_namespaceObject.z["enum"]([
|
|
416
|
+
'success',
|
|
417
|
+
'failed'
|
|
418
|
+
]),
|
|
419
|
+
startedAt: timestampSchema,
|
|
420
|
+
endedAt: timestampSchema,
|
|
421
|
+
durationMs: v4_namespaceObject.z.number().nonnegative().finite(),
|
|
422
|
+
beforeAll: v4_namespaceObject.z.array(validatedStepSchema),
|
|
423
|
+
afterAll: v4_namespaceObject.z.array(validatedStepSchema),
|
|
424
|
+
executionErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
425
|
+
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
426
|
+
reportRefs: v4_namespaceObject.z.array(v4_namespaceObject.z.string().min(1)).optional()
|
|
427
|
+
}).optional(),
|
|
428
|
+
lifecycle: v4_namespaceObject.z.strictObject({
|
|
429
|
+
cleanup: cleanupSchema.optional(),
|
|
430
|
+
pendingOperations: pendingOperationsSchema.optional(),
|
|
431
|
+
projectName: v4_namespaceObject.z.string().min(1),
|
|
432
|
+
status: v4_namespaceObject.z["enum"]([
|
|
433
|
+
'success',
|
|
434
|
+
'failed'
|
|
435
|
+
]),
|
|
436
|
+
setupError: errorSchema.optional(),
|
|
437
|
+
teardownErrors: v4_namespaceObject.z.array(errorSchema).optional(),
|
|
438
|
+
startedAt: timestampSchema,
|
|
439
|
+
endedAt: timestampSchema,
|
|
440
|
+
durationMs: v4_namespaceObject.z.number().nonnegative().finite()
|
|
441
|
+
}).optional(),
|
|
442
|
+
artifacts: v4_namespaceObject.z.array(v4_namespaceObject.z.strictObject({
|
|
443
|
+
name: v4_namespaceObject.z.string().min(1),
|
|
444
|
+
uri: v4_namespaceObject.z.string().min(1),
|
|
445
|
+
mediaType: v4_namespaceObject.z.string().min(1).optional()
|
|
446
|
+
})).optional(),
|
|
447
|
+
metadata: v4_namespaceObject.z.record(v4_namespaceObject.z.string(), v4_namespaceObject.z.union([
|
|
448
|
+
v4_namespaceObject.z.string(),
|
|
449
|
+
v4_namespaceObject.z.number().finite(),
|
|
450
|
+
v4_namespaceObject.z.boolean(),
|
|
451
|
+
v4_namespaceObject.z["null"]()
|
|
452
|
+
])).optional()
|
|
453
|
+
});
|
|
454
|
+
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);
|
|
455
|
+
const hasUnfinishedWork = (result)=>void 0 !== result.executionStatus && 'success' !== result.executionStatus || void 0 !== result.cleanup && ![
|
|
456
|
+
'completed',
|
|
457
|
+
'not-required'
|
|
458
|
+
].includes(result.cleanup.status) || (result.pendingOperations?.length ?? 0) > 0;
|
|
459
|
+
function assertTestCaseTaskRunResult(task, value) {
|
|
460
|
+
const parsed = resultSchema.safeParse(value);
|
|
461
|
+
if (!parsed.success) throw new TypeError(`Executor returned invalid transport data for ${task.caseId}: ${v4_namespaceObject.z.prettifyError(parsed.error)}`);
|
|
462
|
+
const outcome = parsed.data.case;
|
|
463
|
+
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}.`);
|
|
464
|
+
const attempts = outcome.attempts ?? [];
|
|
465
|
+
for (const attempt of attempts){
|
|
466
|
+
const steps = [
|
|
467
|
+
...attempt.beforeEach,
|
|
468
|
+
...attempt.steps,
|
|
469
|
+
...attempt.afterEach
|
|
470
|
+
];
|
|
471
|
+
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}.`);
|
|
316
472
|
}
|
|
317
|
-
|
|
473
|
+
if (new Set(attempts.map((attempt)=>attempt.runId)).size !== attempts.length) throw new TypeError(`Executor returned duplicate attempt IDs for ${task.caseId}.`);
|
|
474
|
+
if (attempts.some((attempt)=>!attemptMatchesTask(task, attempt))) throw new TypeError(`Executor returned invalid attempts for ${task.caseId}.`);
|
|
475
|
+
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}.`);
|
|
476
|
+
const finalAttempt = attempts.at(-1);
|
|
477
|
+
if ('not-run' === outcome.status) {
|
|
478
|
+
if (outcome.run || attempts.length || !outcome.notRunReason) throw new TypeError(`Executor returned an inconsistent not-run result for ${task.caseId}.`);
|
|
479
|
+
} 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}.`);
|
|
480
|
+
const reportRefs = [
|
|
481
|
+
...attempts.flatMap((attempt)=>attempt.reportRefs ?? []),
|
|
482
|
+
...parsed.data.document?.reportRefs ?? []
|
|
483
|
+
];
|
|
484
|
+
if (reportRefs.some((reference)=>!isSafeResultPathSegment(reference))) throw new TypeError(`Executor returned an invalid report reference for ${task.caseId}.`);
|
|
485
|
+
const document = parsed.data.document;
|
|
486
|
+
if (document?.status === 'success' && (hasUnfinishedWork(document) || (document.hostErrors?.length ?? 0) > 0 || (document.executionErrors?.length ?? 0) > 0 || [
|
|
487
|
+
...document.beforeAll,
|
|
488
|
+
...document.afterAll
|
|
489
|
+
].some((step)=>'success' !== step.status) || (document.teardownErrors?.length ?? 0) > 0)) throw new TypeError(`Executor returned an inconsistent successful document for ${task.caseId}.`);
|
|
490
|
+
const lifecycle = parsed.data.lifecycle;
|
|
491
|
+
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}.`);
|
|
492
|
+
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}.`);
|
|
493
|
+
if (parsed.data.lifecycle && parsed.data.lifecycle.projectName !== task.projectName) throw new Error(`Executor returned a lifecycle that does not match task ${task.caseId}.`);
|
|
318
494
|
}
|
|
319
495
|
const external_node_fs_namespaceObject = require("node:fs");
|
|
496
|
+
const external_node_path_namespaceObject = require("node:path");
|
|
497
|
+
const test_runner_namespaceObject = require("@midscene/core/internal/test-runner");
|
|
320
498
|
const external_tinyglobby_namespaceObject = require("tinyglobby");
|
|
321
499
|
const external_node_crypto_namespaceObject = require("node:crypto");
|
|
322
500
|
const createCaseInvocationId = (projectId, sourcePath, caseIndex, invocationIndex = 0)=>(0, external_node_crypto_namespaceObject.createHash)('sha256').update(JSON.stringify([
|
|
@@ -442,7 +620,7 @@ const resolveWorkflowVariables = (value, options)=>{
|
|
|
442
620
|
]));
|
|
443
621
|
return value;
|
|
444
622
|
};
|
|
445
|
-
function
|
|
623
|
+
function collect_define_property(obj, key, value) {
|
|
446
624
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
447
625
|
value: value,
|
|
448
626
|
enumerable: true,
|
|
@@ -459,11 +637,11 @@ class NativeWorkflowFormatError extends test_runner_namespaceObject.WorkflowPars
|
|
|
459
637
|
sourcePath,
|
|
460
638
|
actualFormat,
|
|
461
639
|
expectedFormat: 'native'
|
|
462
|
-
}),
|
|
640
|
+
}), collect_define_property(this, "actualFormat", void 0);
|
|
463
641
|
this.actualFormat = actualFormat;
|
|
464
642
|
}
|
|
465
643
|
}
|
|
466
|
-
const
|
|
644
|
+
const rejectUnknownKeys = (value, allowed, label)=>{
|
|
467
645
|
const allowedSet = new Set(allowed);
|
|
468
646
|
const unknown = Object.keys(value).filter((key)=>!allowedSet.has(key));
|
|
469
647
|
if (unknown.length) throw new test_runner_namespaceObject.WorkflowParseError(`${label} has unsupported field "${unknown[0]}".`, {
|
|
@@ -485,7 +663,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
485
663
|
if (!isMapping(parsed)) throw new test_runner_namespaceObject.WorkflowParseError('Workflow document must be a mapping.');
|
|
486
664
|
const format = (0, test_runner_namespaceObject.classifyWorkflowDocumentFormat)(parsed);
|
|
487
665
|
if ('legacy' === format || 'legacy-batch' === format || 'mixed' === format) throw new NativeWorkflowFormatError(format, source.sourcePath);
|
|
488
|
-
|
|
666
|
+
rejectUnknownKeys(parsed, [
|
|
489
667
|
'beforeAll',
|
|
490
668
|
'beforeEach',
|
|
491
669
|
'cases',
|
|
@@ -538,11 +716,16 @@ function collectWorkflowDocument(source, options) {
|
|
|
538
716
|
if (!isMapping(definition)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} must be a mapping.`, {
|
|
539
717
|
caseIndex
|
|
540
718
|
});
|
|
541
|
-
|
|
719
|
+
rejectUnknownKeys(definition, [
|
|
720
|
+
'id',
|
|
542
721
|
'name',
|
|
543
722
|
'tags',
|
|
723
|
+
'resources',
|
|
544
724
|
'steps'
|
|
545
725
|
], `Case ${caseIndex + 1}`);
|
|
726
|
+
if (void 0 !== definition.id && !isSafeResultPathSegment(definition.id)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} id must start with an ASCII letter or number, contain only letters, numbers, dots, underscores, or hyphens, and be at most 128 characters.`, {
|
|
727
|
+
caseIndex
|
|
728
|
+
});
|
|
546
729
|
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
730
|
caseIndex
|
|
548
731
|
});
|
|
@@ -553,6 +736,13 @@ function collectWorkflowDocument(source, options) {
|
|
|
553
736
|
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
737
|
caseIndex
|
|
555
738
|
});
|
|
739
|
+
const resources = definition.resources ?? [];
|
|
740
|
+
if (!Array.isArray(resources) || resources.some((resource)=>'string' != typeof resource || 0 === resource.trim().length)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} resources must be an array of non-empty strings.`, {
|
|
741
|
+
caseIndex
|
|
742
|
+
});
|
|
743
|
+
if (new Set(resources).size !== resources.length) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} resources must not contain duplicates.`, {
|
|
744
|
+
caseIndex
|
|
745
|
+
});
|
|
556
746
|
const steps = normalizeSteps(definition.steps, options.resolveNode).map((normalized, stepIndex)=>{
|
|
557
747
|
if (!options.resolveNode(normalized.node)) throw new test_runner_namespaceObject.WorkflowParseError(`Case ${caseIndex + 1} step ${stepIndex + 1} references unknown node "${normalized.node}".`, {
|
|
558
748
|
caseIndex,
|
|
@@ -561,7 +751,7 @@ function collectWorkflowDocument(source, options) {
|
|
|
561
751
|
});
|
|
562
752
|
return resolveStepVariables(normalized, 'steps', stepIndex, caseIndex);
|
|
563
753
|
});
|
|
564
|
-
const caseId = createCaseId(source.projectId, sourcePath, caseIndex);
|
|
754
|
+
const caseId = 'string' == typeof definition.id ? definition.id : createCaseId(source.projectId, sourcePath, caseIndex);
|
|
565
755
|
if (ids.has(caseId)) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision: ${caseId}.`, {
|
|
566
756
|
caseId
|
|
567
757
|
});
|
|
@@ -572,8 +762,14 @@ function collectWorkflowDocument(source, options) {
|
|
|
572
762
|
sourcePath,
|
|
573
763
|
caseIndex,
|
|
574
764
|
definition: {
|
|
765
|
+
...'string' == typeof definition.id ? {
|
|
766
|
+
id: definition.id
|
|
767
|
+
} : {},
|
|
575
768
|
name: definition.name,
|
|
576
769
|
tags: tags,
|
|
770
|
+
...resources.length > 0 ? {
|
|
771
|
+
resources: resources
|
|
772
|
+
} : {},
|
|
577
773
|
steps
|
|
578
774
|
}
|
|
579
775
|
};
|
|
@@ -714,6 +910,38 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
714
910
|
...outcome.notRunReason ? {
|
|
715
911
|
notRunReason: outcome.notRunReason
|
|
716
912
|
} : {},
|
|
913
|
+
...outcome.execution ? {
|
|
914
|
+
execution: {
|
|
915
|
+
executor: outcome.execution.executor,
|
|
916
|
+
resources: outcome.execution.resources,
|
|
917
|
+
...void 0 === outcome.execution.attempts ? {} : {
|
|
918
|
+
attempts: outcome.execution.attempts
|
|
919
|
+
},
|
|
920
|
+
...outcome.execution.failure ? {
|
|
921
|
+
failure: outcome.execution.failure
|
|
922
|
+
} : {},
|
|
923
|
+
...outcome.execution.artifacts ? {
|
|
924
|
+
artifacts: outcome.execution.artifacts
|
|
925
|
+
} : {},
|
|
926
|
+
...outcome.execution.metadata ? {
|
|
927
|
+
metadata: outcome.execution.metadata
|
|
928
|
+
} : {},
|
|
929
|
+
...outcome.execution.lifecycle ? {
|
|
930
|
+
lifecycle: {
|
|
931
|
+
status: outcome.execution.lifecycle.status,
|
|
932
|
+
startedAt: outcome.execution.lifecycle.startedAt,
|
|
933
|
+
endedAt: outcome.execution.lifecycle.endedAt,
|
|
934
|
+
durationMs: outcome.execution.lifecycle.durationMs,
|
|
935
|
+
...outcome.execution.lifecycle.setupError ? {
|
|
936
|
+
setupError: errorJson(outcome.execution.lifecycle.setupError)
|
|
937
|
+
} : {},
|
|
938
|
+
...outcome.execution.lifecycle.teardownErrors ? {
|
|
939
|
+
teardownErrors: outcome.execution.lifecycle.teardownErrors.map(errorJson)
|
|
940
|
+
} : {}
|
|
941
|
+
}
|
|
942
|
+
} : {}
|
|
943
|
+
}
|
|
944
|
+
} : {},
|
|
717
945
|
...outcome.attempts ? {
|
|
718
946
|
attempts: outcome.attempts.map((attempt)=>({
|
|
719
947
|
attemptId: attempt.runId,
|
|
@@ -770,6 +998,308 @@ const writeTestProjectRunResult = (options)=>{
|
|
|
770
998
|
}))
|
|
771
999
|
});
|
|
772
1000
|
};
|
|
1001
|
+
const external_node_url_namespaceObject = require("node:url");
|
|
1002
|
+
const api_namespaceObject = require("tsx/cjs/api");
|
|
1003
|
+
const NodeRegistry = test_runner_namespaceObject.NodeRegistry;
|
|
1004
|
+
const projectIdFromIndex = (index)=>`project-${index}`;
|
|
1005
|
+
const defineTestProject = (definition)=>definition;
|
|
1006
|
+
const defineProjectSetup = (definition)=>definition;
|
|
1007
|
+
const getDefaultExport = (loaded, absolutePath)=>{
|
|
1008
|
+
if ('object' == typeof loaded && null !== loaded && 'default' in loaded) {
|
|
1009
|
+
const defaultExport = loaded.default;
|
|
1010
|
+
if ('object' == typeof defaultExport && null !== defaultExport && '__esModule' in defaultExport) {
|
|
1011
|
+
if ('default' in defaultExport) return defaultExport.default;
|
|
1012
|
+
throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
|
|
1013
|
+
}
|
|
1014
|
+
return defaultExport;
|
|
1015
|
+
}
|
|
1016
|
+
throw new TypeError(`Midscene config "${absolutePath}" must have a default export.`);
|
|
1017
|
+
};
|
|
1018
|
+
const isRecord = (value)=>'object' == typeof value && null !== value && !Array.isArray(value);
|
|
1019
|
+
const test_project_rejectUnknownKeys = (value, allowedKeys, label)=>{
|
|
1020
|
+
const allowed = new Set(allowedKeys);
|
|
1021
|
+
const unknown = Object.keys(value).find((key)=>!allowed.has(key));
|
|
1022
|
+
if (unknown) throw new TypeError(`Midscene config ${label}.${unknown} is not supported.`);
|
|
1023
|
+
};
|
|
1024
|
+
const isAbsolutePattern = (pattern)=>(0, external_node_path_namespaceObject.isAbsolute)(pattern) || /^[A-Za-z]:[\\/]/.test(pattern) || pattern.startsWith('\\\\');
|
|
1025
|
+
const validatePatterns = (value, field, label = 'files', allowEmptyInclude = false)=>{
|
|
1026
|
+
if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
|
|
1027
|
+
if ('include' === field && 0 === value.length && !allowEmptyInclude) throw new TypeError(`Midscene config ${label}.include must be a non-empty array.`);
|
|
1028
|
+
return value.map((pattern, index)=>{
|
|
1029
|
+
if ('string' != typeof pattern || 0 === pattern.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
|
|
1030
|
+
if (isAbsolutePattern(pattern)) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be relative to the project root.`);
|
|
1031
|
+
if (pattern.split(/[\\/]/).includes('..')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must not contain a ".." path segment.`);
|
|
1032
|
+
if (pattern.includes('\\')) throw new TypeError(`Midscene config ${label}.${field}[${index}] must use POSIX path separators (/).`);
|
|
1033
|
+
if (pattern.startsWith('!')) {
|
|
1034
|
+
if ('include' === field) throw new TypeError(`Midscene config ${label}.include[${index}] must not be a negated pattern. Use files.exclude instead.`);
|
|
1035
|
+
throw new TypeError(`Midscene config ${label}.exclude[${index}] must not be a negated pattern.`);
|
|
1036
|
+
}
|
|
1037
|
+
return pattern;
|
|
1038
|
+
});
|
|
1039
|
+
};
|
|
1040
|
+
const validateTestFileSelection = (value, label = 'files')=>{
|
|
1041
|
+
if (void 0 === value) return;
|
|
1042
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1043
|
+
test_project_rejectUnknownKeys(value, [
|
|
1044
|
+
'include',
|
|
1045
|
+
'exclude'
|
|
1046
|
+
], label);
|
|
1047
|
+
const include = validatePatterns(value.include, 'include', label);
|
|
1048
|
+
const exclude = void 0 === value.exclude ? void 0 : validatePatterns(value.exclude, 'exclude', label);
|
|
1049
|
+
return Object.freeze({
|
|
1050
|
+
include: Object.freeze(include),
|
|
1051
|
+
...exclude ? {
|
|
1052
|
+
exclude: Object.freeze(exclude)
|
|
1053
|
+
} : {}
|
|
1054
|
+
});
|
|
1055
|
+
};
|
|
1056
|
+
const validateTagList = (value, field, label)=>{
|
|
1057
|
+
if (void 0 === value) return [];
|
|
1058
|
+
if (!Array.isArray(value)) throw new TypeError(`Midscene config ${label}.${field} must be an array.`);
|
|
1059
|
+
return value.map((tag, index)=>{
|
|
1060
|
+
if ('string' != typeof tag || 0 === tag.trim().length) throw new TypeError(`Midscene config ${label}.${field}[${index}] must be a non-empty string.`);
|
|
1061
|
+
return tag;
|
|
1062
|
+
});
|
|
1063
|
+
};
|
|
1064
|
+
const validateTagSelection = (value, label)=>{
|
|
1065
|
+
if (void 0 === value) return Object.freeze({
|
|
1066
|
+
include: [],
|
|
1067
|
+
exclude: []
|
|
1068
|
+
});
|
|
1069
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1070
|
+
return Object.freeze({
|
|
1071
|
+
include: Object.freeze(validateTagList(value.include, 'include', label)),
|
|
1072
|
+
exclude: Object.freeze(validateTagList(value.exclude, 'exclude', label))
|
|
1073
|
+
});
|
|
1074
|
+
};
|
|
1075
|
+
function assertJsonValue(value, path, seen) {
|
|
1076
|
+
if (null === value || 'string' == typeof value || 'boolean' == typeof value) return;
|
|
1077
|
+
if ('number' == typeof value) {
|
|
1078
|
+
if (Number.isFinite(value)) return;
|
|
1079
|
+
throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1080
|
+
}
|
|
1081
|
+
if ('object' != typeof value || void 0 === value) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1082
|
+
if (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype && null !== Object.getPrototypeOf(value)) throw new TypeError(`Midscene config ${path} must be JSON-compatible.`);
|
|
1083
|
+
if (seen.has(value)) throw new TypeError(`Midscene config ${path} must not contain cycles.`);
|
|
1084
|
+
seen.add(value);
|
|
1085
|
+
if (Array.isArray(value)) value.forEach((item, index)=>assertJsonValue(item, `${path}[${index}]`, seen));
|
|
1086
|
+
else for (const [key, child] of Object.entries(value))assertJsonValue(child, `${path}.${key}`, seen);
|
|
1087
|
+
seen.delete(value);
|
|
1088
|
+
}
|
|
1089
|
+
const deepFreezeJson = (value)=>{
|
|
1090
|
+
if ('object' == typeof value && null !== value) {
|
|
1091
|
+
for (const child of Object.values(value))deepFreezeJson(child);
|
|
1092
|
+
Object.freeze(value);
|
|
1093
|
+
}
|
|
1094
|
+
return value;
|
|
1095
|
+
};
|
|
1096
|
+
const validateVariables = (value, label)=>{
|
|
1097
|
+
if (void 0 === value) return Object.freeze({});
|
|
1098
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1099
|
+
assertJsonValue(value, label, new Set());
|
|
1100
|
+
return deepFreezeJson(value);
|
|
1101
|
+
};
|
|
1102
|
+
const validateSetup = (value, label)=>{
|
|
1103
|
+
if (void 0 === value) return;
|
|
1104
|
+
if (!isRecord(value)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1105
|
+
test_project_rejectUnknownKeys(value, [
|
|
1106
|
+
'name',
|
|
1107
|
+
'setup'
|
|
1108
|
+
], label);
|
|
1109
|
+
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
1110
|
+
if ('function' != typeof value.setup) throw new TypeError(`Midscene config ${label}.setup must be a function.`);
|
|
1111
|
+
return value;
|
|
1112
|
+
};
|
|
1113
|
+
const validatePositiveInteger = (value, fallback, label)=>{
|
|
1114
|
+
if (void 0 === value) return fallback;
|
|
1115
|
+
if (!Number.isInteger(value) || value <= 0) throw new TypeError(`Midscene config ${label} must be a positive integer.`);
|
|
1116
|
+
return value;
|
|
1117
|
+
};
|
|
1118
|
+
const validateNonNegativeInteger = (value, fallback, label)=>{
|
|
1119
|
+
if (void 0 === value) return fallback;
|
|
1120
|
+
if (!Number.isInteger(value) || value < 0) throw new TypeError(`Midscene config ${label} must be a non-negative integer.`);
|
|
1121
|
+
return value;
|
|
1122
|
+
};
|
|
1123
|
+
const validateExecutionProjects = (value, defaultSetup, globalNodes)=>{
|
|
1124
|
+
if (void 0 === value) {
|
|
1125
|
+
const setup = validateSetup(defaultSetup, 'setup');
|
|
1126
|
+
return {
|
|
1127
|
+
hasExplicitProjects: false,
|
|
1128
|
+
projects: Object.freeze([
|
|
1129
|
+
Object.freeze({
|
|
1130
|
+
projectId: projectIdFromIndex(0),
|
|
1131
|
+
name: 'default',
|
|
1132
|
+
...setup ? {
|
|
1133
|
+
setup
|
|
1134
|
+
} : {},
|
|
1135
|
+
tags: Object.freeze({
|
|
1136
|
+
include: [],
|
|
1137
|
+
exclude: []
|
|
1138
|
+
}),
|
|
1139
|
+
retry: 0,
|
|
1140
|
+
variables: Object.freeze({}),
|
|
1141
|
+
nodes: new NodeRegistry(globalNodes.definitions())
|
|
1142
|
+
})
|
|
1143
|
+
])
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
if (void 0 !== defaultSetup) throw new TypeError('Midscene config setup cannot be used together with projects. Move setup to projects[].setup.');
|
|
1147
|
+
if (!Array.isArray(value) || 0 === value.length) throw new TypeError('Midscene config projects must be a non-empty array when declared.');
|
|
1148
|
+
const names = new Set();
|
|
1149
|
+
const projects = value.map((candidate, index)=>{
|
|
1150
|
+
const label = `projects[${index}]`;
|
|
1151
|
+
if (!isRecord(candidate)) throw new TypeError(`Midscene config ${label} must be an object.`);
|
|
1152
|
+
test_project_rejectUnknownKeys(candidate, [
|
|
1153
|
+
'name',
|
|
1154
|
+
'setup',
|
|
1155
|
+
'nodes',
|
|
1156
|
+
'files',
|
|
1157
|
+
'tags',
|
|
1158
|
+
'retry',
|
|
1159
|
+
'variables'
|
|
1160
|
+
], label);
|
|
1161
|
+
if ('string' != typeof candidate.name || 0 === candidate.name.trim().length) throw new TypeError(`Midscene config ${label}.name must be non-empty.`);
|
|
1162
|
+
if (names.has(candidate.name)) throw new TypeError(`Midscene config project name "${candidate.name}" must be unique.`);
|
|
1163
|
+
names.add(candidate.name);
|
|
1164
|
+
const files = validateTestFileSelection(candidate.files, `${label}.files`);
|
|
1165
|
+
if (void 0 !== candidate.nodes && !Array.isArray(candidate.nodes)) throw new TypeError(`Midscene config ${label}.nodes must be an array.`);
|
|
1166
|
+
const localNodes = new NodeRegistry(candidate.nodes);
|
|
1167
|
+
const nodes = new NodeRegistry([
|
|
1168
|
+
...globalNodes.definitions().filter((node)=>!localNodes.has(node.name)),
|
|
1169
|
+
...localNodes.definitions()
|
|
1170
|
+
]);
|
|
1171
|
+
return Object.freeze({
|
|
1172
|
+
projectId: projectIdFromIndex(index),
|
|
1173
|
+
name: candidate.name,
|
|
1174
|
+
nodes,
|
|
1175
|
+
...files ? {
|
|
1176
|
+
files
|
|
1177
|
+
} : {},
|
|
1178
|
+
tags: validateTagSelection(candidate.tags, `${label}.tags`),
|
|
1179
|
+
retry: validateNonNegativeInteger(candidate.retry, 0, `${label}.retry`),
|
|
1180
|
+
variables: validateVariables(candidate.variables, `${label}.variables`),
|
|
1181
|
+
...void 0 === candidate.setup ? {} : {
|
|
1182
|
+
setup: validateSetup(candidate.setup, `${label}.setup`)
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
projects: Object.freeze(projects),
|
|
1188
|
+
hasExplicitProjects: true
|
|
1189
|
+
};
|
|
1190
|
+
};
|
|
1191
|
+
const validateTestOptions = (value)=>{
|
|
1192
|
+
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config test must be an object.');
|
|
1193
|
+
const candidate = value ?? {};
|
|
1194
|
+
const executionUnit = candidate.executionUnit ?? 'project';
|
|
1195
|
+
if ('project' !== executionUnit && 'case' !== executionUnit) throw new TypeError('Midscene config test.executionUnit must be "project" or "case".');
|
|
1196
|
+
return Object.freeze({
|
|
1197
|
+
maxConcurrency: validatePositiveInteger(candidate.maxConcurrency, 1, 'test.maxConcurrency'),
|
|
1198
|
+
bail: validateNonNegativeInteger(candidate.bail, 0, 'test.bail'),
|
|
1199
|
+
testTimeout: validatePositiveInteger(candidate.testTimeout, 120000, 'test.testTimeout'),
|
|
1200
|
+
executionUnit,
|
|
1201
|
+
executorRetry: validateNonNegativeInteger(candidate.executorRetry, 0, 'test.executorRetry')
|
|
1202
|
+
});
|
|
1203
|
+
};
|
|
1204
|
+
const validateTestExecutor = (value)=>{
|
|
1205
|
+
if (void 0 === value) return;
|
|
1206
|
+
if (!isRecord(value)) throw new TypeError('Midscene config executor must be an object.');
|
|
1207
|
+
test_project_rejectUnknownKeys(value, [
|
|
1208
|
+
'name',
|
|
1209
|
+
'execute'
|
|
1210
|
+
], 'executor');
|
|
1211
|
+
if ('string' != typeof value.name || 0 === value.name.trim().length) throw new TypeError('Midscene config executor.name must be non-empty.');
|
|
1212
|
+
if ('function' != typeof value.execute) throw new TypeError('Midscene config executor.execute must be a function.');
|
|
1213
|
+
return Object.freeze(value);
|
|
1214
|
+
};
|
|
1215
|
+
const validateOutputPath = (value, fallback, label)=>{
|
|
1216
|
+
if (void 0 === value) return fallback;
|
|
1217
|
+
if ('string' != typeof value || 0 === value.trim().length) throw new TypeError(`Midscene config ${label} must be non-empty.`);
|
|
1218
|
+
return value;
|
|
1219
|
+
};
|
|
1220
|
+
const validateOutput = (value)=>{
|
|
1221
|
+
if (void 0 !== value && !isRecord(value)) throw new TypeError('Midscene config output must be an object.');
|
|
1222
|
+
const candidate = value ?? {};
|
|
1223
|
+
test_project_rejectUnknownKeys(candidate, [
|
|
1224
|
+
'reportDir'
|
|
1225
|
+
], 'output');
|
|
1226
|
+
return Object.freeze({
|
|
1227
|
+
reportDir: validateOutputPath(candidate.reportDir, './midscene_run/report', 'output.reportDir')
|
|
1228
|
+
});
|
|
1229
|
+
};
|
|
1230
|
+
const validateTestProjectDefinition = (definition)=>{
|
|
1231
|
+
if (!isRecord(definition)) throw new TypeError('Midscene config must default export an object.');
|
|
1232
|
+
if (void 0 !== definition.nodes && !Array.isArray(definition.nodes)) throw new TypeError('Midscene config nodes must be an array.');
|
|
1233
|
+
if ('setupWorkflow' in definition) throw new TypeError('Midscene config setupWorkflow is no longer supported. Use setup or projects[].setup instead.');
|
|
1234
|
+
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.');
|
|
1235
|
+
if ('root' in definition) throw new TypeError('Midscene config root is not supported. Pass the Test Project directory to midscene-test instead.');
|
|
1236
|
+
if ('files' in definition) throw new TypeError('Midscene config files is not supported at the root. Move it to projects[].files.');
|
|
1237
|
+
if ('testRunner' in definition) throw new TypeError('Midscene config testRunner is not supported. Rename it to test.');
|
|
1238
|
+
test_project_rejectUnknownKeys(definition, [
|
|
1239
|
+
'setup',
|
|
1240
|
+
'projects',
|
|
1241
|
+
'test',
|
|
1242
|
+
'output',
|
|
1243
|
+
'nodes',
|
|
1244
|
+
'executor'
|
|
1245
|
+
], 'root');
|
|
1246
|
+
const nodes = new NodeRegistry(definition.nodes);
|
|
1247
|
+
const resolvedProjects = validateExecutionProjects(definition.projects, definition.setup, nodes);
|
|
1248
|
+
const test = validateTestOptions(definition.test);
|
|
1249
|
+
const executor = validateTestExecutor(definition.executor);
|
|
1250
|
+
if (executor && 'case' !== test.executionUnit) throw new TypeError('Midscene config executor requires test.executionUnit to be "case".');
|
|
1251
|
+
return {
|
|
1252
|
+
...resolvedProjects,
|
|
1253
|
+
test,
|
|
1254
|
+
output: validateOutput(definition.output),
|
|
1255
|
+
nodes,
|
|
1256
|
+
...executor ? {
|
|
1257
|
+
executor
|
|
1258
|
+
} : {},
|
|
1259
|
+
resolveNode: (name)=>nodes.get(name)
|
|
1260
|
+
};
|
|
1261
|
+
};
|
|
1262
|
+
const assertSupportedConfig = (absolutePath)=>{
|
|
1263
|
+
if (!absolutePath.endsWith('.ts') && !absolutePath.endsWith('.mjs')) {
|
|
1264
|
+
const extension = absolutePath.match(/(\.[^./\\]+)$/)?.[1] ?? '(none)';
|
|
1265
|
+
throw new TypeError(`Unsupported Midscene config extension: ${extension}. Supported extensions: .ts, .mjs.`);
|
|
1266
|
+
}
|
|
1267
|
+
return absolutePath.endsWith('.mjs') ? '.mjs' : '.ts';
|
|
1268
|
+
};
|
|
1269
|
+
const canRetryWithCjsLoader = (error)=>{
|
|
1270
|
+
if (!(error instanceof Error)) return false;
|
|
1271
|
+
const errorCode = error.code;
|
|
1272
|
+
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);
|
|
1273
|
+
};
|
|
1274
|
+
async function loadTestProject(configPath) {
|
|
1275
|
+
if (!configPath) return validateTestProjectDefinition({
|
|
1276
|
+
nodes: []
|
|
1277
|
+
});
|
|
1278
|
+
const absolutePath = (0, external_node_path_namespaceObject.resolve)(configPath);
|
|
1279
|
+
const extension = assertSupportedConfig(absolutePath);
|
|
1280
|
+
let loaded;
|
|
1281
|
+
try {
|
|
1282
|
+
if ('.mjs' === extension) loaded = await import((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href);
|
|
1283
|
+
else {
|
|
1284
|
+
const { tsImport } = await import("tsx/esm/api");
|
|
1285
|
+
loaded = await tsImport((0, external_node_url_namespaceObject.pathToFileURL)(absolutePath).href, {
|
|
1286
|
+
parentURL: (0, external_node_url_namespaceObject.pathToFileURL)(`${(0, external_node_path_namespaceObject.dirname)(absolutePath)}${external_node_path_namespaceObject.sep}`).href,
|
|
1287
|
+
tsconfig: false
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
try {
|
|
1292
|
+
if ('.mjs' === extension || !canRetryWithCjsLoader(error)) throw error;
|
|
1293
|
+
loaded = (0, api_namespaceObject.require)(absolutePath, (0, external_node_url_namespaceObject.pathToFileURL)(absolutePath));
|
|
1294
|
+
} catch (fallbackError) {
|
|
1295
|
+
const message = fallbackError instanceof Error ? `: ${fallbackError.message}` : '';
|
|
1296
|
+
throw new Error(`Failed to load Midscene config "${absolutePath}"${message}`, {
|
|
1297
|
+
cause: fallbackError
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
return validateTestProjectDefinition(getDefaultExport(loaded, absolutePath));
|
|
1302
|
+
}
|
|
773
1303
|
const CONFIG_NAMES = [
|
|
774
1304
|
'midscene.config.ts',
|
|
775
1305
|
'midscene.config.mjs'
|
|
@@ -791,6 +1321,27 @@ const DEFAULT_TEST_FILE_SELECTION = {
|
|
|
791
1321
|
]
|
|
792
1322
|
};
|
|
793
1323
|
const project_preparation_toPosix = (value)=>value.split(external_node_path_namespaceObject.sep).join('/');
|
|
1324
|
+
const discoverSelectedTestFiles = (projectRoot, paths)=>{
|
|
1325
|
+
const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
|
|
1326
|
+
const files = new Set();
|
|
1327
|
+
for (const requestedPath of paths){
|
|
1328
|
+
if ('string' != typeof requestedPath || 0 === requestedPath.trim().length) throw new TypeError('Test selection paths must be non-empty strings.');
|
|
1329
|
+
const absolutePath = (0, external_node_path_namespaceObject.resolve)(root, requestedPath);
|
|
1330
|
+
const relativePath = (0, external_node_path_namespaceObject.relative)(root, absolutePath);
|
|
1331
|
+
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}`);
|
|
1332
|
+
if (!(0, external_node_fs_namespaceObject.existsSync)(absolutePath)) throw new Error(`Test selection path does not exist: ${requestedPath}`);
|
|
1333
|
+
if ((0, external_node_fs_namespaceObject.statSync)(absolutePath).isDirectory()) for (const file of discoverTestFiles(absolutePath))files.add(file);
|
|
1334
|
+
else if (/\.ya?ml$/i.test(absolutePath)) files.add(absolutePath);
|
|
1335
|
+
else throw new Error(`Test selection file must be YAML: ${requestedPath}`);
|
|
1336
|
+
}
|
|
1337
|
+
return [
|
|
1338
|
+
...files
|
|
1339
|
+
].sort((a, b)=>{
|
|
1340
|
+
const relativeA = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, a));
|
|
1341
|
+
const relativeB = project_preparation_toPosix((0, external_node_path_namespaceObject.relative)(root, b));
|
|
1342
|
+
return relativeA < relativeB ? -1 : relativeA > relativeB ? 1 : 0;
|
|
1343
|
+
});
|
|
1344
|
+
};
|
|
794
1345
|
const discoverResolvedTestFiles = (projectRoot, selection)=>{
|
|
795
1346
|
const root = (0, external_node_path_namespaceObject.resolve)(projectRoot);
|
|
796
1347
|
const match = (patterns)=>(0, external_tinyglobby_namespaceObject.globSync)(patterns, {
|
|
@@ -850,8 +1401,14 @@ const matchesTags = (tags, selection)=>{
|
|
|
850
1401
|
if (selection.exclude.some((tag)=>tags.includes(tag))) return false;
|
|
851
1402
|
return 0 === selection.include.length || selection.include.some((tag)=>tags.includes(tag));
|
|
852
1403
|
};
|
|
853
|
-
const filterDocumentCases = (document, project)=>{
|
|
854
|
-
const cases = document.cases.filter((item)=>
|
|
1404
|
+
const filterDocumentCases = (document, project, selection)=>{
|
|
1405
|
+
const cases = document.cases.filter((item)=>{
|
|
1406
|
+
const tags = item.definition.tags ?? [];
|
|
1407
|
+
return matchesTags(tags, project.tags) && matchesTags(tags, {
|
|
1408
|
+
include: selection?.tags?.include ?? [],
|
|
1409
|
+
exclude: selection?.tags?.exclude ?? []
|
|
1410
|
+
}) && (!selection?.caseIds?.length || selection.caseIds.includes(item.caseId));
|
|
1411
|
+
});
|
|
855
1412
|
const filtered = document.cases.length - cases.length;
|
|
856
1413
|
return 0 === cases.length ? {
|
|
857
1414
|
filtered
|
|
@@ -876,7 +1433,9 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
876
1433
|
const platform = options.platform?.trim();
|
|
877
1434
|
const fileSelection = project.files ?? DEFAULT_TEST_FILE_SELECTION;
|
|
878
1435
|
const prerequisiteFile = options.prerequisiteFile ? (0, external_node_path_namespaceObject.resolve)(options.prerequisiteFile) : void 0;
|
|
879
|
-
const
|
|
1436
|
+
const projectFiles = options.files ?? discoverResolvedTestFiles(projectRoot, fileSelection);
|
|
1437
|
+
const selectedFiles = options.selection?.paths?.length ? new Set(discoverSelectedTestFiles(projectRoot, options.selection.paths)) : void 0;
|
|
1438
|
+
const mainFiles = selectedFiles ? projectFiles.filter((file)=>selectedFiles.has(file)) : projectFiles;
|
|
880
1439
|
const files = [
|
|
881
1440
|
...prerequisiteFile ? [
|
|
882
1441
|
prerequisiteFile
|
|
@@ -899,6 +1458,7 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
899
1458
|
const invocations = [];
|
|
900
1459
|
const plans = [];
|
|
901
1460
|
let filteredCaseCount = 0;
|
|
1461
|
+
const availableCaseIds = new Set();
|
|
902
1462
|
if (0 === sources.length) {
|
|
903
1463
|
const error = asCollectionError(project.projectId, project.name, '<project>', new test_runner_namespaceObject.WorkflowParseError(`No workflow YAML files found for project "${project.name}" in ${projectRoot}.`, {
|
|
904
1464
|
projectName: project.name,
|
|
@@ -929,10 +1489,16 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
929
1489
|
variables: project.variables,
|
|
930
1490
|
env: process.env
|
|
931
1491
|
});
|
|
1492
|
+
const duplicate = document.cases.find((item)=>availableCaseIds.has(item.caseId));
|
|
1493
|
+
if (duplicate) throw new test_runner_namespaceObject.WorkflowParseError(`Case id collision in project "${project.name}": ${duplicate.caseId}.`, {
|
|
1494
|
+
caseId: duplicate.caseId,
|
|
1495
|
+
projectName: project.name
|
|
1496
|
+
});
|
|
1497
|
+
for (const item of document.cases)availableCaseIds.add(item.caseId);
|
|
932
1498
|
const filtered = source.absolutePath === prerequisiteFile ? {
|
|
933
1499
|
document,
|
|
934
1500
|
filtered: 0
|
|
935
|
-
} : filterDocumentCases(document, project);
|
|
1501
|
+
} : filterDocumentCases(document, project, options.selection);
|
|
936
1502
|
const plan = (0, test_runner_namespaceObject.buildTestRunReportPlan)(document, {
|
|
937
1503
|
selectedCaseIds: filtered.document?.cases.map((item)=>item.caseId) ?? [],
|
|
938
1504
|
defaultTimeoutMs,
|
|
@@ -980,9 +1546,42 @@ async function prepareProject(project, projectRoot, runDir, defaultTimeoutMs, op
|
|
|
980
1546
|
plans,
|
|
981
1547
|
collectionErrors,
|
|
982
1548
|
selectedCaseCount: documents.reduce((total, document)=>total + document.cases.length, 0),
|
|
983
|
-
filteredCaseCount
|
|
1549
|
+
filteredCaseCount,
|
|
1550
|
+
availableCaseIds: [
|
|
1551
|
+
...availableCaseIds
|
|
1552
|
+
]
|
|
984
1553
|
};
|
|
985
1554
|
}
|
|
1555
|
+
const validateUniqueStrings = (values, label)=>{
|
|
1556
|
+
if (void 0 === values) return;
|
|
1557
|
+
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.`);
|
|
1558
|
+
if (new Set(values).size !== values.length) throw new TypeError(`${label} must not contain duplicates.`);
|
|
1559
|
+
return values;
|
|
1560
|
+
};
|
|
1561
|
+
const validateRunSelection = (options)=>{
|
|
1562
|
+
const paths = validateUniqueStrings(options.paths, 'Test selection paths');
|
|
1563
|
+
const caseIds = validateUniqueStrings(options.caseIds, 'Test selection caseIds');
|
|
1564
|
+
const include = validateUniqueStrings(options.tags?.include, 'Test selection tags.include');
|
|
1565
|
+
const exclude = validateUniqueStrings(options.tags?.exclude, 'Test selection tags.exclude');
|
|
1566
|
+
return {
|
|
1567
|
+
...paths ? {
|
|
1568
|
+
paths
|
|
1569
|
+
} : {},
|
|
1570
|
+
...caseIds ? {
|
|
1571
|
+
caseIds
|
|
1572
|
+
} : {},
|
|
1573
|
+
...include || exclude ? {
|
|
1574
|
+
tags: {
|
|
1575
|
+
...include ? {
|
|
1576
|
+
include
|
|
1577
|
+
} : {},
|
|
1578
|
+
...exclude ? {
|
|
1579
|
+
exclude
|
|
1580
|
+
} : {}
|
|
1581
|
+
}
|
|
1582
|
+
} : {}
|
|
1583
|
+
};
|
|
1584
|
+
};
|
|
986
1585
|
const padDatePart = (value)=>String(value).padStart(2, '0');
|
|
987
1586
|
const createTestRunId = (date = new Date(), uuid = (0, external_node_crypto_namespaceObject.randomUUID)())=>{
|
|
988
1587
|
const timestamp = [
|
|
@@ -1020,6 +1619,7 @@ async function prepareTestRun(options) {
|
|
|
1020
1619
|
const startedAt = new Date();
|
|
1021
1620
|
const runId = createTestRunId(startedAt);
|
|
1022
1621
|
const input = resolveRunInput(options);
|
|
1622
|
+
const selection = validateRunSelection(options);
|
|
1023
1623
|
const configPath = options.configPath ? (0, external_node_path_namespaceObject.resolve)(input.configSearchRoot, options.configPath) : discoverTestConfig(input.configSearchRoot);
|
|
1024
1624
|
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}\`.`);
|
|
1025
1625
|
if (options.configPath && !(0, external_node_fs_namespaceObject.existsSync)(configPath)) throw new Error(`Midscene config does not exist: ${configPath}`);
|
|
@@ -1033,11 +1633,20 @@ async function prepareTestRun(options) {
|
|
|
1033
1633
|
});
|
|
1034
1634
|
(0, external_node_fs_namespaceObject.mkdirSync)(runDir);
|
|
1035
1635
|
const projects = [];
|
|
1036
|
-
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout,
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1636
|
+
for (const project of selectProjects(definition.projects, options.projectNames))projects.push(await prepareProject(project, input.projectRoot, runDir, definition.test.testTimeout, {
|
|
1637
|
+
selection,
|
|
1638
|
+
...input.singleFile ? {
|
|
1639
|
+
files: [
|
|
1640
|
+
input.singleFile
|
|
1641
|
+
]
|
|
1642
|
+
} : {}
|
|
1643
|
+
}));
|
|
1644
|
+
if (selection.caseIds?.length) {
|
|
1645
|
+
const availableCaseIds = new Set(projects.flatMap((project)=>project.availableCaseIds ?? []));
|
|
1646
|
+
const unknown = selection.caseIds.find((id)=>!availableCaseIds.has(id));
|
|
1647
|
+
if (unknown) throw new Error(`Unknown Midscene case id: ${unknown}`);
|
|
1648
|
+
}
|
|
1649
|
+
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.');
|
|
1041
1650
|
const hasFormatMismatch = projects.some((project)=>project.collectionErrors.some(({ error })=>error instanceof NativeWorkflowFormatError));
|
|
1042
1651
|
return {
|
|
1043
1652
|
startedAt,
|
|
@@ -1136,20 +1745,601 @@ async function writeTestResultMarkdown(result, index = emptyIndex()) {
|
|
|
1136
1745
|
}));
|
|
1137
1746
|
return output;
|
|
1138
1747
|
}
|
|
1139
|
-
const
|
|
1748
|
+
const asNotRun = (documentId, collectedCase, projectName, reason)=>({
|
|
1749
|
+
documentId,
|
|
1750
|
+
caseId: collectedCase.caseId,
|
|
1751
|
+
projectName,
|
|
1752
|
+
name: collectedCase.definition.name,
|
|
1753
|
+
sourcePath: collectedCase.sourcePath,
|
|
1754
|
+
caseIndex: collectedCase.caseIndex,
|
|
1755
|
+
status: 'not-run',
|
|
1756
|
+
notRunReason: reason
|
|
1757
|
+
});
|
|
1140
1758
|
const formatStep = (info)=>{
|
|
1141
|
-
const position =
|
|
1759
|
+
const position = 'case' === info.scope ? info.case : info.document;
|
|
1142
1760
|
const phase = 'steps' === position.phase ? 'step' : position.phase;
|
|
1143
1761
|
return `${phase} ${position.stepIndex + 1}/${info.stepCount}: ${info.node}`;
|
|
1144
1762
|
};
|
|
1145
|
-
const formatStepResult = (info, result)=>{
|
|
1146
|
-
const indent = 'case' === info.scope ? ' ' : ' ';
|
|
1763
|
+
const formatStepResult = (info, result, indent = '')=>{
|
|
1147
1764
|
if ('not-run' === result.status) return `${indent}– ${formatStep(info)} (not run: ${result.notRunReason})`;
|
|
1148
1765
|
const symbol = 'success' === result.status ? '✓' : '✗';
|
|
1149
1766
|
const error = result.error ? ` — ${result.error.message}` : '';
|
|
1150
1767
|
const continuation = result.continuedAfterError ? '; continuing' : '';
|
|
1151
1768
|
return `${indent}${symbol} ${formatStep(info)} (${result.durationMs} ms)${error}${continuation}`;
|
|
1152
1769
|
};
|
|
1770
|
+
const latestById = (items, id)=>[
|
|
1771
|
+
...new Map(items.map((item)=>[
|
|
1772
|
+
id(item),
|
|
1773
|
+
item
|
|
1774
|
+
])).values()
|
|
1775
|
+
];
|
|
1776
|
+
const buildProjectResult = (prepared, cases, documents, lifecycle)=>{
|
|
1777
|
+
const { project } = prepared;
|
|
1778
|
+
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';
|
|
1779
|
+
return {
|
|
1780
|
+
projectId: project.projectId,
|
|
1781
|
+
name: project.name,
|
|
1782
|
+
...prepared.platform ? {
|
|
1783
|
+
platform: prepared.platform
|
|
1784
|
+
} : {},
|
|
1785
|
+
status: projectFailed ? 'failed' : 'success',
|
|
1786
|
+
retry: project.retry,
|
|
1787
|
+
fileSelection: prepared.fileSelection,
|
|
1788
|
+
tagSelection: project.tags,
|
|
1789
|
+
sourceCount: prepared.sources.length,
|
|
1790
|
+
selectedCaseCount: prepared.selectedCaseCount,
|
|
1791
|
+
filteredCaseCount: prepared.filteredCaseCount,
|
|
1792
|
+
...lifecycle ? {
|
|
1793
|
+
lifecycle
|
|
1794
|
+
} : {},
|
|
1795
|
+
cases,
|
|
1796
|
+
plans: prepared.plans,
|
|
1797
|
+
documents,
|
|
1798
|
+
collectionErrors: prepared.collectionErrors
|
|
1799
|
+
};
|
|
1800
|
+
};
|
|
1801
|
+
function createExecutorReports(outputDir) {
|
|
1802
|
+
const paths = new Map();
|
|
1803
|
+
const sources = new Map();
|
|
1804
|
+
return {
|
|
1805
|
+
materializeSource (source) {
|
|
1806
|
+
if (!source.dumpPath) {
|
|
1807
|
+
const reference = this.materialize(source.sourcePath);
|
|
1808
|
+
sources.set(reference, {
|
|
1809
|
+
sourcePath: this.resolve(reference),
|
|
1810
|
+
collection: source.collection
|
|
1811
|
+
});
|
|
1812
|
+
return reference;
|
|
1813
|
+
}
|
|
1814
|
+
const dump = (0, external_node_fs_namespaceObject.realpathSync)(source.dumpPath);
|
|
1815
|
+
const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1816
|
+
const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
|
|
1817
|
+
(0, external_node_fs_namespaceObject.mkdirSync)(destination, {
|
|
1818
|
+
recursive: true
|
|
1819
|
+
});
|
|
1820
|
+
const dumpPath = (0, external_node_path_namespaceObject.join)(destination, 'snapshot.json');
|
|
1821
|
+
(0, external_node_fs_namespaceObject.cpSync)(dump, dumpPath);
|
|
1822
|
+
const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(dump), 'screenshots');
|
|
1823
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
|
|
1824
|
+
recursive: true,
|
|
1825
|
+
dereference: true
|
|
1826
|
+
});
|
|
1827
|
+
const sourcePath = (0, external_node_path_namespaceObject.join)(destination, 'index.html');
|
|
1828
|
+
sources.set(reference, {
|
|
1829
|
+
sourcePath,
|
|
1830
|
+
dumpPath,
|
|
1831
|
+
collection: source.collection
|
|
1832
|
+
});
|
|
1833
|
+
paths.set(reference, sourcePath);
|
|
1834
|
+
return reference;
|
|
1835
|
+
},
|
|
1836
|
+
resolveSource (reference) {
|
|
1837
|
+
return sources.get(reference);
|
|
1838
|
+
},
|
|
1839
|
+
materialize (sourcePath) {
|
|
1840
|
+
const source = (0, external_node_fs_namespaceObject.realpathSync)(sourcePath);
|
|
1841
|
+
const entry = (0, external_node_fs_namespaceObject.statSync)(source).isDirectory() ? (0, external_node_path_namespaceObject.join)(source, 'index.html') : source;
|
|
1842
|
+
if (!(0, external_node_fs_namespaceObject.statSync)(entry).isFile()) throw new TestExecutorError(`Executor report must have a regular HTML entry: ${sourcePath}`, {
|
|
1843
|
+
kind: 'report'
|
|
1844
|
+
});
|
|
1845
|
+
const reference = (0, external_node_crypto_namespaceObject.randomUUID)();
|
|
1846
|
+
const destination = (0, external_node_path_namespaceObject.join)(outputDir, 'reports', reference);
|
|
1847
|
+
(0, external_node_fs_namespaceObject.mkdirSync)(destination, {
|
|
1848
|
+
recursive: true
|
|
1849
|
+
});
|
|
1850
|
+
const isDirectoryReport = 'index.html' === (0, external_node_path_namespaceObject.basename)(entry);
|
|
1851
|
+
const reportPath = (0, external_node_path_namespaceObject.join)(destination, isDirectoryReport ? 'index.html' : 'report.html');
|
|
1852
|
+
(0, external_node_fs_namespaceObject.cpSync)(isDirectoryReport ? (0, external_node_path_namespaceObject.dirname)(entry) : entry, isDirectoryReport ? destination : reportPath, {
|
|
1853
|
+
recursive: isDirectoryReport,
|
|
1854
|
+
dereference: true
|
|
1855
|
+
});
|
|
1856
|
+
if (!isDirectoryReport) {
|
|
1857
|
+
const screenshots = (0, external_node_path_namespaceObject.join)((0, external_node_path_namespaceObject.dirname)(entry), 'screenshots');
|
|
1858
|
+
if ((0, external_node_fs_namespaceObject.existsSync)(screenshots)) (0, external_node_fs_namespaceObject.cpSync)(screenshots, (0, external_node_path_namespaceObject.join)(destination, 'screenshots'), {
|
|
1859
|
+
recursive: true,
|
|
1860
|
+
dereference: true
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
paths.set(reference, reportPath);
|
|
1864
|
+
return reference;
|
|
1865
|
+
},
|
|
1866
|
+
resolve (reference) {
|
|
1867
|
+
const reportPath = paths.get(reference);
|
|
1868
|
+
if (!reportPath) throw new TestExecutorError(`Executor returned unknown report reference ${reference}.`, {
|
|
1869
|
+
kind: 'report'
|
|
1870
|
+
});
|
|
1871
|
+
return reportPath;
|
|
1872
|
+
}
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
async function runTaskPool(options) {
|
|
1876
|
+
if (!Number.isInteger(options.maxConcurrency) || options.maxConcurrency <= 0) throw new TypeError('Task scheduler maxConcurrency must be a positive integer.');
|
|
1877
|
+
const pending = options.tasks.map((_, index)=>index);
|
|
1878
|
+
const results = new Array(options.tasks.length);
|
|
1879
|
+
const activeResources = new Set();
|
|
1880
|
+
let active = 0;
|
|
1881
|
+
let firstError;
|
|
1882
|
+
let hasError = false;
|
|
1883
|
+
return await new Promise((resolve, reject)=>{
|
|
1884
|
+
let settled = false;
|
|
1885
|
+
const stopRequested = ()=>hasError || options.signal?.aborted === true || options.shouldStop?.() === true;
|
|
1886
|
+
const finish = ()=>{
|
|
1887
|
+
if (settled) return;
|
|
1888
|
+
settled = true;
|
|
1889
|
+
options.signal?.removeEventListener('abort', pump);
|
|
1890
|
+
if (hasError) reject(firstError);
|
|
1891
|
+
else resolve(results);
|
|
1892
|
+
};
|
|
1893
|
+
const release = (resources)=>{
|
|
1894
|
+
for (const resource of resources)activeResources.delete(resource);
|
|
1895
|
+
};
|
|
1896
|
+
const claimRunnable = ()=>{
|
|
1897
|
+
const pendingIndex = pending.findIndex((taskIndex)=>options.tasks[taskIndex].resources.every((resource)=>!activeResources.has(resource)));
|
|
1898
|
+
if (pendingIndex < 0) return;
|
|
1899
|
+
const [taskIndex] = pending.splice(pendingIndex, 1);
|
|
1900
|
+
return taskIndex;
|
|
1901
|
+
};
|
|
1902
|
+
function pump() {
|
|
1903
|
+
if (settled) return;
|
|
1904
|
+
while(active < options.maxConcurrency && !stopRequested()){
|
|
1905
|
+
const taskIndex = claimRunnable();
|
|
1906
|
+
if (void 0 === taskIndex) break;
|
|
1907
|
+
const task = options.tasks[taskIndex];
|
|
1908
|
+
for (const resource of task.resources)activeResources.add(resource);
|
|
1909
|
+
active += 1;
|
|
1910
|
+
Promise.resolve().then(()=>options.run(task, taskIndex)).then((result)=>{
|
|
1911
|
+
results[taskIndex] = result;
|
|
1912
|
+
}).catch((error)=>{
|
|
1913
|
+
if (!hasError) {
|
|
1914
|
+
hasError = true;
|
|
1915
|
+
firstError = error;
|
|
1916
|
+
}
|
|
1917
|
+
}).finally(()=>{
|
|
1918
|
+
release(task.resources);
|
|
1919
|
+
active -= 1;
|
|
1920
|
+
pump();
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
if (0 === active && (0 === pending.length || stopRequested())) finish();
|
|
1924
|
+
}
|
|
1925
|
+
options.signal?.addEventListener('abort', pump, {
|
|
1926
|
+
once: true
|
|
1927
|
+
});
|
|
1928
|
+
pump();
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
const isolatedDocumentId = (document, collectedCase)=>`${document.documentId}-case-${collectedCase.caseIndex}`;
|
|
1932
|
+
const prepareCaseTasks = (projects)=>projects.flatMap((prepared, projectIndex)=>prepared.documents.flatMap((document)=>document.cases.map((collectedCase)=>{
|
|
1933
|
+
const documentId = isolatedDocumentId(document, collectedCase);
|
|
1934
|
+
return {
|
|
1935
|
+
projectIndex,
|
|
1936
|
+
prepared,
|
|
1937
|
+
document,
|
|
1938
|
+
collectedCase,
|
|
1939
|
+
task: Object.freeze({
|
|
1940
|
+
taskId: `${prepared.project.projectId}:${collectedCase.caseId}`,
|
|
1941
|
+
projectId: prepared.project.projectId,
|
|
1942
|
+
projectName: prepared.project.name,
|
|
1943
|
+
documentId,
|
|
1944
|
+
sourcePath: collectedCase.sourcePath,
|
|
1945
|
+
caseId: collectedCase.caseId,
|
|
1946
|
+
caseName: collectedCase.definition.name,
|
|
1947
|
+
caseIndex: collectedCase.caseIndex,
|
|
1948
|
+
tags: Object.freeze([
|
|
1949
|
+
...collectedCase.definition.tags ?? []
|
|
1950
|
+
]),
|
|
1951
|
+
resources: Object.freeze([
|
|
1952
|
+
...collectedCase.definition.resources ?? []
|
|
1953
|
+
]),
|
|
1954
|
+
retry: prepared.project.retry
|
|
1955
|
+
})
|
|
1956
|
+
};
|
|
1957
|
+
})));
|
|
1958
|
+
const reviveError = (error)=>{
|
|
1959
|
+
const revived = new test_runner_namespaceObject.WorkflowError(error.message, {
|
|
1960
|
+
code: error.code,
|
|
1961
|
+
details: error.details,
|
|
1962
|
+
cause: error.cause
|
|
1963
|
+
});
|
|
1964
|
+
revived.name = error.name;
|
|
1965
|
+
return revived;
|
|
1966
|
+
};
|
|
1967
|
+
const reviveStep = (step)=>{
|
|
1968
|
+
const { error, output, report, ...base } = step;
|
|
1969
|
+
return {
|
|
1970
|
+
...base,
|
|
1971
|
+
input: step.input,
|
|
1972
|
+
meta: {
|
|
1973
|
+
...step.meta
|
|
1974
|
+
},
|
|
1975
|
+
...output ? {
|
|
1976
|
+
output: {
|
|
1977
|
+
...output
|
|
1978
|
+
}
|
|
1979
|
+
} : {},
|
|
1980
|
+
...error ? {
|
|
1981
|
+
error: reviveError(error)
|
|
1982
|
+
} : {},
|
|
1983
|
+
...report ? {
|
|
1984
|
+
report: {
|
|
1985
|
+
traces: report.traces.map((trace)=>({
|
|
1986
|
+
...trace
|
|
1987
|
+
})),
|
|
1988
|
+
...report.collectionErrors ? {
|
|
1989
|
+
collectionErrors: report.collectionErrors.map(reviveError)
|
|
1990
|
+
} : {}
|
|
1991
|
+
}
|
|
1992
|
+
} : {}
|
|
1993
|
+
};
|
|
1994
|
+
};
|
|
1995
|
+
const reviveAttempt = (attempt)=>{
|
|
1996
|
+
const { executionErrors, teardownErrors, reportRefs, beforeEach, steps, afterEach, ...base } = attempt;
|
|
1997
|
+
return {
|
|
1998
|
+
...base,
|
|
1999
|
+
beforeEach: beforeEach.map(reviveStep),
|
|
2000
|
+
steps: steps.map(reviveStep),
|
|
2001
|
+
afterEach: afterEach.map(reviveStep),
|
|
2002
|
+
...executionErrors ? {
|
|
2003
|
+
executionErrors: executionErrors.map(reviveError)
|
|
2004
|
+
} : {},
|
|
2005
|
+
...teardownErrors ? {
|
|
2006
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2007
|
+
} : {}
|
|
2008
|
+
};
|
|
2009
|
+
};
|
|
2010
|
+
const reviveDocument = (document)=>{
|
|
2011
|
+
const { executionErrors, teardownErrors, reportRefs, beforeAll, afterAll, hostErrors, ...base } = document;
|
|
2012
|
+
return {
|
|
2013
|
+
...base,
|
|
2014
|
+
...hostErrors ? {
|
|
2015
|
+
hostErrors: hostErrors.map((error)=>({
|
|
2016
|
+
...error
|
|
2017
|
+
}))
|
|
2018
|
+
} : {},
|
|
2019
|
+
beforeAll: beforeAll.map(reviveStep),
|
|
2020
|
+
afterAll: afterAll.map(reviveStep),
|
|
2021
|
+
...executionErrors ? {
|
|
2022
|
+
executionErrors: executionErrors.map(reviveError)
|
|
2023
|
+
} : {},
|
|
2024
|
+
...teardownErrors ? {
|
|
2025
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2026
|
+
} : {}
|
|
2027
|
+
};
|
|
2028
|
+
};
|
|
2029
|
+
const reviveLifecycle = (lifecycle)=>{
|
|
2030
|
+
const { setupError, teardownErrors, ...base } = lifecycle;
|
|
2031
|
+
return {
|
|
2032
|
+
...base,
|
|
2033
|
+
...setupError ? {
|
|
2034
|
+
setupError: reviveError(setupError)
|
|
2035
|
+
} : {},
|
|
2036
|
+
...teardownErrors ? {
|
|
2037
|
+
teardownErrors: teardownErrors.map(reviveError)
|
|
2038
|
+
} : {}
|
|
2039
|
+
};
|
|
2040
|
+
};
|
|
2041
|
+
const toTransportResult = (result, materializeReport, materializeSource)=>{
|
|
2042
|
+
const transportReports = (scope)=>[
|
|
2043
|
+
...(scope.reportSources ?? []).map(materializeSource),
|
|
2044
|
+
...(scope.reportPaths ?? []).filter((path)=>!scope.reportSources?.some((source)=>source.sourcePath === path)).map(materializeReport)
|
|
2045
|
+
];
|
|
2046
|
+
const mapStep = (step)=>{
|
|
2047
|
+
if (!step.report) return step;
|
|
2048
|
+
const { sources: _sources, ...report } = step.report;
|
|
2049
|
+
return {
|
|
2050
|
+
...step,
|
|
2051
|
+
report
|
|
2052
|
+
};
|
|
2053
|
+
};
|
|
2054
|
+
const mapAttempt = (attempt)=>{
|
|
2055
|
+
const { reportPaths, reportSources, reportScopeId, ...transportAttempt } = attempt;
|
|
2056
|
+
const reportRefs = transportReports(attempt);
|
|
2057
|
+
return {
|
|
2058
|
+
...transportAttempt,
|
|
2059
|
+
beforeEach: attempt.beforeEach.map(mapStep),
|
|
2060
|
+
steps: attempt.steps.map(mapStep),
|
|
2061
|
+
afterEach: attempt.afterEach.map(mapStep),
|
|
2062
|
+
...reportRefs.length ? {
|
|
2063
|
+
reportRefs
|
|
2064
|
+
} : {}
|
|
2065
|
+
};
|
|
2066
|
+
};
|
|
2067
|
+
const attempts = result.case.attempts?.map(mapAttempt);
|
|
2068
|
+
const run = attempts?.at(-1);
|
|
2069
|
+
const { run: _run, attempts: _attempts, ...transportCase } = result.case;
|
|
2070
|
+
const document = result.document ? (()=>{
|
|
2071
|
+
const { reportPaths, reportSources, reportScopeId, ...transportDocument } = result.document;
|
|
2072
|
+
const reportRefs = transportReports(result.document);
|
|
2073
|
+
return {
|
|
2074
|
+
...transportDocument,
|
|
2075
|
+
...result.document.hostErrors ? {
|
|
2076
|
+
hostErrors: result.document.hostErrors.map(({ phase, error })=>({
|
|
2077
|
+
phase,
|
|
2078
|
+
error: (0, test_runner_namespaceObject.sanitizeReportError)(error)
|
|
2079
|
+
}))
|
|
2080
|
+
} : {},
|
|
2081
|
+
beforeAll: result.document.beforeAll.map(mapStep),
|
|
2082
|
+
afterAll: result.document.afterAll.map(mapStep),
|
|
2083
|
+
...reportRefs.length ? {
|
|
2084
|
+
reportRefs
|
|
2085
|
+
} : {}
|
|
2086
|
+
};
|
|
2087
|
+
})() : void 0;
|
|
2088
|
+
return JSON.parse(JSON.stringify({
|
|
2089
|
+
case: {
|
|
2090
|
+
...transportCase,
|
|
2091
|
+
...run ? {
|
|
2092
|
+
run
|
|
2093
|
+
} : {},
|
|
2094
|
+
...attempts ? {
|
|
2095
|
+
attempts
|
|
2096
|
+
} : {}
|
|
2097
|
+
},
|
|
2098
|
+
...document ? {
|
|
2099
|
+
document
|
|
2100
|
+
} : {},
|
|
2101
|
+
...result.lifecycle ? {
|
|
2102
|
+
lifecycle: result.lifecycle
|
|
2103
|
+
} : {}
|
|
2104
|
+
}));
|
|
2105
|
+
};
|
|
2106
|
+
const fromTransportResult = (result, resolveReportReference, resolveSource)=>{
|
|
2107
|
+
const resolveReports = (refs)=>{
|
|
2108
|
+
const reportPaths = [];
|
|
2109
|
+
const reportSources = [];
|
|
2110
|
+
for (const ref of refs ?? []){
|
|
2111
|
+
const path = resolveReportReference(ref);
|
|
2112
|
+
const source = resolveSource(ref);
|
|
2113
|
+
if (source) reportSources.push(source);
|
|
2114
|
+
else reportPaths.push(path);
|
|
2115
|
+
}
|
|
2116
|
+
return {
|
|
2117
|
+
...reportPaths.length ? {
|
|
2118
|
+
reportPaths
|
|
2119
|
+
} : {},
|
|
2120
|
+
...reportSources.length ? {
|
|
2121
|
+
reportSources
|
|
2122
|
+
} : {}
|
|
2123
|
+
};
|
|
2124
|
+
};
|
|
2125
|
+
const attempts = result.case.attempts?.map((attempt)=>({
|
|
2126
|
+
...reviveAttempt(attempt),
|
|
2127
|
+
...resolveReports(attempt.reportRefs)
|
|
2128
|
+
}));
|
|
2129
|
+
const run = attempts?.at(-1);
|
|
2130
|
+
const { run: _run, attempts: _attempts, ...caseBase } = result.case;
|
|
2131
|
+
return {
|
|
2132
|
+
case: {
|
|
2133
|
+
...caseBase,
|
|
2134
|
+
...run ? {
|
|
2135
|
+
run
|
|
2136
|
+
} : {},
|
|
2137
|
+
...attempts ? {
|
|
2138
|
+
attempts
|
|
2139
|
+
} : {}
|
|
2140
|
+
},
|
|
2141
|
+
...result.document ? {
|
|
2142
|
+
document: {
|
|
2143
|
+
...reviveDocument(result.document),
|
|
2144
|
+
...resolveReports(result.document.reportRefs)
|
|
2145
|
+
}
|
|
2146
|
+
} : {},
|
|
2147
|
+
...result.lifecycle ? {
|
|
2148
|
+
lifecycle: reviveLifecycle(result.lifecycle)
|
|
2149
|
+
} : {}
|
|
2150
|
+
};
|
|
2151
|
+
};
|
|
2152
|
+
const runLocalCase = async (preparedTask, options, taskProgress, materializeReport, materializeSource)=>{
|
|
2153
|
+
const { prepared, document, collectedCase, task } = preparedTask;
|
|
2154
|
+
const { project } = prepared;
|
|
2155
|
+
const isolatedDocument = {
|
|
2156
|
+
...document,
|
|
2157
|
+
documentId: task.documentId,
|
|
2158
|
+
cases: [
|
|
2159
|
+
collectedCase
|
|
2160
|
+
]
|
|
2161
|
+
};
|
|
2162
|
+
const runtime = (0, test_runner_namespaceObject.createProjectRuntime)({
|
|
2163
|
+
project,
|
|
2164
|
+
setup: project.setup,
|
|
2165
|
+
signal: options.signal
|
|
2166
|
+
});
|
|
2167
|
+
let lifecycle;
|
|
2168
|
+
let execution;
|
|
2169
|
+
let executionError;
|
|
2170
|
+
try {
|
|
2171
|
+
await runtime.start();
|
|
2172
|
+
if (runtime.canRun) execution = await (0, test_runner_namespaceObject.runWorkflowDocument)(isolatedDocument, {
|
|
2173
|
+
resolveNode: project.nodes.require.bind(project.nodes),
|
|
2174
|
+
project,
|
|
2175
|
+
projectContext: runtime.context,
|
|
2176
|
+
retry: project.retry,
|
|
2177
|
+
signal: runtime.signal,
|
|
2178
|
+
defaultTimeoutMs: options.test.testTimeout,
|
|
2179
|
+
shouldStop: ()=>options.signal.aborted,
|
|
2180
|
+
stopReason: ()=>'interrupted',
|
|
2181
|
+
isFatalError: (run)=>[
|
|
2182
|
+
...run.beforeEach,
|
|
2183
|
+
...run.steps,
|
|
2184
|
+
...run.afterEach
|
|
2185
|
+
].some((step)=>step.error && (0, test_runner_namespaceObject.isFatalDeviceError)(step.error)) || (run.teardownErrors ?? []).some(test_runner_namespaceObject.isFatalDeviceError),
|
|
2186
|
+
onStepStart: (info)=>taskProgress(`→ ${formatStep(info)}`),
|
|
2187
|
+
onStepResult: (info, result)=>taskProgress(formatStepResult(info, result)),
|
|
2188
|
+
onCaseResult: (attempt)=>taskProgress(`${'success' === attempt.status ? '✓' : '✗'} attempt ${attempt.attemptIndex + 1}/${project.retry + 1}: ${attempt.name} (${attempt.durationMs} ms)`)
|
|
2189
|
+
});
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
executionError = error;
|
|
2192
|
+
if (error instanceof test_runner_namespaceObject.WorkflowExecutionFailure) execution = error.result;
|
|
2193
|
+
} finally{
|
|
2194
|
+
const failed = void 0 !== executionError || !runtime.canRun || execution?.cases[0]?.status !== 'success' || execution?.document.status === 'failed';
|
|
2195
|
+
lifecycle = await runtime.finish(failed ? 'failed' : 'success');
|
|
2196
|
+
}
|
|
2197
|
+
if (executionError) {
|
|
2198
|
+
if (!execution) throw executionError;
|
|
2199
|
+
options.onInfrastructureError(executionError);
|
|
2200
|
+
}
|
|
2201
|
+
if (!execution) return toTransportResult({
|
|
2202
|
+
case: asNotRun(task.documentId, collectedCase, project.name, options.signal.aborted ? 'interrupted' : 'project-setup-failed'),
|
|
2203
|
+
lifecycle
|
|
2204
|
+
}, materializeReport, materializeSource);
|
|
2205
|
+
const outcome = execution.cases[0];
|
|
2206
|
+
if (!outcome) throw new Error(`Executor did not produce case result for ${task.caseId}.`);
|
|
2207
|
+
return toTransportResult({
|
|
2208
|
+
case: {
|
|
2209
|
+
...outcome,
|
|
2210
|
+
documentId: task.documentId
|
|
2211
|
+
},
|
|
2212
|
+
document: execution.document,
|
|
2213
|
+
lifecycle
|
|
2214
|
+
}, materializeReport, materializeSource);
|
|
2215
|
+
};
|
|
2216
|
+
const runCaseTask = async (preparedTask, taskIndex, taskCount, options, onCompleted)=>{
|
|
2217
|
+
const { prepared, task } = preparedTask;
|
|
2218
|
+
const taskProgress = (message)=>options.progress(`[case ${taskIndex + 1}/${taskCount}] ${task.projectName} / ${task.sourcePath} / ${task.caseName}: ${message}`);
|
|
2219
|
+
taskProgress('started');
|
|
2220
|
+
const outputDir = (0, external_node_path_namespaceObject.join)(options.runDir, task.projectId, 'executor-output', task.documentId, task.caseId);
|
|
2221
|
+
(0, external_node_fs_namespaceObject.mkdirSync)(outputDir, {
|
|
2222
|
+
recursive: true
|
|
2223
|
+
});
|
|
2224
|
+
const { materialize: materializeReport, materializeSource, resolve: resolveReportReference, resolveSource } = createExecutorReports(outputDir);
|
|
2225
|
+
if (prepared.collectionErrors.length > 0) return {
|
|
2226
|
+
case: asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'project-preflight-failed')
|
|
2227
|
+
};
|
|
2228
|
+
const executor = options.executor ?? localTestExecutor;
|
|
2229
|
+
let result;
|
|
2230
|
+
let executorFailure;
|
|
2231
|
+
let executorAttempts = 0;
|
|
2232
|
+
for(let attemptIndex = 0; attemptIndex <= options.test.executorRetry; attemptIndex += 1){
|
|
2233
|
+
executorAttempts = attemptIndex + 1;
|
|
2234
|
+
try {
|
|
2235
|
+
result = await executor.execute(task, {
|
|
2236
|
+
signal: options.signal,
|
|
2237
|
+
onProgress: taskProgress,
|
|
2238
|
+
outputDir,
|
|
2239
|
+
materializeReport,
|
|
2240
|
+
runLocal: ()=>runLocalCase(preparedTask, options, taskProgress, materializeReport, materializeSource)
|
|
2241
|
+
});
|
|
2242
|
+
executorFailure = void 0;
|
|
2243
|
+
break;
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
if (!(error instanceof TestExecutorError)) throw error;
|
|
2246
|
+
executorFailure = error;
|
|
2247
|
+
if (!error.retryable || attemptIndex >= options.test.executorRetry || options.signal.aborted) break;
|
|
2248
|
+
taskProgress(`executor retry ${attemptIndex + 1}/${options.test.executorRetry}: ${error.message}`);
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
if (!result) {
|
|
2252
|
+
if (!executorFailure) throw new Error(`Executor did not return a result for ${task.caseId}.`);
|
|
2253
|
+
taskProgress(`executor-failed: ${executorFailure.message}`);
|
|
2254
|
+
return {
|
|
2255
|
+
case: {
|
|
2256
|
+
...asNotRun(task.documentId, preparedTask.collectedCase, task.projectName, 'executor-failed'),
|
|
2257
|
+
execution: {
|
|
2258
|
+
executor: executor.name,
|
|
2259
|
+
resources: task.resources,
|
|
2260
|
+
attempts: executorAttempts,
|
|
2261
|
+
failure: {
|
|
2262
|
+
kind: executorFailure.kind,
|
|
2263
|
+
message: executorFailure.message,
|
|
2264
|
+
retryable: executorFailure.retryable
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
assertTestCaseTaskRunResult(task, result);
|
|
2271
|
+
const internalResult = fromTransportResult(result, resolveReportReference, resolveSource);
|
|
2272
|
+
const completed = {
|
|
2273
|
+
...internalResult,
|
|
2274
|
+
case: {
|
|
2275
|
+
...internalResult.case,
|
|
2276
|
+
execution: {
|
|
2277
|
+
executor: executor.name,
|
|
2278
|
+
resources: task.resources,
|
|
2279
|
+
attempts: executorAttempts,
|
|
2280
|
+
...internalResult.lifecycle ? {
|
|
2281
|
+
lifecycle: internalResult.lifecycle
|
|
2282
|
+
} : {},
|
|
2283
|
+
...result.artifacts ? {
|
|
2284
|
+
artifacts: result.artifacts
|
|
2285
|
+
} : {},
|
|
2286
|
+
...result.metadata ? {
|
|
2287
|
+
metadata: result.metadata
|
|
2288
|
+
} : {}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
};
|
|
2292
|
+
onCompleted(completed);
|
|
2293
|
+
for (const attempt of internalResult.case.attempts ?? [])await writeCaseAttemptResult(options.runDir, task.projectId, task.documentId, attempt);
|
|
2294
|
+
if (internalResult.document) await writeWorkflowDocumentResult(options.runDir, internalResult.document);
|
|
2295
|
+
taskProgress(internalResult.case.status);
|
|
2296
|
+
return completed;
|
|
2297
|
+
};
|
|
2298
|
+
const runCaseExecution = async (options)=>{
|
|
2299
|
+
const tasks = prepareCaseTasks(options.projects);
|
|
2300
|
+
let failedCaseCount = 0;
|
|
2301
|
+
const bailReached = ()=>options.test.bail > 0 && failedCaseCount >= options.test.bail;
|
|
2302
|
+
const results = new Array(tasks.length);
|
|
2303
|
+
if (tasks.length > 0) try {
|
|
2304
|
+
await runTaskPool({
|
|
2305
|
+
tasks: tasks.map((preparedTask)=>preparedTask.task),
|
|
2306
|
+
maxConcurrency: Math.min(options.test.maxConcurrency, tasks.length),
|
|
2307
|
+
signal: options.signal,
|
|
2308
|
+
shouldStop: bailReached,
|
|
2309
|
+
run: async (_task, taskIndex)=>{
|
|
2310
|
+
try {
|
|
2311
|
+
const result = await runCaseTask(tasks[taskIndex], taskIndex, tasks.length, options, (completed)=>{
|
|
2312
|
+
results[taskIndex] = completed;
|
|
2313
|
+
});
|
|
2314
|
+
results[taskIndex] = result;
|
|
2315
|
+
if ('failed' === result.case.status) failedCaseCount += 1;
|
|
2316
|
+
return result;
|
|
2317
|
+
} catch (error) {
|
|
2318
|
+
options.onInfrastructureError(error);
|
|
2319
|
+
throw error;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
});
|
|
2323
|
+
} catch (error) {
|
|
2324
|
+
options.onInfrastructureError(error);
|
|
2325
|
+
}
|
|
2326
|
+
return options.projects.map((prepared, projectIndex)=>{
|
|
2327
|
+
const projectTasks = tasks.map((task, taskIndex)=>({
|
|
2328
|
+
task,
|
|
2329
|
+
result: results[taskIndex]
|
|
2330
|
+
})).filter(({ task })=>task.projectIndex === projectIndex);
|
|
2331
|
+
const reason = prepared.collectionErrors.length ? 'project-preflight-failed' : options.signal.aborted ? 'interrupted' : bailReached() ? 'bail' : void 0;
|
|
2332
|
+
const cases = projectTasks.map(({ task, result })=>{
|
|
2333
|
+
if (result) return result.case;
|
|
2334
|
+
if (!reason) throw new Error(`Case scheduler did not produce a result for "${task.task.caseId}".`);
|
|
2335
|
+
return asNotRun(task.task.documentId, task.collectedCase, prepared.project.name, reason);
|
|
2336
|
+
});
|
|
2337
|
+
const documents = projectTasks.flatMap(({ result })=>result?.document ? [
|
|
2338
|
+
result.document
|
|
2339
|
+
] : []);
|
|
2340
|
+
return buildProjectResult(prepared, cases, documents);
|
|
2341
|
+
});
|
|
2342
|
+
};
|
|
1153
2343
|
const caseHasFatalError = (outcome)=>(outcome.attempts ?? []).some((attempt)=>[
|
|
1154
2344
|
...attempt.beforeEach,
|
|
1155
2345
|
...attempt.steps,
|
|
@@ -1195,7 +2385,7 @@ async function executeDocumentInvocation(options) {
|
|
|
1195
2385
|
},
|
|
1196
2386
|
onStepResult: async (info, result)=>{
|
|
1197
2387
|
await invocation.bindings.onStepResult?.(info, result);
|
|
1198
|
-
options.onProgress(formatStepResult(info, result));
|
|
2388
|
+
options.onProgress(formatStepResult(info, result, 'case' === info.scope ? ' ' : ' '));
|
|
1199
2389
|
},
|
|
1200
2390
|
onCaseResult: async (attempt)=>{
|
|
1201
2391
|
await writeCaseAttemptResult(options.runDir, project.projectId, document.documentId, attempt);
|
|
@@ -1313,16 +2503,6 @@ function createOperationJournal(runDir) {
|
|
|
1313
2503
|
}
|
|
1314
2504
|
};
|
|
1315
2505
|
}
|
|
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
|
-
});
|
|
1326
2506
|
const summarize = (projects)=>{
|
|
1327
2507
|
const healthCases = projects.flatMap((project)=>{
|
|
1328
2508
|
const documentAttemptByRunId = new Map(project.documents.map((document)=>[
|
|
@@ -1356,12 +2536,6 @@ const summarize = (projects)=>{
|
|
|
1356
2536
|
projectFailures: projects.filter((project)=>'failed' === project.status).length
|
|
1357
2537
|
};
|
|
1358
2538
|
};
|
|
1359
|
-
const latestById = (items, id)=>[
|
|
1360
|
-
...new Map(items.map((item)=>[
|
|
1361
|
-
id(item),
|
|
1362
|
-
item
|
|
1363
|
-
])).values()
|
|
1364
|
-
];
|
|
1365
2539
|
const notRunSuite = (prepared, reason)=>prepared.documents.flatMap((document)=>document.cases.map((item)=>asNotRun(document.documentId, item, prepared.project.name, reason)));
|
|
1366
2540
|
async function runPreparedTestProject(plan) {
|
|
1367
2541
|
const { startedAt, runId, projectRoot, configPath, resultDir, runDir, summaryPath, reportDir, definition, projects: preparedProjects } = plan;
|
|
@@ -1374,7 +2548,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1374
2548
|
await writeTestRunPlan(runDir, preparedProjects);
|
|
1375
2549
|
const journal = createOperationJournal(runDir);
|
|
1376
2550
|
const rootController = new AbortController();
|
|
1377
|
-
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, effectiveConcurrency + 1), rootController.signal);
|
|
2551
|
+
(0, external_node_events_namespaceObject.setMaxListeners)(Math.max(10, ('case' === definition.test.executionUnit ? Math.min(definition.test.maxConcurrency, totalCases) : effectiveConcurrency) + 1), rootController.signal);
|
|
1378
2552
|
const handleSignal = (signal)=>{
|
|
1379
2553
|
rootController.abort(new Error(`Workflow interrupted by ${signal}.`));
|
|
1380
2554
|
};
|
|
@@ -1416,31 +2590,6 @@ async function runPreparedTestProject(plan) {
|
|
|
1416
2590
|
const { project } = prepared;
|
|
1417
2591
|
runInfrastructureCallback(()=>progress(`[project ${projectIndex + 1}/${preparedProjects.length}] ${project.name}`));
|
|
1418
2592
|
};
|
|
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
|
-
};
|
|
1444
2593
|
const buildSkippedProjectResult = (prepared, projectIndex, reason)=>{
|
|
1445
2594
|
announceProject(prepared, projectIndex);
|
|
1446
2595
|
return buildProjectResult(prepared, notRunSuite(prepared, reason), []);
|
|
@@ -1541,7 +2690,7 @@ async function runPreparedTestProject(plan) {
|
|
|
1541
2690
|
};
|
|
1542
2691
|
const projectResults = new Array(preparedProjects.length);
|
|
1543
2692
|
try {
|
|
1544
|
-
await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
|
|
2693
|
+
if ('case' !== definition.test.executionUnit || 'run' === plan.preflightScope && totalErrors > 0) await (0, test_runner_namespaceObject.runConcurrentJobs)(preparedProjects, {
|
|
1545
2694
|
concurrency: effectiveConcurrency,
|
|
1546
2695
|
shouldStop: ()=>hasInfrastructureError || rootController.signal.aborted || bailReached()
|
|
1547
2696
|
}, async (prepared, index)=>{
|
|
@@ -1552,6 +2701,20 @@ async function runPreparedTestProject(plan) {
|
|
|
1552
2701
|
throw error;
|
|
1553
2702
|
}
|
|
1554
2703
|
});
|
|
2704
|
+
else {
|
|
2705
|
+
const results = await runCaseExecution({
|
|
2706
|
+
projects: preparedProjects,
|
|
2707
|
+
test: definition.test,
|
|
2708
|
+
executor: definition.executor,
|
|
2709
|
+
runDir,
|
|
2710
|
+
signal: rootController.signal,
|
|
2711
|
+
progress,
|
|
2712
|
+
onInfrastructureError: recordInfrastructureError
|
|
2713
|
+
});
|
|
2714
|
+
results.forEach((result, index)=>{
|
|
2715
|
+
projectResults[index] = result;
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
1555
2718
|
} catch (error) {
|
|
1556
2719
|
recordInfrastructureError(error);
|
|
1557
2720
|
} finally{
|
|
@@ -1658,22 +2821,32 @@ async function runTestProject(options = {}) {
|
|
|
1658
2821
|
return runPreparedTestProject(await prepareTestRun(options));
|
|
1659
2822
|
}
|
|
1660
2823
|
exports.DEFAULT_TEST_FILE_SELECTION = __webpack_exports__.DEFAULT_TEST_FILE_SELECTION;
|
|
2824
|
+
exports.TestExecutorError = __webpack_exports__.TestExecutorError;
|
|
2825
|
+
exports.assertTestCaseTaskRunResult = __webpack_exports__.assertTestCaseTaskRunResult;
|
|
1661
2826
|
exports.createTestRunId = __webpack_exports__.createTestRunId;
|
|
1662
2827
|
exports.defineProjectSetup = __webpack_exports__.defineProjectSetup;
|
|
2828
|
+
exports.defineTestExecutor = __webpack_exports__.defineTestExecutor;
|
|
1663
2829
|
exports.defineTestProject = __webpack_exports__.defineTestProject;
|
|
2830
|
+
exports.discoverSelectedTestFiles = __webpack_exports__.discoverSelectedTestFiles;
|
|
1664
2831
|
exports.discoverTestConfig = __webpack_exports__.discoverTestConfig;
|
|
1665
2832
|
exports.discoverTestFiles = __webpack_exports__.discoverTestFiles;
|
|
1666
2833
|
exports.loadTestProject = __webpack_exports__.loadTestProject;
|
|
2834
|
+
exports.localTestExecutor = __webpack_exports__.localTestExecutor;
|
|
1667
2835
|
exports.runTestProject = __webpack_exports__.runTestProject;
|
|
1668
2836
|
exports.validateTestFileSelection = __webpack_exports__.validateTestFileSelection;
|
|
1669
2837
|
for(var __rspack_i in __webpack_exports__)if (-1 === [
|
|
1670
2838
|
"DEFAULT_TEST_FILE_SELECTION",
|
|
2839
|
+
"TestExecutorError",
|
|
2840
|
+
"assertTestCaseTaskRunResult",
|
|
1671
2841
|
"createTestRunId",
|
|
1672
2842
|
"defineProjectSetup",
|
|
2843
|
+
"defineTestExecutor",
|
|
1673
2844
|
"defineTestProject",
|
|
2845
|
+
"discoverSelectedTestFiles",
|
|
1674
2846
|
"discoverTestConfig",
|
|
1675
2847
|
"discoverTestFiles",
|
|
1676
2848
|
"loadTestProject",
|
|
2849
|
+
"localTestExecutor",
|
|
1677
2850
|
"runTestProject",
|
|
1678
2851
|
"validateTestFileSelection"
|
|
1679
2852
|
].indexOf(__rspack_i)) exports[__rspack_i] = __webpack_exports__[__rspack_i];
|