Current Dev State

This commit is contained in:
Tim Lorsbach
2025-06-23 20:13:54 +02:00
parent b4f9bb277d
commit ded50edaa2
22617 changed files with 4345095 additions and 174 deletions

View File

@ -0,0 +1,212 @@
body, html {
margin:0; padding: 0;
height: 100%;
}
body {
font-family: Helvetica Neue, Helvetica, Arial;
font-size: 14px;
color:#333;
}
.small { font-size: 12px;; }
*, *:after, *:before {
-webkit-box-sizing:border-box;
-moz-box-sizing:border-box;
box-sizing:border-box;
}
h1 { font-size: 20px; margin: 0;}
h2 { font-size: 14px; }
pre {
font: 12px/1.4 Consolas, "Liberation Mono", Menlo, Courier, monospace;
margin: 0;
padding: 0;
-moz-tab-size: 2;
-o-tab-size: 2;
tab-size: 2;
}
a { color:#0074D9; text-decoration:none; }
a:hover { text-decoration:underline; }
.strong { font-weight: bold; }
.space-top1 { padding: 10px 0 0 0; }
.pad2y { padding: 20px 0; }
.pad1y { padding: 10px 0; }
.pad2x { padding: 0 20px; }
.pad2 { padding: 20px; }
.pad1 { padding: 10px; }
.space-left2 { padding-left:55px; }
.space-right2 { padding-right:20px; }
.center { text-align:center; }
.clearfix { display:block; }
.clearfix:after {
content:'';
display:block;
height:0;
clear:both;
visibility:hidden;
}
.fl { float: left; }
@media only screen and (max-width:640px) {
.col3 { width:100%; max-width:100%; }
.hide-mobile { display:none!important; }
}
.quiet {
color: #7f7f7f;
color: rgba(0,0,0,0.5);
}
.quiet a { opacity: 0.7; }
.fraction {
font-family: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 10px;
color: #555;
background: #E8E8E8;
padding: 4px 5px;
border-radius: 3px;
vertical-align: middle;
}
div.path a:link, div.path a:visited { color: #333; }
table.coverage {
border-collapse: collapse;
margin: 10px 0 0 0;
padding: 0;
}
table.coverage td {
margin: 0;
padding: 0;
vertical-align: top;
}
table.coverage td.line-count {
text-align: right;
padding: 0 5px 0 20px;
}
table.coverage td.line-coverage {
text-align: right;
padding-right: 10px;
min-width:20px;
}
table.coverage td span.cline-any {
display: inline-block;
padding: 0 5px;
width: 100%;
}
.missing-if-branch {
display: inline-block;
margin-right: 5px;
border-radius: 3px;
position: relative;
padding: 0 4px;
background: #333;
color: yellow;
}
.skip-if-branch {
display: none;
margin-right: 10px;
position: relative;
padding: 0 4px;
background: #ccc;
color: white;
}
.missing-if-branch .typ, .skip-if-branch .typ {
color: inherit !important;
}
.coverage-summary {
border-collapse: collapse;
width: 100%;
}
.coverage-summary tr { border-bottom: 1px solid #bbb; }
.keyline-all { border: 1px solid #ddd; }
.coverage-summary td, .coverage-summary th { padding: 10px; }
.coverage-summary tbody { border: 1px solid #bbb; }
.coverage-summary td { border-right: 1px solid #bbb; }
.coverage-summary td:last-child { border-right: none; }
.coverage-summary th {
text-align: left;
font-weight: normal;
white-space: nowrap;
}
.coverage-summary th.file { border-right: none !important; }
.coverage-summary th.pct { }
.coverage-summary th.pic,
.coverage-summary th.abs,
.coverage-summary td.pct,
.coverage-summary td.abs { text-align: right; }
.coverage-summary td.file { white-space: nowrap; }
.coverage-summary td.pic { min-width: 120px !important; }
.coverage-summary tfoot td { }
.coverage-summary .sorter {
height: 10px;
width: 7px;
display: inline-block;
margin-left: 0.5em;
background: url(sort-arrow-sprite.png) no-repeat scroll 0 0 transparent;
}
.coverage-summary .sorted .sorter {
background-position: 0 -20px;
}
.coverage-summary .sorted-desc .sorter {
background-position: 0 -10px;
}
.status-line { height: 10px; }
/* dark red */
.red.solid, .status-line.low, .low .cover-fill { background:#C21F39 }
.low .chart { border:1px solid #C21F39 }
/* medium red */
.cstat-no, .fstat-no, .cbranch-no, .cbranch-no { background:#F6C6CE }
/* light red */
.low, .cline-no { background:#FCE1E5 }
/* light green */
.high, .cline-yes { background:rgb(230,245,208) }
/* medium green */
.cstat-yes { background:rgb(161,215,106) }
/* dark green */
.status-line.high, .high .cover-fill { background:rgb(77,146,33) }
.high .chart { border:1px solid rgb(77,146,33) }
.medium .chart { border:1px solid #666; }
.medium .cover-fill { background: #666; }
.cbranch-no { background: yellow !important; color: #111; }
.cstat-skip { background: #ddd; color: #111; }
.fstat-skip { background: #ddd; color: #111 !important; }
.cbranch-skip { background: #ddd !important; color: #111; }
span.cline-neutral { background: #eaeaea; }
.medium { background: #eaeaea; }
.cover-fill, .cover-empty {
display:inline-block;
height: 12px;
}
.chart {
line-height: 0;
}
.cover-empty {
background: white;
}
.cover-full {
border-right: none !important;
}
pre.prettyprint {
border: none !important;
padding: 0 !important;
margin: 0 !important;
}
.com { color: #999 !important; }
.ignore-none { color: #999; font-weight: normal; }
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -48px;
}
.footer, .push {
height: 48px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1,156 @@
var addSorting = (function () {
"use strict";
var cols,
currentSort = {
index: 0,
desc: false
};
// returns the summary table element
function getTable() { return document.querySelector('.coverage-summary'); }
// returns the thead element of the summary table
function getTableHeader() { return getTable().querySelector('thead tr'); }
// returns the tbody element of the summary table
function getTableBody() { return getTable().querySelector('tbody'); }
// returns the th element for nth column
function getNthColumn(n) { return getTableHeader().querySelectorAll('th')[n]; }
// loads all columns
function loadColumns() {
var colNodes = getTableHeader().querySelectorAll('th'),
colNode,
cols = [],
col,
i;
for (i = 0; i < colNodes.length; i += 1) {
colNode = colNodes[i];
col = {
key: colNode.getAttribute('data-col'),
sortable: !colNode.getAttribute('data-nosort'),
type: colNode.getAttribute('data-type') || 'string'
};
cols.push(col);
if (col.sortable) {
col.defaultDescSort = col.type === 'number';
colNode.innerHTML = colNode.innerHTML + '<span class="sorter"></span>';
}
}
return cols;
}
// attaches a data attribute to every tr element with an object
// of data values keyed by column name
function loadRowData(tableRow) {
var tableCols = tableRow.querySelectorAll('td'),
colNode,
col,
data = {},
i,
val;
for (i = 0; i < tableCols.length; i += 1) {
colNode = tableCols[i];
col = cols[i];
val = colNode.getAttribute('data-value');
if (col.type === 'number') {
val = Number(val);
}
data[col.key] = val;
}
return data;
}
// loads all row data
function loadData() {
var rows = getTableBody().querySelectorAll('tr'),
i;
for (i = 0; i < rows.length; i += 1) {
rows[i].data = loadRowData(rows[i]);
}
}
// sorts the table using the data for the ith column
function sortByIndex(index, desc) {
var key = cols[index].key,
sorter = function (a, b) {
a = a.data[key];
b = b.data[key];
return a < b ? -1 : a > b ? 1 : 0;
},
finalSorter = sorter,
tableBody = document.querySelector('.coverage-summary tbody'),
rowNodes = tableBody.querySelectorAll('tr'),
rows = [],
i;
if (desc) {
finalSorter = function (a, b) {
return -1 * sorter(a, b);
};
}
for (i = 0; i < rowNodes.length; i += 1) {
rows.push(rowNodes[i]);
tableBody.removeChild(rowNodes[i]);
}
rows.sort(finalSorter);
for (i = 0; i < rows.length; i += 1) {
tableBody.appendChild(rows[i]);
}
}
// removes sort indicators for current column being sorted
function removeSortIndicators() {
var col = getNthColumn(currentSort.index),
cls = col.className;
cls = cls.replace(/ sorted$/, '').replace(/ sorted-desc$/, '');
col.className = cls;
}
// adds sort indicators for current column being sorted
function addSortIndicators() {
getNthColumn(currentSort.index).className += currentSort.desc ? ' sorted-desc' : ' sorted';
}
// adds event listeners for all sorter widgets
function enableUI() {
var i,
el,
ithSorter = function ithSorter(i) {
var col = cols[i];
return function () {
var desc = col.defaultDescSort;
if (currentSort.index === i) {
desc = !currentSort.desc;
}
sortByIndex(i, desc);
removeSortIndicators();
currentSort.index = i;
currentSort.desc = desc;
addSortIndicators();
};
};
for (i =0 ; i < cols.length; i += 1) {
if (cols[i].sortable) {
el = getNthColumn(i).querySelector('.sorter');
if (el.addEventListener) {
el.addEventListener('click', ithSorter(i));
} else {
el.attachEvent('onclick', ithSorter(i));
}
}
}
}
// adds sorting functionality to the UI
return function () {
if (!getTable()) {
return;
}
cols = loadColumns();
loadData(cols);
addSortIndicators();
enableUI();
};
})();
window.addEventListener('load', addSorting);

View File

@ -0,0 +1 @@
.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}

File diff suppressed because one or more lines are too long

94
static/js/ketcher2/node_modules/babel-istanbul/lib/cli.js generated vendored Executable file
View File

@ -0,0 +1,94 @@
#!/usr/bin/env node
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var async = require('async'),
Command = require('./command'),
inputError = require('./util/input-error'),
exitProcess = process.exit; //hold a reference to original process.exit so that we are not affected even when a test changes it
require('./register-plugins');
function findCommandPosition(args) {
var i;
for (i = 0; i < args.length; i += 1) {
if (args[i].charAt(0) !== '-') {
return i;
}
}
return -1;
}
function exit(ex, code) {
// flush output for Node.js Windows pipe bug
// https://github.com/joyent/node/issues/6247 is just one bug example
// https://github.com/visionmedia/mocha/issues/333 has a good discussion
var streams = [process.stdout, process.stderr];
async.forEach(streams, function (stream, done) {
// submit a write request and wait until it's written
stream.write('', done);
}, function () {
if (ex) {
throw ex; // turn it into an uncaught exception
} else {
exitProcess(code);
}
});
}
function errHandler (ex) {
if (!ex) { return; }
if (!ex.inputError) {
// exit with exception stack trace
exit(ex);
} else {
//don't print nasty traces but still exit(1)
console.error(ex.message);
console.error('Try "babel-istanbul help" for usage');
exit(null, 1);
}
}
function runCommand(args, callback) {
var pos = findCommandPosition(args),
command,
commandArgs,
commandObject;
if (pos < 0) {
return callback(inputError.create('Need a command to run'));
}
commandArgs = args.slice(0, pos);
command = args[pos];
commandArgs.push.apply(commandArgs, args.slice(pos + 1));
try {
commandObject = Command.create(command);
} catch (ex) {
errHandler(inputError.create(ex.message));
return;
}
commandObject.run(commandArgs, errHandler);
}
function runToCompletion(args) {
runCommand(args, errHandler);
}
/* istanbul ignore if: untestable */
if (require.main === module) {
var args = Array.prototype.slice.call(process.argv, 2);
runToCompletion(args);
}
module.exports = {
runToCompletion: runToCompletion
};

View File

@ -0,0 +1,162 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
"use strict";
var MemoryStore = require('./store/memory'),
utils = require('./object-utils');
/**
* a mechanism to merge multiple coverage objects into one. Handles the use case
* of overlapping coverage information for the same files in multiple coverage
* objects and does not double-count in this situation. For example, if
* you pass the same coverage object multiple times, the final merged object will be
* no different that any of the objects passed in (except for execution counts).
*
* The `Collector` is built for scale to handle thousands of coverage objects.
* By default, all processing is done in memory since the common use-case is of
* one or a few coverage objects. You can work around memory
* issues by passing in a `Store` implementation that stores temporary computations
* on disk (the `tmp` store, for example).
*
* The `getFinalCoverage` method returns an object with merged coverage information
* and is provided as a convenience for implementors working with coverage information
* that can fit into memory. Reporters, in the interest of generality, should *not* use this method for
* creating reports.
*
* Usage
* -----
*
* var collector = new require('istanbul').Collector();
*
* files.forEach(function (f) {
* //each coverage object can have overlapping information about multiple files
* collector.add(JSON.parse(fs.readFileSync(f, 'utf8')));
* });
*
* collector.files().forEach(function(file) {
* var fileCoverage = collector.fileCoverageFor(file);
* console.log('Coverage for ' + file + ' is:' + JSON.stringify(fileCoverage));
* });
*
* // convenience method: do not use this when dealing with a large number of files
* var finalCoverage = collector.getFinalCoverage();
*
* @class Collector
* @module main
* @constructor
* @param {Object} options Optional. Configuration options.
* @param {Store} options.store - an implementation of `Store` to use for temporary
* calculations.
*/
function Collector(options) {
options = options || {};
this.store = options.store || new MemoryStore();
}
Collector.prototype = {
/**
* adds a coverage object to the collector.
*
* @method add
* @param {Object} coverage the coverage object.
* @param {String} testName Optional. The name of the test used to produce the object.
* This is currently not used.
*/
add: function (coverage /*, testName */) {
var store = this.store;
Object.keys(coverage).forEach(function (key) {
var fileCoverage = coverage[key];
if (store.hasKey(key)) {
store.setObject(key, utils.mergeFileCoverage(fileCoverage, store.getObject(key)));
} else {
store.setObject(key, fileCoverage);
}
});
},
/**
* returns a list of unique file paths for which coverage information has been added.
* @method files
* @return {Array} an array of file paths for which coverage information is present.
*/
files: function () {
return this.store.keys();
},
/**
* return file coverage information for a single file
* @method fileCoverageFor
* @param {String} fileName the path for the file for which coverage information is
* required. Must be one of the values returned in the `files()` method.
* @return {Object} the coverage information for the specified file.
*/
fileCoverageFor: function (fileName) {
var ret = this.store.getObject(fileName);
utils.addDerivedInfoForFile(ret);
var mod = {
path: ret.path,
};
function doFilterMap(key, filter) {
var original = ret[key];
var keys = Object.keys(original);
mod[key] = makeObj(original, keys.filter(filter));
}
function makeObj(original, keys) {
var ret = {};
keys.forEach(function (k) {
ret[k] = original[k];
});
return ret;
}
function filterLine(i) {
return i > 0;
}
function filterStatement(i) {
return ret.statementMap[i].start.line > 0;
}
function filterFunction(i) {
return ret.fnMap[i].line > 0;
}
function filterBranch(i) {
return ret.branchMap[i].line > 0;
}
doFilterMap('s', filterStatement);
doFilterMap('f', filterFunction);
doFilterMap('l', filterLine);
doFilterMap('b', filterBranch);
doFilterMap('statementMap', filterStatement);
doFilterMap('fnMap', filterFunction);
doFilterMap('branchMap', filterBranch);
return mod;
},
/**
* returns file coverage information for all files. This has the same format as
* any of the objects passed in to the `add` method. The number of keys in this
* object will be a superset of all keys found in the objects passed to `add()`
* @method getFinalCoverage
* @return {Object} the merged coverage information
*/
getFinalCoverage: function () {
var ret = {},
that = this;
this.files().forEach(function (file) {
ret[file] = that.fileCoverageFor(file);
});
return ret;
},
/**
* disposes this collector and reclaims temporary resources used in the
* computation. Calls `dispose()` on the underlying store.
* @method dispose
*/
dispose: function () {
this.store.dispose();
}
};
module.exports = Collector;

View File

@ -0,0 +1,195 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var nopt = require('nopt'),
path = require('path'),
fs = require('fs'),
Collector = require('../collector'),
formatOption = require('../util/help-formatter').formatOption,
util = require('util'),
utils = require('../object-utils'),
filesFor = require('../util/file-matcher').filesFor,
Command = require('./index'),
configuration = require('../config');
function isAbsolute(file) {
if (path.isAbsolute) {
return path.isAbsolute(file);
}
return path.resolve(file) === path.normalize(file);
}
function CheckCoverageCommand() {
Command.call(this);
}
function removeFiles(covObj, root, files) {
var filesObj = {},
obj = {};
// Create lookup table.
files.forEach(function (file) {
filesObj[file] = true;
});
Object.keys(covObj).forEach(function (key) {
// Exclude keys will always be relative, but covObj keys can be absolute or relative
var excludeKey = isAbsolute(key) ? path.relative(root, key) : key;
// Also normalize for files that start with `./`, etc.
excludeKey = path.normalize(excludeKey);
if (filesObj[excludeKey] !== true) {
obj[key] = covObj[key];
}
});
return obj;
}
CheckCoverageCommand.TYPE = 'check-coverage';
util.inherits(CheckCoverageCommand, Command);
Command.mix(CheckCoverageCommand, {
synopsis: function () {
return "checks overall/per-file coverage against thresholds from coverage JSON files. Exits 1 if thresholds are not met, 0 otherwise";
},
usage: function () {
console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> [<include-pattern>]\n\nOptions are:\n\n' +
[
formatOption('--statements <threshold>', 'global statement coverage threshold'),
formatOption('--functions <threshold>', 'global function coverage threshold'),
formatOption('--branches <threshold>', 'global branch coverage threshold'),
formatOption('--lines <threshold>', 'global line coverage threshold')
].join('\n\n') + '\n');
console.error('\n\n');
console.error('Thresholds, when specified as a positive number are taken to be the minimum percentage required.');
console.error('When a threshold is specified as a negative number it represents the maximum number of uncovered entities allowed.\n');
console.error('For example, --statements 90 implies minimum statement coverage is 90%.');
console.error(' --statements -10 implies that no more than 10 uncovered statements are allowed\n');
console.error('Per-file thresholds can be specified via a configuration file.\n');
console.error('<include-pattern> is a fileset pattern that can be used to select one or more coverage files ' +
'for merge. This defaults to "**/coverage*.json"');
console.error('\n');
},
run: function (args, callback) {
var template = {
config: path,
root: path,
statements: Number,
lines: Number,
branches: Number,
functions: Number,
verbose: Boolean
},
opts = nopt(template, { v : '--verbose' }, args, 0),
// Translate to config opts.
config = configuration.loadFile(opts.config, {
verbose: opts.verbose,
check: {
global: {
statements: opts.statements,
lines: opts.lines,
branches: opts.branches,
functions: opts.functions
}
}
}),
includePattern = '**/coverage*.json',
root,
collector = new Collector(),
errors = [];
if (opts.argv.remain.length > 0) {
includePattern = opts.argv.remain[0];
}
root = opts.root || process.cwd();
filesFor({
root: root,
includes: [ includePattern ]
}, function (err, files) {
if (err) { throw err; }
if (files.length === 0) {
return callback('ERROR: No coverage files found.');
}
files.forEach(function (file) {
var coverageObject = JSON.parse(fs.readFileSync(file, 'utf8'));
collector.add(coverageObject);
});
var thresholds = {
global: {
statements: config.check.global.statements || 0,
branches: config.check.global.branches || 0,
lines: config.check.global.lines || 0,
functions: config.check.global.functions || 0,
excludes: config.check.global.excludes || []
},
each: {
statements: config.check.each.statements || 0,
branches: config.check.each.branches || 0,
lines: config.check.each.lines || 0,
functions: config.check.each.functions || 0,
excludes: config.check.each.excludes || []
}
},
rawCoverage = collector.getFinalCoverage(),
globalResults = utils.summarizeCoverage(removeFiles(rawCoverage, root, thresholds.global.excludes)),
eachResults = removeFiles(rawCoverage, root, thresholds.each.excludes);
// Summarize per-file results and mutate original results.
Object.keys(eachResults).forEach(function (key) {
eachResults[key] = utils.summarizeFileCoverage(eachResults[key]);
});
if (config.verbose) {
console.log('Compare actuals against thresholds');
console.log(JSON.stringify({ global: globalResults, each: eachResults, thresholds: thresholds }, undefined, 4));
}
function check(name, thresholds, actuals) {
[
"statements",
"branches",
"lines",
"functions"
].forEach(function (key) {
var actual = actuals[key].pct,
actualUncovered = actuals[key].total - actuals[key].covered,
threshold = thresholds[key];
if (threshold < 0) {
if (threshold * -1 < actualUncovered) {
errors.push('ERROR: Uncovered count for ' + key + ' (' + actualUncovered +
') exceeds ' + name + ' threshold (' + -1 * threshold + ')');
}
} else {
if (actual < threshold) {
errors.push('ERROR: Coverage for ' + key + ' (' + actual +
'%) does not meet ' + name + ' threshold (' + threshold + '%)');
}
}
});
}
check("global", thresholds.global, globalResults);
Object.keys(eachResults).forEach(function (key) {
check("per-file" + " (" + key + ") ", thresholds.each, eachResults[key]);
});
return callback(errors.length === 0 ? null : errors.join("\n"));
});
}
});
module.exports = CheckCoverageCommand;

View File

@ -0,0 +1,268 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Module = require('module'),
path = require('path'),
fs = require('fs'),
nopt = require('nopt'),
which = require('which'),
mkdirp = require('mkdirp'),
existsSync = fs.existsSync || path.existsSync,
inputError = require('../../util/input-error'),
matcherFor = require('../../util/file-matcher').matcherFor,
Instrumenter = require('../../instrumenter'),
Collector = require('../../collector'),
formatOption = require('../../util/help-formatter').formatOption,
hook = require('../../hook'),
Reporter = require('../../reporter'),
resolve = require('resolve'),
configuration = require('../../config');
function usage(arg0, command) {
console.error('\nUsage: ' + arg0 + ' ' + command + ' [<options>] <executable-js-file-or-command> [-- <arguments-to-jsfile>]\n\nOptions are:\n\n'
+ [
formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'),
formatOption('--babel-config <path-to-config>', 'a babel specific configuration file, same as .babelrc, supports YAML'),
formatOption('--root <path> ', 'the root path to look for files to instrument, defaults to .'),
formatOption('-x <exclude-pattern> [-x <exclude-pattern>]', 'one or more fileset patterns e.g. "**/vendor/**"'),
formatOption('-i <include-pattern> [-i <include-pattern>]', 'one or more fileset patterns e.g. "**/*.js"'),
formatOption('--[no-]default-excludes', 'apply default excludes [ **/node_modules/**, **/test/**, **/tests/** ], defaults to true'),
formatOption('--hook-run-in-context', 'hook vm.runInThisContext in addition to require (supports RequireJS), defaults to false'),
formatOption('--post-require-hook <file> | <module>', 'JS module that exports a function for post-require processing'),
formatOption('--report <format> [--report <format>] ', 'report format, defaults to lcov (= lcov.info + HTML)'),
formatOption('--dir <report-dir>', 'report directory, defaults to ./coverage'),
formatOption('--print <type>', 'type of report to print to console, one of summary (default), detail, both or none'),
formatOption('--verbose, -v', 'verbose mode'),
formatOption('--[no-]preserve-comments', 'remove / preserve comments in the output, defaults to false'),
formatOption('--include-all-sources', 'instrument all unused sources after running tests, defaults to false'),
formatOption('--[no-]include-pid', 'include PID in output coverage filename')
].join('\n\n') + '\n');
console.error('\n');
}
function run(args, commandName, enableHooks, callback) {
var template = {
config: path,
'babel-config': path,
root: path,
x: [ Array, String ],
report: [Array, String ],
dir: path,
verbose: Boolean,
yui: Boolean,
'default-excludes': Boolean,
print: String,
'self-test': Boolean,
'hook-run-in-context': Boolean,
'post-require-hook': String,
'preserve-comments': Boolean,
'include-all-sources': Boolean,
'preload-sources': Boolean,
'include-pid': Boolean,
i: [ Array, String ]
},
opts = nopt(template, { v : '--verbose' }, args, 0),
overrides = {
verbose: opts.verbose,
instrumentation: {
root: opts.root,
'default-excludes': opts['default-excludes'],
excludes: opts.x,
'include-all-sources': opts['include-all-sources'],
'preload-sources': opts['preload-sources'],
'include-pid': opts['include-pid']
},
reporting: {
reports: opts.report,
print: opts.print,
dir: opts.dir
},
hooks: {
'hook-run-in-context': opts['hook-run-in-context'],
'post-require-hook': opts['post-require-hook'],
'handle-sigint': opts['handle-sigint']
}
},
config = configuration.loadFile(opts.config, overrides),
babelConfig = opts['babel-config'] ? configuration.readFile(opts['babel-config']) : {},
verbose = config.verbose,
cmdAndArgs = opts.argv.remain,
preserveComments = opts['preserve-comments'],
includePid = opts['include-pid'],
cmd,
cmdArgs,
reportingDir,
reporter = new Reporter(config),
runFn,
excludes;
if (cmdAndArgs.length === 0) {
return callback(inputError.create('Need a filename argument for the ' + commandName + ' command!'));
}
cmd = cmdAndArgs.shift();
cmdArgs = cmdAndArgs;
if (!existsSync(cmd)) {
try {
cmd = which.sync(cmd);
} catch (ex) {
return callback(inputError.create('Unable to resolve file [' + cmd + ']'));
}
} else {
cmd = path.resolve(cmd);
}
runFn = function () {
process.argv = ["node", cmd].concat(cmdArgs);
if (verbose) {
console.log('Running: ' + process.argv.join(' '));
}
process.env.running_under_istanbul=1;
Module.runMain(cmd, null, true);
};
excludes = config.instrumentation.excludes(true);
if (enableHooks) {
reportingDir = path.resolve(config.reporting.dir());
mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
reporter.dir = reportingDir;
reporter.addAll(config.reporting.reports());
if (config.reporting.print() !== 'none') {
switch (config.reporting.print()) {
case 'detail':
reporter.add('text');
break;
case 'both':
reporter.add('text');
reporter.add('text-summary');
break;
default:
reporter.add('text-summary');
break;
}
}
excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
matcherFor({
root: config.instrumentation.root() || process.cwd(),
includes: opts.i || config.instrumentation.extensions().map(function (ext) {
return '**/*' + ext;
}),
excludes: excludes
},
function (err, matchFn) {
if (err) { return callback(err); }
var coverageVar = '$$cov_' + new Date().getTime() + '$$',
instrumenter = new Instrumenter({
babelConfig: babelConfig,
coverageVariable: coverageVar,
preserveComments: preserveComments
}),
transformer = instrumenter.instrumentSync.bind(instrumenter),
hookOpts = { verbose: verbose, extensions: config.instrumentation.extensions() },
postRequireHook = config.hooks.postRequireHook(),
postLoadHookFile;
if (postRequireHook) {
postLoadHookFile = path.resolve(postRequireHook);
} else if (opts.yui) { //EXPERIMENTAL code: do not rely on this in anyway until the docs say it is allowed
postLoadHookFile = path.resolve(__dirname, '../../util/yui-load-hook');
}
if (postRequireHook) {
if (!existsSync(postLoadHookFile)) { //assume it is a module name and resolve it
try {
postLoadHookFile = resolve.sync(postRequireHook, { basedir: process.cwd() });
} catch (ex) {
if (verbose) { console.error('Unable to resolve [' + postRequireHook + '] as a node module'); }
callback(ex);
return;
}
}
}
if (postLoadHookFile) {
if (verbose) { console.error('Use post-load-hook: ' + postLoadHookFile); }
hookOpts.postLoadHook = require(postLoadHookFile)(matchFn, transformer, verbose);
}
if (opts['self-test']) {
hook.unloadRequireCache(matchFn);
}
// runInThisContext is used by RequireJS [issue #23]
if (config.hooks.hookRunInContext()) {
hook.hookRunInThisContext(matchFn, transformer, hookOpts);
}
hook.hookRequire(matchFn, transformer, hookOpts);
//initialize the global variable to stop mocha from complaining about leaks
global[coverageVar] = {};
// enable passing --handle-sigint to write reports on SIGINT.
// This allows a user to manually kill a process while
// still getting the istanbul report.
if (config.hooks.handleSigint()) {
process.once('SIGINT', process.exit);
}
process.once('exit', function () {
var pidExt = includePid ? ('-' + process.pid) : '',
file = path.resolve(reportingDir, 'coverage' + pidExt + '.json'),
collector,
cov;
if (typeof global[coverageVar] === 'undefined' || Object.keys(global[coverageVar]).length === 0) {
console.error('No coverage information was collected, exit without writing coverage information');
return;
} else {
cov = global[coverageVar];
}
//important: there is no event loop at this point
//everything that happens in this exit handler MUST be synchronous
if (config.instrumentation.includeAllSources()) {
// Files that are not touched by code ran by the test runner is manually instrumented, to
// illustrate the missing coverage.
matchFn.files.forEach(function (file) {
if (!cov[file]) {
transformer(fs.readFileSync(file, 'utf-8'), file);
// When instrumenting the code, istanbul will give each FunctionDeclaration a value of 1 in coverState.s,
// presumably to compensate for function hoisting. We need to reset this, as the function was not hoisted,
// as it was never loaded.
Object.keys(instrumenter.coverState.s).forEach(function (key) {
instrumenter.coverState.s[key] = 0;
});
cov[file] = instrumenter.coverState;
}
});
}
mkdirp.sync(reportingDir); //yes, do this again since some test runners could clean the dir initially created
if (config.reporting.print() !== 'none') {
console.error('=============================================================================');
console.error('Writing coverage object [' + file + ']');
}
fs.writeFileSync(file, JSON.stringify(cov), 'utf8');
collector = new Collector();
collector.add(cov);
if (config.reporting.print() !== 'none') {
console.error('Writing coverage reports at [' + reportingDir + ']');
console.error('=============================================================================');
}
reporter.write(collector, true, callback);
});
runFn();
});
} else {
runFn();
}
}
module.exports = {
run: run,
usage: usage
};

View File

@ -0,0 +1,33 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var runWithCover = require('./common/run-with-cover'),
util = require('util'),
Command = require('./index');
function CoverCommand() {
Command.call(this);
}
CoverCommand.TYPE = 'cover';
util.inherits(CoverCommand, Command);
Command.mix(CoverCommand, {
synopsis: function () {
return "transparently adds coverage information to a node command. Saves coverage.json and reports at the end of execution";
},
usage: function () {
runWithCover.usage(this.toolName(), this.type());
},
run: function (args, callback) {
runWithCover.run(args, this.type(), true, callback);
}
});
module.exports = CoverCommand;

View File

@ -0,0 +1,102 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Command = require('./index.js'),
util = require('util'),
formatOption = require('../util/help-formatter').formatOption,
VERSION = require('../../index').VERSION,
configuration = require('../config'),
yaml = require('js-yaml'),
formatPara = require('../util/help-formatter').formatPara;
function showConfigHelp(toolName) {
console.error('\nConfiguring ' + toolName);
console.error('====================');
console.error('\n' +
formatPara(toolName + ' can be configured globally using a .istanbul.yml YAML file ' +
'at the root of your source tree. Every command also accepts a --config=<config-file> argument to ' +
'customize its location per command. The alternate config file can be in YAML, JSON or node.js ' +
'(exporting the config object).'));
console.error('\n' +
formatPara('The config file currently has four sections for instrumentation, reporting, hooks, ' +
'and checking. Note that certain commands (like `cover`) use information from multiple sections.'));
console.error('\n' +
formatPara('Keys in the config file usually correspond to command line parameters with the same name. ' +
'The verbose option for every command shows you the exact configuration used. See the api ' +
'docs for an explanation of each key.'));
console.error('\n' +
formatPara('You only need to specify the keys that you want to override. Your overrides will be merged ' +
'with the default config.'));
console.error('\nThe default configuration is as follows:\n');
console.error(yaml.safeDump(configuration.defaultConfig(), { indent: 4, flowLevel: 3 }));
console.error('\n' +
formatPara('The `watermarks` section does not have a command line equivalent. It allows you to set up ' +
'low and high watermark percentages for reporting. These are honored by all reporters that colorize ' +
'their output based on low/ medium/ high coverage.'));
console.error('\n' +
formatPara('The `reportConfig` section allows you to configure each report format independently ' +
'and has no command-line equivalent either.'));
console.error('\n' +
formatPara('The `check` section configures minimum threshold enforcement for coverage results. ' +
'`global` applies to all files together and `each` on a per-file basis. A list of files can ' +
'be excluded from enforcement relative to root via the `exclude` property.'));
console.error('');
}
function HelpCommand() {
Command.call(this);
}
HelpCommand.TYPE = 'help';
util.inherits(HelpCommand, Command);
Command.mix(HelpCommand, {
synopsis: function () {
return "shows help";
},
usage: function () {
console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' config | <command>\n');
console.error('`config` provides help with istanbul configuration\n');
console.error('Available commands are:\n');
var commandObj;
Command.getCommandList().forEach(function (cmd) {
commandObj = Command.create(cmd);
console.error(formatOption(cmd, commandObj.synopsis()));
console.error("\n");
});
console.error("Command names can be abbreviated as long as the abbreviation is unambiguous");
console.error(this.toolName() + ' version:' + VERSION);
console.error("\n");
},
run: function (args, callback) {
var command;
if (args.length === 0) {
this.usage();
} else {
if (args[0] === 'config') {
showConfigHelp(this.toolName());
} else {
try {
command = Command.create(args[0]);
command.usage('istanbul', Command.resolveCommandName(args[0]));
} catch (ex) {
console.error('Invalid command: ' + args[0]);
this.usage();
}
}
}
return callback();
}
});
module.exports = HelpCommand;

View File

@ -0,0 +1,33 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Factory = require('../util/factory'),
factory = new Factory('command', __dirname, true);
function Command() {}
// add register, create, mix, loadAll, getCommandList, resolveCommandName to the Command object
factory.bindClassMethods(Command);
Command.prototype = {
toolName: function () {
return require('../util/meta').NAME;
},
type: function () {
return this.constructor.TYPE;
},
synopsis: /* istanbul ignore next: base method */ function () {
return "the developer has not written a one-line summary of the " + this.type() + " command";
},
usage: /* istanbul ignore next: base method */ function () {
console.error("the developer has not provided a usage for the " + this.type() + " command");
},
run: /* istanbul ignore next: abstract method */ function (args, callback) {
return callback(new Error("run: must be overridden for the " + this.type() + " command"));
}
};
module.exports = Command;

View File

@ -0,0 +1,265 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
mkdirp = require('mkdirp'),
once = require('once'),
async = require('async'),
fs = require('fs'),
filesFor = require('../util/file-matcher').filesFor,
nopt = require('nopt'),
Instrumenter = require('../instrumenter'),
inputError = require('../util/input-error'),
formatOption = require('../util/help-formatter').formatOption,
util = require('util'),
Command = require('./index'),
Collector = require('../collector'),
configuration = require('../config'),
verbose;
/*
* Chunk file size to use when reading non JavaScript files in memory
* and copying them over when using complete-copy flag.
*/
var READ_FILE_CHUNK_SIZE = 64 * 1024;
function BaselineCollector(instrumenter) {
this.instrumenter = instrumenter;
this.collector = new Collector();
this.instrument = instrumenter.instrument.bind(this.instrumenter);
var origInstrumentSync = instrumenter.instrumentSync;
this.instrumentSync = function () {
var args = Array.prototype.slice.call(arguments),
ret = origInstrumentSync.apply(this.instrumenter, args),
baseline = this.instrumenter.lastFileCoverage(),
coverage = {};
coverage[baseline.path] = baseline;
this.collector.add(coverage);
return ret;
};
//monkey patch the instrumenter to call our version instead
instrumenter.instrumentSync = this.instrumentSync.bind(this);
}
BaselineCollector.prototype = {
getCoverage: function () {
return this.collector.getFinalCoverage();
}
};
function processFiles(instrumenter, inputDir, outputDir, relativeNames, extensions) {
var processor = function (name, callback) {
var inputFile = path.resolve(inputDir, name),
outputFile = path.resolve(outputDir, name),
inputFileExtenstion = path.extname(inputFile),
isJavaScriptFile = extensions.indexOf(inputFileExtenstion) > -1,
oDir = path.dirname(outputFile),
readStream, writeStream;
callback = once(callback);
mkdirp.sync(oDir);
if (fs.statSync(inputFile).isDirectory()) {
return callback(null, name);
}
if (isJavaScriptFile) {
fs.readFile(inputFile, 'utf8', function (err, data) {
if (err) { return callback(err, name); }
instrumenter.instrument(data, inputFile, function (iErr, instrumented) {
if (iErr) { return callback(iErr, name); }
fs.writeFile(outputFile, instrumented, 'utf8', function (err) {
return callback(err, name);
});
});
});
}
else {
// non JavaScript file, copy it as is
readStream = fs.createReadStream(inputFile, {'bufferSize': READ_FILE_CHUNK_SIZE});
writeStream = fs.createWriteStream(outputFile);
readStream.on('error', callback);
writeStream.on('error', callback);
readStream.pipe(writeStream);
readStream.on('end', function() {
callback(null, name);
});
}
},
q = async.queue(processor, 10),
errors = [],
count = 0,
startTime = new Date().getTime();
q.push(relativeNames, function (err, name) {
var inputFile, outputFile;
if (err) {
errors.push({ file: name, error: err.message || err.toString() });
inputFile = path.resolve(inputDir, name);
outputFile = path.resolve(outputDir, name);
fs.writeFileSync(outputFile, fs.readFileSync(inputFile));
}
if (verbose) {
console.log('Processed: ' + name);
} else {
if (count % 100 === 0) { process.stdout.write('.'); }
}
count += 1;
});
q.drain = function () {
var endTime = new Date().getTime();
console.log('\nProcessed [' + count + '] files in ' + Math.floor((endTime - startTime) / 1000) + ' secs');
if (errors.length > 0) {
console.log('The following ' + errors.length + ' file(s) had errors and were copied as-is');
console.log(errors);
}
};
}
function InstrumentCommand() {
Command.call(this);
}
InstrumentCommand.TYPE = 'instrument';
util.inherits(InstrumentCommand, Command);
Command.mix(InstrumentCommand, {
synopsis: function synopsis() {
return "instruments a file or a directory tree and writes the instrumented code to the desired output location";
},
usage: function () {
console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> <file-or-directory>\n\nOptions are:\n\n' +
[
formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'),
formatOption('--output <file-or-dir>', 'The output file or directory. This is required when the input is a directory, ' +
'defaults to standard output when input is a file'),
formatOption('-x <exclude-pattern> [-x <exclude-pattern>]', 'one or more fileset patterns (e.g. "**/vendor/**" to ignore all files ' +
'under a vendor directory). Also see the --default-excludes option'),
formatOption('--variable <global-coverage-variable-name>', 'change the variable name of the global coverage variable from the ' +
'default value of `__coverage__` to something else'),
formatOption('--embed-source', 'embed source code into the coverage object, defaults to false'),
formatOption('--[no-]compact', 'produce [non]compact output, defaults to compact'),
formatOption('--[no-]preserve-comments', 'remove / preserve comments in the output, defaults to false'),
formatOption('--[no-]complete-copy', 'also copy non-javascript files to the ouput directory as is, defaults to false'),
formatOption('--save-baseline', 'produce a baseline coverage.json file out of all files instrumented'),
formatOption('--baseline-file <file>', 'filename of baseline file, defaults to coverage/coverage-baseline.json'),
formatOption('--es-modules', 'source code uses es import/export module syntax')
].join('\n\n') + '\n');
console.error('\n');
},
run: function (args, callback) {
var template = {
config: path,
output: path,
x: [Array, String],
variable: String,
compact: Boolean,
'complete-copy': Boolean,
verbose: Boolean,
'save-baseline': Boolean,
'baseline-file': path,
'embed-source': Boolean,
'preserve-comments': Boolean,
'es-modules': Boolean
},
opts = nopt(template, { v : '--verbose' }, args, 0),
overrides = {
verbose: opts.verbose,
instrumentation: {
variable: opts.variable,
compact: opts.compact,
'embed-source': opts['embed-source'],
'preserve-comments': opts['preserve-comments'],
excludes: opts.x,
'complete-copy': opts['complete-copy'],
'save-baseline': opts['save-baseline'],
'baseline-file': opts['baseline-file'],
'es-modules': opts['es-modules']
}
},
config = configuration.loadFile(opts.config, overrides),
iOpts = config.instrumentation,
cmdArgs = opts.argv.remain,
file,
stats,
stream,
includes,
instrumenter,
needBaseline = iOpts.saveBaseline(),
baselineFile = path.resolve(iOpts.baselineFile()),
output = opts.output;
verbose = config.verbose;
if (cmdArgs.length !== 1) {
return callback(inputError.create('Need exactly one filename/ dirname argument for the instrument command!'));
}
if (iOpts.completeCopy()) {
includes = ['**/*'];
}
else {
includes = iOpts.extensions().map(function(ext) {
return '**/*' + ext;
});
}
instrumenter = new Instrumenter({
coverageVariable: iOpts.variable(),
embedSource: iOpts.embedSource(),
noCompact: !iOpts.compact(),
preserveComments: iOpts.preserveComments(),
esModules: iOpts.esModules()
});
if (needBaseline) {
mkdirp.sync(path.dirname(baselineFile));
instrumenter = new BaselineCollector(instrumenter);
process.on('exit', function () {
console.log('Saving baseline coverage at: ' + baselineFile);
fs.writeFileSync(baselineFile, JSON.stringify(instrumenter.getCoverage()), 'utf8');
});
}
file = path.resolve(cmdArgs[0]);
stats = fs.statSync(file);
if (stats.isDirectory()) {
if (!output) { return callback(inputError.create('Need an output directory [-o <dir>] when input is a directory!')); }
if (output === file) { return callback(inputError.create('Cannot instrument into the same directory/ file as input!')); }
mkdirp.sync(output);
filesFor({
root: file,
includes: includes,
excludes: opts.x || iOpts.excludes(false), // backwards-compat, *sigh*
relative: true
}, function (err, files) {
if (err) { return callback(err); }
processFiles(instrumenter, file, output, files, iOpts.extensions());
});
} else {
if (output) {
stream = fs.createWriteStream(output);
} else {
stream = process.stdout;
}
stream.write(instrumenter.instrumentSync(fs.readFileSync(file, 'utf8'), file));
if (stream !== process.stdout) {
stream.end();
}
}
}
});
module.exports = InstrumentCommand;

View File

@ -0,0 +1,123 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var nopt = require('nopt'),
Report = require('../report'),
Reporter = require('../reporter'),
path = require('path'),
fs = require('fs'),
Collector = require('../collector'),
helpFormatter = require('../util/help-formatter'),
formatOption = helpFormatter.formatOption,
formatPara = helpFormatter.formatPara,
filesFor = require('../util/file-matcher').filesFor,
util = require('util'),
Command = require('./index'),
configuration = require('../config');
function ReportCommand() {
Command.call(this);
}
ReportCommand.TYPE = 'report';
util.inherits(ReportCommand, Command);
function printDeprecationMessage(pat, fmt) {
console.error('**********************************************************************');
console.error('DEPRECATION WARNING! You are probably using the old format of the report command');
console.error('This will stop working soon, see `babel-istanbul help report` for the new command format');
console.error('Assuming you meant: babel-istanbul report --include=' + pat + ' ' + fmt);
console.error('**********************************************************************');
}
Command.mix(ReportCommand, {
synopsis: function () {
return "writes reports for coverage JSON objects produced in a previous run";
},
usage: function () {
console.error('\nUsage: ' + this.toolName() + ' ' + this.type() + ' <options> [ <format> ... ]\n\nOptions are:\n\n' +
[
formatOption('--config <path-to-config>', 'the configuration file to use, defaults to .istanbul.yml'),
formatOption('--root <input-directory>', 'The input root directory for finding coverage files'),
formatOption('--dir <report-directory>', 'The output directory where files will be written. This defaults to ./coverage/'),
formatOption('--include <glob>', 'The fileset pattern to select one or more coverage files, defaults to **/coverage*.json'),
formatOption('--verbose, -v', 'verbose mode')
].join('\n\n'));
console.error('\n');
console.error('<format> is one of ');
Report.getReportList().forEach(function (name) {
console.error(formatOption(name, Report.create(name).synopsis()));
});
console.error("");
console.error(formatPara([
'Default format is lcov unless otherwise specified in the config file.',
'In addition you can tweak the file names for various reports using the config file.',
'Type `babel-istanbul help config` to see what can be tweaked.'
].join(' ')));
console.error('\n');
},
run: function (args, callback) {
var template = {
config: path,
root: path,
dir: path,
include: String,
verbose: Boolean
},
opts = nopt(template, { v : '--verbose' }, args, 0),
includePattern = opts.include || '**/coverage*.json',
root,
collector = new Collector(),
config = configuration.loadFile(opts.config, {
verbose: opts.verbose,
reporting: {
dir: opts.dir
}
}),
formats = opts.argv.remain,
reporter = new Reporter(config);
// Start: backward compatible processing
if (formats.length === 2 &&
Report.getReportList().indexOf(formats[1]) < 0) {
includePattern = formats[1];
formats = [ formats[0] ];
printDeprecationMessage(includePattern, formats[0]);
}
// End: backward compatible processing
if (formats.length === 0) {
formats = config.reporting.reports();
}
if (formats.length === 0) {
formats = [ 'lcov' ];
}
reporter.addAll(formats);
root = opts.root || process.cwd();
filesFor({
root: root,
includes: [ includePattern ]
}, function (err, files) {
if (err) { throw err; }
files.forEach(function (file) {
var coverageObject = JSON.parse(fs.readFileSync(file, 'utf8'));
collector.add(coverageObject);
});
reporter.write(collector, false, function (err) {
console.log('Done');
return callback(err);
});
});
}
});
module.exports = ReportCommand;

View File

@ -0,0 +1,31 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var runWithCover = require('./common/run-with-cover'),
util = require('util'),
Command = require('./index');
function TestCommand() {
Command.call(this);
}
TestCommand.TYPE = 'test';
util.inherits(TestCommand, Command);
Command.mix(TestCommand, {
synopsis: function () {
return "cover a node command only when npm_config_coverage is set. Use in an `npm test` script for conditional coverage";
},
usage: function () {
runWithCover.usage(this.toolName(), this.type());
},
run: function (args, callback) {
runWithCover.run(args, this.type(), !!process.env.npm_config_coverage, callback);
}
});
module.exports = TestCommand;

View File

@ -0,0 +1,502 @@
/*
Copyright (c) 2013, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
fs = require('fs'),
existsSync = fs.existsSync || path.existsSync,
CAMEL_PATTERN = /([a-z])([A-Z])/g,
YML_PATTERN = /\.ya?ml$/,
yaml = require('js-yaml'),
defaults = require('./report/common/defaults');
function defaultConfig(includeBackCompatAttrs) {
var ret = {
verbose: false,
instrumentation: {
root: '.',
extensions: ['.js','.jsx','.es6','.es'],
'default-excludes': true,
excludes: [],
'embed-source': false,
variable: '__coverage__',
compact: true,
'preserve-comments': false,
'complete-copy': false,
'save-baseline': false,
'baseline-file': './coverage/coverage-baseline.json',
'include-all-sources': false,
'include-pid': false,
'es-modules': false
},
reporting: {
print: 'summary',
reports: [ 'lcov' ],
dir: './coverage'
},
hooks: {
'hook-run-in-context': false,
'post-require-hook': null,
'handle-sigint': false
},
check: {
global: {
statements: 0,
lines: 0,
branches: 0,
functions: 0,
excludes: [] // Currently list of files (root + path). For future, extend to patterns.
},
each: {
statements: 0,
lines: 0,
branches: 0,
functions: 0,
excludes: []
}
}
};
ret.reporting.watermarks = defaults.watermarks();
ret.reporting['report-config'] = defaults.defaultReportConfig();
if (includeBackCompatAttrs) {
ret.instrumentation['preload-sources'] = false;
}
return ret;
}
function dasherize(word) {
return word.replace(CAMEL_PATTERN, function (match, lch, uch) {
return lch + '-' + uch.toLowerCase();
});
}
function isScalar(v) {
if (v === null) { return true; }
return v !== undefined && !Array.isArray(v) && typeof v !== 'object';
}
function isObject(v) {
return typeof v === 'object' && v !== null && !Array.isArray(v);
}
function mergeObjects(explicit, template) {
var ret = {};
Object.keys(template).forEach(function (k) {
var v1 = template[k],
v2 = explicit[k];
if (Array.isArray(v1)) {
ret[k] = Array.isArray(v2) && v2.length > 0 ? v2 : v1;
} else if (isObject(v1)) {
v2 = isObject(v2) ? v2 : {};
ret[k] = mergeObjects(v2, v1);
} else {
ret[k] = isScalar(v2) ? v2 : v1;
}
});
return ret;
}
function mergeDefaults(explicit, implicit) {
return mergeObjects(explicit || {}, implicit);
}
function addMethods() {
var args = Array.prototype.slice.call(arguments),
cons = args.shift();
args.forEach(function (arg) {
var method = arg,
property = dasherize(arg);
cons.prototype[method] = function () {
return this.config[property];
};
});
}
/**
* Object that returns instrumentation options
* @class InstrumentOptions
* @module config
* @constructor
* @param config the instrumentation part of the config object
*/
function InstrumentOptions(config) {
if (config['preload-sources']) {
console.error('The preload-sources option is deprecated, please use include-all-sources instead.');
config['include-all-sources'] = config['preload-sources'];
}
this.config = config;
}
/**
* returns if default excludes should be turned on. Used by the `cover` command.
* @method defaultExcludes
* @return {Boolean} true if default excludes should be turned on
*/
/**
* returns if non-JS files should be copied during instrumentation. Used by the
* `instrument` command.
* @method completeCopy
* @return {Boolean} true if non-JS files should be copied
*/
/**
* returns if the source should be embedded in the instrumented code. Used by the
* `instrument` command.
* @method embedSource
* @return {Boolean} true if the source should be embedded in the instrumented code
*/
/**
* the coverage variable name to use. Used by the `instrument` command.
* @method variable
* @return {String} the coverage variable name to use
*/
/**
* returns if the output should be compact JS. Used by the `instrument` command.
* @method compact
* @return {Boolean} true if the output should be compact
*/
/**
* returns if comments should be preserved in the generated JS. Used by the
* `cover` and `instrument` commands.
* @method preserveComments
* @return {Boolean} true if comments should be preserved in the generated JS
*/
/**
* returns if a zero-coverage baseline file should be written as part of
* instrumentation. This allows reporting to display numbers for files that have
* no tests. Used by the `instrument` command.
* @method saveBaseline
* @return {Boolean} true if a baseline coverage file should be written.
*/
/**
* Sets the baseline coverage filename. Used by the `instrument` command.
* @method baselineFile
* @return {String} the name of the baseline coverage file.
*/
/**
* returns if comments the JS to instrument contains es6 Module syntax.
* @method esModules
* @return {Boolean} true if code contains es6 import/export statements.
*/
/**
* returns if the coverage filename should include the PID. Used by the `instrument` command.
* @method includePid
* @return {Boolean} true to include pid in coverage filename.
*/
addMethods(InstrumentOptions,
'extensions', 'defaultExcludes', 'completeCopy',
'embedSource', 'variable', 'compact', 'preserveComments',
'saveBaseline', 'baselineFile', 'esModules',
'includeAllSources', 'includePid');
/**
* returns the root directory used by istanbul which is typically the root of the
* source tree. Used by the `cover` and `report` commands.
* @method root
* @return {String} the root directory used by istanbul.
*/
InstrumentOptions.prototype.root = function () { return path.resolve(this.config.root); };
/**
* returns an array of fileset patterns that should be excluded for instrumentation.
* Used by the `instrument` and `cover` commands.
* @method excludes
* @return {Array} an array of fileset patterns that should be excluded for
* instrumentation.
*/
InstrumentOptions.prototype.excludes = function (excludeTests) {
var defs;
if (this.defaultExcludes()) {
defs = [ '**/node_modules/**' ];
if (excludeTests) {
defs = defs.concat(['**/test/**', '**/tests/**']);
}
return defs.concat(this.config.excludes);
}
return this.config.excludes;
};
/**
* Object that returns reporting options
* @class ReportingOptions
* @module config
* @constructor
* @param config the reporting part of the config object
*/
function ReportingOptions(config) {
this.config = config;
}
/**
* returns the kind of information to be printed on the console. May be one
* of `summary`, `detail`, `both` or `none`. Used by the
* `cover` command.
* @method print
* @return {String} the kind of information to print to the console at the end
* of the `cover` command execution.
*/
/**
* returns a list of reports that should be generated at the end of a run. Used
* by the `cover` and `report` commands.
* @method reports
* @return {Array} an array of reports that should be produced
*/
/**
* returns the directory under which reports should be generated. Used by the
* `cover` and `report` commands.
*
* @method dir
* @return {String} the directory under which reports should be generated.
*/
/**
* returns an object that has keys that are report format names and values that are objects
* containing detailed configuration for each format. Running `istanbul help config`
* will give you all the keys per report format that can be overridden.
* Used by the `cover` and `report` commands.
* @method reportConfig
* @return {Object} detailed report configuration per report format.
*/
addMethods(ReportingOptions, 'print', 'reports', 'dir', 'reportConfig');
function isInvalidMark(v, key) {
var prefix = 'Watermark for [' + key + '] :';
if (v.length !== 2) {
return prefix + 'must be an array of length 2';
}
v[0] = Number(v[0]);
v[1] = Number(v[1]);
if (isNaN(v[0]) || isNaN(v[1])) {
return prefix + 'must have valid numbers';
}
if (v[0] < 0 || v[1] < 0) {
return prefix + 'must be positive numbers';
}
if (v[1] > 100) {
return prefix + 'cannot exceed 100';
}
if (v[1] <= v[0]) {
return prefix + 'low must be less than high';
}
return null;
}
/**
* returns the low and high watermarks to be used to designate whether coverage
* is `low`, `medium` or `high`. Statements, functions, branches and lines can
* have independent watermarks. These are respected by all reports
* that color for low, medium and high coverage. See the default configuration for exact syntax
* using `istanbul help config`. Used by the `cover` and `report` commands.
*
* @method watermarks
* @return {Object} an object containing low and high watermarks for statements,
* branches, functions and lines.
*/
ReportingOptions.prototype.watermarks = function () {
var v = this.config.watermarks,
defs = defaults.watermarks(),
ret = {};
Object.keys(defs).forEach(function (k) {
var mark = v[k], //it will already be a non-zero length array because of the way the merge works
message = isInvalidMark(mark, k);
if (message) {
console.error(message);
ret[k] = defs[k];
} else {
ret[k] = mark;
}
});
return ret;
};
/**
* Object that returns hook options. Note that istanbul does not provide an
* option to hook `require`. This is always done by the `cover` command.
* @class HookOptions
* @module config
* @constructor
* @param config the hooks part of the config object
*/
function HookOptions(config) {
this.config = config;
}
/**
* returns if `vm.runInThisContext` needs to be hooked, in addition to the standard
* `require` hooks added by istanbul. This should be true for code that uses
* RequireJS for example. Used by the `cover` command.
* @method hookRunInContext
* @return {Boolean} true if `vm.runInThisContext` needs to be hooked for coverage
*/
/**
* returns a path to JS file or a dependent module that should be used for
* post-processing files after they have been required. See the `yui-istanbul` module for
* an example of a post-require hook. This particular hook modifies the yui loader when
* that file is required to add istanbul interceptors. Use by the `cover` command
*
* @method postRequireHook
* @return {String} a path to a JS file or the name of a node module that needs
* to be used as a `require` post-processor
*/
/**
* returns if istanbul needs to add a SIGINT (control-c, usually) handler to
* save coverage information. Useful for getting code coverage out of processes
* that run forever and need a SIGINT to terminate.
* @method handleSigint
* @return {Boolean} true if SIGINT needs to be hooked to write coverage information
*/
addMethods(HookOptions, 'hookRunInContext', 'postRequireHook', 'handleSigint');
/**
* represents the istanbul configuration and provides sub-objects that can
* return instrumentation, reporting and hook options respectively.
* Usage
* -----
*
* var configObj = require('istanbul').config.loadFile();
*
* console.log(configObj.reporting.reports());
*
* @class Configuration
* @module config
* @param {Object} obj the base object to use as the configuration
* @param {Object} overrides optional - override attributes that are merged into
* the base config
* @constructor
*/
function Configuration(obj, overrides) {
var config = mergeDefaults(obj, defaultConfig(true));
if (isObject(overrides)) {
config = mergeDefaults(overrides, config);
}
if (config.verbose) {
console.error('Using configuration');
console.error('-------------------');
console.error(yaml.safeDump(config, { indent: 4, flowLevel: 3 }));
console.error('-------------------\n');
}
this.verbose = config.verbose;
this.instrumentation = new InstrumentOptions(config.instrumentation);
this.reporting = new ReportingOptions(config.reporting);
this.hooks = new HookOptions(config.hooks);
this.check = config.check; // Pass raw config sub-object.
}
/**
* true if verbose logging is required
* @property verbose
* @type Boolean
*/
/**
* instrumentation options
* @property instrumentation
* @type InstrumentOptions
*/
/**
* reporting options
* @property reporting
* @type ReportingOptions
*/
/**
* hook options
* @property hooks
* @type HookOptions
*/
function readFile(file) {
return file.match(YML_PATTERN) ?
yaml.safeLoad(fs.readFileSync(file, 'utf8'), {filename: file}) :
require(path.resolve(file));
}
function loadFile(file, overrides) {
var defaultConfigFile = path.resolve('.istanbul.yml'),
configObject;
if (file) {
if (!existsSync(file)) {
throw new Error('Invalid configuration file specified:' + file);
}
} else {
if (existsSync(defaultConfigFile)) {
file = defaultConfigFile;
}
}
if (file) {
if (overrides && overrides.verbose === true) {
console.error('Loading config: ' + file);
}
configObject = readFile(file);
}
return new Configuration(configObject, overrides);
}
function loadObject(obj, overrides) {
return new Configuration(obj, overrides);
}
/**
* methods to load the configuration object.
* Usage
* -----
*
* var config = require('istanbul').config,
* configObj = config.loadFile();
*
* console.log(configObj.reporting.reports());
*
* @class Config
* @module main
* @static
*/
module.exports = {
/**
* loads the specified configuration file with optional overrides. Throws
* when a file is specified and it is not found.
* @method loadFile
* @static
* @param {String} file the file to load. If falsy, the default config file, if present, is loaded.
* If not a default config is used.
* @param {Object} overrides - an object with override keys that are merged into the
* config object loaded
* @return {Configuration} the config object with overrides applied
*/
loadFile: loadFile,
/**
* reads the contents of a configuration file
* @method readFile
* @static
* @param {String} file the file to read
* @return {Object} configuration object
*/
readFile: readFile,
/**
* loads the specified configuration object with optional overrides.
* @method loadObject
* @static
* @param {Object} obj the object to use as the base configuration.
* @param {Object} overrides - an object with override keys that are merged into the
* config object
* @return {Configuration} the config object with overrides applied
*/
loadObject: loadObject,
/**
* returns the default configuration object. Note that this is a plain object
* and not a `Configuration` instance.
* @method defaultConfig
* @static
* @return {Object} an object that represents the default config
*/
defaultConfig: defaultConfig
};

View File

@ -0,0 +1,198 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/**
* provides a mechanism to transform code in the scope of `require` or `vm.createScript`.
* This mechanism is general and relies on a user-supplied `matcher` function that determines when transformations should be
* performed and a user-supplied `transformer` function that performs the actual transform.
* Instrumenting code for coverage is one specific example of useful hooking.
*
* Note that both the `matcher` and `transformer` must execute synchronously.
*
* For the common case of matching filesystem paths based on inclusion/ exclusion patterns, use the `matcherFor`
* function in the istanbul API to get a matcher.
*
* It is up to the transformer to perform processing with side-effects, such as caching, storing the original
* source code to disk in case of dynamically generated scripts etc. The `Store` class can help you with this.
*
* Usage
* -----
*
* var hook = require('istanbul').hook,
* myMatcher = function (file) { return file.match(/foo/); },
* myTransformer = function (code, file) { return 'console.log("' + file + '");' + code; };
*
* hook.hookRequire(myMatcher, myTransformer);
*
* var foo = require('foo'); //will now print foo's module path to console
*
* @class Hook
* @module main
*/
var path = require('path'),
fs = require('fs'),
Module = require('module'),
vm = require('vm'),
originalLoaders = {},
originalCreateScript = vm.createScript,
originalRunInThisContext = vm.runInThisContext;
function transformFn(matcher, transformer, verbose) {
return function (code, filename) {
var shouldHook = typeof filename === 'string' && matcher(path.resolve(filename)),
transformed,
changed = false;
if (shouldHook) {
if (verbose) {
console.error('Module load hook: transform [' + filename + ']');
}
try {
transformed = transformer(code, filename);
changed = true;
} catch (ex) {
console.error('Transformation error; return original code');
console.error(ex);
transformed = code;
}
} else {
transformed = code;
}
return { code: transformed, changed: changed };
};
}
function unloadRequireCache(matcher) {
if (matcher && typeof require !== 'undefined' && require && require.cache) {
Object.keys(require.cache).forEach(function (filename) {
if (matcher(filename)) {
delete require.cache[filename];
}
});
}
}
/**
* hooks `require` to return transformed code to the node module loader.
* Exceptions in the transform result in the original code being used instead.
* @method hookRequire
* @static
* @param matcher {Function(filePath)} a function that is called with the absolute path to the file being
* `require`-d. Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
* @param transformer {Function(code, filePath)} a function called with the original code and the associated path of the file
* from where the code was loaded. Should return the transformed code.
* @param options {Object} options Optional.
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
* @param {Function} [options.postLoadHook] a function that is called with the name of the file being
* required. This is called after the require is processed irrespective of whether it was transformed.
*/
function hookRequire(matcher, transformer, options) {
options = options || {};
var extensions,
fn = transformFn(matcher, transformer, options.verbose),
postLoadHook = options.postLoadHook &&
typeof options.postLoadHook === 'function' ? options.postLoadHook : null;
extensions = options.extensions || ['.js'];
extensions.forEach(function(ext){
if (!(ext in originalLoaders)) {
originalLoaders[ext] = Module._extensions[ext] || Module._extensions['.js'];
}
Module._extensions[ext] = function (module, filename) {
var ret = fn(fs.readFileSync(filename, 'utf8'), filename);
if (ret.changed) {
module._compile(ret.code, filename);
} else {
originalLoaders[ext](module, filename);
}
if (postLoadHook) {
postLoadHook(filename);
}
};
});
}
/**
* unhook `require` to restore it to its original state.
* @method unhookRequire
* @static
*/
function unhookRequire() {
Object.keys(originalLoaders).forEach(function(ext) {
Module._extensions[ext] = originalLoaders[ext];
});
}
/**
* hooks `vm.createScript` to return transformed code out of which a `Script` object will be created.
* Exceptions in the transform result in the original code being used instead.
* @method hookCreateScript
* @static
* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
* `vm.createScript`. Should return the transformed code.
* @param options {Object} options Optional.
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
*/
function hookCreateScript(matcher, transformer, opts) {
opts = opts || {};
var fn = transformFn(matcher, transformer, opts.verbose);
vm.createScript = function (code, file) {
var ret = fn(code, file);
return originalCreateScript(ret.code, file);
};
}
/**
* unhooks vm.createScript, restoring it to its original state.
* @method unhookCreateScript
* @static
*/
function unhookCreateScript() {
vm.createScript = originalCreateScript;
}
/**
* hooks `vm.runInThisContext` to return transformed code.
* @method hookRunInThisContext
* @static
* @param matcher {Function(filePath)} a function that is called with the filename passed to `vm.createScript`
* Should return a truthy value when transformations need to be applied to the code, a falsy value otherwise
* @param transformer {Function(code, filePath)} a function called with the original code and the filename passed to
* `vm.createScript`. Should return the transformed code.
* @param options {Object} options Optional.
* @param {Boolean} [options.verbose] write a line to standard error every time the transformer is called
*/
function hookRunInThisContext(matcher, transformer, opts) {
opts = opts || {};
var fn = transformFn(matcher, transformer, opts.verbose);
vm.runInThisContext = function (code, file) {
var ret = fn(code, file);
return originalRunInThisContext(ret.code, file);
};
}
/**
* unhooks vm.runInThisContext, restoring it to its original state.
* @method unhookRunInThisContext
* @static
*/
function unhookRunInThisContext() {
vm.runInThisContext = originalRunInThisContext;
}
module.exports = {
hookRequire: hookRequire,
unhookRequire: unhookRequire,
hookCreateScript: hookCreateScript,
unhookCreateScript: unhookCreateScript,
hookRunInThisContext : hookRunInThisContext,
unhookRunInThisContext : unhookRunInThisContext,
unloadRequireCache: unloadRequireCache
};

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,425 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/**
* utility methods to process coverage objects. A coverage object has the following
* format.
*
* {
* "/path/to/file1.js": { file1 coverage },
* "/path/to/file2.js": { file2 coverage }
* }
*
* The internals of the file coverage object are intentionally not documented since
* it is not a public interface.
*
* *Note:* When a method of this module has the word `File` in it, it will accept
* one of the sub-objects of the main coverage object as an argument. Other
* methods accept the higher level coverage object with multiple keys.
*
* Works on `node` as well as the browser.
*
* Usage on nodejs
* ---------------
*
* var objectUtils = require('istanbul').utils;
*
* Usage in a browser
* ------------------
*
* Load this file using a `script` tag or other means. This will set `window.coverageUtils`
* to this module's exports.
*
* @class ObjectUtils
* @module main
* @static
*/
(function (isNode) {
/**
* adds line coverage information to a file coverage object, reverse-engineering
* it from statement coverage. The object passed in is updated in place.
*
* Note that if line coverage information is already present in the object,
* it is not recomputed.
*
* @method addDerivedInfoForFile
* @static
* @param {Object} fileCoverage the coverage object for a single file
*/
function addDerivedInfoForFile(fileCoverage) {
var statementMap = fileCoverage.statementMap,
statements = fileCoverage.s,
lineMap;
if (!fileCoverage.l) {
fileCoverage.l = lineMap = {};
Object.keys(statements).forEach(function (st) {
var line = statementMap[st].start.line,
count = statements[st],
prevVal = lineMap[line];
if (count === 0 && statementMap[st].skip) { count = 1; }
if (typeof prevVal === 'undefined' || prevVal < count) {
lineMap[line] = count;
}
});
}
}
/**
* adds line coverage information to all file coverage objects.
*
* @method addDerivedInfo
* @static
* @param {Object} coverage the coverage object
*/
function addDerivedInfo(coverage) {
Object.keys(coverage).forEach(function (k) {
addDerivedInfoForFile(coverage[k]);
});
}
/**
* removes line coverage information from all file coverage objects
* @method removeDerivedInfo
* @static
* @param {Object} coverage the coverage object
*/
function removeDerivedInfo(coverage) {
Object.keys(coverage).forEach(function (k) {
delete coverage[k].l;
});
}
function percent(covered, total) {
var tmp;
if (total > 0) {
tmp = 1000 * 100 * covered / total + 5;
return Math.floor(tmp / 10) / 100;
} else {
return 100.00;
}
}
function computeSimpleTotals(fileCoverage, property, mapProperty) {
var stats = fileCoverage[property],
map = mapProperty ? fileCoverage[mapProperty] : null,
ret = { total: 0, covered: 0, skipped: 0 };
Object.keys(stats).forEach(function (key) {
var covered = !!stats[key],
skipped = map && map[key].skip;
ret.total += 1;
if (covered || skipped) {
ret.covered += 1;
}
if (!covered && skipped) {
ret.skipped += 1;
}
});
ret.pct = percent(ret.covered, ret.total);
return ret;
}
function computeBranchTotals(fileCoverage) {
var stats = fileCoverage.b,
branchMap = fileCoverage.branchMap,
ret = { total: 0, covered: 0, skipped: 0 };
Object.keys(stats).forEach(function (key) {
var branches = stats[key],
map = branchMap[key],
covered,
skipped,
i;
for (i = 0; i < branches.length; i += 1) {
covered = branches[i] > 0;
skipped = map.locations && map.locations[i] && map.locations[i].skip;
if (covered || skipped) {
ret.covered += 1;
}
if (!covered && skipped) {
ret.skipped += 1;
}
}
ret.total += branches.length;
});
ret.pct = percent(ret.covered, ret.total);
return ret;
}
/**
* returns a blank summary metrics object. A metrics object has the following
* format.
*
* {
* lines: lineMetrics,
* statements: statementMetrics,
* functions: functionMetrics,
* branches: branchMetrics
* linesCovered: lineCoveredCount
* }
*
* Each individual metric object looks as follows:
*
* {
* total: n,
* covered: m,
* pct: percent
* }
*
* @method blankSummary
* @static
* @return {Object} a blank metrics object
*/
function blankSummary() {
return {
lines: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
statements: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
functions: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
branches: {
total: 0,
covered: 0,
skipped: 0,
pct: 'Unknown'
},
linesCovered: {}
};
}
/**
* returns the summary metrics given the coverage object for a single file. See `blankSummary()`
* to understand the format of the returned object.
*
* @method summarizeFileCoverage
* @static
* @param {Object} fileCoverage the coverage object for a single file.
* @return {Object} the summary metrics for the file
*/
function summarizeFileCoverage(fileCoverage) {
var ret = blankSummary();
addDerivedInfoForFile(fileCoverage);
ret.lines = computeSimpleTotals(fileCoverage, 'l');
ret.functions = computeSimpleTotals(fileCoverage, 'f', 'fnMap');
ret.statements = computeSimpleTotals(fileCoverage, 's', 'statementMap');
ret.branches = computeBranchTotals(fileCoverage);
ret.linesCovered = fileCoverage.l;
return ret;
}
/**
* merges two instances of file coverage objects *for the same file*
* such that the execution counts are correct.
*
* @method mergeFileCoverage
* @static
* @param {Object} first the first file coverage object for a given file
* @param {Object} second the second file coverage object for the same file
* @return {Object} an object that is a result of merging the two. Note that
* the input objects are not changed in any way.
*/
function mergeFileCoverage(first, second) {
var ret = JSON.parse(JSON.stringify(first)),
i;
delete ret.l; //remove derived info
Object.keys(second.s).forEach(function (k) {
ret.s[k] += second.s[k];
});
Object.keys(second.f).forEach(function (k) {
ret.f[k] += second.f[k];
});
Object.keys(second.b).forEach(function (k) {
var retArray = ret.b[k],
secondArray = second.b[k];
for (i = 0; i < retArray.length; i += 1) {
retArray[i] += secondArray[i];
}
});
return ret;
}
/**
* merges multiple summary metrics objects by summing up the `totals` and
* `covered` fields and recomputing the percentages. This function is generic
* and can accept any number of arguments.
*
* @method mergeSummaryObjects
* @static
* @param {Object} summary... multiple summary metrics objects
* @return {Object} the merged summary metrics
*/
function mergeSummaryObjects() {
var ret = blankSummary(),
args = Array.prototype.slice.call(arguments),
keys = ['lines', 'statements', 'branches', 'functions'],
increment = function (obj) {
if (obj) {
keys.forEach(function (key) {
ret[key].total += obj[key].total;
ret[key].covered += obj[key].covered;
ret[key].skipped += obj[key].skipped;
});
// keep track of all lines we have coverage for.
Object.keys(obj.linesCovered).forEach(function (key) {
if (!ret.linesCovered[key]) {
ret.linesCovered[key] = obj.linesCovered[key];
} else {
ret.linesCovered[key] += obj.linesCovered[key];
}
});
}
};
args.forEach(function (arg) {
increment(arg);
});
keys.forEach(function (key) {
ret[key].pct = percent(ret[key].covered, ret[key].total);
});
return ret;
}
/**
* returns the coverage summary for a single coverage object. This is
* wrapper over `summarizeFileCoverage` and `mergeSummaryObjects` for
* the common case of a single coverage object
* @method summarizeCoverage
* @static
* @param {Object} coverage the coverage object
* @return {Object} summary coverage metrics across all files in the coverage object
*/
function summarizeCoverage(coverage) {
var fileSummary = [];
Object.keys(coverage).forEach(function (key) {
fileSummary.push(summarizeFileCoverage(coverage[key]));
});
return mergeSummaryObjects.apply(null, fileSummary);
}
/**
* makes the coverage object generated by this library yuitest_coverage compatible.
* Note that this transformation is lossy since the returned object will not have
* statement and branch coverage.
*
* @method toYUICoverage
* @static
* @param {Object} coverage The `istanbul` coverage object
* @return {Object} a coverage object in `yuitest_coverage` format.
*/
function toYUICoverage(coverage) {
var ret = {};
addDerivedInfo(coverage);
Object.keys(coverage).forEach(function (k) {
var fileCoverage = coverage[k],
lines = fileCoverage.l,
functions = fileCoverage.f,
fnMap = fileCoverage.fnMap,
o;
o = ret[k] = {
lines: {},
calledLines: 0,
coveredLines: 0,
functions: {},
calledFunctions: 0,
coveredFunctions: 0
};
Object.keys(lines).forEach(function (k) {
o.lines[k] = lines[k];
o.coveredLines += 1;
if (lines[k] > 0) {
o.calledLines += 1;
}
});
Object.keys(functions).forEach(function (k) {
var name = fnMap[k].name + ':' + fnMap[k].line;
o.functions[name] = functions[k];
o.coveredFunctions += 1;
if (functions[k] > 0) {
o.calledFunctions += 1;
}
});
});
return ret;
}
/**
* Creates new file coverage object with incremented hits count
* on skipped statements, branches and functions
*
* @method incrementIgnoredTotals
* @static
* @param {Object} cov File coverage object
* @return {Object} New file coverage object
*/
function incrementIgnoredTotals(cov) {
//TODO: This may be slow in the browser and may break in older browsers
// Look into using a library that works in Node and the browser
var fileCoverage = JSON.parse(JSON.stringify(cov));
[
{mapKey: 'statementMap', hitsKey: 's'},
{mapKey: 'branchMap', hitsKey: 'b'},
{mapKey: 'fnMap', hitsKey: 'f'}
].forEach(function (keys) {
Object.keys(fileCoverage[keys.mapKey])
.forEach(function (key) {
var map = fileCoverage[keys.mapKey][key];
var hits = fileCoverage[keys.hitsKey];
if (keys.mapKey === 'branchMap') {
var locations = map.locations;
locations.forEach(function (location, index) {
if (hits[key][index] === 0 && location.skip) {
hits[key][index] = 1;
}
});
return;
}
if (hits[key] === 0 && map.skip) {
hits[key] = 1;
}
});
});
return fileCoverage;
}
var exportables = {
addDerivedInfo: addDerivedInfo,
addDerivedInfoForFile: addDerivedInfoForFile,
removeDerivedInfo: removeDerivedInfo,
blankSummary: blankSummary,
summarizeFileCoverage: summarizeFileCoverage,
summarizeCoverage: summarizeCoverage,
mergeFileCoverage: mergeFileCoverage,
mergeSummaryObjects: mergeSummaryObjects,
toYUICoverage: toYUICoverage,
incrementIgnoredTotals: incrementIgnoredTotals
};
/* istanbul ignore else: windows */
if (isNode) {
module.exports = exportables;
} else {
window.coverageUtils = exportables;
}
}(typeof module !== 'undefined' && typeof module.exports !== 'undefined' && typeof exports !== 'undefined'));

View File

@ -0,0 +1,15 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Store = require('./store'),
Report = require('./report'),
Command = require('./command');
Store.loadAll();
Report.loadAll();
Command.loadAll();

View File

@ -0,0 +1,227 @@
var path = require('path'),
util = require('util'),
Report = require('./index'),
FileWriter = require('../util/file-writer'),
TreeSummarizer = require('../util/tree-summarizer'),
utils = require('../object-utils');
/**
* a `Report` implementation that produces a clover-style XML file.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('clover');
*
* @class CloverReport
* @module report
* @extends Report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the clover.xml will be written
* @param {String} [opts.file] the file name, defaulted to config attribute or 'clover.xml'
*/
function CloverReport(opts) {
Report.call(this);
opts = opts || {};
this.projectRoot = process.cwd();
this.dir = opts.dir || this.projectRoot;
this.file = opts.file || this.getDefaultConfig().file;
this.opts = opts;
}
CloverReport.TYPE = 'clover';
util.inherits(CloverReport, Report);
function asJavaPackage(node) {
return node.displayShortName().
replace(/\//g, '.').
replace(/\\/g, '.').
replace(/\.$/, '');
}
function asClassName(node) {
/*jslint regexp: true */
return node.fullPath().replace(/.*[\\\/]/, '');
}
function quote(thing) {
return '"' + thing + '"';
}
function attr(n, v) {
return ' ' + n + '=' + quote(v) + ' ';
}
function branchCoverageByLine(fileCoverage) {
var branchMap = fileCoverage.branchMap,
branches = fileCoverage.b,
ret = {};
Object.keys(branchMap).forEach(function (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 (k) {
var dataArray = ret[k],
covered = dataArray.filter(function (item) { return item > 0; }),
coverage = covered.length / dataArray.length * 100;
ret[k] = { covered: covered.length, total: dataArray.length, coverage: coverage };
});
return ret;
}
function addClassStats(node, fileCoverage, writer) {
fileCoverage = utils.incrementIgnoredTotals(fileCoverage);
var metrics = node.metrics,
branchByLine = branchCoverageByLine(fileCoverage),
fnMap,
lines;
writer.println('\t\t\t<file' +
attr('name', asClassName(node)) +
attr('path', node.fullPath()) +
'>');
writer.println('\t\t\t\t<metrics' +
attr('statements', metrics.lines.total) +
attr('coveredstatements', metrics.lines.covered) +
attr('conditionals', metrics.branches.total) +
attr('coveredconditionals', metrics.branches.covered) +
attr('methods', metrics.functions.total) +
attr('coveredmethods', metrics.functions.covered) +
'/>');
fnMap = fileCoverage.fnMap;
lines = fileCoverage.l;
Object.keys(lines).forEach(function (k) {
var str = '\t\t\t\t<line' +
attr('num', k) +
attr('count', lines[k]),
branchDetail = branchByLine[k];
if (!branchDetail) {
str += ' type="stmt" ';
} else {
str += ' type="cond" ' +
attr('truecount', branchDetail.covered) +
attr('falsecount', (branchDetail.total - branchDetail.covered));
}
writer.println(str + '/>');
});
writer.println('\t\t\t</file>');
}
function walk(node, collector, writer, level, projectRoot) {
var metrics,
totalFiles = 0,
totalPackages = 0,
totalLines = 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 (child) { return child.kind === 'dir'; }).
forEach(function (child) {
totalPackages += 1;
child.children.filter(function (child) { return child.kind !== 'dir'; }).
forEach(function (child) {
Object.keys(collector.fileCoverageFor(child.fullPath()).l).forEach(function (k){
tempLines = k;
});
totalLines += Number(tempLines);
totalFiles += 1;
});
});
writer.println('\t\t<metrics' +
attr('statements', metrics.lines.total) +
attr('coveredstatements', metrics.lines.covered) +
attr('conditionals', metrics.branches.total) +
attr('coveredconditionals', metrics.branches.covered) +
attr('methods', metrics.functions.total) +
attr('coveredmethods', metrics.functions.covered) +
attr('elements', metrics.lines.total + metrics.branches.total + metrics.functions.total) +
attr('coveredelements', metrics.lines.covered + metrics.branches.covered + metrics.functions.covered) +
attr('complexity', 0) +
attr('packages', totalPackages) +
attr('files', totalFiles) +
attr('classes', totalFiles) +
attr('loc', totalLines) +
attr('ncloc', totalLines) +
'/>');
}
if (node.packageMetrics) {
metrics = node.packageMetrics;
writer.println('\t\t<package' +
attr('name', asJavaPackage(node)) +
'>');
writer.println('\t\t\t<metrics' +
attr('statements', metrics.lines.total) +
attr('coveredstatements', metrics.lines.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 (child) { return child.kind !== 'dir'; }).
forEach(function (child) {
addClassStats(child, collector.fileCoverageFor(child.fullPath()), writer);
});
writer.println('\t\t</package>');
}
node.children.filter(function (child) { return child.kind === 'dir'; }).
forEach(function (child) {
walk(child, collector, writer, level + 1, projectRoot);
});
if (level === 0) {
writer.println('\t</project>');
writer.println('</coverage>');
}
}
Report.mix(CloverReport, {
synopsis: function () {
return 'XML coverage report that can be consumed by the clover tool';
},
getDefaultConfig: function () {
return { file: 'clover.xml' };
},
writeReport: function (collector, sync) {
var summarizer = new TreeSummarizer(),
outputFile = path.join(this.dir, this.file),
writer = this.opts.writer || new FileWriter(sync),
projectRoot = this.projectRoot,
that = this,
tree,
root;
collector.files().forEach(function (key) {
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key)));
});
tree = summarizer.getTreeSummary();
root = tree.root;
writer.on('done', function () { that.emit('done'); });
writer.writeFile(outputFile, function (contentWriter) {
walk(root, collector, contentWriter, 0, projectRoot);
writer.done();
});
}
});
module.exports = CloverReport;

View File

@ -0,0 +1,221 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
util = require('util'),
Report = require('./index'),
FileWriter = require('../util/file-writer'),
TreeSummarizer = require('../util/tree-summarizer'),
utils = require('../object-utils');
/**
* a `Report` implementation that produces a cobertura-style XML file that conforms to the
* http://cobertura.sourceforge.net/xml/coverage-04.dtd DTD.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('cobertura');
*
* @class CoberturaReport
* @module report
* @extends Report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the cobertura-coverage.xml will be written
*/
function CoberturaReport(opts) {
Report.call(this);
opts = opts || {};
this.projectRoot = process.cwd();
this.dir = opts.dir || this.projectRoot;
this.file = opts.file || this.getDefaultConfig().file;
this.opts = opts;
}
CoberturaReport.TYPE = 'cobertura';
util.inherits(CoberturaReport, Report);
function asJavaPackage(node) {
return node.displayShortName().
replace(/\//g, '.').
replace(/\\/g, '.').
replace(/\.$/, '');
}
function asClassName(node) {
/*jslint regexp: true */
return node.fullPath().replace(/.*[\\\/]/, '');
}
function quote(thing) {
return '"' + thing + '"';
}
function attr(n, v) {
return ' ' + n + '=' + quote(v) + ' ';
}
function branchCoverageByLine(fileCoverage) {
var branchMap = fileCoverage.branchMap,
branches = fileCoverage.b,
ret = {};
Object.keys(branchMap).forEach(function (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 (k) {
var dataArray = ret[k],
covered = dataArray.filter(function (item) { return item > 0; }),
coverage = covered.length / dataArray.length * 100;
ret[k] = { covered: covered.length, total: dataArray.length, coverage: coverage };
});
return ret;
}
function addClassStats(node, fileCoverage, writer, projectRoot) {
fileCoverage = utils.incrementIgnoredTotals(fileCoverage);
var metrics = node.metrics,
branchByLine = branchCoverageByLine(fileCoverage),
fnMap,
lines;
writer.println('\t\t<class' +
attr('name', asClassName(node)) +
attr('filename', path.relative(projectRoot, node.fullPath())) +
attr('line-rate', metrics.lines.pct / 100.0) +
attr('branch-rate', metrics.branches.pct / 100.0) +
'>');
writer.println('\t\t<methods>');
fnMap = fileCoverage.fnMap;
Object.keys(fnMap).forEach(function (k) {
var name = fnMap[k].name,
hits = fileCoverage.f[k];
writer.println(
'\t\t\t<method' +
attr('name', name) +
attr('hits', hits) +
attr('signature', '()V') + //fake out a no-args void return
'>'
);
//Add the function definition line and hits so that jenkins cobertura plugin records method hits
writer.println(
'\t\t\t\t<lines>' +
'<line' +
attr('number', fnMap[k].line) +
attr('hits', fileCoverage.f[k]) +
'/>' +
'</lines>'
);
writer.println('\t\t\t</method>');
});
writer.println('\t\t</methods>');
writer.println('\t\t<lines>');
lines = fileCoverage.l;
Object.keys(lines).forEach(function (k) {
var str = '\t\t\t<line' +
attr('number', k) +
attr('hits', lines[k]),
branchDetail = branchByLine[k];
if (!branchDetail) {
str += attr('branch', false);
} else {
str += attr('branch', true) +
attr('condition-coverage', branchDetail.coverage +
'% (' + branchDetail.covered + '/' + branchDetail.total + ')');
}
writer.println(str + '/>');
});
writer.println('\t\t</lines>');
writer.println('\t\t</class>');
}
function walk(node, collector, writer, level, projectRoot) {
var metrics;
if (level === 0) {
metrics = node.metrics;
writer.println('<?xml version="1.0" ?>');
writer.println('<!DOCTYPE coverage SYSTEM "http://cobertura.sourceforge.net/xml/coverage-04.dtd">');
writer.println('<coverage' +
attr('lines-valid', metrics.lines.total) +
attr('lines-covered', metrics.lines.covered) +
attr('line-rate', metrics.lines.pct / 100.0) +
attr('branches-valid', metrics.branches.total) +
attr('branches-covered', metrics.branches.covered) +
attr('branch-rate', metrics.branches.pct / 100.0) +
attr('timestamp', Date.now()) +
'complexity="0" version="0.1">');
writer.println('<sources>');
writer.println('\t<source>' + projectRoot + '</source>');
writer.println('</sources>');
writer.println('<packages>');
}
if (node.packageMetrics) {
metrics = node.packageMetrics;
writer.println('\t<package' +
attr('name', asJavaPackage(node)) +
attr('line-rate', metrics.lines.pct / 100.0) +
attr('branch-rate', metrics.branches.pct / 100.0) +
'>');
writer.println('\t<classes>');
node.children.filter(function (child) { return child.kind !== 'dir'; }).
forEach(function (child) {
addClassStats(child, collector.fileCoverageFor(child.fullPath()), writer, projectRoot);
});
writer.println('\t</classes>');
writer.println('\t</package>');
}
node.children.filter(function (child) { return child.kind === 'dir'; }).
forEach(function (child) {
walk(child, collector, writer, level + 1, projectRoot);
});
if (level === 0) {
writer.println('</packages>');
writer.println('</coverage>');
}
}
Report.mix(CoberturaReport, {
synopsis: function () {
return 'XML coverage report that can be consumed by the cobertura tool';
},
getDefaultConfig: function () {
return { file: 'cobertura-coverage.xml' };
},
writeReport: function (collector, sync) {
var summarizer = new TreeSummarizer(),
outputFile = path.join(this.dir, this.file),
writer = this.opts.writer || new FileWriter(sync),
projectRoot = this.projectRoot,
that = this,
tree,
root;
collector.files().forEach(function (key) {
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key)));
});
tree = summarizer.getTreeSummary();
root = tree.root;
writer.on('done', function () { that.emit('done'); });
writer.writeFile(outputFile, function (contentWriter) {
walk(root, collector, contentWriter, 0, projectRoot);
writer.done();
});
}
});
module.exports = CoberturaReport;

View File

@ -0,0 +1,49 @@
/*
Copyright (c) 2013, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Report = require('../index');
var supportsColor = require('supports-color');
module.exports = {
watermarks: function () {
return {
statements: [ 50, 80 ],
lines: [ 50, 80 ],
functions: [ 50, 80],
branches: [ 50, 80 ]
};
},
classFor: function (type, metrics, watermarks) {
var mark = watermarks[type],
value = metrics[type].pct;
return value >= mark[1] ? 'high' : value >= mark[0] ? 'medium' : 'low';
},
colorize: function (str, clazz) {
/* istanbul ignore if: untestable in batch mode */
if (supportsColor) {
switch (clazz) {
case 'low' : str = '\033[91m' + str + '\033[0m'; break;
case 'medium': str = '\033[93m' + str + '\033[0m'; break;
case 'high': str = '\033[92m' + str + '\033[0m'; break;
}
}
return str;
},
defaultReportConfig: function () {
var cfg = {};
Report.getReportList().forEach(function (type) {
var rpt = Report.create(type),
c = rpt.getDefaultConfig();
if (c) {
cfg[type] = c;
}
});
return cfg;
}
};

View File

@ -0,0 +1,572 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
/*jshint maxlen: 300 */
var handlebars = require('handlebars'),
defaults = require('./common/defaults'),
path = require('path'),
fs = require('fs'),
util = require('util'),
FileWriter = require('../util/file-writer'),
Report = require('./index'),
Store = require('../store'),
InsertionText = require('../util/insertion-text'),
TreeSummarizer = require('../util/tree-summarizer'),
utils = require('../object-utils'),
templateFor = function (name) { return handlebars.compile(fs.readFileSync(path.resolve(__dirname, 'templates', name + '.txt'), 'utf8')); },
headerTemplate = templateFor('head'),
footerTemplate = templateFor('foot'),
detailTemplate = handlebars.compile([
'<tr>',
'<td class="line-count quiet">{{#show_lines}}{{maxLines}}{{/show_lines}}</td>',
'<td class="line-coverage quiet">{{#show_line_execution_counts fileCoverage}}{{maxLines}}{{/show_line_execution_counts}}</td>',
'<td class="text"><pre class="prettyprint lang-js">{{#show_code structured}}{{/show_code}}</pre></td>',
'</tr>\n'
].join('')),
summaryTableHeader = [
'<div class="pad1">',
'<table class="coverage-summary">',
'<thead>',
'<tr>',
' <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>',
' <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>',
' <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>',
' <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>',
' <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>',
' <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>',
' <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>',
' <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>',
' <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>',
' <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>',
'</tr>',
'</thead>',
'<tbody>'
].join('\n'),
summaryLineTemplate = handlebars.compile([
'<tr>',
'<td class="file {{reportClasses.statements}}" data-value="{{file}}"><a href="{{output}}">{{file}}</a></td>',
'<td data-value="{{metrics.statements.pct}}" class="pic {{reportClasses.statements}}"><div class="chart">{{#show_picture}}{{metrics.statements.pct}}{{/show_picture}}</div></td>',
'<td data-value="{{metrics.statements.pct}}" class="pct {{reportClasses.statements}}">{{metrics.statements.pct}}%</td>',
'<td data-value="{{metrics.statements.total}}" class="abs {{reportClasses.statements}}">{{metrics.statements.covered}}/{{metrics.statements.total}}</td>',
'<td data-value="{{metrics.branches.pct}}" class="pct {{reportClasses.branches}}">{{metrics.branches.pct}}%</td>',
'<td data-value="{{metrics.branches.total}}" class="abs {{reportClasses.branches}}">{{metrics.branches.covered}}/{{metrics.branches.total}}</td>',
'<td data-value="{{metrics.functions.pct}}" class="pct {{reportClasses.functions}}">{{metrics.functions.pct}}%</td>',
'<td data-value="{{metrics.functions.total}}" class="abs {{reportClasses.functions}}">{{metrics.functions.covered}}/{{metrics.functions.total}}</td>',
'<td data-value="{{metrics.lines.pct}}" class="pct {{reportClasses.lines}}">{{metrics.lines.pct}}%</td>',
'<td data-value="{{metrics.lines.total}}" class="abs {{reportClasses.lines}}">{{metrics.lines.covered}}/{{metrics.lines.total}}</td>',
'</tr>\n'
].join('\n\t')),
summaryTableFooter = [
'</tbody>',
'</table>',
'</div>'
].join('\n'),
lt = '\u0001',
gt = '\u0002',
RE_LT = /</g,
RE_GT = />/g,
RE_AMP = /&/g,
RE_lt = /\u0001/g,
RE_gt = /\u0002/g;
handlebars.registerHelper('show_picture', function (opts) {
var num = Number(opts.fn(this)),
rest,
cls = '';
if (isFinite(num)) {
if (num === 100) {
cls = ' cover-full';
}
num = Math.floor(num);
rest = 100 - num;
return '<div class="cover-fill' + cls + '" style="width: ' + num + '%;"></div>' +
'<div class="cover-empty" style="width:' + rest + '%;"></div>';
} else {
return '';
}
});
handlebars.registerHelper('if_has_ignores', function (metrics, opts) {
return (metrics.statements.skipped +
metrics.functions.skipped +
metrics.branches.skipped) === 0 ? '' : opts.fn(this);
});
handlebars.registerHelper('show_ignores', function (metrics) {
var statements = metrics.statements.skipped,
functions = metrics.functions.skipped,
branches = metrics.branches.skipped,
result;
if (statements === 0 && functions === 0 && branches === 0) {
return '<span class="ignore-none">none</span>';
}
result = [];
if (statements >0) { result.push(statements === 1 ? '1 statement': statements + ' statements'); }
if (functions >0) { result.push(functions === 1 ? '1 function' : functions + ' functions'); }
if (branches >0) { result.push(branches === 1 ? '1 branch' : branches + ' branches'); }
return result.join(', ');
});
handlebars.registerHelper('show_lines', function (opts) {
var maxLines = Number(opts.fn(this)),
i,
array = [];
for (i = 0; i < maxLines; i += 1) {
array[i] = i + 1;
}
return array.join('\n');
});
handlebars.registerHelper('show_line_execution_counts', function (context, opts) {
var lines = context.l,
maxLines = Number(opts.fn(this)),
i,
lineNumber,
array = [],
covered,
value = '';
for (i = 0; i < maxLines; i += 1) {
lineNumber = i + 1;
value = '&nbsp;';
covered = 'neutral';
if (lines.hasOwnProperty(lineNumber)) {
if (lines[lineNumber] > 0) {
covered = 'yes';
value = lines[lineNumber] + '×';
} else {
covered = 'no';
}
}
array.push('<span class="cline-any cline-' + covered + '">' + value + '</span>');
}
return array.join('\n');
});
function customEscape(text) {
text = text.toString();
return text.replace(RE_AMP, '&amp;')
.replace(RE_LT, '&lt;')
.replace(RE_GT, '&gt;')
.replace(RE_lt, '<')
.replace(RE_gt, '>');
}
handlebars.registerHelper('show_code', function (context /*, opts */) {
var array = [];
context.forEach(function (item) {
array.push(customEscape(item.text) || '&nbsp;');
});
return array.join('\n');
});
function title(str) {
return ' title="' + str + '" ';
}
function annotateLines(fileCoverage, structuredText) {
var lineStats = fileCoverage.l;
if (!lineStats) { return; }
Object.keys(lineStats).forEach(function (lineNumber) {
var count = lineStats[lineNumber];
if (structuredText[lineNumber]) {
structuredText[lineNumber].covered = count > 0 ? 'yes' : 'no';
}
});
structuredText.forEach(function (item) {
if (item.covered === null) {
item.covered = 'neutral';
}
});
}
function annotateStatements(fileCoverage, structuredText) {
var statementStats = fileCoverage.s,
statementMeta = fileCoverage.statementMap;
Object.keys(statementStats).forEach(function (stName) {
var count = statementStats[stName],
meta = statementMeta[stName],
type = count > 0 ? 'yes' : 'no',
startCol = meta.start.column,
endCol = meta.end.column + 1,
startLine = meta.start.line,
endLine = meta.end.line,
openSpan = lt + 'span class="' + (meta.skip ? 'cstat-skip' : 'cstat-no') + '"' + title('statement not covered') + gt,
closeSpan = lt + '/span' + gt,
text;
if (type === 'no') {
if (endLine !== startLine) {
endLine = startLine;
endCol = structuredText[startLine].text.originalLength();
}
text = structuredText[startLine].text;
text.wrap(startCol,
openSpan,
startLine === endLine ? endCol : text.originalLength(),
closeSpan);
}
});
}
function annotateFunctions(fileCoverage, structuredText) {
var fnStats = fileCoverage.f,
fnMeta = fileCoverage.fnMap;
if (!fnStats) { return; }
Object.keys(fnStats).forEach(function (fName) {
var count = fnStats[fName],
meta = fnMeta[fName],
type = count > 0 ? 'yes' : 'no',
startCol = meta.loc.start.column,
endCol = meta.loc.end.column + 1,
startLine = meta.loc.start.line,
endLine = meta.loc.end.line,
openSpan = lt + 'span class="' + (meta.skip ? 'fstat-skip' : 'fstat-no') + '"' + title('function not covered') + gt,
closeSpan = lt + '/span' + gt,
text;
if (type === 'no') {
if (endLine !== startLine) {
endLine = startLine;
endCol = structuredText[startLine].text.originalLength();
}
text = structuredText[startLine].text;
text.wrap(startCol,
openSpan,
startLine === endLine ? endCol : text.originalLength(),
closeSpan);
}
});
}
function annotateBranches(fileCoverage, structuredText) {
var branchStats = fileCoverage.b,
branchMeta = fileCoverage.branchMap;
if (!branchStats) { return; }
Object.keys(branchStats).forEach(function (branchName) {
var branchArray = branchStats[branchName],
sumCount = branchArray.reduce(function (p, n) { return p + n; }, 0),
metaArray = branchMeta[branchName].locations,
i,
count,
meta,
type,
startCol,
endCol,
startLine,
endLine,
openSpan,
closeSpan,
text;
if (sumCount > 0) { //only highlight if partial branches are missing
for (i = 0; i < branchArray.length; i += 1) {
count = branchArray[i];
meta = metaArray[i];
type = count > 0 ? 'yes' : 'no';
startCol = meta.start.column;
endCol = meta.end.column + 1;
startLine = meta.start.line;
endLine = meta.end.line;
openSpan = lt + 'span class="branch-' + i + ' ' + (meta.skip ? 'cbranch-skip' : 'cbranch-no') + '"' + title('branch not covered') + gt;
closeSpan = lt + '/span' + gt;
if (count === 0) { //skip branches taken
if (endLine !== startLine) {
endLine = startLine;
endCol = structuredText[startLine].text.originalLength();
}
text = structuredText[startLine].text;
if (branchMeta[branchName].type === 'if') { // and 'if' is a special case since the else branch might not be visible, being non-existent
text.insertAt(startCol, lt + 'span class="' + (meta.skip ? 'skip-if-branch' : 'missing-if-branch') + '"' +
title((i === 0 ? 'if' : 'else') + ' path not taken') + gt +
(i === 0 ? 'I' : 'E') + lt + '/span' + gt, true, false);
} else {
text.wrap(startCol,
openSpan,
startLine === endLine ? endCol : text.originalLength(),
closeSpan);
}
}
}
}
});
}
function getReportClass(stats, watermark) {
var coveragePct = stats.pct,
identity = 1;
if (coveragePct * identity === coveragePct) {
return coveragePct >= watermark[1] ? 'high' : coveragePct >= watermark[0] ? 'medium' : 'low';
} else {
return '';
}
}
function cleanPath(name) {
var SEP = path.sep || '/';
return (SEP !== '/') ? name.split(SEP).join('/') : name;
}
function isEmptySourceStore(sourceStore) {
if (!sourceStore) {
return true;
}
var cache = sourceStore.sourceCache;
return cache && !Object.keys(cache).length;
}
/**
* a `Report` implementation that produces HTML coverage reports.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('html');
*
*
* @class HtmlReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to generate reports. Defaults to `./html-report`
*/
function HtmlReport(opts) {
Report.call(this);
this.opts = opts || {};
this.opts.dir = this.opts.dir || path.resolve(process.cwd(), 'html-report');
this.opts.sourceStore = isEmptySourceStore(this.opts.sourceStore) ?
Store.create('fslookup') : this.opts.sourceStore;
this.opts.linkMapper = this.opts.linkMapper || this.standardLinkMapper();
this.opts.writer = this.opts.writer || null;
this.opts.templateData = { datetime: Date() };
this.opts.watermarks = this.opts.watermarks || defaults.watermarks();
}
HtmlReport.TYPE = 'html';
util.inherits(HtmlReport, Report);
Report.mix(HtmlReport, {
synopsis: function () {
return 'Navigable HTML coverage report for every file and directory';
},
getPathHtml: function (node, linkMapper) {
var parent = node.parent,
nodePath = [],
linkPath = [],
i;
while (parent) {
nodePath.push(parent);
parent = parent.parent;
}
for (i = 0; i < nodePath.length; i += 1) {
linkPath.push('<a href="' + linkMapper.ancestor(node, i + 1) + '">' +
(cleanPath(nodePath[i].relativeName) || 'all files') + '</a>');
}
linkPath.reverse();
return linkPath.length > 0 ? linkPath.join(' / ') + ' ' +
cleanPath(node.displayShortName()) : '/';
},
fillTemplate: function (node, templateData) {
var opts = this.opts,
linkMapper = opts.linkMapper;
templateData.entity = node.name || 'All files';
templateData.metrics = node.metrics;
templateData.reportClass = getReportClass(node.metrics.statements, opts.watermarks.statements);
templateData.pathHtml = this.getPathHtml(node, linkMapper);
templateData.base = {
css: linkMapper.asset(node, 'base.css')
};
templateData.sorter = {
js: linkMapper.asset(node, 'sorter.js'),
image: linkMapper.asset(node, 'sort-arrow-sprite.png')
};
templateData.prettify = {
js: linkMapper.asset(node, 'prettify.js'),
css: linkMapper.asset(node, 'prettify.css')
};
},
writeDetailPage: function (writer, node, fileCoverage) {
var opts = this.opts,
sourceStore = opts.sourceStore,
templateData = opts.templateData,
sourceText = fileCoverage.code && Array.isArray(fileCoverage.code) ?
fileCoverage.code.join('\n') + '\n' : sourceStore.get(fileCoverage.path),
code = sourceText.split(/(?:\r?\n)|\r/),
count = 0,
structured = code.map(function (str) { count += 1; return { line: count, covered: null, text: new InsertionText(str, true) }; }),
context;
structured.unshift({ line: 0, covered: null, text: new InsertionText("") });
this.fillTemplate(node, templateData);
writer.write(headerTemplate(templateData));
writer.write('<pre><table class="coverage">\n');
annotateLines(fileCoverage, structured);
//note: order is important, since statements typically result in spanning the whole line and doing branches late
//causes mismatched tags
annotateBranches(fileCoverage, structured);
annotateFunctions(fileCoverage, structured);
annotateStatements(fileCoverage, structured);
structured.shift();
context = {
structured: structured,
maxLines: structured.length,
fileCoverage: fileCoverage
};
writer.write(detailTemplate(context));
writer.write('</table></pre>\n');
writer.write(footerTemplate(templateData));
},
writeIndexPage: function (writer, node) {
var linkMapper = this.opts.linkMapper,
templateData = this.opts.templateData,
children = Array.prototype.slice.apply(node.children),
watermarks = this.opts.watermarks;
children.sort(function (a, b) {
return a.name < b.name ? -1 : 1;
});
this.fillTemplate(node, templateData);
writer.write(headerTemplate(templateData));
writer.write(summaryTableHeader);
children.forEach(function (child) {
var metrics = child.metrics,
reportClasses = {
statements: getReportClass(metrics.statements, watermarks.statements),
lines: getReportClass(metrics.lines, watermarks.lines),
functions: getReportClass(metrics.functions, watermarks.functions),
branches: getReportClass(metrics.branches, watermarks.branches)
},
data = {
metrics: metrics,
reportClasses: reportClasses,
file: cleanPath(child.displayShortName()),
output: linkMapper.fromParent(child)
};
writer.write(summaryLineTemplate(data) + '\n');
});
writer.write(summaryTableFooter);
writer.write(footerTemplate(templateData));
},
writeFiles: function (writer, node, dir, collector) {
var that = this,
indexFile = path.resolve(dir, 'index.html'),
childFile;
if (this.opts.verbose) { console.error('Writing ' + indexFile); }
writer.writeFile(indexFile, function (contentWriter) {
that.writeIndexPage(contentWriter, node);
});
node.children.forEach(function (child) {
if (child.kind === 'dir') {
that.writeFiles(writer, child, path.resolve(dir, child.relativeName), collector);
} else {
childFile = path.resolve(dir, child.relativeName + '.html');
if (that.opts.verbose) { console.error('Writing ' + childFile); }
writer.writeFile(childFile, function (contentWriter) {
that.writeDetailPage(contentWriter, child, collector.fileCoverageFor(child.fullPath()));
});
}
});
},
standardLinkMapper: function () {
return {
fromParent: function (node) {
var relativeName = cleanPath(node.relativeName);
return node.kind === 'dir' ? relativeName + 'index.html' : relativeName + '.html';
},
ancestorHref: function (node, num) {
var href = '',
notDot = function(part) {
return part !== '.';
},
separated,
levels,
i,
j;
for (i = 0; i < num; i += 1) {
separated = cleanPath(node.relativeName).split('/').filter(notDot);
levels = separated.length - 1;
for (j = 0; j < levels; j += 1) {
href += '../';
}
node = node.parent;
}
return href;
},
ancestor: function (node, num) {
return this.ancestorHref(node, num) + 'index.html';
},
asset: function (node, name) {
var i = 0,
parent = node.parent;
while (parent) { i += 1; parent = parent.parent; }
return this.ancestorHref(node, i) + name;
}
};
},
writeReport: function (collector, sync) {
var opts = this.opts,
dir = opts.dir,
summarizer = new TreeSummarizer(),
writer = opts.writer || new FileWriter(sync),
that = this,
tree,
copyAssets = function (subdir) {
var srcDir = path.resolve(__dirname, '..', 'assets', subdir);
fs.readdirSync(srcDir).forEach(function (f) {
var resolvedSource = path.resolve(srcDir, f),
resolvedDestination = path.resolve(dir, f),
stat = fs.statSync(resolvedSource);
if (stat.isFile()) {
if (opts.verbose) {
console.log('Write asset: ' + resolvedDestination);
}
writer.copyFile(resolvedSource, resolvedDestination);
}
});
};
collector.files().forEach(function (key) {
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(collector.fileCoverageFor(key)));
});
tree = summarizer.getTreeSummary();
[ '.', 'vendor'].forEach(function (subdir) {
copyAssets(subdir);
});
writer.on('done', function () { that.emit('done'); });
//console.log(JSON.stringify(tree.root, undefined, 4));
this.writeFiles(writer, tree.root, dir, collector);
writer.done();
}
});
module.exports = HtmlReport;

View File

@ -0,0 +1,104 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
EventEmitter = require('events').EventEmitter,
Factory = require('../util/factory'),
factory = new Factory('report', __dirname, false);
/**
* An abstraction for producing coverage reports.
* This class is both the base class as well as a factory for `Report` implementations.
* All reports are event emitters and are expected to emit a `done` event when
* the report writing is complete.
*
* See also the `Reporter` class for easily producing multiple coverage reports
* with a single call.
*
* Usage
* -----
*
* var Report = require('istanbul').Report,
* report = Report.create('html'),
* collector = new require('istanbul').Collector;
*
* collector.add(coverageObject);
* report.on('done', function () { console.log('done'); });
* report.writeReport(collector);
*
* @class Report
* @module report
* @main report
* @constructor
* @protected
* @param {Object} options Optional. The options supported by a specific store implementation.
*/
function Report(/* options */) {
EventEmitter.call(this);
}
util.inherits(Report, EventEmitter);
//add register, create, mix, loadAll, getReportList as class methods
factory.bindClassMethods(Report);
/**
* registers a new report implementation.
* @method register
* @static
* @param {Function} constructor the constructor function for the report. This function must have a
* `TYPE` property of type String, that will be used in `Report.create()`
*/
/**
* returns a report implementation of the specified type.
* @method create
* @static
* @param {String} type the type of report to create
* @param {Object} opts Optional. Options specific to the report implementation
* @return {Report} a new store of the specified type
*/
/**
* returns the list of available reports as an array of strings
* @method getReportList
* @static
* @return an array of supported report formats
*/
var proto = {
/**
* returns a one-line summary of the report
* @method synopsis
* @return {String} a description of what the report is about
*/
synopsis: function () {
throw new Error('synopsis must be overridden');
},
/**
* returns a config object that has override-able keys settable via config
* @method getDefaultConfig
* @return {Object|null} an object representing keys that can be overridden via
* the istanbul configuration where the values are the defaults used when
* not specified. A null return implies no config attributes
*/
getDefaultConfig: function () {
return null;
},
/**
* writes the report for a set of coverage objects added to a collector.
* @method writeReport
* @param {Collector} collector the collector for getting the set of files and coverage
* @param {Boolean} sync true if reports must be written synchronously, false if they can be written using asynchronous means (e.g. stream.write)
*/
writeReport: function (/* collector, sync */) {
throw new Error('writeReport: must be overridden');
}
};
Object.keys(proto).forEach(function (k) {
Report.prototype[k] = proto[k];
});
module.exports = Report;

View File

@ -0,0 +1,75 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
objectUtils = require('../object-utils'),
Writer = require('../util/file-writer'),
util = require('util'),
Report = require('./index');
/**
* a `Report` implementation that produces a coverage JSON object with summary info only.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('json-summary');
*
*
* @class JsonSummaryReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to write the `coverage-summary.json` file. Defaults to `process.cwd()`
*/
function JsonSummaryReport(opts) {
this.opts = opts || {};
this.opts.dir = this.opts.dir || process.cwd();
this.opts.file = this.opts.file || this.getDefaultConfig().file;
this.opts.writer = this.opts.writer || null;
}
JsonSummaryReport.TYPE = 'json-summary';
util.inherits(JsonSummaryReport, Report);
Report.mix(JsonSummaryReport, {
synopsis: function () {
return 'prints a summary coverage object as JSON to a file';
},
getDefaultConfig: function () {
return {
file: 'coverage-summary.json'
};
},
writeReport: function (collector, sync) {
var outputFile = path.resolve(this.opts.dir, this.opts.file),
writer = this.opts.writer || new Writer(sync),
that = this;
var summaries = [],
finalSummary;
collector.files().forEach(function (file) {
summaries.push(objectUtils.summarizeFileCoverage(collector.fileCoverageFor(file)));
});
finalSummary = objectUtils.mergeSummaryObjects.apply(null, summaries);
writer.on('done', function () { that.emit('done'); });
writer.writeFile(outputFile, function (contentWriter) {
contentWriter.println("{");
contentWriter.write('"total":');
contentWriter.write(JSON.stringify(finalSummary));
collector.files().forEach(function (key) {
contentWriter.println(",");
contentWriter.write(JSON.stringify(key));
contentWriter.write(":");
contentWriter.write(JSON.stringify(objectUtils.summarizeFileCoverage(collector.fileCoverageFor(key))));
});
contentWriter.println("}");
});
writer.done();
}
});
module.exports = JsonSummaryReport;

View File

@ -0,0 +1,69 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
Writer = require('../util/file-writer'),
util = require('util'),
Report = require('./index');
/**
* a `Report` implementation that produces a coverage JSON object.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('json');
*
*
* @class JsonReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to write the `coverage-final.json` file. Defaults to `process.cwd()`
*/
function JsonReport(opts) {
this.opts = opts || {};
this.opts.dir = this.opts.dir || process.cwd();
this.opts.file = this.opts.file || this.getDefaultConfig().file;
this.opts.writer = this.opts.writer || null;
}
JsonReport.TYPE = 'json';
util.inherits(JsonReport, Report);
Report.mix(JsonReport, {
synopsis: function () {
return 'prints the coverage object as JSON to a file';
},
getDefaultConfig: function () {
return {
file: 'coverage-final.json'
};
},
writeReport: function (collector, sync) {
var outputFile = path.resolve(this.opts.dir, this.opts.file),
writer = this.opts.writer || new Writer(sync),
that = this;
writer.on('done', function () { that.emit('done'); });
writer.writeFile(outputFile, function (contentWriter) {
var first = true;
contentWriter.println("{");
collector.files().forEach(function (key) {
if (first) {
first = false;
} else {
contentWriter.println(",");
}
contentWriter.write(JSON.stringify(key));
contentWriter.write(":");
contentWriter.write(JSON.stringify(collector.fileCoverageFor(key)));
});
contentWriter.println("}");
});
writer.done();
}
});
module.exports = JsonReport;

View File

@ -0,0 +1,65 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
util = require('util'),
mkdirp = require('mkdirp'),
Report = require('./index'),
LcovOnlyReport = require('./lcovonly'),
HtmlReport = require('./html');
/**
* a `Report` implementation that produces an LCOV coverage file and an associated HTML report from coverage objects.
* The name and behavior of this report is designed to ease migration for projects that currently use `yuitest_coverage`
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('lcov');
*
*
* @class LcovReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the `lcov.info` file.
* HTML files are written in a subdirectory called `lcov-report`. Defaults to `process.cwd()`
*/
function LcovReport(opts) {
Report.call(this);
opts = opts || {};
var baseDir = path.resolve(opts.dir || process.cwd()),
htmlDir = path.resolve(baseDir, 'lcov-report');
mkdirp.sync(baseDir);
this.lcov = new LcovOnlyReport({ dir: baseDir, watermarks: opts.watermarks });
this.html = new HtmlReport({ dir: htmlDir, watermarks: opts.watermarks, sourceStore: opts.sourceStore});
}
LcovReport.TYPE = 'lcov';
util.inherits(LcovReport, Report);
Report.mix(LcovReport, {
synopsis: function () {
return 'combined lcovonly and html report that generates an lcov.info file as well as HTML';
},
writeReport: function (collector, sync) {
var handler = this.handleDone.bind(this);
this.inProgress = 2;
this.lcov.on('done', handler);
this.html.on('done', handler);
this.lcov.writeReport(collector, sync);
this.html.writeReport(collector, sync);
},
handleDone: function () {
this.inProgress -= 1;
if (this.inProgress === 0) {
this.emit('done');
}
}
});
module.exports = LcovReport;

View File

@ -0,0 +1,103 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
Writer = require('../util/file-writer'),
util = require('util'),
Report = require('./index'),
utils = require('../object-utils');
/**
* a `Report` implementation that produces an LCOV coverage file from coverage objects.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('lcovonly');
*
*
* @class LcovOnlyReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the `lcov.info` file. Defaults to `process.cwd()`
*/
function LcovOnlyReport(opts) {
this.opts = opts || {};
this.opts.dir = this.opts.dir || process.cwd();
this.opts.file = this.opts.file || this.getDefaultConfig().file;
this.opts.writer = this.opts.writer || null;
}
LcovOnlyReport.TYPE = 'lcovonly';
util.inherits(LcovOnlyReport, Report);
Report.mix(LcovOnlyReport, {
synopsis: function () {
return 'lcov coverage report that can be consumed by the lcov tool';
},
getDefaultConfig: function () {
return { file: 'lcov.info' };
},
writeFileCoverage: function (writer, fc) {
var functions = fc.f,
functionMap = fc.fnMap,
lines = fc.l,
branches = fc.b,
branchMap = fc.branchMap,
summary = utils.summarizeFileCoverage(fc);
writer.println('TN:'); //no test name
writer.println('SF:' + fc.path);
Object.keys(functions).forEach(function (key) {
var meta = functionMap[key];
writer.println('FN:' + [ meta.line, meta.name ].join(','));
});
writer.println('FNF:' + summary.functions.total);
writer.println('FNH:' + summary.functions.covered);
Object.keys(functions).forEach(function (key) {
var stats = functions[key],
meta = functionMap[key];
writer.println('FNDA:' + [ stats, meta.name ].join(','));
});
Object.keys(lines).forEach(function (key) {
var stat = lines[key];
writer.println('DA:' + [ key, stat ].join(','));
});
writer.println('LF:' + summary.lines.total);
writer.println('LH:' + summary.lines.covered);
Object.keys(branches).forEach(function (key) {
var branchArray = branches[key],
meta = branchMap[key],
line = meta.line,
i = 0;
branchArray.forEach(function (b) {
writer.println('BRDA:' + [line, key, i, b].join(','));
i += 1;
});
});
writer.println('BRF:' + summary.branches.total);
writer.println('BRH:' + summary.branches.covered);
writer.println('end_of_record');
},
writeReport: function (collector, sync) {
var outputFile = path.resolve(this.opts.dir, this.opts.file),
writer = this.opts.writer || new Writer(sync),
that = this;
writer.on('done', function () { that.emit('done'); });
writer.writeFile(outputFile, function (contentWriter) {
collector.files().forEach(function (key) {
that.writeFileCoverage(contentWriter, collector.fileCoverageFor(key));
});
});
writer.done();
}
});
module.exports = LcovOnlyReport;

View File

@ -0,0 +1,41 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
Report = require('./index');
/**
* a `Report` implementation that does nothing. Use to specify that no reporting
* is needed.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('none');
*
*
* @class NoneReport
* @extends Report
* @module report
* @constructor
*/
function NoneReport() {
Report.call(this);
}
NoneReport.TYPE = 'none';
util.inherits(NoneReport, Report);
Report.mix(NoneReport, {
synopsis: function () {
return 'Does nothing. Useful to override default behavior and suppress reporting entirely';
},
writeReport: function (/* collector, sync */) {
//noop
this.emit('done');
}
});
module.exports = NoneReport;

View File

@ -0,0 +1,92 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
util = require('util'),
mkdirp = require('mkdirp'),
fs = require('fs'),
utils = require('../object-utils'),
Report = require('./index');
/**
* a `Report` implementation that produces system messages interpretable by TeamCity.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('teamcity');
*
* @class TeamcityReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the text coverage report will be written, when writing to a file
* @param {String} [opts.file] the filename for the report. When omitted, the report is written to console
*/
function TeamcityReport(opts) {
Report.call(this);
opts = opts || {};
this.dir = opts.dir || process.cwd();
this.file = opts.file;
this.blockName = opts.blockName || this.getDefaultConfig().blockName;
}
TeamcityReport.TYPE = 'teamcity';
util.inherits(TeamcityReport, Report);
function lineForKey(value, teamcityVar) {
return '##teamcity[buildStatisticValue key=\'' + teamcityVar + '\' value=\'' + value + '\']';
}
Report.mix(TeamcityReport, {
synopsis: function () {
return 'report with system messages that can be interpreted with TeamCity';
},
getDefaultConfig: function () {
return { file: null , blockName: 'Code Coverage Summary'};
},
writeReport: function (collector /*, sync */) {
var summaries = [],
finalSummary,
lines = [],
text;
collector.files().forEach(function (file) {
summaries.push(utils.summarizeFileCoverage(collector.fileCoverageFor(file)));
});
finalSummary = utils.mergeSummaryObjects.apply(null, summaries);
lines.push('');
lines.push('##teamcity[blockOpened name=\''+ this.blockName +'\']');
//Statements Covered
lines.push(lineForKey(finalSummary.statements.pct, 'CodeCoverageB'));
//Methods Covered
lines.push(lineForKey(finalSummary.functions.covered, 'CodeCoverageAbsMCovered'));
lines.push(lineForKey(finalSummary.functions.total, 'CodeCoverageAbsMTotal'));
lines.push(lineForKey(finalSummary.functions.pct, 'CodeCoverageM'));
//Lines Covered
lines.push(lineForKey(finalSummary.lines.covered, 'CodeCoverageAbsLCovered'));
lines.push(lineForKey(finalSummary.lines.total, 'CodeCoverageAbsLTotal'));
lines.push(lineForKey(finalSummary.lines.pct, 'CodeCoverageL'));
lines.push('##teamcity[blockClosed name=\''+ this.blockName +'\']');
text = lines.join('\n');
if (this.file) {
mkdirp.sync(this.dir);
fs.writeFileSync(path.join(this.dir, this.file), text, 'utf8');
} else {
console.log(text);
}
this.emit('done');
}
});
module.exports = TeamcityReport;

View File

@ -0,0 +1,20 @@
<div class='push'></div><!-- for sticky footer -->
</div><!-- /wrapper -->
<div class='footer quiet pad2 space-top1 center small'>
Code coverage
generated by <a href="http://istanbul-js.org/" target="_blank">istanbul</a> at {{datetime}}
</div>
</div>
{{#if prettify}}
<script src="{{prettify.js}}"></script>
<script>
window.onload = function () {
if (typeof prettyPrint === 'function') {
prettyPrint();
}
};
</script>
{{/if}}
<script src="{{sorter.js}}"></script>
</body>
</html>

View File

@ -0,0 +1,60 @@
<!doctype html>
<html lang="en">
<head>
<title>Code coverage report for {{entity}}</title>
<meta charset="utf-8" />
{{#if prettify}}
<link rel="stylesheet" href="{{prettify.css}}" />
{{/if}}
<link rel="stylesheet" href="{{base.css}}" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<style type='text/css'>
.coverage-summary .sorter {
background-image: url({{sorter.image}});
}
</style>
</head>
<body>
<div class='wrapper'>
<div class='pad1'>
<h1>
{{{pathHtml}}}
</h1>
<div class='clearfix'>
{{#with metrics.statements}}
<div class='fl pad1y space-right2'>
<span class="strong">{{pct}}% </span>
<span class="quiet">Statements</span>
<span class='fraction'>{{covered}}/{{total}}</span>
</div>
{{/with}}
{{#with metrics.branches}}
<div class='fl pad1y space-right2'>
<span class="strong">{{pct}}% </span>
<span class="quiet">Branches</span>
<span class='fraction'>{{covered}}/{{total}}</span>
</div>
{{/with}}
{{#with metrics.functions}}
<div class='fl pad1y space-right2'>
<span class="strong">{{pct}}% </span>
<span class="quiet">Functions</span>
<span class='fraction'>{{covered}}/{{total}}</span>
</div>
{{/with}}
{{#with metrics.lines}}
<div class='fl pad1y space-right2'>
<span class="strong">{{pct}}% </span>
<span class="quiet">Lines</span>
<span class='fraction'>{{covered}}/{{total}}</span>
</div>
{{/with}}
{{#if_has_ignores metrics}}
<div class='fl pad1y'>
<span class="strong">{{#show_ignores metrics}}{{/show_ignores}}</span>
<span class="quiet">Ignored</span> &nbsp;&nbsp;&nbsp;&nbsp;
</div>
{{/if_has_ignores}}
</div>
</div>
<div class='status-line {{reportClass}}'></div>

View File

@ -0,0 +1,50 @@
var LcovOnly = require('./lcovonly'),
util = require('util');
/**
* a `Report` implementation that produces an LCOV coverage and prints it
* to standard out.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('text-lcov');
*
* @class TextLcov
* @module report
* @extends LcovOnly
* @constructor
* @param {Object} opts optional
* @param {String} [opts.log] the method used to log to console.
*/
function TextLcov(opts) {
var that = this;
LcovOnly.call(this);
this.opts = opts || {};
this.opts.log = this.opts.log || console.log;
this.opts.writer = {
println: function (ln) {
that.opts.log(ln);
}
};
}
TextLcov.TYPE = 'text-lcov';
util.inherits(TextLcov, LcovOnly);
LcovOnly.super_.mix(TextLcov, {
writeReport: function (collector) {
var that = this,
writer = this.opts.writer;
collector.files().forEach(function (key) {
that.writeFileCoverage(writer, collector.fileCoverageFor(key));
});
this.emit('done');
}
});
module.exports = TextLcov;

View File

@ -0,0 +1,93 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
util = require('util'),
mkdirp = require('mkdirp'),
defaults = require('./common/defaults'),
fs = require('fs'),
utils = require('../object-utils'),
Report = require('./index');
/**
* a `Report` implementation that produces text output for overall coverage in summary format.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('text-summary');
*
* @class TextSummaryReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the text coverage report will be written, when writing to a file
* @param {String} [opts.file] the filename for the report. When omitted, the report is written to console
*/
function TextSummaryReport(opts) {
Report.call(this);
opts = opts || {};
this.dir = opts.dir || process.cwd();
this.file = opts.file;
this.watermarks = opts.watermarks || defaults.watermarks();
}
TextSummaryReport.TYPE = 'text-summary';
util.inherits(TextSummaryReport, Report);
function lineForKey(summary, key, watermarks) {
var metrics = summary[key],
skipped,
result,
clazz = defaults.classFor(key, summary, watermarks);
key = key.substring(0, 1).toUpperCase() + key.substring(1);
if (key.length < 12) { key += ' '.substring(0, 12 - key.length); }
result = [ key , ':', metrics.pct + '%', '(', metrics.covered + '/' + metrics.total, ')'].join(' ');
skipped = metrics.skipped;
if (skipped > 0) {
result += ', ' + skipped + ' ignored';
}
return defaults.colorize(result, clazz);
}
Report.mix(TextSummaryReport, {
synopsis: function () {
return 'text report that prints a coverage summary across all files, typically to console';
},
getDefaultConfig: function () {
return { file: null };
},
writeReport: function (collector /*, sync */) {
var summaries = [],
finalSummary,
lines = [],
watermarks = this.watermarks,
text;
collector.files().forEach(function (file) {
summaries.push(utils.summarizeFileCoverage(collector.fileCoverageFor(file)));
});
finalSummary = utils.mergeSummaryObjects.apply(null, summaries);
lines.push('');
lines.push('=============================== Coverage summary ===============================');
lines.push.apply(lines, [
lineForKey(finalSummary, 'statements', watermarks),
lineForKey(finalSummary, 'branches', watermarks),
lineForKey(finalSummary, 'functions', watermarks),
lineForKey(finalSummary, 'lines', watermarks)
]);
lines.push('================================================================================');
text = lines.join('\n');
if (this.file) {
mkdirp.sync(this.dir);
fs.writeFileSync(path.join(this.dir, this.file), text, 'utf8');
} else {
console.log(text);
}
this.emit('done');
}
});
module.exports = TextSummaryReport;

View File

@ -0,0 +1,234 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
mkdirp = require('mkdirp'),
util = require('util'),
fs = require('fs'),
defaults = require('./common/defaults'),
Report = require('./index'),
TreeSummarizer = require('../util/tree-summarizer'),
utils = require('../object-utils'),
PCT_COLS = 9,
MISSING_COL = 15,
TAB_SIZE = 1,
DELIM = ' |',
COL_DELIM = '-|';
/**
* a `Report` implementation that produces text output in a detailed table.
*
* Usage
* -----
*
* var report = require('istanbul').Report.create('text');
*
* @class TextReport
* @extends Report
* @module report
* @constructor
* @param {Object} opts optional
* @param {String} [opts.dir] the directory in which to the text coverage report will be written, when writing to a file
* @param {String} [opts.file] the filename for the report. When omitted, the report is written to console
* @param {Number} [opts.maxCols] the max column width of the report. By default, the width of the report is adjusted based on the length of the paths
* to be reported.
*/
function TextReport(opts) {
Report.call(this);
opts = opts || {};
this.dir = opts.dir || process.cwd();
this.file = opts.file;
this.summary = opts.summary;
this.maxCols = opts.maxCols || 0;
this.watermarks = opts.watermarks || defaults.watermarks();
}
TextReport.TYPE = 'text';
util.inherits(TextReport, Report);
function padding(num, ch) {
var str = '',
i;
ch = ch || ' ';
for (i = 0; i < num; i += 1) {
str += ch;
}
return str;
}
function fill(str, width, right, tabs, clazz) {
tabs = tabs || 0;
str = String(str);
var leadingSpaces = tabs * TAB_SIZE,
remaining = width - leadingSpaces,
leader = padding(leadingSpaces),
fmtStr = '',
fillStr,
strlen = str.length;
if (remaining > 0) {
if (remaining >= strlen) {
fillStr = padding(remaining - strlen);
fmtStr = right ? fillStr + str : str + fillStr;
} else {
fmtStr = str.substring(strlen - remaining);
fmtStr = '... ' + fmtStr.substring(4);
}
}
fmtStr = defaults.colorize(fmtStr, clazz);
return leader + fmtStr;
}
function formatName(name, maxCols, level, clazz) {
return fill(name, maxCols, false, level, clazz);
}
function formatPct(pct, clazz, width) {
return fill(pct, width || PCT_COLS, true, 0, clazz);
}
function nodeName(node) {
return node.displayShortName() || 'All files';
}
function tableHeader(maxNameCols) {
var elements = [];
elements.push(formatName('File', maxNameCols, 0));
elements.push(formatPct('% Stmts'));
elements.push(formatPct('% Branch'));
elements.push(formatPct('% Funcs'));
elements.push(formatPct('% Lines'));
elements.push(formatPct('Uncovered Lines', undefined, MISSING_COL));
return elements.join(' |') + ' |';
}
function collectMissingLines(kind, linesCovered) {
var missingLines = [];
if (kind !== 'file') {
return [];
}
Object.keys(linesCovered).forEach(function (key) {
if (!linesCovered[key]) {
missingLines.push(key);
}
});
return missingLines;
}
function tableRow(node, maxNameCols, level, watermarks) {
var name = nodeName(node),
statements = node.metrics.statements.pct,
branches = node.metrics.branches.pct,
functions = node.metrics.functions.pct,
lines = node.metrics.lines.pct,
missingLines = collectMissingLines(node.kind, node.metrics.linesCovered),
elements = [];
elements.push(formatName(name, maxNameCols, level, defaults.classFor('statements', node.metrics, watermarks)));
elements.push(formatPct(statements, defaults.classFor('statements', node.metrics, watermarks)));
elements.push(formatPct(branches, defaults.classFor('branches', node.metrics, watermarks)));
elements.push(formatPct(functions, defaults.classFor('functions', node.metrics, watermarks)));
elements.push(formatPct(lines, defaults.classFor('lines', node.metrics, watermarks)));
elements.push(formatPct(missingLines.join(','), 'low', MISSING_COL));
return elements.join(DELIM) + DELIM;
}
function findNameWidth(node, level, last) {
last = last || 0;
level = level || 0;
var idealWidth = TAB_SIZE * level + nodeName(node).length;
if (idealWidth > last) {
last = idealWidth;
}
node.children.forEach(function (child) {
last = findNameWidth(child, level + 1, last);
});
return last;
}
function makeLine(nameWidth) {
var name = padding(nameWidth, '-'),
pct = padding(PCT_COLS, '-'),
elements = [];
elements.push(name);
elements.push(pct);
elements.push(pct);
elements.push(pct);
elements.push(pct);
elements.push(padding(MISSING_COL, '-'));
return elements.join(COL_DELIM) + COL_DELIM;
}
function walk(node, nameWidth, array, level, watermarks) {
var line;
if (level === 0) {
line = makeLine(nameWidth);
array.push(line);
array.push(tableHeader(nameWidth));
array.push(line);
} else {
array.push(tableRow(node, nameWidth, level, watermarks));
}
node.children.forEach(function (child) {
walk(child, nameWidth, array, level + 1, watermarks);
});
if (level === 0) {
array.push(line);
array.push(tableRow(node, nameWidth, level, watermarks));
array.push(line);
}
}
Report.mix(TextReport, {
synopsis: function () {
return 'text report that prints a coverage line for every file, typically to console';
},
getDefaultConfig: function () {
return { file: null, maxCols: 0 };
},
writeReport: function (collector /*, sync */) {
var summarizer = new TreeSummarizer(),
tree,
root,
nameWidth,
statsWidth = 4 * (PCT_COLS + 2) + MISSING_COL,
maxRemaining,
strings = [],
text;
collector.files().forEach(function (key) {
summarizer.addFileCoverageSummary(key, utils.summarizeFileCoverage(
collector.fileCoverageFor(key)
));
});
tree = summarizer.getTreeSummary();
root = tree.root;
nameWidth = findNameWidth(root);
if (this.maxCols > 0) {
maxRemaining = this.maxCols - statsWidth - 2;
if (nameWidth > maxRemaining) {
nameWidth = maxRemaining;
}
}
walk(root, nameWidth, strings, 0, this.watermarks);
text = strings.join('\n') + '\n';
if (this.file) {
mkdirp.sync(this.dir);
fs.writeFileSync(path.join(this.dir, this.file), text, 'utf8');
} else {
console.log(text);
}
this.emit('done');
}
});
module.exports = TextReport;

View File

@ -0,0 +1,111 @@
/*
Copyright (c) 2014, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Report = require('./report'),
configuration = require('./config'),
inputError = require('./util/input-error');
/**
* convenience mechanism to write one or more reports ensuring that config
* options are respected.
* Usage
* -----
*
* var fs = require('fs'),
* reporter = new require('istanbul').Reporter(),
* collector = new require('istanbul').Collector(),
* sync = true;
*
* collector.add(JSON.parse(fs.readFileSync('coverage.json', 'utf8')));
* reporter.add('lcovonly');
* reporter.addAll(['clover', 'cobertura']);
* reporter.write(collector, sync, function () { console.log('done'); });
*
* @class Reporter
* @param {Configuration} cfg the config object, a falsy value will load the
* default configuration instead
* @param {String} dir the directory in which to write the reports, may be falsy
* to use config or global defaults
* @constructor
* @module main
*/
function Reporter(cfg, dir) {
this.config = cfg || configuration.loadFile();
this.dir = dir || this.config.reporting.dir();
this.reports = {};
}
Reporter.prototype = {
/**
* adds a report to be generated. Must be one of the entries returned
* by `Report.getReportList()`
* @method add
* @param {String} fmt the format of the report to generate
*/
add: function (fmt) {
if (this.reports[fmt]) { // already added
return;
}
var config = this.config,
rptConfig = config.reporting.reportConfig()[fmt] || {};
rptConfig.verbose = config.verbose;
rptConfig.dir = this.dir;
rptConfig.watermarks = config.reporting.watermarks();
try {
this.reports[fmt] = Report.create(fmt, rptConfig);
} catch (ex) {
throw inputError.create('Invalid report format [' + fmt + ']');
}
},
/**
* adds an array of report formats to be generated
* @method addAll
* @param {Array} fmts an array of report formats
*/
addAll: function (fmts) {
var that = this;
fmts.forEach(function (f) {
that.add(f);
});
},
/**
* writes all reports added and calls the callback when done
* @method write
* @param {Collector} collector the collector having the coverage data
* @param {Boolean} sync true to write reports synchronously
* @param {Function} callback the callback to call when done. When `sync`
* is true, the callback will be called in the same process tick.
*/
write: function (collector, sync, callback) {
var reports = this.reports,
verbose = this.config.verbose,
handler = this.handleDone.bind(this, callback);
this.inProgress = Object.keys(reports).length;
Object.keys(reports).forEach(function (name) {
var report = reports[name];
if (verbose) {
console.error('Write report: ' + name);
}
report.on('done', handler);
report.writeReport(collector, sync);
});
},
/*
* handles listening on all reports to be completed before calling the callback
* @method handleDone
* @private
* @param {Function} callback the callback to call when all reports are
* written
*/
handleDone: function (callback) {
this.inProgress -= 1;
if (this.inProgress === 0) {
return callback();
}
}
};
module.exports = Reporter;

View File

@ -0,0 +1,61 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
fs = require('fs'),
Store = require('./index');
/**
* a `Store` implementation that doesn't actually store anything. It assumes that keys
* are absolute file paths, and contents are contents of those files.
* Thus, `set` for this store is no-op, `get` returns the
* contents of the filename that the key represents, `hasKey` returns true if the key
* supplied is a valid file path and `keys` always returns an empty array.
*
* Usage
* -----
*
* var store = require('istanbul').Store.create('fslookup');
*
*
* @class LookupStore
* @extends Store
* @module store
* @constructor
*/
function LookupStore(opts) {
Store.call(this, opts);
}
LookupStore.TYPE = 'fslookup';
util.inherits(LookupStore, Store);
Store.mix(LookupStore, {
keys: function () {
return [];
},
get: function (key) {
return fs.readFileSync(key, 'utf8');
},
hasKey: function (key) {
var stats;
try {
stats = fs.statSync(key);
return stats.isFile();
} catch (ex) {
return false;
}
},
set: function (key /*, contents */) {
if (!this.hasKey(key)) {
throw new Error('Attempt to set contents for non-existent file [' + key + '] on a fslookup store');
}
return key;
}
});
module.exports = LookupStore;

View File

@ -0,0 +1,123 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var Factory = require('../util/factory'),
factory = new Factory('store', __dirname, false);
/**
* An abstraction for keeping track of content against some keys (e.g.
* original source, instrumented source, coverage objects against file names).
* This class is both the base class as well as a factory for `Store` implementations.
*
* Usage
* -----
*
* var Store = require('istanbul').Store,
* store = Store.create('memory');
*
* //basic use
* store.set('foo', 'foo-content');
* var content = store.get('foo');
*
* //keys and values
* store.keys().forEach(function (key) {
* console.log(key + ':\n' + store.get(key);
* });
* if (store.hasKey('bar') { console.log(store.get('bar'); }
*
*
* //syntactic sugar
* store.setObject('foo', { foo: true });
* console.log(store.getObject('foo').foo);
*
* store.dispose();
*
* @class Store
* @constructor
* @module store
* @param {Object} options Optional. The options supported by a specific store implementation.
* @main store
*/
function Store(/* options */) {}
//add register, create, mix, loadAll, getStoreList as class methods
factory.bindClassMethods(Store);
/**
* registers a new store implementation.
* @method register
* @static
* @param {Function} constructor the constructor function for the store. This function must have a
* `TYPE` property of type String, that will be used in `Store.create()`
*/
/**
* returns a store implementation of the specified type.
* @method create
* @static
* @param {String} type the type of store to create
* @param {Object} opts Optional. Options specific to the store implementation
* @return {Store} a new store of the specified type
*/
Store.prototype = {
/**
* sets some content associated with a specific key. The manner in which
* duplicate keys are handled for multiple `set()` calls with the same
* key is implementation-specific.
*
* @method set
* @param {String} key the key for the content
* @param {String} contents the contents for the key
*/
set: function (/* key, contents */) { throw new Error("set: must be overridden"); },
/**
* returns the content associated to a specific key or throws if the key
* was not `set`
* @method get
* @param {String} key the key for which to get the content
* @return {String} the content for the specified key
*/
get: function (/* key */) { throw new Error("get: must be overridden"); },
/**
* returns a list of all known keys
* @method keys
* @return {Array} an array of seen keys
*/
keys: function () { throw new Error("keys: must be overridden"); },
/**
* returns true if the key is one for which a `get()` call would work.
* @method hasKey
* @param {String} key
* @return true if the key is valid for this store, false otherwise
*/
hasKey: function (/* key */) { throw new Error("hasKey: must be overridden"); },
/**
* lifecycle method to dispose temporary resources associated with the store
* @method dispose
*/
dispose: function () {},
/**
* sugar method to return an object associated with a specific key. Throws
* if the content set against the key was not a valid JSON string.
* @method getObject
* @param {String} key the key for which to return the associated object
* @return {Object} the object corresponding to the key
*/
getObject: function (key) {
return JSON.parse(this.get(key));
},
/**
* sugar method to set an object against a specific key.
* @method setObject
* @param {String} key the key for the object
* @param {Object} object the object to be stored
*/
setObject: function (key, object) {
return this.set(key, JSON.stringify(object));
}
};
module.exports = Store;

View File

@ -0,0 +1,56 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
Store = require('./index');
/**
* a `Store` implementation using an in-memory object.
*
* Usage
* -----
*
* var store = require('istanbul').Store.create('memory');
*
*
* @class MemoryStore
* @extends Store
* @module store
* @constructor
*/
function MemoryStore() {
Store.call(this);
this.map = {};
}
MemoryStore.TYPE = 'memory';
util.inherits(MemoryStore, Store);
Store.mix(MemoryStore, {
set: function (key, contents) {
this.map[key] = contents;
},
get: function (key) {
if (!this.hasKey(key)) {
throw new Error('Unable to find entry for [' + key + ']');
}
return this.map[key];
},
hasKey: function (key) {
return this.map.hasOwnProperty(key);
},
keys: function () {
return Object.keys(this.map);
},
dispose: function () {
this.map = {};
}
});
module.exports = MemoryStore;

View File

@ -0,0 +1,81 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
path = require('path'),
os = require('os'),
fs = require('fs'),
mkdirp = require('mkdirp'),
Store = require('./index');
function makeTempDir() {
var dir = path.join(os.tmpDir ? os.tmpDir() : /* istanbul ignore next */ (process.env.TMPDIR || '/tmp'), 'ts' + new Date().getTime());
mkdirp.sync(dir);
return dir;
}
/**
* a `Store` implementation using temporary files.
*
* Usage
* -----
*
* var store = require('istanbul').Store.create('tmp');
*
*
* @class TmpStore
* @extends Store
* @module store
* @param {Object} opts Optional.
* @param {String} [opts.tmp] a pre-existing directory to use as the `tmp` directory. When not specified, a random directory
* is created under `os.tmpDir()`
* @constructor
*/
function TmpStore(opts) {
opts = opts || {};
this.tmp = opts.tmp || makeTempDir();
this.map = {};
this.seq = 0;
this.prefix = 't' + new Date().getTime() + '-';
}
TmpStore.TYPE = 'tmp';
util.inherits(TmpStore, Store);
Store.mix(TmpStore, {
generateTmpFileName: function () {
this.seq += 1;
return this.prefix + this.seq + '.tmp';
},
set: function (key, contents) {
var tmpFile = this.generateTmpFileName();
fs.writeFileSync(tmpFile, contents, 'utf8');
this.map[key] = tmpFile;
},
get: function (key) {
var tmpFile = this.map[key];
if (!tmpFile) { throw new Error('Unable to find tmp entry for [' + tmpFile + ']'); }
return fs.readFileSync(tmpFile, 'utf8');
},
hasKey: function (key) {
return !!this.map[key];
},
keys: function () {
return Object.keys(this.map);
},
dispose: function () {
var map = this.map;
Object.keys(map).forEach(function (key) {
fs.unlinkSync(map[key]);
});
this.map = {};
}
});
module.exports = TmpStore;

View File

@ -0,0 +1,88 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
path = require('path'),
fs = require('fs'),
abbrev = require('abbrev');
function Factory(kind, dir, allowAbbreviations) {
this.kind = kind;
this.dir = dir;
this.allowAbbreviations = allowAbbreviations;
this.classMap = {};
this.abbreviations = null;
}
Factory.prototype = {
knownTypes: function () {
var keys = Object.keys(this.classMap);
keys.sort();
return keys;
},
resolve: function (abbreviatedType) {
if (!this.abbreviations) {
this.abbreviations = abbrev(this.knownTypes());
}
return this.abbreviations[abbreviatedType];
},
register: function (constructor) {
var type = constructor.TYPE;
if (!type) { throw new Error('Could not register ' + this.kind + ' constructor [no TYPE property]: ' + util.inspect(constructor)); }
this.classMap[type] = constructor;
this.abbreviations = null;
},
create: function (type, opts) {
var allowAbbrev = this.allowAbbreviations,
realType = allowAbbrev ? this.resolve(type) : type,
Cons;
Cons = realType ? this.classMap[realType] : null;
if (!Cons) { throw new Error('Invalid ' + this.kind + ' [' + type + '], allowed values are ' + this.knownTypes().join(', ')); }
return new Cons(opts);
},
loadStandard: function (dir) {
var that = this;
fs.readdirSync(dir).forEach(function (file) {
if (file !== 'index.js' && file.indexOf('.js') === file.length - 3) {
try {
that.register(require(path.resolve(dir, file)));
} catch (ex) {
console.error(ex.message);
console.error(ex.stack);
throw new Error('Could not register ' + that.kind + ' from file ' + file);
}
}
});
},
bindClassMethods: function (Cons) {
var tmpKind = this.kind.charAt(0).toUpperCase() + this.kind.substring(1), //ucfirst
allowAbbrev = this.allowAbbreviations;
Cons.mix = Factory.mix;
Cons.register = this.register.bind(this);
Cons.create = this.create.bind(this);
Cons.loadAll = this.loadStandard.bind(this, this.dir);
Cons['get' + tmpKind + 'List'] = this.knownTypes.bind(this);
if (allowAbbrev) {
Cons['resolve' + tmpKind + 'Name'] = this.resolve.bind(this);
}
}
};
Factory.mix = function (cons, proto) {
Object.keys(proto).forEach(function (key) {
cons.prototype[key] = proto[key];
});
};
module.exports = Factory;

View File

@ -0,0 +1,76 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var async = require('async'),
fileset = require('fileset'),
fs = require('fs'),
path = require('path'),
seq = 0;
function filesFor(options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
var root = options.root,
includes = options.includes,
excludes = options.excludes,
realpath = options.realpath,
relative = options.relative,
opts;
root = root || process.cwd();
includes = includes && Array.isArray(includes) ? includes : [ '**/*.js' ];
excludes = excludes && Array.isArray(excludes) ? excludes : [ '**/node_modules/**' ];
opts = { cwd: root, nodir: true };
seq += 1;
opts['x' + seq + new Date().getTime()] = true; //cache buster for minimatch cache bug
fileset(includes.join(' '), excludes.join(' '), opts, function (err, files) {
if (err) { return callback(err); }
if (relative) { return callback(err, files); }
if (!realpath) {
files = files.map(function (file) { return path.resolve(root, file); });
return callback(err, files);
}
var realPathCache = module.constructor._realpathCache || {};
async.map(files, function (file, done) {
fs.realpath(path.resolve(root, file), realPathCache, done);
}, callback);
});
}
function matcherFor(options, callback) {
if (!callback && typeof options === 'function') {
callback = options;
options = null;
}
options = options || {};
options.relative = false; //force absolute paths
options.realpath = true; //force real paths (to match Node.js module paths)
filesFor(options, function (err, files) {
var fileMap = {},
matchFn;
if (err) { return callback(err); }
files.forEach(function (file) { fileMap[file] = true; });
matchFn = function (file) { return fileMap[file]; };
matchFn.files = Object.keys(fileMap);
return callback(null, matchFn);
});
}
module.exports = {
filesFor: filesFor,
matcherFor: matcherFor
};

View File

@ -0,0 +1,154 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
util = require('util'),
fs = require('fs'),
async = require('async'),
mkdirp = require('mkdirp'),
writer = require('./writer'),
Writer = writer.Writer,
ContentWriter = writer.ContentWriter;
function extend(cons, proto) {
Object.keys(proto).forEach(function (k) {
cons.prototype[k] = proto[k];
});
}
function BufferedContentWriter() {
ContentWriter.call(this);
this.content = '';
}
util.inherits(BufferedContentWriter, ContentWriter);
extend(BufferedContentWriter, {
write: function (str) {
this.content += str;
},
getContent: function () {
return this.content;
}
});
function StreamContentWriter(stream) {
ContentWriter.call(this);
this.stream = stream;
}
util.inherits(StreamContentWriter, ContentWriter);
extend(StreamContentWriter, {
write: function (str) {
this.stream.write(str);
}
});
function SyncFileWriter() {
Writer.call(this);
}
util.inherits(SyncFileWriter, Writer);
extend(SyncFileWriter, {
writeFile: function (file, callback) {
mkdirp.sync(path.dirname(file));
var cw = new BufferedContentWriter();
callback(cw);
fs.writeFileSync(file, cw.getContent(), 'utf8');
},
done: function () {
this.emit('done'); //everything already done
}
});
function AsyncFileWriter() {
this.queue = async.queue(this.processFile.bind(this), 20);
this.openFileMap = {};
}
util.inherits(AsyncFileWriter, Writer);
extend(AsyncFileWriter, {
writeFile: function (file, callback) {
this.openFileMap[file] = true;
this.queue.push({ file: file, callback: callback });
},
processFile: function (task, cb) {
var file = task.file,
userCallback = task.callback,
that = this,
stream,
contentWriter;
mkdirp.sync(path.dirname(file));
stream = fs.createWriteStream(file);
stream.on('close', function () {
delete that.openFileMap[file];
cb();
that.checkDone();
});
stream.on('error', function (err) { that.emit('error', err); });
contentWriter = new StreamContentWriter(stream);
userCallback(contentWriter);
stream.end();
},
done: function () {
this.doneCalled = true;
this.checkDone();
},
checkDone: function () {
if (!this.doneCalled) { return; }
if (Object.keys(this.openFileMap).length === 0) {
this.emit('done');
}
}
});
/**
* a concrete writer implementation that can write files synchronously or
* asynchronously based on the constructor argument passed to it.
*
* Usage
* -----
*
* var sync = true,
* fileWriter = new require('istanbul').FileWriter(sync);
*
* fileWriter.on('done', function () { console.log('done'); });
* fileWriter.copyFile('/foo/bar.jpg', '/baz/bar.jpg');
* fileWriter.writeFile('/foo/index.html', function (contentWriter) {
* contentWriter.println('<html>');
* contentWriter.println('</html>');
* });
* fileWriter.done(); // will emit the `done` event when all files are written
*
* @class FileWriter
* @extends Writer
* @module io
* @param sync
* @constructor
*/
function FileWriter(sync) {
Writer.call(this);
var that = this;
this.delegate = sync ? new SyncFileWriter() : new AsyncFileWriter();
this.delegate.on('error', function (err) { that.emit('error', err); });
this.delegate.on('done', function () { that.emit('done'); });
}
util.inherits(FileWriter, Writer);
extend(FileWriter, {
copyFile: function (source, dest) {
mkdirp.sync(path.dirname(dest));
fs.writeFileSync(dest, fs.readFileSync(source));
},
writeFile: function (file, callback) {
this.delegate.writeFile(file, callback);
},
done: function () {
this.delegate.done();
}
});
module.exports = FileWriter;

View File

@ -0,0 +1,30 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var OPT_PREFIX = " ",
OPT_START = OPT_PREFIX.length,
TEXT_START = 14,
STOP = 80,
wrap = require('wordwrap')(TEXT_START, STOP),
paraWrap = require('wordwrap')(1, STOP);
function formatPara(text) {
return paraWrap(text);
}
function formatOption(option, helpText) {
var formattedText = wrap(helpText);
if (option.length > TEXT_START - OPT_START - 2) {
return OPT_PREFIX + option + '\n' + formattedText;
} else {
return OPT_PREFIX + option + formattedText.substring((OPT_PREFIX + option).length);
}
}
module.exports = {
formatPara: formatPara,
formatOption: formatOption
};

View File

@ -0,0 +1,12 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
module.exports.create = function (message) {
var err = new Error(message);
err.inputError = true;
return err;
};

View File

@ -0,0 +1,109 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
function InsertionText(text, consumeBlanks) {
this.text = text;
this.origLength = text.length;
this.offsets = [];
this.consumeBlanks = consumeBlanks;
this.startPos = this.findFirstNonBlank();
this.endPos = this.findLastNonBlank();
}
var WHITE_RE = /[ \f\n\r\t\v\u00A0\u2028\u2029]/;
InsertionText.prototype = {
findFirstNonBlank: function () {
var pos = -1,
text = this.text,
len = text.length,
i;
for (i = 0; i < len; i += 1) {
if (!text.charAt(i).match(WHITE_RE)) {
pos = i;
break;
}
}
return pos;
},
findLastNonBlank: function () {
var text = this.text,
len = text.length,
pos = text.length + 1,
i;
for (i = len - 1; i >= 0; i -= 1) {
if (!text.charAt(i).match(WHITE_RE)) {
pos = i;
break;
}
}
return pos;
},
originalLength: function () {
return this.origLength;
},
insertAt: function (col, str, insertBefore, consumeBlanks) {
consumeBlanks = typeof consumeBlanks === 'undefined' ? this.consumeBlanks : consumeBlanks;
col = col > this.originalLength() ? this.originalLength() : col;
col = col < 0 ? 0 : col;
if (consumeBlanks) {
if (col <= this.startPos) {
col = 0;
}
if (col > this.endPos) {
col = this.origLength;
}
}
var len = str.length,
offset = this.findOffset(col, len, insertBefore),
realPos = col + offset,
text = this.text;
this.text = text.substring(0, realPos) + str + text.substring(realPos);
return this;
},
findOffset: function (pos, len, insertBefore) {
var offsets = this.offsets,
offsetObj,
cumulativeOffset = 0,
i;
for (i = 0; i < offsets.length; i += 1) {
offsetObj = offsets[i];
if (offsetObj.pos < pos || (offsetObj.pos === pos && !insertBefore)) {
cumulativeOffset += offsetObj.len;
}
if (offsetObj.pos >= pos) {
break;
}
}
if (offsetObj && offsetObj.pos === pos) {
offsetObj.len += len;
} else {
offsets.splice(i, 0, { pos: pos, len: len });
}
return cumulativeOffset;
},
wrap: function (startPos, startText, endPos, endText, consumeBlanks) {
this.insertAt(startPos, startText, true, consumeBlanks);
this.insertAt(endPos, endText, false, consumeBlanks);
return this;
},
wrapLine: function (startText, endText) {
this.wrap(0, startText, this.originalLength(), endText);
},
toString: function () {
return this.text;
}
};
module.exports = InsertionText;

View File

@ -0,0 +1,13 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
fs = require('fs'),
pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', '..', 'package.json'), 'utf8'));
module.exports = {
NAME: pkg.name,
VERSION: pkg.version
};

View File

@ -0,0 +1,213 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var path = require('path'),
SEP = path.sep || '/',
utils = require('../object-utils');
function commonArrayPrefix(first, second) {
var len = first.length < second.length ? first.length : second.length,
i,
ret = [];
for (i = 0; i < len; i += 1) {
if (first[i] === second[i]) {
ret.push(first[i]);
} else {
break;
}
}
return ret;
}
function findCommonArrayPrefix(args) {
if (args.length === 0) {
return [];
}
var separated = args.map(function (arg) { return arg.split(SEP); }),
ret = separated.pop();
if (separated.length === 0) {
return ret.slice(0, ret.length - 1);
} else {
return separated.reduce(commonArrayPrefix, ret);
}
}
function Node(fullName, kind, metrics) {
this.name = fullName;
this.fullName = fullName;
this.kind = kind;
this.metrics = metrics || null;
this.parent = null;
this.children = [];
}
Node.prototype = {
displayShortName: function () {
return this.relativeName;
},
fullPath: function () {
return this.fullName;
},
addChild: function (child) {
this.children.push(child);
child.parent = this;
},
toJSON: function () {
return {
name: this.name,
relativeName: this.relativeName,
fullName: this.fullName,
kind: this.kind,
metrics: this.metrics,
parent: this.parent === null ? null : this.parent.name,
children: this.children.map(function (node) { return node.toJSON(); })
};
}
};
function TreeSummary(summaryMap, commonPrefix) {
this.prefix = commonPrefix;
this.convertToTree(summaryMap, commonPrefix);
}
TreeSummary.prototype = {
getNode: function (shortName) {
return this.map[shortName];
},
convertToTree: function (summaryMap, arrayPrefix) {
var nodes = [],
rootPath = arrayPrefix.join(SEP) + SEP,
root = new Node(rootPath, 'dir'),
tmp,
tmpChildren,
seen = {},
filesUnderRoot = false;
seen[rootPath] = root;
Object.keys(summaryMap).forEach(function (key) {
var metrics = summaryMap[key],
node,
parentPath,
parent;
node = new Node(key, 'file', metrics);
seen[key] = node;
nodes.push(node);
parentPath = path.dirname(key) + SEP;
if (parentPath === SEP + SEP || parentPath === '.' + SEP) {
parentPath = SEP + '__root__' + SEP;
}
parent = seen[parentPath];
if (!parent) {
parent = new Node(parentPath, 'dir');
root.addChild(parent);
seen[parentPath] = parent;
}
parent.addChild(node);
if (parent === root) { filesUnderRoot = true; }
});
if (filesUnderRoot && arrayPrefix.length > 0) {
arrayPrefix.pop(); //start at one level above
tmp = root;
tmpChildren = tmp.children;
tmp.children = [];
root = new Node(arrayPrefix.join(SEP) + SEP, 'dir');
root.addChild(tmp);
tmpChildren.forEach(function (child) {
if (child.kind === 'dir') {
root.addChild(child);
} else {
tmp.addChild(child);
}
});
}
this.fixupNodes(root, arrayPrefix.join(SEP) + SEP);
this.calculateMetrics(root);
this.root = root;
this.map = {};
this.indexAndSortTree(root, this.map);
},
fixupNodes: function (node, prefix, parent) {
var that = this;
if (node.name.indexOf(prefix) === 0) {
node.name = node.name.substring(prefix.length);
}
if (node.name.charAt(0) === SEP) {
node.name = node.name.substring(1);
}
if (parent) {
if (parent.name !== '__root__' + SEP) {
node.relativeName = node.name.substring(parent.name.length);
} else {
node.relativeName = node.name;
}
} else {
node.relativeName = node.name.substring(prefix.length);
}
node.children.forEach(function (child) {
that.fixupNodes(child, prefix, node);
});
},
calculateMetrics: function (entry) {
var that = this,
fileChildren;
if (entry.kind !== 'dir') {return; }
entry.children.forEach(function (child) {
that.calculateMetrics(child);
});
entry.metrics = utils.mergeSummaryObjects.apply(
null,
entry.children.map(function (child) { return child.metrics; })
);
// calclulate "java-style" package metrics where there is no hierarchy
// across packages
fileChildren = entry.children.filter(function (n) { return n.kind !== 'dir'; });
if (fileChildren.length > 0) {
entry.packageMetrics = utils.mergeSummaryObjects.apply(
null,
fileChildren.map(function (child) { return child.metrics; })
);
} else {
entry.packageMetrics = null;
}
},
indexAndSortTree: function (node, map) {
var that = this;
map[node.name] = node;
node.children.sort(function (a, b) {
a = a.relativeName;
b = b.relativeName;
return a < b ? -1 : a > b ? 1 : 0;
});
node.children.forEach(function (child) {
that.indexAndSortTree(child, map);
});
},
toJSON: function () {
return {
prefix: this.prefix,
root: this.root.toJSON()
};
}
};
function TreeSummarizer() {
this.summaryMap = {};
}
TreeSummarizer.prototype = {
addFileCoverageSummary: function (filePath, metrics) {
this.summaryMap[filePath] = metrics;
},
getTreeSummary: function () {
var commonArrayPrefix = findCommonArrayPrefix(Object.keys(this.summaryMap));
return new TreeSummary(this.summaryMap, commonArrayPrefix);
}
};
module.exports = TreeSummarizer;

View File

@ -0,0 +1,92 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
var util = require('util'),
EventEmitter = require('events').EventEmitter;
function extend(cons, proto) {
Object.keys(proto).forEach(function (k) {
cons.prototype[k] = proto[k];
});
}
/**
* abstract interfaces for writing content
* @class ContentWriter
* @module io
* @main io
* @constructor
*/
//abstract interface for writing content
function ContentWriter() {
}
ContentWriter.prototype = {
/**
* writes the specified string as-is
* @method write
* @param {String} str the string to write
*/
write: /* istanbul ignore next: abstract method */ function (/* str */) {
throw new Error('write: must be overridden');
},
/**
* writes the specified string with a newline at the end
* @method println
* @param {String} str the string to write
*/
println: function (str) { this.write(str + '\n'); }
};
/**
* abstract interface for writing files and assets. The caller is expected to
* call `done` on the writer after it has finished writing all the required
* files. The writer is an event-emitter that emits a `done` event when `done`
* is called on it *and* all files have successfully been written.
*
* @class Writer
* @constructor
*/
function Writer() {
EventEmitter.call(this);
}
util.inherits(Writer, EventEmitter);
extend(Writer, {
/**
* allows writing content to a file using a callback that is passed a content writer
* @method writeFile
* @param {String} file the name of the file to write
* @param {Function} callback the callback that is called as `callback(contentWriter)`
*/
writeFile: /* istanbul ignore next: abstract method */ function (/* file, callback */) {
throw new Error('writeFile: must be overridden');
},
/**
* copies a file from source to destination
* @method copyFile
* @param {String} source the file to copy, found on the file system
* @param {String} dest the destination path
*/
copyFile: /* istanbul ignore next: abstract method */ function (/* source, dest */) {
throw new Error('copyFile: must be overridden');
},
/**
* marker method to indicate that the caller is done with this writer object
* The writer is expected to emit a `done` event only after this method is called
* and it is truly done.
* @method done
*/
done: /* istanbul ignore next: abstract method */ function () {
throw new Error('done: must be overridden');
}
});
module.exports = {
Writer: Writer,
ContentWriter: ContentWriter
};

View File

@ -0,0 +1,49 @@
/*
Copyright (c) 2012, Yahoo! Inc. All rights reserved.
Copyrights licensed under the New BSD License. See the accompanying LICENSE file for terms.
*/
//EXPERIMENTAL code: do not rely on this in anyway until the docs say it is allowed
var path = require('path'),
yuiRegexp = /yui-nodejs\.js$/;
module.exports = function (matchFn, transformFn, verbose) {
return function (file) {
if (!file.match(yuiRegexp)) {
return;
}
var YMain = require(file),
YUI,
loaderFn,
origGet;
if (YMain.YUI) {
YUI = YMain.YUI;
loaderFn = YUI.Env && YUI.Env.mods && YUI.Env.mods['loader-base'] ? YUI.Env.mods['loader-base'].fn : null;
if (!loaderFn) { return; }
if (verbose) { console.log('Applying YUI load post-hook'); }
YUI.Env.mods['loader-base'].fn = function (Y) {
loaderFn.call(null, Y);
origGet = Y.Get._exec;
Y.Get._exec = function (data, url, cb) {
if (matchFn(url) || matchFn(path.resolve(url))) { //allow for relative paths as well
if (verbose) {
console.log('Transforming [' + url + ']');
}
try {
data = transformFn(data, url);
} catch (ex) {
console.error('Error transforming: ' + url + ' return original code');
console.error(ex.message || ex);
if (ex.stack) { console.error(ex.stack); }
}
}
return origGet.call(Y, data, url, cb);
};
return Y;
};
}
};
};