Skip to content

Commit

Permalink
feat(clear text reporter): Prettify the clear-text report (#1185)
Browse files Browse the repository at this point in the history
Make the clear-text reporter a lot cleaner. Reports take less space on the screen and have nice colors.

Example (in real live has even more color)

```
7. [Survived] IfStatement
C:\z\github\stryker-mutator\stryker\packages\stryker-webpack-transpiler\src\WebpackTranspiler.ts:12:8
-       if (options.produceSourceMaps) {
+       if (false) {
```

Configure the clear text reporter with 

```
clearTextReporter: {
   logTests: false,
   allowColors: true
}
```
  • Loading branch information
Josh Goldberg authored and nicojs committed Oct 28, 2018
1 parent 2732895 commit a49829b
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 27 deletions.
5 changes: 4 additions & 1 deletion packages/stryker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,10 @@ By default `clear-text` and `progress` are active if no reporters are configured
You can load additional plugins to get more reporters. See [stryker-mutator.io](https://stryker-mutator.io)
for an up-to-date list of supported reporter plugins and a description on each reporter.

The `clear-text` reporter supports an additional config option to show more tests that were executed to kill a mutant. The config for your config file is: `clearTextReporter: { maxTestsToLog: 3 },`
The `clear-text` reporter supports three additional config options:
* `allowColor` to use cyan and yellow in printing source file names and positions. This defaults to `true`, so specify as `clearTextReporter: { allowColor: false },` to disable if you must.
* `logTests` to log the names of unit tests that were run to allow mutants. By default, only the first three are logged. The config for your config file is: `clearTextReporter: { logTests: true },`
* `maxTestsToLog` to show more tests that were executed to kill a mutant when `logTests` is true. The config for your config file is: `clearTextReporter: { logTests: true, maxTestsToLog: 7 },`

The `dashboard` reporter is a special kind of reporter. It sends a report to https://dashboard.stryker-mutator.io, enabling you to add a fancy mutation score badge to your readme! To make sure no unwanted results are sent to the dashboards, it will only send the report if it is run from a build server. The reporter currently detects [Travis](https://travis-ci.org/) and [CircleCI](https://circleci.com/). Please open an [issue](https://github.com/stryker-mutator/stryker/issues/new) if your build server is missing. On all these environments, it will ignore builds of pull requests. Apart from buildserver-specific environment variables, the reporter uses one environment variable:

Expand Down
46 changes: 32 additions & 14 deletions packages/stryker/src/reporters/ClearTextReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import chalk from 'chalk';
import { getLogger } from 'stryker-api/logging';
import { Reporter, MutantResult, MutantStatus, ScoreResult } from 'stryker-api/report';
import { Config } from 'stryker-api/config';
import { Position } from 'stryker-api/core';
import ClearTextScoreTable from './ClearTextScoreTable';
import * as os from 'os';

Expand All @@ -25,43 +26,42 @@ export default class ClearTextReporter implements Reporter {
const logDebugFn = (input: string) => this.log.debug(input);
const writeLineFn = (input: string) => this.writeLine(input);

mutantResults.forEach(result => {
mutantResults.forEach((result, index) => {
if (result.testsRan) {
totalTests += result.testsRan.length;
}
switch (result.status) {
case MutantStatus.Killed:
this.log.debug(chalk.bold.green('Mutant killed!'));
this.logMutantResult(result, logDebugFn);
this.logMutantResult(result, index, logDebugFn);
break;
case MutantStatus.TimedOut:
this.log.debug(chalk.bold.yellow('Mutant timed out!'));
this.logMutantResult(result, logDebugFn);
this.logMutantResult(result, index, logDebugFn);
break;
case MutantStatus.RuntimeError:
this.log.debug(chalk.bold.yellow('Mutant caused a runtime error!'));
this.logMutantResult(result, logDebugFn);
this.logMutantResult(result, index, logDebugFn);
break;
case MutantStatus.TranspileError:
this.log.debug(chalk.bold.yellow('Mutant caused a transpile error!'));
this.logMutantResult(result, logDebugFn);
this.logMutantResult(result, index, logDebugFn);
break;
case MutantStatus.Survived:
this.writeLine(chalk.bold.red('Mutant survived!'));
this.logMutantResult(result, writeLineFn);
this.logMutantResult(result, index, writeLineFn);
break;
case MutantStatus.NoCoverage:
this.writeLine(chalk.bold.yellow('Mutant survived! (no coverage)'));
this.logMutantResult(result, writeLineFn);
this.logMutantResult(result, index, writeLineFn);
break;
}
});
this.writeLine(`Ran ${(totalTests / mutantResults.length).toFixed(2)} tests per mutant on average.`);
}

private logMutantResult(result: MutantResult, logImplementation: (input: string) => void): void {
logImplementation(result.sourceFilePath + ':' + result.location.start.line + ':' + result.location.start.column);
logImplementation('Mutator: ' + result.mutatorName);
private logMutantResult(result: MutantResult, index: number, logImplementation: (input: string) => void): void {
logImplementation(`${index}. [${MutantStatus[result.status]}] ${result.mutatorName}`);
logImplementation(this.colorSourceFileAndLocation(result.sourceFilePath, result.location.start));

result.originalLines.split('\n').forEach(line => {
logImplementation(chalk.red('- ' + line));
});
Expand All @@ -76,12 +76,30 @@ export default class ClearTextReporter implements Reporter {
}
}

private logExecutedTests(result: MutantResult, logImplementation: (input: string) => void) {
private colorSourceFileAndLocation(sourceFilePath: string, position: Position): string {
const clearTextReporterConfig = this.options.clearTextReporter;

if (clearTextReporterConfig && clearTextReporterConfig.allowColor !== false) {
return sourceFilePath + ':' + position.line + ':' + position.column;
}

return [
chalk.cyan(sourceFilePath),
chalk.yellow(`${position.line}`),
chalk.yellow(`${position.column}`),
].join(':');
}

private logExecutedTests(result: MutantResult, logImplementation: (input: string) => void) {
const clearTextReporterConfig = this.options.clearTextReporter || {};

if (!clearTextReporterConfig.logTests) {
return;
}

if (result.testsRan && result.testsRan.length > 0) {
let testsToLog = 3;
if (clearTextReporterConfig && typeof clearTextReporterConfig.maxTestsToLog === 'number') {
if (typeof clearTextReporterConfig.maxTestsToLog === 'number') {
testsToLog = clearTextReporterConfig.maxTestsToLog;
}

Expand Down
62 changes: 50 additions & 12 deletions packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import { MutantStatus, MutantResult } from 'stryker-api/report';
import ClearTextReporter from '../../../src/reporters/ClearTextReporter';
import { scoreResult, mutationScoreThresholds, config, mutantResult } from '../../helpers/producers';

const colorizeFileAndPosition = (sourceFilePath: string, line: number, column: Number) => {
return [
chalk.cyan(sourceFilePath),
chalk.yellow(`${line}`),
chalk.yellow(`${column}`),
].join(':');
};

describe('ClearTextReporter', () => {
let sut: ClearTextReporter;
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -104,7 +112,7 @@ describe('ClearTextReporter', () => {
});

describe('when coverageAnalysis is "all"', () => {
beforeEach(() => sut = new ClearTextReporter(config({ coverageAnalysis: 'all' })));
beforeEach(() => sut = new ClearTextReporter(config({ coverageAnalysis: 'all', clearTextReporter: { logTests: true } })));

describe('onAllMutantsTested() all mutants except error', () => {

Expand All @@ -123,7 +131,7 @@ describe('ClearTextReporter', () => {
});

it('should report on the survived mutant', () => {
expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match('Mutator: Math'));
expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match('1. [Survived] Math'));
expect(process.stdout.write).to.have.been.calledWith(chalk.red('- original line') + os.EOL);
expect(process.stdout.write).to.have.been.calledWith(chalk.green('+ mutated line') + os.EOL);
});
Expand All @@ -138,22 +146,52 @@ describe('ClearTextReporter', () => {

describe('when coverageAnalysis: "perTest"', () => {

beforeEach(() => sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest' })));

describe('onAllMutantsTested()', () => {

it('should log individual ran tests', () => {
it('should log source file names with colored text when clearTextReporter is not false', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest'}));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match(colorizeFileAndPosition('sourceFile.ts', 1, 2)));
});

it('should not log source file names with colored text when clearTextReporter is false', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest'}));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

expect(process.stdout.write).to.have.been.calledWithMatch(colorizeFileAndPosition('sourceFile.ts', 1, 2));
});

it('should not log individual ran tests when logTests is not true', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest'}));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match('Tests ran: '));
expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match(' a test'));
expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match(' a second test'));
expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match(' a third test'));
expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match('Ran all tests for this mutant.'));

});

it('should log individual ran tests when logTests is true', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { logTests: true } }));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match('Tests ran: '));
expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match(' a test'));
expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match(' a second test'));
expect(process.stdout.write).to.have.been.calledWithMatch(sinon.match(' a third test'));
expect(process.stdout.write).to.not.have.been.calledWithMatch(sinon.match('Ran all tests for this mutant.'));
});

describe('with less tests that may be logged', () => {
it('should log less tests', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { maxTestsToLog: 1 } }));
describe('with fewer tests that may be logged', () => {
it('should log fewer tests', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { logTests: true, maxTestsToLog: 1 } }));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

Expand All @@ -166,7 +204,7 @@ describe('ClearTextReporter', () => {

describe('with more tests that may be logged', () => {
it('should log all tests', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { maxTestsToLog: 10 } }));
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { logTests: true, maxTestsToLog: 10 } }));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

Expand All @@ -180,7 +218,7 @@ describe('ClearTextReporter', () => {

describe('with the default amount of tests that may be logged', () => {
it('should log all tests', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { maxTestsToLog: 3 } }));
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { logTests: true, maxTestsToLog: 3 } }));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

Expand All @@ -194,7 +232,7 @@ describe('ClearTextReporter', () => {

describe('with no tests that may be logged', () => {
it('should not log a test', () => {
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { maxTestsToLog: 0 } }));
sut = new ClearTextReporter(config({ coverageAnalysis: 'perTest', clearTextReporter: { logTests: true, maxTestsToLog: 0 } }));

sut.onAllMutantsTested(mutantResults(MutantStatus.Killed, MutantStatus.Survived, MutantStatus.TimedOut, MutantStatus.NoCoverage));

Expand Down Expand Up @@ -236,7 +274,7 @@ describe('ClearTextReporter', () => {
originalLines: 'original line',
range: [0, 0],
replacement: '',
sourceFilePath: '',
sourceFilePath: 'sourceFile.ts',
status,
testsRan: ['a test', 'a second test', 'a third test']
});
Expand Down

0 comments on commit a49829b

Please sign in to comment.