diff --git a/.circleci/config.yml b/.circleci/config.yml index 3956cd2e..36f96851 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -369,6 +369,7 @@ workflows: store_artifacts: true post-steps: - run: cat examples/exclude-files/.nyc_output/out.json + - run: cat examples/exclude-files/coverage/coverage-final.json # store the created coverage report folder # you can click on it in the CircleCI UI # to see live static HTML site @@ -380,9 +381,11 @@ workflows: working_directory: examples/exclude-files - run: name: Check code coverage 📈 + # we will check the final coverage report + # to make sure it only has files we are interested in command: | ../../node_modules/.bin/check-coverage main.js - ../../node_modules/.bin/only-covered main.js + ../../node_modules/.bin/only-covered --from coverage/coverage-final.json main.js working_directory: examples/exclude-files - cypress/run: diff --git a/.nycrc.json b/.nycrc.json new file mode 100644 index 00000000..6f8691fd --- /dev/null +++ b/.nycrc.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + "support-utils.js", + "task-utils.js" + ] +} diff --git a/cypress/integration/combine-spec.js b/cypress/integration/combine-spec.js new file mode 100644 index 00000000..9c05d1a6 --- /dev/null +++ b/cypress/integration/combine-spec.js @@ -0,0 +1,66 @@ +const { combineNycOptions, defaultNycOptions } = require('../../task-utils') +describe('Combine NYC options', () => { + it('overrides defaults', () => { + const pkgNycOptions = { + extends: '@istanbuljs/nyc-config-typescript', + all: true + } + const combined = combineNycOptions({ + pkgNycOptions, + defaultNycOptions + }) + cy.wrap(combined).should('deep.equal', { + extends: '@istanbuljs/nyc-config-typescript', + all: true, + 'report-dir': './coverage', + reporter: ['lcov', 'clover', 'json'], + extension: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'], + excludeAfterRemap: true + }) + }) + + it('allows to specify reporter, but changes to array', () => { + const pkgNycOptions = { + reporter: 'text' + } + const combined = combineNycOptions({ + pkgNycOptions, + defaultNycOptions + }) + cy.wrap(combined).should('deep.equal', { + 'report-dir': './coverage', + reporter: ['text'], + extension: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'], + excludeAfterRemap: true + }) + }) + + it('combines multiple options', () => { + const pkgNycOptions = { + all: true, + extension: '.js' + } + const nycrc = { + include: ['foo.js'] + } + const nycrcJson = { + exclude: ['bar.js'], + reporter: ['json'] + } + const combined = combineNycOptions({ + pkgNycOptions, + nycrc, + nycrcJson, + defaultNycOptions + }) + cy.wrap(combined).should('deep.equal', { + all: true, + 'report-dir': './coverage', + reporter: ['json'], + extension: ['.js'], + excludeAfterRemap: true, + include: ['foo.js'], + exclude: ['bar.js'] + }) + }) +}) diff --git a/package-lock.json b/package-lock.json index af4a9ca9..ff483e70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4924,12 +4924,21 @@ } }, "check-code-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/check-code-coverage/-/check-code-coverage-1.0.1.tgz", - "integrity": "sha512-gQ61+sUoChj5krJoIi2CYWqrnLrol7VVRV5XksslabXfX4tle9KqARVuL6NqbcKSa2yQ1eN2kloKDwmib8ut9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/check-code-coverage/-/check-code-coverage-1.1.0.tgz", + "integrity": "sha512-l9FJyUN2S6+tP2AjaMWg+DqwJZCcSs8NbtOCxGIYQ85w8RR3O/0zi7jq/T0irGnVNcVdxIHJU5sHEI4f0KjxDA==", "dev": true, "requires": { + "arg": "4.1.3", "lodash": "4.17.15" + }, + "dependencies": { + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + } } }, "check-more-types": { diff --git a/package.json b/package.json index 6303a497..17fb3f41 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@cypress/webpack-preprocessor": "5.1.2", "babel-loader": "8.1.0", "babel-plugin-istanbul": "6.0.0", - "check-code-coverage": "1.0.1", + "check-code-coverage": "1.1.0", "console-log-div": "0.6.3", "cypress": "4.4.0", "express": "4.17.1", @@ -72,10 +72,5 @@ "typescript": "3.8.3", "webpack": "4.42.1", "webpack-cli": "3.3.11" - }, - "nyc": { - "exclude": [ - "utils.js" - ] } } diff --git a/task-utils.js b/task-utils.js index 1030fceb..289af83b 100644 --- a/task-utils.js +++ b/task-utils.js @@ -7,6 +7,66 @@ const { readFileSync, writeFileSync, existsSync } = require('fs') const { isAbsolute, resolve, join } = require('path') const debug = require('debug')('code-coverage') +function combineNycOptions({ + pkgNycOptions, + nycrc, + nycrcJson, + defaultNycOptions +}) { + // last option wins + const nycOptions = Object.assign( + {}, + defaultNycOptions, + nycrc, + nycrcJson, + pkgNycOptions + ) + + if (typeof nycOptions.reporter === 'string') { + nycOptions.reporter = [nycOptions.reporter] + } + if (typeof nycOptions.extension === 'string') { + nycOptions.extension = [nycOptions.extension] + } + + return nycOptions +} + +const defaultNycOptions = { + 'report-dir': './coverage', + reporter: ['lcov', 'clover', 'json'], + extension: ['.js', '.cjs', '.mjs', '.ts', '.tsx', '.jsx'], + excludeAfterRemap: true +} + +function readNycOptions(workingDirectory) { + const pkgFilename = join(workingDirectory, 'package.json') + const pkg = existsSync(pkgFilename) + ? JSON.parse(readFileSync(pkgFilename, 'utf8')) + : {} + const pkgNycOptions = pkg.nyc || {} + + const nycrcFilename = join(workingDirectory, '.nycrc') + const nycrc = existsSync(nycrcFilename) + ? JSON.parse(readFileSync(nycrcFilename, 'utf8')) + : {} + + const nycrcJsonFilename = join(workingDirectory, '.nycrc.json') + const nycrcJson = existsSync(nycrcJsonFilename) + ? JSON.parse(readFileSync(nycrcJsonFilename, 'utf8')) + : {} + + const nycOptions = combineNycOptions({ + pkgNycOptions, + nycrc, + nycrcJson, + defaultNycOptions + }) + debug('combined NYC options %o', nycOptions) + + return nycOptions +} + function checkAllPathsNotFound(nycFilename) { const nycCoverage = JSON.parse(readFileSync(nycFilename, 'utf8')) @@ -198,5 +258,8 @@ module.exports = { showNycInfo, resolveRelativePaths, checkAllPathsNotFound, - tryFindingLocalFiles + tryFindingLocalFiles, + readNycOptions, + combineNycOptions, + defaultNycOptions } diff --git a/task.js b/task.js index 898cb28a..4e3f44f0 100644 --- a/task.js +++ b/task.js @@ -7,7 +7,8 @@ const { showNycInfo, resolveRelativePaths, checkAllPathsNotFound, - tryFindingLocalFiles + tryFindingLocalFiles, + readNycOptions } = require('./task-utils') const { fixSourcePaths } = require('./support-utils') const NYC = require('nyc') @@ -28,7 +29,6 @@ const pkgFilename = join(processWorkingDirectory, 'package.json') const pkg = existsSync(pkgFilename) ? JSON.parse(readFileSync(pkgFilename, 'utf8')) : {} -const nycOptions = pkg.nyc || {} const scripts = pkg.scripts || {} const DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME = 'coverage:report' const customNycReportScript = scripts[DEFAULT_CUSTOM_COVERAGE_SCRIPT_NAME] @@ -42,6 +42,37 @@ function saveCoverage(coverage) { writeFileSync(nycFilename, JSON.stringify(coverage, null, 2)) } +function maybePrintFinalCoverageFiles(folder) { + const jsonReportFilename = join(folder, 'coverage-final.json') + if (!existsSync) { + debug('Did not find final coverage file %s', jsonReportFilename) + return + } + + debug('Final coverage in %s', jsonReportFilename) + const finalCoverage = JSON.parse(readFileSync(jsonReportFilename, 'utf8')) + Object.keys(finalCoverage).forEach(key => { + const s = finalCoverage[key].s || {} + const statements = Object.keys(s) + const totalStatements = statements.length + let coveredStatements = 0 + statements.forEach(statementKey => { + if (s[statementKey]) { + coveredStatements += 1 + } + }) + + const allCovered = coveredStatements === totalStatements + debug( + '%s %s statements covered %d/%d', + allCovered ? '✅' : '⚠️', + key, + coveredStatements, + totalStatements + ) + }) +} + const tasks = { /** * Clears accumulated code coverage information. @@ -122,32 +153,13 @@ const tasks = { }) } - const reportFolder = nycOptions['report-dir'] || './coverage' - const reportDir = resolve(reportFolder) - const reporter = nycOptions['reporter'] || ['lcov', 'clover', 'json'] - - // TODO we could look at how NYC is parsing its CLI arguments - // I am mostly worried about additional NYC options that are stored in - // package.json and .nycrc resource files. - // for now let's just camel case all options // https://github.com/istanbuljs/nyc#common-configuration-options - const nycReportOptions = { - reportDir, - tempDir: coverageFolder, - reporter: [].concat(reporter), // make sure this is a list - include: nycOptions.include, - exclude: nycOptions.exclude, - // from working with TypeScript code seems we need these settings too - excludeAfterRemap: true, - extension: nycOptions.extension || [ - '.js', - '.cjs', - '.mjs', - '.ts', - '.tsx', - '.jsx' - ], - all: nycOptions.all + const nycReportOptions = readNycOptions(processWorkingDirectory) + + // override a couple of options + nycReportOptions.tempDir = coverageFolder + if (nycReportOptions['report-dir']) { + nycReportOptions['report-dir'] = resolve(nycReportOptions['report-dir']) } debug('calling NYC reporter with options %o', nycReportOptions) @@ -155,8 +167,15 @@ const tasks = { const nyc = new NYC(nycReportOptions) const returnReportFolder = () => { - debug('after reporting, returning the report folder name %s', reportDir) - return reportDir + const reportFolder = nycReportOptions['report-dir'] + debug( + 'after reporting, returning the report folder name %s', + reportFolder + ) + + maybePrintFinalCoverageFiles(reportFolder) + + return reportFolder } return nyc.report().then(returnReportFolder) }