├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .nyc_output ├── no-information.json └── out.json ├── .prettierrc.json ├── README.md ├── bin ├── check-coverage.js ├── check-total.js ├── only-covered.js ├── set-gh-status.js └── update-badge.js ├── coverage └── coverage-summary.json ├── images ├── commit-status.png └── coverage-diff.png ├── package-lock.json ├── package.json ├── renovate.json └── src ├── index.js └── test.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: push 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Checkout 🛎 8 | uses: actions/checkout@v3 9 | 10 | - name: Install NPM dependencies 📦 11 | uses: bahmutov/npm-install@v1 12 | 13 | - name: Unit tests 🧪 14 | run: npm test 15 | 16 | - name: Check all covered files 📊 17 | run: | 18 | node bin/check-coverage main1.js 19 | node bin/check-coverage to/main2.js 20 | node bin/only-covered main1.js main2.js 21 | # and can check multiple files at once 22 | node bin/check-coverage main1.js main2.js 23 | 24 | - name: Check totals 🛡 25 | run: node bin/check-total --min 30 26 | 27 | - name: Set commit status using REST 28 | # https://developer.github.com/v3/repos/statuses/ 29 | run: | 30 | curl --request POST \ 31 | --url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \ 32 | --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ 33 | --header 'content-type: application/json' \ 34 | --data '{ 35 | "state": "success", 36 | "description": "REST commit status", 37 | "context": "a test" 38 | }' 39 | 40 | - name: Set code coverage commit status 📫 41 | run: node bin/set-gh-status 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Check coverage change from README 📫 46 | run: node bin/set-gh-status --check-against-readme 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Update code coverage badge 🥇 51 | run: node bin/update-badge 52 | 53 | - name: Semantic Release 🚀 54 | uses: cycjimmy/semantic-release-action@v3 55 | if: github.ref == 'refs/heads/master' 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.nyc_output/no-information.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-coverage.js": { 3 | "path": "no-coverage.js", 4 | "s": {} 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.nyc_output/out.json: -------------------------------------------------------------------------------- 1 | { 2 | "main1.js": { 3 | "path": "main1.js", 4 | "s": { 5 | "0": 2, 6 | "1": 1, 7 | "2": 2, 8 | "3": 1 9 | } 10 | }, 11 | 12 | "main2.js": { 13 | "path": "/path/to/main2.js", 14 | "s": { 15 | "0": 2, 16 | "1": 1, 17 | "2": 2, 18 | "3": 1 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # check-code-coverage [![ci status][ci image]][ci url] ![check-code-coverage](https://img.shields.io/badge/code--coverage-100%25-brightgreen) 2 | 3 | > Utilities for checking the coverage produced by NYC against extra or missing files 4 | 5 | ## Use 6 | 7 | ```shell 8 | npm i -D check-code-coverage 9 | # check if .nyc_output/out.json has files foo.js and bar.js covered and nothing else 10 | npx only-covered foo.js bar.js 11 | ``` 12 | 13 | Watch these short videos to see these tools in action: 14 | 15 | - [Check code coverage robustly using 3rd party tool](https://youtu.be/dwU5gUG2-EM) 16 | - [Adding code coverage badge to your project](https://youtu.be/bNVRxb-MKGo) 17 | - [Show code coverage in commit status check](https://youtu.be/AAl4HmJ3YuM) 18 | 19 | ## check-coverage 20 | 21 | Checks if the file is present in the output JSON file and has 100% statement coverage 22 | 23 | ```shell 24 | # check if .nyc_output/out.json has 100% code coverage for main.js 25 | npx check-coverage main.js 26 | # read coverage report from particular JSON file 27 | check-coverage --from examples/exclude-files/coverage/coverage-final.json main.js 28 | ``` 29 | 30 | The file has to end with "main.js". You can specify part of the path, like this 31 | 32 | ```shell 33 | npx check-coverage src/app/main.js 34 | ``` 35 | 36 | You can pass multiple filenames 37 | 38 | ```shell 39 | npx check-coverage main.js src/person.js 40 | ``` 41 | 42 | ## only-covered 43 | 44 | Check if the coverage JSON file only the given list of files and nothing else. By default `only-covered` script reads `.nyc_output/out.json` file from the current working directory. You can specify a different file using `--from` parameter. 45 | 46 | ```shell 47 | # check if coverage has info about two files and nothing else 48 | only-covered src/lib/utils.ts src/main.js 49 | # read coverage from another file and check if it only has info on "main.js" 50 | only-covered --from examples/exclude-files/coverage/coverage-final.json main.js 51 | ``` 52 | 53 | ## check-total 54 | 55 | If you generate coverage report using reporter `json-summary`, you can check the total statements percentage 56 | 57 | ```shell 58 | check-total 59 | # with default options 60 | check-total --from coverage/coverage-summary.json --min 80 61 | ``` 62 | 63 | The command exits with 0 if the total is above or equal to the minimum number. If the code coverage is below the minimum, the command exits with code 1. On most CIs any command exiting with non-zero code fails the build. 64 | 65 | ## update-badge 66 | 67 | If your README.md includes Shields.io badge, like this 68 | 69 | ![check-code-coverage](https://img.shields.io/badge/code--coverage-80%-brightgreen) 70 | 71 | You can update it using statements covered percentage from `coverage/coverage-summary.json` by running 72 | 73 | ```shell 74 | update-badge 75 | ``` 76 | 77 | If the coverage summary has 96%, then the above badge would be updated to 78 | 79 | ![check-code-coverage](https://img.shields.io/badge/code--coverage-96%-brightgreen) 80 | 81 | - The badges will have different colors, depending on the coverage, see [bin/update-badge.js](bin/update-badge.js) 82 | - If the code coverage badge is not found, a new badge is inserted on the first line. 83 | 84 | You can change the JSON summary filename to read coverage from: 85 | 86 | ```shell 87 | update-badge --from path/to/json/summary/file.json 88 | ``` 89 | 90 | You can also skip reading file and set the coverage number directly 91 | 92 | ```shell 93 | update-badge --set 78 94 | update-badge --set 78% 95 | ``` 96 | 97 | Related project: [dependency-version-badge](https://github.com/bahmutov/dependency-version-badge) 98 | 99 | ## set-gh-status 100 | 101 | If you run your tests on [GitHub Actions](https://glebbahmutov.com/blog/trying-github-actions/), there is an easy way to add commit status with code coverage percentage. From your CI workflow use command: 102 | 103 | ```yaml 104 | - name: Set code coverage commit status 📫 105 | run: npx -p check-code-coverage set-gh-status 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | ``` 109 | 110 | Which should show a commit status message like: 111 | 112 | ![Commit status check](images/commit-status.png) 113 | 114 | This script reads the code coverage summary from `coverage/coverage-summary.json` by default (you can specific a different file name using `--from` option) and posts the commit status, always passing for now. 115 | 116 | **Note:** to write the commit status, the GitHub token needs "write" permission, so set it in your workflow file: 117 | 118 | ```yml 119 | permissions: 120 | statuses: write 121 | ``` 122 | 123 | If there is a coverage badge in the README file, you can add 2nd status check. This check will read the code coverage from the README file (by parsing the badge text), then will set a failing status check if the coverage dropped more than 1 percent. **Tip:** use this check on pull requests to ensure tests and code are updated together before merging. 124 | 125 | ```yaml 126 | - name: Ensure coverage has not dropped 📈 127 | run: npx -p check-code-coverage set-gh-status --check-against-readme 128 | env: 129 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 130 | ``` 131 | 132 | ![Coverage diff](images/coverage-diff.png) 133 | 134 | ### Pull requests 135 | 136 | When setting a status on a GitHub pull request, you need to use SHA of the merged commit. You can pass it as `GH_SHA` environment variable. 137 | 138 | ```yaml 139 | - name: Ensure coverage has not dropped 📈 140 | run: npx -p check-code-coverage set-gh-status --check-against-readme 141 | env: 142 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 143 | GH_SHA: ${{ github.event.after }} 144 | ``` 145 | 146 | ## Debug 147 | 148 | To see verbose log messages, run with `DEBUG=check-code-coverage` environment variable 149 | 150 | [ci image]: https://github.com/bahmutov/check-code-coverage/workflows/ci/badge.svg?branch=master 151 | [ci url]: https://github.com/bahmutov/check-code-coverage/actions 152 | -------------------------------------------------------------------------------- /bin/check-coverage.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | const { join, resolve } = require('path') 4 | const arg = require('arg') 5 | 6 | const args = arg({ 7 | '--from': String, // input filename, by default ".nyc_output/out.json" 8 | }) 9 | 10 | const filenames = args._ 11 | if (!filenames.length) { 12 | console.error( 13 | 'Usage: node %s ...', 14 | __filename, 15 | ) 16 | process.exit(1) 17 | } 18 | 19 | const fromFilename = args['--from'] || join('.nyc_output', 'out.json') 20 | const coverageFilename = resolve(fromFilename) 21 | const coverage = require(coverageFilename) 22 | 23 | filenames.forEach((filename) => { 24 | const fileCoverageKey = Object.keys(coverage).find((name) => { 25 | const fileCover = coverage[name] 26 | if (fileCover.path.endsWith(filename)) { 27 | return fileCover 28 | } 29 | }) 30 | 31 | if (!fileCoverageKey) { 32 | console.error( 33 | 'Could not find file %s in coverage in file %s', 34 | filename, 35 | coverageFilename, 36 | ) 37 | process.exit(1) 38 | } 39 | 40 | const fileCoverage = coverage[fileCoverageKey] 41 | const statementCounters = fileCoverage.s 42 | const isThereUncoveredStatement = Object.keys(statementCounters).some( 43 | (k, key) => { 44 | return statementCounters[key] === 0 45 | }, 46 | ) 47 | if (isThereUncoveredStatement) { 48 | console.error( 49 | 'file %s has statements that were not covered by tests', 50 | fileCoverage.path, 51 | ) 52 | console.log('statement counters %o', statementCounters) 53 | 54 | process.exit(1) 55 | } 56 | 57 | console.log( 58 | '✅ All statements in file %s (found for %s) were covered', 59 | fileCoverage.path, 60 | filename, 61 | ) 62 | }) 63 | -------------------------------------------------------------------------------- /bin/check-total.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | const { join, resolve } = require('path') 4 | const arg = require('arg') 5 | 6 | const args = arg({ 7 | '--from': String, // input json-summary filename, by default "coverage/coverage-summary.json" 8 | '--min': Number 9 | }) 10 | 11 | const minStatementPercentage = args['--min'] || 80 12 | const fromFilename = args['--from'] || join('coverage', 'coverage-summary.json') 13 | const coverageFilename = resolve(fromFilename) 14 | 15 | const coverage = require(coverageFilename) 16 | const total = coverage.total 17 | if (!total) { 18 | console.error('Could not find "total" object in %s', fromFilename) 19 | process.exit(1) 20 | } 21 | 22 | // total should have objects for lines, statements, functions and branches 23 | // each object should have total, covered, skipped and pct numbers 24 | const statements = total.statements 25 | if (!statements) { 26 | console.error('Could not find statements in total %o', total) 27 | process.exit(1) 28 | } 29 | 30 | if (statements.pct < minStatementPercentage) { 31 | console.log('🚨 Statement coverage %d is below minimum %d%%', statements.pct, minStatementPercentage) 32 | console.log('file %s', coverageFilename) 33 | process.exit(1) 34 | } 35 | 36 | console.log( 37 | '✅ Total statement coverage %d%% is >= minimum %d%%', 38 | statements.pct, minStatementPercentage 39 | ) 40 | -------------------------------------------------------------------------------- /bin/only-covered.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | const { join, resolve } = require('path') 4 | const _ = require('lodash') 5 | const arg = require('arg') 6 | 7 | const args = arg({ 8 | '--from': String, // input filename, by default ".nyc_output/out.json" 9 | }) 10 | 11 | const filenames = args._ 12 | if (!filenames.length) { 13 | console.error('Usage: node %s ', __filename) 14 | process.exit(1) 15 | } 16 | 17 | const shouldBeCovered = filepath => 18 | filenames.some(name => filepath.endsWith(name)) 19 | 20 | const fromFilename = args['--from'] || join('.nyc_output', 'out.json') 21 | const coverageFilename = resolve(fromFilename) 22 | console.log('reading coverage results from %s', coverageFilename) 23 | 24 | const coverage = require(coverageFilename) 25 | 26 | const coveredFilepaths = Object.keys(coverage).map(name => coverage[name].path) 27 | 28 | // console.log(coveredFilepaths) 29 | 30 | const [covered, extraCoveredFiles] = _.partition( 31 | coveredFilepaths, 32 | shouldBeCovered 33 | ) 34 | 35 | if (extraCoveredFiles.length) { 36 | console.error('Error: found extra covered files 🔥') 37 | console.error('Expected the following files in coverage results') 38 | console.error(filenames.join('\n')) 39 | console.error('extra files covered 🔥') 40 | console.error(extraCoveredFiles.join('\n')) 41 | process.exit(1) 42 | } 43 | 44 | if (covered.length < filenames.length) { 45 | console.error('Error: expected all files from the list to be covered 🔥') 46 | console.error('Expected the following files in coverage results') 47 | console.error(filenames.join('\n')) 48 | console.error('But found only these files to be covered') 49 | console.error(covered.join('\n')) 50 | 51 | console.error('Files missing from the coverage 🔥') 52 | const missingFiles = filenames.filter( 53 | filename => 54 | !covered.some(coveredFilename => coveredFilename.endsWith(filename)) 55 | ) 56 | console.error(missingFiles.join('\n')) 57 | 58 | process.exit(1) 59 | } 60 | 61 | console.log('✅ All and only expected files were covered') 62 | -------------------------------------------------------------------------------- /bin/set-gh-status.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | 4 | const got = require('got') 5 | const debug = require('debug')('check-code-coverage') 6 | const {readCoverage, toPercent, badge} = require('..') 7 | 8 | const arg = require('arg') 9 | 10 | const args = arg({ 11 | '--from': String, // input json-summary filename, by default "coverage/coverage-summary.json" 12 | '--check-against-readme': Boolean 13 | }) 14 | debug('args: %o', args) 15 | 16 | async function setGitHubCommitStatus(options, envOptions) { 17 | const pct = toPercent(readCoverage(options.filename)) 18 | debug('setting commit coverage: %d', pct) 19 | debug('with options %o', { 20 | repository: envOptions.repository, 21 | sha: envOptions.sha 22 | }) 23 | 24 | // REST call to GitHub API 25 | // https://developer.github.com/v3/repos/statuses/ 26 | // https://help.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#example-calling-the-rest-api 27 | // a typical request would be like: 28 | // curl --request POST \ 29 | // --url https://api.github.com/repos/${{ github.repository }}/statuses/${{ github.sha }} \ 30 | // --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \ 31 | // --header 'content-type: application/json' \ 32 | // --data '{ 33 | // "state": "success", 34 | // "description": "REST commit status", 35 | // "context": "a test" 36 | // }' 37 | const url = `https://api.github.com/repos/${envOptions.repository}/statuses/${envOptions.sha}` 38 | // @ts-ignore 39 | const res = await got.post(url, { 40 | headers: { 41 | authorization: `Bearer ${envOptions.token}` 42 | }, 43 | json: { 44 | context: 'code-coverage', 45 | state: 'success', 46 | description: `${pct}% of statements` 47 | } 48 | }) 49 | console.log('response status: %d %s', res.statusCode, res.statusMessage) 50 | 51 | if (options.checkAgainstReadme) { 52 | const readmePercent = badge.getCoverageFromReadme() 53 | if (typeof readmePercent !== 'number') { 54 | console.error('Could not get code coverage percentage from README') 55 | console.error('readmePercent is', readmePercent) 56 | process.exit(1) 57 | } 58 | 59 | if (pct > readmePercent) { 60 | console.log('coverage 📈 from %d% to %d%', readmePercent, pct) 61 | // @ts-ignore 62 | await got.post(url, { 63 | headers: { 64 | authorization: `Bearer ${envOptions.token}` 65 | }, 66 | json: { 67 | context: 'code-coverage Δ', 68 | state: 'success', 69 | description: `went up from ${readmePercent}% to ${pct}%` 70 | } 71 | }) 72 | } else if (Math.abs(pct - readmePercent) < 1) { 73 | console.log('coverage stayed the same %d% ~ %d%', readmePercent, pct) 74 | // @ts-ignore 75 | await got.post(url, { 76 | headers: { 77 | authorization: `Bearer ${envOptions.token}` 78 | }, 79 | json: { 80 | context: 'code-coverage Δ', 81 | state: 'success', 82 | description: `stayed the same at ${pct}%` 83 | } 84 | }) 85 | } else { 86 | console.log('coverage 📉 from %d% to %d%', readmePercent, pct) 87 | // @ts-ignore 88 | await got.post(url, { 89 | headers: { 90 | authorization: `Bearer ${envOptions.token}` 91 | }, 92 | json: { 93 | context: 'code-coverage Δ', 94 | state: 'failure', 95 | description: `decreased from ${readmePercent}% to ${pct}%` 96 | } 97 | }) 98 | } 99 | } 100 | } 101 | 102 | function checkEnvVariables(env) { 103 | if (!env.GITHUB_TOKEN) { 104 | console.error('Cannot find environment variable GITHUB_TOKEN') 105 | process.exit(1) 106 | } 107 | 108 | if (!env.GITHUB_REPOSITORY) { 109 | console.error('Cannot find environment variable GITHUB_REPOSITORY') 110 | process.exit(1) 111 | } 112 | 113 | if (!env.GITHUB_SHA) { 114 | console.error('Cannot find environment variable GITHUB_SHA') 115 | process.exit(1) 116 | } 117 | } 118 | 119 | checkEnvVariables(process.env) 120 | 121 | debug('GH env variables: GITHUB_REPOSITORY %s GH_SHA %s GITHUB_SHA %s', 122 | process.env.GITHUB_REPOSITORY, process.env.GH_SHA, process.env.GITHUB_SHA) 123 | 124 | const options = { 125 | filename: args['--file'], 126 | checkAgainstReadme: args['--check-against-readme'] 127 | } 128 | const envOptions = { 129 | token: process.env.GITHUB_TOKEN, 130 | repository: process.env.GITHUB_REPOSITORY, 131 | // allow overriding the commit SHA, useful in pull requests 132 | // where we want a merged commit SHA from GH event 133 | sha: process.env.GH_SHA || process.env.GITHUB_SHA 134 | } 135 | setGitHubCommitStatus(options, envOptions).catch(e => { 136 | console.error(e) 137 | process.exit(1) 138 | }) 139 | -------------------------------------------------------------------------------- /bin/update-badge.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @ts-check 3 | 4 | const debug = require('debug')('check-code-coverage') 5 | const path = require('path') 6 | const fs = require('fs') 7 | const os = require('os') 8 | const arg = require('arg') 9 | const {readCoverage, toPercent, badge} = require('..') 10 | 11 | const args = arg({ 12 | '--from': String, // input json-summary filename, by default "coverage/coverage-summary.json" 13 | '--set': String // so we can convert "78%" into numbers ourselves 14 | }) 15 | debug('args: %o', args) 16 | 17 | function updateBadge(args) { 18 | let pct = 0 19 | if (args['--set']) { 20 | // make sure we can handle "--set 70" and "--set 70%" 21 | pct = parseFloat(args['--set']) 22 | debug('using coverage number: %d', pct) 23 | } else { 24 | pct = readCoverage(args['--from']) 25 | } 26 | pct = toPercent(pct) 27 | debug('clamped coverage: %d', pct) 28 | 29 | const readmeFilename = path.join(process.cwd(), 'README.md') 30 | const readmeText = fs.readFileSync(readmeFilename, 'utf8') 31 | 32 | function replaceShield() { 33 | const coverageRe = badge.getCoverageRe() 34 | debug('coverage regex: "%s"', coverageRe) 35 | 36 | const coverageBadge = badge.getCoverageBadge(pct) 37 | debug('new coverage badge: "%s"', coverageBadge) 38 | if (!coverageBadge) { 39 | console.error('cannot form new badge for %d%', pct) 40 | return readmeText 41 | } 42 | 43 | let found 44 | let updatedReadmeText = readmeText.replace( 45 | coverageRe, 46 | (match) => { 47 | found = true 48 | debug('match: %o', match) 49 | return coverageBadge 50 | }, 51 | ) 52 | 53 | if (!found) { 54 | console.log('⚠️ Could not find code coverage badge in file %s', readmeFilename) 55 | console.log('Insert new badge on the first line') 56 | // use NPM package name as label to flag where this badge is coming from 57 | const badge = `![check-code-coverage](${coverageBadge})` 58 | debug('inserting new badge: %s', badge) 59 | 60 | const lines = readmeText.split(os.EOL) 61 | if (lines.length < 1) { 62 | console.error('File %s has no lines, cannot insert code coverage badge', readmeFilename) 63 | return readmeText 64 | } 65 | lines[0] += ' ' + badge 66 | updatedReadmeText = lines.join(os.EOL) 67 | } 68 | return updatedReadmeText 69 | } 70 | 71 | const maybeChangedText = replaceShield() 72 | if (maybeChangedText !== readmeText) { 73 | console.log('saving updated readme with coverage %d%%', pct) 74 | fs.writeFileSync(readmeFilename, maybeChangedText, 'utf8') 75 | } else { 76 | debug('no code coverage badge change') 77 | } 78 | } 79 | 80 | updateBadge(args) 81 | -------------------------------------------------------------------------------- /coverage/coverage-summary.json: -------------------------------------------------------------------------------- 1 | { 2 | "total": { 3 | "lines": { 4 | "total": 3, 5 | "covered": 3, 6 | "skipped": 0, 7 | "pct": 99 8 | }, 9 | "statements": { 10 | "total": 3, 11 | "covered": 3, 12 | "skipped": 0, 13 | "pct": 100 14 | }, 15 | "functions": { 16 | "total": 1, 17 | "covered": 1, 18 | "skipped": 0, 19 | "pct": 100 20 | }, 21 | "branches": { 22 | "total": 0, 23 | "covered": 0, 24 | "skipped": 0, 25 | "pct": 100 26 | } 27 | }, 28 | "/Users/gleb/git/instrument-example/src/App.js": { 29 | "lines": { 30 | "total": 1, 31 | "covered": 1, 32 | "skipped": 0, 33 | "pct": 100 34 | }, 35 | "functions": { 36 | "total": 1, 37 | "covered": 1, 38 | "skipped": 0, 39 | "pct": 100 40 | }, 41 | "statements": { 42 | "total": 1, 43 | "covered": 1, 44 | "skipped": 0, 45 | "pct": 100 46 | }, 47 | "branches": { 48 | "total": 0, 49 | "covered": 0, 50 | "skipped": 0, 51 | "pct": 100 52 | } 53 | }, 54 | "/Users/gleb/git/instrument-example/src/index.js": { 55 | "lines": { 56 | "total": 2, 57 | "covered": 2, 58 | "skipped": 0, 59 | "pct": 100 60 | }, 61 | "functions": { 62 | "total": 0, 63 | "covered": 0, 64 | "skipped": 0, 65 | "pct": 100 66 | }, 67 | "statements": { 68 | "total": 2, 69 | "covered": 2, 70 | "skipped": 0, 71 | "pct": 100 72 | }, 73 | "branches": { 74 | "total": 0, 75 | "covered": 0, 76 | "skipped": 0, 77 | "pct": 100 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /images/commit-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/check-code-coverage/40c0c41d570d49897250eb38465f4f7d75d32c73/images/commit-status.png -------------------------------------------------------------------------------- /images/coverage-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bahmutov/check-code-coverage/40c0c41d570d49897250eb38465f4f7d75d32c73/images/coverage-diff.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "check-code-coverage", 3 | "version": "0.0.0-development", 4 | "description": "Utilities for checking the coverage produced by NYC against extra or missing files", 5 | "main": "src/index.js", 6 | "bin": { 7 | "check-coverage": "bin/check-coverage.js", 8 | "only-covered": "bin/only-covered.js", 9 | "check-total": "bin/check-total.js", 10 | "update-badge": "bin/update-badge.js", 11 | "set-gh-status": "bin/set-gh-status.js" 12 | }, 13 | "files": [ 14 | "bin", 15 | "src", 16 | "!src/test.js" 17 | ], 18 | "scripts": { 19 | "test": "ava src/test.js", 20 | "semantic-release": "semantic-release" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/bahmutov/check-code-coverage.git" 25 | }, 26 | "keywords": [ 27 | "coverage", 28 | "code-coverage", 29 | "utility", 30 | "nyc" 31 | ], 32 | "author": "Gleb Bahmutov ", 33 | "license": "MIT", 34 | "bugs": { 35 | "url": "https://github.com/bahmutov/check-code-coverage/issues" 36 | }, 37 | "homepage": "https://github.com/bahmutov/check-code-coverage#readme", 38 | "devDependencies": { 39 | "ava": "3.15.0", 40 | "prettier": "2.8.8", 41 | "semantic-release": "19.0.5" 42 | }, 43 | "dependencies": { 44 | "arg": "4.1.3", 45 | "debug": "4.3.4", 46 | "got": "11.8.6", 47 | "lodash": "4.17.21" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "automerge": true, 4 | "major": { 5 | "automerge": false 6 | }, 7 | "prConcurrentLimit": 3, 8 | "prHourlyLimit": 2, 9 | "schedule": ["after 2am and before 3am on saturday"], 10 | "updateNotScheduled": false, 11 | "timezone": "America/New_York", 12 | "lockFileMaintenance": { 13 | "enabled": true 14 | }, 15 | "separatePatchReleases": true, 16 | "separateMultipleMajor": true, 17 | "masterIssue": true, 18 | "labels": ["type: dependencies", "renovate"] 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | const path = require('path') 4 | const fs = require('fs') 5 | const debug = require('debug')('check-code-coverage') 6 | 7 | /** 8 | * Reads coverage JSON file produced with NYC reporter "json-summary" 9 | * and returns total statements percentage. 10 | * @param {string} filename File to read, by default "coverage/coverage-summary.json" 11 | * @returns {number} Percentage from 0 to 100 12 | */ 13 | function readCoverage(filename) { 14 | debug('reading coverage from %o', { filename }) 15 | if (!filename) { 16 | filename = path.join(process.cwd(), 'coverage', 'coverage-summary.json') 17 | } 18 | debug('reading coverage summary from: %s', filename) 19 | const coverage = require(filename) 20 | return coverage.total.statements.pct 21 | } 22 | 23 | function toPercent(x) { 24 | if (typeof x !== 'number') { 25 | throw new Error(`Expected ${x} to be a number, not ${typeof x}`) 26 | } 27 | if (x < 0) { 28 | return 0 29 | } 30 | if (x > 100) { 31 | return 100 32 | } 33 | return x 34 | } 35 | 36 | const availableColors = ['red', 'yellow', 'green', 'brightgreen'] 37 | 38 | const availableColorsReStr = '(:?' + availableColors.join('|') + ')' 39 | 40 | function getCoverageRe() { 41 | // note, Shields.io escaped '-' with '--' 42 | // should match the expression in getCoverageFromText 43 | const coverageRe = new RegExp( 44 | `https://img\\.shields\\.io/badge/code--coverage-\\d+(\\.?\\d+)?%25-${availableColorsReStr}`, 45 | ) 46 | return coverageRe 47 | } 48 | 49 | function getColor(coveredPercent) { 50 | if (coveredPercent < 60) { 51 | return 'red' 52 | } 53 | if (coveredPercent < 80) { 54 | return 'yellow' 55 | } 56 | if (coveredPercent < 90) { 57 | return 'green' 58 | } 59 | return 'brightgreen' 60 | } 61 | 62 | function getCoverageBadge(pct) { 63 | const color = getColor(pct) || 'lightgrey' 64 | debug('for coverage %d% badge color "%s"', pct, color) 65 | 66 | const coverageBadge = `https://img.shields.io/badge/code--coverage-${pct}%25-${color}` 67 | return coverageBadge 68 | } 69 | 70 | function getCoverageFromReadme() { 71 | const readmeFilename = path.join(process.cwd(), 'README.md') 72 | debug('reading the README from %s', readmeFilename) 73 | const readmeText = fs.readFileSync(readmeFilename, 'utf8') 74 | return getCoverageFromText(readmeText) 75 | } 76 | 77 | /** 78 | * Given Markdown text, finds the code coverage badge and 79 | * extracts the percentage number. 80 | * @returns {number|undefined} Returns converted percentage if found 81 | */ 82 | function getCoverageFromText(text) { 83 | // should match the expression in "getCoverageRe" function 84 | const coverageRe = new RegExp( 85 | `https://img\\.shields\\.io/badge/code--coverage-(\\d+(\\.?\\d+)?)%25-${availableColorsReStr}`, 86 | ) 87 | const matches = coverageRe.exec(text) 88 | 89 | if (!matches) { 90 | console.log('Could not find coverage badge in the given text') 91 | console.log('text\n---\n' + text + '\n---') 92 | return 93 | } 94 | debug('coverage badge "%s" percentage "%s"', matches[0], matches[1]) 95 | const pct = toPercent(parseFloat(matches[1])) 96 | debug('parsed percentage: %d', pct) 97 | return pct 98 | } 99 | 100 | module.exports = { 101 | toPercent, 102 | readCoverage, 103 | badge: { 104 | availableColors, 105 | availableColorsReStr, 106 | getCoverageFromReadme, 107 | getCoverageFromText, 108 | getCoverageRe, 109 | getCoverageBadge 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const {getCoverageBadge, getCoverageFromText} = require('.').badge 3 | 4 | test('parses 100%', t => { 5 | const text = getCoverageBadge(100) 6 | const pct = getCoverageFromText(text) 7 | t.is(pct, 100, '100%') 8 | }); 9 | 10 | test('parses 99%', t => { 11 | const text = getCoverageBadge(99) 12 | const pct = getCoverageFromText(text) 13 | t.is(pct, 99, '99%') 14 | }); 15 | 16 | test('parses 99.99%', t => { 17 | const text = getCoverageBadge(99.99) 18 | const pct = getCoverageFromText(text) 19 | t.is(pct, 99.99, '99.99%') 20 | }); 21 | 22 | test('parses 60%', t => { 23 | const text = getCoverageBadge(60) 24 | const pct = getCoverageFromText(text) 25 | t.is(pct, 60, '60%') 26 | }); 27 | 28 | test('parses 2%', t => { 29 | const text = getCoverageBadge(2) 30 | const pct = getCoverageFromText(text) 31 | t.is(pct, 2, '2%') 32 | }); 33 | 34 | test('parses 100.0%', t => { 35 | const text = getCoverageBadge(100.0) 36 | const pct = getCoverageFromText(text) 37 | t.is(pct, 100, '100.0%') 38 | }); 39 | 40 | test('parses 80.25%', t => { 41 | const text = getCoverageBadge(80.25) 42 | const pct = getCoverageFromText(text) 43 | t.is(pct, 80.25, '80.25%') 44 | }); 45 | 46 | test('parses 32.5%', t => { 47 | const text = getCoverageBadge(32.5) 48 | const pct = getCoverageFromText(text) 49 | t.is(pct, 32.5, '32.5%') 50 | }); 51 | 52 | test('parses 2.5%', t => { 53 | const text = getCoverageBadge(2.5) 54 | const pct = getCoverageFromText(text) 55 | t.is(pct, 2.5, '2.5%') 56 | }); 57 | --------------------------------------------------------------------------------