UNPKG

@stryker-mutator/core

Version:

The extendable JavaScript mutation testing framework

158 lines (129 loc) 5.04 kB
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); } }