velocious 1.0.666 → 1.0.668

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.
Files changed (174) hide show
  1. package/README.md +40 -11
  2. package/build/background-jobs/adapter-client.js +22 -0
  3. package/build/background-jobs/adapter.js +15 -0
  4. package/build/background-jobs/client.js +147 -1
  5. package/build/background-jobs/job-semantics.js +30 -0
  6. package/build/background-jobs/job.js +26 -1
  7. package/build/background-jobs/local-adapter.js +36 -8
  8. package/build/background-jobs/local-store.js +410 -39
  9. package/build/background-jobs/main.js +69 -2
  10. package/build/background-jobs/platform-job.js +26 -1
  11. package/build/background-jobs/runtime.js +58 -0
  12. package/build/background-jobs/store.js +374 -6
  13. package/build/background-jobs/types.js +27 -2
  14. package/build/background-jobs/web/controller.js +1 -0
  15. package/build/configuration.js +30 -6
  16. package/build/database/drivers/mssql/column.js +2 -2
  17. package/build/database/drivers/mssql/table.js +19 -5
  18. package/build/database/pool/async-tracked-multi-connection.js +2 -1
  19. package/build/database/query/alter-table-base.js +8 -1
  20. package/build/database/record/attachments/store.js +2 -2
  21. package/build/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  22. package/build/environment-handlers/node/cli/commands/test.js +17 -69
  23. package/build/frontend-model-controller.js +22 -2
  24. package/build/frontend-models/websocket-channel.js +18 -0
  25. package/build/http-server/client/websocket-session.js +41 -4
  26. package/build/http-server/websocket-channel.js +14 -0
  27. package/build/http-server/websocket-event-log-store.js +85 -12
  28. package/build/http-server/websocket-events-host.js +17 -7
  29. package/build/src/background-jobs/adapter-client.d.ts +17 -0
  30. package/build/src/background-jobs/adapter-client.d.ts.map +1 -1
  31. package/build/src/background-jobs/adapter-client.js +19 -1
  32. package/build/src/background-jobs/adapter.d.ts +15 -0
  33. package/build/src/background-jobs/adapter.d.ts.map +1 -1
  34. package/build/src/background-jobs/adapter.js +14 -1
  35. package/build/src/background-jobs/client.d.ts +17 -0
  36. package/build/src/background-jobs/client.d.ts.map +1 -1
  37. package/build/src/background-jobs/client.js +129 -1
  38. package/build/src/background-jobs/job-semantics.d.ts +18 -0
  39. package/build/src/background-jobs/job-semantics.d.ts.map +1 -1
  40. package/build/src/background-jobs/job-semantics.js +28 -1
  41. package/build/src/background-jobs/job.d.ts +15 -0
  42. package/build/src/background-jobs/job.d.ts.map +1 -1
  43. package/build/src/background-jobs/job.js +21 -2
  44. package/build/src/background-jobs/local-adapter.d.ts +23 -8
  45. package/build/src/background-jobs/local-adapter.d.ts.map +1 -1
  46. package/build/src/background-jobs/local-adapter.js +35 -9
  47. package/build/src/background-jobs/local-store.d.ts +120 -5
  48. package/build/src/background-jobs/local-store.d.ts.map +1 -1
  49. package/build/src/background-jobs/local-store.js +366 -40
  50. package/build/src/background-jobs/main.d.ts +24 -2
  51. package/build/src/background-jobs/main.d.ts.map +1 -1
  52. package/build/src/background-jobs/main.js +68 -3
  53. package/build/src/background-jobs/platform-job.d.ts +15 -0
  54. package/build/src/background-jobs/platform-job.d.ts.map +1 -1
  55. package/build/src/background-jobs/platform-job.js +19 -2
  56. package/build/src/background-jobs/runtime.d.ts +39 -0
  57. package/build/src/background-jobs/runtime.d.ts.map +1 -1
  58. package/build/src/background-jobs/runtime.js +49 -1
  59. package/build/src/background-jobs/store.d.ts +100 -1
  60. package/build/src/background-jobs/store.d.ts.map +1 -1
  61. package/build/src/background-jobs/store.js +334 -8
  62. package/build/src/background-jobs/types.d.ts +97 -4
  63. package/build/src/background-jobs/types.d.ts.map +1 -1
  64. package/build/src/background-jobs/types.js +28 -3
  65. package/build/src/background-jobs/web/controller.d.ts.map +1 -1
  66. package/build/src/background-jobs/web/controller.js +2 -1
  67. package/build/src/configuration.d.ts +18 -1
  68. package/build/src/configuration.d.ts.map +1 -1
  69. package/build/src/configuration.js +28 -6
  70. package/build/src/database/drivers/mssql/column.d.ts.map +1 -1
  71. package/build/src/database/drivers/mssql/column.js +2 -2
  72. package/build/src/database/drivers/mssql/table.d.ts.map +1 -1
  73. package/build/src/database/drivers/mssql/table.js +19 -5
  74. package/build/src/database/pool/async-tracked-multi-connection.d.ts.map +1 -1
  75. package/build/src/database/pool/async-tracked-multi-connection.js +3 -2
  76. package/build/src/database/query/alter-table-base.d.ts.map +1 -1
  77. package/build/src/database/query/alter-table-base.js +9 -2
  78. package/build/src/database/record/attachments/store.js +3 -3
  79. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.d.ts.map +1 -1
  80. package/build/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +3 -47
  81. package/build/src/environment-handlers/node/cli/commands/test.d.ts +2 -28
  82. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  83. package/build/src/environment-handlers/node/cli/commands/test.js +10 -61
  84. package/build/src/frontend-model-controller.d.ts.map +1 -1
  85. package/build/src/frontend-model-controller.js +18 -3
  86. package/build/src/frontend-models/websocket-channel.d.ts +9 -0
  87. package/build/src/frontend-models/websocket-channel.d.ts.map +1 -1
  88. package/build/src/frontend-models/websocket-channel.js +16 -1
  89. package/build/src/http-server/client/websocket-session.d.ts +25 -2
  90. package/build/src/http-server/client/websocket-session.d.ts.map +1 -1
  91. package/build/src/http-server/client/websocket-session.js +40 -5
  92. package/build/src/http-server/websocket-channel.d.ts +11 -0
  93. package/build/src/http-server/websocket-channel.d.ts.map +1 -1
  94. package/build/src/http-server/websocket-channel.js +14 -1
  95. package/build/src/http-server/websocket-event-log-store.d.ts +60 -41
  96. package/build/src/http-server/websocket-event-log-store.d.ts.map +1 -1
  97. package/build/src/http-server/websocket-event-log-store.js +79 -12
  98. package/build/src/http-server/websocket-events-host.d.ts +10 -3
  99. package/build/src/http-server/websocket-events-host.d.ts.map +1 -1
  100. package/build/src/http-server/websocket-events-host.js +17 -8
  101. package/build/src/testing/test-files-finder.d.ts +17 -117
  102. package/build/src/testing/test-files-finder.d.ts.map +1 -1
  103. package/build/src/testing/test-files-finder.js +29 -294
  104. package/build/src/testing/test-filter-parser.d.ts +32 -22
  105. package/build/src/testing/test-filter-parser.d.ts.map +1 -1
  106. package/build/src/testing/test-filter-parser.js +25 -217
  107. package/build/src/testing/test-profile-activity.d.ts +1 -6
  108. package/build/src/testing/test-profile-activity.d.ts.map +1 -1
  109. package/build/src/testing/test-profile-activity.js +2 -12
  110. package/build/src/testing/test-profile-output.d.ts +1 -40
  111. package/build/src/testing/test-profile-output.d.ts.map +1 -1
  112. package/build/src/testing/test-profile-output.js +2 -217
  113. package/build/src/testing/test-profiler.d.ts +15 -659
  114. package/build/src/testing/test-profiler.d.ts.map +1 -1
  115. package/build/src/testing/test-profiler.js +35 -833
  116. package/build/src/testing/test-runner.d.ts +10 -1
  117. package/build/src/testing/test-runner.d.ts.map +1 -1
  118. package/build/src/testing/test-runner.js +26 -4
  119. package/build/src/testing/test-suite-splitter.d.ts +1 -128
  120. package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
  121. package/build/src/testing/test-suite-splitter.js +2 -259
  122. package/build/src/testing/timing-manifest.d.ts +5 -93
  123. package/build/src/testing/timing-manifest.d.ts.map +1 -1
  124. package/build/src/testing/timing-manifest.js +4 -283
  125. package/build/src/testing/velocious-attempt-executor.d.ts.map +1 -1
  126. package/build/src/testing/velocious-attempt-executor.js +8 -2
  127. package/build/testing/test-files-finder.js +32 -343
  128. package/build/testing/test-filter-parser.js +29 -248
  129. package/build/testing/test-profile-activity.js +1 -12
  130. package/build/testing/test-profile-output.js +8 -253
  131. package/build/testing/test-profiler.js +38 -895
  132. package/build/testing/test-runner.js +33 -9
  133. package/build/testing/test-suite-splitter.js +1 -301
  134. package/build/testing/timing-manifest.js +10 -344
  135. package/build/testing/velocious-attempt-executor.js +7 -1
  136. package/package.json +3 -3
  137. package/scripts/test-browser.js +30 -6
  138. package/scripts/verify-docker-dev-environment.js +13 -1
  139. package/src/background-jobs/adapter-client.js +22 -0
  140. package/src/background-jobs/adapter.js +15 -0
  141. package/src/background-jobs/client.js +147 -1
  142. package/src/background-jobs/job-semantics.js +30 -0
  143. package/src/background-jobs/job.js +26 -1
  144. package/src/background-jobs/local-adapter.js +36 -8
  145. package/src/background-jobs/local-store.js +410 -39
  146. package/src/background-jobs/main.js +69 -2
  147. package/src/background-jobs/platform-job.js +26 -1
  148. package/src/background-jobs/runtime.js +58 -0
  149. package/src/background-jobs/store.js +374 -6
  150. package/src/background-jobs/types.js +27 -2
  151. package/src/background-jobs/web/controller.js +1 -0
  152. package/src/configuration.js +30 -6
  153. package/src/database/drivers/mssql/column.js +2 -2
  154. package/src/database/drivers/mssql/table.js +19 -5
  155. package/src/database/pool/async-tracked-multi-connection.js +2 -1
  156. package/src/database/query/alter-table-base.js +8 -1
  157. package/src/database/record/attachments/store.js +2 -2
  158. package/src/environment-handlers/node/cli/commands/test/timing-manifest/merge.js +2 -48
  159. package/src/environment-handlers/node/cli/commands/test.js +17 -69
  160. package/src/frontend-model-controller.js +22 -2
  161. package/src/frontend-models/websocket-channel.js +18 -0
  162. package/src/http-server/client/websocket-session.js +41 -4
  163. package/src/http-server/websocket-channel.js +14 -0
  164. package/src/http-server/websocket-event-log-store.js +85 -12
  165. package/src/http-server/websocket-events-host.js +17 -7
  166. package/src/testing/test-files-finder.js +32 -343
  167. package/src/testing/test-filter-parser.js +29 -248
  168. package/src/testing/test-profile-activity.js +1 -12
  169. package/src/testing/test-profile-output.js +8 -253
  170. package/src/testing/test-profiler.js +38 -895
  171. package/src/testing/test-runner.js +33 -9
  172. package/src/testing/test-suite-splitter.js +1 -301
  173. package/src/testing/timing-manifest.js +10 -344
  174. package/src/testing/velocious-attempt-executor.js +7 -1
@@ -2,19 +2,20 @@
2
2
 
3
3
  import fs from "node:fs/promises"
4
4
  import path from "path"
5
- import {AsyncLocalStorage} from "node:async_hooks"
6
- import {createTestContext, defaultTestContext} from "@velocious/testing"
7
- import {TestRunner as PackageTestRunner} from "@velocious/testing/runner"
5
+ import { AsyncLocalStorage } from "node:async_hooks"
6
+ import { createTestContext, defaultTestContext } from "@velocious/testing"
7
+ import { slowestTestResults } from "@velocious/testing/reporters"
8
+ import { TestRunner as PackageTestRunner } from "@velocious/testing/runner"
8
9
  import Application from "../../src/application.js"
9
10
  import RequestClient from "./request-client.js"
10
11
  import picocolors from "picocolors"
11
12
  import restArgsError from "../utils/rest-args-error.js"
12
- import {testConfig} from "./test.js"
13
- import {fileURLToPath, pathToFileURL} from "url"
13
+ import { testConfig } from "./test.js"
14
+ import { fileURLToPath, pathToFileURL } from "url"
14
15
  import SharedTransactionBroker from "./shared-transaction-broker.js"
15
16
  import { SHARED_TRANSACTION_BROKER_ENV } from "./shared-transaction-proxy-driver.js"
16
17
  import VelociousAttemptExecutor from "./velocious-attempt-executor.js"
17
- import VelociousRunnerReporter, {AbortRemainingTestsError} from "./velocious-runner-reporter.js"
18
+ import VelociousRunnerReporter, { AbortRemainingTestsError } from "./velocious-runner-reporter.js"
18
19
  import VelociousSuiteHookExecutor from "./velocious-suite-hook-executor.js"
19
20
  import VelociousTestArguments from "./velocious-test-arguments.js"
20
21
 
@@ -177,8 +178,11 @@ export default class TestRunner {
177
178
  * @param {Record<string, number[]>} [args.lineFilters] - Line filters by file.
178
179
  * @param {RegExp[]} [args.examplePatterns] - Example patterns.
179
180
  * @param {import("./test-profiler.js").default} [args.profiler] - Opt-in profiler.
181
+ * @param {number} [args.retries] - Default retry count.
182
+ * @param {string[]} [args.setupFiles] - Setup files imported before test files.
183
+ * @param {number} [args.timeoutMs] - Default lifecycle timeout.
180
184
  */
181
- constructor({configuration, context = defaultTestContext, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, ...restArgs}) {
185
+ constructor({configuration, context = defaultTestContext, excludeTags, includeTags, testFiles, lineFilters, examplePatterns, profiler, retries, setupFiles, timeoutMs, ...restArgs}) {
182
186
  restArgsError(restArgs)
183
187
 
184
188
  if (!configuration) throw new Error("configuration is required")
@@ -193,6 +197,9 @@ export default class TestRunner {
193
197
  this._lineFilters = lineFilters || {}
194
198
  this._examplePatterns = examplePatterns || []
195
199
  this._profiler = profiler
200
+ this._retries = retries
201
+ this._setupFiles = setupFiles || []
202
+ this._timeoutMs = timeoutMs
196
203
  this._abortRemainingTests = false
197
204
 
198
205
  this._failedTests = 0
@@ -1124,6 +1131,15 @@ export default class TestRunner {
1124
1131
  * @returns {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} - Slowest tests, slowest first.
1125
1132
  */
1126
1133
  getSlowestTests(limit = 10) {
1134
+ if (this._packageResult) {
1135
+ return slowestTestResults(this._packageResult, {limit}).map((result) => ({
1136
+ fullDescription: result.fullName,
1137
+ filePath: result.filePath ?? "<unknown>",
1138
+ line: result.line ?? 0,
1139
+ durationMs: result.durationMs
1140
+ }))
1141
+ }
1142
+
1127
1143
  const sorted = [...this._testDurations].sort((testA, testB) => testB.durationMs - testA.durationMs)
1128
1144
 
1129
1145
  return limit > 0 ? sorted.slice(0, limit) : sorted
@@ -1160,6 +1176,12 @@ export default class TestRunner {
1160
1176
  const testingConfigPath = this.getConfiguration().getTesting()
1161
1177
 
1162
1178
  await context.describe("", {databaseCleaning: {transaction: true}}, async () => {
1179
+ if (this._setupFiles.length > 0) {
1180
+ await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
1181
+ await this.getConfiguration().getEnvironmentHandler().importTestFiles(this._setupFiles)
1182
+ })
1183
+ }
1184
+
1163
1185
  if (testingConfigPath) {
1164
1186
  await this.runProfileSpan({phase: "testing config/global setup"}, async () => {
1165
1187
  await this.getConfiguration().getEnvironmentHandler().importTestingConfigPath()
@@ -1549,7 +1571,7 @@ export default class TestRunner {
1549
1571
  * @returns {number} - Effective retry count.
1550
1572
  */
1551
1573
  retryCount(test) {
1552
- const value = test.options.retries ?? test.options.retry ?? this.getTestContext().config.retries
1574
+ const value = test.options.retries ?? test.options.retry ?? this._retries ?? this.getTestContext().config.retries
1553
1575
  return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0
1554
1576
  }
1555
1577
 
@@ -1650,7 +1672,9 @@ export default class TestRunner {
1650
1672
  attemptExecutor: (input) => this._attemptExecutor.execute(input),
1651
1673
  testArgumentResolver: (input) => this._testArguments.resolve(input),
1652
1674
  suiteHookExecutor: (input) => this._suiteHookExecutor.execute(input),
1653
- reporter: this._runnerReporter
1675
+ reporter: this._runnerReporter,
1676
+ retries: this._retries,
1677
+ timeoutMs: this._timeoutMs
1654
1678
  })
1655
1679
  const failureStart = this._suiteHookFailures.length
1656
1680
  const restoreRetryOptions = this.normalizePackageRetriesForExecution()
@@ -1,303 +1,3 @@
1
1
  // @ts-check
2
2
 
3
- import path from "path"
4
- import restArgsError from "../utils/rest-args-error.js"
5
- import { canonicalTimingManifestPath, compareTimingManifestPaths } from "./timing-manifest.js"
6
-
7
- /**
8
- * SplitterFileEntry type.
9
- * @typedef {object} SplitterFileEntry
10
- * @property {string} filePath - Absolute file path.
11
- * @property {number} weight - Computed weight for load balancing.
12
- */
13
-
14
- /**
15
- * GroupBucket type.
16
- * @typedef {object} GroupBucket
17
- * @property {number} totalWeight - Accumulated weight.
18
- * @property {string[]} files - Files assigned to this group.
19
- */
20
-
21
- /** Default weight for a regular test file. */
22
- const DEFAULT_WEIGHT = 1
23
-
24
- /**
25
- * Weight multipliers by spec directory name.
26
- * Heavier test types get higher weights so greedy distribution balances wall-clock time.
27
- * @type {Record<string, number>}
28
- */
29
- const DIRECTORY_WEIGHTS = {
30
- system: 20,
31
- "frontend-models": 10,
32
- controller: 3
33
- }
34
-
35
- /** Extra multiplier applied to browser spec files on top of directory weight. */
36
- const BROWSER_SPEC_MULTIPLIER = 2
37
-
38
- /**
39
- * Splits a list of test files into balanced groups using a greedy load-balancing algorithm.
40
- * Modeled after test_suite_splitter for RSpec.
41
- */
42
- export default class TestSuiteSplitter {
43
- /**
44
- * Runs constructor.
45
- * @param {object} args - Options.
46
- * @param {number} args.groups - Total number of groups.
47
- * @param {number} args.groupNumber - Which group to return (1-indexed).
48
- * @param {string[]} args.testFiles - All discovered test file paths.
49
- * @param {string} [args.baseDirectory] - Base directory for relative path computation.
50
- * @param {ReturnType<typeof JSON.parse>} [args.timingManifest] - Relative test paths mapped to durations.
51
- */
52
- constructor({groups, groupNumber, testFiles, baseDirectory, timingManifest, ...restArgs}) {
53
- restArgsError(restArgs)
54
-
55
- if (!Number.isInteger(groups) || groups < 1) {
56
- throw new Error(`--groups must be a positive integer, got: ${groups}`)
57
- }
58
-
59
- if (!Number.isInteger(groupNumber) || groupNumber < 1 || groupNumber > groups) {
60
- throw new Error(`--group-number must be between 1 and ${groups}, got: ${groupNumber}`)
61
- }
62
-
63
- this._groups = groups
64
- this._groupNumber = groupNumber
65
- this._testFiles = testFiles
66
- this._baseDirectory = baseDirectory || process.cwd()
67
- this._timingManifest = this.normalizeTimingManifest(timingManifest)
68
- }
69
-
70
- /**
71
- * Returns the test files assigned to this group.
72
- * @returns {string[]} - File paths for the requested group.
73
- */
74
- getGroupFiles() {
75
- const weighted = this.computeWeightedFiles()
76
- const sorted = this.sortByWeightDescending(weighted)
77
- const buckets = this.distributeGreedily(sorted)
78
-
79
- return buckets[this._groupNumber - 1].files
80
- }
81
-
82
- /**
83
- * Computes weight for each test file based on directory type and file suffix.
84
- * @returns {SplitterFileEntry[]} - Weighted file entries.
85
- */
86
- computeWeightedFiles() {
87
- return this._testFiles.map((filePath) => ({
88
- filePath,
89
- weight: this.computeWeight(filePath)
90
- }))
91
- }
92
-
93
- /**
94
- * Computes the weight for a single file.
95
- * @param {string} filePath - Absolute file path.
96
- * @returns {number} - Weight value.
97
- */
98
- computeWeight(filePath) {
99
- const duration = this.timingManifestDuration(filePath)
100
-
101
- if (duration !== undefined && duration > 0) {
102
- return duration
103
- }
104
-
105
- const relativePath = this.heuristicRelativePath(filePath)
106
- let weight = DEFAULT_WEIGHT
107
-
108
- // Extract the type directory from the relative path.
109
- // Matches both "spec/system/..." (base is project root) and "system/..." (base is spec/ itself).
110
- const specDirMatch = relativePath.match(/^(?:(?:spec|__tests__|tests)\/)?([^/]+)\//)
111
-
112
- if (specDirMatch) {
113
- const dirName = specDirMatch[1]
114
-
115
- if (DIRECTORY_WEIGHTS[dirName] !== undefined) {
116
- weight = DIRECTORY_WEIGHTS[dirName]
117
- }
118
- }
119
-
120
- // Browser spec files are heavier
121
- if (filePath.endsWith(".browser-spec.js") || filePath.endsWith(".browser-spec.mjs")) {
122
- weight *= BROWSER_SPEC_MULTIPLIER
123
- }
124
-
125
- return weight
126
- }
127
-
128
- /**
129
- * Keeps only usable positive finite duration entries keyed by normalized relative path.
130
- * @param {ReturnType<typeof JSON.parse>} timingManifest - Parsed timing manifest.
131
- * @returns {Record<string, number>} - Valid normalized duration weights.
132
- */
133
- normalizeTimingManifest(timingManifest) {
134
- /** @type {Record<string, number>} */
135
- const normalized = {}
136
-
137
- if (!timingManifest || typeof timingManifest !== "object" || Array.isArray(timingManifest)) {
138
- return normalized
139
- }
140
-
141
- for (const [filePath, duration] of Object.entries(timingManifest)) {
142
- if (typeof duration !== "number" || !Number.isFinite(duration) || duration < 0) continue
143
-
144
- try {
145
- normalized[this.normalizeRelativePath(filePath)] = duration
146
- } catch (error) {
147
- if (!(error instanceof Error)) throw error
148
- }
149
- }
150
-
151
- return normalized
152
- }
153
-
154
- /**
155
- * Normalizes a relative test path to the manifest's portable slash format.
156
- * @param {string} filePath - Relative test path.
157
- * @returns {string} - Normalized relative test path.
158
- */
159
- normalizeRelativePath(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)
170
- .replaceAll("\\", "/")
171
- .replace(/^\.\//, "")
172
- }
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
-
245
- /**
246
- * Sorts files by weight descending, then by path for determinism.
247
- * @param {SplitterFileEntry[]} files - Weighted files.
248
- * @returns {SplitterFileEntry[]} - Sorted files.
249
- */
250
- sortByWeightDescending(files) {
251
- return [...files].sort((a, b) => {
252
- if (b.weight !== a.weight) {
253
- return b.weight - a.weight
254
- }
255
-
256
- return compareTimingManifestPaths(a.filePath, b.filePath)
257
- })
258
- }
259
-
260
- /**
261
- * Distributes files greedily into N balanced groups.
262
- * Each file is assigned to the group with the least accumulated weight.
263
- * @param {SplitterFileEntry[]} sortedFiles - Files sorted by weight descending.
264
- * @returns {GroupBucket[]} - Array of group buckets.
265
- */
266
- distributeGreedily(sortedFiles) {
267
- /**
268
- * Buckets.
269
- * @type {GroupBucket[]} */
270
- const buckets = []
271
-
272
- for (let i = 0; i < this._groups; i++) {
273
- buckets.push({totalWeight: 0, files: []})
274
- }
275
-
276
- for (const entry of sortedFiles) {
277
- const lightest = this.findLightestBucket(buckets)
278
-
279
- lightest.files.push(entry.filePath)
280
- lightest.totalWeight += entry.weight
281
- }
282
-
283
- return buckets
284
- }
285
-
286
- /**
287
- * Finds the bucket with the least accumulated weight.
288
- * Ties are broken by bucket index (earlier bucket wins) for determinism.
289
- * @param {GroupBucket[]} buckets - Group buckets.
290
- * @returns {GroupBucket} - The lightest bucket.
291
- */
292
- findLightestBucket(buckets) {
293
- let lightest = buckets[0]
294
-
295
- for (let i = 1; i < buckets.length; i++) {
296
- if (buckets[i].totalWeight < lightest.totalWeight) {
297
- lightest = buckets[i]
298
- }
299
- }
300
-
301
- return lightest
302
- }
303
- }
3
+ export { TestSuiteSplitter as default } from "@velocious/testing/node"