From 2eb631e460eba3d06925ee1d128e0db82ec50b6c Mon Sep 17 00:00:00 2001 From: Joseph Mordetsky Date: Thu, 28 Nov 2019 12:31:44 -0500 Subject: [PATCH] feat: adds --all functionality (#158) --- .gitignore | 1 + README.md | 10 ++++ lib/commands/report.js | 3 +- lib/parse-args.js | 6 ++ lib/report.js | 55 ++++++++++++++++--- lib/source-map-from-file.js | 25 +++++++++ package-lock.json | 2 +- package.json | 2 +- test/fixtures/all/ts-compiled/dir/unloaded.js | 8 +++ .../all/ts-compiled/dir/unloaded.js.map | 1 + test/fixtures/all/ts-compiled/dir/unloaded.ts | 5 ++ test/fixtures/all/ts-compiled/loaded.js | 23 ++++++++ test/fixtures/all/ts-compiled/loaded.js.map | 1 + test/fixtures/all/ts-compiled/loaded.ts | 19 +++++++ test/fixtures/all/ts-compiled/main.js | 7 +++ test/fixtures/all/ts-compiled/main.js.map | 1 + test/fixtures/all/ts-compiled/main.ts | 4 ++ test/fixtures/all/ts-only/dir/unloaded.ts | 5 ++ test/fixtures/all/ts-only/loaded.ts | 19 +++++++ test/fixtures/all/ts-only/main.ts | 4 ++ test/fixtures/all/vanilla/dir/unloaded.js | 5 ++ test/fixtures/all/vanilla/loaded.js | 19 +++++++ test/fixtures/all/vanilla/main.js | 4 ++ test/integration.js | 43 +++++++++++++++ test/integration.js.snap | 51 +++++++++++++++++ 25 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 lib/source-map-from-file.js create mode 100644 test/fixtures/all/ts-compiled/dir/unloaded.js create mode 100644 test/fixtures/all/ts-compiled/dir/unloaded.js.map create mode 100644 test/fixtures/all/ts-compiled/dir/unloaded.ts create mode 100644 test/fixtures/all/ts-compiled/loaded.js create mode 100644 test/fixtures/all/ts-compiled/loaded.js.map create mode 100644 test/fixtures/all/ts-compiled/loaded.ts create mode 100644 test/fixtures/all/ts-compiled/main.js create mode 100644 test/fixtures/all/ts-compiled/main.js.map create mode 100644 test/fixtures/all/ts-compiled/main.ts create mode 100644 test/fixtures/all/ts-only/dir/unloaded.ts create mode 100644 test/fixtures/all/ts-only/loaded.ts create mode 100644 test/fixtures/all/ts-only/main.ts create mode 100644 test/fixtures/all/vanilla/dir/unloaded.js create mode 100644 test/fixtures/all/vanilla/loaded.js create mode 100644 test/fixtures/all/vanilla/main.js diff --git a/.gitignore b/.gitignore index 20cf3c43..e96120f7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules .nyc_output coverage tmp +.idea \ No newline at end of file diff --git a/README.md b/README.md index f0dcc709..f3310f28 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,16 @@ c8 node foo.js The above example will output coverage metrics for `foo.js`. +## Checking for "full" source coverage using `--all` + +By default v8 will only give us coverage for files that were loaded by the engine. If there are source files in your +project that are flexed in production but not in your tests, your coverage numbers will not reflect this. For example, +if your project's `main.js` loads `a.js` and `b.js` but your unit tests only load `a.js` your total coverage +could show as `100%` for `a.js` when in fact both `main.js` and `b.js` are uncovered. + +By supplying `--all` to c8, all files in `cwd` that pass the `--include` and `--exclude` flag checks, will be loaded into the +report. If any of those files remain uncovered they will be factored into the report with a default of 0% coverage. + ## c8 report run `c8 report` to regenerate reports after `c8` has already been run. diff --git a/lib/commands/report.js b/lib/commands/report.js index cdc363b6..c02f4e94 100644 --- a/lib/commands/report.js +++ b/lib/commands/report.js @@ -19,7 +19,8 @@ exports.outputReport = async function (argv) { watermarks: argv.watermarks, resolve: argv.resolve, omitRelative: argv.omitRelative, - wrapperLength: argv.wrapperLength + wrapperLength: argv.wrapperLength, + all: argv.all }) await report.run() if (argv.checkCoverage) checkCoverages(argv, report) diff --git a/lib/parse-args.js b/lib/parse-args.js index 7be226ac..ee57b6c0 100644 --- a/lib/parse-args.js +++ b/lib/parse-args.js @@ -83,6 +83,12 @@ function buildYargs (withCommands = false) { type: 'boolean', describe: 'should temp files be deleted before script execution' }) + .options('all', { + default: false, + type: 'boolean', + describe: 'supplying --all will cause c8 to consider all src files in the current working directory ' + + 'when the determining coverage. Respects include/exclude.' + }) .pkgConf('c8') .config(config) .demandCommand(1) diff --git a/lib/report.js b/lib/report.js index 7657f065..cc325965 100644 --- a/lib/report.js +++ b/lib/report.js @@ -3,8 +3,9 @@ const furi = require('furi') const libCoverage = require('istanbul-lib-coverage') const libReport = require('istanbul-lib-report') const reports = require('istanbul-reports') -const { readdirSync, readFileSync } = require('fs') -const { isAbsolute, resolve } = require('path') +const { readdirSync, readFileSync, statSync } = require('fs') +const { isAbsolute, resolve, extname } = require('path') +const getSourceMapFromFile = require('./source-map-from-file') // TODO: switch back to @c88/v8-coverage once patch is landed. const v8toIstanbul = require('v8-to-istanbul') const isCjsEsmBridgeCov = require('./is-cjs-esm-bridge') @@ -19,7 +20,8 @@ class Report { watermarks, omitRelative, wrapperLength, - resolve: resolvePaths + resolve: resolvePaths, + all }) { this.reporter = reporter this.reportsDirectory = reportsDirectory @@ -33,6 +35,8 @@ class Report { this.omitRelative = omitRelative this.sourceMapCache = {} this.wrapperLength = wrapperLength + this.all = all + this.src = process.cwd() } async run () { @@ -56,8 +60,8 @@ class Report { // use-case. if (this._allCoverageFiles) return this._allCoverageFiles + const map = libCoverage.createCoverageMap() const v8ProcessCov = this._getMergedProcessCov() - const map = libCoverage.createCoverageMap({}) const resultCountPerPath = new Map() const possibleCjsEsmBridges = new Map() @@ -94,7 +98,6 @@ class Report { map.merge(converter.toIstanbul()) } } - this._allCoverageFiles = map return this._allCoverageFiles } @@ -139,14 +142,50 @@ class Report { _getMergedProcessCov () { const { mergeProcessCovs } = require('@bcoe/v8-coverage') const v8ProcessCovs = [] + const fileIndex = new Set() // Set for (const v8ProcessCov of this._loadReports()) { if (this._isCoverageObject(v8ProcessCov)) { if (v8ProcessCov['source-map-cache']) { Object.assign(this.sourceMapCache, v8ProcessCov['source-map-cache']) } - v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov)) + v8ProcessCovs.push(this._normalizeProcessCov(v8ProcessCov, fileIndex)) } } + + if (this.all) { + const emptyReports = [] + v8ProcessCovs.unshift({ + result: emptyReports + }) + const workingDir = process.cwd() + this.exclude.globSync(workingDir).forEach((f) => { + const fullPath = resolve(workingDir, f) + if (!fileIndex.has(fullPath)) { + const ext = extname(f) + if (ext === '.js' || ext === '.ts' || ext === '.mjs') { + const stat = statSync(f) + const sourceMap = getSourceMapFromFile(f) + if (sourceMap !== undefined) { + this.sourceMapCache[`file://${fullPath}`] = { data: JSON.parse(readFileSync(sourceMap).toString()) } + } + emptyReports.push({ + scriptId: 0, + url: resolve(f), + functions: [{ + functionName: '(empty-report)', + ranges: [{ + startOffset: 0, + endOffset: stat.size, + count: 0 + }], + isBlockCoverage: true + }] + }) + } + } + }) + } + return mergeProcessCovs(v8ProcessCovs) } @@ -193,15 +232,17 @@ class Report { * There is no deep cloning. * * @param v8ProcessCov V8 process coverage to normalize. + * @param fileIndex a Set of paths discovered in coverage * @return {v8ProcessCov} Normalized V8 process coverage. * @private */ - _normalizeProcessCov (v8ProcessCov) { + _normalizeProcessCov (v8ProcessCov, fileIndex) { const result = [] for (const v8ScriptCov of v8ProcessCov.result) { if (/^file:\/\//.test(v8ScriptCov.url)) { try { v8ScriptCov.url = furi.toSysPath(v8ScriptCov.url) + fileIndex.add(v8ScriptCov.url) } catch (err) { console.warn(err) continue diff --git a/lib/source-map-from-file.js b/lib/source-map-from-file.js new file mode 100644 index 00000000..8bc97114 --- /dev/null +++ b/lib/source-map-from-file.js @@ -0,0 +1,25 @@ +const { isAbsolute, join, dirname } = require('path') +const { readFileSync } = require('fs') +/** + * Extract the sourcemap url from a source file + * reference: https://sourcemaps.info/spec.html + * @param {String} file - compilation target file + * @returns {String} full path to source map file + * @private + */ +function getSourceMapFromFile (file) { + const fileBody = readFileSync(file).toString() + const sourceMapLineRE = /\/\/[#@] ?sourceMappingURL=([^\s'"]+)\s*$/mg + const results = fileBody.match(sourceMapLineRE) + if (results !== null) { + const sourceMap = results[results.length - 1].split('=')[1] + if (isAbsolute(sourceMap)) { + return sourceMap + } else { + const base = dirname(file) + return join(base, sourceMap) + } + } +} + +module.exports = getSourceMapFromFile diff --git a/package-lock.json b/package-lock.json index cb9922bc..2291cc2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3389,4 +3389,4 @@ "dev": true } } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 2d2709db..390e4997 100644 --- a/package.json +++ b/package.json @@ -61,4 +61,4 @@ "bin", "LICENSE" ] -} +} \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/dir/unloaded.js b/test/fixtures/all/ts-compiled/dir/unloaded.js new file mode 100644 index 00000000..097235bd --- /dev/null +++ b/test/fixtures/all/ts-compiled/dir/unloaded.js @@ -0,0 +1,8 @@ +"use strict"; +exports.__esModule = true; +function Unloaded() { + return 'Never loaded :('; +} +exports["default"] = Unloaded; +console.log("This file shouldn't have been evaluated"); +//# sourceMappingURL=unloaded.js.map \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/dir/unloaded.js.map b/test/fixtures/all/ts-compiled/dir/unloaded.js.map new file mode 100644 index 00000000..9f40d2ca --- /dev/null +++ b/test/fixtures/all/ts-compiled/dir/unloaded.js.map @@ -0,0 +1 @@ +{"version":3,"file":"unloaded.js","sourceRoot":"","sources":["unloaded.ts"],"names":[],"mappings":";;AAAA,SAAwB,QAAQ;IAC9B,OAAO,iBAAiB,CAAA;AAC1B,CAAC;AAFD,8BAEC;AAED,OAAO,CAAC,GAAG,CAAC,yCAAyC,CAAC,CAAA"} \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/dir/unloaded.ts b/test/fixtures/all/ts-compiled/dir/unloaded.ts new file mode 100644 index 00000000..c2a4e2ba --- /dev/null +++ b/test/fixtures/all/ts-compiled/dir/unloaded.ts @@ -0,0 +1,5 @@ +export default function Unloaded(){ + return 'Never loaded :(' +} + +console.log("This file shouldn't have been evaluated") \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/loaded.js b/test/fixtures/all/ts-compiled/loaded.js new file mode 100644 index 00000000..db8ceaac --- /dev/null +++ b/test/fixtures/all/ts-compiled/loaded.js @@ -0,0 +1,23 @@ +"use strict"; +exports.__esModule = true; +function getString(i) { + if (typeof i === 'number') { + if (isNaN(i)) { + return 'NaN'; + } + else if (i === 0) { + return 'zero'; + } + else if (i > 0) { + return 'positive'; + } + else { + return 'negative'; + } + } + else { + return 'wat?'; + } +} +exports["default"] = getString; +//# sourceMappingURL=loaded.js.map \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/loaded.js.map b/test/fixtures/all/ts-compiled/loaded.js.map new file mode 100644 index 00000000..3ce3fd1f --- /dev/null +++ b/test/fixtures/all/ts-compiled/loaded.js.map @@ -0,0 +1 @@ +{"version":3,"file":"loaded.js","sourceRoot":"","sources":["loaded.ts"],"names":[],"mappings":";;AAAA,SAAwB,SAAS,CAAC,CAAC;IACjC,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAC;QACxB,IAAI,KAAK,CAAC,CAAC,CAAC,EAAC;YACX,OAAO,KAAK,CAAA;SACb;aACI,IAAI,CAAC,KAAK,CAAC,EAAC;YACf,OAAO,MAAM,CAAA;SACd;aACI,IAAI,CAAC,GAAG,CAAC,EAAC;YACb,OAAO,UAAU,CAAA;SAClB;aACI;YACH,OAAO,UAAU,CAAA;SAClB;KACF;SACI;QACH,OAAO,MAAM,CAAA;KACd;AACH,CAAC;AAlBD,+BAkBC"} \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/loaded.ts b/test/fixtures/all/ts-compiled/loaded.ts new file mode 100644 index 00000000..83f45893 --- /dev/null +++ b/test/fixtures/all/ts-compiled/loaded.ts @@ -0,0 +1,19 @@ +export default function getString(i){ + if (typeof i === 'number'){ + if (isNaN(i)){ + return 'NaN' + } + else if (i === 0){ + return 'zero' + } + else if (i > 0){ + return 'positive' + } + else { + return 'negative' + } + } + else { + return 'wat?' + } +} \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/main.js b/test/fixtures/all/ts-compiled/main.js new file mode 100644 index 00000000..2e0c0852 --- /dev/null +++ b/test/fixtures/all/ts-compiled/main.js @@ -0,0 +1,7 @@ +"use strict"; +exports.__esModule = true; +var loaded_1 = require("./loaded"); +console.log(loaded_1["default"](0)); +console.log(loaded_1["default"](1)); +console.log(loaded_1["default"](-1)); +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/main.js.map b/test/fixtures/all/ts-compiled/main.js.map new file mode 100644 index 00000000..7e2b0cdb --- /dev/null +++ b/test/fixtures/all/ts-compiled/main.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.js","sourceRoot":"","sources":["main.ts"],"names":[],"mappings":";;AAAA,mCAAgC;AAChC,OAAO,CAAC,GAAG,CAAC,mBAAS,CAAC,CAAC,CAAC,CAAC,CAAA;AACzB,OAAO,CAAC,GAAG,CAAC,mBAAS,CAAC,CAAC,CAAC,CAAC,CAAA;AACzB,OAAO,CAAC,GAAG,CAAC,mBAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA"} \ No newline at end of file diff --git a/test/fixtures/all/ts-compiled/main.ts b/test/fixtures/all/ts-compiled/main.ts new file mode 100644 index 00000000..6677c2e0 --- /dev/null +++ b/test/fixtures/all/ts-compiled/main.ts @@ -0,0 +1,4 @@ +import getString from "./loaded" +console.log(getString(0)) +console.log(getString(1)) +console.log(getString(-1)) \ No newline at end of file diff --git a/test/fixtures/all/ts-only/dir/unloaded.ts b/test/fixtures/all/ts-only/dir/unloaded.ts new file mode 100644 index 00000000..c2a4e2ba --- /dev/null +++ b/test/fixtures/all/ts-only/dir/unloaded.ts @@ -0,0 +1,5 @@ +export default function Unloaded(){ + return 'Never loaded :(' +} + +console.log("This file shouldn't have been evaluated") \ No newline at end of file diff --git a/test/fixtures/all/ts-only/loaded.ts b/test/fixtures/all/ts-only/loaded.ts new file mode 100644 index 00000000..83f45893 --- /dev/null +++ b/test/fixtures/all/ts-only/loaded.ts @@ -0,0 +1,19 @@ +export default function getString(i){ + if (typeof i === 'number'){ + if (isNaN(i)){ + return 'NaN' + } + else if (i === 0){ + return 'zero' + } + else if (i > 0){ + return 'positive' + } + else { + return 'negative' + } + } + else { + return 'wat?' + } +} \ No newline at end of file diff --git a/test/fixtures/all/ts-only/main.ts b/test/fixtures/all/ts-only/main.ts new file mode 100644 index 00000000..6677c2e0 --- /dev/null +++ b/test/fixtures/all/ts-only/main.ts @@ -0,0 +1,4 @@ +import getString from "./loaded" +console.log(getString(0)) +console.log(getString(1)) +console.log(getString(-1)) \ No newline at end of file diff --git a/test/fixtures/all/vanilla/dir/unloaded.js b/test/fixtures/all/vanilla/dir/unloaded.js new file mode 100644 index 00000000..9baeca2e --- /dev/null +++ b/test/fixtures/all/vanilla/dir/unloaded.js @@ -0,0 +1,5 @@ +module.exports = function Unloaded(){ + return 'Never loaded :(' +} + +console.log("This file shouldn't have been evaluated") \ No newline at end of file diff --git a/test/fixtures/all/vanilla/loaded.js b/test/fixtures/all/vanilla/loaded.js new file mode 100644 index 00000000..ca9eb7f8 --- /dev/null +++ b/test/fixtures/all/vanilla/loaded.js @@ -0,0 +1,19 @@ +module.exports = function getString(i){ + if (typeof i === 'number'){ + if (isNaN(i)){ + return 'NaN' + } + else if (i === 0){ + return 'zero' + } + else if (i > 0){ + return 'positive' + } + else { + return 'negative' + } + } + else { + return 'wat?' + } +} \ No newline at end of file diff --git a/test/fixtures/all/vanilla/main.js b/test/fixtures/all/vanilla/main.js new file mode 100644 index 00000000..72b77148 --- /dev/null +++ b/test/fixtures/all/vanilla/main.js @@ -0,0 +1,4 @@ +const loaded = require('./loaded.js'); +console.log(loaded(0)) +console.log(loaded(1)) +console.log(loaded(-1)) \ No newline at end of file diff --git a/test/integration.js b/test/integration.js index 7caaf3c1..6b5536d6 100644 --- a/test/integration.js +++ b/test/integration.js @@ -327,4 +327,47 @@ describe('c8', () => { output.toString('utf8').should.matchSnapshot() }) }) + describe('--all', () => { + it('reports coverage for unloaded js files as 0 for line, branch and function', () => { + const { output } = spawnSync(nodePath, [ + c8Path, + '--temp-directory=tmp/vanilla-all', + '--clean=false', + '--all=true', + '--include=test/fixtures/all/vanilla/**/*.js', + '--exclude=**/*.ts', // add an exclude to avoid default excludes of test/** + nodePath, + require.resolve('./fixtures/all/vanilla/main') + ]) + output.toString('utf8').should.matchSnapshot() + }) + + it('reports coverage for unloaded transpiled ts files as 0 for line, branch and function', () => { + const { output } = spawnSync(nodePath, [ + c8Path, + '--temp-directory=tmp/all-ts', + '--clean=false', + '--all=true', + '--include=test/fixtures/all/ts-compiled/**/*.js', + '--exclude="test/*.js"', // add an exclude to avoid default excludes of test/** + nodePath, + require.resolve('./fixtures/all/ts-compiled/main.js') + ]) + output.toString('utf8').should.matchSnapshot() + }) + + it('reports coverage for unloaded ts files as 0 for line, branch and function when using ts-node', () => { + const { output } = spawnSync(nodePath, [ + c8Path, + '--temp-directory=tmp/all-ts-node', + '--clean=false', + '--all=true', + '--include=test/fixtures/all/ts-only/**/*.ts', + '--exclude="test/*.js"', // add an exclude to avoid default excludes of test/** + './node_modules/.bin/ts-node', + require.resolve('./fixtures/all/ts-only/main.ts') + ]) + output.toString('utf8').should.matchSnapshot() + }) + }) }) diff --git a/test/integration.js.snap b/test/integration.js.snap index b623cec5..33aeb563 100644 --- a/test/integration.js.snap +++ b/test/integration.js.snap @@ -14,6 +14,57 @@ All files | 86.21 | 91.67 | 66.67 | 86.21 | ," `; +exports[`c8 --all reports coverage for unloaded js files as 0 for line, branch and function 1`] = ` +",zero +positive +negative +--------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +--------------|----------|----------|----------|----------|-------------------| +All files | 64.29 | 66.67 | 50 | 64.29 | | + vanilla | 78.26 | 75 | 100 | 78.26 | | + loaded.js | 73.68 | 71.43 | 100 | 73.68 | 4,5,16,17,18 | + main.js | 100 | 100 | 100 | 100 | | + vanilla/dir | 0 | 0 | 0 | 0 | | + unloaded.js | 0 | 0 | 0 | 0 | 1,2,3,4,5 | +--------------|----------|----------|----------|----------|-------------------| +," +`; + +exports[`c8 --all reports coverage for unloaded transpiled ts files as 0 for line, branch and function 1`] = ` +",zero +positive +negative +-----------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +-----------------|----------|----------|----------|----------|-------------------| +All files | 64.29 | 66.67 | 50 | 64.29 | | + ts-compiled | 78.26 | 75 | 100 | 78.26 | | + loaded.ts | 73.68 | 71.43 | 100 | 73.68 | 4,5,16,17,18 | + main.ts | 100 | 100 | 100 | 100 | | + ts-compiled/dir | 0 | 0 | 0 | 0 | | + unloaded.ts | 0 | 0 | 0 | 0 | 1,2,3,4,5 | +-----------------|----------|----------|----------|----------|-------------------| +," +`; + +exports[`c8 --all reports coverage for unloaded ts files as 0 for line, branch and function when using ts-node 1`] = ` +",zero +positive +negative +--------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +--------------|----------|----------|----------|----------|-------------------| +All files | 64.29 | 66.67 | 50 | 64.29 | | + ts-only | 78.26 | 75 | 100 | 78.26 | | + loaded.ts | 73.68 | 71.43 | 100 | 73.68 | 4,5,16,17,18 | + main.ts | 100 | 100 | 100 | 100 | | + ts-only/dir | 0 | 0 | 0 | 0 | | + unloaded.ts | 0 | 0 | 0 | 0 | 1,2,3,4,5 | +--------------|----------|----------|----------|----------|-------------------| +," +`; + exports[`c8 ESM Modules collects coverage for ESM modules 1`] = ` ",bar foo ------------|----------|----------|----------|----------|-------------------|