lib/clover-limits.js
"use strict";
var path = require("path");
var Report = require("istanbul").Report;
var FileWriter = require("./util/file-writer");
var TreeSummarizer = require("istanbul").TreeSummarizer;
var utils = require("istanbul").utils;
var sloc = require("sloc");
var fs = require("fs");
var defaults = require("./defaults");
/**
* {description}.
* @module {name}
* @extends istanbul.Report
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the clover report will be written
* @param {String} [opts.file] the file name for the coverage report, defaulted to config attribute or "clover.xml"
* @param {String} [opts.testDir] the directory in which to the summary coverage test report will be written
* @param {String} [opts.testFile] the file name for the summary coverage test report, defaulted to config attribute
* or "clover-tests.json"
* @param {Object} [opts.watermarks] watermarks with three limits for the coverage report and summary coverage test
* report.
* 1) lower limit for html report and minimum code coverage test
* 2) mid limit for html report
* 3) skipped limit for maximum skipped code test
* Only the lower limit [index 0] and skipped [index 2] is used by the summary coverage test report.
* Example watermark object:
* {~lb}
statements: [ 50, 80, 20 ],
lines: [ 50, 80, 20],
functions: [ 50, 80, 20],
branches: [ 50, 80, 20 ]
* {~rb}
*/
var CloverJSONReport = function CloverJSONReport(opts) {
Report.call(this);
opts = opts || {};
this.projectRoot = process.cwd();
this.dir = opts.dir || this.projectRoot;
this.file = opts.file || "clover.xml";
this.testDir = opts.testDir || opts.dir;
this.testFile = opts.testFile || "clover-tests.json";
this.opts = opts;
this.opts.watermarks = this.opts.watermarks || defaults.watermarks();
};
var asJavaPackage = function asJavaPackage(node) {
return node.displayShortName().
replace(/\//g, ".").
replace(/\\/g, ".").
replace(/\.$/, "");
};
var asClassName = function asClassName(node) {
return node.fullPath().replace(/.*[\\\/]/, "");
};
var quote = function quote(thing) {
return "\"" + thing + "\"";
};
var attr = function attr(n, v) {
return " " + n + "=" + quote(v) + " ";
};
var branchCoverageByLine = function branchCoverageByLine(fileCoverage) {
var branchMap = fileCoverage.branchMap,
branches = fileCoverage.b,
ret = {};
Object.keys(branchMap).forEach(function eachBranchMap(k) {
var line = branchMap[k].line,
branchData = branches[k];
ret[line] = ret[line] || [];
ret[line].push.apply(ret[line], branchData);
});
Object.keys(ret).forEach(function eachRet(k) {
var dataArray = ret[k],
covered = dataArray.filter(function filterGtZero(item) {
return item > 0;
}),
coverage = covered.length / dataArray.length * 100;
ret[k] = {"covered": covered.length, "total": dataArray.length, "coverage": coverage};
});
return ret;
};
var addClassStats = function addClassStats(node, fileCoverage, writer, jsonResults, limits) {
var metrics = node.metrics,
branchByLine = branchCoverageByLine(fileCoverage),
//funcMap,
lines,
linePercent = (metrics.statements.covered / metrics.statements.total).toFixed(2),
branchesPercent = (metrics.branches.covered / metrics.branches.total).toFixed(2),
functionsPercent = (metrics.functions.covered / metrics.functions.total).toFixed(2),
lineIgnoredPercent = (metrics.statements.skipped / metrics.statements.total).toFixed(2),
branchesIgnoredPercent = (metrics.branches.skipped / metrics.branches.total).toFixed(2),
functionsIgnoredPercent = (metrics.functions.skipped / metrics.functions.total).toFixed(2),
result = {
"title": "Coverage: " + asClassName(node),
"fullTitle": "Coverage: " + node.fullPath(), /*"statement coverage: " + (linePercent * 100) +
"% (statements covered = " + metrics.statements.covered +
" total statements = " + metrics.statements.total + ")" +
"\n" +
"branch coverage: " + (branchesPercent * 100) +
"% (branches covered = " + metrics.branches.covered +
" total branches = " + metrics.branches.total + ")" +
"\n" +
"function coverage: " + (functionsPercent * 100) +
"% (functions covered = " + metrics.functions.covered +
" total functions = " + metrics.functions.total + ")",*/
"duration": 0
};
writer.println("\t\t\t<file" +
attr("name", asClassName(node)) +
attr("path", node.fullPath()) +
">");
writer.println("\t\t\t\t<metrics" +
attr("statements", metrics.statements.total) +
attr("coveredstatements", metrics.statements.covered) +
attr("conditionals", metrics.branches.total) +
attr("coveredconditionals", metrics.branches.covered) +
attr("methods", metrics.functions.total) +
attr("coveredmethods", metrics.functions.covered) +
"/>");
if (linePercent < limits.statements ||
branchesPercent < limits.branches ||
functionsPercent < limits.functions ||
lineIgnoredPercent > limits.ignored.statements ||
branchesIgnoredPercent > limits.ignored.branches ||
functionsIgnoredPercent > limits.ignored.functions) {
result.error = "";
if (linePercent < limits.statements) {
result.error = result.error + "Insufficient statement coverage: actual=" + linePercent * 100 +
"% required=" + limits.statements * 100 +
"% (statements covered = " + metrics.statements.covered +
" total statements = " + metrics.statements.total + ")" +
"\n";
}
if (branchesPercent < limits.branches) {
result.error = result.error + "Insufficient branch coverage: actual=" + branchesPercent * 100 +
"% required=" + limits.branches * 100 +
"% (branches covered = " + metrics.branches.covered +
" total branches = " + metrics.branches.total + ")" +
"\n";
}
if (functionsPercent < limits.functions) {
result.error = result.error + "Insufficient function coverage: actual=" + functionsPercent * 100 +
"% required=" + limits.functions * 100 +
"% (functions covered = " + metrics.functions.covered +
" total functions = " + metrics.functions.total + ")";
}
if (lineIgnoredPercent > limits.ignored.statements) {
result.error = result.error + "Too many statements skipped: actual=" + lineIgnoredPercent * 100 +
"% required=" + limits.ignored.statements * 100 +
"% (statements skipped = " + metrics.statements.skipped +
" total statements = " + metrics.statements.total + ")" +
"\n";
}
if (branchesIgnoredPercent > limits.ignored.branches) {
result.error = result.error + "Too many branches skipped: actual=" + branchesIgnoredPercent * 100 +
"% required=" + limits.ignored.branches * 100 +
"% (branches skipped = " + metrics.branches.skipped +
" total branches = " + metrics.branches.total + ")" +
"\n";
}
if (functionsIgnoredPercent > limits.ignored.functions) {
result.error = result.error + "Too many functions skipped: actual=" + functionsIgnoredPercent * 100 +
"% required=" + limits.ignored.functions * 100 +
"% (functions skipped = " + metrics.functions.skipped +
" total functions = " + metrics.functions.total + ")";
}
jsonResults.failures.push(result);
} else {
jsonResults.passes.push(result);
}
//funcMap = fileCoverage.fnMap;
lines = fileCoverage.l;
Object.keys(lines).forEach(function eachLine(k) {
var str = "\t\t\t\t<line" +
attr("num", k) +
attr("count", lines[k]),
branchDetail = branchByLine[k];
if (!branchDetail) {
str = str + " type=\"stmt\" ";
} else {
str = str + " type=\"cond\" " +
attr("truecount", branchDetail.covered) +
attr("falsecount", branchDetail.total - branchDetail.covered);
}
writer.println(str + "/>");
});
writer.println("\t\t\t</file>");
};
var walk = function walk(node, collector, writer, level, projectRoot, jsonResults, limits) {
var metrics,
slocStats,
totalFiles = 0,
totalPackages = 0,
totalLines = 0,
totalSourceLines = 0;
//tempLines = 0;
if (level === 0) {
metrics = node.metrics;
writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
writer.println("<coverage" +
attr("generated", Date.now()) +
"clover=\"3.2.0\">");
writer.println("\t<project" +
attr("timestamp", Date.now()) +
attr("name", "All Files") +
">");
node.children.filter(function filterByDir(child) {
return child.kind === "dir";
}).
forEach(function eachChild(child) {
totalPackages = totalPackages + 1;
child.children.filter(function filterByDir(sibling) {
return sibling.kind !== "dir";
}).
forEach(function eachChild2(sibling) {
Object.keys(collector.fileCoverageFor(sibling.fullPath()).l).forEach(function eachChild3() {
//tempLines = k;
});
totalFiles = totalFiles + 1;
//get sloc
slocStats = sloc(fs.readFileSync(sibling.fullPath(), "utf8"), "js");
totalLines = totalLines + slocStats.total;
totalSourceLines = totalSourceLines + slocStats.source;
});
});
jsonResults.stats = {
"suites": totalFiles,
"tests": totalFiles,
"passes": 0,
"pending": 0,
"failures": 0,
"start": new global.Date(),
"end": "",
"duration": 100
};
jsonResults.failures = [];
jsonResults.passes = [];
jsonResults.skipped = [];
writer.println("\t\t<metrics" +
attr("statements", metrics.statements.total) +
attr("coveredstatements", metrics.statements.covered) +
attr("conditionals", metrics.branches.total) +
attr("coveredconditionals", metrics.branches.covered) +
attr("methods", metrics.functions.total) +
attr("coveredmethods", metrics.functions.covered) +
attr("elements", metrics.statements.total + metrics.branches.total + metrics.functions.total) +
attr("coveredelements", metrics.statements.covered + metrics.branches.covered + metrics.functions.covered) +
attr("complexity", 0) +
attr("packages", totalPackages) +
attr("files", totalFiles) +
attr("classes", totalFiles) +
attr("loc", totalLines) +
attr("ncloc", totalSourceLines) +
"/>");
}
if (node.packageMetrics) {
metrics = node.packageMetrics;
writer.println("\t\t<package" +
attr("name", asJavaPackage(node)) +
">");
writer.println("\t\t\t<metrics" +
attr("statements", metrics.statements.total) +
attr("coveredstatements", metrics.statements.covered) +
attr("conditionals", metrics.branches.total) +
attr("coveredconditionals", metrics.branches.covered) +
attr("methods", metrics.functions.total) +
attr("coveredmethods", metrics.functions.covered) +
"/>");
node.children.filter(function filterByDir(child) {
return child.kind !== "dir";
}).
forEach(function eachChild(child) {
addClassStats(child, collector.fileCoverageFor(child.fullPath()), writer, jsonResults, limits);
});
writer.println("\t\t</package>");
}
node.children.filter(function filterByDir(child) {
return child.kind === "dir";
}).
forEach(function eachChild(child) {
walk(child, collector, writer, level + 1, projectRoot, jsonResults, limits);
});
if (level === 0) {
writer.println("\t</project>");
writer.println("</coverage>");
}
};
CloverJSONReport.TYPE = "clover-limits";
Report.mix(CloverJSONReport, {
"writeReport": function writeReport(collector, sync) {
var summarizer = new TreeSummarizer(),
outputFile = path.join(this.dir, this.file),
jsonOutputFile = path.join(this.testDir, this.testFile),
writer = this.opts.writer || new FileWriter(sync),
projectRoot = this.projectRoot,
tree,
root,
jsonResults = {},
watermarks = this.opts.watermarks,
limits = {
"statements": watermarks.statements[0] / 100,
"branches": watermarks.branches[0] / 100,
"functions": watermarks.functions[0] / 100,
"ignored": {
"statements": watermarks.statements[2] / 100,
"branches": watermarks.branches[2] / 100,
"functions": watermarks.functions[2] / 100
}
};
collector.files().forEach(function eachFile(key) {
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key)));
});
tree = summarizer.getTreeSummary();
root = tree.root;
writer.writeFile(outputFile, function writeFileCallback(contentWriter) {
walk(root, collector, contentWriter, 0, projectRoot, jsonResults, limits);
writer.writeFile(jsonOutputFile, function writeFileCallback2(contentWriter1) {
//set stats now we have them
jsonResults.stats.passes = jsonResults.passes.length;
jsonResults.stats.failures = jsonResults.failures.length;
jsonResults.stats.end = new global.Date();
jsonResults.stats.duration = new global.Date() - jsonResults.stats.start;
contentWriter1.println(JSON.stringify(jsonResults, null, 2));
});
});
}
});
module.exports = CloverJSONReport;