velocious 1.0.602 → 1.0.604
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 +38 -5
- package/build/configuration.js +19 -0
- package/build/database/drivers/base.js +296 -46
- package/build/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/build/database/drivers/mysql/index.js +42 -36
- package/build/database/pool/async-tracked-multi-connection.js +145 -42
- package/build/database/pool/base.js +20 -3
- package/build/environment-handlers/base.js +16 -0
- package/build/environment-handlers/node/cli/commands/test.js +220 -84
- package/build/environment-handlers/node.js +40 -1
- package/build/src/configuration.d.ts +9 -0
- package/build/src/configuration.d.ts.map +1 -1
- package/build/src/configuration.js +17 -1
- package/build/src/database/drivers/base.d.ts +150 -4
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +271 -44
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.d.ts +80 -0
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.d.ts.map +1 -0
- package/build/src/database/drivers/mysql/deadlock-diagnostic-parser.js +196 -0
- package/build/src/database/drivers/mysql/index.d.ts +21 -2
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +43 -36
- package/build/src/database/pool/async-tracked-multi-connection.d.ts +42 -2
- package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
- package/build/src/database/pool/async-tracked-multi-connection.js +127 -43
- package/build/src/database/pool/base.d.ts +18 -1
- package/build/src/database/pool/base.d.ts.map +1 -1
- package/build/src/database/pool/base.js +18 -3
- package/build/src/environment-handlers/base.d.ts +14 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +15 -1
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +22 -0
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +200 -82
- package/build/src/environment-handlers/node.d.ts +14 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +38 -2
- package/build/src/testing/test-filter-parser.d.ts +15 -0
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +57 -6
- package/build/src/testing/test-profile-activity.d.ts +7 -0
- package/build/src/testing/test-profile-activity.d.ts.map +1 -0
- package/build/src/testing/test-profile-activity.js +13 -0
- package/build/src/testing/test-profile-context.d.ts +15 -0
- package/build/src/testing/test-profile-context.d.ts.map +1 -0
- package/build/src/testing/test-profile-context.js +25 -0
- package/build/src/testing/test-profile-output.d.ts +30 -0
- package/build/src/testing/test-profile-output.d.ts.map +1 -0
- package/build/src/testing/test-profile-output.js +204 -0
- package/build/src/testing/test-profiler.d.ts +668 -0
- package/build/src/testing/test-profiler.d.ts.map +1 -0
- package/build/src/testing/test-profiler.js +847 -0
- package/build/src/testing/test-runner.d.ts +85 -2
- package/build/src/testing/test-runner.d.ts.map +1 -1
- package/build/src/testing/test-runner.js +182 -33
- package/build/testing/test-filter-parser.js +57 -5
- package/build/testing/test-profile-activity.js +14 -0
- package/build/testing/test-profile-context.js +28 -0
- package/build/testing/test-profile-output.js +240 -0
- package/build/testing/test-profiler.js +917 -0
- package/build/testing/test-runner.js +196 -33
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/configuration.js +19 -0
- package/src/database/drivers/base.js +296 -46
- package/src/database/drivers/mysql/deadlock-diagnostic-parser.js +227 -0
- package/src/database/drivers/mysql/index.js +42 -36
- package/src/database/pool/async-tracked-multi-connection.js +145 -42
- package/src/database/pool/base.js +20 -3
- package/src/environment-handlers/base.js +16 -0
- package/src/environment-handlers/node/cli/commands/test.js +220 -84
- package/src/environment-handlers/node.js +40 -1
- package/src/testing/test-filter-parser.js +57 -5
- package/src/testing/test-profile-activity.js +14 -0
- package/src/testing/test-profile-context.js +28 -0
- package/src/testing/test-profile-output.js +240 -0
- package/src/testing/test-profiler.js +917 -0
- package/src/testing/test-runner.js +196 -33
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import Configuration from "../../configuration.js"
|
|
4
4
|
import Logger from "../../logger.js"
|
|
5
5
|
import baseMethodsForward from "./base-methods-forward.js"
|
|
6
|
+
import sha256Hex from "../../utils/sha256-hex.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Opaque ownership handle for an in-process test shared connection registration.
|
|
@@ -44,7 +45,7 @@ import baseMethodsForward from "./base-methods-forward.js"
|
|
|
44
45
|
* @property {Array<DatabasePoolPendingCheckoutDebugSnapshot>} [pendingCheckouts] - Waiting checkout snapshots.
|
|
45
46
|
* @property {number} pendingCheckoutCount - Number of queued checkout requests.
|
|
46
47
|
* @property {string} poolClass - Pool class name.
|
|
47
|
-
* @property {{checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapDisposalCount: number}} [telemetry] - Cumulative pool lifecycle telemetry.
|
|
48
|
+
* @property {{connectionCreationCount: number, connectionCreationFailureCount: number, connectionCreationMaxMs: number, connectionCreationTotalMs: number, checkoutTimeoutCount: number, checkoutWaitCount: number, checkoutWaitMaxMs: number, checkoutWaitTotalMs: number, idleReapCount: number, idleReapDisposalCount: number, idleReapFailureCount: number, idleReapMaxMs: number, idleReapTotalMs: number, peakLiveConnections: number}} [telemetry] - Cumulative pool lifecycle telemetry.
|
|
48
49
|
*/
|
|
49
50
|
export const POOL_CONFIGURATION_KEY = Symbol("velociousPoolConfigurationKey")
|
|
50
51
|
|
|
@@ -337,15 +338,31 @@ class VelociousDatabasePoolBase {
|
|
|
337
338
|
async spawnConnectionForConfiguration(databaseConfiguration) {
|
|
338
339
|
const reuseKey = this.getConfigurationReuseKey(databaseConfiguration)
|
|
339
340
|
const connection = await this.spawnConnectionWithConfiguration(databaseConfiguration)
|
|
341
|
+
|
|
342
|
+
this.stampConnectionForConfigurationReuseKey(connection, reuseKey)
|
|
343
|
+
|
|
344
|
+
return connection
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Stamps a connected driver with its pool reuse identity and diagnostic-safe opaque identity.
|
|
349
|
+
* @param {import("../drivers/base.js").default} connection - Connected driver.
|
|
350
|
+
* @param {string} reuseKey - Exact physical configuration reuse key captured for this spawn.
|
|
351
|
+
* @returns {void}
|
|
352
|
+
*/
|
|
353
|
+
stampConnectionForConfigurationReuseKey(connection, reuseKey) {
|
|
340
354
|
const connectionWithPoolKey = /** @type {import("../drivers/base.js").default & {[POOL_CONFIGURATION_KEY]?: string}} */ (connection)
|
|
355
|
+
const databaseIdentityFingerprint = `sha256:${sha256Hex(`database-configuration-reuse:v1\0${reuseKey}`)}`
|
|
341
356
|
|
|
342
357
|
connectionWithPoolKey[POOL_CONFIGURATION_KEY] = reuseKey
|
|
358
|
+
connection.setPoolDiagnosticIdentity({
|
|
359
|
+
databaseIdentifier: this.identifier,
|
|
360
|
+
databaseIdentityFingerprint
|
|
361
|
+
})
|
|
343
362
|
connection.setSchemaCacheInvalidator(() => {
|
|
344
363
|
this.clearSchemaCache()
|
|
345
364
|
this.configuration.clearSchemaCachesForReuseKey(reuseKey)
|
|
346
365
|
})
|
|
347
|
-
|
|
348
|
-
return connection
|
|
349
366
|
}
|
|
350
367
|
|
|
351
368
|
/**
|
|
@@ -54,6 +54,22 @@ export default class VelociousEnvironmentHandlerBase {
|
|
|
54
54
|
*/
|
|
55
55
|
runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Runs work with test-profile attribution. Runtimes without async-context
|
|
59
|
+
* storage execute the callback without installing ambient attribution.
|
|
60
|
+
* @template T
|
|
61
|
+
* @param {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} _context - Captured profile context, or an explicit absence of attribution.
|
|
62
|
+
* @param {() => T} callback - Profiled work.
|
|
63
|
+
* @returns {T} - Callback result.
|
|
64
|
+
*/
|
|
65
|
+
runWithTestProfileContext(_context, callback) { return callback() }
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets the current test-profile attribution context.
|
|
69
|
+
* @returns {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} - Active context.
|
|
70
|
+
*/
|
|
71
|
+
getCurrentTestProfileContext() { return undefined }
|
|
72
|
+
|
|
57
73
|
/**
|
|
58
74
|
* Mutable ambient tenant used by runtimes without async-context storage.
|
|
59
75
|
* @type {ReturnType<typeof JSON.parse> | undefined}
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import BaseCommand from "../../../../cli/base-command.js"
|
|
4
4
|
import fs from "fs/promises"
|
|
5
|
+
import path from "node:path"
|
|
5
6
|
import picocolors from "picocolors"
|
|
6
7
|
import TestFilesFinder from "../../../../testing/test-files-finder.js"
|
|
8
|
+
import TestProfiler from "../../../../testing/test-profiler.js"
|
|
9
|
+
import { formatTestProfileSummary, writeTestProfileOutputs } from "../../../../testing/test-profile-output.js"
|
|
7
10
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
8
11
|
import TestSuiteSplitter from "../../../../testing/test-suite-splitter.js"
|
|
9
|
-
import {normalizeExamplePatterns, parseFilters} from "../../../../testing/test-filter-parser.js"
|
|
10
|
-
import {prepareSourcePeerPackage} from "../../source-peer-package.js"
|
|
12
|
+
import { normalizeExamplePatterns, parseFilters } from "../../../../testing/test-filter-parser.js"
|
|
13
|
+
import { prepareSourcePeerPackage } from "../../source-peer-package.js"
|
|
11
14
|
|
|
12
15
|
export default class VelociousCliCommandsTest extends BaseCommand {
|
|
13
16
|
async execute() {
|
|
@@ -27,119 +30,252 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
27
30
|
directories.push(`${this.directory()}/spec`)
|
|
28
31
|
}
|
|
29
32
|
|
|
30
|
-
const {
|
|
33
|
+
const {
|
|
34
|
+
includeTags,
|
|
35
|
+
excludeTags,
|
|
36
|
+
examplePatterns,
|
|
37
|
+
filteredProcessArgs,
|
|
38
|
+
groups,
|
|
39
|
+
groupNumber,
|
|
40
|
+
profile,
|
|
41
|
+
profileJsonPath,
|
|
42
|
+
timingManifestPath,
|
|
43
|
+
timingManifestOutputPath
|
|
44
|
+
} = parseFilters(this.processArgs || [])
|
|
45
|
+
const profileOptions = resolveTestProfileOptions({
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
profile,
|
|
48
|
+
profileJsonPath,
|
|
49
|
+
timingManifestPath,
|
|
50
|
+
timingManifestOutputPath
|
|
51
|
+
})
|
|
52
|
+
const selection = {
|
|
53
|
+
excludeTagCount: excludeTags.length,
|
|
54
|
+
hasExampleFilters: examplePatterns.length > 0,
|
|
55
|
+
includeTagCount: includeTags.length,
|
|
56
|
+
shard: groups !== undefined && groupNumber !== undefined ? {groups, groupNumber} : undefined
|
|
57
|
+
}
|
|
58
|
+
const profiler = profileOptions.profile
|
|
59
|
+
? new TestProfiler({configuration: this.getConfiguration(), projectDirectory: directory, selection})
|
|
60
|
+
: undefined
|
|
31
61
|
const testFilesFinder = new TestFilesFinder({directory, directories, processArgs: filteredProcessArgs})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
62
|
+
/** @type {TestRunner | undefined} */
|
|
63
|
+
let testRunner
|
|
64
|
+
let profileFinalized = false
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Finalizes requested outputs once for every command outcome.
|
|
68
|
+
* @param {string} status - Run status.
|
|
69
|
+
* @returns {Promise<void>} - Resolves after requested outputs are written.
|
|
70
|
+
*/
|
|
71
|
+
const finalizeProfile = async (status) => {
|
|
72
|
+
if (!profiler || profileFinalized) return
|
|
73
|
+
|
|
74
|
+
profileFinalized = true
|
|
75
|
+
const failed = testRunner?.getFailedTests() ?? 0
|
|
76
|
+
const passed = testRunner?.getSuccessfulTests() ?? 0
|
|
77
|
+
const profileDocument = profiler.finish({
|
|
78
|
+
counts: {
|
|
79
|
+
discovered: testRunner?.getTestsCount() ?? 0,
|
|
80
|
+
executed: testRunner?.getExecutedTestsCount() ?? 0,
|
|
81
|
+
failed,
|
|
82
|
+
passed
|
|
83
|
+
},
|
|
84
|
+
focused: Boolean(testRunner?.anyTestsFocussed),
|
|
85
|
+
status
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await writeTestProfileOutputs({
|
|
89
|
+
profile: profileDocument,
|
|
90
|
+
profileJsonPath: profileOptions.profileJsonPath,
|
|
91
|
+
timingManifestOutputPath: profileOptions.timingManifestOutputPath
|
|
92
|
+
})
|
|
93
|
+
console.log(`\n${formatTestProfileSummary(profileDocument, profileOptions)}`)
|
|
44
94
|
}
|
|
45
95
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
includeTags,
|
|
50
|
-
testFiles,
|
|
51
|
-
lineFilters: testFilesFinder.getLineFiltersByFile(),
|
|
52
|
-
examplePatterns: normalizeExamplePatterns(examplePatterns)
|
|
53
|
-
})
|
|
54
|
-
let signalHandled = false
|
|
96
|
+
try {
|
|
97
|
+
const discoverTestFiles = async () => {
|
|
98
|
+
let discoveredTestFiles = await testFilesFinder.findTestFiles()
|
|
55
99
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
100
|
+
if (groups !== undefined || groupNumber !== undefined) {
|
|
101
|
+
if (groups === undefined || groupNumber === undefined) {
|
|
102
|
+
throw new Error("Both --groups and --group-number must be provided together")
|
|
103
|
+
}
|
|
60
104
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
105
|
+
const timingManifest = await loadTimingManifest(profileOptions.timingManifestPath)
|
|
106
|
+
const splitter = new TestSuiteSplitter({
|
|
107
|
+
groups,
|
|
108
|
+
groupNumber,
|
|
109
|
+
testFiles: discoveredTestFiles,
|
|
110
|
+
baseDirectory: directory,
|
|
111
|
+
timingManifest
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
discoveredTestFiles = splitter.getGroupFiles()
|
|
115
|
+
console.log(picocolors.cyan(`Running group ${groupNumber} of ${groups} (${discoveredTestFiles.length} files)`))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return discoveredTestFiles
|
|
119
|
+
}
|
|
120
|
+
const testFiles = profiler
|
|
121
|
+
? await profiler.measurePhase("discovery", discoverTestFiles)
|
|
122
|
+
: await discoverTestFiles()
|
|
123
|
+
|
|
124
|
+
profiler?.setSelection({fileCount: testFiles.length})
|
|
125
|
+
testRunner = new TestRunner({
|
|
126
|
+
configuration: this.getConfiguration(),
|
|
127
|
+
excludeTags,
|
|
128
|
+
includeTags,
|
|
129
|
+
testFiles,
|
|
130
|
+
lineFilters: testFilesFinder.getLineFiltersByFile(),
|
|
131
|
+
examplePatterns: normalizeExamplePatterns(examplePatterns),
|
|
132
|
+
profiler
|
|
133
|
+
})
|
|
134
|
+
const activeTestRunner = testRunner
|
|
135
|
+
let signalHandled = false
|
|
136
|
+
|
|
137
|
+
const handleSignal = async (/** @type {string} */ signal) => {
|
|
138
|
+
if (signalHandled) return
|
|
139
|
+
signalHandled = true
|
|
140
|
+
profiler?.interrupt()
|
|
141
|
+
console.error(`\nReceived ${signal}, running afterAll hooks before exit...`)
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await activeTestRunner.runAfterAllsForActiveScopes()
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error("Failed while running afterAll hooks:", error)
|
|
147
|
+
} finally {
|
|
148
|
+
try {
|
|
149
|
+
await finalizeProfile("interrupted")
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error("Failed while writing interrupted test profile:", error)
|
|
152
|
+
}
|
|
153
|
+
process.exit(130)
|
|
154
|
+
}
|
|
67
155
|
}
|
|
68
|
-
}
|
|
69
156
|
|
|
70
|
-
|
|
71
|
-
|
|
157
|
+
process.once("SIGINT", () => { void handleSignal("SIGINT") })
|
|
158
|
+
process.once("SIGTERM", () => { void handleSignal("SIGTERM") })
|
|
72
159
|
|
|
73
|
-
|
|
160
|
+
await testRunner.prepare()
|
|
74
161
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
await testRunner.run()
|
|
162
|
+
if (testRunner.getTestsCount() === 0) {
|
|
163
|
+
await finalizeProfile("no-tests")
|
|
164
|
+
throw new Error(`${testRunner.getTestsCount()} tests was found in ${testFiles.length} file(s)`)
|
|
165
|
+
}
|
|
80
166
|
|
|
81
|
-
|
|
167
|
+
await testRunner.run()
|
|
82
168
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
169
|
+
const executedTests = testRunner.getExecutedTestsCount()
|
|
170
|
+
const lineFilters = testRunner.getLineFilters()
|
|
171
|
+
const hasLineFilters = Object.keys(lineFilters).length > 0
|
|
172
|
+
const hasExampleFilters = examplePatterns.length > 0
|
|
173
|
+
const hasTagFilters = includeTags.length > 0 || excludeTags.length > 0
|
|
87
174
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
175
|
+
if ((hasTagFilters || hasLineFilters || hasExampleFilters) && executedTests === 0) {
|
|
176
|
+
console.error(picocolors.red("\nNo tests matched the provided filters"))
|
|
177
|
+
await finalizeProfile("no-tests")
|
|
178
|
+
process.exit(1)
|
|
179
|
+
}
|
|
92
180
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
181
|
+
// Report the slowest tests so suite hotspots are visible every run. Defaults to
|
|
182
|
+
// the top 10; tune with VELOCIOUS_SLOW_TEST_COUNT (0 disables). Skipped for
|
|
183
|
+
// single-test runs where it would just be noise.
|
|
184
|
+
const slowTestCount = resolveSlowTestCount(process.env.VELOCIOUS_SLOW_TEST_COUNT)
|
|
97
185
|
|
|
98
|
-
|
|
99
|
-
|
|
186
|
+
if (slowTestCount > 0 && executedTests > 1) {
|
|
187
|
+
const slowestTests = testRunner.getSlowestTests(slowTestCount)
|
|
100
188
|
|
|
101
|
-
|
|
102
|
-
|
|
189
|
+
if (slowestTests.length > 0) {
|
|
190
|
+
console.log(picocolors.cyan(`\nSlowest ${slowestTests.length} tests:`))
|
|
103
191
|
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
for (const slowTest of slowestTests) {
|
|
193
|
+
const location = slowTest.filePath && slowTest.line ? ` (${slowTest.filePath}:${slowTest.line})` : ""
|
|
106
194
|
|
|
107
|
-
|
|
195
|
+
console.log(picocolors.cyan(` ${String(slowTest.durationMs).padStart(6)}ms ${slowTest.fullDescription}${location}`))
|
|
196
|
+
}
|
|
108
197
|
}
|
|
109
198
|
}
|
|
110
|
-
}
|
|
111
199
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
200
|
+
if (testRunner.isFailed()) {
|
|
201
|
+
await testRunner.persistFailedTestConsoleOutputsToAssets()
|
|
202
|
+
const failedTests = testRunner.getFailedTestDetails()
|
|
115
203
|
|
|
116
|
-
|
|
117
|
-
|
|
204
|
+
if (failedTests.length > 0) {
|
|
205
|
+
console.error(picocolors.red("\nFailed tests:"))
|
|
118
206
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
207
|
+
for (const failed of failedTests) {
|
|
208
|
+
const location = failed.filePath && failed.line
|
|
209
|
+
? ` (${failed.filePath}:${failed.line})`
|
|
210
|
+
: ""
|
|
211
|
+
console.error(picocolors.red(`- ${failed.fullDescription}${location}`))
|
|
124
212
|
|
|
125
|
-
|
|
126
|
-
|
|
213
|
+
if (failed.consoleLogPath) {
|
|
214
|
+
console.error(picocolors.red(` Console log: ${failed.consoleLogPath}`))
|
|
215
|
+
}
|
|
127
216
|
}
|
|
128
217
|
}
|
|
218
|
+
|
|
219
|
+
console.error(picocolors.red(`\nTest run failed with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`))
|
|
220
|
+
await finalizeProfile("failed")
|
|
221
|
+
process.exit(1)
|
|
222
|
+
} else if (testRunner.areAnyTestsFocussed()) {
|
|
223
|
+
console.error(picocolors.red(`\nFocussed run with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`))
|
|
224
|
+
await finalizeProfile("focused")
|
|
225
|
+
process.exit(1)
|
|
226
|
+
} else {
|
|
227
|
+
console.log(picocolors.green(`\nTest run succeeded with ${testRunner.getSuccessfulTests()} successful tests`))
|
|
228
|
+
await finalizeProfile("passed")
|
|
229
|
+
process.exit(0)
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
try {
|
|
233
|
+
await finalizeProfile("error")
|
|
234
|
+
} catch (profileError) {
|
|
235
|
+
throw new AggregateError([error, profileError], "Test command and profile finalization both failed", {cause: profileError})
|
|
129
236
|
}
|
|
130
237
|
|
|
131
|
-
|
|
132
|
-
process.exit(1)
|
|
133
|
-
} else if (testRunner.areAnyTestsFocussed()) {
|
|
134
|
-
console.error(picocolors.red(`\nFocussed run with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`))
|
|
135
|
-
process.exit(1)
|
|
136
|
-
} else {
|
|
137
|
-
console.log(picocolors.green(`\nTest run succeeded with ${testRunner.getSuccessfulTests()} successful tests`))
|
|
138
|
-
process.exit(0)
|
|
238
|
+
throw error
|
|
139
239
|
}
|
|
140
240
|
}
|
|
141
241
|
}
|
|
142
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Resolves and validates profiling paths before test discovery starts.
|
|
245
|
+
* @param {object} args - Raw profiling options.
|
|
246
|
+
* @param {string} args.cwd - Command working directory.
|
|
247
|
+
* @param {boolean} args.profile - Whether console profiling was requested.
|
|
248
|
+
* @param {string} [args.profileJsonPath] - Rich profile output path.
|
|
249
|
+
* @param {string} [args.timingManifestPath] - Timing manifest input path.
|
|
250
|
+
* @param {string} [args.timingManifestOutputPath] - Timing manifest output path.
|
|
251
|
+
* @returns {{profile: boolean, profileJsonPath: string | undefined, timingManifestPath: string | undefined, timingManifestOutputPath: string | undefined}} - Resolved profiling options.
|
|
252
|
+
*/
|
|
253
|
+
export function resolveTestProfileOptions({cwd, profile, profileJsonPath, timingManifestPath, timingManifestOutputPath}) {
|
|
254
|
+
const resolvedProfileJsonPath = profileJsonPath ? path.resolve(cwd, profileJsonPath) : undefined
|
|
255
|
+
const resolvedTimingManifestPath = timingManifestPath ? path.resolve(cwd, timingManifestPath) : undefined
|
|
256
|
+
const resolvedTimingManifestOutputPath = timingManifestOutputPath
|
|
257
|
+
? path.resolve(cwd, timingManifestOutputPath)
|
|
258
|
+
: undefined
|
|
259
|
+
|
|
260
|
+
if (resolvedProfileJsonPath && resolvedTimingManifestOutputPath && resolvedProfileJsonPath === resolvedTimingManifestOutputPath) {
|
|
261
|
+
throw new Error("Test profiling output paths must be different")
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (resolvedTimingManifestPath && (
|
|
265
|
+
resolvedProfileJsonPath === resolvedTimingManifestPath ||
|
|
266
|
+
resolvedTimingManifestOutputPath === resolvedTimingManifestPath
|
|
267
|
+
)) {
|
|
268
|
+
throw new Error("Test profiling outputs must not overwrite --timing-manifest input")
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
profile: profile || Boolean(resolvedProfileJsonPath || resolvedTimingManifestOutputPath),
|
|
273
|
+
profileJsonPath: resolvedProfileJsonPath,
|
|
274
|
+
timingManifestPath: resolvedTimingManifestPath,
|
|
275
|
+
timingManifestOutputPath: resolvedTimingManifestOutputPath
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
143
279
|
/**
|
|
144
280
|
* Loads an optional JSON timing manifest. Unreadable or malformed input intentionally falls back to heuristics.
|
|
145
281
|
* @param {string | undefined} timingManifestPath - Timing manifest path.
|
|
@@ -43,7 +43,7 @@ import { createSharedTransactionProxyDriver, sharedTransactionBrokerConfig } fro
|
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
45
|
* Defines this typedef.
|
|
46
|
-
* @typedef {{ability?: import("../authorization/ability.js").default, offsetMinutes: number, requestTiming?: import("../http-server/client/request-timing.js").default, tenant?: ReturnType<typeof JSON.parse>, timeZone?: string}} TimezoneStore */
|
|
46
|
+
* @typedef {{ability?: import("../authorization/ability.js").default, offsetMinutes: number, requestTiming?: import("../http-server/client/request-timing.js").default, tenant?: ReturnType<typeof JSON.parse>, testProfileContext?: import("../testing/test-profiler.js").TestProfileAsyncContext, timeZone?: string}} TimezoneStore */
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* Runs path within allowed prefixes.
|
|
@@ -147,6 +147,36 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
147
147
|
return this._sharedTransactionCoordinatorAsyncLocalStorage.run(owners, callback)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* Runs work with async-safe test profile attribution.
|
|
152
|
+
* @template T
|
|
153
|
+
* @param {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} context - Captured profile context, or an explicit absence of attribution.
|
|
154
|
+
* @param {() => T} callback - Profiled work.
|
|
155
|
+
* @returns {T} - Callback result.
|
|
156
|
+
*/
|
|
157
|
+
runWithTestProfileContext(context, callback) {
|
|
158
|
+
if (!this._timezoneAsyncLocalStorage) return callback()
|
|
159
|
+
|
|
160
|
+
const existingStore = this._timezoneAsyncLocalStorage.getStore()
|
|
161
|
+
|
|
162
|
+
return this._timezoneAsyncLocalStorage.run({
|
|
163
|
+
ability: existingStore?.ability,
|
|
164
|
+
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
165
|
+
requestTiming: existingStore?.requestTiming,
|
|
166
|
+
tenant: existingStore?.tenant,
|
|
167
|
+
testProfileContext: context,
|
|
168
|
+
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
169
|
+
}, callback)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Gets the current async-safe test profile attribution context.
|
|
174
|
+
* @returns {import("../testing/test-profiler.js").TestProfileAsyncContext | undefined} - Current context.
|
|
175
|
+
*/
|
|
176
|
+
getCurrentTestProfileContext() {
|
|
177
|
+
return this._timezoneAsyncLocalStorage?.getStore()?.testProfileContext
|
|
178
|
+
}
|
|
179
|
+
|
|
150
180
|
/**
|
|
151
181
|
* Find commands result.
|
|
152
182
|
* @type {import("./base.js").CommandFileObjectType[] | undefined} */
|
|
@@ -378,6 +408,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
378
408
|
offsetMinutes,
|
|
379
409
|
requestTiming: existingStore?.requestTiming,
|
|
380
410
|
tenant: existingStore?.tenant,
|
|
411
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
381
412
|
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
382
413
|
}, callback)
|
|
383
414
|
}
|
|
@@ -400,6 +431,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
400
431
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
401
432
|
requestTiming: existingStore?.requestTiming,
|
|
402
433
|
tenant: existingStore?.tenant,
|
|
434
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
403
435
|
timeZone: validateTimeZone(timeZone, "timeZone")
|
|
404
436
|
}, callback)
|
|
405
437
|
}
|
|
@@ -424,6 +456,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
424
456
|
offsetMinutes,
|
|
425
457
|
requestTiming: existingStore?.requestTiming,
|
|
426
458
|
tenant: existingStore?.tenant,
|
|
459
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
427
460
|
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
428
461
|
})
|
|
429
462
|
}
|
|
@@ -453,6 +486,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
453
486
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
454
487
|
requestTiming: existingStore?.requestTiming,
|
|
455
488
|
tenant: existingStore?.tenant,
|
|
489
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
456
490
|
timeZone: normalizedTimeZone
|
|
457
491
|
})
|
|
458
492
|
}
|
|
@@ -510,6 +544,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
510
544
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
511
545
|
requestTiming: existingStore?.requestTiming,
|
|
512
546
|
tenant: existingStore?.tenant,
|
|
547
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
513
548
|
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
514
549
|
}, callback)
|
|
515
550
|
}
|
|
@@ -535,6 +570,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
535
570
|
offsetMinutes: this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
536
571
|
requestTiming: undefined,
|
|
537
572
|
tenant: undefined,
|
|
573
|
+
testProfileContext: undefined,
|
|
538
574
|
timeZone: this.getTimeZone(this.getConfiguration())
|
|
539
575
|
})
|
|
540
576
|
}
|
|
@@ -570,6 +606,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
570
606
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
571
607
|
requestTiming,
|
|
572
608
|
tenant: existingStore?.tenant,
|
|
609
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
573
610
|
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
574
611
|
}, callback)
|
|
575
612
|
}
|
|
@@ -604,6 +641,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
604
641
|
offsetMinutes: existingStore?.offsetMinutes ?? this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
605
642
|
requestTiming: existingStore?.requestTiming,
|
|
606
643
|
tenant,
|
|
644
|
+
testProfileContext: existingStore?.testProfileContext,
|
|
607
645
|
timeZone: existingStore?.timeZone ?? this.getTimeZone(this.getConfiguration())
|
|
608
646
|
}, callback)
|
|
609
647
|
}
|
|
@@ -629,6 +667,7 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
629
667
|
offsetMinutes: this.getTimezoneOffsetMinutes(this.getConfiguration()),
|
|
630
668
|
requestTiming: undefined,
|
|
631
669
|
tenant,
|
|
670
|
+
testProfileContext: undefined,
|
|
632
671
|
timeZone: this.getTimeZone(this.getConfiguration())
|
|
633
672
|
})
|
|
634
673
|
}
|
|
@@ -1346,6 +1346,15 @@ export default class VelociousConfiguration {
|
|
|
1346
1346
|
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Callback result.
|
|
1347
1347
|
*/
|
|
1348
1348
|
runWithRequestTiming(requestTiming: import("./http-server/client/request-timing.js").default | undefined, callback: () => Promise<ReturnType<typeof JSON.parse>>): Promise<ReturnType<typeof JSON.parse>>;
|
|
1349
|
+
/**
|
|
1350
|
+
* Profiles an application-defined test activity when an opt-in test profile
|
|
1351
|
+
* context is active. The callback always runs, including outside profiling.
|
|
1352
|
+
* @template T
|
|
1353
|
+
* @param {string} name - Low-cardinality activity identifier.
|
|
1354
|
+
* @param {() => (T | Promise<T>)} callback - Activity callback.
|
|
1355
|
+
* @returns {Promise<T>} - Callback result.
|
|
1356
|
+
*/
|
|
1357
|
+
profileTestActivity<T>(name: string, callback: () => (T | Promise<T>)): Promise<T>;
|
|
1349
1358
|
/**
|
|
1350
1359
|
* Runs run with timezone.
|
|
1351
1360
|
* @param {string} timeZone - IANA timezone identifier.
|