Skip to content

Commit

Permalink
feat(clear-text reporter): add allowEmojis option in console (#3820)
Browse files Browse the repository at this point in the history
Add `clearTextReporterOptions.allowEmojis` boolean option in your stryker.conf.json file. When enabled, the clear-text reporter will display emojis for the mutant statuses.

Default value is `false`

Co-authored-by: Nico Jansen <jansennico@gmail.com>
  • Loading branch information
brdv and nicojs authored Oct 30, 2022
1 parent 132e55a commit 79cc05f
Show file tree
Hide file tree
Showing 11 changed files with 189 additions and 56 deletions.
5 changes: 5 additions & 0 deletions packages/api/schema/stryker-core.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
"type": "boolean",
"default": true
},
"allowEmojis": {
"description": "Enable emojis in your clear text report (experimental).",
"type": "boolean",
"default": false
},
"logTests": {
"description": "Indicates whether or not to log which tests were executed for a given mutant.",
"type": "boolean",
Expand Down
25 changes: 19 additions & 6 deletions packages/core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"chalk": "~5.1.0",
"commander": "~9.4.0",
"diff-match-patch": "1.0.5",
"emoji-regex": "~10.2.1",
"execa": "~6.1.0",
"file-url": "~4.0.0",
"get-port": "~6.1.0",
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/reporters/clear-text-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Reporter } from '@stryker-mutator/api/report';
import { MetricsResult, MutantModel, TestModel, MutationTestMetricsResult, TestFileModel, TestMetrics, TestStatus } from 'mutation-testing-metrics';
import { tokens } from 'typed-inject';

import { plural } from '../utils/string-utils.js';
import { getEmojiForStatus, plural } from '../utils/string-utils.js';

import { ClearTextScoreTable } from './clear-text-score-table.js';

Expand Down Expand Up @@ -40,7 +40,7 @@ export class ClearTextReporter implements Reporter {
this.writeLine();
this.reportAllTests(metrics);
this.reportAllMutants(metrics);
this.writeLine(new ClearTextScoreTable(metrics.systemUnderTestMetrics, this.options.thresholds).draw());
this.writeLine(new ClearTextScoreTable(metrics.systemUnderTestMetrics, this.options).draw());
}

private reportAllTests(metrics: MutationTestMetricsResult) {
Expand Down Expand Up @@ -109,8 +109,13 @@ export class ClearTextReporter implements Reporter {
this.writeLine(`Ran ${(totalTests / systemUnderTestMetrics.metrics.totalMutants).toFixed(2)} tests per mutant on average.`);
}

private statusLabel(mutant: MutantModel): string {
const status = MutantStatus[mutant.status];
return this.options.clearTextReporter.allowEmojis ? `${getEmojiForStatus(status)} ${status}` : status.toString();
}

private reportMutantResult(result: MutantModel, logImplementation: (input: string) => void): void {
logImplementation(`[${MutantStatus[result.status]}] ${result.mutatorName}`);
logImplementation(`[${this.statusLabel(result)}] ${result.mutatorName}`);
logImplementation(this.colorSourceFileAndLocation(result.fileName, result.location.start));

result
Expand Down
43 changes: 30 additions & 13 deletions packages/core/src/reporters/clear-text-score-table.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import os from 'os';

import { MutationScoreThresholds } from '@stryker-mutator/api/core';
import { MutationScoreThresholds, StrykerOptions } from '@stryker-mutator/api/core';

import { MetricsResult } from 'mutation-testing-metrics';

import chalk from 'chalk';
import flatMap from 'lodash.flatmap';

import emojiRegex from 'emoji-regex';

import { stringWidth } from '../utils/string-utils.js';

const FILES_ROOT_NAME = 'All files';

const emojiRe = emojiRegex();

type TableCellValueFactory = (row: MetricsResult, ancestorCount: number) => string;

const repeat = (char: string, nTimes: number) => new Array(nTimes > -1 ? nTimes + 1 : 0).join(char);
Expand All @@ -19,25 +26,31 @@ const dots = (n: number) => repeat('.', n);
*/
class Column {
protected width: number;
private readonly emojiMatchInHeader: RegExpExecArray | null;

constructor(public header: string, public valueFactory: TableCellValueFactory, public rows: MetricsResult) {
this.emojiMatchInHeader = emojiRe.exec(this.header);
const maxContentSize = this.determineValueSize();
this.width = this.pad(dots(maxContentSize)).length;
}

protected determineValueSize(row: MetricsResult = this.rows, ancestorCount = 0): number {
private determineValueSize(row: MetricsResult = this.rows, ancestorCount = 0): number {
const valueWidths = row.childResults.map((child) => this.determineValueSize(child, ancestorCount + 1));
valueWidths.push(this.header.length);
valueWidths.push(this.headerLength);
valueWidths.push(this.valueFactory(row, ancestorCount).length);
return Math.max(...valueWidths);
}

private get headerLength() {
return stringWidth(this.header);
}

/**
* Adds padding (spaces) to the front and end of a value
* @param input The string input
*/
protected pad(input: string): string {
return `${spaces(this.width - input.length - 2)} ${input} `;
return `${spaces(this.width - stringWidth(input) - 2)} ${input} `;
}

public drawLine(): string {
Expand Down Expand Up @@ -80,8 +93,8 @@ class FileColumn extends Column {
constructor(rows: MetricsResult) {
super('File', (row, ancestorCount) => spaces(ancestorCount) + (ancestorCount === 0 ? FILES_ROOT_NAME : row.name), rows);
}
protected pad(input: string): string {
return `${input} ${spaces(this.width - input.length - 1)}`;
protected override pad(input: string): string {
return `${input} ${spaces(this.width - stringWidth(input) - 1)}`;
}
}

Expand All @@ -91,15 +104,19 @@ class FileColumn extends Column {
export class ClearTextScoreTable {
private readonly columns: Column[];

constructor(private readonly metricsResult: MetricsResult, thresholds: MutationScoreThresholds) {
constructor(private readonly metricsResult: MetricsResult, options: StrykerOptions) {
this.columns = [
new FileColumn(metricsResult),
new MutationScoreColumn(metricsResult, thresholds),
new Column('# killed', (row) => row.metrics.killed.toString(), metricsResult),
new Column('# timeout', (row) => row.metrics.timeout.toString(), metricsResult),
new Column('# survived', (row) => row.metrics.survived.toString(), metricsResult),
new Column('# no cov', (row) => row.metrics.noCoverage.toString(), metricsResult),
new Column('# error', (row) => (row.metrics.runtimeErrors + row.metrics.compileErrors).toString(), metricsResult),
new MutationScoreColumn(metricsResult, options.thresholds),
new Column(`${options.clearTextReporter.allowEmojis ? '✅' : '#'} killed`, (row) => row.metrics.killed.toString(), metricsResult),
new Column(`${options.clearTextReporter.allowEmojis ? '⌛️' : '#'} timeout`, (row) => row.metrics.timeout.toString(), metricsResult),
new Column(`${options.clearTextReporter.allowEmojis ? '👽' : '#'} survived`, (row) => row.metrics.survived.toString(), metricsResult),
new Column(`${options.clearTextReporter.allowEmojis ? '🙈' : '#'} no cov`, (row) => row.metrics.noCoverage.toString(), metricsResult),
new Column(
`${options.clearTextReporter.allowEmojis ? '💥' : '#'} errors`,
(row) => (row.metrics.runtimeErrors + row.metrics.compileErrors).toString(),
metricsResult
),
];
}

Expand Down
33 changes: 33 additions & 0 deletions packages/core/src/utils/string-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { propertyPath } from '@stryker-mutator/util';
import { schema } from '@stryker-mutator/api/core';
import { StrykerOptions } from '@stryker-mutator/api/core';

import emojiRegex from 'emoji-regex';

const emojiRe = emojiRegex();

const { MutantStatus } = schema;

export function wrapInClosure(codeFragment: string): string {
return `
(function (window) {
Expand Down Expand Up @@ -31,6 +38,32 @@ export function deserialize<T>(stringified: string): T {
return JSON.parse(stringified);
}

export function getEmojiForStatus(status: schema.MutantStatus): string {
switch (status) {
case MutantStatus.Killed:
return '✅';
case MutantStatus.NoCoverage:
return '🙈';
case MutantStatus.Ignored:
return '🤥';
case MutantStatus.Survived:
return '👽';
case MutantStatus.Timeout:
return '⌛';
case MutantStatus.RuntimeError:
case MutantStatus.CompileError:
return '💥';
}
}

export function stringWidth(input: string): number {
let length = input.length;
for (const match of input.matchAll(emojiRe)) {
length = length - match[0].length + 2;
}
return length;
}

/**
* Print the name of (or path to) a stryker option
*/
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/helpers/producers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ function factoryMethod<T>(defaultsFactory: () => T) {

export const createClearTextReporterOptions = factoryMethod<ClearTextReporterOptions>(() => ({
allowColor: true,
allowEmojis: false,
logTests: true,
maxTestsToLog: 3,
}));
Expand Down
1 change: 1 addition & 0 deletions packages/core/test/unit/config/options-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe(OptionsValidator.name, () => {
checkerNodeArgs: [],
clearTextReporter: {
allowColor: true,
allowEmojis: false,
logTests: true,
maxTestsToLog: 3,
},
Expand Down
66 changes: 47 additions & 19 deletions packages/core/test/unit/reporters/clear-text-reporter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,44 @@ describe(ClearTextReporter.name, () => {
const serializedTable: string = stdoutStub.getCalls().pop()!.args[0];
const rows = serializedTable.split(os.EOL);
expect(rows).to.deep.eq([
'----------|---------|----------|-----------|------------|----------|---------|',
'File | % score | # killed | # timeout | # survived | # no cov | # error |',
'----------|---------|----------|-----------|------------|----------|---------|',
`All files |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|---------|',
'----------|---------|----------|-----------|------------|----------|----------|',
'File | % score | # killed | # timeout | # survived | # no cov | # errors |',
'----------|---------|----------|-----------|------------|----------|----------|',
`All files |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|----------|',
'',
]);
});

it('should show emojis in table with enableConsoleEmojis flag', () => {
testInjector.options.clearTextReporter.allowEmojis = true;

act({
files: {
'src/file.js': {
language: 'js',
mutants: [
{
id: '1',
location: { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } },
mutatorName: 'Block',
replacement: '{}',
status: MutantStatus.Killed,
},
],
source: 'console.log("hello world!")',
},
},
schemaVersion: '1.0',
thresholds: factory.mutationScoreThresholds({}),
});

const serializedTable: string = stdoutStub.getCalls().pop()!.args[0];
const rows = serializedTable.split(os.EOL);
expect(rows[1]).to.eq('File | % score | ✅ killed | ⌛️ timeout | 👽 survived | 🙈 no cov | 💥 errors |');
});

it('should report the clear text table with full n/a values', () => {
act({
files: {
Expand All @@ -82,12 +110,12 @@ describe(ClearTextReporter.name, () => {
const rows = serializedTable.split(os.EOL);

expect(rows).to.deep.eq([
'----------|---------|----------|-----------|------------|----------|---------|',
'File | % score | # killed | # timeout | # survived | # no cov | # error |',
'----------|---------|----------|-----------|------------|----------|---------|',
`All files |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|---------|',
'----------|---------|----------|-----------|------------|----------|----------|',
'File | % score | # killed | # timeout | # survived | # no cov | # errors |',
'----------|---------|----------|-----------|------------|----------|----------|',
`All files |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|----------|',
'',
]);
});
Expand Down Expand Up @@ -129,13 +157,13 @@ describe(ClearTextReporter.name, () => {
const rows = serializedTable.split(os.EOL);

expect(rows).to.deep.eq([
'----------|---------|----------|-----------|------------|----------|---------|',
'File | % score | # killed | # timeout | # survived | # no cov | # error |',
'----------|---------|----------|-----------|------------|----------|---------|',
`All files |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
` file2.js |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|---------|',
'----------|---------|----------|-----------|------------|----------|----------|',
'File | % score | # killed | # timeout | # survived | # no cov | # errors |',
'----------|---------|----------|-----------|------------|----------|----------|',
`All files |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
` file.js |${chalk.grey(' n/a ')}| 0 | 0 | 0 | 0 | 0 |`,
` file2.js |${chalk.green(' 100.00 ')}| 1 | 0 | 0 | 0 | 0 |`,
'----------|---------|----------|-----------|------------|----------|----------|',
'',
]);
});
Expand Down
Loading

0 comments on commit 79cc05f

Please sign in to comment.