@stryker-mutator/core
Version:
The extendable JavaScript mutation testing framework
158 lines (129 loc) • 5.04 kB
text/typescript
import os from 'os';
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);
const spaces = (n: number) => repeat(' ', n);
const dots = (n: number) => repeat('.', n);
/**
* Represents a column in the clear text table
*/
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;
}
private determineValueSize(row: MetricsResult = this.rows, ancestorCount = 0): number {
const valueWidths = row.childResults.map((child) => this.determineValueSize(child, ancestorCount + 1));
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 - stringWidth(input) - 2)} ${input} `;
}
public drawLine(): string {
return repeat('-', this.width);
}
public drawTableCell(score: MetricsResult, ancestorCount: number) {
return this.color(score)(this.pad(this.valueFactory(score, ancestorCount)));
}
public drawHeader() {
return this.pad(this.header);
}
protected color(_score: MetricsResult) {
return (input: string) => input;
}
}
class MutationScoreColumn extends Column {
constructor(
rows: MetricsResult,
private readonly thresholds: MutationScoreThresholds,
) {
super('% score', (row) => (isNaN(row.metrics.mutationScore) ? 'n/a' : row.metrics.mutationScore.toFixed(2)), rows);
}
protected color(metricsResult: MetricsResult) {
const { mutationScore: score } = metricsResult.metrics;
if (isNaN(score)) {
return chalk.grey;
} else if (score >= this.thresholds.high) {
return chalk.green;
} else if (score >= this.thresholds.low) {
return chalk.yellow;
} else {
return chalk.red;
}
}
}
class FileColumn extends Column {
constructor(rows: MetricsResult) {
super('File', (row, ancestorCount) => spaces(ancestorCount) + (ancestorCount === 0 ? FILES_ROOT_NAME : row.name), rows);
}
protected override pad(input: string): string {
return `${input} ${spaces(this.width - stringWidth(input) - 1)}`;
}
}
/**
* Represents a clear text table for mutation score
*/
export class ClearTextScoreTable {
private readonly columns: Column[];
constructor(
private readonly metricsResult: MetricsResult,
options: StrykerOptions,
) {
this.columns = [
new FileColumn(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,
),
];
}
private drawBorder() {
return this.drawRow((column) => column.drawLine());
}
private drawHeader() {
return this.drawRow((c) => c.drawHeader());
}
private drawRow(toDraw: (col: Column) => string) {
return this.columns.map(toDraw).join('|') + '|';
}
private drawValues(current = this.metricsResult, ancestorCount = 0): string[] {
return [this.drawRow((c) => c.drawTableCell(current, ancestorCount))].concat(
flatMap(current.childResults, (child) => this.drawValues(child, ancestorCount + 1)),
);
}
/**
* Returns a string with the score results drawn in a table.
*/
public draw(): string {
return [this.drawBorder(), this.drawHeader(), this.drawBorder(), this.drawValues().join(os.EOL), this.drawBorder()].join(os.EOL);
}
}