velocious 1.0.604 → 1.0.605
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 +19 -10
- package/build/cli/commands/test/timing-manifest/merge.js +14 -0
- package/build/environment-handlers/base.js +9 -0
- package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +108 -0
- package/build/environment-handlers/node/cli/commands/test.js +48 -7
- package/build/environment-handlers/node.js +10 -0
- package/build/src/cli/commands/test/timing-manifest/merge.d.ts +10 -0
- package/build/src/cli/commands/test/timing-manifest/merge.d.ts.map +1 -0
- package/build/src/cli/commands/test/timing-manifest/merge.js +13 -0
- package/build/src/environment-handlers/base.d.ts +6 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +9 -1
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts +32 -0
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -0
- package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +97 -0
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +3 -3
- package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
- package/build/src/environment-handlers/node/cli/commands/test.js +36 -9
- package/build/src/environment-handlers/node.d.ts +6 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +10 -1
- package/build/src/testing/test-profile-output.d.ts +11 -0
- package/build/src/testing/test-profile-output.d.ts.map +1 -1
- package/build/src/testing/test-profile-output.js +21 -7
- package/build/src/testing/test-profiler.d.ts.map +1 -1
- package/build/src/testing/test-profiler.js +3 -2
- package/build/src/testing/test-suite-splitter.d.ts +33 -0
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +84 -8
- package/build/src/testing/timing-manifest.d.ts +94 -0
- package/build/src/testing/timing-manifest.d.ts.map +1 -0
- package/build/src/testing/timing-manifest.js +284 -0
- package/build/testing/test-profile-output.js +23 -8
- package/build/testing/test-profiler.js +2 -1
- package/build/testing/test-suite-splitter.js +91 -7
- package/build/testing/timing-manifest.js +346 -0
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/bootstrap-provider-runtime.sh +249 -0
- package/scripts/verify-docker-dev-environment.js +219 -29
- package/src/cli/commands/test/timing-manifest/merge.js +14 -0
- package/src/environment-handlers/base.js +9 -0
- package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +108 -0
- package/src/environment-handlers/node/cli/commands/test.js +48 -7
- package/src/environment-handlers/node.js +10 -0
- package/src/testing/test-profile-output.js +23 -8
- package/src/testing/test-profiler.js +2 -1
- package/src/testing/test-suite-splitter.js +91 -7
- package/src/testing/timing-manifest.js +346 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import BaseCommand from "../../../../../../cli/base-command.js"
|
|
4
|
+
import fs from "node:fs/promises"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { writeTimingManifest } from "../../../../../../testing/test-profile-output.js"
|
|
7
|
+
import { mergeTestProfileTimingManifests } from "../../../../../../testing/timing-manifest.js"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} TimingManifestMergeArguments
|
|
11
|
+
* @property {string[]} inputPaths - Rich profile input paths.
|
|
12
|
+
* @property {string} outputPath - Plain timing manifest output path.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Node implementation for timing-manifest aggregation. */
|
|
16
|
+
export default class TestTimingManifestMerge extends BaseCommand {
|
|
17
|
+
/**
|
|
18
|
+
* Runs execute.
|
|
19
|
+
* @returns {Promise<Record<string, number>>} - Complete merged timing manifest.
|
|
20
|
+
*/
|
|
21
|
+
async execute() {
|
|
22
|
+
const {inputPaths, outputPath} = parseTimingManifestMergeArguments(this.processArgs || [], process.cwd())
|
|
23
|
+
const inputs = []
|
|
24
|
+
|
|
25
|
+
for (const inputPath of inputPaths) {
|
|
26
|
+
let content
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
content = await fs.readFile(inputPath, "utf8")
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Failed to read test profile: ${inputPath}`, {cause: error})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let profile
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
profile = JSON.parse(content)
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`Failed to parse test profile: ${inputPath}`, {cause: error})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
inputs.push({profile, source: inputPath})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const timingManifest = mergeTestProfileTimingManifests(inputs)
|
|
46
|
+
|
|
47
|
+
await writeTimingManifest({outputPath, timingManifest})
|
|
48
|
+
console.log(`Merged ${inputPaths.length} test profile shards into ${outputPath} (${Object.keys(timingManifest).length} files)`)
|
|
49
|
+
|
|
50
|
+
return timingManifest
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Recognizes one output option spelling.
|
|
56
|
+
* @param {string} argument - Current argument.
|
|
57
|
+
* @param {string | undefined} nextArgument - Following argument.
|
|
58
|
+
* @returns {{matched: boolean, skipNext: boolean, value: string | undefined}} - Parsed output option.
|
|
59
|
+
*/
|
|
60
|
+
function timingManifestOutputArgument(argument, nextArgument) {
|
|
61
|
+
if (argument === "--output") {
|
|
62
|
+
return {matched: true, skipNext: true, value: nextArgument}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (argument.startsWith("--output=")) {
|
|
66
|
+
return {matched: true, skipNext: false, value: argument.slice("--output=".length)}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {matched: false, skipNext: false, value: undefined}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses strict merge arguments and resolves their paths.
|
|
74
|
+
* @param {string[]} processArgs - Raw CLI arguments, including command name.
|
|
75
|
+
* @param {string} cwd - Command working directory.
|
|
76
|
+
* @returns {TimingManifestMergeArguments} - Validated resolved paths.
|
|
77
|
+
*/
|
|
78
|
+
export function parseTimingManifestMergeArguments(processArgs, cwd) {
|
|
79
|
+
const commandName = processArgs[0] || "test:timing-manifest:merge"
|
|
80
|
+
const inputPaths = []
|
|
81
|
+
let outputPath
|
|
82
|
+
|
|
83
|
+
for (let index = 1; index < processArgs.length; index++) {
|
|
84
|
+
const argument = processArgs[index]
|
|
85
|
+
const outputArgument = timingManifestOutputArgument(argument, processArgs[index + 1])
|
|
86
|
+
|
|
87
|
+
if (outputArgument.matched) {
|
|
88
|
+
if (!outputArgument.value || outputArgument.value.startsWith("-")) throw new Error("Missing value for --output")
|
|
89
|
+
if (outputPath) throw new Error("--output may only be provided once")
|
|
90
|
+
outputPath = path.resolve(cwd, outputArgument.value)
|
|
91
|
+
if (outputArgument.skipNext) index++
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (argument.startsWith("-")) throw new Error(`Unknown argument for ${commandName}: ${argument}`)
|
|
96
|
+
inputPaths.push(path.resolve(cwd, argument))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!outputPath) throw new Error("--output is required")
|
|
100
|
+
if (inputPaths.length === 0) throw new Error("At least one rich test profile input is required")
|
|
101
|
+
|
|
102
|
+
const uniqueInputPaths = new Set(inputPaths)
|
|
103
|
+
|
|
104
|
+
if (uniqueInputPaths.size !== inputPaths.length) throw new Error("Each rich test profile input must be provided once")
|
|
105
|
+
if (uniqueInputPaths.has(outputPath)) throw new Error("Timing manifest output must not overwrite an input profile")
|
|
106
|
+
|
|
107
|
+
return {inputPaths, outputPath}
|
|
108
|
+
}
|
|
@@ -10,6 +10,11 @@ import { formatTestProfileSummary, writeTestProfileOutputs } from "../../../../t
|
|
|
10
10
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
11
11
|
import TestSuiteSplitter from "../../../../testing/test-suite-splitter.js"
|
|
12
12
|
import { normalizeExamplePatterns, parseFilters } from "../../../../testing/test-filter-parser.js"
|
|
13
|
+
import {
|
|
14
|
+
canonicalTimingManifestPath,
|
|
15
|
+
timingManifestFileSetHash,
|
|
16
|
+
validateTimingManifest
|
|
17
|
+
} from "../../../../testing/timing-manifest.js"
|
|
13
18
|
import { prepareSourcePeerPackage } from "../../source-peer-package.js"
|
|
14
19
|
|
|
15
20
|
export default class VelociousCliCommandsTest extends BaseCommand {
|
|
@@ -95,14 +100,28 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
95
100
|
|
|
96
101
|
try {
|
|
97
102
|
const discoverTestFiles = async () => {
|
|
103
|
+
const timingManifest = await loadTimingManifest(profileOptions.timingManifestPath)
|
|
98
104
|
let discoveredTestFiles = await testFilesFinder.findTestFiles()
|
|
105
|
+
const lineFilters = testFilesFinder.getLineFiltersByFile()
|
|
106
|
+
|
|
107
|
+
if (profiler) {
|
|
108
|
+
const discoveredFilePaths = discoveredTestFiles.map((filePath) => {
|
|
109
|
+
return canonicalTimingManifestPath(path.relative(directory, filePath))
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
profiler.setSelection({
|
|
113
|
+
discoveredFileCount: discoveredTestFiles.length,
|
|
114
|
+
hasLineFilters: Object.keys(lineFilters).length > 0,
|
|
115
|
+
pathBase: process.env.VELOCIOUS_TEST_DIR ? "test-directory" : "configuration-directory",
|
|
116
|
+
testFileSetHash: timingManifestFileSetHash(discoveredFilePaths)
|
|
117
|
+
})
|
|
118
|
+
}
|
|
99
119
|
|
|
100
120
|
if (groups !== undefined || groupNumber !== undefined) {
|
|
101
121
|
if (groups === undefined || groupNumber === undefined) {
|
|
102
122
|
throw new Error("Both --groups and --group-number must be provided together")
|
|
103
123
|
}
|
|
104
124
|
|
|
105
|
-
const timingManifest = await loadTimingManifest(profileOptions.timingManifestPath)
|
|
106
125
|
const splitter = new TestSuiteSplitter({
|
|
107
126
|
groups,
|
|
108
127
|
groupNumber,
|
|
@@ -111,6 +130,15 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
111
130
|
timingManifest
|
|
112
131
|
})
|
|
113
132
|
|
|
133
|
+
if (profileOptions.timingManifestPath) {
|
|
134
|
+
const coverage = splitter.getTimingManifestCoverage()
|
|
135
|
+
|
|
136
|
+
console.log(picocolors.cyan(
|
|
137
|
+
`Timing manifest coverage: measured=${coverage.measuredFiles} ` +
|
|
138
|
+
`heuristic=${coverage.heuristicFiles} stale=${coverage.staleEntries}`
|
|
139
|
+
))
|
|
140
|
+
}
|
|
141
|
+
|
|
114
142
|
discoveredTestFiles = splitter.getGroupFiles()
|
|
115
143
|
console.log(picocolors.cyan(`Running group ${groupNumber} of ${groups} (${discoveredTestFiles.length} files)`))
|
|
116
144
|
}
|
|
@@ -158,6 +186,9 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
158
186
|
process.once("SIGTERM", () => { void handleSignal("SIGTERM") })
|
|
159
187
|
|
|
160
188
|
await testRunner.prepare()
|
|
189
|
+
const effectiveExcludeTagCount = testRunner.getExcludeTagSet().size
|
|
190
|
+
|
|
191
|
+
profiler?.setSelection({excludeTagCount: effectiveExcludeTagCount})
|
|
161
192
|
|
|
162
193
|
if (testRunner.getTestsCount() === 0) {
|
|
163
194
|
await finalizeProfile("no-tests")
|
|
@@ -170,7 +201,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
170
201
|
const lineFilters = testRunner.getLineFilters()
|
|
171
202
|
const hasLineFilters = Object.keys(lineFilters).length > 0
|
|
172
203
|
const hasExampleFilters = examplePatterns.length > 0
|
|
173
|
-
const hasTagFilters = includeTags.length > 0 ||
|
|
204
|
+
const hasTagFilters = includeTags.length > 0 || effectiveExcludeTagCount > 0
|
|
174
205
|
|
|
175
206
|
if ((hasTagFilters || hasLineFilters || hasExampleFilters) && executedTests === 0) {
|
|
176
207
|
console.error(picocolors.red("\nNo tests matched the provided filters"))
|
|
@@ -277,20 +308,30 @@ export function resolveTestProfileOptions({cwd, profile, profileJsonPath, timing
|
|
|
277
308
|
}
|
|
278
309
|
|
|
279
310
|
/**
|
|
280
|
-
* Loads an
|
|
311
|
+
* Loads and validates an explicitly supplied plain JSON timing manifest.
|
|
281
312
|
* @param {string | undefined} timingManifestPath - Timing manifest path.
|
|
282
|
-
* @returns {Promise<
|
|
313
|
+
* @returns {Promise<Record<string, number> | undefined>} - Canonical manifest, or undefined when not requested.
|
|
283
314
|
*/
|
|
284
315
|
export async function loadTimingManifest(timingManifestPath) {
|
|
285
316
|
if (!timingManifestPath) return undefined
|
|
286
317
|
|
|
318
|
+
let content
|
|
319
|
+
|
|
287
320
|
try {
|
|
288
|
-
|
|
321
|
+
content = await fs.readFile(timingManifestPath, "utf8")
|
|
289
322
|
} catch (error) {
|
|
290
|
-
|
|
323
|
+
throw new Error(`Failed to read timing manifest: ${timingManifestPath}`, {cause: error})
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
let parsed
|
|
291
327
|
|
|
292
|
-
|
|
328
|
+
try {
|
|
329
|
+
parsed = JSON.parse(content)
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new Error(`Failed to parse timing manifest: ${timingManifestPath}`, {cause: error})
|
|
293
332
|
}
|
|
333
|
+
|
|
334
|
+
return validateTimingManifest(parsed, {source: `Timing manifest ${timingManifestPath}`})
|
|
294
335
|
}
|
|
295
336
|
|
|
296
337
|
/**
|
|
@@ -14,6 +14,7 @@ import CliCommandsLintRelationships from "./node/cli/commands/lint/relationships
|
|
|
14
14
|
import CliCommandsRoutes from "./node/cli/commands/routes.js"
|
|
15
15
|
import CliCommandsServer from "./node/cli/commands/server.js"
|
|
16
16
|
import CliCommandsTest from "./node/cli/commands/test.js"
|
|
17
|
+
import CliCommandsTestTimingManifestMerge from "./node/cli/commands/test/timing-manifest/merge.js"
|
|
17
18
|
import CliCommandsBackgroundJobsMain from "./node/cli/commands/background-jobs-main.js"
|
|
18
19
|
import CliCommandsBackgroundJobsWorker from "./node/cli/commands/background-jobs-worker.js"
|
|
19
20
|
import CliCommandsBackgroundJobsRunner from "./node/cli/commands/background-jobs-runner.js"
|
|
@@ -830,6 +831,15 @@ export default class VelociousEnvironmentHandlerNode extends Base{
|
|
|
830
831
|
return await this.forwardCommand(command, CliCommandsTest)
|
|
831
832
|
}
|
|
832
833
|
|
|
834
|
+
/**
|
|
835
|
+
* Runs cli commands test timing manifest merge.
|
|
836
|
+
* @param {import("../cli/base-command.js").default} command - Command.
|
|
837
|
+
* @returns {Promise<ReturnType<typeof JSON.parse>>} - Resolves with the command result.
|
|
838
|
+
*/
|
|
839
|
+
async cliCommandsTestTimingManifestMerge(command) {
|
|
840
|
+
return await this.forwardCommand(command, CliCommandsTestTimingManifestMerge)
|
|
841
|
+
}
|
|
842
|
+
|
|
833
843
|
/**
|
|
834
844
|
* Runs cli commands background jobs main.
|
|
835
845
|
* @param {import("../cli/base-command.js").default} command - Command.
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from "node:fs/promises"
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { roundProfileDuration } from "./test-profiler.js"
|
|
6
|
+
import { validateTimingManifest } from "./timing-manifest.js"
|
|
6
7
|
|
|
7
8
|
const FORBIDDEN_PROFILE_FIELDS = new Set([
|
|
8
9
|
"bind",
|
|
@@ -156,20 +157,34 @@ async function atomicWrite(outputPath, content) {
|
|
|
156
157
|
export function timingManifestFromProfile(profile) {
|
|
157
158
|
/** @type {Record<string, number>} */
|
|
158
159
|
const manifest = {}
|
|
160
|
+
const validatedManifest = validateTimingManifest(profile.timingManifest || {}, {source: "Test profile timing manifest"})
|
|
159
161
|
|
|
160
|
-
for (const filePath of Object.
|
|
161
|
-
const durationMs = profile.timingManifest[filePath]
|
|
162
|
-
|
|
163
|
-
if (typeof durationMs !== "number" || !Number.isFinite(durationMs) || durationMs < 0) {
|
|
164
|
-
throw new Error(`Invalid timing manifest duration for ${filePath}`)
|
|
165
|
-
}
|
|
166
|
-
|
|
162
|
+
for (const [filePath, durationMs] of Object.entries(validatedManifest)) {
|
|
167
163
|
manifest[filePath] = roundProfileDuration(durationMs)
|
|
168
164
|
}
|
|
169
165
|
|
|
170
166
|
return manifest
|
|
171
167
|
}
|
|
172
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Atomically writes a canonical splitter-compatible timing manifest.
|
|
171
|
+
* @param {object} args - Output arguments.
|
|
172
|
+
* @param {string} args.outputPath - Final output path.
|
|
173
|
+
* @param {Record<string, number>} args.timingManifest - Validated or candidate timing manifest.
|
|
174
|
+
* @returns {Promise<void>} - Resolves after atomic replacement.
|
|
175
|
+
*/
|
|
176
|
+
export async function writeTimingManifest({outputPath, timingManifest}) {
|
|
177
|
+
const normalized = validateTimingManifest(timingManifest)
|
|
178
|
+
/** @type {Record<string, number>} */
|
|
179
|
+
const rounded = {}
|
|
180
|
+
|
|
181
|
+
for (const [filePath, durationMs] of Object.entries(normalized)) {
|
|
182
|
+
rounded[filePath] = roundProfileDuration(durationMs)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
await atomicWrite(outputPath, `${JSON.stringify(rounded, null, 2)}\n`)
|
|
186
|
+
}
|
|
187
|
+
|
|
173
188
|
/**
|
|
174
189
|
* Atomically writes requested test profile outputs.
|
|
175
190
|
* @param {object} args - Output options.
|
|
@@ -189,7 +204,7 @@ export async function writeTestProfileOutputs({profile, profileJsonPath, timingM
|
|
|
189
204
|
}
|
|
190
205
|
|
|
191
206
|
if (timingManifestOutputPath) {
|
|
192
|
-
await
|
|
207
|
+
await writeTimingManifest({outputPath: timingManifestOutputPath, timingManifest})
|
|
193
208
|
}
|
|
194
209
|
}
|
|
195
210
|
|
|
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto"
|
|
|
4
4
|
import path from "node:path"
|
|
5
5
|
import { registerTestProfileContextReader } from "./test-profile-context.js"
|
|
6
6
|
import { validateTestActivityName } from "./test-profile-activity.js"
|
|
7
|
+
import { compareTimingManifestPaths } from "./timing-manifest.js"
|
|
7
8
|
|
|
8
9
|
/** @typedef {"passed" | "failed" | "interrupted" | "timed-out"} TestProfileAttemptStatus */
|
|
9
10
|
/** @typedef {{user: number, system: number}} ProcessCpuUsage */
|
|
@@ -625,7 +626,7 @@ export default class TestProfiler {
|
|
|
625
626
|
attemptsMs: roundProfileDuration(file.attemptsMs),
|
|
626
627
|
totalMs: roundProfileDuration(file.totalMs)
|
|
627
628
|
}))
|
|
628
|
-
.sort((fileA, fileB) => fileA.path
|
|
629
|
+
.sort((fileA, fileB) => compareTimingManifestPaths(fileA.path, fileB.path))
|
|
629
630
|
const timingManifest = Object.fromEntries(files.map((file) => [file.path, file.totalMs]))
|
|
630
631
|
const attempts = this._tests.reduce((sum, test) => sum + test.attempts.length, 0)
|
|
631
632
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import path from "path"
|
|
4
4
|
import restArgsError from "../utils/rest-args-error.js"
|
|
5
|
+
import { canonicalTimingManifestPath, compareTimingManifestPaths } from "./timing-manifest.js"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* SplitterFileEntry type.
|
|
@@ -95,14 +96,13 @@ export default class TestSuiteSplitter {
|
|
|
95
96
|
* @returns {number} - Weight value.
|
|
96
97
|
*/
|
|
97
98
|
computeWeight(filePath) {
|
|
98
|
-
const
|
|
99
|
-
const projectRelativePath = this.normalizeRelativePath(path.join(path.basename(this._baseDirectory), relativePath))
|
|
100
|
-
const duration = this._timingManifest[relativePath] ?? this._timingManifest[projectRelativePath]
|
|
99
|
+
const duration = this.timingManifestDuration(filePath)
|
|
101
100
|
|
|
102
|
-
if (duration !== undefined) {
|
|
101
|
+
if (duration !== undefined && duration > 0) {
|
|
103
102
|
return duration
|
|
104
103
|
}
|
|
105
104
|
|
|
105
|
+
const relativePath = this.heuristicRelativePath(filePath)
|
|
106
106
|
let weight = DEFAULT_WEIGHT
|
|
107
107
|
|
|
108
108
|
// Extract the type directory from the relative path.
|
|
@@ -139,8 +139,12 @@ export default class TestSuiteSplitter {
|
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
for (const [filePath, duration] of Object.entries(timingManifest)) {
|
|
142
|
-
if (typeof duration
|
|
142
|
+
if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) continue
|
|
143
|
+
|
|
144
|
+
try {
|
|
143
145
|
normalized[this.normalizeRelativePath(filePath)] = duration
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (!(error instanceof Error)) throw error
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
@@ -153,11 +157,91 @@ export default class TestSuiteSplitter {
|
|
|
153
157
|
* @returns {string} - Normalized relative test path.
|
|
154
158
|
*/
|
|
155
159
|
normalizeRelativePath(filePath) {
|
|
156
|
-
return filePath
|
|
160
|
+
return canonicalTimingManifestPath(filePath)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Returns the permissive relative path used only for heuristic classification.
|
|
165
|
+
* @param {string} filePath - Absolute test file path.
|
|
166
|
+
* @returns {string} - Portable relative path which may escape the profiling base.
|
|
167
|
+
*/
|
|
168
|
+
heuristicRelativePath(filePath) {
|
|
169
|
+
return path.relative(this._baseDirectory, filePath)
|
|
157
170
|
.replaceAll("\\", "/")
|
|
158
171
|
.replace(/^\.\//, "")
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Returns a canonical manifest key when the file is representable under the profiling base.
|
|
176
|
+
* @param {string} filePath - Absolute test file path.
|
|
177
|
+
* @returns {string | undefined} - Canonical relative path, or undefined for an external file.
|
|
178
|
+
*/
|
|
179
|
+
manifestRelativePath(filePath) {
|
|
180
|
+
const relativePath = path.relative(this._baseDirectory, filePath)
|
|
181
|
+
|
|
182
|
+
if (!relativePath || path.isAbsolute(relativePath)) return
|
|
183
|
+
if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) return
|
|
184
|
+
|
|
185
|
+
return this.normalizeRelativePath(relativePath)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Returns the first manifest entry matching a discovered test file.
|
|
190
|
+
* @param {string} filePath - Absolute test file path.
|
|
191
|
+
* @returns {number | undefined} - Recorded duration, including zero.
|
|
192
|
+
*/
|
|
193
|
+
timingManifestDuration(filePath) {
|
|
194
|
+
for (const manifestPath of this.timingManifestPaths(filePath)) {
|
|
195
|
+
if (Object.hasOwn(this._timingManifest, manifestPath)) return this._timingManifest[manifestPath]
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns compatible manifest keys for one discovered file.
|
|
203
|
+
* @param {string} filePath - Absolute test file path.
|
|
204
|
+
* @returns {string[]} - Canonical keys in matching priority order.
|
|
205
|
+
*/
|
|
206
|
+
timingManifestPaths(filePath) {
|
|
207
|
+
const relativePath = this.manifestRelativePath(filePath)
|
|
208
|
+
|
|
209
|
+
if (!relativePath) return []
|
|
210
|
+
|
|
211
|
+
const projectRelativePath = this.normalizeRelativePath(path.join(path.basename(this._baseDirectory), relativePath))
|
|
212
|
+
|
|
213
|
+
return relativePath === projectRelativePath ? [relativePath] : [relativePath, projectRelativePath]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Summarizes timing-history coverage for the complete discovered suite.
|
|
218
|
+
* @returns {{heuristicFiles: number, measuredFiles: number, staleEntries: number}} - Compact coverage counts.
|
|
219
|
+
*/
|
|
220
|
+
getTimingManifestCoverage() {
|
|
221
|
+
const matchedManifestPaths = new Set()
|
|
222
|
+
let measuredFiles = 0
|
|
223
|
+
|
|
224
|
+
for (const filePath of this._testFiles) {
|
|
225
|
+
let matchedDuration
|
|
226
|
+
|
|
227
|
+
for (const manifestPath of this.timingManifestPaths(filePath)) {
|
|
228
|
+
if (!Object.hasOwn(this._timingManifest, manifestPath)) continue
|
|
229
|
+
|
|
230
|
+
matchedManifestPaths.add(manifestPath)
|
|
231
|
+
matchedDuration = this._timingManifest[manifestPath]
|
|
232
|
+
break
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (matchedDuration !== undefined && matchedDuration > 0) measuredFiles++
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
heuristicFiles: this._testFiles.length - measuredFiles,
|
|
240
|
+
measuredFiles,
|
|
241
|
+
staleEntries: Object.keys(this._timingManifest).length - matchedManifestPaths.size
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
161
245
|
/**
|
|
162
246
|
* Sorts files by weight descending, then by path for determinism.
|
|
163
247
|
* @param {SplitterFileEntry[]} files - Weighted files.
|
|
@@ -169,7 +253,7 @@ export default class TestSuiteSplitter {
|
|
|
169
253
|
return b.weight - a.weight
|
|
170
254
|
}
|
|
171
255
|
|
|
172
|
-
return a.filePath
|
|
256
|
+
return compareTimingManifestPaths(a.filePath, b.filePath)
|
|
173
257
|
})
|
|
174
258
|
}
|
|
175
259
|
|