Skip to content

Commit

Permalink
Initial source map implementation
Browse files Browse the repository at this point in the history
* Capture source maps when running tests
* Implement include-all-sources correctly such that it works with custom module loaders
  * Never try to instrument files directly, always `require` them
  * Ensure that the additional requires have no side-effects in affecting the coverage
    already collected
  * Ensure that missing files are reported with 0 coverage
  • Loading branch information
gotwarlost committed Nov 24, 2015
1 parent 0e98ed7 commit 6c44390
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 44 deletions.
101 changes: 72 additions & 29 deletions lib/run-cover.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
var path = require('path'),
fs = require('fs'),
mkdirp = require('mkdirp'),
clone = require('clone'),
matcherFor = require('./file-matcher').matcherFor,
libInstrument = require('istanbul-lib-instrument'),
libCoverage = require('istanbul-lib-coverage'),
libSourceMaps = require('istanbul-lib-source-maps'),
hook = require('istanbul-lib-hook'),
Reporter = require('./reporter');

Expand All @@ -24,22 +26,34 @@ function getCoverFunctions(config, includes, callback) {
excludes = config.instrumentation.excludes(true),
coverageVar = '$$cov_' + new Date().getTime() + '$$',
instOpts = config.instrumentation.getInstrumenterOpts(),
sourceMapStore = libSourceMaps.createSourceMapStore({}),
instrumenter,
transformer,
reportInitFn,
hookFn,
unhookFn,
coverageFinderFn,
coverageSetterFn,
beforeReportFn,
exitFn;

instOpts.coverageVariable = coverageVar;
instOpts.sourceMapUrlCallback = function (file, url) {
sourceMapStore.registerURL(file, url);
};
instrumenter = libInstrument.createInstrumenter(instOpts);
transformer = instrumenter.instrumentSync.bind(instrumenter);
transformer = function (code, file) {
return instrumenter.instrumentSync(code, file);
};

coverageFinderFn = function () {
return global[coverageVar];
};

coverageSetterFn = function (cov) {
global[coverageVar] = cov;
};

reportInitFn = function () {
// set up reporter
mkdirp.sync(reportingDir); //ensure we fail early if we cannot do this
Expand Down Expand Up @@ -67,82 +81,111 @@ function getCoverFunctions(config, includes, callback) {
};

//initialize the global variable
global[coverageVar] = {};

coverageSetterFn({});
reportInitFn();
// internal to istanbul
/* istanbul ignore else */
if (config['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);
};

unhookFn = function (matchFn) {
hook.unhookRequire();
hook.unhookRunInThisContext();
hook.unloadRequireCache(matchFn);
};

beforeReportFn = function (matchFn, cov) {
var pidExt = includePid ? ('-' + process.pid) : '',
file = path.resolve(reportingDir, 'coverage' + pidExt + '.raw.json');
file = path.resolve(reportingDir, 'coverage' + pidExt + '.raw.json'),
missingFiles,
finalCoverage = cov;

if (config.instrumentation.includeAllSources()) {
// ensure we don't increase the coverage of existing tested files
// in any way when we require untested files
if (config.verbose) {
console.error("Including all sources not require'd by tests");
}
missingFiles = [];
// 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]) {
missingFiles.push(file);
}
});
if (missingFiles.length > 0) {
finalCoverage = clone(cov);
missingFiles.forEach(function (file) {
try {
transformer(fs.readFileSync(file, 'utf-8'), file);
cov[file] = instrumenter.lastFileCoverage();
require(file);
} catch (ex) {
console.error('Unable to post-instrument: ' + file);
}
}
});
});
missingFiles.forEach(function (file) {
var fc;
if (cov[file]) {
fc = libCoverage.createFileCoverage(cov[file]);
fc.resetHits();
finalCoverage[file] = fc.toJSON();
}
});
}
}
if (config.verbose) {
console.error('=============================================================================');
console.error('Writing coverage object [' + file + ']');
console.error('Writing coverage reports at [' + reportingDir + ']');
console.error('=============================================================================');
if (Object.keys(finalCoverage).length >0) {
if (config.verbose) {
console.error('=============================================================================');
console.error('Writing coverage object [' + file + ']');
console.error('Writing coverage reports at [' + reportingDir + ']');
console.error('=============================================================================');
}
fs.writeFileSync(file, JSON.stringify(finalCoverage), 'utf8');
}
fs.writeFileSync(file, JSON.stringify(cov), 'utf8');
return finalCoverage;
};

exitFn = function (matchFn, reporterOpts) {
var cov,
coverageMap;
coverageMap,
transformed;

cov = coverageFinderFn() || {};
cov = beforeReportFn(matchFn, cov);
coverageSetterFn(cov);

cov = coverageFinderFn();
if (!(cov && typeof cov === 'object') || Object.keys(cov).length === 0) {
console.error('No coverage information was collected, exit without writing coverage information');
return;
}

beforeReportFn(matchFn, cov);
coverageMap = libCoverage.createCoverageMap(cov);
reporter.write(coverageMap, reporterOpts);
transformed = sourceMapStore.transformCoverage(coverageMap);
reporterOpts.sourceFinder = transformed.sourceFinder;
reporter.write(transformed.map, reporterOpts);
};

excludes.push(path.relative(process.cwd(), path.join(reportingDir, '**', '*')));
includes = includes || config.instrumentation.extensions().map(function (ext) {
return '**/*' + ext;
});
return '**/*' + ext;
});
var matchConfig = {
root: config.instrumentation.root() || /* istanbul ignore next: untestable */ process.cwd(),
includes: includes,
excludes: excludes
};
matcherFor(matchConfig, function (err, matchFn) {
/* istanbul ignore if: untestable */
if (err) { return callback(err); }
if (err) {
return callback(err);
}
return callback(null, {
coverageVar: coverageVar,
coverageFn: coverageFinderFn,
hookFn: hookFn.bind(null, matchFn),
exitFn: exitFn.bind(null, matchFn, {})
exitFn: exitFn.bind(null, matchFn, {}), // XXX: reporter opts
unhookFn: unhookFn.bind(null, matchFn)
});
});
}
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,15 @@
"rimraf": "^2.4.3"
},
"dependencies": {
"async": "1.x",
"clone": "^1.0.2",
"fileset": "0.2.x",
"istanbul-lib-coverage": "^1.0.0-alpha",
"istanbul-lib-hook": "^1.0.0-alpha",
"istanbul-lib-instrument": "^1.0.0-alpha",
"istanbul-lib-report": "^1.0.0-alpha",
"istanbul-lib-hook": "^1.0.0-alpha",
"istanbul-lib-source-maps": "^1.0.0-alpha",
"istanbul-reports": "^1.0.0-alpha",
"async": "1.x",
"fileset": "0.2.x",
"js-yaml": "3.x",
"mkdirp": "0.5.x",
"once": "1.x"
Expand Down
2 changes: 1 addition & 1 deletion test/run-check-coverage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ describe('run instrument', function () {
dir: outputDir
}
}, overrides);
cfg['self-test'] = true;
return cfg;
}

Expand All @@ -40,6 +39,7 @@ describe('run instrument', function () {
hookFn();
require('./sample-code/test/foo.test.js');
exitFn();
data.unhookFn();
cb();
});
});
Expand Down
38 changes: 27 additions & 11 deletions test/run-cover.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,20 @@ var assert = require('chai').assert,
codeRoot = path.resolve(__dirname, 'sample-code'),
outputDir = path.resolve(__dirname, 'coverage'),
configuration = require('../lib/config'),
cover = require('../lib/run-cover');
cover = require('../lib/run-cover'),
unhookFn;

describe('run cover', function () {

beforeEach(function () {
unhookFn = null;
mkdirp.sync(outputDir);
});
afterEach(function () {
rimraf.sync(outputDir);
if (unhookFn) {
unhookFn();
}
});

function getConfig(overrides) {
Expand All @@ -28,26 +34,27 @@ describe('run cover', function () {
dir: outputDir
}
}, overrides);
cfg['self-test'] = true;
return cfg;
}

it('hooks require and provides coverage', function (cb) {
var config = getConfig({ verbose: true, instrumentation: { 'include-all-sources': false }});
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
var v = data.coverageVar,
var fn = data.coverageFn,
hookFn = data.hookFn,
exitFn = data.exitFn,
coverageMap,
coverage,
otherMap;
assert.ok(v);
unhookFn = data.unhookFn;
assert.isFunction(fn);
assert.isFunction(unhookFn);
assert.isFunction(hookFn);
assert.isFunction(exitFn);
hookFn();
require('./sample-code/foo');
coverageMap = global[v];
coverageMap = fn();
assert.ok(coverageMap);
coverage = coverageMap[path.resolve(codeRoot, 'foo.js')];
assert.ok(coverage);
Expand All @@ -71,12 +78,13 @@ describe('run cover', function () {
});
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
var v = data.coverageVar,
var fn = data.coverageFn,
hookFn = data.hookFn,
coverageMap;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/context');
coverageMap = global[v];
coverageMap = fn();
assert.ok(coverageMap);
assert.ok(coverageMap[path.resolve(codeRoot, 'context.js')]);
assert.ok(coverageMap[path.resolve(codeRoot, 'foo.js')]);
Expand All @@ -94,14 +102,15 @@ describe('run cover', function () {
});
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
var v = data.coverageVar,
var fn = data.coverageFn,
hookFn = data.hookFn,
exitFn = data.exitFn,
coverageMap;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
coverageMap = global[v];
coverageMap = fn();
assert.ok(coverageMap);
assert.ok(coverageMap[path.resolve(codeRoot, 'context.js')]);
assert.ok(coverageMap[path.resolve(codeRoot, 'foo.js')]);
Expand All @@ -116,6 +125,7 @@ describe('run cover', function () {
assert.ok(!err);
var hookFn = data.hookFn,
exitFn = data.exitFn;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
Expand All @@ -131,14 +141,15 @@ describe('run cover', function () {
});
cover.getCoverFunctions(config, [ '**/foo.js' ], function (err, data) {
assert.ok(!err);
var v = data.coverageVar,
var fn = data.coverageFn,
hookFn = data.hookFn,
exitFn = data.exitFn,
coverageMap;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/context');
exitFn();
coverageMap = global[v];
coverageMap = fn();
assert.ok(coverageMap);
assert.ok(!coverageMap[path.resolve(codeRoot, 'context.js')]);
assert.ok(coverageMap[path.resolve(codeRoot, 'foo.js')]);
Expand All @@ -151,6 +162,7 @@ describe('run cover', function () {
var config = getConfig();
cover.getCoverFunctions(config, function(err, data) {
assert.ok(!err);
unhookFn = data.unhookFn;
assert.doesNotThrow(data.exitFn);
cb();
});
Expand All @@ -174,6 +186,7 @@ describe('run cover', function () {
assert.ok(!err);
var hookFn = data.hookFn,
exitFn = data.exitFn;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
Expand All @@ -188,6 +201,7 @@ describe('run cover', function () {
assert.ok(!err);
var hookFn = data.hookFn,
exitFn = data.exitFn;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
Expand All @@ -202,6 +216,7 @@ describe('run cover', function () {
assert.ok(!err);
var hookFn = data.hookFn,
exitFn = data.exitFn;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
Expand All @@ -216,6 +231,7 @@ describe('run cover', function () {
assert.ok(!err);
var hookFn = data.hookFn,
exitFn = data.exitFn;
unhookFn = data.unhookFn;
hookFn();
require('./sample-code/foo');
exitFn();
Expand Down

0 comments on commit 6c44390

Please sign in to comment.