Skip to content

Commit

Permalink
feat(progress-reporter): Create new progress reporter (#202)
Browse files Browse the repository at this point in the history
* Rename current `'progress'` reporter to `'dots'` reporter
* Add new fancy progress-reporter using a progress bar
* chore(npm-start): Update npm start command
  • Loading branch information
avassem85 authored and nicojs committed Dec 29, 2016
1 parent 7d05482 commit 11c345e
Show file tree
Hide file tree
Showing 12 changed files with 360 additions and 61 deletions.
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
// "preLaunchTask": "build",
"stopOnEntry": false,
"args": [
"--configFile",
"run",
"stryker.conf.js"
],
"cwd": "${workspaceRoot}",
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -34,7 +33,8 @@
"Nico Stapelbroek <[email protected]>",
"Jeremy Nagel <[email protected]>",
"Michael Williamson <[email protected]>",
"Philipp Weissenbacher <[email protected]>"
"Philipp Weissenbacher <[email protected]>",
"Alex van Assem <[email protected]"
],
"license": "Apache-2.0",
"bin": {
Expand All @@ -51,6 +51,7 @@
"lodash": "^3.10.1",
"log4js": "^0.6.33",
"mkdirp": "^0.5.1",
"progress": "^1.1.8",
"serialize-javascript": "^1.3.0"
},
"devDependencies": {
Expand All @@ -66,6 +67,7 @@
"@types/log4js": "0.0.32",
"@types/mkdirp": "^0.3.28",
"@types/mocha": "^2.2.34",
"@types/progress": "^1.1.28",
"@types/sinon": "^1.16.33",
"@types/sinon-as-promised": "^4.0.5",
"@types/sinon-chai": "^2.7.27",
Expand Down Expand Up @@ -93,11 +95,11 @@
"sinon": "^1.17.2",
"sinon-as-promised": "^4.0.2",
"sinon-chai": "^2.8.0",
"stryker-api": "^0.4.1",
"stryker-api": "^0.4.2-rc1",
"tslint": "^3.15.1",
"typescript": "^2.1.4"
},
"peerDependencies": {
"stryker-api": "^0.4.0"
"stryker-api": "^0.4.2-rc1"
}
}
22 changes: 21 additions & 1 deletion src/MutantTestMatcher.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Mutant from './Mutant';
import { Reporter, MatchedMutant } from 'stryker-api/report';
import { StatementMapDictionary } from './coverage/CoverageInstrumenter';
import { RunResult, CoverageCollection, CoverageCollectionPerTest, CoverageResult, StatementMap } from 'stryker-api/test_runner';
import { Location, StrykerOptions } from 'stryker-api/core';
import * as _ from 'lodash';

export default class MutantTestMatcher {

constructor(private mutants: Mutant[], private initialRunResult: RunResult, private statementMaps: StatementMapDictionary, private options: StrykerOptions) { }
constructor(private mutants: Mutant[], private initialRunResult: RunResult, private statementMaps: StatementMapDictionary, private options: StrykerOptions, private reporter: Reporter) { }

matchWithMutants() {
this.mutants.forEach(mutant => {
Expand All @@ -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);
}

/**
Expand Down
2 changes: 2 additions & 0 deletions src/ReporterOrchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion src/Stryker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion src/reporters/BroadcastReporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
31 changes: 31 additions & 0 deletions src/reporters/DotsReporter.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions src/reporters/ProgressBar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ProgressBar = require('progress');

export default ProgressBar;
70 changes: 54 additions & 16 deletions src/reporters/ProgressReporter.ts
Original file line number Diff line number Diff line change
@@ -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<MatchedMutant>): 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);
}
}
51 changes: 47 additions & 4 deletions test/unit/MutantTestMatcherSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {

Expand All @@ -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',
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading

0 comments on commit 11c345e

Please sign in to comment.