Permalink
Browse files

Add istanbul code coverage tooling

  • Loading branch information...
1 parent 6fab36b commit ce6e495299caa165e429705f15c23fc88693fc2c @petkaantonov committed Jan 22, 2015
Showing with 215 additions and 13 deletions.
  1. +1 −0 .gitignore
  2. +49 −0 .istanbul.yml
  3. +2 −0 README.md
  4. +2 −0 package.json
  5. +2 −1 src/captured_trace.js
  6. +24 −7 tools/build.js
  7. +7 −1 tools/mocha_runner.js
  8. +125 −3 tools/test.js
  9. +3 −1 tools/utils.js
View
@@ -2,3 +2,4 @@ node_modules
test/browser/bundle.js
js/*
zalgo.js
+coverage/*
View
@@ -0,0 +1,49 @@
+verbose: false
+instrumentation:
+ root: .
+ 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
+reporting:
+ print: summary
+ reports:
+ - lcov
+ dir: ./coverage
+ watermarks:
+ statements: [50, 80]
+ lines: [50, 80]
+ functions: [50, 80]
+ branches: [50, 80]
+ report-config:
+ clover: {file: clover.xml}
+ cobertura: {file: cobertura-coverage.xml}
+ json: {file: coverage-final.json}
+ json-summary: {file: coverage-summary.json}
+ lcovonly: {file: lcov.info}
+ teamcity: {file: null}
+ text: {file: null, maxCols: 0}
+ text-summary: {file: null}
+hooks:
+ hook-run-in-context: false
+ post-require-hook: null
+ handle-sigint: false
+check:
+ global:
+ statements: 0
+ lines: 0
+ branches: 0
+ functions: 0
+ excludes: []
+ each:
+ statements: 0
+ lines: 0
+ branches: 0
+ functions: 0
+ excludes: []
View
@@ -3,6 +3,7 @@
title="Promises/A+ 1.1 compliant" align="right" />
</a>
[![Build Status](https://travis-ci.org/petkaantonov/bluebird.svg?branch=master)](https://travis-ci.org/petkaantonov/bluebird)
+<!-- coverage -->[![coverage-88%](http://img.shields.io/badge/coverage-88%-green.svg)](http://petkaantonov.github.io/bluebird/coverage/debug/index.html)<!-- /coverage -->
#Introduction
@@ -476,6 +477,7 @@ Keep the test tab active because some tests are timing-sensitive and will fail i
The value of boolean flags is determined by presence, if you want to pass false value for a boolean flag, use the `no-`-prefix e.g. `--no-browser`.
- `--run=String`. Which tests to run (or compile when testing in browser). Default `"all"`
+ - `--cover=String`. Create code coverage using the String as istanbul reporter. Coverage is created in the ./coverage folder. No coverage is created by default, default reporter is `"html"` (use `--cover` to use default reporter).
- `--browser` - Whether to compile tests for browsers. Default `false`.
- `--port=Number` - Whe port where local server is hosted when testing in browser. Default `9999`
- `--execute-browser-tests` - Whether to execute the compiled tests for browser when using `--browser`. Default `true`.
View
@@ -20,6 +20,7 @@
"scripts": {
"lint": "node scripts/jshint.js",
"test": "node tools/test.js",
+ "istanbul": "istanbul",
"prepublish": "node tools/build.js --no-debug --main --zalgo --browser --minify"
},
"homepage": "https://github.com/petkaantonov/bluebird",
@@ -44,6 +45,7 @@
"cross-spawn": "^0.2.3",
"glob": "^4.3.2",
"grunt-saucelabs": "~8.4.1",
+ "istanbul": "^0.3.5",
"jshint": "^2.5.11",
"jshint-stylish": "~0.2.0",
"mkdirp": "~0.5.0",
@@ -3,7 +3,8 @@ module.exports = function() {
var async = require("./async.js");
var ASSERT = require("./assert.js");
var inherits = require("./util.js").inherits;
-var bluebirdFramePattern = /[\\\/]bluebird[\\\/]js[\\\/](main|debug|zalgo)/;
+var bluebirdFramePattern =
+ /[\\\/]bluebird[\\\/]js[\\\/](main|debug|zalgo|instrumented)/;
var stackFramePattern = null;
var formatStack = null;
View
@@ -262,16 +262,31 @@ function buildBrowser(sources, dir, tmpDir, depsRequireCode, minify, npmPackage,
});
}
+var root = process.cwd();
+// Since rm -rf is called, better be sure...
+if (path.basename(root).toLowerCase() !== "bluebird") {
+ throw new Error("cwd must be se to bluebird project root. Cwd is currently\n\n" +
+ " " + process.cwd() + "\n");
+}
+var dirs = {
+ main: path.join(root, "js", "main"),
+ debug: path.join(root, "js", "debug"),
+ zalgo: path.join(root, "js", "zalgoDir"),
+ browser: path.join(root, "js", "browser"),
+ browserTmp: path.join(root, "js", "tmp"),
+ instrumented: path.join(root, "js", "instrumented"),
+ coverage: path.join(root, "coverage")
+};
+
function build(options) {
var npmPackage = fs.readFileAsync("./package.json").then(JSON.parse);
var sourceFileNames = getSourcePaths(options.features);
var license = utils.getLicense();
- var root = process.cwd();
- var mainDir = ensureDirectory(path.join(root, "js", "main"), options.main);
- var debugDir = ensureDirectory(path.join(root, "js", "debug"), options.debug);
- var zalgoDir = ensureDirectory(path.join(root, "js", "zalgo"), options.zalgo);
- var browserDir = ensureDirectory(path.join(root, "js", "browser"), options.browser);
- var browserTmpDir = ensureDirectory(path.join(root, "js", "tmp"), options.browser);
+ var mainDir = ensureDirectory(dirs.main, options.main);
+ var debugDir = ensureDirectory(dirs.debug, options.debug);
+ var zalgoDir = ensureDirectory(dirs.zalgo, options.zalgo);
+ var browserDir = ensureDirectory(dirs.browser, options.browser);
+ var browserTmpDir = ensureDirectory(dirs.browserTmp, options.browser);
return license.then(function(license) {
return sourceFileNames.map(function(sourceFileName) {
return jobRunner.run(function() {
@@ -309,12 +324,14 @@ function build(options) {
zalgo = buildZalgo(results, depsRequireCode, zalgoDir);
if (options.browser)
browser = buildBrowser(results, browserDir, browserTmpDir, depsRequireCode, options.minify, npmPackage, license);
-
+
return Promise.all([main, debug, zalgo, browser]);
});
}
module.exports = build;
+module.exports.ensureDirectory = ensureDirectory;
+module.exports.dirs = dirs;
if (require.main === module) {
@@ -65,7 +65,10 @@ module.exports = function mochaRun(progress) {
global.clearInterval = clearInterval;
}
var failures = [];
- global.adapter = require("./js/debug/bluebird.js");
+ global.adapter = cover
+ ? require("./js/instrumented/bluebird.js")
+ : require("./js/debug/bluebird.js");
+ global.Promise = adapter;
return Promise.each(testGroup, function(test, index, length) {
var mocha = new Mocha({
reporter: "spec",
@@ -111,5 +114,8 @@ module.exports = function mochaRun(progress) {
}
throw error;
}
+ if (cover) {
+ return __coverage__;
+ }
});
};
View
@@ -2,12 +2,15 @@ var assert = require("assert");
assert.equal(require.main, module);
var Promise = require("bluebird");
var build = require("./build.js");
-var tableLogger = require("./utils.js").tableLogger;
+var utils = require("./utils.js");
+var tableLogger = utils.tableLogger;
var argv = require("optimist").argv;
var glob = Promise.promisify(require("glob"));
var path = require("path");
+var mkdirp = Promise.promisify(require("mkdirp"));
var jobRunner = require("./job-runner/job-runner.js");
var mochaRunner = require("./mocha_runner.js");
+var fs = Promise.promisifyAll(require("fs"));
jobRunner.setVerbose(0);
// Random slowness after tests complete
function getTests(options) {
@@ -44,6 +47,54 @@ function getTests(options) {
});
}
+function getColorForCoverage(coveragePct) {
+ var colorThresholds = {
+ 95: "brightgreen",
+ 85: "green",
+ 80: "yellowgreen",
+ 70: "yellow",
+ 60: "red"
+ };
+ var values = Object.keys(colorThresholds).map(Number).sort(function(a, b) {
+ return b - a;
+ });
+ for (var i = 0; i < values.length; ++i) {
+ if (coveragePct >= values[i]) return colorThresholds[values[i].toString()];
+ }
+ return colorThresholds[values[values.length - 1].toString()];
+}
+
+function getCoverage() {
+ return utils.run("npm", ["run", "istanbul", "--", "report", "text-summary"]).then(function(result) {
+ var stdout = result.stdout;
+ var pctPattern = /(\d+\.\d+)%/g;
+ var matches = stdout.match(pctPattern);
+ var sum = matches.map(function(pct) {
+ return parseFloat(pct.replace(/[^0-9.]/g, ""))
+ }).reduce(function(a, b) {
+ return a + b;
+ }, 0);
+ var average = Math.round(sum / matches.length);
+ return average;
+ });
+}
+
+function generateCoverageBadge(coverage) {
+ var text = "coverage-" + coverage + "%";
+ var color = getColorForCoverage(coverage);
+ var imgSrc = "http://img.shields.io/badge/" + text + "-" + color + ".svg";
+ var link = "http://petkaantonov.github.io/bluebird/coverage/debug/index.html";
+ var markdown = "[!["+text+"]("+imgSrc+")]("+link+")";
+ return markdown;
+}
+
+function writeCoverageFile(coverage, groupNumber) {
+ var dir = build.dirs.coverage;
+ var fileName = path.join(dir, "coverage-group" + groupNumber + ".json");
+ var json = JSON.stringify(coverage);
+ return fs.writeFileAsync(fileName, json, "utf8").thenReturn(fileName);
+}
+
function needsFreshProcess(testName) {
return /domain|schedule/.test(testName);
}
@@ -56,7 +107,8 @@ function runTestGroup(testGroup, options, progress) {
context: {
testGroup: testGroup,
singleTest: options.singleTest,
- fakeTimers: options.fakeTimers
+ fakeTimers: options.fakeTimers,
+ cover: options.cover
}
});
}
@@ -97,6 +149,7 @@ var options = {
return false;
}
})(),
+ cover: false,
testName: testName,
singleTest: false,
saucelabs: !!argv.saucelabs,
@@ -109,13 +162,58 @@ var options = {
? argv["fake-timers"] : true,
jsHint: typeof argv["js-hint"] === "boolean" ? argv["js-hint"] : true
};
+options.cover = !!argv["cover"] && !options.testBrowser;
+if (options.cover && typeof argv["cover"] === "string") {
+ options.coverFormat = argv["cover"];
+} else {
+ options.coverFormat = "html";
+}
var jsHint = options.jsHint ? require("./jshint.js")() : Promise.resolve();
var tests = getTests(options);
var buildResult = build({
debug: true
});
+if (options.cover) {
+ var exclusions = ["assert.js", "captured_trace.js"];
+ var coverageInstrumentedRoot = build.ensureDirectory(build.dirs.instrumented,options.cover);
+ var coverageReportsRoot = build.ensureDirectory(build.dirs.coverage, true);
+ buildResult = Promise.join(coverageInstrumentedRoot, buildResult, coverageReportsRoot, function() {
+ return utils.run("npm", ["-v"]).then(function(result) {
+ var version = result.stdout.split(".").map(Number);
+ if (version[0] < 2) {
+ throw new Error("Npm version 2.x.x required, current version is " + result.stdout);
+ }
+ });
+ }).tap(function() {
+ var copyExclusions = Promise.map(exclusions, function(exclusion) {
+ var fromPath = path.join(build.dirs.debug, exclusion);
+ var toPath = path.join(build.dirs.instrumented, exclusion);
+ return fs.readFileAsync(fromPath, "utf8").then(function(contents) {
+ return fs.writeFileAsync(toPath, contents, "utf8");
+ });
+ });
+ var args = [
+ "run",
+ "istanbul",
+ "--",
+ "instrument",
+ "--output",
+ build.dirs.instrumented,
+ "--no-compact",
+ "--preserve-comments",
+ "--embed-source"
+ ];
+ exclusions.forEach(function(x) {
+ args.push("-x", x);
+ });
+ args.push(build.dirs.debug);
+ var istanbul = utils.run("npm", args, null, true);
+ return Promise.all([istanbul, copyExclusions]);
+ });
+}
+
var testResults = Promise.join(tests, buildResult, function(tests) {
var singleTest = tests.length === 1;
options.singleTest = singleTest;
@@ -128,16 +226,40 @@ var testResults = Promise.join(tests, buildResult, function(tests) {
process.stdout.cursorTo(0, 0);
process.stdout.clearScreenDown();
tableLogger.addTests(tests);
- return Promise.map(combineTests(tests), function(testGroup) {
+ return Promise.map(combineTests(tests), function(testGroup, index) {
return runTestGroup(testGroup, options, function(test) {
if (test.failed) {
tableLogger.testFail(test);
} else {
tableLogger.testSuccess(test);
}
+ }).then(function(maybeCoverage) {
+ if (options.cover) {
+ return writeCoverageFile(maybeCoverage.result, index + 1);
+ }
})
}).then(function() {
+ var p = Promise.resolve();
+ if (options.cover) {
+ var coverage = getCoverage();
+ if (process.execPath.indexOf("iojs") >= 0 && testName === "all") {
+ var badge = coverage.then(generateCoverageBadge);
+ var readme = fs.readFileAsync("README.md", "utf8");
+ p = Promise.join(readme, badge, function(readmeContents, badgeMarkDown) {
+ var coveragePattern = /<!-- coverage -->.*?<!-- \/coverage -->/
+ badgeMarkDown = "<!-- coverage -->" + badgeMarkDown+ "<!-- /coverage -->";
+ readmeContents = readmeContents.replace(coveragePattern, badgeMarkDown);
+ return fs.writeFileAsync("README.md", readmeContents, "utf8");
+ });
+ }
+ p = p.then(function() {
+ return utils.run("npm", ["run", "istanbul", "--", "report", options.coverFormat], null, true);
+ }).return(coverage).then(function(coverage) {
+ console.log("Total coverage " + coverage + "%");
+ });
+ }
console.log("All tests passed");
+ return p;
});
}
});
View
@@ -35,7 +35,7 @@ function getLicense() {
}
-function run(cmd, args, dir, pipe) {
+function run(cmd, args, dir, log) {
return new Promise(function(resolve, reject) {
function makeResult(errorMessage) {
var ret = errorMessage ? new Error(errorMessage) : {};
@@ -49,9 +49,11 @@ function run(cmd, args, dir, pipe) {
var c = spawn(cmd, args, {stdin: ["ignore", "ignore", "ignore"], cwd: dir || process.cwd()});
c.stdout.on("data", function(data) {
+ if (log) process.stdout.write(data.toString());
out += data;
});
c.stderr.on("data", function(data) {
+ if (log) process.stderr.write(data.toString());
err += data;
});

0 comments on commit ce6e495

Please sign in to comment.