diff --git a/.vscode/launch.json b/.vscode/launch.json index a98f27e383..abf6f5188c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -78,7 +78,7 @@ // "preLaunchTask": "build", "stopOnEntry": false, "args": [ - "--configFile", + "run", "stryker.conf.js" ], "cwd": "${workspaceRoot}", diff --git a/package.json b/package.json index 7847bd3f2f..260a3fc788 100644 --- a/package.json +++ b/package.json @@ -6,8 +6,7 @@ "typings": "src/Stryker.d.ts", "scripts": { "test": "grunt test", - "start": "concurrently \"npm run tsc:w\" \"grunt serve\"", - "tsc:w": "tsc -w", + "start": "tsc -w", "sample": "node src/stryker-cli.js --configFile testResources/sampleProject/stryker.conf.js", "preversion": "grunt" }, @@ -34,7 +33,8 @@ "Nico Stapelbroek ", "Jeremy Nagel ", "Michael Williamson ", - "Philipp Weissenbacher " + "Philipp Weissenbacher ", + "Alex van Assem { @@ -28,6 +30,24 @@ export default class MutantTestMatcher { } }); }); + + this.reporter.onAllMutantsMatchedWithTests(Object.freeze(this.mutants.map(this.mapMutantOnMatchedMutant))); + } + + /** + * Map the Mutant object on the MatchMutant Object. + * @param mutant The mutant. + * @returns The MatchedMutant + */ + private mapMutantOnMatchedMutant(mutant: Mutant): MatchedMutant { + const matchedMutant = _.cloneDeep({ + mutatorName: mutant.mutatorName, + scopedTestIds: mutant.scopedTestIds, + timeSpentScopedTests: mutant.timeSpentScopedTests, + filename: mutant.filename, + replacement: mutant.replacement + }); + return Object.freeze(matchedMutant); } /** diff --git a/src/ReporterOrchestrator.ts b/src/ReporterOrchestrator.ts index e75caa283e..6966f47422 100644 --- a/src/ReporterOrchestrator.ts +++ b/src/ReporterOrchestrator.ts @@ -2,6 +2,7 @@ import {StrykerOptions} from 'stryker-api/core'; import {Reporter, ReporterFactory} from 'stryker-api/report'; import ClearTextReporter from './reporters/ClearTextReporter'; import ProgressReporter from './reporters/ProgressReporter'; +import DotsReporter from './reporters/DotsReporter'; import EventRecorderReporter from './reporters/EventRecorderReporter'; import BroadcastReporter, {NamedReporter} from './reporters/BroadcastReporter'; import * as log4js from 'log4js'; @@ -44,6 +45,7 @@ export default class ReporterOrchestrator { private registerDefaultReporters() { ReporterFactory.instance().register('progress', ProgressReporter); + ReporterFactory.instance().register('dots', DotsReporter); ReporterFactory.instance().register('clear-text', ClearTextReporter); ReporterFactory.instance().register('event-recorder', EventRecorderReporter); } diff --git a/src/Stryker.ts b/src/Stryker.ts index f3c80ecdb3..3be1df0575 100644 --- a/src/Stryker.ts +++ b/src/Stryker.ts @@ -138,7 +138,7 @@ export default class Stryker { .filter(inputFile => inputFile.mutated) .map(file => file.path)); log.info(`${mutants.length} Mutant(s) generated`); - let mutantRunResultMatcher = new MutantTestMatcher(mutants, runResult, this.coverageInstrumenter.retrieveStatementMapsPerFile(), this.config); + let mutantRunResultMatcher = new MutantTestMatcher(mutants, runResult, this.coverageInstrumenter.retrieveStatementMapsPerFile(), this.config, this.reporter); mutantRunResultMatcher.matchWithMutants(); return mutants; } diff --git a/src/reporters/BroadcastReporter.ts b/src/reporters/BroadcastReporter.ts index edee4f21e4..8ad563a680 100644 --- a/src/reporters/BroadcastReporter.ts +++ b/src/reporters/BroadcastReporter.ts @@ -11,7 +11,7 @@ export interface NamedReporter { reporter: Reporter; } -export const ALL_EVENT_METHOD_NAMES = ['onSourceFileRead', 'onAllSourceFilesRead', 'onMutantTested', 'onAllMutantsTested', 'onConfigRead']; +export const ALL_EVENT_METHOD_NAMES = ['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onConfigRead']; export default class BroadcastReporter implements Reporter { diff --git a/src/reporters/DotsReporter.ts b/src/reporters/DotsReporter.ts new file mode 100644 index 0000000000..1c29c201b3 --- /dev/null +++ b/src/reporters/DotsReporter.ts @@ -0,0 +1,31 @@ +import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; +import * as chalk from 'chalk'; +import * as os from 'os'; + +export default class DotsReporter implements Reporter { + onMutantTested(result: MutantResult) { + let toLog: string; + switch (result.status) { + case MutantStatus.Killed: + toLog = '.'; + break; + case MutantStatus.TimedOut: + toLog = chalk.yellow('T'); + break; + case MutantStatus.Survived: + toLog = chalk.bold.red('S'); + break; + case MutantStatus.Error: + toLog = chalk.yellow('E'); + break; + default: + toLog = ''; + break; + } + process.stdout.write(toLog); + } + + onAllMutantsTested(): void { + process.stdout.write(os.EOL); + } +} \ No newline at end of file diff --git a/src/reporters/ProgressBar.ts b/src/reporters/ProgressBar.ts new file mode 100644 index 0000000000..2677ed5c2c --- /dev/null +++ b/src/reporters/ProgressBar.ts @@ -0,0 +1,3 @@ +import ProgressBar = require('progress'); + +export default ProgressBar; \ No newline at end of file diff --git a/src/reporters/ProgressReporter.ts b/src/reporters/ProgressReporter.ts index ca2b6b912a..5255eaa926 100644 --- a/src/reporters/ProgressReporter.ts +++ b/src/reporters/ProgressReporter.ts @@ -1,31 +1,69 @@ -import {Reporter, MutantResult, MutantStatus} from 'stryker-api/report'; +import { Reporter, MatchedMutant, MutantResult, MutantStatus } from 'stryker-api/report'; +import ProgressBar from './ProgressBar'; import * as chalk from 'chalk'; -import * as os from 'os'; export default class ProgressReporter implements Reporter { - onMutantTested(result: MutantResult) { - let toLog: string; + private progressBar: ProgressBar; + + // tickValues contains Labels, because on initation of the ProgressBar the width is determined based on the amount of characters of the progressBarContent inclusive ASCII-codes for colors + private tickValues = { + error: 0, + survived: 0, + killed: 0, + timeout: 0, + noCoverage: 0, + killedLabel: `${chalk.green.bold('killed')}`, + survivedLabel: `${chalk.red.bold('survived')}`, + noCoverageLabel: `${chalk.red.bold('no coverage')}`, + timeoutLabel: `${chalk.yellow.bold('timeout')}`, + errorLabel: `${chalk.yellow.bold('error')}` + }; + + onAllMutantsMatchedWithTests(matchedMutants: ReadonlyArray): void { + let progressBarContent: string; + + progressBarContent = + `Mutation testing [:bar] :percent (ETC :etas)` + + `[:killed :killedLabel] ` + + `[:survived :survivedLabel] ` + + `[:noCoverage :noCoverageLabel] ` + + `[:timeout :timeoutLabel] ` + + `[:error :errorLabel]`; + + this.progressBar = new ProgressBar(progressBarContent, { + width: 50, + complete: '=', + incomplete: ' ', + total: matchedMutants.filter(m => m.scopedTestIds.length > 0).length + }); + } + + onMutantTested(result: MutantResult): void { switch (result.status) { - case MutantStatus.Killed: - toLog = '.'; + case MutantStatus.NoCoverage: + this.tickValues.noCoverage++; + this.progressBar.render(this.tickValues); break; - case MutantStatus.TimedOut: - toLog = chalk.yellow('T'); + case MutantStatus.Killed: + this.tickValues.killed++; + this.tick(); break; case MutantStatus.Survived: - toLog = chalk.bold.red('S'); + this.tickValues.survived++; + this.tick(); break; - case MutantStatus.Error: - toLog = chalk.yellow('E'); + case MutantStatus.TimedOut: + this.tickValues.timeout++; + this.tick(); break; - default: - toLog = ''; + case MutantStatus.Error: + this.tickValues.error++; + this.tick(); break; } - process.stdout.write(toLog); } - onAllMutantsTested(): void { - process.stdout.write(os.EOL); + tick(): void { + this.progressBar.tick(this.tickValues); } } \ No newline at end of file diff --git a/test/unit/MutantTestMatcherSpec.ts b/test/unit/MutantTestMatcherSpec.ts index fe3cd00338..c08825a322 100644 --- a/test/unit/MutantTestMatcherSpec.ts +++ b/test/unit/MutantTestMatcherSpec.ts @@ -5,6 +5,7 @@ import { StrykerOptions } from 'stryker-api/core'; import { StatementMapDictionary } from '../../src/coverage/CoverageInstrumenter'; import MutantTestMatcher from '../../src/MutantTestMatcher'; import Mutant from '../../src/Mutant'; +import { Reporter, MatchedMutant } from 'stryker-api/report'; describe('MutantTestMatcher', () => { @@ -13,25 +14,48 @@ describe('MutantTestMatcher', () => { let runResult: RunResult; let statementMapDictionary: StatementMapDictionary; let strykerOptions: StrykerOptions; + let reporter: Reporter; beforeEach(() => { mutants = []; statementMapDictionary = Object.create(null); runResult = { tests: [], status: RunStatus.Complete }; strykerOptions = {}; - sut = new MutantTestMatcher(mutants, runResult, statementMapDictionary, strykerOptions); + reporter = { onAllMutantsMatchedWithTests: sinon.stub() }; + sut = new MutantTestMatcher(mutants, runResult, statementMapDictionary, strykerOptions, reporter); }); describe('with coverageAnalysis: "perTest"', () => { - beforeEach(() => strykerOptions.coverageAnalysis = 'perTest'); + beforeEach(() => { + strykerOptions.coverageAnalysis = 'perTest'; + }); describe('matchWithMutants()', () => { describe('with 2 mutants and 2 testResults', () => { let mutantOne: any, mutantTwo: any, testResultOne: TestResult, testResultTwo: TestResult; beforeEach(() => { - mutantOne = { mutantOne: true, filename: 'fileWithMutantOne', location: { start: { line: 5, column: 6 }, end: { line: 5, column: 6 } }, addTestResult: sinon.stub() }; - mutantTwo = { mutantTwo: true, filename: 'fileWithMutantTwo', location: { start: { line: 10, column: 0 }, end: { line: 10, column: 0 } }, addTestResult: sinon.stub() }; + mutantOne = { + mutatorName: 'myMutator', + mutantOne: true, + filename: 'fileWithMutantOne', + location: { start: { line: 5, column: 6 }, end: { line: 5, column: 6 } }, + replacement: '>', + addTestResult: sinon.stub(), + scopedTestIds: [1, 2], + timeSpentScopedTests: 1, + }; + mutantTwo = { + mutatorName: 'myMutator', + mutantTwo: true, + filename: 'fileWithMutantTwo', + location: { start: { line: 10, column: 0 }, end: { line: 10, column: 0 } }, + replacement: '<', + addTestResult: sinon.stub(), + scopedTestIds: [9, 10], + timeSpentScopedTests: 59, + }; + testResultOne = { status: TestStatus.Success, name: 'test one', @@ -60,6 +84,25 @@ describe('MutantTestMatcher', () => { expect(mutantTwo.addTestResult).to.have.been.calledWith(0, testResultOne); expect(mutantTwo.addTestResult).to.have.been.calledWith(1, testResultTwo); }); + it('should have both mutants matched', () => { + let matchedMutants: MatchedMutant[] = [ + { + mutatorName: mutants[0].mutatorName, + scopedTestIds: mutants[0].scopedTestIds, + timeSpentScopedTests: mutants[0].timeSpentScopedTests, + filename: mutants[0].filename, + replacement: mutants[0].replacement + }, + { + mutatorName: mutants[1].mutatorName, + scopedTestIds: mutants[1].scopedTestIds, + timeSpentScopedTests: mutants[1].timeSpentScopedTests, + filename: mutants[1].filename, + replacement: mutants[1].replacement + } + ]; + expect(reporter.onAllMutantsMatchedWithTests).to.have.been.calledWith(Object.freeze(matchedMutants)); + }); }); describe('without the tests having covered the mutants', () => { diff --git a/test/unit/reporters/DotsReporterSpec.ts b/test/unit/reporters/DotsReporterSpec.ts new file mode 100644 index 0000000000..ca91a50138 --- /dev/null +++ b/test/unit/reporters/DotsReporterSpec.ts @@ -0,0 +1,80 @@ +import DotsReporter from '../../../src/reporters/DotsReporter'; +import * as sinon from 'sinon'; +import {MutantStatus, MutantResult} from 'stryker-api/report'; +import {expect} from 'chai'; +import * as chalk from 'chalk'; +import * as os from 'os'; + +describe('DotsReporter', () => { + + let sut: DotsReporter; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sut = new DotsReporter(); + sandbox = sinon.sandbox.create(); + sandbox.stub(process.stdout, 'write'); + }); + + describe('onMutantTested()', () => { + + describe('when status is KILLED', () => { + + beforeEach(() => { + sut.onMutantTested(mutantResult(MutantStatus.Killed)); + }); + + it('should log "."', () => { + expect(process.stdout.write).to.have.been.calledWith('.'); + }); + }); + + describe('when status is TIMEDOUT', () => { + + beforeEach(() => { + sut.onMutantTested(mutantResult(MutantStatus.TimedOut)); + }); + + it('should log "T"', () => { + expect(process.stdout.write).to.have.been.calledWith(chalk.yellow('T')); + }); + }); + + describe('when status is SURVIVED', () => { + + beforeEach(() => { + sut.onMutantTested(mutantResult(MutantStatus.Survived)); + }); + + it('should log "S"', () => { + expect(process.stdout.write).to.have.been.calledWith(chalk.bold.red('S')); + }); + }); + }); + + describe('onAllMutantsTested()', () => { + it('should write a new line', () => { + sut.onAllMutantsTested(); + expect(process.stdout.write).to.have.been.calledWith(os.EOL); + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + function mutantResult(status: MutantStatus): MutantResult { + return { + location: null, + mutatedLines: null, + mutatorName: null, + originalLines: null, + replacement: null, + sourceFilePath: null, + testsRan: null, + status, + range: null + }; + } + +}); diff --git a/test/unit/reporters/ProgressReporterSpec.ts b/test/unit/reporters/ProgressReporterSpec.ts index 9b8b1c9e3c..9512567276 100644 --- a/test/unit/reporters/ProgressReporterSpec.ts +++ b/test/unit/reporters/ProgressReporterSpec.ts @@ -1,22 +1,69 @@ import ProgressReporter from '../../../src/reporters/ProgressReporter'; +import * as progressBarModule from '../../../src/reporters/ProgressBar'; +import { MutantStatus, MutantResult, MatchedMutant } from 'stryker-api/report'; +import { expect } from 'chai'; import * as sinon from 'sinon'; -import {MutantStatus, MutantResult} from 'stryker-api/report'; -import {expect} from 'chai'; import * as chalk from 'chalk'; -import * as os from 'os'; describe('ProgressReporter', () => { let sut: ProgressReporter; let sandbox: sinon.SinonSandbox; + let matchedMutants: MatchedMutant[]; + let progressBar: { tick: sinon.SinonStub, render: sinon.SinonStub }; + const progressBarContent: string = + `Mutation testing [:bar] :percent (ETC :etas)` + + `[:killed :killedLabel] ` + + `[:survived :survivedLabel] ` + + `[:noCoverage :noCoverageLabel] ` + + `[:timeout :timeoutLabel] ` + + `[:error :errorLabel]`; + let progressBarOptions: any; beforeEach(() => { sut = new ProgressReporter(); sandbox = sinon.sandbox.create(); - sandbox.stub(process.stdout, 'write'); + progressBar = { tick: sandbox.stub(), render: sandbox.stub() }; + sandbox.stub(progressBarModule, 'default').returns(progressBar); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('onAllMutantsMatchedWithTests()', () => { + describe('when there are 3 MatchedMutants that all contain Tests', () => { + beforeEach(() => { + matchedMutants = [matchedMutant(1), matchedMutant(4), matchedMutant(2)]; + + sut.onAllMutantsMatchedWithTests(matchedMutants); + }); + + it('the total of MatchedMutants in the progressbar should be 3', () => { + expect(progressBarModule.default).to.have.been.calledWithMatch(progressBarContent, { total: 3 }); + }); + }); + describe('when there are 2 MatchedMutants that all contain Tests and 1 MatchMutant that doesnt have tests', () => { + beforeEach(() => { + matchedMutants = [matchedMutant(1), matchedMutant(0), matchedMutant(2)]; + + sut.onAllMutantsMatchedWithTests(matchedMutants); + }); + + it('the total of MatchedMutants in the progressbar should be 2', () => { + expect(progressBarModule.default).to.have.been.calledWithMatch(progressBarContent, { total: 2 }); + }); + }); }); describe('onMutantTested()', () => { + let progressBarTickTokens: any; + + beforeEach(() => { + matchedMutants = [matchedMutant(1), matchedMutant(4), matchedMutant(2)]; + + sut.onAllMutantsMatchedWithTests(matchedMutants); + }); describe('when status is KILLED', () => { @@ -24,8 +71,9 @@ describe('ProgressReporter', () => { sut.onMutantTested(mutantResult(MutantStatus.Killed)); }); - it('should log "."', () => { - expect(process.stdout.write).to.have.been.calledWith('.'); + it('should tick the ProgressBar with 1 killed mutant', () => { + progressBarTickTokens = { error: 0, killed: 1, noCoverage: 0, survived: 0, timeout: 0 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); }); }); @@ -35,8 +83,9 @@ describe('ProgressReporter', () => { sut.onMutantTested(mutantResult(MutantStatus.TimedOut)); }); - it('should log "T"', () => { - expect(process.stdout.write).to.have.been.calledWith(chalk.yellow('T')); + it('should tick the ProgressBar with 1 timed out mutant', () => { + progressBarTickTokens = { error: 0, killed: 0, noCoverage: 0, survived: 0, timeout: 1 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); }); }); @@ -46,35 +95,66 @@ describe('ProgressReporter', () => { sut.onMutantTested(mutantResult(MutantStatus.Survived)); }); - it('should log "S"', () => { - expect(process.stdout.write).to.have.been.calledWith(chalk.bold.red('S')); + it('should tick the ProgressBar with 1 survived mutant', () => { + progressBarTickTokens = { error: 0, killed: 0, noCoverage: 0, survived: 1, timeout: 0 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); }); }); - }); - describe('onAllMutantsTested()', () => { - it('should write a new line', () => { - sut.onAllMutantsTested(); - expect(process.stdout.write).to.have.been.calledWith(os.EOL); + describe('when status is ERRORED', () => { + + beforeEach(() => { + sut.onMutantTested(mutantResult(MutantStatus.Error)); + }); + + it('should tick the ProgressBar with 1 errored mutant', () => { + progressBarTickTokens = { error: 1, killed: 0, noCoverage: 0, survived: 0, timeout: 0 }; + expect(progressBar.tick).to.have.been.calledWithMatch(progressBarTickTokens); + }); }); - }); - afterEach(() => { - sandbox.restore(); - }); + describe('when status is NO COVERAGE', () => { - function mutantResult(status: MutantStatus): MutantResult { - return { - location: null, - mutatedLines: null, - mutatorName: null, - originalLines: null, - replacement: null, - sourceFilePath: null, - testsRan: null, - status, - range: null - }; - } + beforeEach(() => { + sut.onMutantTested(mutantResult(MutantStatus.NoCoverage)); + }); + + it('should not tick the ProgressBar', () => { + expect(progressBar.tick).to.not.have.been.called; + }); + it('should render the ProgressBar', () => { + progressBarTickTokens = { error: 0, killed: 0, noCoverage: 1, survived: 0, timeout: 0 }; + expect(progressBar.render).to.have.been.calledWithMatch(progressBarTickTokens); + }); + }); + }); }); + +function mutantResult(status: MutantStatus): MutantResult { + return { + location: null, + mutatedLines: null, + mutatorName: null, + originalLines: null, + replacement: null, + sourceFilePath: null, + testsRan: null, + status, + range: null + }; +} + +function matchedMutant(numberOfTests: number): MatchedMutant { + let scopedTestIds: number[] = []; + for (let i = 0; i < numberOfTests; i++) { + scopedTestIds.push(1); + } + return { + mutatorName: null, + scopedTestIds: scopedTestIds, + timeSpentScopedTests: null, + filename: null, + replacement: null + }; +}