From a49829b7627cba2f2839aba684f5603b37bc49c6 Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Sun, 28 Oct 2018 08:28:13 -0400 Subject: [PATCH] feat(clear text reporter): Prettify the clear-text report (#1185) 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 } ``` --- packages/stryker/README.md | 5 +- .../src/reporters/ClearTextReporter.ts | 46 +++++++++----- .../unit/reporters/ClearTextReporterSpec.ts | 62 +++++++++++++++---- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/packages/stryker/README.md b/packages/stryker/README.md index 4be711e2cc..df27579880 100644 --- a/packages/stryker/README.md +++ b/packages/stryker/README.md @@ -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: diff --git a/packages/stryker/src/reporters/ClearTextReporter.ts b/packages/stryker/src/reporters/ClearTextReporter.ts index d69601f5ed..78df319858 100644 --- a/packages/stryker/src/reporters/ClearTextReporter.ts +++ b/packages/stryker/src/reporters/ClearTextReporter.ts @@ -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'; @@ -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)); }); @@ -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; } diff --git a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts index fa525c4a47..0ba5118a97 100644 --- a/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts +++ b/packages/stryker/test/unit/reporters/ClearTextReporterSpec.ts @@ -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; @@ -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', () => { @@ -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); }); @@ -138,12 +146,42 @@ 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')); @@ -151,9 +189,9 @@ describe('ClearTextReporter', () => { 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)); @@ -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)); @@ -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)); @@ -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)); @@ -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'] });