monocart-coverage-reports 3.1.0 → 3.2.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/README.md +51 -0
- package/README.zh-Hans.md +51 -0
- package/lib/cli.js +25 -60
- package/lib/converter/converter.js +20 -24
- package/lib/default/options.js +5 -0
- package/lib/generate.js +3 -2
- package/lib/index.d.ts +67 -0
- package/lib/packages/monocart-coverage-converter.js +9 -9
- package/lib/packages/monocart-coverage-vendor.js +2 -1
- package/lib/packages/monocart-coverage-vendor.js.LICENSE +69 -9
- package/lib/test/test-reporter.js +191 -0
- package/lib/test/test-results.js +104 -0
- package/lib/test/test-runner.js +115 -0
- package/migrate-to-3.md +4 -0
- package/package.json +161 -161
- package/skills/monocart-coverage-reports/references/cli.md +5 -0
- package/skills/monocart-coverage-reports/references/reports-and-ci.md +2 -0
|
@@ -4,19 +4,79 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
|
|
|
4
4
|
|
|
5
5
|
The following inlined npm packages have license or notice files included below.
|
|
6
6
|
|
|
7
|
+
- @isaacs/nonrecursive-glob-match@1.0.2
|
|
7
8
|
- balanced-match@4.0.4 (https://github.com/juliangruber/balanced-match)
|
|
8
|
-
- brace-expansion@5.0.
|
|
9
|
+
- brace-expansion@5.0.12 (https://github.com/juliangruber/brace-expansion)
|
|
9
10
|
- diff-sequences@29.6.3 (https://github.com/jestjs/jest)
|
|
10
11
|
- find-up@8.0.0 (https://github.com/sindresorhus/find-up)
|
|
11
12
|
- locate-path@8.0.0 (https://github.com/sindresorhus/locate-path)
|
|
12
|
-
- minimatch@
|
|
13
|
+
- minimatch@11.0.2 (https://github.com/isaacs/minimatch)
|
|
13
14
|
- nanoid@3.3.20 (https://github.com/ai/nanoid)
|
|
14
15
|
- picocolors@1.1.1 (https://github.com/alexeyraspopov/picocolors)
|
|
15
|
-
- postcss@8.5.
|
|
16
|
+
- postcss@8.5.32 (https://github.com/postcss/postcss)
|
|
16
17
|
- source-map-js@1.2.2 (https://github.com/7rulnik/source-map-js)
|
|
17
18
|
- supports-color@11.0.0 (https://github.com/chalk/supports-color)
|
|
18
19
|
- unicorn-magic@0.3.0 (https://github.com/sindresorhus/unicorn-magic)
|
|
19
20
|
|
|
21
|
+
%% @isaacs/nonrecursive-glob-match@1.0.2 NOTICES AND INFORMATION BEGIN HERE
|
|
22
|
+
=========================================
|
|
23
|
+
# Blue Oak Model License
|
|
24
|
+
|
|
25
|
+
Version 1.0.0
|
|
26
|
+
|
|
27
|
+
## Purpose
|
|
28
|
+
|
|
29
|
+
This license gives everyone as much permission to work with
|
|
30
|
+
this software as possible, while protecting contributors
|
|
31
|
+
from liability.
|
|
32
|
+
|
|
33
|
+
## Acceptance
|
|
34
|
+
|
|
35
|
+
In order to receive this license, you must agree to its
|
|
36
|
+
rules. The rules of this license are both obligations
|
|
37
|
+
under that agreement and conditions to your license.
|
|
38
|
+
You must not do anything with this software that triggers
|
|
39
|
+
a rule that you cannot or will not follow.
|
|
40
|
+
|
|
41
|
+
## Copyright
|
|
42
|
+
|
|
43
|
+
Each contributor licenses you to do everything with this
|
|
44
|
+
software that would otherwise infringe that contributor's
|
|
45
|
+
copyright in it.
|
|
46
|
+
|
|
47
|
+
## Notices
|
|
48
|
+
|
|
49
|
+
You must ensure that everyone who gets a copy of
|
|
50
|
+
any part of this software from you, with or without
|
|
51
|
+
changes, also gets the text of this license or a link to
|
|
52
|
+
<https://blueoakcouncil.org/license/1.0.0>.
|
|
53
|
+
|
|
54
|
+
## Excuse
|
|
55
|
+
|
|
56
|
+
If anyone notifies you in writing that you have not
|
|
57
|
+
complied with [Notices](#notices), you can keep your
|
|
58
|
+
license by taking all practical steps to comply within 30
|
|
59
|
+
days after the notice. If you do not do so, your license
|
|
60
|
+
ends immediately.
|
|
61
|
+
|
|
62
|
+
## Patent
|
|
63
|
+
|
|
64
|
+
Each contributor licenses you to do everything with this
|
|
65
|
+
software that would otherwise infringe any patent claims
|
|
66
|
+
they can license or become able to license.
|
|
67
|
+
|
|
68
|
+
## Reliability
|
|
69
|
+
|
|
70
|
+
No contributor can revoke this license.
|
|
71
|
+
|
|
72
|
+
## No Liability
|
|
73
|
+
|
|
74
|
+
***As far as the law allows, this software comes as is,
|
|
75
|
+
without any warranty or condition, and no contributor
|
|
76
|
+
will be liable to anyone for any damages related to this
|
|
77
|
+
software or this license, under any kind of legal claim.***
|
|
78
|
+
=========================================
|
|
79
|
+
END OF @isaacs/nonrecursive-glob-match@1.0.2 NOTICES AND INFORMATION
|
|
20
80
|
%% balanced-match@4.0.4 NOTICES AND INFORMATION BEGIN HERE
|
|
21
81
|
=========================================
|
|
22
82
|
(MIT)
|
|
@@ -44,7 +104,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
44
104
|
SOFTWARE.
|
|
45
105
|
=========================================
|
|
46
106
|
END OF balanced-match@4.0.4 NOTICES AND INFORMATION
|
|
47
|
-
%% brace-expansion@5.0.
|
|
107
|
+
%% brace-expansion@5.0.12 NOTICES AND INFORMATION BEGIN HERE
|
|
48
108
|
=========================================
|
|
49
109
|
MIT License
|
|
50
110
|
|
|
@@ -70,7 +130,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
70
130
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
71
131
|
SOFTWARE.
|
|
72
132
|
=========================================
|
|
73
|
-
END OF brace-expansion@5.0.
|
|
133
|
+
END OF brace-expansion@5.0.12 NOTICES AND INFORMATION
|
|
74
134
|
%% diff-sequences@29.6.3 NOTICES AND INFORMATION BEGIN HERE
|
|
75
135
|
=========================================
|
|
76
136
|
MIT License
|
|
@@ -122,7 +182,7 @@ The above copyright notice and this permission notice shall be included in all c
|
|
|
122
182
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
123
183
|
=========================================
|
|
124
184
|
END OF locate-path@8.0.0 NOTICES AND INFORMATION
|
|
125
|
-
%% minimatch@
|
|
185
|
+
%% minimatch@11.0.2 NOTICES AND INFORMATION BEGIN HERE
|
|
126
186
|
=========================================
|
|
127
187
|
# Blue Oak Model License
|
|
128
188
|
|
|
@@ -180,7 +240,7 @@ without any warranty or condition, and no contributor
|
|
|
180
240
|
will be liable to anyone for any damages related to this
|
|
181
241
|
software or this license, under any kind of legal claim._**
|
|
182
242
|
=========================================
|
|
183
|
-
END OF minimatch@
|
|
243
|
+
END OF minimatch@11.0.2 NOTICES AND INFORMATION
|
|
184
244
|
%% nanoid@3.3.20 NOTICES AND INFORMATION BEGIN HERE
|
|
185
245
|
=========================================
|
|
186
246
|
The MIT License (MIT)
|
|
@@ -224,7 +284,7 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
|
224
284
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
225
285
|
=========================================
|
|
226
286
|
END OF picocolors@1.1.1 NOTICES AND INFORMATION
|
|
227
|
-
%% postcss@8.5.
|
|
287
|
+
%% postcss@8.5.32 NOTICES AND INFORMATION BEGIN HERE
|
|
228
288
|
=========================================
|
|
229
289
|
The MIT License (MIT)
|
|
230
290
|
|
|
@@ -247,7 +307,7 @@ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
|
247
307
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
248
308
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
249
309
|
=========================================
|
|
250
|
-
END OF postcss@8.5.
|
|
310
|
+
END OF postcss@8.5.32 NOTICES AND INFORMATION
|
|
251
311
|
%% source-map-js@1.2.2 NOTICES AND INFORMATION BEGIN HERE
|
|
252
312
|
=========================================
|
|
253
313
|
Copyright (c) 2009-2011, Mozilla Foundation and contributors
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Transform } from 'node:stream';
|
|
3
|
+
|
|
4
|
+
const reporterOptions = new URL(import.meta.url).searchParams;
|
|
5
|
+
|
|
6
|
+
const getStatus = (type, data) => {
|
|
7
|
+
// Node also accepts an empty string as a skip/todo reason.
|
|
8
|
+
if (data.skip === true || typeof data.skip === 'string') {
|
|
9
|
+
return 'skipped';
|
|
10
|
+
}
|
|
11
|
+
if (data.todo === true || typeof data.todo === 'string') {
|
|
12
|
+
return 'todo';
|
|
13
|
+
}
|
|
14
|
+
const failureType = data.details.error?.failureType;
|
|
15
|
+
if (['cancelledByParent', 'testTimeoutFailure', 'testAborted'].includes(failureType)) {
|
|
16
|
+
return 'cancelled';
|
|
17
|
+
}
|
|
18
|
+
return type === 'test:pass' ? 'passed' : 'failed';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const reservedErrorKeys = new Set(['name', 'message', '__proto__', 'constructor', 'prototype']);
|
|
22
|
+
|
|
23
|
+
const getErrorString = (error, key, fallback) => {
|
|
24
|
+
// Standard Error names are usually inherited. Resolve only these fixed
|
|
25
|
+
// fields through descriptors, without invoking accessors or coercion hooks.
|
|
26
|
+
for (let current = error; current; current = Object.getPrototypeOf(current)) {
|
|
27
|
+
const descriptor = Object.getOwnPropertyDescriptor(current, key);
|
|
28
|
+
if (descriptor) {
|
|
29
|
+
return typeof descriptor.value === 'string' ? descriptor.value : fallback;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return fallback;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const getErrorPrimitive = (value) => {
|
|
36
|
+
if (value === null) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
switch (typeof value) {
|
|
40
|
+
case 'string':
|
|
41
|
+
case 'boolean':
|
|
42
|
+
return value;
|
|
43
|
+
case 'number':
|
|
44
|
+
if (Number.isFinite(value)) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
case 'bigint':
|
|
49
|
+
return String(value);
|
|
50
|
+
default:
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const getError = (error) => {
|
|
56
|
+
if (!error) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const result = {
|
|
60
|
+
name: getErrorString(error, 'name', 'Error'),
|
|
61
|
+
message: getErrorString(error, 'message', '')
|
|
62
|
+
};
|
|
63
|
+
// Copy only own enumerable data properties, never getters or object graphs.
|
|
64
|
+
// Primitive fields may still contain secrets or large strings; redact them
|
|
65
|
+
// in onTestEnd before raw/debug export when appropriate.
|
|
66
|
+
for (const key of Object.keys(error)) {
|
|
67
|
+
if (reservedErrorKeys.has(key)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const descriptor = Object.getOwnPropertyDescriptor(error, key);
|
|
71
|
+
if (!descriptor?.enumerable || !Object.hasOwn(descriptor, 'value')) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const value = getErrorPrimitive(descriptor.value);
|
|
75
|
+
if (typeof value !== 'undefined') {
|
|
76
|
+
result[key] = value;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const getTestResult = (type, data) => ({
|
|
83
|
+
name: data.name,
|
|
84
|
+
type: data.details.type || 'test',
|
|
85
|
+
nesting: data.nesting,
|
|
86
|
+
testNumber: data.testNumber,
|
|
87
|
+
file: data.file,
|
|
88
|
+
line: data.line,
|
|
89
|
+
column: data.column,
|
|
90
|
+
duration: data.details.duration_ms,
|
|
91
|
+
status: getStatus(type, data),
|
|
92
|
+
skip: data.skip,
|
|
93
|
+
todo: data.todo,
|
|
94
|
+
error: getError(data.details.error)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const getSummary = (data) => ({
|
|
98
|
+
tests: data.counts.tests,
|
|
99
|
+
suites: data.counts.suites,
|
|
100
|
+
passed: data.counts.passed,
|
|
101
|
+
failed: data.counts.failed,
|
|
102
|
+
cancelled: data.counts.cancelled,
|
|
103
|
+
skipped: data.counts.skipped,
|
|
104
|
+
todo: data.counts.todo,
|
|
105
|
+
duration: data.duration_ms,
|
|
106
|
+
success: data.success
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Node instantiates this stream without arguments. Per-collector options are
|
|
110
|
+
// passed through its module URL, not process.env, so collectors stay isolated.
|
|
111
|
+
export default class TestReporter extends Transform {
|
|
112
|
+
constructor(id = reporterOptions.get('id'), metadataPath = reporterOptions.get('meta')) {
|
|
113
|
+
super({
|
|
114
|
+
writableObjectMode: true
|
|
115
|
+
});
|
|
116
|
+
this.id = id;
|
|
117
|
+
this.metadataPath = metadataPath;
|
|
118
|
+
this.complete = false;
|
|
119
|
+
this.summary = null;
|
|
120
|
+
this.recordCount = 0;
|
|
121
|
+
this.pending = [];
|
|
122
|
+
this.pendingBytes = 0;
|
|
123
|
+
const header = `${JSON.stringify({
|
|
124
|
+
id, framework: 'node:test'
|
|
125
|
+
}).slice(0, -1)},"tests":[`;
|
|
126
|
+
this.byteLength = Buffer.byteLength(header);
|
|
127
|
+
this.push(header);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_transform(event, _encoding, callback) {
|
|
131
|
+
let chunk;
|
|
132
|
+
try {
|
|
133
|
+
const { type, data } = event;
|
|
134
|
+
if (type === 'test:summary' && !data.file) {
|
|
135
|
+
this.complete = true;
|
|
136
|
+
this.summary = getSummary(data);
|
|
137
|
+
} else if (type === 'test:pass' || type === 'test:fail') {
|
|
138
|
+
chunk = this.addRecord(getTestResult(type, data));
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
callback(error);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
callback(null, chunk);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
addRecord(data) {
|
|
148
|
+
const record = `${this.recordCount ? ',' : ''}${JSON.stringify(data)}`;
|
|
149
|
+
const byteLength = Buffer.byteLength(record);
|
|
150
|
+
this.recordCount += 1;
|
|
151
|
+
this.byteLength += byteLength;
|
|
152
|
+
this.pending.push(record);
|
|
153
|
+
this.pendingBytes += byteLength;
|
|
154
|
+
// Bound memory while avoiding a separate filesystem write for every
|
|
155
|
+
// small record. Emit the first record without delay.
|
|
156
|
+
if (this.recordCount === 1 || this.pendingBytes >= 64 * 1024) {
|
|
157
|
+
return this.takePending();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
takePending() {
|
|
162
|
+
const chunk = this.pending.join('');
|
|
163
|
+
this.pending = [];
|
|
164
|
+
this.pendingBytes = 0;
|
|
165
|
+
return chunk;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
_flush(callback) {
|
|
169
|
+
let chunk;
|
|
170
|
+
try {
|
|
171
|
+
const { complete, summary } = this;
|
|
172
|
+
chunk = `],${JSON.stringify({
|
|
173
|
+
complete, summary
|
|
174
|
+
}).slice(1)}`;
|
|
175
|
+
this.byteLength += Buffer.byteLength(chunk);
|
|
176
|
+
chunk = this.takePending() + chunk;
|
|
177
|
+
if (this.metadataPath) {
|
|
178
|
+
// The parent checks this small completion record and the final
|
|
179
|
+
// byte count after child exit; no full JSON read is needed
|
|
180
|
+
// unless onTestEnd requests the complete result object.
|
|
181
|
+
fs.writeFileSync(this.metadataPath, JSON.stringify({
|
|
182
|
+
id: this.id, complete, byteLength: this.byteLength
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
callback(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
callback(null, chunk);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { Util } from '../utils/util.js';
|
|
3
|
+
|
|
4
|
+
// Only tokenize NODE_OPTIONS to count option names, keeping quoted values
|
|
5
|
+
// together. The original options and values are passed to Node untouched.
|
|
6
|
+
const splitNodeOptions = (value) => {
|
|
7
|
+
const tokens = value.match(/(?:[^\s"]|"(?:\\.|[^"\\])*")+/g) || [];
|
|
8
|
+
return tokens.map((token) => token.replaceAll('"', ''));
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const getRunnerOptions = (args) => {
|
|
12
|
+
const index = args.indexOf('--');
|
|
13
|
+
return index === -1 ? args : args.slice(0, index);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Node treats underscores and hyphens as equivalent in option names. Inspect
|
|
17
|
+
// only names here; the original arguments and their values remain untouched.
|
|
18
|
+
const countOption = (args, name) => args.filter((arg) => arg.split('=', 1)[0].replaceAll('_', '-') === name).length;
|
|
19
|
+
|
|
20
|
+
const getReporterArgs = (args) => {
|
|
21
|
+
const reporters = countOption(args, '--test-reporter');
|
|
22
|
+
const destinations = countOption(args, '--test-reporter-destination');
|
|
23
|
+
// Preserve Node's validation instead of accidentally fixing an invalid pair.
|
|
24
|
+
if ((reporters > 1 || destinations > 0) && reporters !== destinations) {
|
|
25
|
+
throw new Error('The number of --test-reporter options must match --test-reporter-destination options');
|
|
26
|
+
}
|
|
27
|
+
const extraArgs = [];
|
|
28
|
+
if (!reporters) {
|
|
29
|
+
// Node 22 defaults to TAP when stdout is not a TTY; Node 23+ uses spec.
|
|
30
|
+
const reporter = Number(process.versions.node.split('.')[0]) < 23 && !process.stdout.isTTY ? 'tap' : 'spec';
|
|
31
|
+
extraArgs.push(`--test-reporter=${reporter}`);
|
|
32
|
+
}
|
|
33
|
+
if (!destinations) {
|
|
34
|
+
extraArgs.push('--test-reporter-destination=stdout');
|
|
35
|
+
}
|
|
36
|
+
return extraArgs;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const readMetadata = (metadataPath, resultsPath, id) => {
|
|
40
|
+
// This is an internal completion record, not an external report input.
|
|
41
|
+
// Keep the no-hook path bounded even if an interrupted file is malformed.
|
|
42
|
+
if (fs.statSync(metadataPath).size > 1024) {
|
|
43
|
+
throw new Error('Invalid Node test report metadata');
|
|
44
|
+
}
|
|
45
|
+
const metadata = JSON.parse(fs.readFileSync(metadataPath, 'utf8'));
|
|
46
|
+
if (metadata?.id !== id || typeof metadata.complete !== 'boolean' || !Number.isSafeInteger(metadata.byteLength) || metadata.byteLength <= 0) {
|
|
47
|
+
throw new Error('Invalid Node test report metadata');
|
|
48
|
+
}
|
|
49
|
+
if (metadata.byteLength !== fs.statSync(resultsPath).size) {
|
|
50
|
+
throw new Error('Incomplete Node test report output');
|
|
51
|
+
}
|
|
52
|
+
return metadata;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const validateResults = (results, id) => {
|
|
56
|
+
if (results?.id !== id || results.framework !== 'node:test' || typeof results.complete !== 'boolean' || !Array.isArray(results.tests)) {
|
|
57
|
+
throw new Error('Invalid Node test report');
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const createTestResultsCollector = (runnerArgs, cacheDir) => {
|
|
62
|
+
const args = [... splitNodeOptions(process.env.NODE_OPTIONS || ''), ... getRunnerOptions(runnerArgs)];
|
|
63
|
+
const extraArgs = getReporterArgs(args);
|
|
64
|
+
fs.mkdirSync(cacheDir, {
|
|
65
|
+
recursive: true
|
|
66
|
+
});
|
|
67
|
+
const id = Util.uid();
|
|
68
|
+
const { cachePath } = Util.getCacheFileInfo('test-results', id, cacheDir);
|
|
69
|
+
const resultsPath = `${cachePath}.tmp`;
|
|
70
|
+
const metadataPath = `${cachePath}.meta.tmp`;
|
|
71
|
+
const reporterURL = new URL('./test-reporter.js', import.meta.url);
|
|
72
|
+
reporterURL.searchParams.set('id', id);
|
|
73
|
+
reporterURL.searchParams.set('meta', metadataPath);
|
|
74
|
+
extraArgs.push(`--test-reporter=${reporterURL.href}`, `--test-reporter-destination=${resultsPath}`);
|
|
75
|
+
return {
|
|
76
|
+
args: extraArgs,
|
|
77
|
+
readMetadata: () => readMetadata(metadataPath, resultsPath, id),
|
|
78
|
+
read: () => {
|
|
79
|
+
const results = JSON.parse(fs.readFileSync(resultsPath, 'utf8'));
|
|
80
|
+
validateResults(results, id);
|
|
81
|
+
return results;
|
|
82
|
+
},
|
|
83
|
+
publish: (results) => {
|
|
84
|
+
readMetadata(metadataPath, resultsPath, id);
|
|
85
|
+
if (results) {
|
|
86
|
+
validateResults(results, id);
|
|
87
|
+
// onTestEnd can redact messages/names before raw/debug export.
|
|
88
|
+
// Only configured hooks require a full read and rewrite.
|
|
89
|
+
fs.writeFileSync(resultsPath, JSON.stringify(results));
|
|
90
|
+
}
|
|
91
|
+
fs.renameSync(resultsPath, cachePath);
|
|
92
|
+
},
|
|
93
|
+
dispose: () => {
|
|
94
|
+
fs.rmSync(resultsPath, {
|
|
95
|
+
force: true
|
|
96
|
+
});
|
|
97
|
+
fs.rmSync(metadataPath, {
|
|
98
|
+
force: true
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
export { createTestResultsCollector, getRunnerOptions };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { Util } from '../utils/util.js';
|
|
3
|
+
import { createTestResultsCollector, getRunnerOptions } from './test-results.js';
|
|
4
|
+
|
|
5
|
+
// Also recognize `mcr -c config -- test targets -- runner-options` without
|
|
6
|
+
// falling through to the shell's unrelated `test` command. Only MCR options
|
|
7
|
+
// before the first separator are parsed in that form.
|
|
8
|
+
const getTestCommand = (args, subArgs) => {
|
|
9
|
+
if (args[0] === 'test') {
|
|
10
|
+
return {
|
|
11
|
+
targets: args.slice(1), runnerArgs: subArgs.slice(1)
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (args.length || subArgs[1] !== 'test') {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const remaining = subArgs.slice(2);
|
|
18
|
+
const index = remaining.indexOf('--');
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
return {
|
|
21
|
+
targets: remaining, runnerArgs: []
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
targets: remaining.slice(0, index), runnerArgs: remaining.slice(index + 1)
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Node owns target parsing, discovery and validation. Preserve each argument.
|
|
30
|
+
const getTestTargets = (test) => {
|
|
31
|
+
if (!test || (Array.isArray(test) && !test.length)) {
|
|
32
|
+
Util.logInfo('No test targets configured; skipping tests.');
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
return Array.isArray(test) ? test : [test];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const initTestResultsCollector = (coverageOptions, runnerArgs, childArgs) => {
|
|
39
|
+
// Read results before coverage generation, which may remove the cache.
|
|
40
|
+
const testResultsCollector = createTestResultsCollector(runnerArgs, coverageOptions.cacheDir);
|
|
41
|
+
// Internal options must stay before Node's end-of-options marker.
|
|
42
|
+
childArgs.splice(1 + getRunnerOptions(runnerArgs).length, 0, ... testResultsCollector.args);
|
|
43
|
+
return testResultsCollector;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const runTestEnd = async (testResultsCollector, onTestEnd) => {
|
|
47
|
+
if (!testResultsCollector) {
|
|
48
|
+
return 0;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const metadata = testResultsCollector.readMetadata();
|
|
52
|
+
const failed = !metadata.complete;
|
|
53
|
+
if (failed) {
|
|
54
|
+
Util.logError('Node test report is incomplete');
|
|
55
|
+
}
|
|
56
|
+
let testResults;
|
|
57
|
+
if (typeof onTestEnd === 'function') {
|
|
58
|
+
testResults = testResultsCollector.read();
|
|
59
|
+
await onTestEnd(testResults);
|
|
60
|
+
}
|
|
61
|
+
// Publish only after a successful hook, so redaction reaches raw/debug
|
|
62
|
+
// artifacts. Never fall back to unredacted data if the hook throws.
|
|
63
|
+
testResultsCollector.publish(testResults);
|
|
64
|
+
return process.exitCode || (failed ? 1 : 0);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
Util.logError(`Node test report collection or onTestEnd failed: ${message}`);
|
|
68
|
+
return 1;
|
|
69
|
+
} finally {
|
|
70
|
+
// Do not export unfinished reporter output through coverage raw reports.
|
|
71
|
+
testResultsCollector.dispose();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const createTestRunner = (coverageOptions, runnerArgs = []) => {
|
|
76
|
+
const targets = getTestTargets(coverageOptions.test);
|
|
77
|
+
if (!targets) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const childArgs = ['--test', ... runnerArgs, ... targets];
|
|
81
|
+
let testResultsCollector;
|
|
82
|
+
let onTestEnd;
|
|
83
|
+
return {
|
|
84
|
+
initCoverageDir: (nodeV8CoverageDir) => {
|
|
85
|
+
// Tests may spawn children with a different working directory.
|
|
86
|
+
process.env.NODE_V8_COVERAGE = path.resolve(nodeV8CoverageDir);
|
|
87
|
+
},
|
|
88
|
+
onClose: async (generateCoverage) => {
|
|
89
|
+
const testExitCode = await runTestEnd(testResultsCollector, onTestEnd);
|
|
90
|
+
try {
|
|
91
|
+
const report = await generateCoverage();
|
|
92
|
+
if (!report) {
|
|
93
|
+
throw new Error('No coverage report was generated');
|
|
94
|
+
}
|
|
95
|
+
} finally {
|
|
96
|
+
// A later coverage hook must not mask a test hook/collection failure.
|
|
97
|
+
if (testExitCode) {
|
|
98
|
+
process.exitCode = process.exitCode || testExitCode;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
execute: async (executeSubprocess, onClose) => {
|
|
103
|
+
// Always collect results; onStart may still configure the optional hook.
|
|
104
|
+
onTestEnd = coverageOptions.onTestEnd;
|
|
105
|
+
testResultsCollector = initTestResultsCollector(coverageOptions, runnerArgs, childArgs);
|
|
106
|
+
try {
|
|
107
|
+
return await executeSubprocess(process.execPath, childArgs, onClose);
|
|
108
|
+
} finally {
|
|
109
|
+
testResultsCollector?.dispose();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export { getTestCommand, createTestRunner };
|
package/migrate-to-3.md
CHANGED
|
@@ -158,6 +158,10 @@ Custom reporter interfaces are unchanged: Istanbul reporters implement `execute(
|
|
|
158
158
|
- `mcr test` is a new **optional** command; you do not have to replace your existing test runner.
|
|
159
159
|
- `outputFilter` is a new **optional** filter, disabled by default. You do not have to replace `entryFilter` / `sourceFilter` with it.
|
|
160
160
|
|
|
161
|
+
### Review test-result artifacts when upgrading to 3.2.0+
|
|
162
|
+
|
|
163
|
+
Starting in 3.2.0, `mcr test` always collects structured test results, even without `onTestEnd`. Raw exports (including ZIP) and debug caches can now include `test-results-<id>.json`, containing test names, paths and error strings that may be sensitive. Review existing automatic artifact uploads and retention policies. Use [`onTestEnd`](README.md#ontestend--mcr-test-only) to redact persisted results before publication; terminal output and temporary files already written are not redacted. Raw imports still merge coverage only, not test results. Other commands and API calls do not collect these test results.
|
|
164
|
+
|
|
161
165
|
## Migration checklist
|
|
162
166
|
|
|
163
167
|
- [ ] Upgrade Node.js in local, CI, container and relevant child-process environments.
|