velocious 1.0.581 → 1.0.582
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 +17 -1
- package/build/database/drivers/base.js +134 -13
- package/build/environment-handlers/node/cli/commands/test.js +21 -2
- package/build/frontend-model-controller.js +131 -28
- package/build/src/database/drivers/base.d.ts +32 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +109 -17
- package/build/src/environment-handlers/node/cli/commands/test.d.ts +6 -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 +22 -3
- package/build/src/frontend-model-controller.d.ts +40 -0
- package/build/src/frontend-model-controller.d.ts.map +1 -1
- package/build/src/frontend-model-controller.js +116 -28
- package/build/src/testing/test-filter-parser.d.ts +5 -0
- package/build/src/testing/test-filter-parser.d.ts.map +1 -1
- package/build/src/testing/test-filter-parser.js +19 -2
- package/build/src/testing/test-suite-splitter.d.ts +16 -1
- package/build/src/testing/test-suite-splitter.d.ts.map +1 -1
- package/build/src/testing/test-suite-splitter.js +38 -3
- package/build/testing/test-filter-parser.js +21 -1
- package/build/testing/test-suite-splitter.js +44 -2
- package/package.json +3 -1
- package/src/database/drivers/base.js +134 -13
- package/src/environment-handlers/node/cli/commands/test.js +21 -2
- package/src/frontend-model-controller.js +131 -28
- package/src/testing/test-filter-parser.js +21 -1
- package/src/testing/test-suite-splitter.js +44 -2
|
@@ -118,6 +118,14 @@
|
|
|
118
118
|
* @property {string[]} updateColumns - Columns to update on conflict.
|
|
119
119
|
*/
|
|
120
120
|
|
|
121
|
+
/**
|
|
122
|
+
* SqlTokenResult type.
|
|
123
|
+
* @typedef {object} SqlTokenResult
|
|
124
|
+
* @property {boolean} incomplete - Whether the scan hit its bound before finishing trivia/token parsing.
|
|
125
|
+
* @property {string | undefined} token - Lowercased token when parsing completed; undefined when no token was found.
|
|
126
|
+
* @property {number} index - Index immediately after the parsed token or trivia.
|
|
127
|
+
*/
|
|
128
|
+
|
|
121
129
|
import BacktraceCleaner from "../../utils/backtrace-cleaner.js"
|
|
122
130
|
import { getDatabaseAnnotations } from "../annotations.js"
|
|
123
131
|
import { formatDateForDatabase } from "../datetime-storage.js"
|
|
@@ -134,6 +142,11 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
|
|
|
134
142
|
import wait from "awaitery/build/wait.js"
|
|
135
143
|
import {optionalPositiveInteger} from "typanic"
|
|
136
144
|
|
|
145
|
+
/** Maximum characters inspected when building the debug SQL preview. */
|
|
146
|
+
const SQL_PREVIEW_SCAN_LIMIT = 4096
|
|
147
|
+
/** Maximum characters inspected when deciding whether a statement invalidates schema metadata. */
|
|
148
|
+
const SCHEMA_INVALIDATION_SCAN_LIMIT = 8192
|
|
149
|
+
|
|
137
150
|
/**
|
|
138
151
|
* Marks a callback failure that happened after the owning transaction was durably committed.
|
|
139
152
|
* The public transaction boundary unwraps it before deadlock classification.
|
|
@@ -1604,13 +1617,25 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1604
1617
|
}
|
|
1605
1618
|
}
|
|
1606
1619
|
|
|
1620
|
+
/**
|
|
1621
|
+
* Returns a bounded prefix of `sql` for lightweight diagnostic scanning.
|
|
1622
|
+
* @param {string} sql - SQL string.
|
|
1623
|
+
* @param {number} limit - Maximum code units to inspect.
|
|
1624
|
+
* @returns {string} - Prefix of `sql`.
|
|
1625
|
+
*/
|
|
1626
|
+
_diagnosticSqlPrefix(sql, limit) {
|
|
1627
|
+
return sql.length <= limit ? sql : sql.slice(0, limit)
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1607
1630
|
/**
|
|
1608
1631
|
* Runs debug sql preview.
|
|
1609
1632
|
* @param {string} sql - SQL to preview.
|
|
1610
1633
|
* @returns {string} - Normalized truncated SQL preview for diagnostics.
|
|
1611
1634
|
*/
|
|
1612
1635
|
_debugSqlPreview(sql) {
|
|
1613
|
-
|
|
1636
|
+
const prefix = this._diagnosticSqlPrefix(sql, SQL_PREVIEW_SCAN_LIMIT)
|
|
1637
|
+
|
|
1638
|
+
return prefix
|
|
1614
1639
|
.replace(/\s+/g, " ")
|
|
1615
1640
|
.trim()
|
|
1616
1641
|
.slice(0, 500)
|
|
@@ -1664,25 +1689,121 @@ export default class VelociousDatabaseDriversBase {
|
|
|
1664
1689
|
.replace(/"/g, "'")
|
|
1665
1690
|
}
|
|
1666
1691
|
|
|
1692
|
+
/**
|
|
1693
|
+
* Reads the next SQL token starting at `startIndex`, skipping leading trivia
|
|
1694
|
+
* (BOM, whitespace, block comments, line comments). If the scan cannot finish
|
|
1695
|
+
* skipping trivia before `limit`, the result is marked incomplete so callers
|
|
1696
|
+
* can conservatively treat the statement as schema-invalidating.
|
|
1697
|
+
* @param {string} sql - SQL string.
|
|
1698
|
+
* @param {number} startIndex - Index to start scanning.
|
|
1699
|
+
* @param {number} limit - Maximum absolute index to scan while skipping leading trivia.
|
|
1700
|
+
* @returns {SqlTokenResult} - Token result.
|
|
1701
|
+
*/
|
|
1702
|
+
_readSqlToken(sql, startIndex, limit) {
|
|
1703
|
+
let i = startIndex
|
|
1704
|
+
const len = sql.length
|
|
1705
|
+
|
|
1706
|
+
while (i < len && i < limit) {
|
|
1707
|
+
const char = sql[i]
|
|
1708
|
+
|
|
1709
|
+
if (char === "\ufeff" || /\s/.test(char)) {
|
|
1710
|
+
i++
|
|
1711
|
+
continue
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
if (char === "/" && sql[i + 1] === "*") {
|
|
1715
|
+
const close = sql.indexOf("*/", i + 2)
|
|
1716
|
+
|
|
1717
|
+
if (close === -1 || close + 2 > limit) {
|
|
1718
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
i = close + 2
|
|
1722
|
+
continue
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
if (char === "-" && sql[i + 1] === "-") {
|
|
1726
|
+
const newline = sql.indexOf("\n", i + 2)
|
|
1727
|
+
|
|
1728
|
+
if (newline === -1) {
|
|
1729
|
+
return {incomplete: false, index: len, token: undefined}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
if (newline + 1 > limit) {
|
|
1733
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
i = newline + 1
|
|
1737
|
+
continue
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
let token = ""
|
|
1741
|
+
|
|
1742
|
+
while (i < len) {
|
|
1743
|
+
const c = sql[i]
|
|
1744
|
+
|
|
1745
|
+
if (/\s/.test(c) || c === "\ufeff") break
|
|
1746
|
+
if (c === "/" && sql[i + 1] === "*") break
|
|
1747
|
+
if (c === "-" && sql[i + 1] === "-") break
|
|
1748
|
+
|
|
1749
|
+
token += c
|
|
1750
|
+
i++
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return {incomplete: false, token: token.toLowerCase(), index: i}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (i >= len) {
|
|
1757
|
+
return {incomplete: false, index: len, token: undefined}
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
return {incomplete: true, index: i, token: undefined}
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1667
1763
|
/**
|
|
1668
1764
|
* Runs schema cache invalidating sql.
|
|
1669
1765
|
* @param {string} sql - SQL string.
|
|
1670
1766
|
* @returns {boolean} - Whether the SQL should invalidate schema metadata.
|
|
1671
1767
|
*/
|
|
1672
1768
|
_schemaCacheInvalidatingSql(sql) {
|
|
1673
|
-
const
|
|
1674
|
-
.trim()
|
|
1675
|
-
.replace(/^\ufeff/, "")
|
|
1676
|
-
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
|
1677
|
-
.replace(/--[^\n]*(\n|$)/g, " ")
|
|
1678
|
-
.replace(/\s+/g, " ")
|
|
1679
|
-
.toLowerCase()
|
|
1769
|
+
const first = this._readSqlToken(sql, 0, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1680
1770
|
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
if (
|
|
1771
|
+
if (first.incomplete) return true
|
|
1772
|
+
|
|
1773
|
+
const firstToken = first.token
|
|
1774
|
+
|
|
1775
|
+
if (!firstToken) return false
|
|
1776
|
+
if (/^(create|alter|drop|rename)$/.test(firstToken)) return true
|
|
1777
|
+
|
|
1778
|
+
if (firstToken === "comment") {
|
|
1779
|
+
const next = this._readSqlToken(sql, first.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1780
|
+
|
|
1781
|
+
return next.incomplete || next.token === "on"
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
if (firstToken === "exec" || firstToken === "execute") {
|
|
1785
|
+
const next = this._readSqlToken(sql, first.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1786
|
+
|
|
1787
|
+
return next.incomplete || next.token === "sp_rename"
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1790
|
+
if (firstToken === "if") {
|
|
1791
|
+
let index = first.index
|
|
1792
|
+
|
|
1793
|
+
while (true) {
|
|
1794
|
+
const result = this._readSqlToken(sql, index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1795
|
+
|
|
1796
|
+
if (result.incomplete) return true
|
|
1797
|
+
if (!result.token) return false
|
|
1798
|
+
if (result.token === "begin") {
|
|
1799
|
+
const ddlResult = this._readSqlToken(sql, result.index, SCHEMA_INVALIDATION_SCAN_LIMIT)
|
|
1800
|
+
|
|
1801
|
+
return ddlResult.incomplete || /^(create|alter|drop|rename)$/.test(ddlResult.token || "")
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
index = result.index
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1686
1807
|
|
|
1687
1808
|
return false
|
|
1688
1809
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import BaseCommand from "../../../../cli/base-command.js"
|
|
4
|
+
import fs from "fs/promises"
|
|
4
5
|
import picocolors from "picocolors"
|
|
5
6
|
import TestFilesFinder from "../../../../testing/test-files-finder.js"
|
|
6
7
|
import TestRunner from "../../../../testing/test-runner.js"
|
|
@@ -26,7 +27,7 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
26
27
|
directories.push(`${this.directory()}/spec`)
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
const {includeTags, excludeTags, examplePatterns, filteredProcessArgs, groups, groupNumber} = parseFilters(this.processArgs || [])
|
|
30
|
+
const {includeTags, excludeTags, examplePatterns, filteredProcessArgs, groups, groupNumber, timingManifestPath} = parseFilters(this.processArgs || [])
|
|
30
31
|
const testFilesFinder = new TestFilesFinder({directory, directories, processArgs: filteredProcessArgs})
|
|
31
32
|
let testFiles = await testFilesFinder.findTestFiles()
|
|
32
33
|
|
|
@@ -35,7 +36,8 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
35
36
|
throw new Error("Both --groups and --group-number must be provided together")
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
const
|
|
39
|
+
const timingManifest = await loadTimingManifest(timingManifestPath)
|
|
40
|
+
const splitter = new TestSuiteSplitter({groups, groupNumber, testFiles, baseDirectory: directory, timingManifest})
|
|
39
41
|
|
|
40
42
|
testFiles = splitter.getGroupFiles()
|
|
41
43
|
console.log(picocolors.cyan(`Running group ${groupNumber} of ${groups} (${testFiles.length} files)`))
|
|
@@ -138,6 +140,23 @@ export default class VelociousCliCommandsTest extends BaseCommand {
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Loads an optional JSON timing manifest. Unreadable or malformed input intentionally falls back to heuristics.
|
|
145
|
+
* @param {string | undefined} timingManifestPath - Timing manifest path.
|
|
146
|
+
* @returns {Promise<ReturnType<typeof JSON.parse> | undefined>} - Parsed manifest when readable and valid JSON.
|
|
147
|
+
*/
|
|
148
|
+
export async function loadTimingManifest(timingManifestPath) {
|
|
149
|
+
if (!timingManifestPath) return undefined
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(await fs.readFile(timingManifestPath, "utf8"))
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof Error) return undefined
|
|
155
|
+
|
|
156
|
+
throw error
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
141
160
|
/**
|
|
142
161
|
* Resolves how many slowest tests to report from the `VELOCIOUS_SLOW_TEST_COUNT`
|
|
143
162
|
* env value: defaults to 10 when unset; 0 (or an unparseable value) disables the
|
|
@@ -72,6 +72,12 @@ import {RansackQueryError, normalizeRansackGroup, parseRansackSort} from "./util
|
|
|
72
72
|
* @property {import("./frontend-model-resource/base-resource.js").default} [resource] - Resource providing query hooks.
|
|
73
73
|
*/
|
|
74
74
|
/** @typedef {import("./database/query/model-class-query.js").default & Record<symbol, Set<string> | undefined>} FrontendModelQueryMetadata */
|
|
75
|
+
/**
|
|
76
|
+
* @callback FrontendModelSerializationResourceInstanceHook
|
|
77
|
+
* @param {import("./database/record/index.js").default} model - Model instance being serialized.
|
|
78
|
+
* @param {import("./frontend-model-resource/base-resource.js").default | null} resource - Resolved serialization resource instance, if any.
|
|
79
|
+
* @returns {void}
|
|
80
|
+
*/
|
|
75
81
|
|
|
76
82
|
/**
|
|
77
83
|
* Runs normalize frontend model preload.
|
|
@@ -603,6 +609,17 @@ export default class FrontendModelController extends Controller {
|
|
|
603
609
|
* own arguments rather than the route metadata. Only set on the shared-endpoint path.
|
|
604
610
|
* @type {Record<string, ReturnType<typeof JSON.parse>> | undefined} */
|
|
605
611
|
_frontendModelCustomCommandClientArguments = undefined
|
|
612
|
+
/**
|
|
613
|
+
* Request-scoped cache for serialization resource instances.
|
|
614
|
+
* Keyed by model class, then by whether the resource is for a related model
|
|
615
|
+
* (so self-referential relationships do not accidentally reuse root params).
|
|
616
|
+
* @type {Map<typeof import("./database/record/index.js").default, Map<boolean, import("./frontend-model-resource/base-resource.js").default>> | undefined} */
|
|
617
|
+
_frontendModelSerializationResourceInstances = undefined
|
|
618
|
+
/**
|
|
619
|
+
* Optional per-instance hook invoked for every serialization resource instance
|
|
620
|
+
* resolution. Intended for tests and benchmarks; absent in production.
|
|
621
|
+
* @type {FrontendModelSerializationResourceInstanceHook | null | undefined} */
|
|
622
|
+
_frontendModelSerializationResourceInstanceHook = undefined
|
|
606
623
|
|
|
607
624
|
/**
|
|
608
625
|
* Runs frontend model params.
|
|
@@ -628,15 +645,18 @@ export default class FrontendModelController extends Controller {
|
|
|
628
645
|
async withFrontendModelParams(params, callback) {
|
|
629
646
|
const previousOverride = this._frontendModelParamsOverride
|
|
630
647
|
const previousParams = this._frontendModelParams
|
|
648
|
+
const previousSerializationResourceInstances = this._frontendModelSerializationResourceInstances
|
|
631
649
|
|
|
632
650
|
this._frontendModelParamsOverride = params
|
|
633
651
|
this._frontendModelParams = undefined
|
|
652
|
+
this._frontendModelSerializationResourceInstances = undefined
|
|
634
653
|
|
|
635
654
|
try {
|
|
636
655
|
return await callback()
|
|
637
656
|
} finally {
|
|
638
657
|
this._frontendModelParamsOverride = previousOverride
|
|
639
658
|
this._frontendModelParams = previousParams
|
|
659
|
+
this._frontendModelSerializationResourceInstances = previousSerializationResourceInstances
|
|
640
660
|
}
|
|
641
661
|
}
|
|
642
662
|
|
|
@@ -2792,47 +2812,130 @@ export default class FrontendModelController extends Controller {
|
|
|
2792
2812
|
return serializedAttributes
|
|
2793
2813
|
}
|
|
2794
2814
|
|
|
2815
|
+
/**
|
|
2816
|
+
* Returns the request-scoped serialization resource instance cache.
|
|
2817
|
+
* @returns {Map<typeof import("./database/record/index.js").default, Map<boolean, import("./frontend-model-resource/base-resource.js").default>>} - Cache.
|
|
2818
|
+
*/
|
|
2819
|
+
_frontendModelSerializationResourceInstancesMap() {
|
|
2820
|
+
if (!this._frontendModelSerializationResourceInstances) {
|
|
2821
|
+
this._frontendModelSerializationResourceInstances = new Map()
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
return this._frontendModelSerializationResourceInstances
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
/**
|
|
2828
|
+
* Looks up a cached serialization resource instance.
|
|
2829
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2830
|
+
* @param {boolean} isRelated - Whether the resource is for a related (non-root) model.
|
|
2831
|
+
* @returns {import("./frontend-model-resource/base-resource.js").default | undefined} - Cached resource or undefined.
|
|
2832
|
+
*/
|
|
2833
|
+
_cachedSerializationResourceInstance(modelClass, isRelated) {
|
|
2834
|
+
return this._frontendModelSerializationResourceInstancesMap().get(modelClass)?.get(isRelated)
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
/**
|
|
2838
|
+
* Stores a serialization resource instance in the request-scoped cache.
|
|
2839
|
+
* @param {typeof import("./database/record/index.js").default} modelClass - Model class.
|
|
2840
|
+
* @param {boolean} isRelated - Whether the resource is for a related (non-root) model.
|
|
2841
|
+
* @param {import("./frontend-model-resource/base-resource.js").default} resource - Resource instance.
|
|
2842
|
+
* @returns {void}
|
|
2843
|
+
*/
|
|
2844
|
+
_setCachedSerializationResourceInstance(modelClass, isRelated, resource) {
|
|
2845
|
+
const byClass = this._frontendModelSerializationResourceInstancesMap()
|
|
2846
|
+
let byRelated = byClass.get(modelClass)
|
|
2847
|
+
|
|
2848
|
+
if (!byRelated) {
|
|
2849
|
+
byRelated = new Map()
|
|
2850
|
+
byClass.set(modelClass, byRelated)
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
byRelated.set(isRelated, resource)
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
/**
|
|
2857
|
+
* Sets a per-instance hook invoked for every serialization resource instance
|
|
2858
|
+
* resolution. The hook is scoped to this controller; it never affects other
|
|
2859
|
+
* controller instances. Passing `null` clears the hook.
|
|
2860
|
+
* @param {FrontendModelSerializationResourceInstanceHook | null} hook - Hook callback or null.
|
|
2861
|
+
* @returns {() => void} - Cleanup function that restores the previous hook.
|
|
2862
|
+
*/
|
|
2863
|
+
setSerializationResourceInstanceHook(hook) {
|
|
2864
|
+
const previousHook = this._frontendModelSerializationResourceInstanceHook
|
|
2865
|
+
|
|
2866
|
+
this._frontendModelSerializationResourceInstanceHook = hook
|
|
2867
|
+
|
|
2868
|
+
return () => {
|
|
2869
|
+
this._frontendModelSerializationResourceInstanceHook = previousHook
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2795
2873
|
/**
|
|
2796
2874
|
* Runs serialization resource instance for model.
|
|
2797
2875
|
* @param {import("./database/record/index.js").default} model - Model instance.
|
|
2798
2876
|
* @returns {import("./frontend-model-resource/base-resource.js").default | null} - Resource instance or null.
|
|
2799
2877
|
*/
|
|
2800
2878
|
_serializationResourceInstanceForModel(model) {
|
|
2801
|
-
const
|
|
2879
|
+
const modelClass = /** @type {typeof import("./database/record/index.js").default} */ (model.constructor)
|
|
2880
|
+
const isRelated = modelClass !== this.frontendModelClass()
|
|
2881
|
+
const cachedResource = this._cachedSerializationResourceInstance(modelClass, isRelated)
|
|
2882
|
+
|
|
2883
|
+
if (cachedResource) {
|
|
2884
|
+
if (this._frontendModelSerializationResourceInstanceHook) {
|
|
2885
|
+
this._frontendModelSerializationResourceInstanceHook(model, cachedResource)
|
|
2886
|
+
}
|
|
2802
2887
|
|
|
2803
|
-
|
|
2804
|
-
return resource
|
|
2888
|
+
return cachedResource
|
|
2805
2889
|
}
|
|
2806
2890
|
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
const modelClassName = /** @type {typeof import("./database/record/index.js").default} */ (model.constructor).getModelName()
|
|
2891
|
+
/** @type {import("./frontend-model-resource/base-resource.js").default | null} */
|
|
2892
|
+
let resource
|
|
2810
2893
|
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2894
|
+
if (!isRelated) {
|
|
2895
|
+
resource = this.frontendModelResourceInstance()
|
|
2896
|
+
|
|
2897
|
+
this._setCachedSerializationResourceInstance(modelClass, false, resource)
|
|
2898
|
+
} else {
|
|
2899
|
+
const configuration = this.getConfiguration()
|
|
2900
|
+
const backendProjects = configuration.getBackendProjects()
|
|
2901
|
+
const modelClassName = modelClass.getModelName()
|
|
2902
|
+
|
|
2903
|
+
resource = null
|
|
2904
|
+
|
|
2905
|
+
for (const backendProject of backendProjects) {
|
|
2906
|
+
const resources = frontendModelResourcesWithBuiltInsForBackendProject(backendProject)
|
|
2907
|
+
const resourceDefinition = resources[modelClassName]
|
|
2908
|
+
const resourceClass = resourceDefinition ? frontendModelResourceClassFromDefinition(resourceDefinition) : null
|
|
2909
|
+
|
|
2910
|
+
if (resourceClass) {
|
|
2911
|
+
resource = new resourceClass({
|
|
2912
|
+
ability: this.currentAbility(),
|
|
2913
|
+
// Propagate the controller so a related/preloaded model's serialization
|
|
2914
|
+
// resource can use request context (e.g. `requestBaseUrl()` for signed
|
|
2915
|
+
// download URLs). Without it, any `<attr>Attribute` method that reaches
|
|
2916
|
+
// for the controller throws "requires a controller instance." when a
|
|
2917
|
+
// relationship is serialized as a preload.
|
|
2918
|
+
controller: this,
|
|
2919
|
+
context: this.currentAbility()?.getContext() || {},
|
|
2920
|
+
locals: this.currentAbility()?.getLocals() || {},
|
|
2921
|
+
modelClass,
|
|
2922
|
+
modelName: modelClassName,
|
|
2923
|
+
params: {},
|
|
2924
|
+
resourceConfiguration: resourceClass.resourceConfig()
|
|
2925
|
+
})
|
|
2926
|
+
|
|
2927
|
+
this._setCachedSerializationResourceInstance(modelClass, true, resource)
|
|
2928
|
+
|
|
2929
|
+
break
|
|
2930
|
+
}
|
|
2832
2931
|
}
|
|
2833
2932
|
}
|
|
2834
2933
|
|
|
2835
|
-
|
|
2934
|
+
if (this._frontendModelSerializationResourceInstanceHook) {
|
|
2935
|
+
this._frontendModelSerializationResourceInstanceHook(model, resource)
|
|
2936
|
+
}
|
|
2937
|
+
|
|
2938
|
+
return resource
|
|
2836
2939
|
}
|
|
2837
2940
|
|
|
2838
2941
|
/**
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @property {string[]} filteredProcessArgs - Remaining process args with filter flags removed.
|
|
8
8
|
* @property {number | undefined} groups - Total number of groups for test splitting.
|
|
9
9
|
* @property {number | undefined} groupNumber - Which group to run (1-indexed).
|
|
10
|
+
* @property {string | undefined} timingManifestPath - JSON timing manifest path.
|
|
10
11
|
*/
|
|
11
12
|
// @ts-check
|
|
12
13
|
|
|
@@ -15,6 +16,7 @@ const EXCLUDE_TAG_FLAGS = new Set(["--exclude-tag", "--skip-tag", "-x"])
|
|
|
15
16
|
const EXAMPLE_FLAGS = new Set(["--example", "--name", "-e"])
|
|
16
17
|
const GROUPS_FLAGS = new Set(["--groups"])
|
|
17
18
|
const GROUP_NUMBER_FLAGS = new Set(["--group-number"])
|
|
19
|
+
const TIMING_MANIFEST_FLAGS = new Set(["--timing-manifest"])
|
|
18
20
|
|
|
19
21
|
/**
|
|
20
22
|
* Runs split tags.
|
|
@@ -79,6 +81,8 @@ export function parseFilters(processArgs) {
|
|
|
79
81
|
* Defines groupNumber.
|
|
80
82
|
* @type {number | undefined} */
|
|
81
83
|
let groupNumber
|
|
84
|
+
/** @type {string | undefined} */
|
|
85
|
+
let timingManifestPath
|
|
82
86
|
|
|
83
87
|
let inRestArgs = false
|
|
84
88
|
|
|
@@ -181,6 +185,21 @@ export function parseFilters(processArgs) {
|
|
|
181
185
|
groupNumber = parseInt(arg.slice("--group-number=".length), 10)
|
|
182
186
|
continue
|
|
183
187
|
}
|
|
188
|
+
|
|
189
|
+
if (TIMING_MANIFEST_FLAGS.has(arg)) {
|
|
190
|
+
const nextValue = processArgs[i + 1]
|
|
191
|
+
|
|
192
|
+
if (nextValue && !nextValue.startsWith("-")) {
|
|
193
|
+
timingManifestPath = nextValue
|
|
194
|
+
i++
|
|
195
|
+
}
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (arg.startsWith("--timing-manifest=")) {
|
|
200
|
+
timingManifestPath = arg.slice("--timing-manifest=".length)
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
184
203
|
}
|
|
185
204
|
|
|
186
205
|
filteredProcessArgs.push(arg)
|
|
@@ -192,6 +211,7 @@ export function parseFilters(processArgs) {
|
|
|
192
211
|
examplePatterns,
|
|
193
212
|
filteredProcessArgs,
|
|
194
213
|
groups,
|
|
195
|
-
groupNumber
|
|
214
|
+
groupNumber,
|
|
215
|
+
timingManifestPath
|
|
196
216
|
}
|
|
197
217
|
}
|
|
@@ -46,8 +46,9 @@ export default class TestSuiteSplitter {
|
|
|
46
46
|
* @param {number} args.groupNumber - Which group to return (1-indexed).
|
|
47
47
|
* @param {string[]} args.testFiles - All discovered test file paths.
|
|
48
48
|
* @param {string} [args.baseDirectory] - Base directory for relative path computation.
|
|
49
|
+
* @param {ReturnType<typeof JSON.parse>} [args.timingManifest] - Relative test paths mapped to durations.
|
|
49
50
|
*/
|
|
50
|
-
constructor({groups, groupNumber, testFiles, baseDirectory, ...restArgs}) {
|
|
51
|
+
constructor({groups, groupNumber, testFiles, baseDirectory, timingManifest, ...restArgs}) {
|
|
51
52
|
restArgsError(restArgs)
|
|
52
53
|
|
|
53
54
|
if (!Number.isInteger(groups) || groups < 1) {
|
|
@@ -62,6 +63,7 @@ export default class TestSuiteSplitter {
|
|
|
62
63
|
this._groupNumber = groupNumber
|
|
63
64
|
this._testFiles = testFiles
|
|
64
65
|
this._baseDirectory = baseDirectory || process.cwd()
|
|
66
|
+
this._timingManifest = this.normalizeTimingManifest(timingManifest)
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -93,7 +95,14 @@ export default class TestSuiteSplitter {
|
|
|
93
95
|
* @returns {number} - Weight value.
|
|
94
96
|
*/
|
|
95
97
|
computeWeight(filePath) {
|
|
96
|
-
const relativePath = path.relative(this._baseDirectory, filePath)
|
|
98
|
+
const relativePath = this.normalizeRelativePath(path.relative(this._baseDirectory, filePath))
|
|
99
|
+
const projectRelativePath = this.normalizeRelativePath(path.join(path.basename(this._baseDirectory), relativePath))
|
|
100
|
+
const duration = this._timingManifest[relativePath] ?? this._timingManifest[projectRelativePath]
|
|
101
|
+
|
|
102
|
+
if (duration !== undefined) {
|
|
103
|
+
return duration
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
let weight = DEFAULT_WEIGHT
|
|
98
107
|
|
|
99
108
|
// Extract the type directory from the relative path.
|
|
@@ -116,6 +125,39 @@ export default class TestSuiteSplitter {
|
|
|
116
125
|
return weight
|
|
117
126
|
}
|
|
118
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) {
|
|
143
|
+
normalized[this.normalizeRelativePath(filePath)] = duration
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return normalized
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Normalizes a relative test path to the manifest's portable slash format.
|
|
152
|
+
* @param {string} filePath - Relative test path.
|
|
153
|
+
* @returns {string} - Normalized relative test path.
|
|
154
|
+
*/
|
|
155
|
+
normalizeRelativePath(filePath) {
|
|
156
|
+
return filePath
|
|
157
|
+
.replaceAll("\\", "/")
|
|
158
|
+
.replace(/^\.\//, "")
|
|
159
|
+
}
|
|
160
|
+
|
|
119
161
|
/**
|
|
120
162
|
* Sorts files by weight descending, then by path for determinism.
|
|
121
163
|
* @param {SplitterFileEntry[]} files - Weighted files.
|