UNPKG

6.98 kBPlain TextView Raw
1import os from 'os';
2
3import chalk, { Color } from 'chalk';
4import { schema, Position, StrykerOptions } from '@stryker-mutator/api/core';
5import { Logger } from '@stryker-mutator/api/logging';
6import { commonTokens } from '@stryker-mutator/api/plugin';
7import { Reporter } from '@stryker-mutator/api/report';
8import { MetricsResult, MutantModel, TestModel, MutationTestMetricsResult, TestFileModel, TestMetrics, TestStatus } from 'mutation-testing-metrics';
9import { tokens } from 'typed-inject';
10
11import { getEmojiForStatus, plural } from '../utils/string-utils.js';
12
13import { ClearTextScoreTable } from './clear-text-score-table.js';
14
15const { MutantStatus } = schema;
16
17export class ClearTextReporter implements Reporter {
18 public static inject = tokens(commonTokens.logger, commonTokens.options);
19 constructor(private readonly log: Logger, private readonly options: StrykerOptions) {
20 this.configConsoleColor();
21 }
22
23 private readonly out: NodeJS.WritableStream = process.stdout;
24
25 private readonly writeLine = (output?: string) => {
26 this.out.write(`${output ?? ''}${os.EOL}`);
27 };
28
29 private readonly writeDebugLine = (input: string) => {
30 this.log.debug(input);
31 };
32
33 private configConsoleColor() {
34 if (!this.options.allowConsoleColors) {
35 chalk.level = 0; // All colors disabled
36 }
37 }
38
39 public onMutationTestReportReady(_report: schema.MutationTestResult, metrics: MutationTestMetricsResult): void {
40 this.writeLine();
41 this.reportAllTests(metrics);
42 this.reportAllMutants(metrics);
43 this.writeLine(new ClearTextScoreTable(metrics.systemUnderTestMetrics, this.options).draw());
44 }
45
46 private reportAllTests(metrics: MutationTestMetricsResult) {
47 function indent(depth: number) {
48 return new Array(depth).fill(' ').join('');
49 }
50 const formatTestLine = (test: TestModel, state: string): string => {
51 return `${this.color('grey', `${test.name}${test.location ? ` [line ${test.location.start.line}]` : ''}`)} (${state})`;
52 };
53
54 if (metrics.testMetrics) {
55 const reportTests = (currentResult: MetricsResult<TestFileModel, TestMetrics>, depth = 0) => {
56 const nameParts: string[] = [currentResult.name];
57 while (!currentResult.file && currentResult.childResults.length === 1) {
58 currentResult = currentResult.childResults[0];
59 nameParts.push(currentResult.name);
60 }
61 this.writeLine(`${indent(depth)}${nameParts.join('/')}`);
62 currentResult.file?.tests.forEach((test) => {
63 switch (test.status) {
64 case TestStatus.Killing:
65 this.writeLine(`${indent(depth + 1)}${this.color('greenBright', '✓')} ${formatTestLine(test, `killed ${test.killedMutants?.length}`)}`);
66 break;
67 case TestStatus.Covering:
68 this.writeLine(
69 `${indent(depth + 1)}${this.color('blueBright', '~')} ${formatTestLine(test, `covered ${test.coveredMutants?.length}`)}`
70 );
71 break;
72 case TestStatus.NotCovering:
73 this.writeLine(`${indent(depth + 1)}${this.color('redBright', '✘')} ${formatTestLine(test, 'covered 0')}`);
74 break;
75 }
76 });
77 currentResult.childResults.forEach((childResult) => reportTests(childResult, depth + 1));
78 };
79 reportTests(metrics.testMetrics);
80 }
81 }
82
83 private reportAllMutants({ systemUnderTestMetrics }: MutationTestMetricsResult): void {
84 this.writeLine();
85 let totalTests = 0;
86
87 const reportMutants = (metrics: MetricsResult[]) => {
88 metrics.forEach((child) => {
89 child.file?.mutants.forEach((result) => {
90 totalTests += result.testsCompleted ?? 0;
91 switch (result.status) {
92 case MutantStatus.Killed:
93 case MutantStatus.Timeout:
94 case MutantStatus.RuntimeError:
95 case MutantStatus.CompileError:
96 this.reportMutantResult(result, this.writeDebugLine);
97 break;
98 case MutantStatus.Survived:
99 case MutantStatus.NoCoverage:
100 this.reportMutantResult(result, this.writeLine);
101 break;
102 default:
103 }
104 });
105 reportMutants(child.childResults);
106 });
107 };
108 reportMutants(systemUnderTestMetrics.childResults);
109 this.writeLine(`Ran ${(totalTests / systemUnderTestMetrics.metrics.totalMutants).toFixed(2)} tests per mutant on average.`);
110 }
111
112 private statusLabel(mutant: MutantModel): string {
113 const status = MutantStatus[mutant.status];
114 return this.options.clearTextReporter.allowEmojis ? `${getEmojiForStatus(status)} ${status}` : status.toString();
115 }
116
117 private reportMutantResult(result: MutantModel, logImplementation: (input: string) => void): void {
118 logImplementation(`[${this.statusLabel(result)}] ${result.mutatorName}`);
119 logImplementation(this.colorSourceFileAndLocation(result.fileName, result.location.start));
120
121 result
122 .getOriginalLines()
123 .split('\n')
124 .filter(Boolean)
125 .forEach((line) => {
126 logImplementation(chalk.red('- ' + line));
127 });
128 result
129 .getMutatedLines()
130 .split('\n')
131 .filter(Boolean)
132 .forEach((line) => {
133 logImplementation(chalk.green('+ ' + line));
134 });
135 if (result.status === MutantStatus.Survived) {
136 if (result.static) {
137 logImplementation('Ran all tests for this mutant.');
138 } else if (result.coveredByTests) {
139 this.logExecutedTests(result.coveredByTests, logImplementation);
140 }
141 } else if (result.status === MutantStatus.Killed && result.killedByTests && result.killedByTests.length) {
142 logImplementation(`Killed by: ${result.killedByTests[0].name}`);
143 } else if (result.status === MutantStatus.RuntimeError || result.status === MutantStatus.CompileError) {
144 logImplementation(`Error message: ${result.statusReason}`);
145 }
146 logImplementation('');
147 }
148
149 private colorSourceFileAndLocation(fileName: string, position: Position): string {
150 return [this.color('cyan', fileName), this.color('yellow', position.line), this.color('yellow', position.column)].join(':');
151 }
152
153 private color(color: Color, ...text: unknown[]) {
154 if (this.options.clearTextReporter.allowColor) {
155 return chalk[color](...text);
156 }
157 return text.join('');
158 }
159
160 private logExecutedTests(tests: TestModel[], logImplementation: (input: string) => void) {
161 if (!this.options.clearTextReporter.logTests) {
162 return;
163 }
164
165 const testCount = Math.min(this.options.clearTextReporter.maxTestsToLog, tests.length);
166
167 if (testCount > 0) {
168 logImplementation('Tests ran:');
169 tests.slice(0, testCount).forEach((test) => {
170 logImplementation(` ${test.name}`);
171 });
172 const diff = tests.length - this.options.clearTextReporter.maxTestsToLog;
173 if (diff > 0) {
174 logImplementation(` and ${diff} more test${plural(diff)}!`);
175 }
176 logImplementation('');
177 }
178 }
179}
180
\No newline at end of file