velocious 1.0.546 → 1.0.548

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.
@@ -237,6 +237,8 @@ export default class TestRunner {
237
237
  this._failedTestDetails = []
238
238
  /** @type {{fullDescription: string, filePath: string, line: number} | null} */
239
239
  this._lastTestContext = null
240
+ /** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
241
+ this._testDurations = []
240
242
  }
241
243
 
242
244
  /**
@@ -696,6 +698,17 @@ export default class TestRunner {
696
698
  return this._successfulTests + this._failedTests
697
699
  }
698
700
 
701
+ /**
702
+ * Returns the tests recorded during the run, slowest first.
703
+ * @param {number} [limit] - Maximum number of tests to return (0 returns all).
704
+ * @returns {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} - Slowest tests, slowest first.
705
+ */
706
+ getSlowestTests(limit = 10) {
707
+ const sorted = [...this._testDurations].sort((testA, testB) => testB.durationMs - testA.durationMs)
708
+
709
+ return limit > 0 ? sorted.slice(0, limit) : sorted
710
+ }
711
+
699
712
  /**
700
713
  * Runs prepare.
701
714
  * @returns {Promise<void>} - Resolves when complete.
@@ -706,6 +719,7 @@ export default class TestRunner {
706
719
  this._successfulTests = 0
707
720
  this._testsCount = 0
708
721
  this._failedTestDetails = []
722
+ this._testDurations = []
709
723
  await this.importTestFiles()
710
724
  await this.analyzeTests(tests)
711
725
  this._onlyFocussed = this.anyTestsFocussed
@@ -940,6 +954,8 @@ export default class TestRunner {
940
954
 
941
955
  console.log(`${leftPadding}it ${testDescription}`)
942
956
 
957
+ const testStartMs = Date.now()
958
+
943
959
  while (true) {
944
960
  let shouldRetry = false
945
961
  /**
@@ -1180,6 +1196,13 @@ export default class TestRunner {
1180
1196
 
1181
1197
  break
1182
1198
  }
1199
+
1200
+ this._testDurations.push({
1201
+ fullDescription: this.buildFullDescription(descriptions, testDescription),
1202
+ filePath: testData.filePath ?? "<unknown>",
1203
+ line: testData.line ?? 0,
1204
+ durationMs: Date.now() - testStartMs
1205
+ })
1183
1206
  }
1184
1207
 
1185
1208
  for (const subDescription in tests.subs) {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "velocious": "build/bin/velocious.js"
4
4
  },
5
5
  "name": "velocious",
6
- "version": "1.0.546",
6
+ "version": "1.0.548",
7
7
  "main": "build/index.js",
8
8
  "types": "build/index.d.ts",
9
9
  "files": [
@@ -2,19 +2,25 @@
2
2
 
3
3
  import TenantIterator from "../tenants/tenant-iterator.js"
4
4
 
5
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 30_000
6
+
5
7
  export default class TenantDatabaseCommandHelper {
6
8
  /**
7
9
  * Runs constructor.
8
10
  * @param {object} args - Options object.
9
11
  * @param {import("./base-command.js").default} args.command - CLI command instance.
12
+ * @param {number} [args.heartbeatIntervalMs] - Interval between progress heartbeats.
10
13
  * @param {string | undefined} args.identifier - Tenant database identifier.
14
+ * @param {(message: string) => void} [args.output] - Progress output handler.
11
15
  */
12
- constructor({command, identifier}) {
16
+ constructor({command, heartbeatIntervalMs = DEFAULT_HEARTBEAT_INTERVAL_MS, identifier, output = console.log}) {
13
17
  if (!identifier) throw new Error("Missing tenant database identifier argument")
14
18
 
15
19
  this.command = command
16
20
  this.configuration = command.getConfiguration()
21
+ this.heartbeatIntervalMs = heartbeatIntervalMs
17
22
  this.identifier = identifier
23
+ this.output = output
18
24
  this.provider = this.configuration.getTenantDatabaseProvider(identifier)
19
25
  }
20
26
 
@@ -78,13 +84,42 @@ export default class TenantDatabaseCommandHelper {
78
84
  */
79
85
  async eachTenant(callback) {
80
86
  const tenants = await this.listTenants()
87
+ const commandName = this.command.processArgs?.[0] || "db:tenants"
88
+ const parallelCount = this.parallelCount()
89
+ const progressPrefix = `${commandName} ${this.identifier}`
90
+ let activeTenantCount = 0
91
+ let completedTenantCount = 0
81
92
  const iterator = new TenantIterator({
82
93
  configuration: this.configuration,
83
94
  identifier: this.identifier,
84
- parallelCount: this.parallelCount()
95
+ parallelCount
85
96
  })
97
+ const progressHeartbeat = setInterval(() => {
98
+ this.output(`${progressPrefix}: heartbeat: ${completedTenantCount}/${tenants.length} completed, ${activeTenantCount} active`)
99
+ }, this.heartbeatIntervalMs)
100
+
101
+ progressHeartbeat.unref()
102
+ this.output(`${progressPrefix}: processing ${tenants.length} tenant(s) with parallelism ${parallelCount}`)
103
+
104
+ try {
105
+ const processedTenantCount = await iterator.run(tenants, async (args) => {
106
+ activeTenantCount++
107
+
108
+ try {
109
+ await callback(args)
110
+ completedTenantCount++
111
+ this.output(`${progressPrefix}: completed ${TenantIterator.tenantLabel(args.tenant)} (${completedTenantCount}/${tenants.length})`)
112
+ } finally {
113
+ activeTenantCount--
114
+ }
115
+ })
86
116
 
87
- return await iterator.run(tenants, callback)
117
+ this.output(`${progressPrefix}: finished ${processedTenantCount}/${tenants.length} tenant(s)`)
118
+
119
+ return processedTenantCount
120
+ } finally {
121
+ clearInterval(progressHeartbeat)
122
+ }
88
123
  }
89
124
 
90
125
  /**
@@ -86,6 +86,25 @@ export default class VelociousCliCommandsTest extends BaseCommand {
86
86
  process.exit(1)
87
87
  }
88
88
 
89
+ // Report the slowest tests so suite hotspots are visible every run. Defaults to
90
+ // the top 10; tune with VELOCIOUS_SLOW_TEST_COUNT (0 disables). Skipped for
91
+ // single-test runs where it would just be noise.
92
+ const slowTestCount = resolveSlowTestCount(process.env.VELOCIOUS_SLOW_TEST_COUNT)
93
+
94
+ if (slowTestCount > 0 && executedTests > 1) {
95
+ const slowestTests = testRunner.getSlowestTests(slowTestCount)
96
+
97
+ if (slowestTests.length > 0) {
98
+ console.log(picocolors.cyan(`\nSlowest ${slowestTests.length} tests:`))
99
+
100
+ for (const slowTest of slowestTests) {
101
+ const location = slowTest.filePath && slowTest.line ? ` (${slowTest.filePath}:${slowTest.line})` : ""
102
+
103
+ console.log(picocolors.cyan(` ${String(slowTest.durationMs).padStart(6)}ms ${slowTest.fullDescription}${location}`))
104
+ }
105
+ }
106
+ }
107
+
89
108
  if (testRunner.isFailed()) {
90
109
  await testRunner.persistFailedTestConsoleOutputsToAssets()
91
110
  const failedTests = testRunner.getFailedTestDetails()
@@ -116,3 +135,16 @@ export default class VelociousCliCommandsTest extends BaseCommand {
116
135
  }
117
136
  }
118
137
  }
138
+
139
+ /**
140
+ * Resolves how many slowest tests to report from the `VELOCIOUS_SLOW_TEST_COUNT`
141
+ * env value: defaults to 10 when unset; 0 (or an unparseable value) disables the
142
+ * report; otherwise the floored, non-negative integer.
143
+ * @param {string | undefined} rawEnvValue - Raw env value.
144
+ * @returns {number} - Number of slowest tests to report (0 disables).
145
+ */
146
+ export function resolveSlowTestCount(rawEnvValue) {
147
+ if (rawEnvValue === undefined) return 10
148
+
149
+ return Math.max(0, Math.floor(Number(rawEnvValue)) || 0)
150
+ }
@@ -101,9 +101,14 @@ export default class TenantIterator {
101
101
  if (tenant && typeof tenant === "object") {
102
102
  const tenantObject = /** @type {{id?: ?, name?: ?, slug?: ?}} */ (tenant)
103
103
 
104
- if (tenantObject.slug) return String(tenantObject.slug)
105
- if (tenantObject.name) return String(tenantObject.name)
106
- if (tenantObject.id) return String(tenantObject.id)
104
+ for (const attributeName of /** @type {Array<"slug" | "name" | "id">} */ (["slug", "name", "id"])) {
105
+ const attributeOrAccessor = tenantObject[attributeName]
106
+ const attributeValue = typeof attributeOrAccessor === "function"
107
+ ? attributeOrAccessor.call(tenant)
108
+ : attributeOrAccessor
109
+
110
+ if (attributeValue) return String(attributeValue)
111
+ }
107
112
  }
108
113
 
109
114
  return JSON.stringify(tenant)
@@ -237,6 +237,8 @@ export default class TestRunner {
237
237
  this._failedTestDetails = []
238
238
  /** @type {{fullDescription: string, filePath: string, line: number} | null} */
239
239
  this._lastTestContext = null
240
+ /** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
241
+ this._testDurations = []
240
242
  }
241
243
 
242
244
  /**
@@ -696,6 +698,17 @@ export default class TestRunner {
696
698
  return this._successfulTests + this._failedTests
697
699
  }
698
700
 
701
+ /**
702
+ * Returns the tests recorded during the run, slowest first.
703
+ * @param {number} [limit] - Maximum number of tests to return (0 returns all).
704
+ * @returns {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} - Slowest tests, slowest first.
705
+ */
706
+ getSlowestTests(limit = 10) {
707
+ const sorted = [...this._testDurations].sort((testA, testB) => testB.durationMs - testA.durationMs)
708
+
709
+ return limit > 0 ? sorted.slice(0, limit) : sorted
710
+ }
711
+
699
712
  /**
700
713
  * Runs prepare.
701
714
  * @returns {Promise<void>} - Resolves when complete.
@@ -706,6 +719,7 @@ export default class TestRunner {
706
719
  this._successfulTests = 0
707
720
  this._testsCount = 0
708
721
  this._failedTestDetails = []
722
+ this._testDurations = []
709
723
  await this.importTestFiles()
710
724
  await this.analyzeTests(tests)
711
725
  this._onlyFocussed = this.anyTestsFocussed
@@ -940,6 +954,8 @@ export default class TestRunner {
940
954
 
941
955
  console.log(`${leftPadding}it ${testDescription}`)
942
956
 
957
+ const testStartMs = Date.now()
958
+
943
959
  while (true) {
944
960
  let shouldRetry = false
945
961
  /**
@@ -1180,6 +1196,13 @@ export default class TestRunner {
1180
1196
 
1181
1197
  break
1182
1198
  }
1199
+
1200
+ this._testDurations.push({
1201
+ fullDescription: this.buildFullDescription(descriptions, testDescription),
1202
+ filePath: testData.filePath ?? "<unknown>",
1203
+ line: testData.line ?? 0,
1204
+ durationMs: Date.now() - testStartMs
1205
+ })
1183
1206
  }
1184
1207
 
1185
1208
  for (const subDescription in tests.subs) {