velocious 1.0.652 → 1.0.654

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 (34) hide show
  1. package/README.md +14 -1
  2. package/build/database/query/model-class-query.js +3 -1
  3. package/build/database/query/where-hash.js +12 -1
  4. package/build/database/query/where-in.js +48 -0
  5. package/build/database/query/where-model-class-hash.js +34 -1
  6. package/build/environment-handlers/node/cli/commands/test.js +2 -1
  7. package/build/src/database/query/model-class-query.js +5 -2
  8. package/build/src/database/query/where-hash.d.ts.map +1 -1
  9. package/build/src/database/query/where-hash.js +14 -2
  10. package/build/src/database/query/where-in.d.ts +22 -0
  11. package/build/src/database/query/where-in.d.ts.map +1 -0
  12. package/build/src/database/query/where-in.js +40 -0
  13. package/build/src/database/query/where-model-class-hash.d.ts +11 -0
  14. package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
  15. package/build/src/database/query/where-model-class-hash.js +33 -2
  16. package/build/src/environment-handlers/node/cli/commands/test.d.ts.map +1 -1
  17. package/build/src/environment-handlers/node/cli/commands/test.js +4 -2
  18. package/build/src/testing/test-runner.d.ts +23 -0
  19. package/build/src/testing/test-runner.d.ts.map +1 -1
  20. package/build/src/testing/test-runner.js +29 -2
  21. package/build/src/testing/velocious-runner-reporter.d.ts +7 -0
  22. package/build/src/testing/velocious-runner-reporter.d.ts.map +1 -1
  23. package/build/src/testing/velocious-runner-reporter.js +54 -12
  24. package/build/testing/test-runner.js +33 -1
  25. package/build/testing/velocious-runner-reporter.js +52 -11
  26. package/package.json +4 -4
  27. package/scripts/browser-test-session.js +21 -23
  28. package/src/database/query/model-class-query.js +3 -1
  29. package/src/database/query/where-hash.js +12 -1
  30. package/src/database/query/where-in.js +48 -0
  31. package/src/database/query/where-model-class-hash.js +34 -1
  32. package/src/environment-handlers/node/cli/commands/test.js +2 -1
  33. package/src/testing/test-runner.js +33 -1
  34. package/src/testing/velocious-runner-reporter.js +52 -11
@@ -0,0 +1,48 @@
1
+ // @ts-check
2
+
3
+ /** @typedef {string | number | boolean | null} InValue */
4
+
5
+ export default class WhereIn {
6
+ /**
7
+ * Validates an explicit membership descriptor at a column boundary.
8
+ * @param {unknown} condition - Untrusted column condition, narrowed before use.
9
+ * @returns {InValue[]} - Validated members in a new array.
10
+ */
11
+ static values(condition) {
12
+ if (condition === null || typeof condition !== "object" ||
13
+ !Object.hasOwn(condition, "in") || !("in" in condition) ||
14
+ Reflect.ownKeys(condition).length !== 1 || !Array.isArray(condition.in)) {
15
+ throw new Error("Invalid IN condition: expected an object with only an own 'in' array")
16
+ }
17
+
18
+ /** @type {InValue[]} */
19
+ const values = []
20
+
21
+ for (const value of condition.in) {
22
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" &&
23
+ !(typeof value === "number" && Number.isFinite(value))) {
24
+ throw new Error("Invalid IN condition: members must be strings, finite numbers, booleans or null")
25
+ }
26
+
27
+ values.push(value)
28
+ }
29
+
30
+ return values
31
+ }
32
+
33
+ /**
34
+ * Renders membership without letting its null branch escape sibling filters.
35
+ * @param {{columnSql: string, inColumnSql?: string, values: InValue[], options: import("../query-parser/options.js").default}} args - Quoted column operands, normalized members and driver quoting.
36
+ * @returns {string} - Complete membership predicate.
37
+ */
38
+ static toSql({columnSql, inColumnSql = columnSql, values, options}) {
39
+ const nonNullValues = values.filter((value) => value !== null)
40
+ const includesNull = values.includes(null)
41
+
42
+ if (nonNullValues.length === 0) return includesNull ? `${columnSql} IS NULL` : "1=0"
43
+
44
+ const membershipSql = `${inColumnSql} IN (${nonNullValues.map((value) => options.quote(value)).join(", ")})`
45
+
46
+ return includesNull ? `(${membershipSql} OR ${columnSql} IS NULL)` : membershipSql
47
+ }
48
+ }
@@ -3,6 +3,7 @@
3
3
  import * as inflection from "inflection"
4
4
  import {isPlainObject} from "is-plain-object"
5
5
  import WhereBase from "./where-base.js"
6
+ import WhereIn from "./where-in.js"
6
7
 
7
8
  /**
8
9
  * No match.
@@ -378,6 +379,34 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
378
379
  return normalized
379
380
  }
380
381
 
382
+ /**
383
+ * Normalizes explicit membership through the model's column metadata.
384
+ * @param {{modelClass: typeof import("../record/index.js").default, columnName: string, tableName?: string, condition: unknown}} args - Resolved column and untrusted membership descriptor.
385
+ * @returns {string} - Complete column membership predicate.
386
+ */
387
+ _whereSQLFromInCondition({modelClass, columnName, tableName, condition}) {
388
+ const options = this.getOptions()
389
+ const values = WhereIn.values(condition)
390
+ const normalizedValues = this._normalizeSqliteBooleanValue({columnName, modelClass, value: values})
391
+ /** @type {import("./where-in.js").InValue[] | typeof NO_MATCH} */
392
+ const typedValues = this._normalizeValueForColumnType({columnName, modelClass, value: normalizedValues})
393
+
394
+ if (typedValues === NO_MATCH) return "1=0"
395
+
396
+ const columnSql = tableName
397
+ ? `${options.quoteTableName(tableName)}.${options.quoteColumnName(columnName)}`
398
+ : options.quoteColumnName(columnName)
399
+ const columnType = modelClass.getColumnTypeByName(columnName)
400
+ const castText = this.getQuery().driver.getType() === "mssql" && columnType?.toLowerCase() === "text"
401
+
402
+ return WhereIn.toSql({
403
+ columnSql,
404
+ inColumnSql: castText ? `CAST(${columnSql} AS NVARCHAR(MAX))` : columnSql,
405
+ options,
406
+ values: typedValues
407
+ })
408
+ }
409
+
381
410
  /**
382
411
  * Runs where sqlfrom hash.
383
412
  * @param {WhereHash} hash - Hash.
@@ -400,7 +429,11 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
400
429
  : null
401
430
  const resolvedColumnName = this._resolveColumnName(modelClass, whereKey)
402
431
 
403
- if (relationship && tuples) {
432
+ if (resolvedColumnName && !relationship && isPlainObject(whereValue)) {
433
+ if (index > 0) sql += " AND "
434
+
435
+ sql += this._whereSQLFromInCondition({columnName: resolvedColumnName, condition: whereValue, modelClass, tableName})
436
+ } else if (relationship && tuples) {
404
437
  if (index > 0) sql += " AND "
405
438
 
406
439
  const rawTargetModelClass = relationship.getTargetModelClass()
@@ -203,7 +203,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
203
203
  const hasExampleFilters = examplePatterns.length > 0
204
204
  const hasTagFilters = includeTags.length > 0 || effectiveExcludeTagCount > 0
205
205
 
206
- if ((hasTagFilters || hasLineFilters || hasExampleFilters) && executedTests === 0) {
206
+ if ((hasTagFilters || hasLineFilters || hasExampleFilters) && testRunner.hasNoMatches()) {
207
207
  console.error(picocolors.red("\nNo tests matched the provided filters"))
208
208
  await finalizeProfile("no-tests")
209
209
  process.exit(1)
@@ -247,6 +247,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
247
247
  }
248
248
  }
249
249
 
250
+ if (testRunner.getNotRunTests() > 0) console.error(`${testRunner.getNotRunTests()} tests not run because a shared resource failed`)
250
251
  console.error(picocolors.red(`\nTest run failed with ${testRunner.getFailedTests()} failed tests and ${testRunner.getSuccessfulTests()} successfull`))
251
252
  await finalizeProfile("failed")
252
253
  process.exit(1)
@@ -142,6 +142,8 @@ import VelociousTestArguments from "./velocious-test-arguments.js"
142
142
  * @property {import("../database/pool/base.js").TestSharedConnectionRegistration | undefined} sharedRegistration - Physical-key shared registration once published.
143
143
  */
144
144
 
145
+ const testingPackageDirectory = path.dirname(fileURLToPath(import.meta.resolve("@velocious/testing/package.json")))
146
+
145
147
  /**
146
148
  * Runs to file slug.
147
149
  * @param {string} value - Value to sanitize.
@@ -197,6 +199,8 @@ export default class TestRunner {
197
199
  this._successfulTests = 0
198
200
  this._testsCount = 0
199
201
  this._failedTestDetails = []
202
+ /** @type {import("@velocious/testing/runner").NonRunTestResult[]} */
203
+ this._notRunTestDetails = []
200
204
  /** @type {{fullDescription: string, filePath: string, line: number} | null} */
201
205
  this._lastTestContext = null
202
206
  /** @type {Array<{fullDescription: string, filePath: string, line: number, durationMs: number}>} */
@@ -1007,6 +1011,25 @@ export default class TestRunner {
1007
1011
  return this._failedTests
1008
1012
  }
1009
1013
 
1014
+ /**
1015
+ * Counts selected tests blocked by a terminal resource.
1016
+ * @returns {number} - Selected tests not executed because a shared resource failed.
1017
+ */
1018
+ getNotRunTests() { return this._notRunTestDetails.length }
1019
+
1020
+ /**
1021
+ * Returns runtime non-run attribution.
1022
+ * @returns {import("@velocious/testing/runner").NonRunTestResult[]} - Runtime non-run details with the originating failure.
1023
+ */
1024
+ getNotRunTestDetails() { return this._notRunTestDetails }
1025
+
1026
+ /**
1027
+ * Records a selected test that did not execute.
1028
+ * @param {import("@velocious/testing/runner").NonRunTestResult} result - Terminal-resource non-run record.
1029
+ * @returns {void}
1030
+ */
1031
+ recordNotRunTest(result) { this._notRunTestDetails.push(result) }
1032
+
1010
1033
  /**
1011
1034
  * Runs get failed test details.
1012
1035
  * @returns {FailedTestDetail[]} - Failed test details.
@@ -1087,6 +1110,14 @@ export default class TestRunner {
1087
1110
  return this._packageResult?.tests.length ?? this._testDurations.length
1088
1111
  }
1089
1112
 
1113
+ /**
1114
+ * Distinguishes an empty selection from a failure before selected cases execute.
1115
+ * @returns {boolean} - Whether selection matched no declarations.
1116
+ */
1117
+ hasNoMatches() {
1118
+ return this._packageResult?.noMatches === true
1119
+ }
1120
+
1090
1121
  /**
1091
1122
  * Returns the tests recorded during the run, slowest first.
1092
1123
  * @param {number} [limit] - Maximum number of tests to return (0 returns all).
@@ -1105,6 +1136,7 @@ export default class TestRunner {
1105
1136
  async prepare() {
1106
1137
  this.anyTestsFocussed = false
1107
1138
  this._failedTests = 0
1139
+ this._notRunTestDetails = []
1108
1140
  this._successfulTests = 0
1109
1141
  this._testsCount = 0
1110
1142
  this._abortRemainingTests = false
@@ -1177,7 +1209,7 @@ export default class TestRunner {
1177
1209
 
1178
1210
  if (portablePath.endsWith("/src/testing/test-runner.js")) continue
1179
1211
  if (portablePath.endsWith("/src/testing/test.js")) continue
1180
- if (portablePath.includes("/node_modules/@velocious/testing/")) continue
1212
+ if (resolvedFilePath.startsWith(`${testingPackageDirectory}${path.sep}`)) continue
1181
1213
 
1182
1214
  return {filePath: resolvedFilePath, line: Number(match[2])}
1183
1215
  }
@@ -1,7 +1,6 @@
1
1
  // @ts-check
2
2
 
3
3
  import { addTrackedStackToError } from "../utils/with-tracked-stack.js"
4
- import BacktraceCleaner from "../utils/backtrace-cleaner-node.js"
5
4
  import picocolors from "picocolors"
6
5
  import restArgsError from "../utils/rest-args-error.js"
7
6
  import { testEvents } from "./test.js"
@@ -21,6 +20,8 @@ function errorFromPackageRecord(errorRecord) {
21
20
 
22
21
  error.name = errorRecord.name
23
22
  if (errorRecord.stack) error.stack = errorRecord.stack
23
+ if (errorRecord.cause) error.cause = errorFromPackageRecord(errorRecord.cause)
24
+ if (errorRecord.terminalResource) Object.assign(error, {terminalResource: errorRecord.terminalResource})
24
25
 
25
26
  return error
26
27
  }
@@ -68,7 +69,27 @@ export default class VelociousRunnerReporter {
68
69
  return
69
70
  }
70
71
 
71
- if (event.type === "run:finish") this.testRunner.recordPackageResult(event.result)
72
+ if (event.type === "test:not-run") {
73
+ const test = this.testRunner.findTestDeclaration(event.test.fullName)
74
+ if (!test) throw new Error(`Package not-run result did not match a declaration: ${event.test.fullName}`)
75
+ this.testRunner.recordNotRunTest(event.test)
76
+ this.testRunner.completeTestDeclaration(test)
77
+ console.error(`Not run: ${event.test.fullName} (terminal resource failure in ${event.test.reason.fullName})`)
78
+ await this.emitEvent("testNotRun", {
79
+ configuration: this.testRunner.getConfiguration(),
80
+ test: event.test,
81
+ testRunner: this.testRunner
82
+ })
83
+ return
84
+ }
85
+
86
+ if (event.type === "run:finish") {
87
+ this.testRunner.recordPackageResult(event.result)
88
+ for (const failure of event.result.errors) {
89
+ console.error(`Suite ${failure.phase} failed: ${failure.suite}`)
90
+ this.printErrorCauses(errorFromPackageRecord(failure.error))
91
+ }
92
+ }
72
93
  }
73
94
 
74
95
  /**
@@ -95,7 +116,7 @@ export default class VelociousRunnerReporter {
95
116
  const failed = outcome?.failed ?? Boolean(attempt.error)
96
117
  const error = outcome?.error
97
118
  const retriesUsed = Math.min(attempt.attemptNumber, retryCount)
98
- const willRetry = failed && !outcome?.abortRemainingTests && attempt.attemptNumber <= retryCount
119
+ const willRetry = failed && !event.terminalFailure && !outcome?.abortRemainingTests && attempt.attemptNumber <= retryCount
99
120
  const {descriptions, testDescription} = this.testRunner.testMetadata(test)
100
121
  const compatibility = this.testRunner.testData(test)
101
122
 
@@ -245,15 +266,8 @@ export default class VelociousRunnerReporter {
245
266
 
246
267
  if (error instanceof Error) {
247
268
  console.error(picocolors.red(`${leftPadding} Test failed: ${error.message}`))
248
- addTrackedStackToError(error)
249
-
250
- const backtraceCleaner = new BacktraceCleaner(error)
251
- const cleanedStack = backtraceCleaner.getCleanedStack()
252
- const stackLines = cleanedStack?.split("\n")
269
+ this.printErrorCauses(error)
253
270
 
254
- if (stackLines) {
255
- for (const stackLine of stackLines) console.error(picocolors.red(`${leftPadding} ${stackLine}`))
256
- }
257
271
  } else {
258
272
  console.error(picocolors.red(`${leftPadding} Test failed with a ${typeof error}: ${String(error)}`))
259
273
  }
@@ -274,6 +288,33 @@ export default class VelociousRunnerReporter {
274
288
  testRunner.printRerunCommand({descriptions, testDescription, testData, leftPadding})
275
289
  }
276
290
 
291
+ /**
292
+ * Prints complete primary and secondary stacks once, including cyclic cause graphs.
293
+ * @param {unknown} error - Thrown value at the reporting boundary.
294
+ * @param {Set<Error>} [reported] - Error identities already printed.
295
+ * @returns {void}
296
+ */
297
+ printErrorCauses(error, reported = new Set()) {
298
+ if (!(error instanceof Error)) {
299
+ console.error(String(error))
300
+ return
301
+ }
302
+ if (reported.has(error)) return
303
+ reported.add(error)
304
+ addTrackedStackToError(error)
305
+ console.error(error.stack || `${error.name}: ${error.message}`)
306
+ if (error.cause !== undefined) {
307
+ console.error("Caused by:")
308
+ this.printErrorCauses(error.cause, reported)
309
+ }
310
+ if (error instanceof AggregateError) {
311
+ for (const secondary of error.errors) {
312
+ console.error("Related failure:")
313
+ this.printErrorCauses(secondary, reported)
314
+ }
315
+ }
316
+ }
317
+
277
318
  /**
278
319
  * Emits one legacy event and awaits listeners in registration order.
279
320
  * @param {string} eventName - Event name.