Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add badge for .nycrc config based coverage, run [nycrc] (#4759)
* feat: add badge for .nycrc coverage * chore: address code review * chore: address additional review * chore: address a tiny bit more of review * chore: address edge case * chore: address LGTM review
- Loading branch information
Showing
2 changed files
with
266 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
'use strict' | ||
|
||
const Joi = require('@hapi/joi') | ||
const { coveragePercentage } = require('../color-formatters') | ||
const { | ||
ConditionalGithubAuthV3Service, | ||
} = require('../github/github-auth-service') | ||
const { fetchJsonFromRepo } = require('../github/github-common-fetch') | ||
const { InvalidParameter, InvalidResponse, NotFound } = require('..') | ||
|
||
const nycrcSchema = Joi.object({ | ||
branches: Joi.number() | ||
.min(0) | ||
.max(100), | ||
lines: Joi.number() | ||
.min(0) | ||
.max(100), | ||
functions: Joi.number() | ||
.min(0) | ||
.max(100), | ||
}).required() | ||
|
||
const pkgJSONSchema = Joi.object({ | ||
c8: Joi.object({ | ||
branches: Joi.number() | ||
.min(0) | ||
.max(100), | ||
lines: Joi.number() | ||
.min(0) | ||
.max(100), | ||
functions: Joi.number() | ||
.min(0) | ||
.max(100), | ||
}).optional(), | ||
nyc: Joi.object({ | ||
branches: Joi.number() | ||
.min(0) | ||
.max(100), | ||
lines: Joi.number() | ||
.min(0) | ||
.max(100), | ||
functions: Joi.number() | ||
.min(0) | ||
.max(100), | ||
}).optional(), | ||
}).required() | ||
|
||
const documentation = `<p> | ||
Create a code coverage badge, based on thresholds stored in a | ||
<a href="https://github.com/istanbuljs/nyc#common-configuration-options">.nycrc config file</a> | ||
on GitHub. | ||
</p>` | ||
|
||
const validThresholds = ['branches', 'lines', 'functions'] | ||
|
||
module.exports = class Nycrc extends ConditionalGithubAuthV3Service { | ||
static get category() { | ||
return 'coverage' | ||
} | ||
|
||
static get route() { | ||
return { | ||
base: 'nycrc', | ||
// TODO: eventually add support for .yml and .yaml: | ||
pattern: ':user/:repo', | ||
queryParamSchema: Joi.object({ | ||
config: Joi.string() | ||
.regex(/(.*\.nycrc)|(.*\.json$)/) | ||
.default('.nycrc'), | ||
// Allow the default threshold detection logic to be overridden, .e.g., | ||
// favoring lines over branches: | ||
preferredThreshold: Joi.string() | ||
.optional() | ||
.allow(...validThresholds), | ||
}).required(), | ||
} | ||
} | ||
|
||
static get examples() { | ||
return [ | ||
{ | ||
title: 'nycrc config on GitHub', | ||
namedParams: { user: 'yargs', repo: 'yargs' }, | ||
queryParams: { config: '.nycrc', preferredThreshold: 'lines' }, | ||
staticPreview: this.render({ coverage: 92 }), | ||
documentation, | ||
}, | ||
] | ||
} | ||
|
||
static get defaultBadgeData() { | ||
return { label: 'min coverage' } | ||
} | ||
|
||
static render({ coverage }) { | ||
return { | ||
message: `${coverage.toFixed(0)}%`, | ||
color: coveragePercentage(coverage), | ||
} | ||
} | ||
|
||
extractThreshold(config, preferredThreshold) { | ||
const { branches, lines } = config | ||
if (preferredThreshold) { | ||
if (!validThresholds.includes(preferredThreshold)) { | ||
throw new InvalidParameter({ | ||
prettyMessage: `threshold must be "branches", "lines", or "functions"`, | ||
}) | ||
} | ||
if (!config[preferredThreshold]) { | ||
throw new InvalidResponse({ | ||
prettyMessage: `"${preferredThreshold}" threshold missing`, | ||
}) | ||
} | ||
return config[preferredThreshold] | ||
} else if (branches || lines) { | ||
// We favor branches over lines for the coverage badge, if both | ||
// thresholds are provided (as branches is the stricter requirement): | ||
return branches || lines | ||
} else { | ||
throw new InvalidResponse({ | ||
prettyMessage: '"branches" or "lines" threshold missing', | ||
}) | ||
} | ||
} | ||
|
||
async handle({ user, repo }, queryParams) { | ||
const { config, preferredThreshold } = queryParams | ||
let coverage | ||
if (config.includes('package.json')) { | ||
const pkgJson = await fetchJsonFromRepo(this, { | ||
schema: pkgJSONSchema, | ||
user, | ||
repo, | ||
branch: 'master', | ||
filename: config, | ||
}) | ||
const nycConfig = pkgJson.c8 || pkgJson.nyc | ||
if (!nycConfig) { | ||
throw new NotFound({ | ||
prettyMessage: 'no nyc or c8 stanza found', | ||
}) | ||
} else { | ||
coverage = this.extractThreshold(nycConfig, preferredThreshold) | ||
} | ||
} else { | ||
coverage = this.extractThreshold( | ||
await fetchJsonFromRepo(this, { | ||
schema: nycrcSchema, | ||
user, | ||
repo, | ||
branch: 'master', | ||
filename: config, | ||
}), | ||
preferredThreshold | ||
) | ||
} | ||
return this.constructor.render({ coverage }) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
'use strict' | ||
|
||
const { isIntegerPercentage } = require('../test-validators') | ||
const t = (module.exports = require('../tester').createServiceTester()) | ||
|
||
t.create('valid .nycrc') | ||
.get('/yargs/yargs.json?config=.nycrc') | ||
.expectBadge({ label: 'min coverage', message: isIntegerPercentage }) | ||
|
||
t.create('.nycrc is default') | ||
.get('/yargs/yargs.json') | ||
.expectBadge({ label: 'min coverage', message: isIntegerPercentage }) | ||
|
||
t.create('alternate threshold is specified') | ||
.get('/yargs/yargs.json?preferredThreshold=lines') | ||
.expectBadge({ label: 'min coverage', message: '100%' }) | ||
|
||
t.create('invalid threshold is specified') | ||
.get('/yargs/yargs.json?preferredThreshold=blerg') | ||
.expectBadge({ | ||
label: 'min coverage', | ||
message: 'threshold must be "branches", "lines", or "functions"', | ||
}) | ||
|
||
t.create('.nycrc in monorepo') | ||
.get('/yargs/yargs.json?config=packages/foo/.nycrc.json') | ||
.intercept(nock => | ||
nock('https://api.github.com') | ||
.get('/repos/yargs/yargs/contents/packages/foo/.nycrc.json?ref=master') | ||
.reply(200, { | ||
content: Buffer.from( | ||
JSON.stringify({ | ||
lines: 99, | ||
}) | ||
).toString('base64'), | ||
encoding: 'base64', | ||
}) | ||
) | ||
.expectBadge({ label: 'min coverage', message: isIntegerPercentage }) | ||
|
||
t.create('.nycrc with no thresholds') | ||
.get('/yargs/yargs.json?config=.nycrc') | ||
.intercept(nock => | ||
nock('https://api.github.com') | ||
.get('/repos/yargs/yargs/contents/.nycrc?ref=master') | ||
.reply(200, { | ||
content: Buffer.from( | ||
JSON.stringify({ | ||
reporter: 'foo', | ||
}) | ||
).toString('base64'), | ||
encoding: 'base64', | ||
}) | ||
) | ||
.expectBadge({ | ||
label: 'min coverage', | ||
message: '"branches" or "lines" threshold missing', | ||
}) | ||
|
||
t.create('package.json with nyc stanza') | ||
.get('/yargs/yargs.json?config=package.json') | ||
.intercept(nock => | ||
nock('https://api.github.com') | ||
.get('/repos/yargs/yargs/contents/package.json?ref=master') | ||
.reply(200, { | ||
content: Buffer.from( | ||
JSON.stringify({ | ||
nyc: { | ||
lines: 99, | ||
}, | ||
}) | ||
).toString('base64'), | ||
encoding: 'base64', | ||
}) | ||
) | ||
.expectBadge({ label: 'min coverage', message: isIntegerPercentage }) | ||
|
||
t.create('package.json with nyc stanza, but no thresholds') | ||
.get('/yargs/yargs.json?config=package.json') | ||
.intercept(nock => | ||
nock('https://api.github.com') | ||
.get('/repos/yargs/yargs/contents/package.json?ref=master') | ||
.reply(200, { | ||
content: Buffer.from( | ||
JSON.stringify({ | ||
nyc: {}, | ||
}) | ||
).toString('base64'), | ||
encoding: 'base64', | ||
}) | ||
) | ||
.expectBadge({ | ||
label: 'min coverage', | ||
message: '"branches" or "lines" threshold missing', | ||
}) | ||
|
||
t.create('package.json with no nyc stanza') | ||
.get('/badges/shields.json?config=package.json') | ||
.expectBadge({ | ||
label: 'min coverage', | ||
message: 'no nyc or c8 stanza found', | ||
}) | ||
|
||
t.create('arbitrary JSON file, matching .nycrc format') | ||
.get('/swellaby/nyc-config.json?config=partial-coverage.json') | ||
.expectBadge({ label: 'min coverage', message: isIntegerPercentage }) |