velocious 1.0.643 → 1.0.644
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/README.md +10 -1
- package/build/src/sync/assets/cache.d.ts +288 -0
- package/build/src/sync/assets/cache.d.ts.map +1 -0
- package/build/src/sync/assets/cache.js +883 -0
- package/build/src/sync/assets/types.d.ts +199 -0
- package/build/src/sync/assets/types.d.ts.map +1 -0
- package/build/src/sync/assets/types.js +56 -0
- package/build/src/testing/test-runner.d.ts +8 -0
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +49 -548
- package/build/src/testing/velocious-attempt-executor.d.ts +69 -0
- package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-attempt-executor.js +402 -0
- package/build/src/testing/velocious-runner-reporter.d.ts +72 -0
- package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -0
- package/build/src/testing/velocious-runner-reporter.js +152 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts +37 -0
- package/build/src/testing/velocious-suite-hook-executor.d.ts.map +1 -0
- package/build/src/testing/velocious-suite-hook-executor.js +66 -0
- package/build/src/testing/velocious-test-arguments.d.ts +34 -0
- package/build/src/testing/velocious-test-arguments.d.ts.map +1 -0
- package/build/src/testing/velocious-test-arguments.js +47 -0
- package/build/src/utils/sha256-bytes-hex.d.ts +7 -0
- package/build/src/utils/sha256-bytes-hex.d.ts.map +1 -0
- package/build/src/utils/sha256-bytes-hex.js +117 -0
- package/build/src/utils/sha256-hex.d.ts +2 -4
- package/build/src/utils/sha256-hex.d.ts.map +1 -1
- package/build/src/utils/sha256-hex.js +6 -120
- package/build/sync/assets/cache.js +998 -0
- package/build/sync/assets/types.js +62 -0
- package/build/testing/test-runner.js +46 -584
- package/build/testing/velocious-attempt-executor.js +431 -0
- package/build/testing/velocious-runner-reporter.js +166 -0
- package/build/testing/velocious-suite-hook-executor.js +71 -0
- package/build/testing/velocious-test-arguments.js +55 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/utils/sha256-bytes-hex.js +132 -0
- package/build/utils/sha256-hex.js +7 -135
- package/package.json +1 -1
- package/src/sync/assets/cache.js +998 -0
- package/src/sync/assets/types.js +62 -0
- package/src/testing/test-runner.js +46 -584
- package/src/testing/velocious-attempt-executor.js +431 -0
- package/src/testing/velocious-runner-reporter.js +166 -0
- package/src/testing/velocious-suite-hook-executor.js +71 -0
- package/src/testing/velocious-test-arguments.js +55 -0
- package/src/utils/sha256-bytes-hex.js +132 -0
- package/src/utils/sha256-hex.js +7 -135
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { TestDatabaseAccessRevokedError } from "../environment-handlers/base.js"
|
|
4
|
+
import { clearDeliveries } from "../mailer.js"
|
|
5
|
+
import restArgsError from "../utils/rest-args-error.js"
|
|
6
|
+
import { testConfig } from "./test.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Marks one whole-lifecycle timeout while its underlying promise keeps running.
|
|
10
|
+
* @typedef {Error & {velociousTestTimeout?: true}} TestTimeoutError
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs one promise with a lifecycle timeout.
|
|
15
|
+
* @param {Promise<ReturnType<typeof JSON.parse>> | ReturnType<typeof JSON.parse>} promise - Promise or value.
|
|
16
|
+
* @param {number} timeoutMs - Timeout in milliseconds.
|
|
17
|
+
* @param {string} testDescription - Test description.
|
|
18
|
+
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Lifecycle result.
|
|
19
|
+
*/
|
|
20
|
+
function runWithTimeout(promise, timeoutMs, testDescription) {
|
|
21
|
+
const timeoutSeconds = (timeoutMs / 1000).toFixed(3).replace(/\.?0+$/, "")
|
|
22
|
+
/** @type {TestTimeoutError} */
|
|
23
|
+
const timeoutError = new Error(`Timed out after ${timeoutSeconds}s: ${testDescription}`)
|
|
24
|
+
timeoutError.velociousTestTimeout = true
|
|
25
|
+
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const timeout = setTimeout(() => reject(timeoutError), timeoutMs)
|
|
28
|
+
|
|
29
|
+
Promise.resolve(promise).then((result) => {
|
|
30
|
+
clearTimeout(timeout)
|
|
31
|
+
resolve(result)
|
|
32
|
+
}).catch((error) => {
|
|
33
|
+
clearTimeout(timeout)
|
|
34
|
+
reject(error)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Waits for detached lifecycle cleanup up to the timeout grace period.
|
|
41
|
+
* @param {Promise<ReturnType<typeof JSON.parse>>} lifecycle - Detached lifecycle promise.
|
|
42
|
+
* @param {number} graceMs - Maximum wait.
|
|
43
|
+
* @returns {Promise<{settled: false} | {settled: true, status: "fulfilled"} | {settled: true, status: "rejected", reason: ReturnType<typeof JSON.parse>}>} - Settlement outcome.
|
|
44
|
+
*/
|
|
45
|
+
function awaitSettledOrGrace(lifecycle, graceMs) {
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
let settled = false
|
|
48
|
+
const graceTimer = setTimeout(() => {
|
|
49
|
+
if (settled) return
|
|
50
|
+
|
|
51
|
+
settled = true
|
|
52
|
+
resolve({settled: false})
|
|
53
|
+
}, graceMs)
|
|
54
|
+
|
|
55
|
+
Promise.resolve(lifecycle).then(
|
|
56
|
+
() => {
|
|
57
|
+
if (settled) return
|
|
58
|
+
|
|
59
|
+
settled = true
|
|
60
|
+
clearTimeout(graceTimer)
|
|
61
|
+
resolve({settled: true, status: "fulfilled"})
|
|
62
|
+
},
|
|
63
|
+
(reason) => {
|
|
64
|
+
if (settled) return
|
|
65
|
+
|
|
66
|
+
settled = true
|
|
67
|
+
clearTimeout(graceTimer)
|
|
68
|
+
resolve({settled: true, status: "rejected", reason})
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Checks whether a late lifecycle stopped only because its attempt access was revoked.
|
|
76
|
+
* @param {ReturnType<typeof JSON.parse>} error - Lifecycle rejection.
|
|
77
|
+
* @returns {boolean} - Whether every contained error is expected revocation.
|
|
78
|
+
*/
|
|
79
|
+
function isTestDatabaseAccessRevocation(error) {
|
|
80
|
+
if (error instanceof TestDatabaseAccessRevokedError) return true
|
|
81
|
+
if (error instanceof AggregateError) {
|
|
82
|
+
return error.errors.length > 0 && error.errors.every((nestedError) => isTestDatabaseAccessRevocation(nestedError))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default class VelociousAttemptExecutor {
|
|
89
|
+
/**
|
|
90
|
+
* Creates an executor for framework-owned attempt lifecycle work.
|
|
91
|
+
* @param {object} args - Constructor arguments.
|
|
92
|
+
* @param {import("./test-runner.js").default} args.testRunner - Owning Velocious runner.
|
|
93
|
+
*/
|
|
94
|
+
constructor({testRunner, ...restArgs}) {
|
|
95
|
+
restArgsError(restArgs)
|
|
96
|
+
this.testRunner = testRunner
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Executes exactly one complete Velocious-owned test attempt.
|
|
101
|
+
* @param {object} args - Attempt arguments.
|
|
102
|
+
* @param {import("./test-runner.js").AfterBeforeEachCallbackObjectType[]} args.afterEaches - Cleanup hooks.
|
|
103
|
+
* @param {number} args.attemptNumber - One-based attempt number.
|
|
104
|
+
* @param {import("./test-runner.js").AfterBeforeEachCallbackObjectType[]} args.beforeEaches - Setup hooks.
|
|
105
|
+
* @param {string[]} args.descriptions - Parent descriptions.
|
|
106
|
+
* @param {import("./velocious-test-arguments.js").TestArgs} args.testArgs - Stable test arguments.
|
|
107
|
+
* @param {import("./velocious-test-arguments.js").TestData} args.testData - Test registration.
|
|
108
|
+
* @param {string} args.testDescription - Test description.
|
|
109
|
+
* @param {number} [args.timeoutMs] - Whole-lifecycle timeout.
|
|
110
|
+
* @returns {Promise<{abortRemainingTests: boolean, consoleOutput: string, error: ReturnType<typeof JSON.parse>, failed: boolean}>} - Attempt outcome.
|
|
111
|
+
*/
|
|
112
|
+
async execute({afterEaches, attemptNumber, beforeEaches, descriptions, testArgs, testData, testDescription, timeoutMs, ...restArgs}) {
|
|
113
|
+
restArgsError(restArgs)
|
|
114
|
+
const testRunner = this.testRunner
|
|
115
|
+
/** @type {ReturnType<typeof JSON.parse>} */
|
|
116
|
+
let caughtError
|
|
117
|
+
let failed = false
|
|
118
|
+
/** @type {Promise<ReturnType<typeof JSON.parse>> | undefined} */
|
|
119
|
+
let testLifecycle
|
|
120
|
+
/** @type {{pool: import("../database/pool/base.js").default, registration: import("../database/pool/base.js").TestSharedConnectionRegistration}[]} */
|
|
121
|
+
let testSharedConnectionRegistrations = []
|
|
122
|
+
let testSharedConnectionsActive = false
|
|
123
|
+
/** @type {import("./test-runner.js").SharedTransactionBrokerRegistration | undefined} */
|
|
124
|
+
let sharedTransactionBrokerRegistration
|
|
125
|
+
/** @type {import("./test-runner.js").SharedTransactionBrokerRegistration | undefined} */
|
|
126
|
+
let sharedTransactionBrokerPreparation
|
|
127
|
+
/** @type {import("./test-runner.js").TransactionalTenantRegistration[]} */
|
|
128
|
+
const transactionalTenantRegistrations = []
|
|
129
|
+
/** @type {import("./test-runner.js").BrowserDummyConnectionRegistration[]} */
|
|
130
|
+
const browserDummyConnectionRegistrations = []
|
|
131
|
+
const testDatabaseAccessScope = {revoked: false}
|
|
132
|
+
/** @type {Set<Error>} */
|
|
133
|
+
const recordedTimeoutCleanupErrors = new Set()
|
|
134
|
+
let abortRemainingTests = false
|
|
135
|
+
let attemptTimedOut = false
|
|
136
|
+
/** @type {string} */
|
|
137
|
+
let consoleOutput
|
|
138
|
+
testArgs.registerTransactionalTenant = async (args) => {
|
|
139
|
+
await testRunner.registerTransactionalTenant(args, transactionalTenantRegistrations)
|
|
140
|
+
}
|
|
141
|
+
const stopConsoleCapture = testRunner.startConsoleCapture({
|
|
142
|
+
passthrough: testConfig.consoleOutput === "live"
|
|
143
|
+
})
|
|
144
|
+
const profiler = testRunner._profiler
|
|
145
|
+
const profileAttempt = profiler?.startAttempt({
|
|
146
|
+
descriptions,
|
|
147
|
+
attemptNumber,
|
|
148
|
+
testData,
|
|
149
|
+
testDescription
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const runLifecycleCallback = async () => await testRunner.runWithDummyIfNeeded(testArgs, async () => {
|
|
154
|
+
const useTransaction = testArgs.databaseCleaning?.transaction === true
|
|
155
|
+
const shouldTruncate = testArgs.databaseCleaning?.truncate ?? !useTransaction
|
|
156
|
+
const useSharedTestConnections = useTransaction || testArgs.type == "request"
|
|
157
|
+
const useTestConnections = useSharedTestConnections || shouldTruncate
|
|
158
|
+
const runTestAttempt = async () => {
|
|
159
|
+
if (useSharedTestConnections) {
|
|
160
|
+
testSharedConnectionRegistrations = testRunner.activateTestSharedConnections()
|
|
161
|
+
testSharedConnectionsActive = true
|
|
162
|
+
}
|
|
163
|
+
/** @type {ReturnType<typeof JSON.parse>[]} */
|
|
164
|
+
const lifecycleErrors = []
|
|
165
|
+
let runCleanupHooks = false
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
if (useSharedTestConnections) {
|
|
169
|
+
sharedTransactionBrokerPreparation = await testRunner.prepareSharedTransactionBroker()
|
|
170
|
+
}
|
|
171
|
+
runCleanupHooks = true
|
|
172
|
+
|
|
173
|
+
clearDeliveries()
|
|
174
|
+
await this.runBeforeEaches({beforeEaches, testArgs, testData})
|
|
175
|
+
|
|
176
|
+
if (useSharedTestConnections) {
|
|
177
|
+
const activeConnections = testRunner.sharedTransactionConnections({transactionsOnly: true})
|
|
178
|
+
if (sharedTransactionBrokerPreparation && !testRunner.sharedTransactionBrokerMatchesConnections(sharedTransactionBrokerPreparation, activeConnections)) {
|
|
179
|
+
testRunner.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
180
|
+
testSharedConnectionRegistrations = []
|
|
181
|
+
testSharedConnectionsActive = false
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
sharedTransactionBrokerRegistration = await testRunner.startSharedTransactionBroker(sharedTransactionBrokerPreparation, activeConnections)
|
|
185
|
+
sharedTransactionBrokerPreparation = undefined
|
|
186
|
+
if (sharedTransactionBrokerRegistration && !testSharedConnectionsActive) {
|
|
187
|
+
testSharedConnectionRegistrations = testRunner.activateTestSharedConnections()
|
|
188
|
+
testSharedConnectionsActive = true
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
testRunner._lastTestContext = {
|
|
193
|
+
fullDescription: testRunner.buildFullDescription(descriptions, testDescription),
|
|
194
|
+
filePath: testData.filePath ?? "<unknown>",
|
|
195
|
+
line: testData.line ?? 0
|
|
196
|
+
}
|
|
197
|
+
await testRunner.runProfileSpan({phase: "test body", filePath: testData.ownerFilePath ?? testData.filePath}, async () => {
|
|
198
|
+
await testData.function(testArgs)
|
|
199
|
+
})
|
|
200
|
+
} catch (error) {
|
|
201
|
+
lifecycleErrors.push(error)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (runCleanupHooks) {
|
|
205
|
+
try {
|
|
206
|
+
await testRunner.getConfiguration().awaitPendingBroadcasts()
|
|
207
|
+
} catch (error) {
|
|
208
|
+
lifecycleErrors.push(error)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
if (testSharedConnectionsActive) {
|
|
213
|
+
testRunner.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
214
|
+
testSharedConnectionRegistrations = []
|
|
215
|
+
testSharedConnectionsActive = false
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
lifecycleErrors.push(error)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await testRunner.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
|
|
223
|
+
sharedTransactionBrokerRegistration = undefined
|
|
224
|
+
sharedTransactionBrokerPreparation = undefined
|
|
225
|
+
} catch (error) {
|
|
226
|
+
lifecycleErrors.push(error)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
await this.runAfterEaches({afterEaches, testArgs, testData})
|
|
231
|
+
} catch (error) {
|
|
232
|
+
lifecycleErrors.push(error)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
try {
|
|
236
|
+
await testRunner.cleanupTransactionalTenants(transactionalTenantRegistrations)
|
|
237
|
+
} catch (error) {
|
|
238
|
+
lifecycleErrors.push(error)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (testSharedConnectionsActive) {
|
|
243
|
+
try {
|
|
244
|
+
testRunner.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
245
|
+
} catch (error) {
|
|
246
|
+
lifecycleErrors.push(error)
|
|
247
|
+
}
|
|
248
|
+
testSharedConnectionsActive = false
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (lifecycleErrors.length == 1) throw lifecycleErrors[0]
|
|
252
|
+
if (lifecycleErrors.length > 1) {
|
|
253
|
+
throw new AggregateError(lifecycleErrors, "Test lifecycle and cleanup failed", {cause: lifecycleErrors[0]})
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (useTestConnections) {
|
|
258
|
+
await testRunner.getConfiguration().ensureConnections({name: `Test: ${testDescription}`}, runTestAttempt)
|
|
259
|
+
} else {
|
|
260
|
+
await runTestAttempt()
|
|
261
|
+
}
|
|
262
|
+
}, browserDummyConnectionRegistrations)
|
|
263
|
+
const lifecycleCallback = async () => await testRunner.getConfiguration().runWithTestDatabaseAccessScope(testDatabaseAccessScope, runLifecycleCallback)
|
|
264
|
+
testLifecycle = profileAttempt && profiler
|
|
265
|
+
? profiler.runAttempt(profileAttempt, lifecycleCallback)
|
|
266
|
+
: lifecycleCallback()
|
|
267
|
+
|
|
268
|
+
if (timeoutMs !== undefined) {
|
|
269
|
+
await runWithTimeout(testLifecycle, timeoutMs, testDescription)
|
|
270
|
+
} else {
|
|
271
|
+
await testLifecycle
|
|
272
|
+
}
|
|
273
|
+
} catch (error) {
|
|
274
|
+
failed = true
|
|
275
|
+
caughtError = error
|
|
276
|
+
const timedOut = Boolean(/** @type {TestTimeoutError} */ (error)?.velociousTestTimeout)
|
|
277
|
+
attemptTimedOut = timedOut
|
|
278
|
+
|
|
279
|
+
if (timedOut && testLifecycle) {
|
|
280
|
+
const emergencyCleanupErrors = []
|
|
281
|
+
|
|
282
|
+
if (profileAttempt && profiler) profiler.finishAttempt(profileAttempt, "timed-out")
|
|
283
|
+
const lifecycleOutcome = await awaitSettledOrGrace(testLifecycle, timeoutMs ?? 60000)
|
|
284
|
+
|
|
285
|
+
if (lifecycleOutcome.settled && lifecycleOutcome.status === "rejected") {
|
|
286
|
+
emergencyCleanupErrors.push(lifecycleOutcome.reason)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!lifecycleOutcome.settled) {
|
|
290
|
+
testDatabaseAccessScope.revoked = true
|
|
291
|
+
void testLifecycle.catch((cleanupError) => {
|
|
292
|
+
if (isTestDatabaseAccessRevocation(cleanupError)) return
|
|
293
|
+
testRunner.recordTimeoutCleanupFailure(cleanupError, "test lifecycle", recordedTimeoutCleanupErrors)
|
|
294
|
+
})
|
|
295
|
+
const quarantine = testRunner.quarantineBrowserDummyConnections(browserDummyConnectionRegistrations)
|
|
296
|
+
const quarantineOutcome = await awaitSettledOrGrace(quarantine, timeoutMs ?? 60000)
|
|
297
|
+
const usesBrowserTransactions = testArgs.databaseCleaning?.transaction === true
|
|
298
|
+
const usesBrowserTruncation = testArgs.databaseCleaning?.truncate ?? !usesBrowserTransactions
|
|
299
|
+
|
|
300
|
+
abortRemainingTests = testRunner.isBrowserTestMode()
|
|
301
|
+
&& testRunner.hasTag(testArgs, "dummy")
|
|
302
|
+
&& (usesBrowserTransactions || usesBrowserTruncation)
|
|
303
|
+
|
|
304
|
+
if (quarantineOutcome.settled && quarantineOutcome.status === "rejected") {
|
|
305
|
+
emergencyCleanupErrors.push(quarantineOutcome.reason)
|
|
306
|
+
} else if (!quarantineOutcome.settled) {
|
|
307
|
+
void quarantine.catch((cleanupError) => {
|
|
308
|
+
testRunner.recordTimeoutCleanupFailure(cleanupError, "browser dummy connection quarantine", recordedTimeoutCleanupErrors)
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
if (testSharedConnectionsActive) {
|
|
315
|
+
testRunner.clearTestSharedConnections(testSharedConnectionRegistrations)
|
|
316
|
+
testSharedConnectionRegistrations = []
|
|
317
|
+
testSharedConnectionsActive = false
|
|
318
|
+
}
|
|
319
|
+
} catch (cleanupError) {
|
|
320
|
+
emergencyCleanupErrors.push(cleanupError)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const brokerCleanup = testRunner.stopSharedTransactionBroker(sharedTransactionBrokerRegistration || sharedTransactionBrokerPreparation)
|
|
324
|
+
const brokerCleanupOutcome = await awaitSettledOrGrace(brokerCleanup, timeoutMs ?? 60000)
|
|
325
|
+
|
|
326
|
+
if (brokerCleanupOutcome.settled && brokerCleanupOutcome.status === "rejected") {
|
|
327
|
+
emergencyCleanupErrors.push(brokerCleanupOutcome.reason)
|
|
328
|
+
} else if (!brokerCleanupOutcome.settled) {
|
|
329
|
+
void brokerCleanup.catch((cleanupError) => {
|
|
330
|
+
testRunner.recordTimeoutCleanupFailure(cleanupError, "shared transaction broker", recordedTimeoutCleanupErrors)
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
sharedTransactionBrokerRegistration = undefined
|
|
334
|
+
sharedTransactionBrokerPreparation = undefined
|
|
335
|
+
const emergencyCleanup = testRunner.cleanupTransactionalTenants(transactionalTenantRegistrations, {discard: true})
|
|
336
|
+
const emergencyCleanupOutcome = await awaitSettledOrGrace(emergencyCleanup, timeoutMs ?? 60000)
|
|
337
|
+
|
|
338
|
+
if (emergencyCleanupOutcome.settled && emergencyCleanupOutcome.status === "rejected") {
|
|
339
|
+
emergencyCleanupErrors.push(emergencyCleanupOutcome.reason)
|
|
340
|
+
} else if (!emergencyCleanupOutcome.settled) {
|
|
341
|
+
void emergencyCleanup.catch((cleanupError) => {
|
|
342
|
+
testRunner.recordTimeoutCleanupFailure(cleanupError, "transactional tenant", recordedTimeoutCleanupErrors)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (emergencyCleanupErrors.length > 0) {
|
|
347
|
+
caughtError = new AggregateError(
|
|
348
|
+
[caughtError, ...emergencyCleanupErrors],
|
|
349
|
+
"Test timeout and emergency cleanup failed",
|
|
350
|
+
{cause: caughtError}
|
|
351
|
+
)
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (browserDummyConnectionRegistrations.some((registration) => registration.quarantined)) {
|
|
356
|
+
testDatabaseAccessScope.revoked = true
|
|
357
|
+
abortRemainingTests = true
|
|
358
|
+
}
|
|
359
|
+
} finally {
|
|
360
|
+
testDatabaseAccessScope.revoked = true
|
|
361
|
+
consoleOutput = stopConsoleCapture()
|
|
362
|
+
|
|
363
|
+
if (profileAttempt && profiler) {
|
|
364
|
+
profiler.finishAttempt(profileAttempt, failed
|
|
365
|
+
? (attemptTimedOut ? "timed-out" : "failed")
|
|
366
|
+
: "passed")
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
abortRemainingTests,
|
|
372
|
+
consoleOutput,
|
|
373
|
+
error: caughtError,
|
|
374
|
+
failed
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Runs before-each hooks in inherited declaration order.
|
|
380
|
+
* @param {object} args - Hook arguments.
|
|
381
|
+
* @param {import("./test-runner.js").AfterBeforeEachCallbackObjectType[]} args.beforeEaches - Setup hooks.
|
|
382
|
+
* @param {import("./velocious-test-arguments.js").TestArgs} args.testArgs - Stable test arguments.
|
|
383
|
+
* @param {import("./velocious-test-arguments.js").TestData} args.testData - Test registration.
|
|
384
|
+
* @returns {Promise<void>} - Resolves after all setup hooks complete.
|
|
385
|
+
*/
|
|
386
|
+
async runBeforeEaches({beforeEaches, testArgs, testData}) {
|
|
387
|
+
for (const hook of beforeEaches) {
|
|
388
|
+
await this.testRunner.runProfileSpan({
|
|
389
|
+
phase: "beforeEach",
|
|
390
|
+
declarationIndex: hook.declarationIndex,
|
|
391
|
+
declarationScopeId: hook.declarationScopeId,
|
|
392
|
+
filePath: hook.ownerFilePath
|
|
393
|
+
}, async () => {
|
|
394
|
+
await hook.callback({configuration: this.testRunner.getConfiguration(), testArgs, testData})
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Runs every after-each hook while preserving all failures.
|
|
401
|
+
* @param {object} args - Hook arguments.
|
|
402
|
+
* @param {import("./test-runner.js").AfterBeforeEachCallbackObjectType[]} args.afterEaches - Cleanup hooks.
|
|
403
|
+
* @param {import("./velocious-test-arguments.js").TestArgs} args.testArgs - Stable test arguments.
|
|
404
|
+
* @param {import("./velocious-test-arguments.js").TestData} args.testData - Test registration.
|
|
405
|
+
* @returns {Promise<void>} - Resolves after every cleanup hook settles.
|
|
406
|
+
*/
|
|
407
|
+
async runAfterEaches({afterEaches, testArgs, testData}) {
|
|
408
|
+
/** @type {ReturnType<typeof JSON.parse>[]} */
|
|
409
|
+
const errors = []
|
|
410
|
+
|
|
411
|
+
for (const hook of afterEaches) {
|
|
412
|
+
try {
|
|
413
|
+
await this.testRunner.runProfileSpan({
|
|
414
|
+
phase: "afterEach",
|
|
415
|
+
declarationIndex: hook.declarationIndex,
|
|
416
|
+
declarationScopeId: hook.declarationScopeId,
|
|
417
|
+
filePath: hook.ownerFilePath
|
|
418
|
+
}, async () => {
|
|
419
|
+
await hook.callback({configuration: this.testRunner.getConfiguration(), testArgs, testData})
|
|
420
|
+
})
|
|
421
|
+
} catch (error) {
|
|
422
|
+
errors.push(error)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (errors.length == 1) throw errors[0]
|
|
427
|
+
if (errors.length > 1) {
|
|
428
|
+
throw new AggregateError(errors, "Multiple afterEach hooks failed", {cause: errors[0]})
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { addTrackedStackToError } from "../utils/with-tracked-stack.js"
|
|
4
|
+
import BacktraceCleaner from "../utils/backtrace-cleaner-node.js"
|
|
5
|
+
import picocolors from "picocolors"
|
|
6
|
+
import restArgsError from "../utils/rest-args-error.js"
|
|
7
|
+
import { testEvents } from "./test.js"
|
|
8
|
+
|
|
9
|
+
export default class VelociousRunnerReporter {
|
|
10
|
+
/**
|
|
11
|
+
* Creates the legacy event and result projection adapter.
|
|
12
|
+
* @param {object} args - Constructor arguments.
|
|
13
|
+
* @param {import("./test-runner.js").default} args.testRunner - Owning Velocious runner.
|
|
14
|
+
*/
|
|
15
|
+
constructor({testRunner, ...restArgs}) {
|
|
16
|
+
restArgsError(restArgs)
|
|
17
|
+
this.testRunner = testRunner
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Projects one completed attempt into legacy events and final result accounting.
|
|
22
|
+
* Retry eligibility is decided by the caller before this method runs.
|
|
23
|
+
* @param {object} args - Completed attempt and retry metadata.
|
|
24
|
+
* @param {import("./test-runner.js").AttemptConsoleOutput[]} args.attemptConsoleOutputs - Captured output across attempts.
|
|
25
|
+
* @param {number} args.attemptNumber - Current one-based attempt.
|
|
26
|
+
* @param {string[]} args.descriptions - Parent description stack.
|
|
27
|
+
* @param {ReturnType<typeof JSON.parse>} args.error - Raw thrown or rejected value.
|
|
28
|
+
* @param {boolean} args.failed - Whether the attempt failed, independently of error truthiness.
|
|
29
|
+
* @param {string} args.leftPadding - Console indentation.
|
|
30
|
+
* @param {number} args.retriesUsed - Retry count consumed after this attempt.
|
|
31
|
+
* @param {number} args.retryCount - Configured retry limit.
|
|
32
|
+
* @param {import("./velocious-test-arguments.js").TestArgs} args.testArgs - Stable test arguments.
|
|
33
|
+
* @param {import("./velocious-test-arguments.js").TestData} args.testData - Test registration.
|
|
34
|
+
* @param {string} args.testDescription - Test description.
|
|
35
|
+
* @param {boolean} args.willRetry - Whether the legacy loop will run another attempt.
|
|
36
|
+
* @returns {Promise<void>} - Resolves after all legacy listeners complete.
|
|
37
|
+
*/
|
|
38
|
+
async reportAttempt({attemptConsoleOutputs, attemptNumber, descriptions, error, failed, leftPadding, retriesUsed, retryCount, testArgs, testData, testDescription, willRetry, ...restArgs}) {
|
|
39
|
+
restArgsError(restArgs)
|
|
40
|
+
const testRunner = this.testRunner
|
|
41
|
+
|
|
42
|
+
if (!failed) testRunner._successfulTests++
|
|
43
|
+
|
|
44
|
+
if (failed) {
|
|
45
|
+
await this.emitEvent("testAttemptFailed", {
|
|
46
|
+
configuration: testRunner.getConfiguration(),
|
|
47
|
+
descriptions,
|
|
48
|
+
error,
|
|
49
|
+
attemptNumber,
|
|
50
|
+
nextAttempt: willRetry ? attemptNumber + 1 : undefined,
|
|
51
|
+
retriesUsed,
|
|
52
|
+
retryCount,
|
|
53
|
+
testArgs,
|
|
54
|
+
testData,
|
|
55
|
+
testDescription,
|
|
56
|
+
testRunner,
|
|
57
|
+
willRetry
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (willRetry) {
|
|
62
|
+
console.warn(picocolors.red(`${leftPadding} Retrying (${retriesUsed}/${retryCount}) after error: ${error instanceof Error ? error.message : String(error)}`))
|
|
63
|
+
await this.emitEvent("testRetrying", {
|
|
64
|
+
configuration: testRunner.getConfiguration(),
|
|
65
|
+
descriptions,
|
|
66
|
+
error,
|
|
67
|
+
nextAttempt: attemptNumber + 1,
|
|
68
|
+
retriesUsed,
|
|
69
|
+
retryCount,
|
|
70
|
+
testArgs,
|
|
71
|
+
testData,
|
|
72
|
+
testDescription,
|
|
73
|
+
testRunner
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (attemptNumber > 1) {
|
|
78
|
+
await this.emitEvent("testRetried", {
|
|
79
|
+
configuration: testRunner.getConfiguration(),
|
|
80
|
+
descriptions,
|
|
81
|
+
error,
|
|
82
|
+
attemptNumber,
|
|
83
|
+
retriesUsed,
|
|
84
|
+
retryCount,
|
|
85
|
+
testArgs,
|
|
86
|
+
testData,
|
|
87
|
+
testDescription,
|
|
88
|
+
testRunner
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (failed && !willRetry) {
|
|
93
|
+
await this.reportFailedTest({attemptConsoleOutputs, descriptions, error, leftPadding, testArgs, testData, testDescription})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Records and emits one final failed test result.
|
|
99
|
+
* @param {object} args - Final failure metadata.
|
|
100
|
+
* @param {import("./test-runner.js").AttemptConsoleOutput[]} args.attemptConsoleOutputs - Captured output across attempts.
|
|
101
|
+
* @param {string[]} args.descriptions - Parent description stack.
|
|
102
|
+
* @param {ReturnType<typeof JSON.parse>} args.error - Raw final failure.
|
|
103
|
+
* @param {string} args.leftPadding - Console indentation.
|
|
104
|
+
* @param {import("./velocious-test-arguments.js").TestArgs} args.testArgs - Stable test arguments.
|
|
105
|
+
* @param {import("./velocious-test-arguments.js").TestData} args.testData - Test registration.
|
|
106
|
+
* @param {string} args.testDescription - Test description.
|
|
107
|
+
* @returns {Promise<void>} - Resolves after the final-failure listener completes.
|
|
108
|
+
*/
|
|
109
|
+
async reportFailedTest({attemptConsoleOutputs, descriptions, error, leftPadding, testArgs, testData, testDescription}) {
|
|
110
|
+
const testRunner = this.testRunner
|
|
111
|
+
const consoleOutput = testRunner.buildConsoleOutput(attemptConsoleOutputs)
|
|
112
|
+
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
console.error(picocolors.red(`${leftPadding} Test failed: ${error.message}`))
|
|
115
|
+
addTrackedStackToError(error)
|
|
116
|
+
|
|
117
|
+
const backtraceCleaner = new BacktraceCleaner(error)
|
|
118
|
+
const cleanedStack = backtraceCleaner.getCleanedStack()
|
|
119
|
+
const stackLines = cleanedStack?.split("\n")
|
|
120
|
+
|
|
121
|
+
if (stackLines) {
|
|
122
|
+
for (const stackLine of stackLines) {
|
|
123
|
+
console.error(picocolors.red(`${leftPadding} ${stackLine}`))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
console.error(picocolors.red(`${leftPadding} Test failed with a ${typeof error}: ${String(error)}`))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
testRunner.printFailedConsoleOutput({consoleOutput, leftPadding})
|
|
131
|
+
testRunner._failedTests++
|
|
132
|
+
testRunner._failedTestDetails.push({
|
|
133
|
+
fullDescription: testRunner.buildFullDescription(descriptions, testDescription),
|
|
134
|
+
filePath: testData.filePath,
|
|
135
|
+
line: testData.line,
|
|
136
|
+
error,
|
|
137
|
+
consoleOutput: consoleOutput || undefined
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
await this.emitEvent("testFailed", {
|
|
141
|
+
configuration: testRunner.getConfiguration(),
|
|
142
|
+
descriptions,
|
|
143
|
+
error,
|
|
144
|
+
testArgs,
|
|
145
|
+
testData,
|
|
146
|
+
testDescription,
|
|
147
|
+
testRunner
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
testRunner.printRerunCommand({descriptions, testDescription, testData, leftPadding})
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Emits one legacy event and awaits listeners in registration order.
|
|
155
|
+
* @param {string} eventName - Event name.
|
|
156
|
+
* @param {object} payload - Event payload.
|
|
157
|
+
* @returns {Promise<void>} - Resolves when all listeners complete.
|
|
158
|
+
*/
|
|
159
|
+
async emitEvent(eventName, payload) {
|
|
160
|
+
const listeners = testEvents.listeners(eventName)
|
|
161
|
+
|
|
162
|
+
for (const listener of listeners) {
|
|
163
|
+
await listener(payload)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import restArgsError from "../utils/rest-args-error.js"
|
|
4
|
+
|
|
5
|
+
export default class VelociousSuiteHookExecutor {
|
|
6
|
+
/**
|
|
7
|
+
* Creates an executor for Velocious suite hooks.
|
|
8
|
+
* @param {object} args - Constructor arguments.
|
|
9
|
+
* @param {import("./test-runner.js").default} args.testRunner - Owning Velocious runner.
|
|
10
|
+
*/
|
|
11
|
+
constructor({testRunner, ...restArgs}) {
|
|
12
|
+
restArgsError(restArgs)
|
|
13
|
+
this.testRunner = testRunner
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs suite setup hooks in declaration order.
|
|
18
|
+
* @param {object} args - Hook execution arguments.
|
|
19
|
+
* @param {import("./test-runner.js").BeforeAfterAllCallbackObjectType[]} args.hooks - Profiled setup hooks.
|
|
20
|
+
* @returns {Promise<void>} - Resolves after every setup hook completes.
|
|
21
|
+
*/
|
|
22
|
+
async runBeforeAlls({hooks, ...restArgs}) {
|
|
23
|
+
restArgsError(restArgs)
|
|
24
|
+
|
|
25
|
+
for (const hook of hooks) {
|
|
26
|
+
await this.runHook(hook, "beforeAll")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Runs every suite teardown hook in reverse declaration order.
|
|
32
|
+
* @param {object} args - Hook execution arguments.
|
|
33
|
+
* @param {import("./test-runner.js").BeforeAfterAllCallbackObjectType[]} args.hooks - Profiled teardown hooks.
|
|
34
|
+
* @returns {Promise<void>} - Resolves after every teardown hook settles.
|
|
35
|
+
*/
|
|
36
|
+
async runAfterAlls({hooks, ...restArgs}) {
|
|
37
|
+
restArgsError(restArgs)
|
|
38
|
+
/** @type {ReturnType<typeof JSON.parse>[]} */
|
|
39
|
+
const errors = []
|
|
40
|
+
|
|
41
|
+
for (const hook of [...hooks].reverse()) {
|
|
42
|
+
try {
|
|
43
|
+
await this.runHook(hook, "afterAll")
|
|
44
|
+
} catch (error) {
|
|
45
|
+
errors.push(error)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (errors.length == 1) throw errors[0]
|
|
50
|
+
if (errors.length > 1) {
|
|
51
|
+
throw new AggregateError(errors, "Multiple afterAll hooks failed", {cause: errors[0]})
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Runs one suite hook with its Velocious profiler attribution.
|
|
57
|
+
* @param {import("./test-runner.js").BeforeAfterAllCallbackObjectType} hook - Hook registration.
|
|
58
|
+
* @param {"beforeAll" | "afterAll"} phase - Profiler phase.
|
|
59
|
+
* @returns {Promise<void>} - Resolves when the hook completes.
|
|
60
|
+
*/
|
|
61
|
+
async runHook(hook, phase) {
|
|
62
|
+
await this.testRunner.runProfileSpan({
|
|
63
|
+
phase,
|
|
64
|
+
declarationIndex: hook.declarationIndex,
|
|
65
|
+
declarationScopeId: hook.declarationScopeId,
|
|
66
|
+
filePath: hook.ownerFilePath
|
|
67
|
+
}, async () => {
|
|
68
|
+
await hook.callback({configuration: this.testRunner.getConfiguration()})
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
}
|