index.js 7.7 KB
/*
 Copyright 2012-2015, Yahoo Inc.
 Copyrights licensed under the New BSD License. See the accompanying LICENSE
 file for terms.
 */
'use strict';
const { ReportBase } = require('istanbul-lib-report');

const NAME_COL = 4;
const PCT_COLS = 7;
const MISSING_COL = 17;
const TAB_SIZE = 1;
const DELIM = ' | ';

function padding(num, ch) {
    let str = '';
    let i;
    ch = ch || ' ';
    for (i = 0; i < num; i += 1) {
        str += ch;
    }
    return str;
}

function fill(str, width, right, tabs) {
    tabs = tabs || 0;
    str = String(str);

    const leadingSpaces = tabs * TAB_SIZE;
    const remaining = width - leadingSpaces;
    const leader = padding(leadingSpaces);
    let fmtStr = '';

    if (remaining > 0) {
        const strlen = str.length;
        let fillStr;

        if (remaining >= strlen) {
            fillStr = padding(remaining - strlen);
        } else {
            fillStr = '...';
            const length = remaining - fillStr.length;

            str = str.substring(strlen - length);
            right = true;
        }
        fmtStr = right ? fillStr + str : str + fillStr;
    }

    return leader + fmtStr;
}

function formatName(name, maxCols, level) {
    return fill(name, maxCols, false, level);
}

function formatPct(pct, width) {
    return fill(pct, width || PCT_COLS, true, 0);
}

function nodeMissing(node) {
    if (node.isSummary()) {
        return '';
    }

    const metrics = node.getCoverageSummary();
    const isEmpty = metrics.isEmpty();
    const lines = isEmpty ? 0 : metrics.lines.pct;

    let coveredLines;

    const fileCoverage = node.getFileCoverage();
    if (lines === 100) {
        const branches = fileCoverage.getBranchCoverageByLine();
        coveredLines = Object.entries(branches).map(([key, { coverage }]) => [
            key,
            coverage === 100
        ]);
    } else {
        coveredLines = Object.entries(fileCoverage.getLineCoverage());
    }

    let newRange = true;
    const ranges = coveredLines
        .reduce((acum, [line, hit]) => {
            if (hit) newRange = true;
            else {
                line = parseInt(line);
                if (newRange) {
                    acum.push([line]);
                    newRange = false;
                } else acum[acum.length - 1][1] = line;
            }

            return acum;
        }, [])
        .map(range => {
            const { length } = range;

            if (length === 1) return range[0];

            return `${range[0]}-${range[1]}`;
        });

    return [].concat(...ranges).join(',');
}

function nodeName(node) {
    return node.getRelativeName() || 'All files';
}

function depthFor(node) {
    let ret = 0;
    node = node.getParent();
    while (node) {
        ret += 1;
        node = node.getParent();
    }
    return ret;
}

function nullDepthFor() {
    return 0;
}

function findWidth(node, context, nodeExtractor, depthFor = nullDepthFor) {
    let last = 0;
    function compareWidth(node) {
        last = Math.max(
            last,
            TAB_SIZE * depthFor(node) + nodeExtractor(node).length
        );
    }
    const visitor = {
        onSummary: compareWidth,
        onDetail: compareWidth
    };
    node.visit(context.getVisitor(visitor));
    return last;
}

function makeLine(nameWidth, missingWidth) {
    const name = padding(nameWidth, '-');
    const pct = padding(PCT_COLS, '-');
    const elements = [];

    elements.push(name);
    elements.push(pct);
    elements.push(padding(PCT_COLS + 1, '-'));
    elements.push(pct);
    elements.push(pct);
    elements.push(padding(missingWidth, '-'));
    return elements.join(DELIM.replace(/ /g, '-')) + '-';
}

function tableHeader(maxNameCols, missingWidth) {
    const elements = [];
    elements.push(formatName('File', maxNameCols, 0));
    elements.push(formatPct('% Stmts'));
    elements.push(formatPct('% Branch', PCT_COLS + 1));
    elements.push(formatPct('% Funcs'));
    elements.push(formatPct('% Lines'));
    elements.push(formatName('Uncovered Line #s', missingWidth));
    return elements.join(DELIM) + ' ';
}

function isFull(metrics) {
    return (
        metrics.statements.pct === 100 &&
        metrics.branches.pct === 100 &&
        metrics.functions.pct === 100 &&
        metrics.lines.pct === 100
    );
}

function tableRow(
    node,
    context,
    colorizer,
    maxNameCols,
    level,
    skipEmpty,
    skipFull,
    missingWidth
) {
    const name = nodeName(node);
    const metrics = node.getCoverageSummary();
    const isEmpty = metrics.isEmpty();
    if (skipEmpty && isEmpty) {
        return '';
    }
    if (skipFull && isFull(metrics)) {
        return '';
    }

    const mm = {
        statements: isEmpty ? 0 : metrics.statements.pct,
        branches: isEmpty ? 0 : metrics.branches.pct,
        functions: isEmpty ? 0 : metrics.functions.pct,
        lines: isEmpty ? 0 : metrics.lines.pct
    };
    const colorize = isEmpty
        ? function(str) {
              return str;
          }
        : function(str, key) {
              return colorizer(str, context.classForPercent(key, mm[key]));
          };
    const elements = [];

    elements.push(colorize(formatName(name, maxNameCols, level), 'statements'));
    elements.push(colorize(formatPct(mm.statements), 'statements'));
    elements.push(colorize(formatPct(mm.branches, PCT_COLS + 1), 'branches'));
    elements.push(colorize(formatPct(mm.functions), 'functions'));
    elements.push(colorize(formatPct(mm.lines), 'lines'));
    elements.push(
        colorizer(
            formatName(nodeMissing(node), missingWidth),
            mm.lines === 100 ? 'medium' : 'low'
        )
    );

    return elements.join(DELIM) + ' ';
}

class TextReport extends ReportBase {
    constructor(opts) {
        super();

        opts = opts || {};
        const { maxCols } = opts;

        this.file = opts.file || null;
        this.maxCols = maxCols != null ? maxCols : process.stdout.columns || 80;
        this.cw = null;
        this.skipEmpty = opts.skipEmpty;
        this.skipFull = opts.skipFull;
    }

    onStart(root, context) {
        this.cw = context.writer.writeFile(this.file);
        this.nameWidth = Math.max(
            NAME_COL,
            findWidth(root, context, nodeName, depthFor)
        );
        this.missingWidth = Math.max(
            MISSING_COL,
            findWidth(root, context, nodeMissing)
        );

        if (this.maxCols > 0) {
            const pct_cols = DELIM.length + 4 * (PCT_COLS + DELIM.length) + 2;

            const maxRemaining = this.maxCols - (pct_cols + MISSING_COL);
            if (this.nameWidth > maxRemaining) {
                this.nameWidth = maxRemaining;
                this.missingWidth = MISSING_COL;
            } else if (this.nameWidth < maxRemaining) {
                const maxRemaining = this.maxCols - (this.nameWidth + pct_cols);
                if (this.missingWidth > maxRemaining) {
                    this.missingWidth = maxRemaining;
                }
            }
        }
        const line = makeLine(this.nameWidth, this.missingWidth);
        this.cw.println(line);
        this.cw.println(tableHeader(this.nameWidth, this.missingWidth));
        this.cw.println(line);
    }

    onSummary(node, context) {
        const nodeDepth = depthFor(node);
        const row = tableRow(
            node,
            context,
            this.cw.colorize.bind(this.cw),
            this.nameWidth,
            nodeDepth,
            this.skipEmpty,
            this.skipFull,
            this.missingWidth
        );
        if (row) {
            this.cw.println(row);
        }
    }

    onDetail(node, context) {
        return this.onSummary(node, context);
    }

    onEnd() {
        this.cw.println(makeLine(this.nameWidth, this.missingWidth));
        this.cw.close();
    }
}

module.exports = TextReport;