Skip to content

Commit

Permalink
feat: add badge for .nycrc config based coverage, run [nycrc] (#4759)
Browse files Browse the repository at this point in the history
* 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
bcoe committed Mar 18, 2020
1 parent 2de4c55 commit 3432421
Show file tree
Hide file tree
Showing 2 changed files with 266 additions and 0 deletions.
160 changes: 160 additions & 0 deletions services/nycrc/nycrc.service.js
@@ -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 })
}
}
106 changes: 106 additions & 0 deletions services/nycrc/nycrc.tester.js
@@ -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 })

0 comments on commit 3432421

Please sign in to comment.