Skip to content

Commit

Permalink
feat(reporter-api): support mutation switching
Browse files Browse the repository at this point in the history
Update the `Reporter` api in order to support mutation switching.

* Implement `MutantResult` as a union type
* Add `killedBy` to `KilledMutantResults`.
* Add `errorMessage` to `InvalidMutantResult`.
* Add the `testFilter` to `InvalidMutantResult`.
These names are in line with the [stryker handbook](https://github.com/stryker-mutator/stryker-handbook/blob/master/mutant-states-and-metrics.md).

Renamed `sourceFilePath` to `fileName` to make it more inline with the naming in the rest of the api.

BREAKING CHANGE: The `onMutantTested` and `onAllMutantsTested` methods on the `Reporter` api have changed
  • Loading branch information
nicojs committed Aug 21, 2020
1 parent 9c2917f commit 67f1ed5
Show file tree
Hide file tree
Showing 14 changed files with 537 additions and 413 deletions.
2 changes: 1 addition & 1 deletion packages/api/report.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as mutationTestReportSchema from 'mutation-testing-report-schema/dist/src/api';

export { default as Reporter } from './src/report/Reporter';
export { default as MutantResult } from './src/report/MutantResult';
export * from './src/report/MutantResult';
export { default as MutantStatus } from './src/report/MutantStatus';
export { default as SourceFile } from './src/report/SourceFile';
export { default as MatchedMutant } from './src/report/MatchedMutant';
Expand Down
28 changes: 23 additions & 5 deletions packages/api/src/report/MutantResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@ import { Location, Range } from '../../core';

import MutantStatus from './MutantStatus';

interface MutantResult {
export interface BaseMutantResult {
id: string;
sourceFilePath: string;
fileName: string;
mutatorName: string;
status: MutantStatus;
replacement: string;
originalLines: string;
mutatedLines: string;
testsRan: string[];
nrOfTestsRan: number;
location: Location;
range: Range;
}

export default MutantResult;
export interface KilledMutantResult extends BaseMutantResult {
status: MutantStatus.Killed;
killedBy: string;
}

export interface InvalidMutantResult extends BaseMutantResult {
status: MutantStatus.RuntimeError | MutantStatus.CompileError;
errorMessage: string;
}

export interface UndetectedMutantResult extends BaseMutantResult {
status: MutantStatus.NoCoverage | MutantStatus.Survived;
testFilter: string[] | undefined;
}

export interface TimeoutMutantResult extends BaseMutantResult {
status: MutantStatus.TimedOut;
}

export type MutantResult = TimeoutMutantResult | UndetectedMutantResult | InvalidMutantResult | KilledMutantResult;
17 changes: 5 additions & 12 deletions packages/api/src/report/MutantStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,21 @@ enum MutantStatus {

/**
* The status of a mutant of which the tests resulted in a runtime error.
*
* For example: the following piece of javascript code will result in a runtime error:
*
* @example
* ```javascript
* const fs = require('f' - 's'); // mutated code
* ```
*
* Mutants that result in a runtime error are not taken into account during score calculation.
*/
RuntimeError,

/**
* The status of a mutant which could not be transpiled.
* For example: the following piece of typescript code will give a TranspileError:
*
* The status of a mutant which could not be compiled.
* @example
* ```typescript
* const a: 5 = 0; // mutated code
* const foo = 'foo' - 'bar'; // mutated code
* ```
*
* Mutants that result in a TranspileError are not taken into account during score calculation.
*/
TranspileError,
CompileError,
}

export default MutantStatus;
2 changes: 1 addition & 1 deletion packages/api/src/report/Reporter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { MutationTestResult } from 'mutation-testing-report-schema';

import MatchedMutant from './MatchedMutant';
import MutantResult from './MutantResult';
import { MutantResult } from './MutantResult';
import SourceFile from './SourceFile';

/**
Expand Down
38 changes: 8 additions & 30 deletions packages/core/src/process/4-MutationTestExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import { from, zip, partition, merge, Observable } from 'rxjs';
import { flatMap, toArray, map, tap, shareReplay } from 'rxjs/operators';
import { tokens, commonTokens } from '@stryker-mutator/api/plugin';
import { StrykerOptions } from '@stryker-mutator/api/core';
import { MutantResult, MutantStatus } from '@stryker-mutator/api/report';
import { MutantRunOptions, MutantRunStatus, TestRunner2 } from '@stryker-mutator/api/test_runner2';
import { MutantResult } from '@stryker-mutator/api/report';
import { MutantRunOptions, TestRunner2 } from '@stryker-mutator/api/test_runner2';
import { Logger } from '@stryker-mutator/api/logging';
import { I } from '@stryker-mutator/util';

import { CheckStatus, Checker } from '@stryker-mutator/api/check';
import { CheckStatus, Checker, CheckResult, PassedCheckResult } from '@stryker-mutator/api/check';

import { coreTokens } from '../di';
import StrictReporter from '../reporters/StrictReporter';
Expand Down Expand Up @@ -71,9 +71,7 @@ export class MutationTestExecutor {
private executeNoCoverage(input$: Observable<MutantTestCoverage>) {
const [coveredMutant$, noCoverageMatchedMutant$] = partition(input$.pipe(shareReplay()), (matchedMutant) => matchedMutant.coveredByTests);
return {
noCoverageResult$: noCoverageMatchedMutant$.pipe(
map((noCoverage) => this.mutationTestReportHelper.reportOne(noCoverage, MutantStatus.NoCoverage))
),
noCoverageResult$: noCoverageMatchedMutant$.pipe(map(({ mutant }) => this.mutationTestReportHelper.reportNoCoverage(mutant))),
coveredMutant$,
};
}
Expand Down Expand Up @@ -103,9 +101,9 @@ export class MutationTestExecutor {
return {
checkResult$: failedMutant$.pipe(
map((failedMutant) =>
this.mutationTestReportHelper.reportOne(
failedMutant.matchedMutant,
this.checkStatusToResultStatus(failedMutant.checkResult.status as Exclude<CheckStatus, CheckStatus.Passed>)
this.mutationTestReportHelper.reportCheckFailed(
failedMutant.matchedMutant.mutant,
failedMutant.checkResult as Exclude<CheckResult, PassedCheckResult>
)
)
),
Expand All @@ -132,31 +130,11 @@ export class MutationTestExecutor {
const mutantRunOptions = this.createMutantRunOptions(matchedMutant);
const result = await testRunner.mutantRun(mutantRunOptions);
this.testRunnerPool.recycle(testRunner);
return this.mutationTestReportHelper.reportOne(matchedMutant, this.mutantRunStatusToResultStatus(result.status));
return this.mutationTestReportHelper.reportMutantRunResult(matchedMutant, result);
})
);
}

private checkStatusToResultStatus(status: Exclude<CheckStatus, CheckStatus.Passed>): MutantStatus {
switch (status) {
case CheckStatus.CompileError:
return MutantStatus.TranspileError;
}
}

private mutantRunStatusToResultStatus(mutantRunStatus: MutantRunStatus): MutantStatus {
switch (mutantRunStatus) {
case MutantRunStatus.Error:
return MutantStatus.RuntimeError;
case MutantRunStatus.Killed:
return MutantStatus.Killed;
case MutantRunStatus.Survived:
return MutantStatus.Survived;
case MutantRunStatus.Timeout:
return MutantStatus.TimedOut;
}
}

private logDone() {
this.log.info('Done in %s.', this.timer.humanReadableElapsed());
}
Expand Down
82 changes: 37 additions & 45 deletions packages/core/src/reporters/ClearTextReporter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import * as os from 'os';

import chalk = require('chalk');
import { Position, StrykerOptions } from '@stryker-mutator/api/core';
import { Logger } from '@stryker-mutator/api/logging';
import { commonTokens } from '@stryker-mutator/api/plugin';
import { MutantResult, MutantStatus, mutationTestReportSchema, Reporter } from '@stryker-mutator/api/report';
import { MutantResult, MutantStatus, mutationTestReportSchema, Reporter, UndetectedMutantResult } from '@stryker-mutator/api/report';
import { calculateMetrics } from 'mutation-testing-metrics';
import { tokens } from 'typed-inject';

import chalk = require('chalk');

import ClearTextScoreTable from './ClearTextScoreTable';

export default class ClearTextReporter implements Reporter {
Expand All @@ -33,43 +32,31 @@ export default class ClearTextReporter implements Reporter {
this.writeLine();
let totalTests = 0;

// use these fn's in order to preserve the 'this` pointer
// use these functions in order to preserve the 'this` pointer
const logDebugFn = (input: string) => this.log.debug(input);
const writeLineFn = (input: string) => this.writeLine(input);

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

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));
private logMutantResult(result: MutantResult, logImplementation: (input: string) => void): void {
logImplementation(`#${result.id}. [${MutantStatus[result.status]}] ${result.mutatorName}`);
logImplementation(this.colorSourceFileAndLocation(result.fileName, result.location.start));

result.originalLines.split('\n').forEach((line) => {
logImplementation(chalk.red('- ' + line));
Expand All @@ -78,42 +65,47 @@ export default class ClearTextReporter implements Reporter {
logImplementation(chalk.green('+ ' + line));
});
logImplementation('');
if (this.options.coverageAnalysis === 'perTest') {
this.logExecutedTests(result, logImplementation);
} else if (result.testsRan.length > 0) {
logImplementation('Ran all tests for this mutant.');
if (result.status === MutantStatus.Survived) {
if (this.options.coverageAnalysis === 'perTest' && result.testFilter) {
this.logExecutedTests(result, logImplementation);
} else {
logImplementation('Ran all tests for this mutant.');
}
} else if (result.status === MutantStatus.Killed) {
logImplementation(`Killed by: ${result.killedBy}`);
} else if (result.status === MutantStatus.RuntimeError || result.status === MutantStatus.CompileError) {
logImplementation(`Error message: ${result.errorMessage}`);
}
}

private colorSourceFileAndLocation(sourceFilePath: string, position: Position): string {
const clearTextReporterConfig = this.options.clearTextReporter;

if (clearTextReporterConfig.allowColor !== false) {
return `${sourceFilePath}:${position.line}:${position.column}`;
private colorSourceFileAndLocation(fileName: string, position: Position): string {
if (!this.options.clearTextReporter.allowColor) {
return `${fileName}:${position.line}:${position.column}`;
}

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

private logExecutedTests(result: MutantResult, logImplementation: (input: string) => void) {
private logExecutedTests(result: UndetectedMutantResult, logImplementation: (input: string) => void) {
if (!this.options.clearTextReporter.logTests) {
return;
}

if (result.testsRan.length > 0) {
let testsToLog = this.options.clearTextReporter.maxTestsToLog;
if (result.nrOfTestsRan > 0) {
const { maxTestsToLog } = this.options.clearTextReporter;

if (testsToLog > 0) {
logImplementation('Tests ran: ');
for (let i = 0; i < testsToLog; i++) {
if (i > result.testsRan.length - 1) {
if (maxTestsToLog > 0) {
logImplementation('Tests ran:');
for (let i = 0; i < maxTestsToLog; i++) {
if (i > result.testFilter!.length - 1) {
break;
}

logImplementation(' ' + result.testsRan[i]);
logImplementation(` ${result.testFilter![i]}`);
}
if (testsToLog < result.testsRan.length) {
logImplementation(` and ${result.testsRan.length - testsToLog} more tests!`);
const diff = result.testFilter!.length - maxTestsToLog;
if (diff > 0) {
const plural = diff === 1 ? '' : 's';
logImplementation(` and ${diff} more test${plural}!`);
}
logImplementation('');
}
Expand Down
Loading

0 comments on commit 67f1ed5

Please sign in to comment.