Skip to content

Commit

Permalink
feat(reporter): implement mutationTestReport
Browse files Browse the repository at this point in the history
Implement the new `onMutationTestReportReady` event.
* Maps input according to the [mutation-testing-report-schema](https://github.com/stryker-mutator/mutation-testing-elements/tree/master/packages/mutation-testing-report-schema)
* Support calling the event when mutation test report is ready
  • Loading branch information
nicojs committed Apr 2, 2019
1 parent 044158d commit 16ba76b
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 38 deletions.
19 changes: 6 additions & 13 deletions packages/core/src/Stryker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { MutantResult } from '@stryker-mutator/api/report';
import { MutantTestMatcher } from './mutants/MutantTestMatcher';
import InputFileResolver from './input/InputFileResolver';
import ScoreResultCalculator from './ScoreResultCalculator';
import { isPromise } from './utils/objectUtils';
import { TempFolder } from './utils/TempFolder';
import { MutatorFacade } from './mutants/MutatorFacade';
import InitialTestExecutor from './process/InitialTestExecutor';
Expand All @@ -17,6 +16,8 @@ import { MutantTranspileScheduler } from './transpiler/MutantTranspileScheduler'
import { SandboxPool } from './SandboxPool';
import { Logger } from '@stryker-mutator/api/logging';
import { transpilerFactory } from './transpiler';
import { MutationTestReportCalculator } from './reporters/MutationTestReportCalculator';
import InputFileCollection from './input/InputFileCollection';

export default class Stryker {

Expand Down Expand Up @@ -80,8 +81,7 @@ export default class Stryker {
if (initialRunResult.runResult.tests.length && testableMutants.length) {
const mutationTestExecutor = mutationTestProcessInjector.injectClass(MutationTestExecutor);
const mutantResults = await mutationTestExecutor.run(testableMutants);
this.reportScore(mutantResults);
await this.wrapUpReporter();
await this.reportScore(mutantResults, inputFileInjector);
await TempFolder.instance().clean();
await this.logDone();
await LogConfigurator.shutdown();
Expand All @@ -93,15 +93,6 @@ export default class Stryker {
return Promise.resolve([]);
}

private wrapUpReporter(): Promise<void> {
const maybePromise = this.reporter.wrapUp();
if (isPromise(maybePromise)) {
return maybePromise;
} else {
return Promise.resolve();
}
}

private logDone() {
this.log.info('Done in %s.', this.timer.humanReadableElapsed());
}
Expand All @@ -112,10 +103,12 @@ export default class Stryker {
}
}

private reportScore(mutantResults: MutantResult[]) {
private async reportScore(mutantResults: MutantResult[], inputFileInjector: Injector<MainContext & { inputFiles: InputFileCollection }>) {
inputFileInjector.injectClass(MutationTestReportCalculator).report(mutantResults);
const calculator = this.injector.injectClass(ScoreResultCalculator);
const score = calculator.calculate(mutantResults);
this.reporter.onScoreCalculated(score);
calculator.determineExitCode(score, this.options.thresholds);
await this.reporter.wrapUp();
}
}
25 changes: 9 additions & 16 deletions packages/core/src/reporters/BroadcastReporter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult } from '@stryker-mutator/api/report';
import { Reporter, SourceFile, MutantResult, MatchedMutant, ScoreResult, mutationTestReportSchema } from '@stryker-mutator/api/report';
import { Logger } from '@stryker-mutator/api/logging';
import { isPromise } from '../utils/objectUtils';
import StrictReporter from './StrictReporter';
import { commonTokens, PluginKind } from '@stryker-mutator/api/plugin';
import { StrykerOptions } from '@stryker-mutator/api/core';
Expand Down Expand Up @@ -48,27 +47,17 @@ export default class BroadcastReporter implements StrictReporter {
}
}

private broadcast(methodName: keyof Reporter, eventArgs: any): Promise<any> | void {
const allPromises: Promise<void>[] = [];
Object.keys(this.reporters).forEach(reporterName => {
private broadcast(methodName: keyof Reporter, eventArgs: any): Promise<any> {
return Promise.all(Object.keys(this.reporters).map(async reporterName => {
const reporter = this.reporters[reporterName];
if (typeof reporter[methodName] === 'function') {
try {
const maybePromise = (reporter[methodName] as any)(eventArgs);
if (isPromise(maybePromise)) {
allPromises.push(maybePromise.catch(error => {
this.handleError(error, methodName, reporterName);
}));
}
await (reporter[methodName] as any)(eventArgs);
} catch (error) {
this.handleError(error, methodName, reporterName);
}
}

});
if (allPromises.length) {
return Promise.all(allPromises);
}
}));
}

public onSourceFileRead(file: SourceFile): void {
Expand All @@ -91,6 +80,10 @@ export default class BroadcastReporter implements StrictReporter {
this.broadcast('onAllMutantsTested', results);
}

public onMutationTestReportReady(report: mutationTestReportSchema.MutationTestResult): void {
this.broadcast('onMutationTestReportReady', report);
}

public onScoreCalculated(score: ScoreResult): void {
this.broadcast('onScoreCalculated', score);
}
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/reporters/EventRecorderReporter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as path from 'path';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult } from '@stryker-mutator/api/report';
import { SourceFile, MutantResult, MatchedMutant, Reporter, ScoreResult, mutationTestReportSchema } from '@stryker-mutator/api/report';
import { cleanFolder } from '../utils/fileUtils';
import StrictReporter from './StrictReporter';
import { fsAsPromised } from '@stryker-mutator/util';
Expand Down Expand Up @@ -74,6 +74,10 @@ export default class EventRecorderReporter implements StrictReporter {
this.work('onScoreCalculated', score);
}

public onMutationTestReportReady(report: mutationTestReportSchema.MutationTestResult): void {
this.work('onMutationTestReportReady', report);
}

public onAllMutantsTested(results: MutantResult[]): void {
this.work('onAllMutantsTested', results);
}
Expand Down
117 changes: 117 additions & 0 deletions packages/core/src/reporters/MutationTestReportCalculator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import * as path from 'path';
import { MutantResult, mutationTestReportSchema, Reporter, MutantStatus } from '@stryker-mutator/api/report';
import { StrykerOptions, Location, Position } from '@stryker-mutator/api/core';
import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { coreTokens } from '../di';
import { Logger } from '@stryker-mutator/api/logging';
import InputFileCollection from '../input/InputFileCollection';
import { normalizeWhitespaces } from '@stryker-mutator/util';

export class MutationTestReportCalculator {

public static inject = tokens(coreTokens.reporter, commonTokens.options, coreTokens.inputFiles, commonTokens.logger);

constructor(
private readonly reporter: Required<Reporter>,
private readonly options: StrykerOptions,
private readonly files: InputFileCollection,
private readonly log: Logger
) { }

public report(results: ReadonlyArray<MutantResult>) {
this.reporter.onMutationTestReportReady(this.mutationTestReport(results));
}

private mutationTestReport(results: ReadonlyArray<MutantResult>): mutationTestReportSchema.MutationTestResult {
return {
files: this.toFileResults(results),
schemaVersion: '1.0',
thresholds: this.options.thresholds
};
}

private toFileResults(results: ReadonlyArray<MutantResult>): mutationTestReportSchema.FileResultDictionary {
const files: mutationTestReportSchema.FileResultDictionary = Object.create(null);
results.forEach(mutantResult => {
const fileResult = files[mutantResult.sourceFilePath];
if (fileResult) {
fileResult.mutants.push(this.toMutantResult(mutantResult));
} else {
const sourceFile = this.files.files.find(file => file.name === mutantResult.sourceFilePath);
if (sourceFile) {
files[mutantResult.sourceFilePath] = {
language: this.determineLanguage(sourceFile.name),
mutants: [this.toMutantResult(mutantResult)],
source: sourceFile.textContent
};
} else {
this.log.warn(normalizeWhitespaces(`File "${mutantResult.sourceFilePath}" not found
in input files, but did receive mutant result for it. This shouldn't happen`));
}
}
});
return files;
}

public determineLanguage(name: string): string {
const ext = path.extname(name).toLowerCase();
switch (ext) {
case '.ts':
case '.tsx':
return 'typescript';
case '.html':
case '.vue':
return 'html';
default:
return 'javascript';
}
}

private toMutantResult(mutantResult: MutantResult): mutationTestReportSchema.MutantResult {
return {
id: mutantResult.id,
location: this.toLocation(mutantResult.location),
mutatorName: mutantResult.mutatorName,
replacement: mutantResult.replacement,
status: this.toStatus(mutantResult.status)
};
}

private toLocation(location: Location): mutationTestReportSchema.Location {
return {
end: this.toPosition(location.end),
start: this.toPosition(location.start)
};
}

private toPosition(pos: Position): mutationTestReportSchema.Position {
return {
column: pos.column + 1, // convert from 0-based to 1-based
line: pos.line + 1
};
}

private toStatus(status: MutantStatus): mutationTestReportSchema.MutantStatus {
switch (status) {
case MutantStatus.Killed:
return mutationTestReportSchema.MutantStatus.Killed;
case MutantStatus.NoCoverage:
return mutationTestReportSchema.MutantStatus.NoCoverage;
case MutantStatus.RuntimeError:
return mutationTestReportSchema.MutantStatus.RuntimeError;
case MutantStatus.Survived:
return mutationTestReportSchema.MutantStatus.Survived;
case MutantStatus.TimedOut:
return mutationTestReportSchema.MutantStatus.Timeout;
case MutantStatus.TranspileError:
return mutationTestReportSchema.MutantStatus.CompileError;
default:
this.logUnsupportedMutantStatus(status);
return mutationTestReportSchema.MutantStatus.RuntimeError;
}
}

private logUnsupportedMutantStatus(status: never) {
this.log.warn('Unable to convert "%s" to a MutantStatus', status);
}
}
4 changes: 0 additions & 4 deletions packages/core/src/utils/objectUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ export function freezeRecursively<T extends { [prop: string]: any }>(
return target;
}

export function isPromise(input: any): input is Promise<any> {
return input && typeof input.then === 'function';
}

export function filterEmpty<T>(input: (T | null | void)[]) {
return input.filter(item => item !== undefined && item !== null) as T[];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/helpers/producers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ export const strykerOptions = factoryMethod<StrykerOptions>(() => ({
export const config = factoryMethod<Config>(() => new Config());

export const ALL_REPORTER_EVENTS: (keyof Reporter)[] =
['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onScoreCalculated', 'wrapUp'];
['onSourceFileRead', 'onAllSourceFilesRead', 'onAllMutantsMatchedWithTests', 'onMutantTested', 'onAllMutantsTested', 'onMutationTestReportReady', 'onScoreCalculated', 'wrapUp'];

export function matchedMutant(numberOfTests: number, mutantId = numberOfTests.toString()): MatchedMutant {
const scopedTestIds: number[] = [];
Expand Down
10 changes: 10 additions & 0 deletions packages/core/test/unit/Stryker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import * as di from '../../src/di';
import Timer from '../../src/utils/Timer';
import { Logger } from '@stryker-mutator/api/logging';
import { Transpiler } from '@stryker-mutator/api/transpile';
import { MutationTestReportCalculator } from '../../src/reporters/MutationTestReportCalculator';

const LOGGING_CONTEXT: LoggingClientContext = Object.freeze({
level: LogLevel.Debug,
Expand All @@ -45,6 +46,7 @@ describe(Stryker.name, () => {
let reporterMock: Mock<BroadcastReporter>;
let tempFolderMock: Mock<TempFolder>;
let scoreResultCalculator: ScoreResultCalculator;
let mutationTestReportCalculatorMock: Mock<MutationTestReportCalculator>;
let configureMainProcessStub: sinon.SinonStub;
let configureLoggingServerStub: sinon.SinonStub;
let shutdownLoggingStub: sinon.SinonStub;
Expand Down Expand Up @@ -72,6 +74,7 @@ describe(Stryker.name, () => {
timerMock = sinon.createStubInstance(Timer);
tempFolderMock = mock(TempFolder as any);
tempFolderMock.clean.resolves();
mutationTestReportCalculatorMock = mock(MutationTestReportCalculator);
scoreResultCalculator = new ScoreResultCalculator(testInjector.logger);
sinon.stub(di, 'buildMainInjector').returns(injectorMock);
sinon.stub(TempFolder, 'instance').returns(tempFolderMock);
Expand All @@ -83,6 +86,7 @@ describe(Stryker.name, () => {
.withArgs(MutatorFacade).returns(mutatorMock)
.withArgs(MutantTestMatcher).returns(mutantTestMatcherMock)
.withArgs(MutationTestExecutor).returns(mutationTestExecutorMock)
.withArgs(MutationTestReportCalculator).returns(mutationTestReportCalculatorMock)
.withArgs(ScoreResultCalculator).returns(scoreResultCalculator);
injectorMock.resolve
.withArgs(commonTokens.options).returns(strykerConfig)
Expand Down Expand Up @@ -208,6 +212,12 @@ describe(Stryker.name, () => {
expect(scoreResultCalculator.determineExitCode).called;
});

it('should report mutation test report ready', async () => {
sut = new Stryker({});
await sut.runMutationTest();
expect(mutationTestReportCalculatorMock.report).called;
});

it('should create the InputFileResolver', async () => {
sut = new Stryker({});
await sut.runMutationTest();
Expand Down
Loading

0 comments on commit 16ba76b

Please sign in to comment.