@systemfsoftware/stryker-js 4.0.0 → 5.0.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/CHANGELOG.md +73 -0
- package/LICENSE +3 -15
- package/README.md +67 -13
- package/dist/Plugins-sA_9VKus.d.mts +96 -0
- package/dist/builtin-reporters-wAuKFSsd.mjs +672 -0
- package/dist/builtin-reporters.d.mts +14 -0
- package/dist/builtin-reporters.mjs +2 -0
- package/dist/compiler-Bq9Nelu8.mjs +48406 -0
- package/dist/config.d.mts +11 -0
- package/dist/config.mjs +7 -0
- package/dist/dist-B3nT7z4G.mjs +5155 -0
- package/dist/execAsync-CfPZgJSD.mjs +10 -0
- package/dist/execAsync-DdCu5QLN.mjs +10 -0
- package/dist/getMachineId-bsd-C6bLIaiv.mjs +27 -0
- package/dist/getMachineId-bsd-CXSiHDga.mjs +27 -0
- package/dist/getMachineId-darwin-D3cIKeqx.mjs +24 -0
- package/dist/getMachineId-darwin-DPZ0kdKD.mjs +24 -0
- package/dist/getMachineId-linux-CQwLSlZk.mjs +20 -0
- package/dist/getMachineId-linux-Ci8OVJU0.mjs +20 -0
- package/dist/getMachineId-unsupported-PX0hnW6X.mjs +15 -0
- package/dist/getMachineId-unsupported-_ExL3Avl.mjs +15 -0
- package/dist/getMachineId-win-BCsS9pC_.mjs +26 -0
- package/dist/getMachineId-win-D9Tb0p7w.mjs +26 -0
- package/dist/index.d.mts +659 -1253
- package/dist/index.mjs +3 -1034
- package/dist/main.mjs +104333 -0
- package/dist/merge-config-BrrNQWje.mjs +16 -0
- package/dist/mutation-test.cell-RmF-sBUm.d.mts +509 -0
- package/dist/output-mode-CfGHDpi5.d.mts +17 -0
- package/dist/parser.wasm32-wasi.wasm +0 -0
- package/dist/plugin-loader.d.mts +106 -0
- package/dist/plugin-loader.mjs +2 -0
- package/dist/plugin-worker-entry-DNu0SHO8.mjs +1413 -0
- package/dist/promises.d.mts +5 -0
- package/dist/promises.mjs +14 -0
- package/dist/rolldown-runtime-Dr7-SnC6.mjs +45 -0
- package/dist/src-Bzh7ykDp.mjs +4724 -0
- package/dist/stryker-config-BlfHxS7n.d.mts +14 -0
- package/dist/wasm-Dr7kZatW.mjs +13017 -0
- package/package.json +80 -40
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as FileSystem from "effect/FileSystem";
|
|
3
|
+
import * as Path from "effect/Path";
|
|
4
|
+
import "effect/Stdio";
|
|
5
|
+
import { MetricsResultSchema, MutationTestResultSchema, ReporterFailed } from "@systemfsoftware/stryker-js-plugin-interface";
|
|
6
|
+
import * as Match from "effect/Match";
|
|
7
|
+
import * as Option from "effect/Option";
|
|
8
|
+
import * as S from "effect/Schema";
|
|
9
|
+
import * as Stream from "effect/Stream";
|
|
10
|
+
import { errorToString } from "@systemfsoftware/stryker-js-instrumenter/mutants";
|
|
11
|
+
import * as Predicate from "effect/Predicate";
|
|
12
|
+
import "effect/Sink";
|
|
13
|
+
//#region src/reporter-output.ts
|
|
14
|
+
const sinksOf = (stdio) => ({
|
|
15
|
+
stdout: () => stdio.stdout({ endOnDone: false }),
|
|
16
|
+
stderr: () => stdio.stderr({ endOnDone: false })
|
|
17
|
+
});
|
|
18
|
+
const sinkFor = (stdio, channel) => sinksOf(stdio)[channel]();
|
|
19
|
+
const write = (stdio, channel, chunks) => Stream.run(Stream.fromIterable(chunks), sinkFor(stdio, channel));
|
|
20
|
+
/**
|
|
21
|
+
* The one Effect→Promise bridge for the sync-loop progress bar (the only reporter
|
|
22
|
+
* that cannot compose Effects — its writes fire inside a synchronous for-await).
|
|
23
|
+
* Write errors are ignored like a broken pipe on a stream handle.
|
|
24
|
+
*
|
|
25
|
+
* Note: effect 4.0.0-rc.112 exports the runner as `Effect.runPromise`; there is no
|
|
26
|
+
* `Runtime.runPromise` in this version (verified against the installed source) —
|
|
27
|
+
* do not invent that import.
|
|
28
|
+
*/
|
|
29
|
+
const writeAsync = (stdio, channel, chunks) => Effect.runPromise(Effect.ignore(write(stdio, channel, chunks)));
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/Reporter.ansi.ts
|
|
32
|
+
const codes = {
|
|
33
|
+
red: "\x1B[31m",
|
|
34
|
+
green: "\x1B[32m",
|
|
35
|
+
yellow: "\x1B[33m",
|
|
36
|
+
grey: "\x1B[90m",
|
|
37
|
+
cyan: "\x1B[36m",
|
|
38
|
+
greenBright: "\x1B[92m",
|
|
39
|
+
redBright: "\x1B[91m",
|
|
40
|
+
blueBright: "\x1B[94m"
|
|
41
|
+
};
|
|
42
|
+
const reset = "\x1B[39m";
|
|
43
|
+
function wrap(color, text) {
|
|
44
|
+
return `${codes[color]}${text}${reset}`;
|
|
45
|
+
}
|
|
46
|
+
const ansi = {
|
|
47
|
+
wrap,
|
|
48
|
+
red: (text) => wrap("red", text),
|
|
49
|
+
green: (text) => wrap("green", text),
|
|
50
|
+
yellow: (text) => wrap("yellow", text),
|
|
51
|
+
grey: (text) => wrap("grey", text),
|
|
52
|
+
cyan: (text) => wrap("cyan", text),
|
|
53
|
+
greenBright: (text) => wrap("greenBright", text),
|
|
54
|
+
redBright: (text) => wrap("redBright", text),
|
|
55
|
+
blueBright: (text) => wrap("blueBright", text)
|
|
56
|
+
};
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/mutation-score-table.ts
|
|
59
|
+
const KNOWN_EMOJI = {
|
|
60
|
+
"✅": true,
|
|
61
|
+
"🙈": true,
|
|
62
|
+
"🤥": true,
|
|
63
|
+
"👽": true,
|
|
64
|
+
"⏰": true,
|
|
65
|
+
"⌛": true,
|
|
66
|
+
"💥": true
|
|
67
|
+
};
|
|
68
|
+
const charWidth = (char) => Match.value(char).pipe(Match.when((candidate) => KNOWN_EMOJI[candidate] === true, () => 2), Match.when((candidate) => (candidate.codePointAt(0) ?? 0) > 65535, () => 2), Match.orElse(() => 1));
|
|
69
|
+
function stringWidth(input) {
|
|
70
|
+
return Array.from(input).reduce((width, char) => width + charWidth(char), 0);
|
|
71
|
+
}
|
|
72
|
+
const FILES_ROOT_NAME = "All files";
|
|
73
|
+
const repeat = (char, nTimes) => {
|
|
74
|
+
if (nTimes > -1) return char.repeat(nTimes);
|
|
75
|
+
return char.repeat(0);
|
|
76
|
+
};
|
|
77
|
+
const spaces = (n) => repeat(" ", n);
|
|
78
|
+
const statusHeader = (allowEmojis, emoji, label) => {
|
|
79
|
+
if (allowEmojis) return `${emoji} ${label}`;
|
|
80
|
+
return `# ${label}`;
|
|
81
|
+
};
|
|
82
|
+
const maxOf = (values) => values.reduce((acc, cur) => {
|
|
83
|
+
if (cur > acc) return cur;
|
|
84
|
+
return acc;
|
|
85
|
+
}, Number.NEGATIVE_INFINITY);
|
|
86
|
+
const widthOrZero = (width) => Match.value(width === Number.NEGATIVE_INFINITY).pipe(Match.when(true, () => 0), Match.when(false, () => width), Match.exhaustive);
|
|
87
|
+
const determineContentWidth = (row, valueFactory, ancestorCount = 0) => {
|
|
88
|
+
const head = valueFactory(row, ancestorCount).length;
|
|
89
|
+
const childWidths = row.childResults.map((child) => determineContentWidth(child, valueFactory, ancestorCount + 1));
|
|
90
|
+
return widthOrZero(maxOf([head, ...childWidths]));
|
|
91
|
+
};
|
|
92
|
+
const columnWidth = (column) => {
|
|
93
|
+
if (column.isFirstColumn) return column.netWidth + 1;
|
|
94
|
+
return column.netWidth + 2;
|
|
95
|
+
};
|
|
96
|
+
const padColumn = (column, input) => Match.value(column.kind === "file").pipe(Match.when(true, () => `${input}${spaces(columnWidth(column) - stringWidth(input))}`), Match.when(false, () => Match.value(column.isFirstColumn).pipe(Match.when(true, () => `${spaces(column.netWidth - stringWidth(input))}${input} `), Match.when(false, () => `${spaces(column.netWidth - stringWidth(input))} ${input} `), Match.exhaustive)), Match.exhaustive);
|
|
97
|
+
const drawLine = (column) => repeat("-", columnWidth(column));
|
|
98
|
+
const drawHeader = (column) => padColumn(column, column.header);
|
|
99
|
+
const mutationScoreOf = (scoreType, metrics) => Match.value(scoreType).pipe(Match.when("total", () => metrics.mutationScore), Match.when("covered", () => metrics.mutationScoreBasedOnCoveredCode), Match.exhaustive);
|
|
100
|
+
const thresholdColor = (thresholds, value) => Match.value(value).pipe(Match.when((present) => Number.isNaN(present), () => ansi.grey), Match.when((present) => present >= thresholds.high, () => ansi.green), Match.when((present) => present >= thresholds.low, () => ansi.yellow), Match.orElse(() => ansi.red));
|
|
101
|
+
const scoreColor = (column, score) => Match.value(column.allowColor).pipe(Match.when(true, () => thresholdColor(column.thresholds, mutationScoreOf(column.scoreType, score.metrics))), Match.when(false, () => (input) => input), Match.exhaustive);
|
|
102
|
+
const colorFor = (column, score) => Match.value(column).pipe(Match.discriminator("kind")("mutationScore", (scored) => scoreColor(scored, score)), Match.orElse(() => (input) => input));
|
|
103
|
+
const drawTableCell = (column, score, ancestorCount) => {
|
|
104
|
+
switch (column.kind) {
|
|
105
|
+
case "group": return column.columns.map((c) => drawTableCell(c, score, ancestorCount)).join("|");
|
|
106
|
+
case "single":
|
|
107
|
+
case "file":
|
|
108
|
+
case "mutationScore": {
|
|
109
|
+
const raw = column.valueFactory(score, ancestorCount);
|
|
110
|
+
const padded = padColumn(column, raw);
|
|
111
|
+
return colorFor(column, score)(padded);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const drawColumnHeaders = (column) => {
|
|
116
|
+
if (column.kind !== "group") return drawHeader(column);
|
|
117
|
+
return column.columns.map((c) => drawHeader(c)).join("|");
|
|
118
|
+
};
|
|
119
|
+
const drawColumnLines = (column) => {
|
|
120
|
+
if (column.kind !== "group") return drawLine(column);
|
|
121
|
+
return column.columns.map((c) => drawLine(c)).join("|");
|
|
122
|
+
};
|
|
123
|
+
const makeSingleColumn = (header, isFirstColumn, valueFactory, rows) => {
|
|
124
|
+
const maxContentSize = determineContentWidth(rows, valueFactory);
|
|
125
|
+
const netWidth = maxOf([maxContentSize, stringWidth(header)]);
|
|
126
|
+
return {
|
|
127
|
+
kind: "single",
|
|
128
|
+
header,
|
|
129
|
+
isFirstColumn,
|
|
130
|
+
netWidth: widthOrZero(netWidth),
|
|
131
|
+
valueFactory,
|
|
132
|
+
rows
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
const makeFileColumn = (rows) => {
|
|
136
|
+
const valueFactory = (row, ancestorCount) => {
|
|
137
|
+
if (ancestorCount === 0) return spaces(ancestorCount) + FILES_ROOT_NAME;
|
|
138
|
+
return spaces(ancestorCount) + row.name;
|
|
139
|
+
};
|
|
140
|
+
const netWidth = maxOf([determineContentWidth(rows, valueFactory), stringWidth("File")]);
|
|
141
|
+
return {
|
|
142
|
+
kind: "file",
|
|
143
|
+
header: "File",
|
|
144
|
+
isFirstColumn: true,
|
|
145
|
+
netWidth: widthOrZero(netWidth),
|
|
146
|
+
valueFactory,
|
|
147
|
+
rows
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const makeMutationScoreColumn = (rows, thresholds, scoreType, allowColor) => {
|
|
151
|
+
const valueFactory = (row) => Match.value(mutationScoreOf(scoreType, row.metrics)).pipe(Match.when((present) => Number.isNaN(present), () => "n/a"), Match.orElse((present) => present.toFixed(2)));
|
|
152
|
+
const netWidth = maxOf([determineContentWidth(rows, valueFactory), stringWidth(scoreType)]);
|
|
153
|
+
return {
|
|
154
|
+
kind: "mutationScore",
|
|
155
|
+
header: scoreType,
|
|
156
|
+
isFirstColumn: false,
|
|
157
|
+
netWidth: widthOrZero(netWidth),
|
|
158
|
+
valueFactory,
|
|
159
|
+
rows,
|
|
160
|
+
thresholds,
|
|
161
|
+
scoreType,
|
|
162
|
+
allowColor
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
const paddingWidth = (isFirstColumn) => Match.value(isFirstColumn).pipe(Match.when(true, () => 1), Match.when(false, () => 2), Match.exhaustive);
|
|
166
|
+
const widthAdjustedColumns = (columns, first, columnsWidth, netWidth) => Match.value(netWidth > columnsWidth + 1).pipe(Match.when(true, () => [{
|
|
167
|
+
...first,
|
|
168
|
+
netWidth: first.netWidth + (netWidth - columnsWidth - 1)
|
|
169
|
+
}, ...columns.slice(1)]), Match.when(false, () => columns), Match.exhaustive);
|
|
170
|
+
const makeGroupColumn = (groupName, ...columns) => {
|
|
171
|
+
const [first, ...rest] = columns;
|
|
172
|
+
if (first === void 0) throw new Error("a group column needs at least one column");
|
|
173
|
+
const columnsWidth = rest.reduce((acc, cur) => acc + columnWidth(cur), columnWidth(first)) - paddingWidth(first.isFirstColumn);
|
|
174
|
+
const netWidth = widthOrZero(maxOf([stringWidth(groupName), columnsWidth]));
|
|
175
|
+
return {
|
|
176
|
+
kind: "group",
|
|
177
|
+
header: groupName,
|
|
178
|
+
isFirstColumn: first.isFirstColumn,
|
|
179
|
+
netWidth,
|
|
180
|
+
columns: widthAdjustedColumns(columns, first, columnsWidth, netWidth)
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const createColumns = (metricsResult, options) => {
|
|
184
|
+
const allowColor = options.clearTextReporter.allowColor;
|
|
185
|
+
const allowEmojis = options.clearTextReporter.allowEmojis;
|
|
186
|
+
return [
|
|
187
|
+
makeGroupColumn("", makeFileColumn(metricsResult)),
|
|
188
|
+
makeGroupColumn("% Mutation score", makeMutationScoreColumn(metricsResult, options.thresholds, "total", allowColor), makeMutationScoreColumn(metricsResult, options.thresholds, "covered", allowColor)),
|
|
189
|
+
makeGroupColumn("", makeSingleColumn(statusHeader(allowEmojis, "✅", "killed"), false, (row) => row.metrics.killed.toString(), metricsResult)),
|
|
190
|
+
makeGroupColumn("", makeSingleColumn(statusHeader(allowEmojis, "⌛️", "timeout"), false, (row) => row.metrics.timeout.toString(), metricsResult)),
|
|
191
|
+
makeGroupColumn("", makeSingleColumn(statusHeader(allowEmojis, "👽", "survived"), false, (row) => row.metrics.survived.toString(), metricsResult)),
|
|
192
|
+
makeGroupColumn("", makeSingleColumn(statusHeader(allowEmojis, "🙈", "no cov"), false, (row) => row.metrics.noCoverage.toString(), metricsResult)),
|
|
193
|
+
makeGroupColumn("", makeSingleColumn(statusHeader(allowEmojis, "💥", "errors"), false, (row) => (row.metrics.runtimeErrors + row.metrics.compileErrors).toString(), metricsResult))
|
|
194
|
+
];
|
|
195
|
+
};
|
|
196
|
+
const drawRow = (columns, toDraw) => `${columns.map(toDraw).join("|")}|`;
|
|
197
|
+
const drawGroupHeader = (columns) => drawRow(columns, (c) => drawHeader(c));
|
|
198
|
+
const drawGroupLine = (columns) => drawRow(columns, (c) => drawLine(c));
|
|
199
|
+
const drawLineRow = (columns) => drawRow(columns, (c) => drawColumnLines(c));
|
|
200
|
+
const drawColumnHeader = (columns) => drawRow(columns, (c) => drawColumnHeaders(c));
|
|
201
|
+
const drawTableBody = (columns, options, current, ancestorCount) => {
|
|
202
|
+
const ownRow = Match.value(options.clearTextReporter.skipFull === false || current.metrics.mutationScore !== 100).pipe(Match.when(true, () => [drawRow(columns, (column) => drawTableCell(column, current, ancestorCount))]), Match.when(false, () => []), Match.exhaustive);
|
|
203
|
+
const childRows = current.childResults.flatMap((child) => drawTableBody(columns, options, child, ancestorCount + 1));
|
|
204
|
+
return [...ownRow, ...childRows];
|
|
205
|
+
};
|
|
206
|
+
const EOL = "\n";
|
|
207
|
+
const drawMutationScoreTable = (metricsResult, options) => {
|
|
208
|
+
const columns = createColumns(metricsResult, options);
|
|
209
|
+
return [
|
|
210
|
+
drawGroupLine(columns),
|
|
211
|
+
drawGroupHeader(columns),
|
|
212
|
+
drawColumnHeader(columns),
|
|
213
|
+
drawLineRow(columns),
|
|
214
|
+
drawTableBody(columns, options, metricsResult, 0).join(EOL),
|
|
215
|
+
drawLineRow(columns)
|
|
216
|
+
].join(EOL);
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region src/Reporter.schema.ts
|
|
220
|
+
var ClearTextReportCommand = class extends S.TaggedClass()("ClearTextReportCommand", {
|
|
221
|
+
report: MutationTestResultSchema,
|
|
222
|
+
metrics: MetricsResultSchema
|
|
223
|
+
}) {};
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/clear-text-report.ts
|
|
226
|
+
function plural(items) {
|
|
227
|
+
if (items > 1) return "s";
|
|
228
|
+
else return "";
|
|
229
|
+
}
|
|
230
|
+
function getEmojiForStatus(status) {
|
|
231
|
+
switch (status) {
|
|
232
|
+
case "Killed": return "✅";
|
|
233
|
+
case "NoCoverage": return "🙈";
|
|
234
|
+
case "Ignored": return "🤥";
|
|
235
|
+
case "Survived": return "👽";
|
|
236
|
+
case "Timeout": return "⏰";
|
|
237
|
+
case "Pending": return "⌛";
|
|
238
|
+
case "RuntimeError":
|
|
239
|
+
case "CompileError": return "💥";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function sourceLocation(fileName, position, allowColor) {
|
|
243
|
+
return [
|
|
244
|
+
(() => {
|
|
245
|
+
if (allowColor) return ansi.cyan(fileName);
|
|
246
|
+
return fileName;
|
|
247
|
+
})(),
|
|
248
|
+
(() => {
|
|
249
|
+
if (allowColor) return ansi.yellow(String(position.line));
|
|
250
|
+
return String(position.line);
|
|
251
|
+
})(),
|
|
252
|
+
(() => {
|
|
253
|
+
if (allowColor) return ansi.yellow(String(position.column));
|
|
254
|
+
return String(position.column);
|
|
255
|
+
})()
|
|
256
|
+
].join(":");
|
|
257
|
+
}
|
|
258
|
+
function mutantLabel(status, allowEmojis) {
|
|
259
|
+
if (allowEmojis) return `${getEmojiForStatus(status)} ${status}`;
|
|
260
|
+
return status;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Where a mutant's block is written: the diagnostics channel for mutants that
|
|
264
|
+
* failed to run, the report for the ones that inform, and nowhere for the rest.
|
|
265
|
+
*/
|
|
266
|
+
const REPORT_CHANNEL = {
|
|
267
|
+
Killed: "debug",
|
|
268
|
+
Timeout: "debug",
|
|
269
|
+
RuntimeError: "debug",
|
|
270
|
+
CompileError: "debug",
|
|
271
|
+
Survived: "stdout",
|
|
272
|
+
NoCoverage: "stdout",
|
|
273
|
+
Ignored: "none",
|
|
274
|
+
Pending: "none"
|
|
275
|
+
};
|
|
276
|
+
function extractReportMutants(report) {
|
|
277
|
+
if (!Predicate.hasProperty(report, "files")) return [];
|
|
278
|
+
return Object.entries(report.files).flatMap(([fileName, file]) => file.mutants.map((mutant) => ({
|
|
279
|
+
fileName,
|
|
280
|
+
mutant: {
|
|
281
|
+
...mutant,
|
|
282
|
+
fileName
|
|
283
|
+
},
|
|
284
|
+
source: file.source
|
|
285
|
+
})));
|
|
286
|
+
}
|
|
287
|
+
function sourceLine(source, position) {
|
|
288
|
+
const raw = source.split("\n")[position.line - 1];
|
|
289
|
+
if (raw === void 0) return "";
|
|
290
|
+
return raw;
|
|
291
|
+
}
|
|
292
|
+
function tailFromColumn(raw, column) {
|
|
293
|
+
if (raw.length === 0) return [];
|
|
294
|
+
return [raw.slice(column)];
|
|
295
|
+
}
|
|
296
|
+
function sliceSource(source, position) {
|
|
297
|
+
if (source === void 0) return [];
|
|
298
|
+
return tailFromColumn(sourceLine(source, position), position.column);
|
|
299
|
+
}
|
|
300
|
+
function originalLines(source, position, allowColor) {
|
|
301
|
+
return sliceSource(source, position).map((l) => {
|
|
302
|
+
if (allowColor) return ansi.red(`- ${l}`);
|
|
303
|
+
return `- ${l}`;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
function replacementLines(replacement, allowColor) {
|
|
307
|
+
if (replacement === void 0) return [];
|
|
308
|
+
return replacement.split("\n").filter(Boolean).map((l) => {
|
|
309
|
+
if (allowColor) return ansi.green(`+ ${l}`);
|
|
310
|
+
return `+ ${l}`;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const logsCoveredTests = (mutant, options) => {
|
|
314
|
+
if (!options.clearTextReporter.logTests) return false;
|
|
315
|
+
return mutant.coveredBy !== void 0;
|
|
316
|
+
};
|
|
317
|
+
const coveredTestsTail = (mutant, options) => {
|
|
318
|
+
if (!logsCoveredTests(mutant, options)) return [];
|
|
319
|
+
return formatCoveredTests(mutant.coveredBy, options);
|
|
320
|
+
};
|
|
321
|
+
const survivorTail = (mutant, options) => {
|
|
322
|
+
if (mutant.static === true) return ["Ran all tests for this mutant."];
|
|
323
|
+
return coveredTestsTail(mutant, options);
|
|
324
|
+
};
|
|
325
|
+
const killedByLine = (killer) => {
|
|
326
|
+
if (killer === void 0) return [];
|
|
327
|
+
return [`Killed by: ${killer}`];
|
|
328
|
+
};
|
|
329
|
+
const killerTail = (mutant) => {
|
|
330
|
+
const killedBy = mutant.killedBy;
|
|
331
|
+
if (killedBy === void 0) return [];
|
|
332
|
+
return killedByLine(killedBy[0]);
|
|
333
|
+
};
|
|
334
|
+
const statusReasonTail = (mutant) => {
|
|
335
|
+
if (mutant.statusReason === void 0) return [];
|
|
336
|
+
return [`Error message: ${mutant.statusReason}`];
|
|
337
|
+
};
|
|
338
|
+
/**
|
|
339
|
+
* The lines below a mutant block: one tail per status, chosen by the status the
|
|
340
|
+
* mutant settled at.
|
|
341
|
+
*/
|
|
342
|
+
const statusTail = (mutant, options) => Match.value(mutant.status).pipe(Match.when("Survived", () => survivorTail(mutant, options)), Match.when("Killed", () => killerTail(mutant)), Match.whenOr("RuntimeError", "CompileError", () => statusReasonTail(mutant)), Match.orElse(() => []));
|
|
343
|
+
const overflowNotice = (hidden) => {
|
|
344
|
+
if (hidden <= 0) return [];
|
|
345
|
+
return [` and ${hidden} more test${plural(hidden)}!`];
|
|
346
|
+
};
|
|
347
|
+
function formatCoveredTests(tests, options) {
|
|
348
|
+
const maxLog = options.clearTextReporter.maxTestsToLog;
|
|
349
|
+
const logged = Math.min(maxLog, tests.length);
|
|
350
|
+
if (logged <= 0) return [];
|
|
351
|
+
return [
|
|
352
|
+
"Tests ran:",
|
|
353
|
+
...tests.slice(0, logged).map((test) => ` ${test}`),
|
|
354
|
+
...overflowNotice(tests.length - maxLog),
|
|
355
|
+
""
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
function mutantBlock(fileName, mutant, source, options) {
|
|
359
|
+
const allowColor = options.clearTextReporter.allowColor;
|
|
360
|
+
const allowEmojis = options.clearTextReporter.allowEmojis;
|
|
361
|
+
const out = [];
|
|
362
|
+
out.push(`[${mutantLabel(mutant.status, allowEmojis)}] ${mutant.mutatorName}`);
|
|
363
|
+
out.push(sourceLocation(fileName, mutant.location.start, allowColor));
|
|
364
|
+
out.push(...originalLines(source, mutant.location.start, allowColor));
|
|
365
|
+
out.push(...replacementLines(mutant.replacement, allowColor));
|
|
366
|
+
out.push(...statusTail(mutant, options));
|
|
367
|
+
out.push("");
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
const linesForChannel = (entries, channel, options) => {
|
|
371
|
+
return entries.filter((entry) => REPORT_CHANNEL[entry.mutant.status] === channel).flatMap((entry) => mutantBlock(entry.fileName, entry.mutant, entry.source, options));
|
|
372
|
+
};
|
|
373
|
+
function collectMutants(report, options) {
|
|
374
|
+
const entries = extractReportMutants(report);
|
|
375
|
+
return {
|
|
376
|
+
stdout: linesForChannel(entries, "stdout", options),
|
|
377
|
+
debug: linesForChannel(entries, "debug", options),
|
|
378
|
+
totalTests: entries.reduce((total, entry) => total + (entry.mutant.testsCompleted ?? 0), 0)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const partialScoresVisible = (metrics, options) => {
|
|
382
|
+
if (!options.clearTextReporter.skipFull) return true;
|
|
383
|
+
return metrics.childResults.some((child) => child.metrics.mutationScore !== 100);
|
|
384
|
+
};
|
|
385
|
+
const drawsScoreTable = (metrics, options) => {
|
|
386
|
+
if (!options.clearTextReporter.reportScoreTable) return false;
|
|
387
|
+
return partialScoresVisible(metrics, options);
|
|
388
|
+
};
|
|
389
|
+
function scoreTable(metrics, options) {
|
|
390
|
+
if (!drawsScoreTable(metrics, options)) return void 0;
|
|
391
|
+
return drawMutationScoreTable(metrics, options);
|
|
392
|
+
}
|
|
393
|
+
const EMPTY_REPORT_LINES = {
|
|
394
|
+
stdout: [],
|
|
395
|
+
debug: []
|
|
396
|
+
};
|
|
397
|
+
const testsPerMutant = (metrics, totalTests) => {
|
|
398
|
+
const total = metrics.metrics.totalMutants;
|
|
399
|
+
if (total === 0) return "0.00";
|
|
400
|
+
return (totalTests / total).toFixed(2);
|
|
401
|
+
};
|
|
402
|
+
const lineIfPresent = (line) => {
|
|
403
|
+
if (line === void 0) return [];
|
|
404
|
+
return [line];
|
|
405
|
+
};
|
|
406
|
+
const mutantReportSection = (report, metrics, options) => {
|
|
407
|
+
if (!options.clearTextReporter.reportMutants) return EMPTY_REPORT_LINES;
|
|
408
|
+
const blocks = collectMutants(report, options);
|
|
409
|
+
return {
|
|
410
|
+
stdout: [
|
|
411
|
+
"",
|
|
412
|
+
...blocks.stdout,
|
|
413
|
+
`Ran ${testsPerMutant(metrics, blocks.totalTests)} tests per mutant on average.`
|
|
414
|
+
],
|
|
415
|
+
debug: blocks.debug
|
|
416
|
+
};
|
|
417
|
+
};
|
|
418
|
+
function renderClearText(report, metrics, options) {
|
|
419
|
+
const section = mutantReportSection(report, metrics, options);
|
|
420
|
+
return {
|
|
421
|
+
stdout: [
|
|
422
|
+
"",
|
|
423
|
+
...section.stdout,
|
|
424
|
+
...lineIfPresent(scoreTable(metrics, options))
|
|
425
|
+
],
|
|
426
|
+
debug: [...section.debug]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const rememberTerminalReport = (seen, event) => {
|
|
430
|
+
Match.value(event).pipe(Match.tag("mutationTestReportReady", (ready) => {
|
|
431
|
+
seen.terminal = {
|
|
432
|
+
report: ready.report,
|
|
433
|
+
metrics: ready.metrics
|
|
434
|
+
};
|
|
435
|
+
}), Match.orElse(() => void 0));
|
|
436
|
+
};
|
|
437
|
+
const decodeClearTextReport = (terminal) => S.decodeUnknownEffect(ClearTextReportCommand)({
|
|
438
|
+
_tag: "ClearTextReportCommand",
|
|
439
|
+
report: terminal.report,
|
|
440
|
+
metrics: terminal.metrics
|
|
441
|
+
}).pipe(Effect.mapError((cause) => ReporterFailed.make({
|
|
442
|
+
reporterName: "clear-text",
|
|
443
|
+
event: "mutationTestReportReady",
|
|
444
|
+
cause: errorToString(cause)
|
|
445
|
+
})));
|
|
446
|
+
const streamErrorOf$1 = (cause) => ReporterFailed.make({
|
|
447
|
+
reporterName: "clear-text",
|
|
448
|
+
event: "mutationTestReportReady",
|
|
449
|
+
cause: errorToString(cause)
|
|
450
|
+
});
|
|
451
|
+
const failAsClearText = (cause) => ReporterFailed.make({
|
|
452
|
+
reporterName: "clear-text",
|
|
453
|
+
event: "mutationTestReportReady",
|
|
454
|
+
cause: errorToString(cause)
|
|
455
|
+
});
|
|
456
|
+
const writeRendered = (services, rendered, options) => {
|
|
457
|
+
const stdoutEffect = write(services.stdio, "stdout", rendered.stdout.map((line) => `${line}\n`)).pipe(Effect.mapError(failAsClearText), Effect.asVoid);
|
|
458
|
+
return Match.value(options.logLevel === "debug").pipe(Match.when(true, () => Effect.andThen(stdoutEffect, write(services.stdio, "stderr", rendered.debug.map((line) => `${line}\n`)).pipe(Effect.mapError(failAsClearText)))), Match.orElse(() => stdoutEffect));
|
|
459
|
+
};
|
|
460
|
+
const makeClearTextReporter = (services) => (options) => (events) => {
|
|
461
|
+
const seen = {};
|
|
462
|
+
return Stream.runForEach(Stream.fromAsyncIterable(events, streamErrorOf$1), (event) => Effect.sync(() => rememberTerminalReport(seen, event))).pipe(Effect.andThen(Effect.sync(() => seen.terminal)), Effect.flatMap((terminal) => Option.match(Option.fromUndefinedOr(terminal), {
|
|
463
|
+
onNone: () => Effect.void,
|
|
464
|
+
onSome: (ready) => decodeClearTextReport(ready).pipe(Effect.flatMap((command) => writeRendered(services, renderClearText(command.report, command.metrics, options), options)))
|
|
465
|
+
})));
|
|
466
|
+
};
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/json-reporter.ts
|
|
469
|
+
const failAsJsonReporter = (cause) => ReporterFailed.make({
|
|
470
|
+
reporterName: "json",
|
|
471
|
+
event: "mutationTestReportReady",
|
|
472
|
+
cause: errorToString(cause)
|
|
473
|
+
});
|
|
474
|
+
const writeReport = (services, options, report) => Effect.gen(function* () {
|
|
475
|
+
const json = yield* S.encodeEffect(S.fromJsonString(S.Unknown, { space: 0 }))(report).pipe(Effect.orDie);
|
|
476
|
+
const fs = yield* FileSystem.FileSystem;
|
|
477
|
+
const path = yield* Path.Path;
|
|
478
|
+
const fileName = path.resolve(options.jsonReporter.fileName);
|
|
479
|
+
if (options.logLevel === "debug") yield* Effect.ignore(write(services.stdio, "stderr", [`Using relative path ${path.normalize(options.jsonReporter.fileName)}\n`]));
|
|
480
|
+
yield* fs.makeDirectory(path.dirname(fileName), { recursive: true }).pipe(Effect.mapError(failAsJsonReporter));
|
|
481
|
+
yield* fs.writeFileString(fileName, json).pipe(Effect.mapError(failAsJsonReporter));
|
|
482
|
+
const url = yield* path.toFileUrl(fileName).pipe(Effect.mapError(failAsJsonReporter));
|
|
483
|
+
yield* Effect.ignore(write(services.stdio, "stdout", [`Your report can be found at: ${url.href}\n`]));
|
|
484
|
+
}).pipe(Effect.provideService(FileSystem.FileSystem, services.fileSystem), Effect.provideService(Path.Path, services.path));
|
|
485
|
+
const rememberReport = (seen, event) => {
|
|
486
|
+
Match.value(event).pipe(Match.tag("mutationTestReportReady", (ready) => {
|
|
487
|
+
seen.report = ready.report;
|
|
488
|
+
}), Match.orElse(() => void 0));
|
|
489
|
+
};
|
|
490
|
+
const streamErrorOf = (cause) => ReporterFailed.make({
|
|
491
|
+
reporterName: "json",
|
|
492
|
+
event: "mutationTestReportReady",
|
|
493
|
+
cause: errorToString(cause)
|
|
494
|
+
});
|
|
495
|
+
const makeJsonReporter = (services) => (options) => (events) => {
|
|
496
|
+
const seen = {};
|
|
497
|
+
return Stream.runForEach(Stream.fromAsyncIterable(events, streamErrorOf), (event) => Effect.sync(() => rememberReport(seen, event))).pipe(Effect.andThen(Effect.sync(() => seen.report)), Effect.flatMap((report) => Option.match(Option.fromUndefinedOr(report), {
|
|
498
|
+
onNone: () => Effect.void,
|
|
499
|
+
onSome: (ready) => writeReport(services, options, ready)
|
|
500
|
+
})));
|
|
501
|
+
};
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/progress-reporter.ts
|
|
504
|
+
const makeProgressBarState = (format, options) => ({
|
|
505
|
+
format,
|
|
506
|
+
total: options.total,
|
|
507
|
+
curr: 0,
|
|
508
|
+
width: options.width,
|
|
509
|
+
complete: options.complete,
|
|
510
|
+
incomplete: options.incomplete
|
|
511
|
+
});
|
|
512
|
+
const tickProgressBar = (state, ticks) => ({
|
|
513
|
+
...state,
|
|
514
|
+
curr: state.curr + ticks
|
|
515
|
+
});
|
|
516
|
+
const renderProgressBar = (state, data) => formatBar(state.format, state.curr, state.total, data, {
|
|
517
|
+
width: state.width,
|
|
518
|
+
complete: state.complete,
|
|
519
|
+
incomplete: state.incomplete
|
|
520
|
+
});
|
|
521
|
+
const isComplete = (state) => state.curr >= state.total;
|
|
522
|
+
function formatBar(format, curr, total, data, options) {
|
|
523
|
+
const ratio = Match.value(total === 0).pipe(Match.when(true, () => 0), Match.when(false, () => Math.min(curr / total, 1)), Match.exhaustive);
|
|
524
|
+
const filled = Math.floor(ratio * options.width);
|
|
525
|
+
const bar = options.complete.repeat(filled) + options.incomplete.repeat(options.width - filled);
|
|
526
|
+
const percent = `${Math.floor(ratio * 100).toString().padStart(3, " ")}%`;
|
|
527
|
+
const printed = format.replace(":bar", bar).replace(":percent", percent);
|
|
528
|
+
return Object.entries(data).reduce((out, [key, value]) => out.replaceAll(`:${key}`, String(value)), printed);
|
|
529
|
+
}
|
|
530
|
+
const emptyTally = (startedAt) => ({
|
|
531
|
+
survived: 0,
|
|
532
|
+
timedOut: 0,
|
|
533
|
+
tested: 0,
|
|
534
|
+
mutants: 0,
|
|
535
|
+
total: 0,
|
|
536
|
+
ticks: 0,
|
|
537
|
+
ticksByMutantId: {},
|
|
538
|
+
timing: {
|
|
539
|
+
net: 0,
|
|
540
|
+
overhead: 0
|
|
541
|
+
},
|
|
542
|
+
capabilities: { reloadEnvironment: false },
|
|
543
|
+
startedAt
|
|
544
|
+
});
|
|
545
|
+
const getElapsedTime = (tally, now) => {
|
|
546
|
+
return formatTime(Math.floor((now - tally.startedAt) / 1e3));
|
|
547
|
+
};
|
|
548
|
+
const getEtc = (tally, now) => {
|
|
549
|
+
const elapsed = Math.floor((now - tally.startedAt) / 1e3);
|
|
550
|
+
const remaining = Math.floor(elapsed / tally.ticks * (tally.total - tally.ticks));
|
|
551
|
+
return Match.value(Number.isFinite(remaining) && remaining > 0).pipe(Match.when(true, () => formatTime(remaining)), Match.when(false, () => "n/a"), Match.exhaustive);
|
|
552
|
+
};
|
|
553
|
+
function formatTime(timeInSeconds) {
|
|
554
|
+
const hours = Math.floor(timeInSeconds / 3600);
|
|
555
|
+
const minutes = Math.floor(timeInSeconds % 3600 / 60);
|
|
556
|
+
return Match.value(hours > 0).pipe(Match.when(true, () => `~${hours}h ${minutes}m`), Match.when(false, () => Match.value(minutes > 0).pipe(Match.when(true, () => `~${minutes}m`), Match.when(false, () => "<1m"), Match.exhaustive)), Match.exhaustive);
|
|
557
|
+
}
|
|
558
|
+
const ticksFor = (plan, tally) => Match.value(tally.capabilities.reloadEnvironment === false && plan.reloadEnvironment).pipe(Match.when(true, () => plan.netTime + tally.timing.overhead), Match.when(false, () => plan.netTime), Match.exhaustive);
|
|
559
|
+
const progressData = (tally, now) => ({
|
|
560
|
+
survived: tally.survived,
|
|
561
|
+
timedOut: tally.timedOut,
|
|
562
|
+
tested: tally.tested,
|
|
563
|
+
mutants: tally.mutants,
|
|
564
|
+
total: tally.total,
|
|
565
|
+
ticks: tally.ticks,
|
|
566
|
+
et: getElapsedTime(tally, now),
|
|
567
|
+
etc: getEtc(tally, now)
|
|
568
|
+
});
|
|
569
|
+
const tallyAfterTest = (tally, tested, ticks) => {
|
|
570
|
+
const counted = Match.value(tested.status).pipe(Match.when("Survived", () => ({
|
|
571
|
+
survived: 1,
|
|
572
|
+
timedOut: 0
|
|
573
|
+
})), Match.when("Timeout", () => ({
|
|
574
|
+
survived: 0,
|
|
575
|
+
timedOut: 1
|
|
576
|
+
})), Match.orElse(() => ({
|
|
577
|
+
survived: 0,
|
|
578
|
+
timedOut: 0
|
|
579
|
+
})));
|
|
580
|
+
return {
|
|
581
|
+
...tally,
|
|
582
|
+
tested: tested.completed,
|
|
583
|
+
ticks: tally.ticks + ticks,
|
|
584
|
+
survived: tally.survived + counted.survived,
|
|
585
|
+
timedOut: tally.timedOut + counted.timedOut
|
|
586
|
+
};
|
|
587
|
+
};
|
|
588
|
+
const PROGRESS_BAR_FORMAT = "Mutation testing [:bar] :percent (elapsed: :et, remaining: :etc) :tested/:mutants Mutants tested (:survived survived, :timedOut timed out)";
|
|
589
|
+
const PROGRESS_BAR_OPTIONS = {
|
|
590
|
+
complete: "=",
|
|
591
|
+
incomplete: " ",
|
|
592
|
+
width: 50
|
|
593
|
+
};
|
|
594
|
+
const progressStreamErrorOf = (cause) => ReporterFailed.make({
|
|
595
|
+
reporterName: "progress",
|
|
596
|
+
event: "mutationTestReportReady",
|
|
597
|
+
cause: errorToString(cause)
|
|
598
|
+
});
|
|
599
|
+
const makeProgressBarReporter = (services) => () => (events) => {
|
|
600
|
+
const progress = {
|
|
601
|
+
tally: emptyTally(0),
|
|
602
|
+
bar: void 0
|
|
603
|
+
};
|
|
604
|
+
let pending = Promise.resolve();
|
|
605
|
+
const enqueue = (text) => {
|
|
606
|
+
pending = pending.then(() => writeAsync(services.stdio, "stdout", [text])).catch(() => void 0);
|
|
607
|
+
};
|
|
608
|
+
const render = (now) => Option.match(Option.fromUndefinedOr(progress.bar), {
|
|
609
|
+
onNone: () => void 0,
|
|
610
|
+
onSome: (bar) => {
|
|
611
|
+
const line = renderProgressBar(bar, progressData(progress.tally, now));
|
|
612
|
+
const newline = Match.value(isComplete(bar)).pipe(Match.when(true, () => "\n"), Match.when(false, () => ""), Match.exhaustive);
|
|
613
|
+
enqueue(`\r${line}${newline}`);
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
return Stream.runForEach(Stream.fromAsyncIterable(events, progressStreamErrorOf), (event) => Effect.sync(() => applyProgressEvent(progress, render, event))).pipe(Effect.ensuring(Effect.ignore(Effect.promise(() => {
|
|
617
|
+
finishProgressBar(progress, enqueue);
|
|
618
|
+
return pending;
|
|
619
|
+
}))));
|
|
620
|
+
};
|
|
621
|
+
const makeProgressStreamReporter = () => (events) => Stream.runDrain(Stream.fromAsyncIterable(events, progressStreamErrorOf));
|
|
622
|
+
const applyProgressEvent = (progress, render, event) => {
|
|
623
|
+
Match.value(event).pipe(Match.tag("dryRunCompleted", (dryRun) => {
|
|
624
|
+
progress.tally = {
|
|
625
|
+
...progress.tally,
|
|
626
|
+
timing: dryRun.timing,
|
|
627
|
+
capabilities: { reloadEnvironment: dryRun.capabilities.reloadEnvironment }
|
|
628
|
+
};
|
|
629
|
+
}), Match.tag("mutationTestingPlanReady", (planReady) => {
|
|
630
|
+
const ticksByMutantId = Object.fromEntries(planReady.plans.flatMap((plan) => {
|
|
631
|
+
if (plan.plan !== "Run") return [];
|
|
632
|
+
return [[plan.mutantId, ticksFor(plan, progress.tally)]];
|
|
633
|
+
}));
|
|
634
|
+
const total = Object.values(ticksByMutantId).reduce((sum, ticks) => sum + ticks, 0);
|
|
635
|
+
progress.tally = {
|
|
636
|
+
...progress.tally,
|
|
637
|
+
startedAt: performance.now(),
|
|
638
|
+
ticksByMutantId,
|
|
639
|
+
mutants: Object.keys(ticksByMutantId).length,
|
|
640
|
+
total
|
|
641
|
+
};
|
|
642
|
+
progress.bar = makeProgressBarState(PROGRESS_BAR_FORMAT, {
|
|
643
|
+
...PROGRESS_BAR_OPTIONS,
|
|
644
|
+
total
|
|
645
|
+
});
|
|
646
|
+
}), Match.tag("mutantTested", (tested) => {
|
|
647
|
+
Option.match(Option.fromUndefinedOr(progress.tally.ticksByMutantId[tested.id]), {
|
|
648
|
+
onNone: () => void 0,
|
|
649
|
+
onSome: (ticks) => {
|
|
650
|
+
progress.tally = tallyAfterTest(progress.tally, tested, ticks);
|
|
651
|
+
progress.bar = Option.getOrUndefined(Option.map(Option.fromUndefinedOr(progress.bar), (bar) => tickProgressBar(bar, ticks)));
|
|
652
|
+
render(performance.now());
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
}), Match.orElse(() => void 0));
|
|
656
|
+
};
|
|
657
|
+
const finishProgressBar = (progress, enqueue) => {
|
|
658
|
+
Option.match(Option.fromUndefinedOr(progress.bar), {
|
|
659
|
+
onNone: () => void 0,
|
|
660
|
+
onSome: (bar) => Match.value(isComplete(bar)).pipe(Match.when(true, () => void 0), Match.when(false, () => enqueue("\n")), Match.exhaustive)
|
|
661
|
+
});
|
|
662
|
+
};
|
|
663
|
+
//#endregion
|
|
664
|
+
//#region src/builtin-reporters.ts
|
|
665
|
+
const makeBuiltinReporterFactories = (services) => ({
|
|
666
|
+
"json": makeJsonReporter(services),
|
|
667
|
+
"clear-text": makeClearTextReporter(services),
|
|
668
|
+
"progress": makeProgressBarReporter(services),
|
|
669
|
+
"progress-stream": makeProgressStreamReporter
|
|
670
|
+
});
|
|
671
|
+
//#endregion
|
|
672
|
+
export { ansi as n, makeBuiltinReporterFactories as t };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as FileSystem from 'effect/FileSystem';
|
|
2
|
+
import * as Path from 'effect/Path';
|
|
3
|
+
import { ReporterFactory } from '@systemfsoftware/stryker-js-plugin-interface';
|
|
4
|
+
import * as Stdio from 'effect/Stdio';
|
|
5
|
+
|
|
6
|
+
export declare interface BuiltinReporterServices {
|
|
7
|
+
readonly fileSystem: FileSystem.FileSystem;
|
|
8
|
+
readonly path: Path.Path;
|
|
9
|
+
readonly stdio: Stdio.Stdio;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare const makeBuiltinReporterFactories: (services: BuiltinReporterServices) => Record<string, ReporterFactory>;
|
|
13
|
+
|
|
14
|
+
export { }
|