From 70e8943b81d4120b6fefdc77a20c34c022a9a691 Mon Sep 17 00:00:00 2001 From: "Benjamin E. Coe" Date: Tue, 22 Jan 2019 21:51:04 -0800 Subject: [PATCH] feat: add thresholds for enforcing coverage percentage (#59) --- README.md | 24 ++++++ bin/c8.js | 45 ++++-------- lib/commands/check-coverage.js | 59 +++++++++++++++ lib/commands/report.js | 24 ++++++ lib/parse-args.js | 130 +++++++++++++++++++++------------ lib/report.js | 16 ++-- package-lock.json | 104 +++++++++++++++++--------- package.json | 2 +- test/fixtures/normal.js | 4 + test/integration.js | 52 ++++++++++++- test/integration.js.snap | 67 ++++++++++++++--- test/parse-args.js | 6 +- 12 files changed, 402 insertions(+), 131 deletions(-) create mode 100644 lib/commands/check-coverage.js create mode 100644 lib/commands/report.js diff --git a/README.md b/README.md index 5eefc5fa..f01c9772 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,30 @@ The above example will output coverage metrics for `foo.js`. run `c8 report` to regenerate reports after `c8` has already been run. +## Checking coverage + +c8 can fail tests if coverage falls below a threshold. +After running your tests with c8, simply run: + +```shell +c8 check-coverage --lines 95 --functions 95 --branches 95 +``` + +c8 also accepts a `--check-coverage` shorthand, which can be used to +both run tests and check that coverage falls within the threshold provided: + +```shell +c8 --check-coverage --lines 100 npm test +``` + +The above check fails if coverage falls below 100%. + +To check thresholds on a per-file basis run: + +```shell +c8 check-coverage --lines 95 --per-file +``` + ## Supported Node.js Versions c8 uses diff --git a/bin/c8.js b/bin/c8.js index 6498be03..7a64fa60 100755 --- a/bin/c8.js +++ b/bin/c8.js @@ -2,48 +2,35 @@ 'use strict' const fs = require('fs') -const util = require('util') - const foreground = require('foreground-child') -const report = require('../lib/report') +const { outputReport } = require('../lib/commands/report') +const { checkCoverages } = require('../lib/commands/check-coverage') +const { promisify } = require('util') const rimraf = require('rimraf') const { + buildYargs, hideInstrumenteeArgs, - hideInstrumenterArgs, - yargs + hideInstrumenterArgs } = require('../lib/parse-args') const instrumenterArgs = hideInstrumenteeArgs() -let argv = yargs.parse(instrumenterArgs) - -const _p = util.promisify - -function outputReport () { - report({ - include: argv.include, - exclude: argv.exclude, - reporter: Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter], - tempDirectory: argv.tempDirectory, - watermarks: argv.watermarks, - resolve: argv.resolve, - omitRelative: argv.omitRelative, - wrapperLength: argv.wrapperLength - }) -} +let argv = buildYargs().parse(instrumenterArgs) -(async function run () { - if (argv._[0] === 'report') { - argv = yargs.parse(process.argv) // support flag arguments after "report". - outputReport() +;(async function run () { + if ([ + 'check-coverage', 'report' + ].indexOf(argv._[0]) !== -1) { + argv = buildYargs(true).parse(process.argv.slice(2)) } else { if (argv.clean) { - await _p(rimraf)(argv.tempDirectory) - await _p(fs.mkdir)(argv.tempDirectory, { recursive: true }) + await promisify(rimraf)(argv.tempDirectory) + await promisify(fs.mkdir)(argv.tempDirectory, { recursive: true }) } - process.env.NODE_V8_COVERAGE = argv.tempDirectory + process.env.NODE_V8_COVERAGE = argv.tempDirectory foreground(hideInstrumenterArgs(argv), () => { - outputReport() + const report = outputReport(argv) + if (argv.checkCoverage) checkCoverages(argv, report) }) } })() diff --git a/lib/commands/check-coverage.js b/lib/commands/check-coverage.js new file mode 100644 index 00000000..136618f5 --- /dev/null +++ b/lib/commands/check-coverage.js @@ -0,0 +1,59 @@ +const { relative } = require('path') +const Report = require('../report') + +exports.command = 'check-coverage' + +exports.describe = 'check whether coverage is within thresholds provided' + +exports.builder = function (yargs) { + yargs + .example('$0 check-coverage --lines 95', "check whether the JSON in c8's output folder meets the thresholds provided") +} + +exports.handler = function (argv) { + const report = Report({ + include: argv.include, + exclude: argv.exclude, + reporter: Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter], + tempDirectory: argv.tempDirectory, + watermarks: argv.watermarks, + resolve: argv.resolve, + omitRelative: argv.omitRelative, + wrapperLength: argv.wrapperLength + }) + exports.checkCoverages(argv, report) +} + +exports.checkCoverages = function (argv, report) { + const thresholds = { + lines: argv.lines, + functions: argv.functions, + branches: argv.branches, + statements: argv.statements + } + const map = report.getCoverageMapFromAllCoverageFiles() + if (argv.perFile) { + map.files().forEach(file => { + checkCoverage(map.fileCoverageFor(file).toSummary(), thresholds, file) + }) + } else { + checkCoverage(map.getCoverageSummary(), thresholds) + } +} + +function checkCoverage (summary, thresholds, file) { + Object.keys(thresholds).forEach(key => { + const coverage = summary[key].pct + if (coverage < thresholds[key]) { + process.exitCode = 1 + if (file) { + console.error( + 'ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet threshold (' + thresholds[key] + '%) for ' + + relative('./', file).replace(/\\/g, '/') // standardize path for Windows. + ) + } else { + console.error('ERROR: Coverage for ' + key + ' (' + coverage + '%) does not meet global threshold (' + thresholds[key] + '%)') + } + } + }) +} diff --git a/lib/commands/report.js b/lib/commands/report.js new file mode 100644 index 00000000..9aa283ed --- /dev/null +++ b/lib/commands/report.js @@ -0,0 +1,24 @@ +const Report = require('../report') + +exports.command = 'report' + +exports.describe = 'read V8 coverage data from temp and output report' + +exports.handler = function (argv) { + exports.outputReport(argv) +} + +exports.outputReport = function (argv) { + const report = Report({ + include: argv.include, + exclude: argv.exclude, + reporter: Array.isArray(argv.reporter) ? argv.reporter : [argv.reporter], + tempDirectory: argv.tempDirectory, + watermarks: argv.watermarks, + resolve: argv.resolve, + omitRelative: argv.omitRelative, + wrapperLength: argv.wrapperLength + }) + report.run() + return report +} diff --git a/lib/parse-args.js b/lib/parse-args.js index d2bd84c1..ee4f1955 100644 --- a/lib/parse-args.js +++ b/lib/parse-args.js @@ -1,56 +1,94 @@ const Exclude = require('test-exclude') const findUp = require('find-up') const { readFileSync } = require('fs') -const yargs = require('yargs') +const Yargs = require('yargs/yargs') const parser = require('yargs-parser') const configPath = findUp.sync(['.c8rc', '.c8rc.json']) const config = configPath ? JSON.parse(readFileSync(configPath)) : {} -yargs() - .usage('$0 [opts] [script] [opts]') - .option('reporter', { - alias: 'r', - describe: 'coverage reporter(s) to use', - default: 'text' - }) - .option('exclude', { - alias: 'x', - default: Exclude.defaultExclude, - describe: 'a list of specific files and directories that should be excluded from coverage (glob patterns are supported)' - }) - .option('include', { - alias: 'n', - default: [], - describe: 'a list of specific files that should be covered (glob patterns are supported)' - }) - .option('temp-directory', { - default: './coverage/tmp', - describe: 'directory V8 coverage data is written to and read from' - }) - .option('resolve', { - default: '', - describe: 'resolve paths to alternate base directory' - }) - .option('wrapper-length', { - describe: 'how many bytes is the wrapper prefix on executed JavaScript', - type: 'number' - }) - .option('omit-relative', { - default: true, - type: 'boolean', - describe: 'omit any paths that are not absolute, e.g., internal/net.js' - }) - .option('clean', { - default: true, - type: 'boolean', - describe: 'should temp files be deleted before script execution' - }) - .command('report', 'read V8 coverage data from temp and output report') - .pkgConf('c8') - .config(config) - .demandCommand(1) - .epilog('visit https://git.io/vHysA for list of available reporters') +function buildYargs (withCommands = false) { + const yargs = Yargs([]) + .usage('$0 [opts] [script] [opts]') + .option('reporter', { + alias: 'r', + describe: 'coverage reporter(s) to use', + default: 'text' + }) + .option('exclude', { + alias: 'x', + default: Exclude.defaultExclude, + describe: 'a list of specific files and directories that should be excluded from coverage (glob patterns are supported)' + }) + .option('include', { + alias: 'n', + default: [], + describe: 'a list of specific files that should be covered (glob patterns are supported)' + }) + .option('check-coverage', { + default: false, + type: 'boolean', + description: 'check whether coverage is within thresholds provided' + }) + .option('branches', { + default: 0, + description: 'what % of branches must be covered?' + }) + .option('functions', { + default: 0, + description: 'what % of functions must be covered?' + }) + .option('lines', { + default: 90, + description: 'what % of lines must be covered?' + }) + .option('statements', { + default: 0, + description: 'what % of statements must be covered?' + }) + .option('per-file', { + default: false, + description: 'check thresholds per file' + }) + .option('temp-directory', { + default: './coverage/tmp', + describe: 'directory V8 coverage data is written to and read from' + }) + .option('resolve', { + default: '', + describe: 'resolve paths to alternate base directory' + }) + .option('wrapper-length', { + describe: 'how many bytes is the wrapper prefix on executed JavaScript', + type: 'number' + }) + .option('omit-relative', { + default: true, + type: 'boolean', + describe: 'omit any paths that are not absolute, e.g., internal/net.js' + }) + .option('clean', { + default: true, + type: 'boolean', + describe: 'should temp files be deleted before script execution' + }) + .pkgConf('c8') + .config(config) + .demandCommand(1) + .epilog('visit https://git.io/vHysA for list of available reporters') + + const checkCoverage = require('./commands/check-coverage') + const report = require('./commands/report') + if (withCommands) { + yargs.command(checkCoverage) + yargs.command(report) + } else { + yargs.command(checkCoverage.command, checkCoverage.describe) + yargs.command(report.command, report.describe) + } + + return yargs +} function hideInstrumenterArgs (yargv) { var argv = process.argv.slice(1) @@ -76,7 +114,7 @@ function hideInstrumenteeArgs () { } module.exports = { - yargs, + buildYargs, hideInstrumenterArgs, hideInstrumenteeArgs } diff --git a/lib/report.js b/lib/report.js index abfd65a9..160b0c7e 100644 --- a/lib/report.js +++ b/lib/report.js @@ -32,7 +32,7 @@ class Report { this.wrapperLength = wrapperLength } run () { - const map = this._getCoverageMapFromAllCoverageFiles() + const map = this.getCoverageMapFromAllCoverageFiles() var context = libReport.createContext({ dir: './coverage', watermarks: this.watermarks @@ -45,7 +45,13 @@ class Report { }) } - _getCoverageMapFromAllCoverageFiles () { + getCoverageMapFromAllCoverageFiles () { + // the merge process can be very expensive, and it's often the case that + // check-coverage is called immediately after a report. We memoize the + // result from getCoverageMapFromAllCoverageFiles() to address this + // use-case. + if (this._allCoverageFiles) return this._allCoverageFiles + const v8ProcessCov = this._getMergedProcessCov() const map = libCoverage.createCoverageMap({}) @@ -61,7 +67,8 @@ class Report { } } - return map + this._allCoverageFiles = map + return this._allCoverageFiles } /** @@ -138,6 +145,5 @@ class Report { } module.exports = function (opts) { - const report = new Report(opts) - report.run() + return new Report(opts) } diff --git a/package-lock.json b/package-lock.json index 052292e7..fa8c3ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1181,6 +1182,14 @@ "safer-buffer": "^2.1.0" } }, + "end-of-stream": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", + "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", + "requires": { + "once": "^1.4.0" + } + }, "error-ex": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", @@ -1888,7 +1897,8 @@ "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true }, "getpass": { "version": "0.1.7", @@ -2190,7 +2200,8 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "optional": true }, "is-builtin-module": { "version": "1.0.0", @@ -2495,6 +2506,7 @@ "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "optional": true, "requires": { "is-buffer": "^1.1.5" } @@ -2597,7 +2609,8 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "optional": true }, "loose-envify": { "version": "1.4.0", @@ -2643,9 +2656,9 @@ } }, "map-age-cleaner": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz", - "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==", + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", + "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "requires": { "p-defer": "^1.0.0" } @@ -3331,6 +3344,15 @@ "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", "dev": true }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", @@ -3455,7 +3477,8 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "optional": true }, "repeating": { "version": "2.0.1", @@ -4266,11 +4289,6 @@ "mkdirp": "^0.5.1" } }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" - }, "xtend": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", @@ -4288,12 +4306,12 @@ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" }, "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", + "version": "12.0.5", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", + "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", "requires": { "cliui": "^4.0.0", - "decamelize": "^2.0.0", + "decamelize": "^1.2.0", "find-up": "^3.0.0", "get-caller-file": "^1.0.1", "os-locale": "^3.0.0", @@ -4303,7 +4321,7 @@ "string-width": "^2.0.0", "which-module": "^2.0.0", "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^10.1.0" + "yargs-parser": "^11.1.1" }, "dependencies": { "ansi-regex": { @@ -4311,6 +4329,11 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, + "camelcase": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", + "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==" + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -4333,21 +4356,13 @@ "which": "^1.2.9" } }, - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "requires": { - "xregexp": "4.0.0" - } - }, "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "requires": { "cross-spawn": "^6.0.0", - "get-stream": "^3.0.0", + "get-stream": "^4.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", @@ -4355,6 +4370,14 @@ "strip-eof": "^1.0.0" } }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, "invert-kv": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", @@ -4379,19 +4402,19 @@ } }, "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", + "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "requires": { - "execa": "^0.10.0", + "execa": "^1.0.0", "lcid": "^2.0.0", "mem": "^4.0.0" } }, "semver": { - "version": "5.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" }, "strip-ansi": { "version": "4.0.0", @@ -4400,6 +4423,15 @@ "requires": { "ansi-regex": "^3.0.0" } + }, + "yargs-parser": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", + "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, diff --git a/package.json b/package.json index 0381298b..4413c28e 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test-exclude": "^5.0.0", "uuid": "^3.3.2", "v8-to-istanbul": "^2.0.2", - "yargs": "^12.0.2", + "yargs": "^12.0.5", "yargs-parser": "^10.1.0" }, "devDependencies": { diff --git a/test/fixtures/normal.js b/test/fixtures/normal.js index 226db158..ad544629 100644 --- a/test/fixtures/normal.js +++ b/test/fixtures/normal.js @@ -15,6 +15,10 @@ function missed () { } +function missed2 () { + +} + apple() apple() apple() diff --git a/test/integration.js b/test/integration.js index dd91bc15..b7671659 100644 --- a/test/integration.js +++ b/test/integration.js @@ -34,7 +34,7 @@ describe('c8', () => { output.toString('utf8').should.matchSnapshot() }) - it('omit-relative can be set to false', () => { + it('allows relative files to be included', () => { const { output } = spawnSync(nodePath, [ c8Path, '--exclude="test/*.js"', @@ -47,4 +47,54 @@ describe('c8', () => { /Error: ENOENT: no such file or directory.*loaders\.js/ ) }) + + describe('check-coverage', () => { + it('exits with 0 if coverage within threshold', () => { + const { output, status } = spawnSync(nodePath, [ + c8Path, + 'check-coverage', + '--exclude="test/*.js"', + '--lines=80' + ]) + status.should.equal(0) + output.toString('utf8').should.matchSnapshot() + }) + + it('allows threshold to be applied on per-file basis', () => { + const { output, status } = spawnSync(nodePath, [ + c8Path, + 'check-coverage', + '--exclude="test/*.js"', + '--lines=80', + '--per-file' + ]) + status.should.equal(1) + output.toString('utf8').should.matchSnapshot() + }) + + it('exits with 1 if coverage is below threshold', () => { + const { output, status } = spawnSync(nodePath, [ + c8Path, + 'check-coverage', + '--exclude="test/*.js"', + '--lines=101' + ]) + status.should.equal(1) + output.toString('utf8').should.matchSnapshot() + }) + + it('allows --check-coverage when executing script', () => { + const { output, status } = spawnSync(nodePath, [ + c8Path, + '--exclude="test/*.js"', + '--clean=false', + '--lines=101', + '--check-coverage', + nodePath, + require.resolve('./fixtures/normal') + ]) + status.should.equal(1) + output.toString('utf8').should.matchSnapshot() + }) + }) }) diff --git a/test/integration.js.snap b/test/integration.js.snap index d7394a14..0a21127d 100644 --- a/test/integration.js.snap +++ b/test/integration.js.snap @@ -1,5 +1,49 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`c8 check-coverage allows --check-coverage when executing script 1`] = ` +",hey +i am a line of code +what +hey +what +hey +what +hey +--------------------|----------|----------|----------|----------|-------------------| +File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | +--------------------|----------|----------|----------|----------|-------------------| +All files | 95.42 | 75.9 | 89.66 | 95.42 | | + bin | 86.49 | 71.43 | 100 | 86.49 | | + c8.js | 86.49 | 71.43 | 100 | 86.49 | 26,34,35,36,37 | + lib | 97.05 | 65 | 100 | 97.05 | | + parse-args.js | 98.35 | 53.85 | 100 | 98.35 | 97,98 | + report.js | 96 | 70.37 | 100 | 96 |... 08,134,135,136 | + lib/commands | 97.65 | 84.21 | 87.5 | 97.65 | | + check-coverage.js | 100 | 92.86 | 100 | 100 | 17 | + report.js | 92 | 60 | 50 | 92 | 8,9 | + test/fixtures | 90.91 | 94.12 | 75 | 90.91 | | + async.js | 100 | 100 | 100 | 100 | | + multiple-spawn.js | 100 | 100 | 100 | 100 | | + normal.js | 76 | 75 | 33.33 | 76 | 14,15,16,18,19,20 | + subprocess.js | 100 | 100 | 100 | 100 | | +--------------------|----------|----------|----------|----------|-------------------| +,ERROR: Coverage for lines (95.42%) does not meet global threshold (101%) +" +`; + +exports[`c8 check-coverage allows threshold to be applied on per-file basis 1`] = ` +",,ERROR: Coverage for lines (78.33%) does not meet threshold (80%) for lib/commands/check-coverage.js +ERROR: Coverage for lines (76%) does not meet threshold (80%) for test/fixtures/normal.js +" +`; + +exports[`c8 check-coverage exits with 0 if coverage within threshold 1`] = `",,"`; + +exports[`c8 check-coverage exits with 1 if coverage is below threshold 1`] = ` +",,ERROR: Coverage for lines (94.99%) does not meet global threshold (101%) +" +`; + exports[`c8 merges reports from subprocesses together 1`] = ` ",first @@ -8,16 +52,19 @@ second --------------------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | --------------------|----------|----------|----------|----------|-------------------| -All files | 94.4 | 71.43 | 95 | 94.4 | | - bin | 88 | 62.5 | 100 | 88 | | - c8.js | 88 | 62.5 | 100 | 88 | 36,40,47,48,49,50 | - lib | 95.59 | 61.29 | 100 | 95.59 | | - parse-args.js | 97.59 | 44.44 | 100 | 97.59 | 59,60 | - report.js | 94.44 | 68.18 | 100 | 94.44 |... 01,127,128,129 | - test/fixtures | 95.16 | 94.12 | 85.71 | 95.16 | | +All files | 84.53 | 67.16 | 74.07 | 84.53 | | + bin | 86.49 | 57.14 | 100 | 86.49 | | + c8.js | 86.49 | 57.14 | 100 | 86.49 | 26,34,35,36,37 | + lib | 95.57 | 55.56 | 100 | 95.57 | | + parse-args.js | 96.69 | 41.67 | 100 | 96.69 | 83,84,97,98 | + report.js | 94.67 | 62.5 | 100 | 94.67 |... 08,134,135,136 | + lib/commands | 43.53 | 71.43 | 16.67 | 43.53 | | + check-coverage.js | 23.33 | 100 | 0 | 23.33 |... 55,56,57,58,59 | + report.js | 92 | 60 | 50 | 92 | 8,9 | + test/fixtures | 90.91 | 94.12 | 75 | 90.91 | | async.js | 100 | 100 | 100 | 100 | | multiple-spawn.js | 100 | 100 | 100 | 100 | | - normal.js | 85.71 | 75 | 50 | 85.71 | 14,15,16 | + normal.js | 76 | 75 | 33.33 | 76 | 14,15,16,18,19,20 | subprocess.js | 100 | 100 | 100 | 100 | | --------------------|----------|----------|----------|----------|-------------------| ," @@ -35,9 +82,9 @@ hey -----------|----------|----------|----------|----------|-------------------| File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | -----------|----------|----------|----------|----------|-------------------| -All files | 91.18 | 88.89 | 80 | 91.18 | | +All files | 84.21 | 88.89 | 66.67 | 84.21 | | async.js | 100 | 100 | 100 | 100 | | - normal.js | 85.71 | 75 | 50 | 85.71 | 14,15,16 | + normal.js | 76 | 75 | 33.33 | 76 | 14,15,16,18,19,20 | -----------|----------|----------|----------|----------|-------------------| ," `; diff --git a/test/parse-args.js b/test/parse-args.js index 43743e21..04f29194 100644 --- a/test/parse-args.js +++ b/test/parse-args.js @@ -1,9 +1,9 @@ /* global describe, it */ const { + buildYargs, hideInstrumenteeArgs, - hideInstrumenterArgs, - yargs + hideInstrumenterArgs } = require('../lib/parse-args') describe('parse-args', () => { @@ -18,7 +18,7 @@ describe('parse-args', () => { describe('hideInstrumenterArgs', () => { it('hides arguments passed to c8 bin', () => { process.argv = ['node', 'c8', '--foo=99', 'my-app', '--help'] - const argv = yargs.parse(hideInstrumenteeArgs()) + const argv = buildYargs().parse(hideInstrumenteeArgs()) const instrumenteeArgs = hideInstrumenterArgs(argv) instrumenteeArgs.should.eql(['my-app', '--help']) })