velocious 1.0.581 → 1.0.583
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 -3
- package/build/configuration-types.js +2 -0
- package/build/database/drivers/base.js +209 -13
- package/build/database/query/preloader/belongs-to.js +32 -31
- package/build/database/query/preloader/has-many.js +50 -26
- package/build/database/query/preloader/has-one.js +20 -12
- package/build/database/query/query-data.js +23 -8
- package/build/database/query/with-count.js +47 -11
- package/build/environment-handlers/node/cli/commands/test.js +21 -2
- package/build/frontend-model-controller.js +131 -28
- package/build/src/configuration-types.d.ts +10 -0
- package/build/src/configuration-types.d.ts.map +1 -1
- package/build/src/configuration-types.js +3 -1
- package/build/src/database/drivers/base.d.ts +71 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +178 -17
- package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
- package/build/src/database/query/preloader/belongs-to.js +33 -31
- package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-many.js +48 -28
- package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
- package/build/src/database/query/preloader/has-one.js +21 -13
- package/build/src/database/query/query-data.js +21 -8
- package/build/src/database/query/with-count.js +41 -12
- 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 +4 -1
- package/src/configuration-types.js +2 -0
- package/src/database/drivers/base.js +209 -13
- package/src/database/query/preloader/belongs-to.js +32 -31
- package/src/database/query/preloader/has-many.js +50 -26
- package/src/database/query/preloader/has-one.js +20 -12
- package/src/database/query/query-data.js +23 -8
- package/src/database/query/with-count.js +47 -11
- 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
|
@@ -115,19 +115,19 @@ export async function runWithCount({models, modelClass, entries}) {
|
|
|
115
115
|
const queryGroups = new Map()
|
|
116
116
|
|
|
117
117
|
for (const entry of entries) {
|
|
118
|
-
const
|
|
119
|
-
const sql =
|
|
118
|
+
const {baseQuery, foreignKey} = queryForEntry({entry, modelClass, sourceModel})
|
|
119
|
+
const sql = baseQuery.toSql()
|
|
120
120
|
const existingGroup = queryGroups.get(sql)
|
|
121
121
|
|
|
122
122
|
if (existingGroup) {
|
|
123
123
|
existingGroup.entries.push(entry)
|
|
124
124
|
} else {
|
|
125
|
-
queryGroups.set(sql, {
|
|
125
|
+
queryGroups.set(sql, {baseQuery, entries: [entry], foreignKey})
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
for (const {
|
|
130
|
-
const counts = await
|
|
129
|
+
for (const {baseQuery, entries: groupedEntries, foreignKey} of queryGroups.values()) {
|
|
130
|
+
const counts = await executeChunkedCountQuery({baseQuery, foreignKey, parentIds})
|
|
131
131
|
|
|
132
132
|
for (const entry of groupedEntries) attachCounts({counts, entry, models, primaryKey})
|
|
133
133
|
}
|
|
@@ -135,14 +135,16 @@ export async function runWithCount({models, modelClass, entries}) {
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Builds the grouped count query for an entry.
|
|
138
|
+
*
|
|
139
|
+
* The returned query does NOT yet filter by parent IDs; callers chunk the
|
|
140
|
+
* parent cohort and apply the foreign-key IN clause per chunk.
|
|
138
141
|
* @param {object} args - Options.
|
|
139
142
|
* @param {WithCountEntry} args.entry - Entry being evaluated.
|
|
140
143
|
* @param {typeof import("../record/index.js").default} args.modelClass - Parent model class.
|
|
141
|
-
* @param {Array<string | number>} args.parentIds - Primary keys of the loaded parents.
|
|
142
144
|
* @param {import("../record/index.js").default} args.sourceModel - Loaded operation owner.
|
|
143
|
-
* @returns {import("./model-class-query.js").default} - Prepared count query.
|
|
145
|
+
* @returns {{baseQuery: import("./model-class-query.js").default, foreignKey: string}} - Prepared count query and its foreign key.
|
|
144
146
|
*/
|
|
145
|
-
function queryForEntry({entry, modelClass,
|
|
147
|
+
function queryForEntry({entry, modelClass, sourceModel}) {
|
|
146
148
|
const relationship = modelClass.getRelationshipByName(entry.relationshipName)
|
|
147
149
|
|
|
148
150
|
if (!relationship) {
|
|
@@ -163,7 +165,7 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
163
165
|
/**
|
|
164
166
|
* Mandatory cohort conditions.
|
|
165
167
|
* @type {Record<string, ReturnType<typeof JSON.parse>>} */
|
|
166
|
-
const mandatoryWhereConditions = {
|
|
168
|
+
const mandatoryWhereConditions = {}
|
|
167
169
|
|
|
168
170
|
if (relationship.getPolymorphic && relationship.getPolymorphic()) {
|
|
169
171
|
const typeColumn = relationship.getPolymorphicTypeColumn()
|
|
@@ -171,8 +173,12 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
const baseQuery = sourceModel.queryForModel(targetModelClass)
|
|
176
|
+
|
|
174
177
|
baseQuery._forceQualifyBaseTable = true
|
|
175
|
-
|
|
178
|
+
|
|
179
|
+
if (Object.keys(mandatoryWhereConditions).length > 0) {
|
|
180
|
+
baseQuery.where(mandatoryWhereConditions)
|
|
181
|
+
}
|
|
176
182
|
|
|
177
183
|
if (entry.where) {
|
|
178
184
|
baseQuery.where(entry.where)
|
|
@@ -192,7 +198,7 @@ function queryForEntry({entry, modelClass, parentIds, sourceModel}) {
|
|
|
192
198
|
countQuery.select(`${qualifiedForeignKey} AS parent_id`)
|
|
193
199
|
countQuery.select("COUNT(*) AS count_value")
|
|
194
200
|
|
|
195
|
-
return countQuery
|
|
201
|
+
return {baseQuery: countQuery, foreignKey}
|
|
196
202
|
}
|
|
197
203
|
|
|
198
204
|
/**
|
|
@@ -219,6 +225,36 @@ async function executeCountQuery(countQuery) {
|
|
|
219
225
|
return counts
|
|
220
226
|
}
|
|
221
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Executes a grouped count query in cohorts so the parent ID IN-list stays
|
|
230
|
+
* within driver limits, merging per-parent counts across chunks.
|
|
231
|
+
* @param {object} args - Options.
|
|
232
|
+
* @param {import("./model-class-query.js").default} args.baseQuery - Prepared count query without parent IDs.
|
|
233
|
+
* @param {string} args.foreignKey - Foreign key used to join to the parents.
|
|
234
|
+
* @param {Array<string | number>} args.parentIds - Primary keys of the loaded parents.
|
|
235
|
+
* @returns {Promise<Map<string | number, number>>} - Map of parent pk → count.
|
|
236
|
+
*/
|
|
237
|
+
async function executeChunkedCountQuery({baseQuery, foreignKey, parentIds}) {
|
|
238
|
+
const driver = baseQuery.driver
|
|
239
|
+
const cohorts = driver.chunkValues(parentIds, (chunk) => baseQuery.clone().where({[foreignKey]: chunk}).toSql())
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Counts.
|
|
243
|
+
* @type {Map<string | number, number>} */
|
|
244
|
+
const counts = new Map()
|
|
245
|
+
|
|
246
|
+
for (const cohort of cohorts) {
|
|
247
|
+
const cohortQuery = baseQuery.clone().where({[foreignKey]: cohort})
|
|
248
|
+
const cohortCounts = await executeCountQuery(cohortQuery)
|
|
249
|
+
|
|
250
|
+
for (const [parentId, count] of cohortCounts) {
|
|
251
|
+
counts.set(parentId, count)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return counts
|
|
256
|
+
}
|
|
257
|
+
|
|
222
258
|
/**
|
|
223
259
|
* Attaches one entry's resolved counts to the loaded models.
|
|
224
260
|
* @param {object} args - Options.
|
|
@@ -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.
|