@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/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@test-station/core",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@test-station/adapter-jest": "workspace:*",
|
|
13
|
+
"@test-station/adapter-node-test": "workspace:*",
|
|
14
|
+
"@test-station/adapter-playwright": "workspace:*",
|
|
15
|
+
"@test-station/adapter-shell": "workspace:*",
|
|
16
|
+
"@test-station/adapter-vitest": "workspace:*",
|
|
17
|
+
"@test-station/plugin-source-analysis": "workspace:*"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node ../../scripts/check-package.mjs ./src/index.js",
|
|
21
|
+
"lint": "node ../../scripts/lint-syntax.mjs ./src"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/adapters.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { createNodeTestAdapter } from '@test-station/adapter-node-test';
|
|
4
|
+
import { createVitestAdapter } from '@test-station/adapter-vitest';
|
|
5
|
+
import { createPlaywrightAdapter } from '@test-station/adapter-playwright';
|
|
6
|
+
import { createShellAdapter } from '@test-station/adapter-shell';
|
|
7
|
+
import { createJestAdapter } from '@test-station/adapter-jest';
|
|
8
|
+
|
|
9
|
+
const builtInAdapterFactories = {
|
|
10
|
+
'node-test': createNodeTestAdapter,
|
|
11
|
+
vitest: createVitestAdapter,
|
|
12
|
+
playwright: createPlaywrightAdapter,
|
|
13
|
+
shell: createShellAdapter,
|
|
14
|
+
jest: createJestAdapter,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export async function resolveAdapterForSuite(suite, loadedConfig) {
|
|
18
|
+
if (suite?.handler) {
|
|
19
|
+
return loadAdapterModule(resolveMaybeRelative(loadedConfig.configDir, suite.handler));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (suite?.adapter && typeof suite.adapter === 'object' && typeof suite.adapter.run === 'function') {
|
|
23
|
+
return suite.adapter;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof suite?.adapter === 'string') {
|
|
27
|
+
const configured = resolveConfiguredAdapter(suite.adapter, loadedConfig.config?.adapters || [], loadedConfig.configDir);
|
|
28
|
+
if (configured) {
|
|
29
|
+
return configured;
|
|
30
|
+
}
|
|
31
|
+
if (builtInAdapterFactories[suite.adapter]) {
|
|
32
|
+
return builtInAdapterFactories[suite.adapter]();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`Unable to resolve adapter for suite ${suite?.id || '<unknown>'}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveConfiguredAdapter(adapterId, configuredAdapters, configDir) {
|
|
40
|
+
for (const entry of configuredAdapters) {
|
|
41
|
+
if (!entry || entry.id !== adapterId) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (entry.adapter && typeof entry.adapter.run === 'function') {
|
|
45
|
+
return entry.adapter;
|
|
46
|
+
}
|
|
47
|
+
if (entry.handler) {
|
|
48
|
+
return loadAdapterModule(resolveMaybeRelative(configDir, entry.handler));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function loadAdapterModule(modulePath) {
|
|
55
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
56
|
+
if (mod.default && typeof mod.default.run === 'function') {
|
|
57
|
+
return mod.default;
|
|
58
|
+
}
|
|
59
|
+
if (typeof mod.createAdapter === 'function') {
|
|
60
|
+
return mod.createAdapter();
|
|
61
|
+
}
|
|
62
|
+
if (mod.default && typeof mod.default === 'function') {
|
|
63
|
+
return mod.default();
|
|
64
|
+
}
|
|
65
|
+
throw new Error(`Adapter module ${modulePath} did not export a supported adapter contract.`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function resolveMaybeRelative(baseDir, targetPath) {
|
|
69
|
+
if (path.isAbsolute(targetPath)) {
|
|
70
|
+
return targetPath;
|
|
71
|
+
}
|
|
72
|
+
return path.resolve(baseDir, targetPath);
|
|
73
|
+
}
|
package/src/artifacts.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export function writeReportArtifacts(context, report, suiteResults) {
|
|
5
|
+
fs.mkdirSync(context.project.outputDir, { recursive: true });
|
|
6
|
+
fs.mkdirSync(context.project.rawDir, { recursive: true });
|
|
7
|
+
|
|
8
|
+
const reportJsonPath = path.join(context.project.outputDir, 'report.json');
|
|
9
|
+
fs.writeFileSync(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`);
|
|
10
|
+
|
|
11
|
+
const rawSuitePaths = [];
|
|
12
|
+
for (const suite of suiteResults) {
|
|
13
|
+
const suiteFileName = `${slugify(suite.packageName || 'default')}-${slugify(suite.id)}.json`;
|
|
14
|
+
const suitePath = path.join(context.project.rawDir, suiteFileName);
|
|
15
|
+
const payload = {
|
|
16
|
+
id: suite.id,
|
|
17
|
+
packageName: suite.packageName,
|
|
18
|
+
status: suite.status,
|
|
19
|
+
runtime: suite.runtime,
|
|
20
|
+
command: suite.command,
|
|
21
|
+
summary: suite.summary,
|
|
22
|
+
coverage: suite.coverage,
|
|
23
|
+
warnings: suite.warnings,
|
|
24
|
+
rawArtifacts: suite.rawArtifacts,
|
|
25
|
+
output: suite.output,
|
|
26
|
+
tests: suite.tests,
|
|
27
|
+
};
|
|
28
|
+
fs.writeFileSync(suitePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
29
|
+
rawSuitePaths.push(suitePath);
|
|
30
|
+
|
|
31
|
+
for (const artifact of suite.rawArtifacts || []) {
|
|
32
|
+
const relativePath = artifact?.relativePath;
|
|
33
|
+
if (!relativePath) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const artifactPath = path.join(context.project.rawDir, relativePath);
|
|
37
|
+
fs.mkdirSync(path.dirname(artifactPath), { recursive: true });
|
|
38
|
+
if (artifact.sourcePath) {
|
|
39
|
+
copyArtifactSource(artifact.sourcePath, artifactPath, artifact.kind);
|
|
40
|
+
} else {
|
|
41
|
+
fs.writeFileSync(artifactPath, artifact.content || '', artifact.encoding || 'utf8');
|
|
42
|
+
}
|
|
43
|
+
rawSuitePaths.push(artifactPath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
reportJsonPath,
|
|
49
|
+
rawSuitePaths,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function copyArtifactSource(sourcePath, destinationPath, kind) {
|
|
54
|
+
const sourceStat = fs.statSync(sourcePath);
|
|
55
|
+
if (kind === 'directory' || sourceStat.isDirectory()) {
|
|
56
|
+
fs.rmSync(destinationPath, { recursive: true, force: true });
|
|
57
|
+
fs.cpSync(sourcePath, destinationPath, { recursive: true });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
fs.copyFileSync(sourcePath, destinationPath);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function slugify(value) {
|
|
64
|
+
return String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
|
|
65
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
export const CONFIG_SCHEMA_VERSION = '1';
|
|
6
|
+
export const REPORT_SCHEMA_VERSION = '1';
|
|
7
|
+
|
|
8
|
+
export function defineConfig(config) {
|
|
9
|
+
return config;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readJson(filePath) {
|
|
13
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function loadConfig(configPath, options = {}) {
|
|
17
|
+
const cwd = path.resolve(options.cwd || process.cwd());
|
|
18
|
+
const resolved = path.resolve(cwd, configPath);
|
|
19
|
+
const mod = await import(pathToFileURL(resolved).href);
|
|
20
|
+
const config = mod.default || mod;
|
|
21
|
+
return {
|
|
22
|
+
resolvedPath: resolved,
|
|
23
|
+
config,
|
|
24
|
+
configDir: path.dirname(resolved),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function applyConfigOverrides(config, configDir, overrides = {}) {
|
|
29
|
+
const nextConfig = {
|
|
30
|
+
...config,
|
|
31
|
+
project: {
|
|
32
|
+
...(config?.project || {}),
|
|
33
|
+
},
|
|
34
|
+
suites: Array.isArray(config?.suites) ? [...config.suites] : [],
|
|
35
|
+
adapters: Array.isArray(config?.adapters) ? [...config.adapters] : [],
|
|
36
|
+
};
|
|
37
|
+
const overrideCwd = path.resolve(overrides.cwd || process.cwd());
|
|
38
|
+
const workspaceFilters = normalizeStringList(overrides.workspaceFilters);
|
|
39
|
+
|
|
40
|
+
if (overrides.outputDir) {
|
|
41
|
+
const outputDir = resolveMaybeRelative(overrideCwd, overrides.outputDir);
|
|
42
|
+
nextConfig.project.outputDir = outputDir;
|
|
43
|
+
nextConfig.project.rawDir = path.join(outputDir, 'raw');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (workspaceFilters.length > 0) {
|
|
47
|
+
nextConfig.suites = nextConfig.suites.filter((suite) => workspaceFilters.includes(resolveSuitePackageName(suite)));
|
|
48
|
+
if (nextConfig.suites.length === 0) {
|
|
49
|
+
throw new Error(`No suites matched workspaces: ${workspaceFilters.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return nextConfig;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function summarizeConfig(config) {
|
|
57
|
+
return {
|
|
58
|
+
schemaVersion: String(config?.schemaVersion || CONFIG_SCHEMA_VERSION),
|
|
59
|
+
projectName: config?.project?.name || null,
|
|
60
|
+
workspaceProvider: config?.workspaceDiscovery?.provider || null,
|
|
61
|
+
suiteCount: Array.isArray(config?.suites) ? config.suites.length : 0,
|
|
62
|
+
adapterCount: Array.isArray(config?.adapters) ? config.adapters.length : 0,
|
|
63
|
+
manifestKeys: Object.keys(config?.manifests || {}),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveProjectContext(config, configDir) {
|
|
68
|
+
const rootDir = resolveMaybeRelative(configDir, config?.project?.rootDir || '.');
|
|
69
|
+
const outputDir = resolveMaybeRelative(rootDir, config?.project?.outputDir || 'artifacts/workspace-tests');
|
|
70
|
+
const rawDir = resolveMaybeRelative(rootDir, config?.project?.rawDir || path.join(path.relative(rootDir, outputDir), 'raw'));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
name: config?.project?.name || path.basename(rootDir),
|
|
74
|
+
rootDir,
|
|
75
|
+
outputDir,
|
|
76
|
+
rawDir,
|
|
77
|
+
configDir,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function resolveMaybeRelative(baseDir, targetPath) {
|
|
82
|
+
if (!targetPath) {
|
|
83
|
+
return path.resolve(baseDir);
|
|
84
|
+
}
|
|
85
|
+
if (path.isAbsolute(targetPath)) {
|
|
86
|
+
return targetPath;
|
|
87
|
+
}
|
|
88
|
+
return path.resolve(baseDir, targetPath);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeStringList(values) {
|
|
92
|
+
return (Array.isArray(values) ? values : [])
|
|
93
|
+
.map((value) => String(value || '').trim())
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function resolveSuitePackageName(suite) {
|
|
98
|
+
return String(suite?.package || suite?.project || 'default');
|
|
99
|
+
}
|
package/src/console.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const SECTION_WIDTH = 88;
|
|
2
|
+
|
|
3
|
+
export function createConsoleProgressReporter(options = {}) {
|
|
4
|
+
const stream = options.stream || process.stdout;
|
|
5
|
+
let hasPrintedHeader = false;
|
|
6
|
+
let packageCountWidth = 2;
|
|
7
|
+
let packageStarted = false;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
onEvent(event) {
|
|
11
|
+
if (!event || typeof event !== 'object') {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (event.type === 'run-start') {
|
|
16
|
+
packageCountWidth = Math.max(2, String(event.totalPackages || 0).length);
|
|
17
|
+
writeLine(stream, banner('Running Workspace Tests'));
|
|
18
|
+
hasPrintedHeader = true;
|
|
19
|
+
packageStarted = false;
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (event.type === 'package-start') {
|
|
24
|
+
if (!hasPrintedHeader) {
|
|
25
|
+
writeLine(stream, banner('Running Workspace Tests'));
|
|
26
|
+
hasPrintedHeader = true;
|
|
27
|
+
}
|
|
28
|
+
if (packageStarted) {
|
|
29
|
+
writeLine(stream, '');
|
|
30
|
+
} else {
|
|
31
|
+
writeLine(stream, '');
|
|
32
|
+
packageStarted = true;
|
|
33
|
+
}
|
|
34
|
+
const label = `${padNumber(event.packageIndex || 0, packageCountWidth)}/${padNumber(event.totalPackages || 0, packageCountWidth)} PACKAGE ${event.packageName || 'default'}`;
|
|
35
|
+
writeLine(stream, `${label}${event.packageLocation ? ` (${event.packageLocation})` : ''}`);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (event.type === 'suite-start') {
|
|
40
|
+
writeLine(stream, ` - ${event.suiteLabel || event.suiteId || 'Suite'}: running ${event.runtime || 'custom'}`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (event.type === 'suite-complete') {
|
|
45
|
+
const result = event.result || {};
|
|
46
|
+
writeLine(stream, ` ${formatStatus(result.status)} ${formatDuration(result.durationMs || 0)} ${formatSummaryInline(result.summary || zeroSummary())}`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (event.type === 'package-complete') {
|
|
51
|
+
writeLine(
|
|
52
|
+
stream,
|
|
53
|
+
`${formatStatus(event.status)} ${event.packageName || 'default'} in ${formatDuration(event.durationMs || 0)} (${formatSummaryInline(event.summary || zeroSummary())})`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function formatConsoleSummary(report, artifactPaths = {}, options = {}) {
|
|
61
|
+
const lines = [
|
|
62
|
+
banner('Workspace Test Report'),
|
|
63
|
+
`Packages: ${report?.summary?.totalPackages || 0}`,
|
|
64
|
+
`Suites: ${report?.summary?.totalSuites || 0}`,
|
|
65
|
+
`Tests: ${report?.summary?.totalTests || 0}`,
|
|
66
|
+
`Passed: ${report?.summary?.passedTests || 0}`,
|
|
67
|
+
`Failed: ${report?.summary?.failedTests || 0}`,
|
|
68
|
+
`Skipped: ${report?.summary?.skippedTests || 0}`,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const coverageLine = formatCoverageLine(report?.summary?.coverage);
|
|
72
|
+
if (coverageLine) {
|
|
73
|
+
lines.push(coverageLine);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
lines.push(`Duration: ${formatDuration(report?.durationMs || report?.summary?.durationMs || 0)}`);
|
|
77
|
+
if (options.htmlPath) {
|
|
78
|
+
lines.push(`HTML report: ${options.htmlPath}`);
|
|
79
|
+
} else if (artifactPaths.reportJsonPath) {
|
|
80
|
+
lines.push(`Report JSON: ${artifactPaths.reportJsonPath}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
lines.push('-'.repeat(SECTION_WIDTH));
|
|
84
|
+
|
|
85
|
+
const packages = Array.isArray(report?.packages) ? report.packages : [];
|
|
86
|
+
const packageNameWidth = Math.max(
|
|
87
|
+
20,
|
|
88
|
+
...packages.map((entry) => String(entry?.name || '').length + 2),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
for (const pkg of packages) {
|
|
92
|
+
const prefix = [
|
|
93
|
+
formatStatus(pkg.status).padEnd(5),
|
|
94
|
+
String(pkg.name || 'default').padEnd(packageNameWidth),
|
|
95
|
+
formatDuration(pkg.durationMs || 0),
|
|
96
|
+
formatSummaryInline(pkg.summary || zeroSummary()),
|
|
97
|
+
].join(' ');
|
|
98
|
+
|
|
99
|
+
const lineCoverage = pkg?.coverage?.lines?.pct;
|
|
100
|
+
lines.push(Number.isFinite(lineCoverage) ? `${prefix} L ${lineCoverage.toFixed(2)}%` : prefix);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
lines.push('='.repeat(SECTION_WIDTH));
|
|
104
|
+
return `${lines.join('\n')}\n`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function banner(title) {
|
|
108
|
+
return `${'='.repeat(SECTION_WIDTH)}\n${title}\n${'='.repeat(SECTION_WIDTH)}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatCoverageLine(coverage) {
|
|
112
|
+
const metrics = [
|
|
113
|
+
['lines', coverage?.lines?.pct],
|
|
114
|
+
['branches', coverage?.branches?.pct],
|
|
115
|
+
['functions', coverage?.functions?.pct],
|
|
116
|
+
['statements', coverage?.statements?.pct],
|
|
117
|
+
].filter(([, pct]) => Number.isFinite(pct));
|
|
118
|
+
|
|
119
|
+
if (metrics.length === 0) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return `Coverage: ${metrics.map(([label, pct]) => `${label} ${pct.toFixed(2)}%`).join(' | ')}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function formatSummaryInline(summary) {
|
|
127
|
+
return `tests ${summary.total || 0} | pass ${summary.passed || 0} | fail ${summary.failed || 0} | skip ${summary.skipped || 0}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function formatStatus(status) {
|
|
131
|
+
if (status === 'failed') return 'FAIL';
|
|
132
|
+
if (status === 'skipped') return 'SKIP';
|
|
133
|
+
return 'PASS';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function formatDuration(durationMs) {
|
|
137
|
+
const totalSeconds = Math.max(0, Math.round((durationMs || 0) / 1000));
|
|
138
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
139
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
140
|
+
const seconds = totalSeconds % 60;
|
|
141
|
+
if (hours > 0) {
|
|
142
|
+
return `${padNumber(hours, 2)}:${padNumber(minutes, 2)}:${padNumber(seconds, 2)}`;
|
|
143
|
+
}
|
|
144
|
+
return `${padNumber(minutes, 2)}:${padNumber(seconds, 2)}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function padNumber(value, width) {
|
|
148
|
+
return String(Math.max(0, Number(value) || 0)).padStart(width, '0');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function zeroSummary() {
|
|
152
|
+
return {
|
|
153
|
+
total: 0,
|
|
154
|
+
passed: 0,
|
|
155
|
+
failed: 0,
|
|
156
|
+
skipped: 0,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function writeLine(stream, value) {
|
|
161
|
+
stream.write(`${value}\n`);
|
|
162
|
+
}
|
package/src/coverage.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export function createCoverageMetric(covered, total) {
|
|
2
|
+
if (!Number.isFinite(total)) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const safeTotal = Math.max(0, total);
|
|
6
|
+
const safeCovered = Number.isFinite(covered) ? Math.max(0, Math.min(safeTotal, covered)) : 0;
|
|
7
|
+
const pct = safeTotal === 0 ? 100 : Number(((safeCovered / safeTotal) * 100).toFixed(2));
|
|
8
|
+
return {
|
|
9
|
+
covered: safeCovered,
|
|
10
|
+
total: safeTotal,
|
|
11
|
+
pct,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function normalizeCoverageSummary(coverage, packageName = null) {
|
|
16
|
+
if (!coverage) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const files = Array.isArray(coverage.files)
|
|
21
|
+
? coverage.files.map((file) => ({
|
|
22
|
+
path: file.path,
|
|
23
|
+
lines: file.lines || null,
|
|
24
|
+
statements: file.statements || null,
|
|
25
|
+
functions: file.functions || null,
|
|
26
|
+
branches: file.branches || null,
|
|
27
|
+
packageName: file.packageName || packageName || null,
|
|
28
|
+
module: file.module || null,
|
|
29
|
+
theme: file.theme || null,
|
|
30
|
+
shared: Boolean(file.shared),
|
|
31
|
+
attributionSource: file.attributionSource || null,
|
|
32
|
+
attributionReason: file.attributionReason || null,
|
|
33
|
+
attributionWeight: Number.isFinite(file.attributionWeight) ? file.attributionWeight : 1,
|
|
34
|
+
}))
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
lines: coverage.lines || aggregateCoverageMetric(files, 'lines'),
|
|
39
|
+
statements: coverage.statements || aggregateCoverageMetric(files, 'statements'),
|
|
40
|
+
functions: coverage.functions || aggregateCoverageMetric(files, 'functions'),
|
|
41
|
+
branches: coverage.branches || aggregateCoverageMetric(files, 'branches'),
|
|
42
|
+
files,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function aggregateCoverageMetric(files, metricKey) {
|
|
47
|
+
const valid = files
|
|
48
|
+
.map((file) => ({
|
|
49
|
+
metric: file?.[metricKey],
|
|
50
|
+
weight: Number.isFinite(file?.attributionWeight) ? file.attributionWeight : 1,
|
|
51
|
+
}))
|
|
52
|
+
.filter((entry) => entry.metric && Number.isFinite(entry.metric.total));
|
|
53
|
+
|
|
54
|
+
if (valid.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const total = valid.reduce((sum, entry) => sum + (entry.metric.total * entry.weight), 0);
|
|
59
|
+
const covered = valid.reduce((sum, entry) => sum + (entry.metric.covered * entry.weight), 0);
|
|
60
|
+
return createCoverageMetric(covered, total);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function mergeCoverageMetric(left, right) {
|
|
64
|
+
if (!left) return right || null;
|
|
65
|
+
if (!right) return left;
|
|
66
|
+
return createCoverageMetric(Math.max(left.covered, right.covered), Math.max(left.total, right.total));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function mergeCoverageSummaries(items) {
|
|
70
|
+
const summaries = (items || []).filter(Boolean);
|
|
71
|
+
if (summaries.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fileMap = new Map();
|
|
76
|
+
for (const coverage of summaries) {
|
|
77
|
+
for (const file of coverage.files || []) {
|
|
78
|
+
if (!fileMap.has(file.path)) {
|
|
79
|
+
fileMap.set(file.path, { ...file });
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const current = fileMap.get(file.path);
|
|
83
|
+
current.lines = mergeCoverageMetric(current.lines, file.lines);
|
|
84
|
+
current.statements = mergeCoverageMetric(current.statements, file.statements);
|
|
85
|
+
current.functions = mergeCoverageMetric(current.functions, file.functions);
|
|
86
|
+
current.branches = mergeCoverageMetric(current.branches, file.branches);
|
|
87
|
+
current.packageName = current.packageName || file.packageName || null;
|
|
88
|
+
current.module = current.module || file.module || null;
|
|
89
|
+
current.theme = current.theme || file.theme || null;
|
|
90
|
+
current.shared = current.shared || Boolean(file.shared);
|
|
91
|
+
current.attributionSource = current.attributionSource || file.attributionSource || null;
|
|
92
|
+
current.attributionReason = current.attributionReason || file.attributionReason || null;
|
|
93
|
+
current.attributionWeight = Number.isFinite(current.attributionWeight) ? current.attributionWeight : (Number.isFinite(file.attributionWeight) ? file.attributionWeight : 1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return normalizeCoverageSummary({ files: Array.from(fileMap.values()) });
|
|
98
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CONFIG_SCHEMA_VERSION, REPORT_SCHEMA_VERSION, defineConfig, loadConfig, applyConfigOverrides, summarizeConfig, resolveProjectContext, readJson } from './config.js';
|
|
2
|
+
export { createPhase1ScaffoldReport, createSummary, normalizeTestResult, normalizeSuiteResult, buildReportFromSuiteResults } from './report.js';
|
|
3
|
+
export { createCoverageMetric, normalizeCoverageSummary, mergeCoverageSummaries } from './coverage.js';
|
|
4
|
+
export { resolveAdapterForSuite } from './adapters.js';
|
|
5
|
+
export { preparePolicyContext, applyPolicyPipeline, collectCoverageAttribution, lookupOwner } from './policy.js';
|
|
6
|
+
export { writeReportArtifacts } from './artifacts.js';
|
|
7
|
+
export { formatConsoleSummary, createConsoleProgressReporter } from './console.js';
|
|
8
|
+
export { runReport } from './run-report.js';
|