[Jenkins] coverage badges rewrite (#2154)

This commit is contained in:
Pyves
2018-10-11 21:42:47 +01:00
committed by GitHub
parent 89ca13a5d6
commit 16dc2ec3a9
6 changed files with 375 additions and 209 deletions

View File

@@ -203,16 +203,6 @@ const allBadgeExamples = [
previewUrl:
'/jenkins/t/https/jenkins.qa.ubuntu.com/view/Precise/view/All%20Precise/job/precise-desktop-amd64_default.svg',
},
{
title: 'Jenkins Coverage (Cobertura)',
previewUrl:
'/jenkins/c/https/jenkins.qa.ubuntu.com/view/Utopic/view/All/job/address-book-service-utopic-i386-ci.svg',
},
{
title: 'Jenkins Coverage (Jacoco)',
previewUrl:
'/jenkins/j/https/jenkins.qa.ubuntu.com/view/Utopic/view/All/job/address-book-service-utopic-i386-ci.svg',
},
{
title: 'Bitbucket Pipelines',
previewUrl: '/bitbucket/pipelines/atlassian/adf-builder-javascript.svg',

View File

@@ -1,80 +1,163 @@
'use strict'
const LegacyService = require('../legacy-service')
const { makeBadgeData: getBadgeData } = require('../../lib/badge-data')
const Joi = require('joi')
const BaseJsonService = require('../base-json')
const serverSecrets = require('../../lib/server-secrets')
const { checkErrorResponse } = require('../../lib/error-helper')
const {
coveragePercentage: coveragePercentageColor,
} = require('../../lib/color-formatters')
// For Jenkins coverage (cobertura + jacoco).
module.exports = class JenkinsCoverage extends LegacyService {
static registerLegacyRouteHandler({ camp, cache }) {
camp.route(
/^\/jenkins(?:-ci)?\/(c|j)\/(http(?:s)?)\/([^/]+)\/(.+)\.(svg|png|gif|jpg|json)$/,
cache((data, match, sendBadge, request) => {
const type = match[1] // c - cobertura | j - jacoco
const scheme = match[2] // http(s)
const host = match[3] // example.org:8080
const job = match[4] // folder/job
const format = match[5]
const options = {
json: true,
uri: `${scheme}://${host}/job/${job}/`,
}
const jacocoCoverageSchema = Joi.object({
instructionCoverage: Joi.object({
percentage: Joi.number()
.min(0)
.max(100)
.required(),
}).required(),
}).required()
if (job.indexOf('/') > -1) {
options.uri = `${scheme}://${host}/${job}/`
}
switch (type) {
case 'c':
options.uri +=
'lastBuild/cobertura/api/json?tree=results[elements[name,denominator,numerator,ratio]]'
break
case 'j':
options.uri +=
'lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'
break
}
if (serverSecrets && serverSecrets.jenkins_user) {
options.auth = {
user: serverSecrets.jenkins_user,
pass: serverSecrets.jenkins_pass,
}
}
const badgeData = getBadgeData('coverage', data)
request(options, (err, res, json) => {
if (checkErrorResponse(badgeData, err, res)) {
sendBadge(format, badgeData)
return
}
try {
const coverageObject = json.instructionCoverage
if (coverageObject === undefined) {
badgeData.text[1] = 'inaccessible'
sendBadge(format, badgeData)
return
}
const coverage = coverageObject.percentage
if (isNaN(coverage)) {
badgeData.text[1] = 'unknown'
sendBadge(format, badgeData)
return
}
badgeData.text[1] = coverage.toFixed(0) + '%'
badgeData.colorscheme = coveragePercentageColor(coverage)
sendBadge(format, badgeData)
} catch (e) {
badgeData.text[1] = 'invalid'
sendBadge(format, badgeData)
}
const coberturaCoverageSchema = Joi.object({
results: Joi.object({
elements: Joi.array()
.items(
Joi.object({
name: 'Lines',
ratio: Joi.number()
.min(0)
.max(100)
.required(),
})
})
)
)
.min(1)
.required(),
}).required(),
}).required()
class BaseJenkinsCoverage extends BaseJsonService {
async fetch({ url, options, schema }) {
return this._requestJson({
url,
options,
schema,
errorMessages: {
404: 'job or coverage not found',
},
})
}
static render({ coverage }) {
return {
message: `${coverage.toFixed(0)}%`,
color: coveragePercentageColor(coverage),
}
}
static get defaultBadgeData() {
return { label: 'coverage' }
}
static get category() {
return 'build'
}
static buildUrl(scheme, host, job, plugin) {
return `${scheme}://${host}/job/${job}/lastBuild/${plugin}/api/json`
}
static buildOptions(treeParam) {
const options = {
qs: {
tree: treeParam,
},
}
if (serverSecrets && serverSecrets.jenkins_user) {
options.auth = {
user: serverSecrets.jenkins_user,
pass: serverSecrets.jenkins_pass,
}
}
return options
}
}
class JacocoJenkinsCoverage extends BaseJenkinsCoverage {
async handle({ scheme, host, job }) {
const url = this.constructor.buildUrl(scheme, host, job, 'jacoco')
const options = this.constructor.buildOptions(
'instructionCoverage[percentage]'
)
const json = await this.fetch({
url,
options,
schema: jacocoCoverageSchema,
})
return this.constructor.render({
coverage: json.instructionCoverage.percentage,
})
}
static get url() {
return {
base: 'jenkins/j',
format: '(http(?:s)?)/([^/]+)/(?:job/)?(.+)',
capture: ['scheme', 'host', 'job'],
}
}
static get examples() {
return [
{
title: 'Jenkins JaCoCo coverage',
exampleUrl: 'https/ci.eclipse.org/ecp/job/gerrit',
urlPattern: ':scheme/:host/:job',
staticExample: this.render({
coverage: 96,
}),
},
]
}
}
class CoberturaJenkinsCoverage extends BaseJenkinsCoverage {
async handle({ scheme, host, job }) {
const url = this.constructor.buildUrl(scheme, host, job, 'cobertura')
const options = this.constructor.buildOptions(
'results[elements[name,ratio]]'
)
const json = await this.fetch({
url,
options,
schema: coberturaCoverageSchema,
})
return this.constructor.render({
coverage: json.results.elements[0].ratio,
})
}
static get url() {
return {
base: 'jenkins/c',
format: '(http(?:s)?)/([^/]+)/(?:job/)?(.+)',
capture: ['scheme', 'host', 'job'],
}
}
static get examples() {
return [
{
title: 'Jenkins Cobertura coverage',
exampleUrl: 'https/builds.apache.org/job/olingo-odata4-cobertura',
urlPattern: ':scheme/:host/:job',
staticExample: this.render({
coverage: 94,
}),
},
]
}
}
module.exports = {
JacocoJenkinsCoverage,
CoberturaJenkinsCoverage,
}

View File

@@ -0,0 +1,170 @@
'use strict'
const ServiceTester = require('../service-tester')
const t = new ServiceTester({
id: 'jenkins-coverage',
title: 'JenkinsCoverage',
pathPrefix: '/jenkins',
})
module.exports = t
t.create('jacoco: valid coverage')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage%5Bpercentage%5D'
)
.reply(200, {
instructionCoverage: {
percentage: 81,
},
})
)
.expectJSON({ name: 'coverage', value: '81%' })
t.create(
'jacoco: valid coverage (badge URL without leading /job after Jenkins host)'
)
.get('/j/https/updates.jenkins-ci.org/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage%5Bpercentage%5D'
)
.reply(200, {
instructionCoverage: {
percentage: 81,
},
})
)
.expectJSON({ name: 'coverage', value: '81%' })
t.create('jacoco: invalid data response (no instructionCoverage object)')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage%5Bpercentage%5D'
)
.reply(200, {
invalidCoverageObject: {
percentage: 81,
},
})
)
.expectJSON({ name: 'coverage', value: 'invalid response data' })
t.create('jacoco: invalid data response (non numeric coverage)')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage%5Bpercentage%5D'
)
.reply(200, {
instructionCoverage: {
percentage: 'non-numeric',
},
})
)
.expectJSON({ name: 'coverage', value: 'invalid response data' })
t.create('jacoco: job not found')
.get('/j/https/updates.jenkins-ci.org/job/does-not-exist.json')
.expectJSON({ name: 'coverage', value: 'job or coverage not found' })
t.create('cobertura: valid coverage')
.get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/cobertura/api/json?tree=results%5Belements%5Bname%2Cratio%5D%5D'
)
.reply(200, {
results: {
elements: [
{
name: 'Conditionals',
ratio: 95.146,
},
{
name: 'Lines',
ratio: 63.745,
},
],
},
})
)
.expectJSON({ name: 'coverage', value: '64%' })
t.create(
'cobertura: valid coverage (badge URL without leading /job after Jenkins host)'
)
.get('/c/https/updates.jenkins-ci.org/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/cobertura/api/json?tree=results%5Belements%5Bname%2Cratio%5D%5D'
)
.reply(200, {
results: {
elements: [
{
name: 'Conditionals',
ratio: 95.146,
},
{
name: 'Lines',
ratio: 63.745,
},
],
},
})
)
.expectJSON({ name: 'coverage', value: '64%' })
t.create('cobertura: invalid data response (non-numeric coverage)')
.get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/cobertura/api/json?tree=results%5Belements%5Bname%2Cratio%5D%5D'
)
.reply(200, {
results: {
elements: [
{
name: 'Lines',
ratio: 'non-numeric',
},
],
},
})
)
.expectJSON({ name: 'coverage', value: 'invalid response data' })
t.create('cobertura: invalid data response (missing line coverage)')
.get('/c/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/cobertura/api/json?tree=results%5Belements%5Bname%2Cratio%5D%5D'
)
.reply(200, {
results: {
elements: [
{
name: 'Conditionals',
ratio: 95.146,
},
],
},
})
)
.expectJSON({ name: 'coverage', value: 'invalid response data' })
t.create('cobertura: job not found')
.get('/c/https/updates.jenkins-ci.org/job/does-not-exist.json')
.expectJSON({ name: 'coverage', value: 'job or coverage not found' })

View File

@@ -0,0 +1,53 @@
'use strict'
const Joi = require('joi')
const ServiceTester = require('../service-tester')
const t = new ServiceTester({
id: 'jenkins-plugin',
title: 'JenkinsPluginVersion',
pathPrefix: '/jenkins',
})
module.exports = t
t.create('latest version')
.get('/plugin/v/blueocean.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
)
.expectJSONTypes(
Joi.object().keys({
name: 'plugin',
value: Joi.string().regex(/^v(.*)$/),
})
)
t.create('version 0')
.get('/plugin/v/blueocean.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '0' } } })
)
.expectJSONTypes(
Joi.object().keys({
name: 'plugin',
value: Joi.string().regex(/^v0$/),
})
)
t.create('inexistent artifact')
.get('/plugin/v/inexistent-artifact-id.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
)
.expectJSON({ name: 'plugin', value: 'not found' })
t.create('connection error')
.get('/plugin/v/blueocean.json')
.networkOff()
.expectJSON({ name: 'plugin', value: 'inaccessible' })

View File

@@ -1,130 +0,0 @@
'use strict'
const Joi = require('joi')
const ServiceTester = require('../service-tester')
const t = new ServiceTester({ id: 'jenkins', title: 'Jenkins' })
module.exports = t
t.create('cobertura: latest version')
.get('/plugin/v/blueocean.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
)
.expectJSONTypes(
Joi.object().keys({
name: 'plugin',
value: Joi.string().regex(/^v(.*)$/),
})
)
t.create('cobertura: version 0')
.get('/plugin/v/blueocean.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '0' } } })
)
.expectJSONTypes(
Joi.object().keys({
name: 'plugin',
value: Joi.string().regex(/^v0$/),
})
)
t.create('cobertura: inexistent artifact')
.get('/plugin/v/inexistent-artifact-id.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get('/current/update-center.actual.json')
.reply(200, { plugins: { blueocean: { version: '1.1.6' } } })
)
.expectJSON({ name: 'plugin', value: 'not found' })
t.create('cobertura: connection error')
.get('/plugin/v/blueocean.json')
.networkOff()
.expectJSON({ name: 'plugin', value: 'inaccessible' })
t.create('jacoco: 81% | valid coverage')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'
)
.reply(200, {
instructionCoverage: {
covered: 39498,
missed: 9508,
percentage: 81,
percentageFloat: 80.5983,
total: 49006,
},
})
)
.expectJSONTypes({ name: 'coverage', value: '81%' })
t.create('jacoco: inaccessible | request error')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.networkOff()
.expectJSONTypes({ name: 'coverage', value: 'inaccessible' })
t.create('jacoco: inaccessible | invalid coverage object')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'
)
.reply(200, {
invalidCoverageObject: {
covered: 39498,
missed: 9508,
percentage: 81,
percentageFloat: 80.5983,
total: 49006,
},
})
)
.expectJSONTypes({ name: 'coverage', value: 'inaccessible' })
t.create('jacoco: unknown | invalid coverage (non-numeric)')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'
)
.reply(200, {
instructionCoverage: {
covered: 39498,
missed: 9508,
percentage: 'non-numeric',
percentageFloat: 80.5983,
total: 49006,
},
})
)
.expectJSONTypes({ name: 'coverage', value: 'unknown' })
t.create('jacoco: unknown | exception handling')
.get('/j/https/updates.jenkins-ci.org/job/hello-project/job/master.json')
.intercept(nock =>
nock('https://updates.jenkins-ci.org')
.get(
'/job/hello-project/job/master/lastBuild/jacoco/api/json?tree=instructionCoverage[covered,missed,percentage,total]'
)
.reply(200, {
instructionCoverage: {
covered: 39498,
missed: 9508,
percentage: '81.x',
percentageFloat: 80.5983,
total: 49006,
},
})
)
.expectJSONTypes({ name: 'coverage', value: 'unknown' })