@test-station/core 0.1.0
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/package.json +23 -0
- package/src/adapters.js +73 -0
- package/src/artifacts.js +65 -0
- package/src/config.js +99 -0
- package/src/console.js +162 -0
- package/src/coverage.js +98 -0
- package/src/index.js +8 -0
- package/src/policy.js +597 -0
- package/src/report.js +517 -0
- package/src/run-report.js +284 -0
package/src/policy.js
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { createSourceAnalysisPlugin } from '@test-station/plugin-source-analysis';
|
|
5
|
+
import { readJson, resolveMaybeRelative } from './config.js';
|
|
6
|
+
|
|
7
|
+
const builtInPluginFactories = {
|
|
8
|
+
'source-analysis': createSourceAnalysisPlugin,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export async function preparePolicyContext(loadedConfig, project) {
|
|
12
|
+
const manifests = loadPolicyManifests(loadedConfig.config, loadedConfig.configDir);
|
|
13
|
+
const plugins = await resolvePolicyPlugins(loadedConfig.config, loadedConfig.configDir);
|
|
14
|
+
const ownershipLookup = buildOwnershipLookup(manifests);
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
manifests,
|
|
18
|
+
plugins,
|
|
19
|
+
ownershipLookup,
|
|
20
|
+
projectRootDir: project.rootDir,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function applyPolicyPipeline(context, suiteResults) {
|
|
25
|
+
if (!context?.policy) {
|
|
26
|
+
return suiteResults;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const normalizedSuites = [];
|
|
30
|
+
for (const suite of suiteResults) {
|
|
31
|
+
const tests = [];
|
|
32
|
+
for (const test of suite.tests || []) {
|
|
33
|
+
const classified = resolveTestClassification(context, suite, test);
|
|
34
|
+
let nextTest = {
|
|
35
|
+
...test,
|
|
36
|
+
module: classified.module,
|
|
37
|
+
theme: classified.theme,
|
|
38
|
+
classificationSource: classified.source,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const plugin of context.policy.plugins || []) {
|
|
42
|
+
if (typeof plugin?.enrichTest !== 'function') {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const patch = await plugin.enrichTest({
|
|
46
|
+
config: context.config,
|
|
47
|
+
project: context.project,
|
|
48
|
+
suite,
|
|
49
|
+
test: nextTest,
|
|
50
|
+
manifests: context.policy.manifests,
|
|
51
|
+
helpers: {
|
|
52
|
+
packageRelativeCandidates: getPackageRelativeCandidates(context, suite, test.file),
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
if (patch && typeof patch === 'object') {
|
|
56
|
+
nextTest = mergeTestPatch(nextTest, patch);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tests.push(nextTest);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
normalizedSuites.push({
|
|
64
|
+
...suite,
|
|
65
|
+
tests,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return normalizedSuites;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function collectCoverageAttribution(policy, packages, project) {
|
|
73
|
+
const files = [];
|
|
74
|
+
const summary = {
|
|
75
|
+
totalFiles: 0,
|
|
76
|
+
attributedFiles: 0,
|
|
77
|
+
sharedFiles: 0,
|
|
78
|
+
moduleOnlyFiles: 0,
|
|
79
|
+
packageOnlySharedFiles: 0,
|
|
80
|
+
unattributedFiles: 0,
|
|
81
|
+
manifestFiles: 0,
|
|
82
|
+
heuristicFiles: 0,
|
|
83
|
+
pluginFiles: 0,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
for (const pkg of packages || []) {
|
|
87
|
+
for (const file of pkg.coverage?.files || []) {
|
|
88
|
+
summary.totalFiles += 1;
|
|
89
|
+
const attribution = classifyCoverageFile(policy, pkg, file, project);
|
|
90
|
+
if (attribution.shared) {
|
|
91
|
+
summary.sharedFiles += 1;
|
|
92
|
+
}
|
|
93
|
+
if (attribution.packageOnly) {
|
|
94
|
+
summary.packageOnlySharedFiles += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!attribution.targets.length) {
|
|
98
|
+
summary.unattributedFiles += 1;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
summary.attributedFiles += 1;
|
|
102
|
+
if (attribution.source === 'manifest') summary.manifestFiles += 1;
|
|
103
|
+
if (attribution.source === 'heuristic') summary.heuristicFiles += 1;
|
|
104
|
+
if (attribution.source === 'plugin') summary.pluginFiles += 1;
|
|
105
|
+
if (attribution.targets.every((target) => !target.theme)) {
|
|
106
|
+
summary.moduleOnlyFiles += 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const sharedWeight = attribution.targets.length > 1
|
|
110
|
+
? Number((1 / attribution.targets.length).toFixed(6))
|
|
111
|
+
: 1;
|
|
112
|
+
for (const target of attribution.targets) {
|
|
113
|
+
files.push({
|
|
114
|
+
...file,
|
|
115
|
+
packageName: pkg.name,
|
|
116
|
+
module: target.module,
|
|
117
|
+
theme: target.theme || null,
|
|
118
|
+
shared: Boolean(target.shared || attribution.shared),
|
|
119
|
+
attributionSource: attribution.source,
|
|
120
|
+
attributionReason: attribution.reason || null,
|
|
121
|
+
attributionWeight: sharedWeight,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { files, summary };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function lookupOwner(policy, moduleName, themeName = null, details = {}) {
|
|
131
|
+
for (const plugin of policy?.plugins || []) {
|
|
132
|
+
if (typeof plugin?.lookupOwner !== 'function') {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const owner = plugin.lookupOwner({
|
|
136
|
+
module: moduleName,
|
|
137
|
+
theme: themeName,
|
|
138
|
+
manifests: policy.manifests,
|
|
139
|
+
details,
|
|
140
|
+
});
|
|
141
|
+
if (owner) {
|
|
142
|
+
return String(owner);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (themeName) {
|
|
147
|
+
const themeOwner = policy?.ownershipLookup?.themeOwners?.get(`${moduleName}/${themeName}`);
|
|
148
|
+
if (themeOwner) {
|
|
149
|
+
return themeOwner;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return policy?.ownershipLookup?.moduleOwners?.get(moduleName || '') || null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resolveTestClassification(context, suite, test) {
|
|
157
|
+
const pluginClassification = runPluginClassification(context, suite, test);
|
|
158
|
+
if (pluginClassification) {
|
|
159
|
+
return pluginClassification;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const manifestMatch = matchClassificationManifest(context.policy?.manifests?.classification, context, suite, test);
|
|
163
|
+
if (manifestMatch) {
|
|
164
|
+
return manifestMatch;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (test?.module && test.module !== 'uncategorized') {
|
|
168
|
+
return {
|
|
169
|
+
module: test.module,
|
|
170
|
+
theme: test.theme || 'uncategorized',
|
|
171
|
+
source: test.classificationSource || 'adapter',
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
module: 'uncategorized',
|
|
177
|
+
theme: 'uncategorized',
|
|
178
|
+
source: 'default',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runPluginClassification(context, suite, test) {
|
|
183
|
+
for (const plugin of context.policy?.plugins || []) {
|
|
184
|
+
if (typeof plugin?.classifyTest !== 'function') {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const classification = plugin.classifyTest({
|
|
188
|
+
config: context.config,
|
|
189
|
+
project: context.project,
|
|
190
|
+
suite,
|
|
191
|
+
test,
|
|
192
|
+
manifests: context.policy.manifests,
|
|
193
|
+
helpers: {
|
|
194
|
+
packageRelativeCandidates: getPackageRelativeCandidates(context, suite, test.file),
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const normalized = normalizeClassification(classification, 'plugin');
|
|
198
|
+
if (normalized) {
|
|
199
|
+
return normalized;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function matchClassificationManifest(manifest, context, suite, test) {
|
|
206
|
+
const rules = Array.isArray(manifest?.rules) ? manifest.rules : [];
|
|
207
|
+
const relativeCandidates = getPackageRelativeCandidates(context, suite, test.file);
|
|
208
|
+
if (relativeCandidates.length === 0) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const rule of rules) {
|
|
213
|
+
if (suite?.packageName && rule.package && rule.package !== suite.packageName) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
for (const relativePath of relativeCandidates) {
|
|
217
|
+
if (!matchesConfiguredRule(rule, relativePath, suite?.packageName || null)) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
module: rule.module || 'uncategorized',
|
|
222
|
+
theme: rule.theme || 'uncategorized',
|
|
223
|
+
source: 'manifest',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function classifyCoverageFile(policy, pkg, file, project) {
|
|
232
|
+
for (const plugin of policy?.plugins || []) {
|
|
233
|
+
if (typeof plugin?.attributeCoverageFile !== 'function') {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
const attribution = plugin.attributeCoverageFile({
|
|
237
|
+
packageName: pkg.name,
|
|
238
|
+
file,
|
|
239
|
+
project,
|
|
240
|
+
manifests: policy.manifests,
|
|
241
|
+
relativePath: normalizeProjectRelative(project?.rootDir, file.path),
|
|
242
|
+
});
|
|
243
|
+
const normalized = normalizeCoverageAttribution(attribution, 'plugin');
|
|
244
|
+
if (normalized) {
|
|
245
|
+
return normalized;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const manifestMatch = matchCoverageManifestRule(policy?.manifests?.coverageAttribution, normalizeProjectRelative(project?.rootDir, file.path), pkg.name);
|
|
250
|
+
if (manifestMatch) {
|
|
251
|
+
return manifestMatch;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (file?.module) {
|
|
255
|
+
return {
|
|
256
|
+
targets: [
|
|
257
|
+
{
|
|
258
|
+
module: file.module,
|
|
259
|
+
theme: file.theme || null,
|
|
260
|
+
shared: Boolean(file.shared),
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
shared: Boolean(file.shared),
|
|
264
|
+
packageOnly: false,
|
|
265
|
+
source: file.attributionSource || 'adapter',
|
|
266
|
+
reason: file.attributionReason || null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
targets: [],
|
|
272
|
+
shared: false,
|
|
273
|
+
packageOnly: false,
|
|
274
|
+
source: 'default',
|
|
275
|
+
reason: null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function matchCoverageManifestRule(manifest, relativePath, packageName = null) {
|
|
280
|
+
const coverageRules = Array.isArray(manifest?.coverageRules) ? manifest.coverageRules : [];
|
|
281
|
+
for (const rule of coverageRules) {
|
|
282
|
+
if (!matchesConfiguredRule(rule, relativePath, packageName)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
return {
|
|
286
|
+
targets: normalizeCoverageTargets(rule),
|
|
287
|
+
shared: Boolean(rule.shared),
|
|
288
|
+
packageOnly: rule.scope === 'package-only' || normalizeCoverageTargets(rule).length === 0,
|
|
289
|
+
source: 'manifest',
|
|
290
|
+
reason: rule.reason || null,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeCoverageTargets(rule) {
|
|
297
|
+
const ruleTargets = Array.isArray(rule.targets)
|
|
298
|
+
? rule.targets
|
|
299
|
+
: (rule.module ? [{ module: rule.module, theme: rule.theme || null }] : []);
|
|
300
|
+
|
|
301
|
+
return ruleTargets
|
|
302
|
+
.filter((target) => target && target.module)
|
|
303
|
+
.map((target) => ({
|
|
304
|
+
module: target.module,
|
|
305
|
+
theme: target.theme || null,
|
|
306
|
+
shared: Boolean(rule.shared || target.shared),
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function normalizeClassification(value, sourceFallback) {
|
|
311
|
+
if (!value || typeof value !== 'object') {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (!value.module) {
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
module: value.module,
|
|
319
|
+
theme: value.theme || 'uncategorized',
|
|
320
|
+
source: value.source || sourceFallback,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function normalizeCoverageAttribution(value, sourceFallback) {
|
|
325
|
+
if (!value || typeof value !== 'object') {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
const targets = Array.isArray(value.targets)
|
|
329
|
+
? value.targets.filter((target) => target && target.module).map((target) => ({
|
|
330
|
+
module: target.module,
|
|
331
|
+
theme: target.theme || null,
|
|
332
|
+
shared: Boolean(target.shared),
|
|
333
|
+
}))
|
|
334
|
+
: (value.module ? [{ module: value.module, theme: value.theme || null, shared: Boolean(value.shared) }] : []);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
targets,
|
|
338
|
+
shared: Boolean(value.shared),
|
|
339
|
+
packageOnly: Boolean(value.packageOnly),
|
|
340
|
+
source: value.source || sourceFallback,
|
|
341
|
+
reason: value.reason || null,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function buildOwnershipLookup(manifests) {
|
|
346
|
+
const moduleOwners = new Map();
|
|
347
|
+
const themeOwners = new Map();
|
|
348
|
+
const ownership = manifests?.ownership?.ownership || {};
|
|
349
|
+
|
|
350
|
+
for (const entry of ownership.modules || []) {
|
|
351
|
+
if (!entry?.module || !entry?.owner) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
moduleOwners.set(entry.module, String(entry.owner));
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
for (const entry of ownership.themes || []) {
|
|
358
|
+
if (!entry?.module || !entry?.theme || !entry?.owner) {
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
themeOwners.set(`${entry.module}/${entry.theme}`, String(entry.owner));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return { moduleOwners, themeOwners };
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function loadPolicyManifests(config, configDir) {
|
|
368
|
+
const cache = new Map();
|
|
369
|
+
return {
|
|
370
|
+
classification: loadManifestEntry(resolveManifestPath(config, 'classification'), configDir, cache),
|
|
371
|
+
coverageAttribution: loadManifestEntry(resolveManifestPath(config, 'coverageAttribution'), configDir, cache),
|
|
372
|
+
ownership: loadManifestEntry(resolveManifestPath(config, 'ownership'), configDir, cache),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function resolveManifestPath(config, key) {
|
|
377
|
+
if (config?.manifests?.[key]) {
|
|
378
|
+
return config.manifests[key];
|
|
379
|
+
}
|
|
380
|
+
const legacySection = config?.[key];
|
|
381
|
+
if (legacySection?.manifestPath) {
|
|
382
|
+
return legacySection.manifestPath;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function loadManifestEntry(manifestPath, configDir, cache) {
|
|
388
|
+
if (!manifestPath) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const resolved = resolveMaybeRelative(configDir, manifestPath);
|
|
392
|
+
if (cache.has(resolved)) {
|
|
393
|
+
return cache.get(resolved);
|
|
394
|
+
}
|
|
395
|
+
if (!fs.existsSync(resolved)) {
|
|
396
|
+
cache.set(resolved, null);
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
const payload = readJson(resolved);
|
|
400
|
+
const manifest = {
|
|
401
|
+
path: resolved,
|
|
402
|
+
rules: Array.isArray(payload.rules) ? payload.rules : [],
|
|
403
|
+
coverageRules: Array.isArray(payload.coverageRules) ? payload.coverageRules : [],
|
|
404
|
+
ownership: {
|
|
405
|
+
modules: Array.isArray(payload.ownership?.modules) ? payload.ownership.modules : [],
|
|
406
|
+
themes: Array.isArray(payload.ownership?.themes) ? payload.ownership.themes : [],
|
|
407
|
+
},
|
|
408
|
+
raw: payload,
|
|
409
|
+
};
|
|
410
|
+
cache.set(resolved, manifest);
|
|
411
|
+
return manifest;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function resolvePolicyPlugins(config, configDir) {
|
|
415
|
+
const entries = gatherPluginEntries(config);
|
|
416
|
+
const loaded = [];
|
|
417
|
+
const seen = new Set();
|
|
418
|
+
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
const plugin = await resolvePluginEntry(entry, configDir);
|
|
421
|
+
if (!plugin) {
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
const key = plugin.id || JSON.stringify(Object.keys(plugin).sort());
|
|
425
|
+
if (seen.has(key)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
seen.add(key);
|
|
429
|
+
loaded.push(plugin);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return loaded;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function gatherPluginEntries(config) {
|
|
436
|
+
const entries = [];
|
|
437
|
+
if (Array.isArray(config?.plugins)) {
|
|
438
|
+
entries.push(...config.plugins);
|
|
439
|
+
}
|
|
440
|
+
if (Array.isArray(config?.classification?.plugins)) {
|
|
441
|
+
entries.push(...config.classification.plugins);
|
|
442
|
+
}
|
|
443
|
+
if (Array.isArray(config?.coverageAttribution?.plugins)) {
|
|
444
|
+
entries.push(...config.coverageAttribution.plugins);
|
|
445
|
+
}
|
|
446
|
+
if (Array.isArray(config?.ownership?.plugins)) {
|
|
447
|
+
entries.push(...config.ownership.plugins);
|
|
448
|
+
}
|
|
449
|
+
const sourceAnalysis = config?.enrichers?.sourceAnalysis;
|
|
450
|
+
if (Array.isArray(sourceAnalysis?.plugins) && sourceAnalysis.plugins.length > 0) {
|
|
451
|
+
entries.push(...sourceAnalysis.plugins);
|
|
452
|
+
} else if (sourceAnalysis?.enabled !== false) {
|
|
453
|
+
entries.push({ use: 'source-analysis', options: sourceAnalysis || {} });
|
|
454
|
+
}
|
|
455
|
+
return entries;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function resolvePluginEntry(entry, configDir) {
|
|
459
|
+
if (!entry) {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
if (hasPolicyHooks(entry)) {
|
|
463
|
+
return entry;
|
|
464
|
+
}
|
|
465
|
+
if (typeof entry === 'string') {
|
|
466
|
+
if (builtInPluginFactories[entry]) {
|
|
467
|
+
return builtInPluginFactories[entry]();
|
|
468
|
+
}
|
|
469
|
+
return loadPluginModule(resolveMaybeRelative(configDir, entry));
|
|
470
|
+
}
|
|
471
|
+
if (typeof entry === 'object') {
|
|
472
|
+
if (entry.plugin && hasPolicyHooks(entry.plugin)) {
|
|
473
|
+
return entry.plugin;
|
|
474
|
+
}
|
|
475
|
+
if (entry.use && builtInPluginFactories[entry.use]) {
|
|
476
|
+
return builtInPluginFactories[entry.use](entry.options || {});
|
|
477
|
+
}
|
|
478
|
+
if (entry.handler) {
|
|
479
|
+
return loadPluginModule(resolveMaybeRelative(configDir, entry.handler), entry.options || {});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function loadPluginModule(modulePath, options = {}) {
|
|
486
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
487
|
+
if (hasPolicyHooks(mod.default)) {
|
|
488
|
+
return mod.default;
|
|
489
|
+
}
|
|
490
|
+
if (typeof mod.createPlugin === 'function') {
|
|
491
|
+
return mod.createPlugin(options);
|
|
492
|
+
}
|
|
493
|
+
if (mod.default && typeof mod.default === 'function') {
|
|
494
|
+
return mod.default(options);
|
|
495
|
+
}
|
|
496
|
+
throw new Error(`Policy plugin module ${modulePath} did not export a supported plugin contract.`);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function hasPolicyHooks(value) {
|
|
500
|
+
if (!value || typeof value !== 'object') {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
return ['classifyTest', 'attributeCoverageFile', 'lookupOwner', 'enrichTest'].some((key) => typeof value[key] === 'function');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function getPackageRelativeCandidates(context, suite, filePath) {
|
|
507
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
508
|
+
return [];
|
|
509
|
+
}
|
|
510
|
+
const absoluteFilePath = path.resolve(filePath);
|
|
511
|
+
const roots = [];
|
|
512
|
+
if (suite?.packageRoot) {
|
|
513
|
+
roots.push(path.resolve(suite.packageRoot));
|
|
514
|
+
}
|
|
515
|
+
if (suite?.packageName && context?.project?.rootDir) {
|
|
516
|
+
roots.push(path.resolve(context.project.rootDir, 'packages', suite.packageName));
|
|
517
|
+
roots.push(path.resolve(context.project.rootDir, suite.packageName));
|
|
518
|
+
}
|
|
519
|
+
if (suite?.cwd) {
|
|
520
|
+
roots.push(path.resolve(suite.cwd));
|
|
521
|
+
}
|
|
522
|
+
if (context?.project?.rootDir) {
|
|
523
|
+
roots.push(path.resolve(context.project.rootDir));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const candidates = [];
|
|
527
|
+
for (const root of roots) {
|
|
528
|
+
if (!absoluteFilePath.startsWith(root)) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
candidates.push(normalizePath(path.relative(root, absoluteFilePath)));
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
return dedupe(candidates);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function normalizeProjectRelative(rootDir, filePath) {
|
|
538
|
+
if (!rootDir || !filePath) {
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
return normalizePath(path.relative(rootDir, path.resolve(filePath)));
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function matchesConfiguredRule(rule, targetPath, packageName = null) {
|
|
545
|
+
if (!targetPath) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
if (packageName && rule.package && rule.package !== packageName) {
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const includes = Array.isArray(rule.include) ? rule.include : [];
|
|
553
|
+
if (includes.length > 0 && !includes.some((pattern) => matchSimpleGlob(targetPath, pattern))) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const excludes = Array.isArray(rule.exclude) ? rule.exclude : [];
|
|
558
|
+
if (excludes.some((pattern) => matchSimpleGlob(targetPath, pattern))) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return true;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function matchSimpleGlob(value, pattern) {
|
|
566
|
+
const escaped = normalizePath(pattern)
|
|
567
|
+
.replace(/[|\\{}()[\]^$+?.]/g, '\\$&')
|
|
568
|
+
// `**/` should match zero or more nested directories.
|
|
569
|
+
.replace(/\*\*\/?/g, (segment) => (segment.endsWith('/') ? '::DOUBLE_STAR_DIR::' : '::DOUBLE_STAR::'))
|
|
570
|
+
.replace(/\*/g, '[^/]*')
|
|
571
|
+
.replace(/::DOUBLE_STAR_DIR::/g, '(?:.*/)?')
|
|
572
|
+
.replace(/::DOUBLE_STAR::/g, '.*');
|
|
573
|
+
return new RegExp(`^${escaped}$`).test(normalizePath(value));
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function mergeTestPatch(test, patch) {
|
|
577
|
+
return {
|
|
578
|
+
...test,
|
|
579
|
+
...patch,
|
|
580
|
+
assertions: dedupe([...(test.assertions || []), ...(patch.assertions || [])]),
|
|
581
|
+
setup: dedupe([...(test.setup || []), ...(patch.setup || [])]),
|
|
582
|
+
mocks: dedupe([...(test.mocks || []), ...(patch.mocks || [])]),
|
|
583
|
+
failureMessages: dedupe([...(test.failureMessages || []), ...(patch.failureMessages || [])]),
|
|
584
|
+
rawDetails: {
|
|
585
|
+
...(test.rawDetails || {}),
|
|
586
|
+
...(patch.rawDetails || {}),
|
|
587
|
+
},
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function dedupe(values) {
|
|
592
|
+
return Array.from(new Set((values || []).filter(Boolean)));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function normalizePath(value) {
|
|
596
|
+
return String(value || '').replace(/\\/g, '/');
|
|
597
|
+
}
|