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