Sunday, September 17, 2017

Adding 0% coverage in Istanbul code coverage report

In this post I am setting up a NodeJS project, it has a test project  to run automation tests, i also takes care of code coverage as well.  The project uses following frameworks:

  • NodeJS (https://github.com/nodejs)
  • Istanbul middleware (https://github.com/gotwarlost/istanbul-middleware)
  • ExpressJS  (https://expressjs.com/)
  • Protractor (https://github.com/angular/protractor)
In first step I am creating a NodeJS project:

  • Install NodeJs 
  • Go to project folder for example C:\\\
  • Open command prompt/terminal in the above location, run the following commands:   
           npm init
           npm install express

  • In the same folder create a sub-folder named public, so in the above project we are creating a static server to serve static files like html and images. In the public folder create a HTML file named index.html. Following are the contents of index.html:





  • In the above public folder you may create a folder named src, under src folder you may have multiple sub-folders, each sub-folder may act as module, and each module may have single or multiple javascript files.
  • Before running any testcase to test the above project all the code in src folder need to be instrumented.
  • For instrumentation istanbul-instrumentor cli utility can be used. Keep the instrumented code in public-coverage folder.
  • If you notice the above index.html one of the instrumented Javascript file is used, so when any automation test case launch this html file the coverage for the particular file will be collected, however other instrumented Javascript file's coverage won't be collected. However those non-loaded file's coverage should be actually 0%.
  Let's create the test project which will run autonomous tests on the above project, basically it will load the index.html in browser, it will collect the coverage data and pushes it to istanbul coverage collector. 
           
  

  • In the above code for staticdir's value you have to give the path where your instrumented code resides.
  • In node_modules/archiver/lib/core.js you have to modify the code to do coverage calculation for non-loaded files during tests. Following is the file's modified code.
/**
* node-archiver
*
* Copyright (c) 2012-2014 Chris Talkington, contributors.
* Licensed under the MIT license.
* https://github.com/archiverjs/node-archiver/blob/master/LICENSE-MIT
*/
var fs = require('fs');
var inherits = require('util').inherits;
var Transform = require('readable-stream').Transform;
var async = require('async');
var util = require('./util');
var Archiver = module.exports = function(options) {
if (!(this instanceof Archiver)) {
return new Archiver(options);
}
options = this.options = util.defaults(options, {
highWaterMark: 1024 * 1024,
statConcurrency: 4
});
Transform.call(this, options);
this._entries = [];
this._format = false;
this._module = false;
this._pending = 0;
this._pointer = 0;
this._queue = async.queue(this._onQueueTask.bind(this), 1);
this._queue.drain = this._onQueueDrain.bind(this);
this._statQueue = async.queue(this._onStatQueueTask.bind(this), options.statConcurrency);
this._state = {
aborted: false,
finalize: false,
finalizing: false,
finalized: false,
modulePiped: false
};
};
inherits(Archiver, Transform);
Archiver.prototype._abort = function() {
this._state.aborted = true;
this._queue.kill();
this._statQueue.kill();
if (this._queue.idle()) {
this._shutdown();
}
};
Archiver.prototype._append = function(filepath, data) {
data = data || {};
var task = {
source: null,
filepath: filepath
};
if (!data.name) {
data.name = filepath;
}
data.sourcePath = filepath;
task.data = data;
if (data.stats && data.stats instanceof fs.Stats) {
task = this._updateQueueTaskWithStats(task, data.stats);
this._queue.push(task);
} else {
this._statQueue.push(task);
}
};
Archiver.prototype._finalize = function() {
if (this._state.finalizing || this._state.finalized || this._state.aborted) {
return;
}
this._state.finalizing = true;
this._moduleFinalize();
this._state.finalizing = false;
this._state.finalized = true;
};
Archiver.prototype._maybeFinalize = function() {
if (this._state.finalizing || this._state.finalized || this._state.aborted) {
return false;
}
if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
this._finalize();
return true;
}
return false;
};
Archiver.prototype._moduleAppend = function(source, data, callback) {
if (this._state.aborted) {
callback();
return;
}
this._module.append(source, data, function(err) {
this._task = null;
if (this._state.aborted) {
this._shutdown();
return;
}
if (err) {
this.emit('error', err);
setImmediate(callback);
return;
}
this.emit('entry', data);
this._entries.push(data);
setImmediate(callback);
}.bind(this));
};
Archiver.prototype._moduleFinalize = function() {
if (typeof this._module.finalize === 'function') {
this._module.finalize();
} else if (typeof this._module.end === 'function') {
this._module.end();
} else {
this.emit('error', new Error('module: no suitable finalize/end method found'));
return;
}
};
Archiver.prototype._modulePipe = function() {
this._module.on('error', this._onModuleError.bind(this));
this._module.pipe(this);
this._state.modulePiped = true;
};
Archiver.prototype._moduleSupports = function(key) {
if (!this._module.supports || !this._module.supports[key]) {
return false;
}
return this._module.supports[key];
};
Archiver.prototype._moduleUnpipe = function() {
this._module.unpipe(this);
this._state.modulePiped = false;
};
Archiver.prototype._normalizeEntryData = function(data, stats) {
data = util.defaults(data, {
type: 'file',
name: null,
date: null,
mode: null,
sourcePath: null,
stats: false
});
if (stats && data.stats === false) {
data.stats = stats;
}
var isDir = data.type === 'directory';
if (data.name) {
data.name = util.sanitizePath(data.name);
if (data.name.slice(-1) === '/') {
isDir = true;
data.type = 'directory';
} else if (isDir) {
data.name += '/';
}
}
if (typeof data.mode === 'number') {
data.mode &= 0777;
} else if (data.stats && data.mode === null) {
data.mode = data.stats.mode & 0777;
} else if (data.mode === null) {
data.mode = isDir ? 0755 : 0644;
}
if (data.stats && data.date === null) {
data.date = data.stats.mtime;
} else {
data.date = util.dateify(data.date);
}
return data;
};
Archiver.prototype._onModuleError = function(err) {
this.emit('error', err);
};
Archiver.prototype._onQueueDrain = function() {
if (this._state.finalizing || this._state.finalized || this._state.aborted) {
return;
}
if (this._state.finalize && this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
this._finalize();
return;
}
};
Archiver.prototype._onQueueTask = function(task, callback) {
if (this._state.finalizing || this._state.finalized || this._state.aborted) {
callback();
return;
}
this._task = task;
this._moduleAppend(task.source, task.data, callback);
};
Archiver.prototype._onStatQueueTask = function(task, callback) {
if (this._state.finalizing || this._state.finalized || this._state.aborted) {
callback();
return;
}
fs.stat(task.filepath, function(err, stats) {
if (this._state.aborted) {
setImmediate(callback);
return;
}
if (err) {
this.emit('error', err);
setImmediate(callback);
return;
}
task = this._updateQueueTaskWithStats(task, stats);
if (task.source !== null) {
this._queue.push(task);
setImmediate(callback);
} else {
this.emit('error', new Error('unsupported entry: ' + task.filepath));
setImmediate(callback);
return;
}
}.bind(this));
};
Archiver.prototype._shutdown = function() {
this._moduleUnpipe();
this.end();
};
Archiver.prototype._transform = function(chunk, encoding, callback) {
if (chunk) {
this._pointer += chunk.length;
}
callback(null, chunk);
};
Archiver.prototype._updateQueueTaskWithStats = function(task, stats) {
if (stats.isFile()) {
task.data.type = 'file';
task.data.sourceType = 'stream';
task.source = util.lazyReadStream(task.filepath);
} else if (stats.isDirectory() && this._moduleSupports('directory')) {
task.data.name = util.trailingSlashIt(task.data.name);
task.data.type = 'directory';
task.data.sourcePath = util.trailingSlashIt(task.filepath);
task.data.sourceType = 'buffer';
task.source = new Buffer(0);
} else {
return task;
}
task.data = this._normalizeEntryData(task.data, stats);
return task;
};
Archiver.prototype.abort = function() {
if (this._state.aborted || this._state.finalized) {
return this;
}
this._abort();
return this;
};
Archiver.prototype.append = function(source, data) {
if (this._state.finalize || this._state.aborted) {
this.emit('error', new Error('append: queue closed'));
return this;
}
data = this._normalizeEntryData(data);
if (typeof data.name !== 'string' || data.name.length === 0) {
this.emit('error', new Error('append: entry name must be a non-empty string value'));
return this;
}
if (data.type === 'directory' && !this._moduleSupports('directory')) {
this.emit('error', new Error('append: entries of "directory" type not currently supported by this module'));
return this;
}
source = util.normalizeInputSource(source);
if (Buffer.isBuffer(source)) {
data.sourceType = 'buffer';
} else if (util.isStream(source)) {
data.sourceType = 'stream';
} else {
this.emit('error', new Error('append: input source must be valid Stream or Buffer instance'));
return this;
}
this._queue.push({
data: data,
source: source
});
return this;
};
Archiver.prototype.bulk = function(mappings) {
if (this._state.finalize || this._state.aborted) {
this.emit('error', new Error('bulk: queue closed'));
return this;
}
if (!Array.isArray(mappings)) {
mappings = [mappings];
}
var self = this;
var files = util.file.normalizeFilesArray(mappings);
files.forEach(function(file){
var isExpandedPair = file.orig.expand || false;
var fileData = file.data || {};
file.src.forEach(function(filepath) {
var data = util._.extend({}, fileData);
data.name = isExpandedPair ? util.unixifyPath(file.dest) : util.unixifyPath(file.dest || '', filepath);
if (data.name === '.') {
return;
}
self._append(filepath, data);
});
});
return this;
};
Archiver.prototype.directory = function(dirpath, destpath, data) {
if (this._state.finalize || this._state.aborted) {
this.emit('error', new Error('directory: queue closed'));
return this;
}
if (typeof dirpath !== 'string' || dirpath.length === 0) {
this.emit('error', new Error('directory: dirpath must be a non-empty string value'));
return this;
}
this._pending++;
if (destpath === false) {
destpath = '';
} else if (typeof destpath !== 'string'){
destpath = dirpath;
}
if (typeof data !== 'object') {
data = {};
}
var self = this;
util.walkdir(dirpath, function(err, results) {
if (err) {
self.emit('error', err);
} else {
results.forEach(function(file) {
var entryData = util._.extend({}, data);
entryData.name = util.sanitizePath(destpath, file.relative);
entryData.stats = file.stats;
self._append(file.path, entryData);
});
}
self._pending--;
self._maybeFinalize();
});
return this;
};
Archiver.prototype.file = function(filepath, data) {
if (this._state.finalize || this._state.aborted) {
this.emit('error', new Error('file: queue closed'));
return this;
}
if (typeof filepath !== 'string' || filepath.length === 0) {
this.emit('error', new Error('file: filepath must be a non-empty string value'));
return this;
}
this._append(filepath, data);
return this;
};
Archiver.prototype.finalize = function() {
if (this._state.aborted) {
this.emit('error', new Error('finalize: archive was aborted'));
return this;
}
if (this._state.finalize) {
this.emit('error', new Error('finalize: archive already finalizing'));
return this;
}
this._state.finalize = true;
if (this._pending === 0 && this._queue.idle() && this._statQueue.idle()) {
this._finalize();
}
return this;
};
Archiver.prototype.setFormat = function(format) {
if (this._format) {
this.emit('error', new Error('format: archive format already set'));
return this;
}
this._format = format;
return this;
};
Archiver.prototype.setModule = function(module) {
if (this._state.aborted) {
this.emit('error', new Error('module: archive was aborted'));
return this;
}
if (this._state.module) {
this.emit('error', new Error('module: module already set'));
return this;
}
this._module = module;
this._modulePipe();
return this;
};
Archiver.prototype.pointer = function() {
return this._pointer;
};
view raw core.js hosted with ❤ by GitHub
       
  • As we have modified the core .js file the node_modules/istanbul-middleware/lib/handlers.js code has to be modified. Following is the code for handler.js
/*
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'),
core = require('./core'),
istanbul = require('istanbul'),
bodyParser = require('body-parser'),
ASSETS_DIR = istanbul.assetsDir,
existsSync = fs.existsSync || path.existsSync,
url = require('url'),
archiver = require('archiver'),
ZipWriter = require('./zip-writer'),
express = require('express'),
Report = istanbul.Report,
Collector = istanbul.Collector,
utils = istanbul.utils,
JS_RE = /\.js$/;
/**
* Set default max limit to 100mb for incoming JSON and urlencoded
* @type {String}
*/
var fileSizeMaximum = '100mb';
var isExtended = true;
function createHandler(opts) {
/*jslint nomen: true */
opts = opts || {};
var app = express();
// using separete options objects to maintain readability as the objects are getting more complex
var dirForAllFiles=opts.staticdir;
var urlOptions = { extended: isExtended, limit: fileSizeMaximum };
var jsonOptions = { limit: fileSizeMaximum };
//send static file for /asset/asset-name
app.use('/asset', express.static(ASSETS_DIR));
app.use('/asset', express.static(path.join(ASSETS_DIR, 'vendor')));
app.use(bodyParser.urlencoded(urlOptions));
app.use(bodyParser.json(jsonOptions));
//show main page for coverage report for /
app.get('/', function (req, res) {
var origUrl = url.parse(req.originalUrl).pathname,
origLength = origUrl.length;
if (origUrl.charAt(origLength - 1) !== '/') {
origUrl += '/';
}
core.render(null, res, origUrl);
});
//show page for specific file/ dir for /show?file=/path/to/file
app.get('/show', function (req, res) {
var origUrl = url.parse(req.originalUrl).pathname,
u = url.parse(req.url).pathname,
pos = origUrl.indexOf(u),
file = req.query.p;
if (pos >= 0) {
origUrl = origUrl.substring(0, pos);
}
if (!file) {
res.setHeader('Content-type', 'text/plain');
return res.end('[p] parameter must be specified');
}
core.render(file, res, origUrl);
});
//reset coverage to baseline on POST /reset
app.post('/reset', function (req, res) {
core.restoreBaseline();
res.json({ ok: true });
});
//opt-in to allow resets on GET as well (useful for easy browser-based demos :)
if (opts.resetOnGet) {
app.get('/reset', function (req, res) {
core.restoreBaseline();
res.json({ ok: true });
});
}
//return global coverage object on /object as JSON
app.get('/object', function (req, res) {
res.json(core.getCoverageObject() || {});
});
//send self-contained download package with coverage and reports on /download
app.get('/download', function (req, res) {
var stream = archiver.createZip(),
writer = new ZipWriter(stream, process.cwd()),
coverageObject = core.getCoverageObject() || {},
collector = new Collector(),
baseDir = process.cwd(),
reports = [
Report.create('html', { writer: writer, dir: path.join(baseDir, 'lcov-report') }),
Report.create('lcovonly', { writer: writer, dir: baseDir })
];
utils.removeDerivedInfo(coverageObject);
collector.add(coverageObject);
res.statusCode = 200;
res.setHeader('Content-type', 'application/zip');
res.setHeader('Content-Disposition', 'attachment; filename=coverage.zip');
stream.pipe(res);
writer.writeFile('coverage.json', function (w) {
w.write(JSON.stringify(coverageObject, undefined, 4));
});
reports.forEach(function (report) {
report.writeReport(collector);
});
writer.done();
});
//merge client coverage posted from browser
app.post('/client', function (req, res) {
var body = req.body;
console.log(opts);
if (!(body && typeof body === 'object')) { //probably needs to be more robust
return res.send(400, 'Please post an object with content-type: application/json');
}
core.createNonTestedFilesCoverage(opts);
core.mergeClientCoverage(body);
res.json({ok: true});
});
return app;
}
function defaultClientMatcher(req) {
var parsed = url.parse(req.url);
return parsed.pathname && parsed.pathname.match(JS_RE);
}
function defaultPathTransformer(root) {
return function (req) {
var parsed = url.parse(req.url),
pathName = parsed.pathname;
if (pathName && pathName.charAt(0) === '/') {
pathName = pathName.substring(1);
}
return path.resolve(root, pathName);
};
}
function clientHandler(matcher, pathTransformer, opts) {
var verbose = opts.verbose;
return function (req, res, next) {
if (!matcher(req)) { return next(); }
var fullPath = pathTransformer(req);
if (!fullPath) { return next(); }
if (!core.getInstrumenter()) {
console.error('No instrumenter set up, please call createHandler() before you use the client middleware');
return next();
}
if (!existsSync(fullPath)) {
console.warn('Could not find file [' + fullPath + '], ignoring');
return next();
}
fs.readFile(fullPath, 'utf8', function (err, contents) {
var instrumented;
if (err) {
console.warn('Error reading file: ' + fullPath);
return next();
}
try {
instrumented = core.getInstrumenter().instrumentSync(contents, fullPath);
if (verbose) { console.log('Sending instrumented code for: ' + fullPath + ', url:' + req.url); }
res.setHeader('Content-type', 'application/javascript');
return res.send(instrumented);
} catch (ex) {
console.warn('Error instrumenting file:' + fullPath);
return next();
}
});
};
}
function createClientHandler(root, opts) {
opts = opts || {};
var app = express(),
matcher = opts.matcher || defaultClientMatcher,
pathTransformer = opts.pathTransformer || defaultPathTransformer(root);
app.get('*', clientHandler(matcher, pathTransformer, opts));
return app;
}
module.exports = {
createClientHandler: createClientHandler,
createHandler: createHandler,
hookLoader: core.hookLoader
};
view raw handlers.js hosted with ❤ by GitHub
  • That's all. Now if you run your tests, whichever files not loaded during test will also be covered in the coverage report having 0% coverage for those.

1 comment:
Write comments