Rewrite [codeclimate] coverage (#3316)

Attacking this in two pieces for ease of review. The legacy implementation for coverage is still there, though I disabled it via the route. That whole file will be removed in the next PR.

Ref #2863
This commit is contained in:
Paul Melnikow
2019-04-15 23:47:25 -04:00
committed by GitHub
parent 700b61e16b
commit 91d6dd6643
8 changed files with 224 additions and 71 deletions

View File

@@ -14,6 +14,7 @@ const { isValidRoute, prepareRoute, namedParamsForMatch } = require('./route')
const trace = require('./trace')
const attrSchema = Joi.object({
name: Joi.string().min(3),
category: isValidCategory,
route: isValidRoute,
transformPath: Joi.func()
@@ -30,6 +31,7 @@ const attrSchema = Joi.object({
module.exports = function redirector(attrs) {
const {
name,
category,
route,
transformPath,
@@ -51,9 +53,13 @@ module.exports = function redirector(attrs) {
}
static get name() {
return `${camelcase(route.base.replace(/\//g, '_'), {
pascalCase: true,
})}Redirect`
if (name) {
return name
} else {
return `${camelcase(route.base.replace(/\//g, '_'), {
pascalCase: true,
})}Redirect`
}
}
static register({ camp, requestCounter }) {

View File

@@ -24,6 +24,15 @@ describe('Redirector', function() {
expect(redirector(attrs).name).to.equal('VeryOldServiceRedirect')
})
it('overrides the name', function() {
expect(
redirector({
...attrs,
name: 'ShinyRedirect',
}).name
).to.equal('ShinyRedirect')
})
it('sets specified route', function() {
expect(redirector(attrs).route).to.deep.equal(route)
})

View File

@@ -0,0 +1,48 @@
'use strict'
const Joi = require('joi')
const { NotFound } = require('..')
const keywords = ['codeclimate']
const repoSchema = Joi.object({
data: Joi.array()
.max(1)
.items(
Joi.object({
id: Joi.string().required(),
relationships: Joi.object({
latest_default_branch_snapshot: Joi.object({
data: Joi.object({
id: Joi.string().required(),
}).allow(null),
}).required(),
latest_default_branch_test_report: Joi.object({
data: Joi.object({
id: Joi.string().required(),
}).allow(null),
}).required(),
}).required(),
})
)
.required(),
}).required()
async function fetchRepo(serviceInstance, { user, repo }) {
const {
data: [repoInfo],
} = await serviceInstance._requestJson({
schema: repoSchema,
url: 'https://api.codeclimate.com/v1/repos',
options: { qs: { github_slug: `${user}/${repo}` } },
})
if (repoInfo === undefined) {
throw new NotFound({ prettyMessage: 'repo not found' })
}
return repoInfo
}
module.exports = {
keywords,
fetchRepo,
}

View File

@@ -0,0 +1,27 @@
'use strict'
const { redirector } = require('..')
module.exports = [
redirector({
name: 'CodeclimateCoverageShortcutRedirect',
category: 'coverage',
route: {
base: 'codeclimate',
pattern: ':which(c|c-letter)/:user/:repo',
},
transformPath: ({ which, user, repo }) =>
`/codeclimate/${which.replace('c', 'coverage')}/${user}/${repo}`,
dateAdded: new Date('2019-04-15'),
}),
redirector({
name: 'CodeclimateTopLevelCoverageRedirect',
category: 'coverage',
route: {
base: 'codeclimate',
pattern: ':user/:repo',
},
transformPath: ({ user, repo }) => `/codeclimate/coverage/${user}/${repo}`,
dateAdded: new Date('2019-04-15'),
}),
]

View File

@@ -0,0 +1,30 @@
'use strict'
const { ServiceTester } = require('../tester')
const t = (module.exports = new ServiceTester({
id: 'CodeclimateCoverageRedirector',
title: 'Code Climate Coverage Redirector',
pathPrefix: '/codeclimate',
}))
t.create('Top-level coverage shortcut')
.get('/jekyll/jekyll.svg', {
followRedirect: false,
})
.expectStatus(301)
.expectHeader('Location', '/codeclimate/coverage/jekyll/jekyll.svg')
t.create('Coverage shortcut')
.get('/c/jekyll/jekyll.svg', {
followRedirect: false,
})
.expectStatus(301)
.expectHeader('Location', '/codeclimate/coverage/jekyll/jekyll.svg')
t.create('Coverage letter shortcut')
.get('/c-letter/jekyll/jekyll.svg', {
followRedirect: false,
})
.expectStatus(301)
.expectHeader('Location', '/codeclimate/coverage-letter/jekyll/jekyll.svg')

View File

@@ -0,0 +1,94 @@
'use strict'
const Joi = require('joi')
const { BaseJsonService, NotFound } = require('..')
const { coveragePercentage, letterScore } = require('../color-formatters')
const { keywords, fetchRepo } = require('./codeclimate-common')
const schema = Joi.object({
data: Joi.object({
attributes: Joi.object({
covered_percent: Joi.number().required(),
rating: Joi.object({
letter: Joi.equal('A', 'B', 'C', 'D', 'E', 'F').required(),
}).required(),
}).required(),
}).allow(null),
}).required()
module.exports = class CodeclimateCoverage extends BaseJsonService {
static get route() {
return {
base: 'codeclimate',
pattern: ':which(coverage|coverage-letter)/:user/:repo',
}
}
static get category() {
return 'coverage'
}
static get examples() {
return [
{
title: 'Code Climate coverage',
namedParams: { which: 'coverage', user: 'jekyll', repo: 'jekyll' },
staticPreview: this.render({
which: 'coverage',
percentage: 95.123,
letter: 'A',
}),
keywords,
},
]
}
async fetch({ user, repo }) {
const {
id: repoId,
relationships: {
latest_default_branch_test_report: { data: testReportInfo },
},
} = await fetchRepo(this, { user, repo })
if (testReportInfo === null) {
throw new NotFound({ prettyMessage: 'test report not found' })
}
const { data } = await this._requestJson({
schema,
url: `https://api.codeclimate.com/v1/repos/${repoId}/test_reports/${
testReportInfo.id
}`,
errorMessages: { 404: 'test report not found' },
})
return data
}
static render({ wantLetter, percentage, letter }) {
if (wantLetter) {
return {
message: letter,
color: letterScore(letter),
}
} else {
return {
message: `${percentage.toFixed(0)}%`,
color: coveragePercentage(percentage),
}
}
}
async handle({ which, user, repo }) {
const {
attributes: {
rating: { letter },
covered_percent: percentage,
},
} = await this.fetch({ user, repo })
return this.constructor.render({
wantLetter: which === 'coverage-letter',
letter,
percentage,
})
}
}

View File

@@ -1,53 +1,33 @@
'use strict'
const Joi = require('joi')
const { ServiceTester } = require('../tester')
const { isIntegerPercentage } = require('../test-validators')
const t = (module.exports = new ServiceTester({
id: 'CodeClimateCoverage',
title: 'Code Climate',
pathPrefix: '/codeclimate',
}))
const t = (module.exports = require('../tester').createServiceTester())
t.create('test coverage percentage')
.get('/c/jekyll/jekyll.json')
.expectBadge({
label: 'coverage',
message: isIntegerPercentage,
})
t.create('test coverage percentage alternative coverage URL')
.get('/coverage/jekyll/jekyll.json')
.expectBadge({
label: 'coverage',
message: isIntegerPercentage,
})
t.create('test coverage percentage alternative top-level URL')
.get('/jekyll/jekyll.json')
.expectBadge({
label: 'coverage',
message: isIntegerPercentage,
})
t.create('test coverage letter')
.get('/c-letter/jekyll/jekyll.json')
.get('/coverage-letter/jekyll/jekyll.json')
.expectBadge({
label: 'coverage',
message: Joi.equal('A', 'B', 'C', 'D', 'E', 'F'),
})
t.create('test coverage percentage for non-existent repo')
.get('/c/unknown/unknown.json')
.get('/coverage/unknown/unknown.json')
.expectBadge({
label: 'coverage',
message: 'not found',
message: 'repo not found',
})
t.create('test coverage percentage for repo without test reports')
.get('/c/angular/angular.js.json')
.get('/coverage/angular/angular.js.json')
.expectBadge({
label: 'coverage',
message: 'unknown',
message: 'test report not found',
})

View File

@@ -8,46 +8,6 @@ const {
colorScale,
} = require('../color-formatters')
class CodeclimateCoverage extends LegacyService {
static get route() {
return {
base: 'codeclimate',
pattern: ':which(coverage|coverage-letter)/:userRepo*',
}
}
static get category() {
return 'coverage'
}
static get examples() {
return [
{
title: 'Code Climate coverage',
pattern: 'coverage/:userRepo',
namedParams: { userRepo: 'jekyll/jekyll' },
staticPreview: {
label: 'coverage',
message: '95%',
color: 'green',
},
},
{
title: 'Code Climate coverage (letter)',
pattern: 'coverage-letter/:userRepo',
namedParams: { userRepo: 'jekyll/jekyll' },
staticPreview: {
label: 'coverage',
message: 'A',
color: 'brightgreen',
},
},
]
}
static registerLegacyRouteHandler() {}
}
// This legacy service should be rewritten to use e.g. BaseJsonService.
//
// Tips for rewriting:
@@ -106,7 +66,7 @@ class Codeclimate extends LegacyService {
static registerLegacyRouteHandler({ camp, cache }) {
camp.route(
/^\/codeclimate(\/(c|coverage|maintainability|issues|tech-debt)(-letter|-percentage)?)?\/(.+)\.(svg|png|gif|jpg|json)$/,
/^\/codeclimate(\/(maintainability|issues|tech-debt)(-letter|-percentage)?)\/(.+)\.(svg|png|gif|jpg|json)$/,
cache((data, match, sendBadge, request) => {
let type
if (match[2] === 'c' || !match[2]) {
@@ -232,6 +192,5 @@ class Codeclimate extends LegacyService {
}
module.exports = {
CodeclimateCoverage,
Codeclimate,
}