diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..1135e27 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] + +indent_style = tab +indent_size = 4 + +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[package.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..5dff305 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/tests diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..0bddd1e --- /dev/null +++ b/.eslintrc @@ -0,0 +1,40 @@ +{ + "extends": "airbnb-base", + "env": { + "es6": true, + "browser": true, + "node": true + }, + "rules": { + "indent": [ + "error", + "tab", + { + "SwitchCase": 1 + } + ], + "no-tabs": 0, + "import/prefer-default-export": 0, + "import/no-extraneous-dependencies": 0, + "import/no-dynamic-require": 1, + "prefer-template": 1, + "max-len": [ + "error", + 120 + ], + "no-restricted-syntax": 0, + "arrow-parens": 0, + "no-param-reassign": 1, + "global-require": 1, + "no-underscore-dangle": 0, + "no-shadow": 1, + "consistent-return": 1, + "comma-dangle": ["error", { + "arrays": "always-multiline", + "objects": "always-multiline", + "imports": "always-multiline", + "exports": "always-multiline", + "functions": "ignore" + }] + } +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..efdba87 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +* text=auto +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 1f9143b..8cb8684 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.idea +html-report node_modules tmp .idea +/lib diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 013a5fd..0000000 --- a/.jshintrc +++ /dev/null @@ -1,73 +0,0 @@ -{ - "asi": false, - "bitwise": false, - "boss": false, - "browser": true, - "camelcase": true, - "couch": false, - "curly": true, - "debug": false, - "devel": true, - "dojo": false, - "eqeqeq": true, - "eqnull": true, - "es3": false, - "esnext": false, - "evil": false, - "expr": true, - "forin": false, - "funcscope": true, - "gcl": false, - "globalstrict": false, - "immed": true, - "iterator": false, - "jquery": false, - "lastsemic": false, - "latedef": false, - "laxbreak": true, - "laxcomma": false, - "loopfunc": true, - "mootools": false, - "moz": false, - "multistr": false, - "newcap": true, - "noarg": true, - "node": false, - "noempty": false, - "nonew": true, - "nonstandard": false, - "nomen": false, - "onecase": false, - "onevar": false, - "passfail": false, - "phantom": false, - "plusplus": false, - "proto": false, - "prototypejs": false, - "regexdash": true, - "regexp": false, - "rhino": false, - "scripturl": true, - "shadow": false, - "shelljs": false, - "smarttabs": true, - "strict": false, - "sub": false, - "supernew": false, - "trailing": true, - "undef": true, - "unused": true, - "validthis": true, - "withstmt": false, - "white": true, - "worker": false, - "wsh": false, - "yui": false, - - "maxlen": 120, - "indent": 4, - "maxerr": 250, - "predef": [ "require", "define" ], - "quotmark": "single", - "maxcomplexity": 10 -} diff --git a/.npmignore b/.npmignore index b4b3891..6abaafc 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,2 @@ tests -.jshintrc .travis.yml diff --git a/.travis.yml b/.travis.yml index f637d1a..97f9f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ notifications: on_failure: always # options: [always|never|change] default: always on_start: never # options: [always|never|change] default: always node_js: - - "5.1" + - 6 cache: directories: - node_modules diff --git a/bin/remap-istanbul b/bin/remap-istanbul deleted file mode 100755 index 0034d01..0000000 --- a/bin/remap-istanbul +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env node - -/* jshint node:true */ -/* global Promise */ -var loadCoverage = require('../lib/loadCoverage'); -var remap = require('../lib/remap'); -var writeReport = require('../lib/writeReport'); -var MemoryStore = require('istanbul/lib/store/memory'); -var Collector = require('istanbul/lib/collector'); - -/** - * Helper function that reads from standard in and resolves a Promise with the - * data or rejects with any errors. - * @return {Promise} A promsie that is resolved with the data from standard in - * or rejected with any errors. - */ -function readStdIn() { - /* istanbul ignore next: too challenging to test for reading from stdin */ - return new Promise(function (resolve, reject) { - var stdin = process.stdin; - var buffer = ''; - - stdin.setEncoding('utf8'); - - stdin.on('data', function (data) { - buffer += data; - }); - - stdin.on('error', function (e) { - reject(e); - }); - - stdin.on('end', function () { - resolve(buffer); - }); - - try { - stdin.resume(); - } - catch (e) { - reject(e); - } - }); -} - -/** - * The main wrapper to provide a CLI interface to remap-istanbul - * @param {Array} argv An array of arguments passed the process - * @return {Promise} A promise that resolves when the remapping is complete - * or rejects if there is an error. - */ -function main (argv) { - /* jshint maxcomplexity:13 */ - - /** - * Helper function that processes the arguments - * @return {String} The next valid argument - */ - function getArg() { - var arg = argv.shift(); - if (arg && arg.indexOf('--') === 0) { - arg = arg.split('='); - if (arg.length > 1) { - argv.unshift(arg.slice(1).join('=')); - } - arg = arg[0]; - } - else if (arg && arg[0] === '-') { - /* istanbul ignore if */ - if (arg.length > 2) { - argv = arg.substring(1).split('').map(function (ch) { - return '-' + ch; - }).concat(argv); - arg = argv.shift(); - } - } - - return arg; - } - - var arg; - var inputFiles = []; - var output; - var reportType; - var basePath; - var exclude; - while ((arg = getArg())) { - switch (arg) { - case '-i': - case '--input': - inputFiles.push(argv.shift()); - break; - case '-o': - case '--output': - output = argv.shift(); - break; - case '-b': - case '--basePath': - basePath = argv.shift(); - break; - case '-t': - case '--type': - reportType = argv.shift(); - break; - case '-e': - case '--exclude': - exclude = argv.shift(); - if (exclude.indexOf(',') !== -1) { - exclude = new RegExp(exclude.replace(/,/g, '|')) - } - break; - default: - throw new SyntaxError('Unrecognised argument: "' + arg + '".'); - } - } - - return new Promise(function (resolve, reject) { - var coverage = inputFiles.length ? loadCoverage(inputFiles) : - /* istanbul ignore next */ readStdIn().then(function (data) { - try { - data = JSON.parse(data); - var collector = new Collector(); - collector.add(data); - return collector.getFinalCoverage(); - } - catch (err) { - console.error(err.stack); - reject(err); - } - }, reject); - - resolve(coverage); - }).then(function (coverage) { - var sources = new MemoryStore(); - var collector = remap(coverage, { - sources: sources, - basePath: basePath || undefined, - exclude: exclude || undefined - }); - if (!Object.keys(sources.map).length) { - sources = undefined; - } - var reportOptions = {}; - /* istanbul ignore else: too hard to test writing to stdout */ - if (output) { - return writeReport(collector, reportType || 'json', reportOptions, output, sources); - } - else { - if (reportType && (reportType === 'lcovonly' || reportType === 'text-lcov')) { - return writeReport(collector, 'text-lcov', reportOptions); - } - else { - process.stdout.write(JSON.stringify(collector.getFinalCoverage()) + '\n'); - } - } - }); -} - -/* istanbul ignore if: we use the module interface in testing */ -if (!module.parent) { - process.title = 'remap-istanbul'; - /* first two arguments are meaningless to the process */ - main(process.argv.slice(2)).then(function (code) { - return process.exit(code || 0); - }, function (err) { - console.log(err.stack); - process.exit(1); - }); -} -else { - module.exports = main; -} diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 9cba407..0000000 --- a/jsconfig.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "compilerOptions": { - "target": "es6", - "module": "commonjs" - }, - "files": [ - "main.js", - "bin/remap-istanbul", - "lib/intern-reporters/JsonCoverage.js", - "lib/gulpRemapIstanbul.js", - "lib/loadCoverage.js", - "lib/node.js", - "lib/remap.js", - "lib/writeReport.js", - "tasks/remapIstanbul.js", - "tests/unit/bin/remap-istanbul.js", - "tests/unit/lib/intern-reporters/JsonCoverage.js", - "tests/unit/lib/gulpRemapIstanbul.js", - "tests/unit/lib/loadCoverage.js", - "tests/unit/lib/remap.js", - "tests/unit/lib/writeReport.js", - "tests/unit/tasks/remapIstanbul.js", - "tests/unit/all.js", - "tests/unit/main.js", - "tests/intern.js", - "typings/main.d.ts" - ] -} diff --git a/lib/gulpRemapIstanbul.js b/lib/gulpRemapIstanbul.js deleted file mode 100644 index 29d879b..0000000 --- a/lib/gulpRemapIstanbul.js +++ /dev/null @@ -1,61 +0,0 @@ -/* jshint node: true */ -/*jshint -W079 */ -if (typeof define !== 'function') { /* istanbul ignore next */ var define = require('amdefine')(module); } -define([ - 'require', - 'exports', - './remap', - './writeReport', - './node!istanbul/lib/store/memory', - './node!gulp-util', - './node!through2' -], function (require, exports, remap, writeReport, MemoryStore, gutil, through) { - /* global Promise */ - var PluginError = gutil.PluginError; - - return function (opts) { - opts = opts || {}; - return through.obj(function (file, enc, cb) { - var p = []; - if(!opts.warn) { - opts.warn = function (message) { - if (opts.fail) { - return cb(new PluginError('remap-istanbul', message)); - } - else { - console.error(message); - } - }; - } - - opts.sources = new MemoryStore(); - - if (file.isNull()) { - return cb(null, file); - } - - if (file.isStream()) { - return cb(new PluginError('remap-istanbul', 'Streaming not supported')); - } - - var collector = remap(JSON.parse(file.contents.toString('utf8')), opts); - - var sources; - if (Object.keys(opts.sources.map).length) { - sources = opts.sources; - } - - if (opts.reports) { - for (var key in opts.reports) { - p.push(writeReport(collector, key, opts.reportOpts || {}, opts.reports[key], sources)); - } - } - - file.contents = new Buffer(JSON.stringify(collector.getFinalCoverage())); - - Promise.all(p).then(function () { - cb(null, file); - }); - }); - }; -}); diff --git a/lib/intern-reporters/JsonCoverage.js b/lib/intern-reporters/JsonCoverage.js deleted file mode 100644 index 31fdd94..0000000 --- a/lib/intern-reporters/JsonCoverage.js +++ /dev/null @@ -1,24 +0,0 @@ -define([ - '../node!istanbul/lib/collector', - '../node!istanbul/lib/report/json' -], function (Collector, Reporter) { - function JsonCoverageReporter(config) { - config = config || {}; - - this._collector = new Collector(); - this._reporter = new Reporter({ - file: config.filename, - watermarks: config.watermarks - }); - } - - JsonCoverageReporter.prototype.coverage = function (sessionId, coverage) { - this._collector.add(coverage); - }; - - JsonCoverageReporter.prototype.runEnd = function () { - this._reporter.writeReport(this._collector, true); - }; - - return JsonCoverageReporter; -}); diff --git a/lib/loadCoverage.js b/lib/loadCoverage.js deleted file mode 100644 index 788dc3d..0000000 --- a/lib/loadCoverage.js +++ /dev/null @@ -1,46 +0,0 @@ -/* jshint node: true */ -/*jshint -W079 */ -if (typeof define !== 'function') { /* istanbul ignore next */ var define = require('amdefine')(module); } -define([ - 'require', - 'exports', - './node!istanbul/lib/collector', - './node!fs' -], function (require, exports, Collector, fs) { - - /** - * Takes sources of coverage information and adds them to a collector which then can be subsequently - * remapped. - * @param {Array|string} sources The source(s) of the JSON coverage information - * @param {Object} options? A hash of options that can be set: - * readJSON?: A function that can read and parse a JSON file - * warn?: A function that logs warning messages - * @return {Object} The loaded coverage object - */ - return function loadCoverage(sources, options) { - options = options || {}; - - var warn = options.warn || console.warn; - - var readJSON = options.readJSON || function readJSON(filePath) { - if (!fs.existsSync(filePath)) { - warn(new Error('Cannot find file: "' + filePath + '"')); - return {}; - } - return JSON.parse(fs.readFileSync(filePath)); - }; - - if (typeof sources === 'string') { - sources = [ sources ]; - } - if (!sources.length) { - warn(new SyntaxError('No coverage files supplied!')); - } - var collector = new Collector(); - sources.forEach(function (filePath) { - collector.add(readJSON(filePath)); - }); - - return collector.getFinalCoverage(); - }; -}); diff --git a/lib/remap.js b/lib/remap.js deleted file mode 100644 index 29a9fba..0000000 --- a/lib/remap.js +++ /dev/null @@ -1,462 +0,0 @@ -/* jshint node: true */ -/*jshint -W079 */ -(function(define) { -define([ - 'require', - 'exports', - './node!istanbul/lib/collector', - './node!path', - './node!fs', - './node!source-map/lib/source-map-consumer' -], function (require, exports, Collector, path, fs, smc) { - /* global WeakMap */ - - var SourceMapConsumer = smc.SourceMapConsumer; - - var sourceMapRegEx = /(?:\/{2}[#@]{1,2}|\/\*)\s+sourceMappingURL\s*=\s*(data:(?:[^;]+;)+base64,)?(\S+)/; - - var MaybeWeakMap = typeof WeakMap === 'function' ? WeakMap : function(){ - var objectList = []; - var dataList = []; - - this.set = function(object, data) { - var index = objectList.indexOf(object); - if(index !== -1) { - data[index] = data; - } else { - objectList.push(object); - dataList.push(data); - } - }; - - this.get = function(object) { - var index = objectList.indexOf(object); - if(index !== -1) { - return dataList[index]; - } - }; - }; - - var metaInfo = new MaybeWeakMap(); - /** - * Generate a coverage object that will be filled with the remapped data - * @param {Object} srcCoverage The coverage object to be populated - * @param {string} filename The name of the file that is being remapped - * @return {Object} An object that provides the actual data and - * its shadow data used for reference. - */ - function getSourceCoverage(srcCoverage, filename) { - var data = srcCoverage[filename]; - if (!data) { - data = srcCoverage[filename] = { - path: filename, - statementMap: {}, - fnMap: {}, - branchMap: {}, - s: {}, - b: {}, - f: {} - }; - metaInfo.set(data, { - indexes: {}, - lastIndex: { - s: 0, - b: 0, - f: 0 - } - }); - } - - return { - data: data, - meta: metaInfo.get(data) - }; - } - - /** - * A function that determines the original position for a given location - * @param {SourceMapConsumer} sourceMap The source map - * @param {string} sourceMapDir The directory where the original is located - * @param {Object} location The original location Object - * @param {Boolean} useAbsolutePaths If `true`, don't resolve the path relative to the `cwd` - * @param {Object} inlineSourceMap If `true`, don't try to resolve the source path, just copy it - * @return {Object} The remapped location Object - */ - function getMapping(sourceMap, sourceMapDir, location, useAbsolutePaths, inlineSourceMap) { - /* jshint maxcomplexity: 15 */ - - /* istanbul ignore if: edge case too hard to test for with babel malformation */ - if (location.start.line < 1 || location.start.column < 0) { - return null; - } - /* istanbul ignore if: edge case too hard to test for with babel malformation */ - if (location.end.line < 1 || location.end.column < 0) { - return null; - } - - var start = sourceMap.originalPositionFor(location.start); - var end = sourceMap.originalPositionFor(location.end); - var src; - var resolvedSource; - - /* istanbul ignore if: edge case too hard to test for */ - if (!start || !end) { - return null; - } - if (!start.source || !end.source || start.source !== end.source) { - return null; - } - /* istanbul ignore if: edge case too hard to test for */ - if (start.line === null || start.column === null) { - return null; - } - /* istanbul ignore if: edge case too hard to test for */ - if (end.line === null || end.column === null) { - return null; - } - src = start.source; - - if (start.line === end.line && start.column === end.column) { - end = sourceMap.originalPositionFor({ - line: location.end.line, - column: location.end.column, - bias: 2 - }); - end.column = end.column - 1; - } - - resolvedSource = start.source in inlineSourceMap ? start.source : path.resolve(sourceMapDir, start.source); - if (!useAbsolutePaths && !(start.source in inlineSourceMap)) { - resolvedSource = path.relative(process.cwd(), resolvedSource); - } - return { - source: resolvedSource, - loc: { - start: { - line: start.line, - column: start.column - }, - end: { - line: end.line, - column: end.column - }, - skip: location.skip - } - }; - } - - /** - * Remaps coverage data based on the source maps it discovers in the - * covered files and returns a coverage Collector that contains the remappped - * data. - * @param {Array|Object} coverage The coverage (or array of coverages) that need to be - * remapped - * @param {Object} options A configuration object: - * basePath? - a string containing to utilise as the base path - * for determining the location of the source file - * exclude? - a string or Regular Expression that filters out - * any coverage where the file path matches - * readFile? - a function that can read a file - * syncronously - * readJSON? - a function that can read and parse a - * JSON file syncronously - * sources? - a Istanbul store where inline sources will be - * added - * warn? - a function that logs warnings - * @return {istanbul/lib/collector} The remapped collector - */ - return function remap(coverage, options) { - options = options || {}; - - var warn = options.warn || console.warn; - - var exclude; - if (options.exclude) { - if (typeof options.exclude === 'string') { - exclude = function (fileName) { - return fileName.indexOf(options.exclude) > -1; - }; - } - else { - exclude = function (fileName) { - return fileName.match(options.exclude); - }; - } - } - - var useAbsolutePaths = !!options.useAbsolutePaths; - - var sourceStore = options.sources; - - var readJSON = options.readJSON || function readJSON(filePath) { - if (!fs.existsSync(filePath)) { - warn(new Error('Could not find file: "' + filePath + '"')); - return null; - } - return JSON.parse(fs.readFileSync(filePath)); - }; - - var readFile = options.readFile || function readFile(filePath) { - if (!fs.existsSync(filePath)) { - warn(new Error('Could not find file: "' + filePath + '"')); - return ''; - } - return fs.readFileSync(filePath); - }; - - var srcCoverage = {}; - - if (!Array.isArray(coverage)) { - coverage = [ coverage ]; - } - - coverage.forEach(function (item) { - Object.keys(item).forEach(function (filePath) { - if (exclude && exclude(filePath)) { - warn('Excluding: "' + filePath + '"'); - return; - } - var fileCoverage = item[filePath]; - /* coverage.json can sometimes include the code inline */ - var codeIsArray = true; - var codeFromFile = false; - var jsText = fileCoverage.code; - if (!jsText) { - jsText = readFile(filePath); - codeFromFile = true; - } - if (Array.isArray(jsText)) { /* sometimes the source is an array */ - jsText = jsText.join('\n'); - } - else { - codeIsArray = false; - } - var match = sourceMapRegEx.exec(jsText); - var sourceMapDir = path.dirname(filePath); - var rawSourceMap; - - if (!match && !codeFromFile) { - codeIsArray = false; - jsText = readFile(filePath); - match = sourceMapRegEx.exec(jsText); - } - - if (match) { - if (match[1]) { - rawSourceMap = JSON.parse((new Buffer(match[2], 'base64').toString('utf8'))); - } - else { - var sourceMapPath = path.join(sourceMapDir, match[2]); - rawSourceMap = readJSON(sourceMapPath); - sourceMapDir = path.dirname(sourceMapPath); - } - } - - if (!match || !rawSourceMap) { - /* We couldn't find a source map, so will copy coverage after warning. */ - warn(new Error('Could not find source map for: "' + filePath + '"')); - try { - fileCoverage.code = String(fs.readFileSync(filePath)).split('\n'); - } - catch (error) { - warn(new Error('Could find source for : "' + filePath + '"')); - } - srcCoverage[filePath] = fileCoverage; - return; - } - - sourceMapDir = options.basePath || sourceMapDir; - - // replace relative paths in source maps with absolute - rawSourceMap.sources = rawSourceMap.sources.map(function (filePath) { - return filePath.substr(0, 1) === '.' - ? path.resolve(sourceMapDir, filePath) - : filePath; - }); - - var sourceMap = new SourceMapConsumer(rawSourceMap); - - /* if there are inline sources and a store to put them into, we will populate it */ - var inlineSourceMap = {}, - origSourceFilename, - origFileName, - fileName; - - if (sourceMap.sourcesContent) { - origSourceFilename = rawSourceMap.sources[0]; - - if (origSourceFilename && path.extname(origSourceFilename) !== '') { - origFileName = rawSourceMap.file; - fileName = filePath.replace(path.extname(origFileName), path.extname(origSourceFilename)); - rawSourceMap.file = fileName; - rawSourceMap.sources = [fileName]; - rawSourceMap.sourceRoot = ''; - sourceMap = new SourceMapConsumer(rawSourceMap); - } - - sourceMap.sourcesContent.forEach(function (source, idx) { - inlineSourceMap[sourceMap.sources[idx]] = true; - getSourceCoverage(srcCoverage, sourceMap.sources[idx]).data.code = codeIsArray ? source.split('\n') : source; - if (sourceStore) { - sourceStore.set(sourceMap.sources[idx], source); - } - }); - } - - Object.keys(fileCoverage.fnMap).forEach(function (index) { - var genItem = fileCoverage.fnMap[index]; - var mapping = getMapping(sourceMap, sourceMapDir, genItem.loc, useAbsolutePaths, inlineSourceMap); - - if (!mapping) { - return; - } - - var hits = fileCoverage.f[index]; - var covInfo = getSourceCoverage(srcCoverage, mapping.source); - var data = covInfo.data; - var meta = covInfo.meta; - var srcItem = { - name: genItem.name, - line: mapping.loc.start.line, - loc: mapping.loc - }; - if (genItem.skip) { - srcItem.skip = genItem.skip; - } - var key = [ - 'f', - srcItem.loc.start.line, srcItem.loc.start.column, - srcItem.loc.end.line, srcItem.loc.end.column - ].join(':'); - - var fnIndex = meta.indexes[key]; - if (!fnIndex) { - fnIndex = ++meta.lastIndex.f; - meta.indexes[key] = fnIndex; - data.fnMap[fnIndex] = srcItem; - } - data.f[fnIndex] = data.f[fnIndex] || 0; - data.f[fnIndex] += hits; - }); - - Object.keys(fileCoverage.statementMap).forEach(function (index) { - var genItem = fileCoverage.statementMap[index]; - - var mapping = getMapping(sourceMap, sourceMapDir, genItem, useAbsolutePaths, inlineSourceMap); - - if (!mapping) { - return; - } - - var hits = fileCoverage.s[index]; - var covInfo = getSourceCoverage(srcCoverage, mapping.source); - var data = covInfo.data; - var meta = covInfo.meta; - - var key = [ - 's', - mapping.loc.start.line, mapping.loc.start.column, - mapping.loc.end.line, mapping.loc.end.column - ].join(':'); - - var stIndex = meta.indexes[key]; - if (!stIndex) { - stIndex = ++meta.lastIndex.s; - meta.indexes[key] = stIndex; - data.statementMap[stIndex] = mapping.loc; - } - data.s[stIndex] = data.s[stIndex] || 0; - data.s[stIndex] += hits; - }); - - Object.keys(fileCoverage.branchMap).forEach(function (index) { - var genItem = fileCoverage.branchMap[index]; - var locations = []; - var source; - var key = [ 'b' ]; - - for (var i = 0; i < genItem.locations.length; ++i) { - var mapping = getMapping(sourceMap, sourceMapDir, genItem.locations[i], useAbsolutePaths, inlineSourceMap); - if (!mapping) { - return; - } - /* istanbul ignore else: edge case too hard to test for */ - if (!source) { - source = mapping.source; - } - else if (source !== mapping.source) { - return; - } - locations.push(mapping.loc); - key.push( - mapping.loc.start.line, mapping.loc.start.column, - mapping.loc.end.line, mapping.loc.end.line - ); - } - - key = key.join(':'); - - var hits = fileCoverage.b[index]; - var covInfo = getSourceCoverage(srcCoverage, source); - var data = covInfo.data; - var meta = covInfo.meta; - - var brIndex = meta.indexes[key]; - if (!brIndex) { - brIndex = ++meta.lastIndex.b; - meta.indexes[key] = brIndex; - data.branchMap[brIndex] = { - line: locations[0].start.line, - type: genItem.type, - locations: locations - }; - } - - if (!data.b[brIndex]) { - data.b[brIndex] = locations.map(function () { - return 0; - }); - } - - for (i = 0; i < hits.length; ++i) { - data.b[brIndex][i] += hits[i]; - } - }); - - if (sourceMap.sourcesContent && options.basePath) { - // Convert path to use base path option - var getPath = function (filePath) { - var absolutePath = path.resolve(options.basePath, filePath); - if (!useAbsolutePaths) { - return path.relative(process.cwd(), absolutePath); - } - return absolutePath; - }; - var fullSourceMapPath = getPath(origFileName.replace(path.extname(origFileName), path.extname(origSourceFilename))); - srcCoverage[fullSourceMapPath] = srcCoverage[fileName]; - srcCoverage[fullSourceMapPath].path = fullSourceMapPath; - delete srcCoverage[fileName]; - } - }); - }); - - var collector = new Collector(); - - srcCoverage = Object.keys(srcCoverage) - .filter(function(filePath){ - return !(exclude && exclude(filePath)) - }) - .reduce(function(obj, name){ - obj[name] = srcCoverage[name] - return obj - }, {}) - - collector.add(srcCoverage); - - /* refreshes the line counts for reports */ - collector.getFinalCoverage(); - - return collector; - }; -}); -})(typeof define === 'function' ? define : require('amdefine')(module)); diff --git a/lib/writeReport.js b/lib/writeReport.js deleted file mode 100644 index bf834c0..0000000 --- a/lib/writeReport.js +++ /dev/null @@ -1,81 +0,0 @@ -/* jshint node: true */ -/*jshint -W079 */ -if (typeof define !== 'function') { /* istanbul ignore next */ var define = require('amdefine')(module); } -define([ - 'require', - 'exports', - './node!istanbul/index' -], function (require) { - /* global Promise */ - - var istanbulReportTypes = { - 'clover': 'file', - 'cobertura': 'file', - 'html': 'directory', - 'json-summary': 'file', - 'json': 'file', - 'lcovonly': 'file', - 'teamcity': 'file', - 'text-lcov': 'console', - 'text-summary': 'file', - 'text': 'file' - }; - - /** - * A utility function to mixin values to a destination object - * @param {any} destination The destination object - * @param {any[]} mixins Any objects to be mixed into the destination - */ - function mixin(destination/*, ...mixins*/) { - for (var i = 1; i < arguments.length; i++) { - var source = arguments[i]; - for (var key in source) { - destination[key] = source[key]; - } - } - return destination; - } - - /** - * Generates an Instanbul Coverage report based on the information passed. - * @param {istanbul/lib/collector} collector An instance of an coverage - * collector - * @param {string} reportType The name of the report type to - * generate - * @param {object} reportOptions The options to pass to the reporter - * @param {string|function} dest The filename or outputting - * function to use for generating - * the report - * @param {istanbul/lib/store} sources? A store of sources to be passed - * the reporter - * @return {Promise} A promise that resolves when the - * report is complete. - */ - return function writeReport(collector, reportType, reportOptions, dest, sources) { - return new Promise(function (resolve, reject) { - if (!(reportType in istanbulReportTypes)) { - reject(new SyntaxError('Unrecognized report type of "' + reportType + '".')); - return; - } - require([ './node!istanbul/lib/report/' + reportType ], function (Reporter){ - var options = mixin({}, reportOptions); - switch (istanbulReportTypes[reportType]) { - case 'file': - options.file = dest; - break; - case 'directory': - options.dir = dest; - break; - case 'console': - options.log = dest || console.log; - break; - } - if (sources) { - options.sourceStore = sources; - } - var reporter = new Reporter(options); - resolve(reporter.writeReport(collector, true)); - }); - }); - }; -}); diff --git a/package.json b/package.json index cdde60a..56c0c1b 100644 --- a/package.json +++ b/package.json @@ -1,42 +1,61 @@ { - "name": "remap-istanbul", - "version": "0.6.5-pre", - "description": "A tool for remapping Istanbul coverage via Source Maps", - "homepage": "http://dojotoolkit.org", - "keywords": [ "gulpplugin", "gruntplugin", "source-map", "istanbul", "coverage" ], - "maintainers": [ - { - "name": "kitsonk", - "email": "me@kitsonkelly.com" - }, { - "name": "bryanforbes", - "email": "bryan@reigndropsfall.net" - } - ], - "bugs": { - "url": "https://github.com/SitePen/remap-istanbul" - }, - "license": "BSD-3-Clause", - "main": "main.js", - "bin": "./bin/remap-istanbul", - "repository": { - "type": "git", - "url": "https://github.com/SitePen/remap-istanbul.git" - }, - "scripts": { - "test": "tests/run.sh" - }, - "dependencies": { - "amdefine": "1.0.0", - "gulp-util": "3.0.7", - "istanbul": "0.4.5", - "source-map": ">=0.5.6", - "through2": "2.0.1" - }, - "devDependencies": { - "codecov.io": "0.1.6", - "grunt": "^1.0.1", - "gulp": "3.9.1", - "intern": "^3.3.0" - } + "name": "remap-istanbul", + "version": "0.8.0-pre", + "description": "A tool for remapping Istanbul coverage via Source Maps", + "homepage": "https://github.com/SitePen/remap-istanbul", + "keywords": [ + "gulpplugin", + "gruntplugin", + "source-map", + "istanbul", + "coverage" + ], + "maintainers": [ + { + "name": "kitsonk", + "email": "me@kitsonkelly.com" + }, + { + "name": "bryanforbes", + "email": "bryan@reigndropsfall.net" + } + ], + "bugs": { + "url": "https://github.com/SitePen/remap-istanbul" + }, + "license": "BSD-3-Clause", + "main": "./lib/main.js", + "bin": "./lib/remap-istanbul.js", + "repository": { + "type": "git", + "url": "https://github.com/SitePen/remap-istanbul.git" + }, + "scripts": { + "lint": "eslint ./src/", + "test": "tests/run.sh", + "build": "rimraf lib && babel --presets es2015 --plugins transform-runtime src --out-dir ./lib --source-maps", + "prepublish": "npm run build" + }, + "dependencies": { + "amdefine": "^1.0.0", + "babel-runtime": "^6.11.6", + "gulp-util": "3.0.7", + "istanbul": "0.4.5", + "source-map": ">=0.5.6", + "through2": "2.0.1" + }, + "devDependencies": { + "babel-cli": "^6.16.0", + "babel-core": "^6.17.0", + "babel-plugin-transform-runtime": "^6.15.0", + "babel-preset-es2015": "^6.16.0", + "codecov.io": "0.1.6", + "eslint": "^3.7.0", + "eslint-config-airbnb-base": "^10.0.1", + "eslint-plugin-import": "^2.0.1", + "grunt": "^1.0.1", + "gulp": "3.9.1", + "intern": "^3.3.0", + "rimraf": "^2.5.4" + } } diff --git a/src/CoverageTransformer.js b/src/CoverageTransformer.js new file mode 100644 index 0000000..1ae11c2 --- /dev/null +++ b/src/CoverageTransformer.js @@ -0,0 +1,246 @@ +const { Collector } = require('istanbul'); +const path = require('path'); +const fs = require('fs'); +const { SourceMapConsumer } = require('source-map'); + +const sourceMapRegEx = /(?:\/{2}[#@]{1,2}|\/\*)\s+sourceMappingURL\s*=\s*(data:(?:[^;]+;)+base64,)?(\S+)/; + +const SparceCoverageCollector = require('./SparceCoverageCollector').SparceCoverageCollector; + +const getMapping = require('./getMapping'); + +const remapFunction = require('./remapFunction'); +const remapBranch = require('./remapBranch'); + + +class CoverageTransformer { + constructor(options) { + this.basePath = options.basePath; + this.warn = options.warn || console.warn; + + this.exclude = () => false; + if (options.exclude) { + if (typeof options.exclude === 'string') { + this.exclude = (fileName) => fileName.indexOf(options.exclude) > -1; + } else { + this.exclude = (fileName) => fileName.match(options.exclude); + } + } + + this.useAbsolutePaths = !!options.useAbsolutePaths; + + this.readJSON = options.readJSON + || function readJSON(filePath) { + if (!fs.existsSync(filePath)) { + this.warn(Error(`Could not find file: "${filePath}"`)); + return null; + } + return JSON.parse(fs.readFileSync(filePath)); + }; + + this.readFile = options.readFile + || function readFile(filePath) { + if (!fs.existsSync(filePath)) { + this.warn(new Error(`Could not find file: "${filePath}"`)); + return ''; + } + return fs.readFileSync(filePath); + }; + + this.sourceStore = options.sources; + + this.sparceCoverageCollector = new SparceCoverageCollector(); + } + + addFileCoverage(filePath, fileCoverage) { + if (this.exclude(filePath)) { + this.warn(`Excluding: "${filePath}"`); + return; + } + + /* coverage.json can sometimes include the code inline */ + let codeIsArray = true; + let codeFromFile = false; + let jsText = fileCoverage.code; + if (!jsText) { + jsText = this.readFile(filePath); + codeFromFile = true; + } + if (Array.isArray(jsText)) { /* sometimes the source is an array */ + jsText = jsText.join('\n'); + } else { + codeIsArray = false; + } + let match = sourceMapRegEx.exec(jsText); + let sourceMapDir = path.dirname(filePath); + let rawSourceMap; + + if (!match && !codeFromFile) { + codeIsArray = false; + jsText = this.readFile(filePath); + match = sourceMapRegEx.exec(jsText); + } + + if (match) { + if (match[1]) { + rawSourceMap = JSON.parse((new Buffer(match[2], 'base64').toString('utf8'))); + } else { + const sourceMapPath = path.join(sourceMapDir, match[2]); + rawSourceMap = this.readJSON(sourceMapPath); + sourceMapDir = path.dirname(sourceMapPath); + } + } + + if (!match || !rawSourceMap) { + /* We couldn't find a source map, so will copy coverage after warning. */ + this.warn(new Error(`Could not find source map for: "${filePath}"`)); + try { + fileCoverage.code = String(fs.readFileSync(filePath)).split('\n'); + } catch (error) { + this.warn(new Error(`Could find source for : "${filePath}"`)); + } + this.sparceCoverageCollector.setCoverage(filePath, fileCoverage); + return; + } + + sourceMapDir = this.basePath || sourceMapDir; + + // replace relative paths in source maps with absolute + rawSourceMap.sources = rawSourceMap.sources.map((srcPath) => ( + srcPath.substr(0, 1) === '.' + ? path.resolve(sourceMapDir, srcPath) + : srcPath + )); + + let sourceMap = new SourceMapConsumer(rawSourceMap); + + /* if there are inline sources and a store to put them into, we will populate it */ + const inlineSourceMap = {}; + let origSourceFilename; + let origFileName; + let fileName; + + if (sourceMap.sourcesContent) { + origSourceFilename = rawSourceMap.sources[0]; + + if (origSourceFilename && path.extname(origSourceFilename) !== '') { + origFileName = rawSourceMap.file; + fileName = filePath.replace(path.extname(origFileName), path.extname(origSourceFilename)); + rawSourceMap.file = fileName; + rawSourceMap.sources = [fileName]; + rawSourceMap.sourceRoot = ''; + sourceMap = new SourceMapConsumer(rawSourceMap); + } + + sourceMap.sourcesContent.forEach((source, idx) => { + inlineSourceMap[sourceMap.sources[idx]] = true; + this.sparceCoverageCollector.setSourceCode( + sourceMap.sources[idx], + codeIsArray ? source.split('\n') : source + ); + if (this.sourceStore) { + this.sourceStore.set(sourceMap.sources[idx], source); + } + }); + } + + const resolvePath = (source) => { + let resolvedSource = source in inlineSourceMap + ? source + : path.resolve(sourceMapDir, source); + + if (!this.useAbsolutePaths && !(source in inlineSourceMap)) { + resolvedSource = path.relative(process.cwd(), resolvedSource); + } + return resolvedSource; + }; + + const getMappingResolved = (location) => { + const mapping = getMapping(sourceMap, location); + if (!mapping) return null; + + return Object.assign(mapping, { source: resolvePath(mapping.source) }); + }; + + Object.keys(fileCoverage.branchMap).forEach((index) => { + const genItem = fileCoverage.branchMap[index]; + const hits = fileCoverage.b[index]; + + const info = remapBranch(genItem, getMappingResolved); + + if (info) { + this.sparceCoverageCollector.updateBranch(info.source, info.srcItem, hits); + } + }); + + Object.keys(fileCoverage.fnMap).forEach((index) => { + const genItem = fileCoverage.fnMap[index]; + const hits = fileCoverage.f[index]; + + const info = remapFunction(genItem, getMappingResolved); + + if (info) { + this.sparceCoverageCollector.updateFunction(info.source, info.srcItem, hits); + } + }); + + Object.keys(fileCoverage.statementMap).forEach((index) => { + const genItem = fileCoverage.statementMap[index]; + const hits = fileCoverage.s[index]; + + const mapping = getMappingResolved(genItem); + + if (mapping) { + this.sparceCoverageCollector.updateStatement(mapping.source, mapping.loc, hits); + } + }); + + // todo: refactor exposing implementation details + const srcCoverage = this.sparceCoverageCollector.getFinalCoverage(); + + if (sourceMap.sourcesContent && this.basePath) { + // Convert path to use base path option + const getPath = filePath => { + const absolutePath = path.resolve(this.basePath, filePath); + if (!this.useAbsolutePaths) { + return path.relative(process.cwd(), absolutePath); + } + return absolutePath; + }; + const fullSourceMapPath = getPath( + origFileName.replace(path.extname(origFileName), path.extname(origSourceFilename)) + ); + srcCoverage[fullSourceMapPath] = srcCoverage[fileName]; + srcCoverage[fullSourceMapPath].path = fullSourceMapPath; + delete srcCoverage[fileName]; + } + } + + addCoverage(item) { + Object.keys(item) + .forEach((filePath) => { + const fileCoverage = item[filePath]; + this.addFileCoverage(filePath, fileCoverage); + }); + } + + getFinalCoverage() { + const collector = new Collector(); + + const srcCoverage = this.sparceCoverageCollector.getFinalCoverage(); + + collector.add(Object.keys(srcCoverage) + .filter((filePath) => !this.exclude(filePath)) + .reduce((obj, name) => { + obj[name] = srcCoverage[name]; + return obj; + }, {})); + + /* refreshes the line counts for reports */ + collector.getFinalCoverage(); + + return collector; + } +} + +module.exports.CoverageTransformer = CoverageTransformer; diff --git a/src/SparceCoverageCollector.js b/src/SparceCoverageCollector.js new file mode 100644 index 0000000..99597b9 --- /dev/null +++ b/src/SparceCoverageCollector.js @@ -0,0 +1,119 @@ +class SparceCoverageCollector { + constructor() { + this.srcCoverage = {}; + this.metaInfo = {}; + } + + getSourceCoverage(filename) { + let data = this.srcCoverage[filename]; + if (!data) { + data = this.srcCoverage[filename] = { + path: filename, + statementMap: {}, + fnMap: {}, + branchMap: {}, + s: {}, + b: {}, + f: {}, + }; + this.metaInfo[filename] = { + indexes: {}, + lastIndex: { + s: 0, + b: 0, + f: 0, + }, + }; + } + + return { + data, + meta: this.metaInfo[filename], + }; + } + + setCoverage(filePath, fileCoverage) { + this.srcCoverage[filePath] = fileCoverage; + } + + setSourceCode(filePath, source) { + this.getSourceCoverage(filePath).data.code = source; + } + + + getFinalCoverage() { + return this.srcCoverage; + } + + updateBranch(source, srcItem, hits) { + const { data, meta } = this.getSourceCoverage(source); + + let key = ['b']; + srcItem.locations.map(loc => key.push( + loc.start.line, loc.start.column, + loc.end.line, loc.end.line + )); + + key = key.join(':'); + + let index = meta.indexes[key]; + if (!index) { + meta.lastIndex.b += 1; + index = meta.lastIndex.b; + meta.indexes[key] = index; + data.branchMap[index] = srcItem; + } + + if (!data.b[index]) { + data.b[index] = hits.map(v => v); + } else { + for (let i = 0; i < hits.length; i += 1) { + data.b[index][i] += hits[i]; + } + } + } + + updateFunction(source, srcItem, hits) { + const { data, meta } = this.getSourceCoverage(source); + + const key = [ + 'f', + srcItem.loc.start.line, srcItem.loc.start.column, + srcItem.loc.end.line, srcItem.loc.end.column, + ].join(':'); + + let index = meta.indexes[key]; + if (!index) { + meta.lastIndex.f += 1; + index = meta.lastIndex.f; + meta.indexes[key] = index; + data.fnMap[index] = srcItem; + } + + data.f[index] = data.f[index] || 0; + data.f[index] += hits; + } + + updateStatement(source, srcItem, hits) { + const { data, meta } = this.getSourceCoverage(source); + + const key = [ + 's', + srcItem.start.line, srcItem.start.column, + srcItem.end.line, srcItem.end.column, + ].join(':'); + + let index = meta.indexes[key]; + if (!index) { + meta.lastIndex.s += 1; + index = meta.lastIndex.s; + meta.indexes[key] = index; + data.statementMap[index] = srcItem; + } + + data.s[index] = data.s[index] || 0; + data.s[index] += hits; + } +} + +module.exports.SparceCoverageCollector = SparceCoverageCollector; diff --git a/src/getMapping.js b/src/getMapping.js new file mode 100644 index 0000000..a4ce85c --- /dev/null +++ b/src/getMapping.js @@ -0,0 +1,63 @@ +const { SourceMapConsumer } = require('source-map'); + +/** + * A function that determines the original position for a given location + * @param {SourceMapConsumer} sourceMap The source map + * @param {Object} location The original location Object + * @return {Object} The remapped location Object + */ +function getMapping(sourceMap, location) { + /* istanbul ignore if: edge case too hard to test for with babel malformation */ + if (location.start.line < 1 || location.start.column < 0) { + return null; + } + /* istanbul ignore if: edge case too hard to test for with babel malformation */ + if (location.end.line < 1 || location.end.column < 0) { + return null; + } + + const start = sourceMap.originalPositionFor(location.start); + let end = sourceMap.originalPositionFor(location.end); + + /* istanbul ignore if: edge case too hard to test for */ + if (!start || !end) { + return null; + } + if (!start.source || !end.source || start.source !== end.source) { + return null; + } + /* istanbul ignore if: edge case too hard to test for */ + if (start.line === null || start.column === null) { + return null; + } + /* istanbul ignore if: edge case too hard to test for */ + if (end.line === null || end.column === null) { + return null; + } + + if (start.line === end.line && start.column === end.column) { + end = sourceMap.originalPositionFor({ + line: location.end.line, + column: location.end.column, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + end.column -= 1; + } + + return { + source: start.source, + loc: { + start: { + line: start.line, + column: start.column, + }, + end: { + line: end.line, + column: end.column, + }, + skip: location.skip, + }, + }; +} + +module.exports = getMapping; diff --git a/src/gruntRemapIstanbul.js b/src/gruntRemapIstanbul.js new file mode 100644 index 0000000..3163664 --- /dev/null +++ b/src/gruntRemapIstanbul.js @@ -0,0 +1,55 @@ +/* jshint node: true */ +/* global Promise */ + +const loadCoverage = require('./loadCoverage'); +const remap = require('./remap'); +const writeReport = require('./writeReport'); +const MemoryStore = require('istanbul/lib/store/memory'); + +module.exports = function (grunt) { + grunt.registerMultiTask('remapIstanbul', function () { + const done = this.async(); + const options = this.options(); + let sources = new MemoryStore(); + let p = []; + + function warn(message) { + if (options.fail) { + grunt.fail.warn(message); + } else { + grunt.log.error(message); + } + } + + this.files.forEach((file) => { + const coverage = remap(loadCoverage(file.src, { + readJSON: grunt.readJSON, + warn, + }), { + readFile: grunt.readFile, + readJSON: grunt.readJSON, + warn, + sources, + basePath: file.basePath, + useAbsolutePaths: options.useAbsolutePaths, + exclude: options.exclude, + }); + + if (!Object.keys(sources.map).length) { + sources = undefined; + } + + if (file.type && file.dest) { + p.push(writeReport(coverage, file.type, {}, file.dest, sources)); + } else { + p = p.concat(Object.keys(options.reports).map((key) => + writeReport(coverage, key, options.reportOpts || {}, options.reports[key], sources) + )); + } + }); + + Promise.all(p).then(() => { + done(); + }, grunt.fail.fatal); + }); +}; diff --git a/src/gulpRemapIstanbul.js b/src/gulpRemapIstanbul.js new file mode 100644 index 0000000..a8f390c --- /dev/null +++ b/src/gulpRemapIstanbul.js @@ -0,0 +1,53 @@ +/* jshint node: true */ +/* jshint -W079 */ +const remap = require('./remap'); +const writeReport = require('./writeReport'); +const MemoryStore = require('istanbul/lib/store/memory'); +const { PluginError } = require('gulp-util'); +const through = require('through2'); + +/* global Promise */ + +module.exports = function (opts = {}) { + return through.obj((file, enc, cb) => { + if (!opts.warn) { + opts.warn = (message) => { + if (opts.fail) { + cb(new PluginError('remap-istanbul', message)); + } else { + console.error(message); + } + }; + } + + opts.sources = new MemoryStore(); + + if (file.isNull()) { + cb(null, file); + } + + if (file.isStream()) { + cb(new PluginError('remap-istanbul', 'Streaming not supported')); + } + + const collector = remap(JSON.parse(file.contents.toString('utf8')), opts); + + let sources; + if (Object.keys(opts.sources.map).length) { + sources = opts.sources; + } + + const p = []; + if (opts.reports) { + Object.keys(opts.reports).forEach((key) => { + p.push(writeReport(collector, key, opts.reportOpts || {}, opts.reports[key], sources)); + }); + } + + file.contents = new Buffer(JSON.stringify(collector.getFinalCoverage())); + + Promise.all(p).then(() => { + cb(null, file); + }); + }); +}; diff --git a/src/intern-reporters/JsonCoverage.js b/src/intern-reporters/JsonCoverage.js new file mode 100644 index 0000000..6a79326 --- /dev/null +++ b/src/intern-reporters/JsonCoverage.js @@ -0,0 +1,20 @@ +const { Collector } = require('istanbul'); +const Reporter = require('istanbul/lib/report/json'); + +function JsonCoverageReporter(config = {}) { + this._collector = new Collector(); + this._reporter = new Reporter({ + file: config.filename, + watermarks: config.watermarks, + }); +} + +JsonCoverageReporter.prototype.coverage = function coverage(sessionId, coverageData) { + this._collector.add(coverageData); +}; + +JsonCoverageReporter.prototype.runEnd = function runEnd() { + this._reporter.writeReport(this._collector, true); +}; + +module.exports = JsonCoverageReporter; diff --git a/src/loadCoverage.js b/src/loadCoverage.js new file mode 100644 index 0000000..63fee41 --- /dev/null +++ b/src/loadCoverage.js @@ -0,0 +1,37 @@ +const { Collector } = require('istanbul'); + +const fs = require('fs'); +/** + * Takes sources of coverage information and adds them to a collector which then can be subsequently + * remapped. + * @param {Array|string} sources The source(s) of the JSON coverage information + * @param {Object} options? A hash of options that can be set: + * readJSON?: A function that can read and parse a JSON file + * warn?: A function that logs warning messages + * @return {Object} The loaded coverage object + */ +module.exports = function loadCoverage(sources, options = {}) { + const warn = options.warn || console.warn; + + const readJSON = options.readJSON + || function (filePath) { + if (!fs.existsSync(filePath)) { + warn(new Error(`Cannot find file: "${filePath}"`)); + return {}; + } + return JSON.parse(fs.readFileSync(filePath)); + }; + + if (typeof sources === 'string') { + sources = [sources]; + } + if (!sources.length) { + warn(new SyntaxError('No coverage files supplied!')); + } + const collector = new Collector(); + sources.forEach((filePath) => { + collector.add(readJSON(filePath)); + }); + + return collector.getFinalCoverage(); +}; diff --git a/main.js b/src/main.js similarity index 65% rename from main.js rename to src/main.js index 798614a..967389e 100644 --- a/main.js +++ b/src/main.js @@ -1,9 +1,9 @@ /* jshint node:true */ /* global Promise */ -var loadCoverage = require('./lib/loadCoverage'); -var remap = require('./lib/remap'); -var writeReport = require('./lib/writeReport'); -var MemoryStore = require('istanbul/lib/store/memory'); +const loadCoverage = require('./loadCoverage'); +const remap = require('./remap'); +const writeReport = require('./writeReport'); +const MemoryStore = require('istanbul/lib/store/memory'); /** * The basic API for utilising remap-istanbul @@ -16,20 +16,24 @@ var MemoryStore = require('istanbul/lib/store/memory'); * @return {Promise} A promise that will resolve when all the reports are written. */ module.exports = function (sources, reports, reportOptions) { - var sourceStore = new MemoryStore(); - var collector = remap(loadCoverage(sources), { - sources: sourceStore + let sourceStore = new MemoryStore(); + const collector = remap(loadCoverage(sources), { + sources: sourceStore, }); if (!Object.keys(sourceStore.map).length) { sourceStore = undefined; } - var p = Object.keys(reports).map(function (reportType) { - return writeReport(collector, reportType, reportOptions || {}, reports[reportType], sourceStore); - }); - return Promise.all(p); + + return Promise.all( + Object.keys(reports) + .map(reportType => + writeReport(collector, reportType, reportOptions || {}, reports[reportType], sourceStore) + ) + ); }; + module.exports.loadCoverage = loadCoverage; module.exports.remap = remap; module.exports.writeReport = writeReport; diff --git a/src/remap-istanbul.js b/src/remap-istanbul.js new file mode 100755 index 0000000..e48c15b --- /dev/null +++ b/src/remap-istanbul.js @@ -0,0 +1,164 @@ +#!/usr/bin/env node + +const loadCoverage = require('./loadCoverage'); +const remap = require('./remap'); +const writeReport = require('./writeReport'); +const MemoryStore = require('istanbul/lib/store/memory'); +const Collector = require('istanbul/lib/collector'); + +/** + * Helper function that reads from standard in and resolves a Promise with the + * data or rejects with any errors. + * @return {Promise} A promsie that is resolved with the data from standard in + * or rejected with any errors. + */ +function readStdIn() { + /* istanbul ignore next: too challenging to test for reading from stdin */ + return new Promise((resolve, reject) => { + const stdin = process.stdin; + let buffer = ''; + + stdin.setEncoding('utf8'); + + stdin.on('data', (data) => { + buffer += data; + }); + + stdin.on('error', (e) => { + reject(e); + }); + + stdin.on('end', () => { + resolve(buffer); + }); + + try { + stdin.resume(); + } catch (e) { + reject(e); + } + }); +} + +/** + * The main wrapper to provide a CLI interface to remap-istanbul + * @param {Array} argv An array of arguments passed the process + * @return {Promise} A promise that resolves when the remapping is complete + * or rejects if there is an error. + */ +function main(argv) { + /* jshint maxcomplexity:13 */ + + /** + * Helper function that processes the arguments + * @return {String} The next valid argument + */ + function getArg() { + let arg = argv.shift(); + if (arg && arg.indexOf('--') === 0) { + arg = arg.split('='); + if (arg.length > 1) { + argv.unshift(arg.slice(1).join('=')); + } + arg = arg[0]; + } else if (arg && arg[0] === '-') { + /* istanbul ignore if */ + if (arg.length > 2) { + argv = arg.substring(1).split('') + .map((ch) => '-' + ch) + .concat(argv); + arg = argv.shift(); + } + } + + return arg; + } + + let arg; + const inputFiles = []; + let output; + let reportType; + let basePath; + let exclude; + for (arg = getArg(); arg; arg = getArg()) { + switch (arg) { + case '-i': + case '--input': + inputFiles.push(argv.shift()); + break; + case '-o': + case '--output': + output = argv.shift(); + break; + case '-b': + case '--basePath': + basePath = argv.shift(); + break; + case '-t': + case '--type': + reportType = argv.shift(); + break; + case '-e': + case '--exclude': + exclude = argv.shift(); + if (exclude.indexOf(',') !== -1) { + exclude = new RegExp(exclude.replace(/,/g, '|')); + } + break; + default: + throw new SyntaxError(`Unrecognised argument: "${arg}".`); + } + } + + return new Promise((resolve, reject) => { + const coverage = inputFiles.length ? loadCoverage(inputFiles) : + /* istanbul ignore next */ + readStdIn().then((data) => { + try { + data = JSON.parse(data); + const collector = new Collector(); + collector.add(data); + return collector.getFinalCoverage(); + } catch (err) { + console.error(err.stack); + throw err; + } + }, reject); + + resolve(coverage); + }).then((coverage) => { + let sources = new MemoryStore(); + const collector = remap(coverage, { + sources, + basePath: basePath || undefined, + exclude: exclude || undefined, + }); + if (!Object.keys(sources.map).length) { + sources = undefined; + } + const reportOptions = {}; + if (output) { + return writeReport(collector, reportType || 'json', reportOptions, output, sources); + } + if (reportType && (reportType === 'lcovonly' || reportType === 'text-lcov')) { + return writeReport(collector, 'text-lcov', reportOptions); + } + process.stdout.write(JSON.stringify(collector.getFinalCoverage()) + '\n'); + return null; + }); +} + +/* istanbul ignore if: we use the module interface in testing */ +if (!module.parent) { + process.title = 'remap-istanbul'; + /* first two arguments are meaningless to the process */ + main(process.argv.slice(2)) + .then( + (code) => process.exit(code || 0), + (err) => { + console.log(err.stack); + process.exit(1); + }); +} else { + module.exports = main; +} diff --git a/src/remap.js b/src/remap.js new file mode 100644 index 0000000..075b820 --- /dev/null +++ b/src/remap.js @@ -0,0 +1,37 @@ +const { CoverageTransformer } = require('./CoverageTransformer'); + +/** + * Remaps coverage data based on the source maps it discovers in the + * covered files and returns a coverage Collector that contains the remappped + * data. + * @param {Array|Object} coverage The coverage (or array of coverages) that need to be + * remapped + * @param {Object} options A configuration object: + * basePath? - a string containing to utilise as the base path + * for determining the location of the source file + * exclude? - a string or Regular Expression that filters out + * any coverage where the file path matches + * readFile? - a function that can read a file + * syncronously + * readJSON? - a function that can read and parse a + * JSON file syncronously + * sources? - a Istanbul store where inline sources will be + * added + * warn? - a function that logs warnings + * @return {istanbul/lib/_collector} The remapped collector + */ +function remap(coverage, options = {}) { + const smc = new CoverageTransformer(options); + + if (!Array.isArray(coverage)) { + coverage = [coverage]; + } + + coverage.forEach(item => { + smc.addCoverage(item); + }); + + return smc.getFinalCoverage(); +} + +module.exports = remap; diff --git a/src/remapBranch.js b/src/remapBranch.js new file mode 100644 index 0000000..3e6673b --- /dev/null +++ b/src/remapBranch.js @@ -0,0 +1,28 @@ +function remapBranch(genItem, getMapping) { + const locations = []; + let source; + + for (let i = 0; i < genItem.locations.length; i += 1) { + const mapping = getMapping(genItem.locations[i]); + if (!mapping) { + return null; + } + /* istanbul ignore else: edge case too hard to test for */ + if (!source) { + source = mapping.source; + } else if (source !== mapping.source) { + return null; + } + locations.push(mapping.loc); + } + + const srcItem = { + line: locations[0].start.line, + type: genItem.type, + locations, + }; + + return { source, srcItem }; +} + +module.exports = remapBranch; diff --git a/src/remapFunction.js b/src/remapFunction.js new file mode 100644 index 0000000..2b2f7f9 --- /dev/null +++ b/src/remapFunction.js @@ -0,0 +1,21 @@ +function remapFunction(genItem, getMapping) { + const mapping = getMapping(genItem.loc); + + if (!mapping) { + return null; + } + + const srcItem = { + name: genItem.name, + line: mapping.loc.start.line, + loc: mapping.loc, + }; + + if (genItem.skip) { + srcItem.skip = genItem.skip; + } + + return { srcItem, source: mapping.source }; +} + +module.exports = remapFunction; diff --git a/src/writeReport.js b/src/writeReport.js new file mode 100644 index 0000000..777c5ff --- /dev/null +++ b/src/writeReport.js @@ -0,0 +1,58 @@ +require('istanbul/index'); + +const istanbulReportTypes = { + clover: 'file', + cobertura: 'file', + html: 'directory', + 'json-summary': 'file', + json: 'file', + lcovonly: 'file', + teamcity: 'file', + 'text-lcov': 'console', + 'text-summary': 'file', + text: 'file', +}; + +/** + * Generates an Instanbul Coverage report based on the information passed. + * @param {istanbul/lib/_collector} collector An instance of an coverage + * collector + * @param {string} reportType The name of the report type to + * generate + * @param {object} reportOptions The options to pass to the reporter + * @param {string|function} dest The filename or outputting + * function to use for generating + * the report + * @param {istanbul/lib/store} sources? A store of sources to be passed + * the reporter + * @return {Promise} A promise that resolves when the + * report is complete. + */ +module.exports = function writeReport(collector, reportType, reportOptions, dest, sources) { + return new Promise((resolve, reject) => { + if (!(reportType in istanbulReportTypes)) { + reject(new SyntaxError(`Unrecognized report type of "${reportType}".`)); + return; + } + const Reporter = require(`istanbul/lib/report/${reportType}`); + const options = Object.assign({}, reportOptions); + switch (istanbulReportTypes[reportType]) { + case 'file': + options.file = dest; + break; + case 'directory': + options.dir = dest; + break; + case 'console': + options.log = dest || console.log; + break; + default: + throw new Error('Unknown reporter type'); + } + if (sources) { + options.sourceStore = sources; + } + const reporter = new Reporter(options); + resolve(reporter.writeReport(collector, true)); + }); +}; diff --git a/tasks/remapIstanbul.js b/tasks/remapIstanbul.js index 9d530ec..3c15af0 100644 --- a/tasks/remapIstanbul.js +++ b/tasks/remapIstanbul.js @@ -1,58 +1 @@ -/* jshint node: true */ -/* global Promise */ - -var loadCoverage = require('../lib/loadCoverage'); -var remap = require('../lib/remap'); -var writeReport = require('../lib/writeReport'); -var MemoryStore = require('istanbul/lib/store/memory'); - -module.exports = function (grunt) { - grunt.registerMultiTask('remapIstanbul', function () { - var done = this.async(); - var options = this.options(); - var sources = new MemoryStore(); - var p = []; - - function warn(message) { - if (options.fail) { - grunt.fail.warn(message); - } - else { - grunt.log.error(message); - } - } - - this.files.forEach(function (file) { - - var coverage = remap(loadCoverage(file.src, { - readJSON: grunt.readJSON, - warn: warn - }), { - readFile: grunt.readFile, - readJSON: grunt.readJSON, - warn: warn, - sources: sources, - basePath: file.basePath, - useAbsolutePaths: options.useAbsolutePaths, - exclude: options.exclude - }); - - if (!Object.keys(sources.map).length) { - sources = undefined; - } - - if (file.type && file.dest) { - p.push(writeReport(coverage, file.type, {}, file.dest, sources)); - } - else { - p = p.concat(Object.keys(options.reports).map(function (key) { - return writeReport(coverage, key, options.reportOpts || {}, options.reports[key], sources); - })); - } - }); - - Promise.all(p).then(function() { - done(); - }, grunt.fail.fatal); - }); -}; +module.exports = require('../lib/gruntRemapIstanbul'); diff --git a/tests/intern.js b/tests/intern.js index 23e6144..3e75216 100644 --- a/tests/intern.js +++ b/tests/intern.js @@ -5,7 +5,7 @@ define({ ] }, - suites: [ 'remap-istanbul/tests/unit/all' ], + suites: ['remap-istanbul/tests/unit/all'], excludeInstrumentation: /^(?:tests|node_modules)\// }); diff --git a/lib/node.js b/tests/node.js similarity index 100% rename from lib/node.js rename to tests/node.js diff --git a/tests/unit/all.js b/tests/unit/all.js index 877b187..c6baac6 100644 --- a/tests/unit/all.js +++ b/tests/unit/all.js @@ -1,10 +1,10 @@ define([ - './main', - './bin/remap-istanbul', + './lib/main', + './lib/remap-istanbul', './lib/gulpRemapIstanbul', './lib/loadCoverage', './lib/remap', './lib/writeReport', './lib/intern-reporters/JsonCoverage', - './tasks/remapIstanbul' + './lib/gruntRemapIstanbul' ], function () {}); diff --git a/tests/unit/tasks/remapIstanbul.js b/tests/unit/lib/gruntRemapIstanbul.js similarity index 93% rename from tests/unit/tasks/remapIstanbul.js rename to tests/unit/lib/gruntRemapIstanbul.js index 93c4c8f..aa20ef9 100644 --- a/tests/unit/tasks/remapIstanbul.js +++ b/tests/unit/lib/gruntRemapIstanbul.js @@ -1,9 +1,10 @@ define([ 'intern!object', 'intern/chai!assert', - '../../../lib/node!grunt', - '../../../lib/node!fs' -], function (registerSuite, assert, grunt, fs) { + '../../node!grunt', + '../../node!fs', + '../../node!../../../src/gruntRemapIstanbul' +], function (registerSuite, assert, grunt, fs, gruntPlugin) { /* creating a mock for logging */ var logStack = []; @@ -24,7 +25,7 @@ define([ } registerSuite({ - name: 'tasks/remapIstanbul', + name: 'src/gruntRemapIstanbul', setup: function () { grunt.initConfig({ remapIstanbul: { @@ -71,7 +72,7 @@ define([ }, src: 'tests/unit/support/coverage-import.json' }, - + nonTransFail: { options: { fail: true, @@ -84,7 +85,7 @@ define([ } }); - grunt.loadTasks('tasks'); + gruntPlugin(grunt); }, 'basic': function () { diff --git a/tests/unit/lib/gulpRemapIstanbul.js b/tests/unit/lib/gulpRemapIstanbul.js index 87edac0..5cfd9f5 100644 --- a/tests/unit/lib/gulpRemapIstanbul.js +++ b/tests/unit/lib/gulpRemapIstanbul.js @@ -3,7 +3,7 @@ define([ 'intern/chai!assert', 'intern/dojo/node!gulp', 'intern/dojo/node!fs', - 'intern/dojo/node!../../../lib/gulpRemapIstanbul' + 'intern/dojo/node!../../../src/gulpRemapIstanbul' ], function (registerSuite, assert, gulp, fs, remapIstanbul) { registerSuite({ name: 'lib/gulpRemapIstanbul', @@ -64,7 +64,7 @@ define([ gulp.start('assertions'); }, - + 'non-transpiled coverage': { 'warn': function () { var dfd = this.async(); diff --git a/tests/unit/lib/intern-reporters/JsonCoverage.js b/tests/unit/lib/intern-reporters/JsonCoverage.js index 93cce8d..6b87c1e 100644 --- a/tests/unit/lib/intern-reporters/JsonCoverage.js +++ b/tests/unit/lib/intern-reporters/JsonCoverage.js @@ -5,7 +5,7 @@ define([ 'intern/dojo/node!istanbul/lib/report/json', 'intern/dojo/node!fs', './support/mocks', - '../../../../lib/intern-reporters/JsonCoverage' + '../../../node!../../../../src/intern-reporters/JsonCoverage' ], function (registerSuite, assert, Collector, Reporter, fs, mock, JsonCoverage) { var sessionId = 'foo'; diff --git a/tests/unit/lib/loadCoverage.js b/tests/unit/lib/loadCoverage.js index 06830e2..c9eef87 100644 --- a/tests/unit/lib/loadCoverage.js +++ b/tests/unit/lib/loadCoverage.js @@ -1,7 +1,7 @@ define([ 'intern!object', 'intern/chai!assert', - '../../../lib/loadCoverage' + '../../node!../../../src/loadCoverage' ], function (registerSuite, assert, loadCoverage) { registerSuite({ name: 'remap-istanbul/lib/loadCoverage', diff --git a/tests/unit/main.js b/tests/unit/lib/main.js similarity index 96% rename from tests/unit/main.js rename to tests/unit/lib/main.js index 1079e12..1f5efb7 100644 --- a/tests/unit/main.js +++ b/tests/unit/lib/main.js @@ -1,8 +1,8 @@ define([ 'intern!object', 'intern/chai!assert', - '../../../lib/node!fs', - '../../../lib/node!../../../main' + '../../node!fs', + '../../node!../../../src/main' ], function (registerSuite, assert, fs, main) { registerSuite({ name: 'main', diff --git a/tests/unit/bin/remap-istanbul.js b/tests/unit/lib/remap-istanbul.js similarity index 96% rename from tests/unit/bin/remap-istanbul.js rename to tests/unit/lib/remap-istanbul.js index 406cd88..047b243 100644 --- a/tests/unit/bin/remap-istanbul.js +++ b/tests/unit/lib/remap-istanbul.js @@ -1,8 +1,8 @@ define([ 'intern!object', 'intern/chai!assert', - '../../../lib/node!fs', - '../../../lib/node!../../../bin/remap-istanbul' + '../../node!fs', + '../../node!../../../src/remap-istanbul' ], function (registerSuite, assert, fs, remapIstanbul) { registerSuite({ name: 'bin/remapIstanbul', diff --git a/tests/unit/lib/remap.js b/tests/unit/lib/remap.js index 923d954..3891b72 100644 --- a/tests/unit/lib/remap.js +++ b/tests/unit/lib/remap.js @@ -2,10 +2,10 @@ define([ 'intern!object', 'intern/chai!assert', 'intern/dojo/node!path', - '../../../lib/node!istanbul/lib/collector', - '../../../lib/node!istanbul/lib/store/memory', - '../../../lib/loadCoverage', - '../../../lib/remap' + '../../node!istanbul/lib/collector', + '../../node!istanbul/lib/store/memory', + '../../node!../../../src/loadCoverage', + '../../node!../../../src/remap' ], function (registerSuite, assert, path, Collector, MemoryStore, loadCoverage, remap) { registerSuite({ name: 'remap-istanbul/lib/remap', diff --git a/tests/unit/lib/writeReport.js b/tests/unit/lib/writeReport.js index 59ca37d..6a00cc6 100644 --- a/tests/unit/lib/writeReport.js +++ b/tests/unit/lib/writeReport.js @@ -1,11 +1,11 @@ define([ 'intern!object', 'intern/chai!assert', - '../../../lib/node!fs', - '../../../lib/node!istanbul/lib/store/memory', - '../../../lib/loadCoverage', - '../../../lib/remap', - '../../../lib/writeReport' + '../../node!fs', + '../../node!istanbul/lib/store/memory', + '../../node!../../../src/loadCoverage', + '../../node!../../../src/remap', + '../../node!../../../src/writeReport' ], function (registerSuite, assert, fs, MemoryStore, loadCoverage, remap, writeReport) { var coverage; var consoleLog;