add [Sonar] badges for various test metrics (#3571)

* feat: add SonarTests badges

* chore: remove commented out line

* refactor: update SonarTestsSummary to override transform
This commit is contained in:
Caleb Cartwright
2019-07-01 15:46:51 -05:00
committed by GitHub
parent 0cf00d963d
commit ea865436a1
11 changed files with 496 additions and 39 deletions

View File

@@ -80,12 +80,20 @@ module.exports = class SonarBase extends BaseJsonService {
transform({ json, sonarVersion }) {
const useLegacyApi = isLegacyVersion({ sonarVersion })
const rawValue = useLegacyApi
? json[0].msr[0].val
: json.component.measures[0].value
const value = parseInt(rawValue)
const metrics = {}
// Most values are numeric, but not all of them.
return { metricValue: value || rawValue }
if (useLegacyApi) {
json[0].msr.forEach(measure => {
// Most values are numeric, but not all of them.
metrics[measure.key] = parseInt(measure.val) || measure.val
})
} else {
json.component.measures.forEach(measure => {
// Most values are numeric, but not all of them.
metrics[measure.metric] = parseInt(measure.value) || measure.value
})
}
return metrics
}
}

View File

@@ -60,7 +60,7 @@ module.exports = class SonarCoverage extends SonarBase {
component,
metricName: 'coverage',
})
const { metricValue: coverage } = this.transform({
const { coverage } = this.transform({
json,
sonarVersion,
})

View File

@@ -63,7 +63,7 @@ module.exports = class SonarDocumentedApiDensity extends SonarBase {
component,
metricName: metric,
})
const { metricValue: density } = this.transform({ json, sonarVersion })
return this.constructor.render({ density })
const metrics = this.transform({ json, sonarVersion })
return this.constructor.render({ density: metrics[metric] })
}
}

View File

@@ -75,7 +75,9 @@ module.exports = class SonarFortifyRating extends SonarBase {
metricName: 'fortify-security-rating',
})
const { metricValue: rating } = this.transform({ json, sonarVersion })
return this.constructor.render({ rating })
const metrics = this.transform({ json, sonarVersion })
return this.constructor.render({
rating: metrics['fortify-security-rating'],
})
}
}

View File

@@ -89,16 +89,10 @@ const testsMetricNames = [
'coverage_line_hits_data',
'lines_to_cover',
'new_lines_to_cover',
'skipped_tests',
'uncovered_conditions',
'new_uncovered_conditions',
'uncovered_lines',
'new_uncovered_lines',
'tests',
'test_execution_time',
'test_errors',
'test_failures',
'test_success_density',
]
const metricNames = [
...complexityMetricNames,
@@ -146,7 +140,10 @@ module.exports = class SonarGeneric extends SonarBase {
metricName,
})
const { metricValue } = this.transform({ json, sonarVersion })
return this.constructor.render({ metricName, metricValue })
const metrics = this.transform({ json, sonarVersion })
return this.constructor.render({
metricName,
metricValue: metrics[metricName],
})
}
}

View File

@@ -66,7 +66,10 @@ module.exports = class SonarQualityGate extends SonarBase {
component,
metricName: 'alert_status',
})
const { metricValue: qualityState } = this.transform({ json, sonarVersion })
const { alert_status: qualityState } = this.transform({
json,
sonarVersion,
})
return this.constructor.render({ qualityState })
}
}

View File

@@ -67,7 +67,7 @@ module.exports = class SonarTechDebt extends SonarBase {
// Special condition for backwards compatibility.
metricName: 'sqale_debt_ratio',
})
const { metricValue: debt } = this.transform({ json, sonarVersion })
const { sqale_debt_ratio: debt } = this.transform({ json, sonarVersion })
return this.constructor.render({ debt, metric })
}
}

View File

@@ -0,0 +1,264 @@
'use strict'
const {
testResultQueryParamSchema,
renderTestResultBadge,
documentation: testResultsDocumentation,
} = require('../test-results')
const { metric: metricCount } = require('../text-formatters')
const SonarBase = require('./sonar-base')
const {
documentation,
keywords,
patternBase,
queryParamSchema,
getLabel,
} = require('./sonar-helpers')
class SonarTestsSummary extends SonarBase {
static get category() {
return 'build'
}
static get route() {
return {
base: 'sonar',
pattern: `${patternBase}/tests`,
queryParamSchema: queryParamSchema.concat(testResultQueryParamSchema),
}
}
static get examples() {
return [
{
title: 'Sonar Tests',
namedParams: {
protocol: 'http',
host: 'sonar.petalslink.com',
component: 'org.ow2.petals:petals-se-ase',
},
queryParams: {
sonarVersion: '4.2',
compact_message: null,
passed_label: 'passed',
failed_label: 'failed',
skipped_label: 'skipped',
},
staticPreview: this.render({
passed: 5,
failed: 1,
skipped: 0,
total: 6,
isCompact: false,
}),
keywords,
documentation: `
${documentation}
${testResultsDocumentation}
`,
},
]
}
static get defaultBadgeData() {
return {
label: 'tests',
}
}
static render({
passed,
failed,
skipped,
total,
passedLabel,
failedLabel,
skippedLabel,
isCompact,
}) {
return renderTestResultBadge({
passed,
failed,
skipped,
total,
passedLabel,
failedLabel,
skippedLabel,
isCompact,
})
}
transform({ json, sonarVersion }) {
const {
tests: total,
skipped_tests: skipped,
test_failures: failed,
} = super.transform({
json,
sonarVersion,
})
return {
total,
passed: total - (skipped + failed),
failed,
skipped,
}
}
async handle(
{ protocol, host, component },
{
sonarVersion,
compact_message: compactMessage,
passed_label: passedLabel,
failed_label: failedLabel,
skipped_label: skippedLabel,
}
) {
const json = await this.fetch({
sonarVersion,
protocol,
host,
component,
metricName: 'tests,test_failures,skipped_tests',
})
const { total, passed, failed, skipped } = this.transform({
json,
sonarVersion,
})
return this.constructor.render({
passed,
failed,
skipped,
total,
isCompact: compactMessage !== undefined,
passedLabel,
failedLabel,
skippedLabel,
})
}
}
class SonarTests extends SonarBase {
static get category() {
return 'build'
}
static get route() {
return {
base: 'sonar',
pattern: `${patternBase}/:metric(total_tests|skipped_tests|test_failures|test_errors|test_execution_time|test_success_density)`,
queryParamSchema,
}
}
static get examples() {
return [
{
title: 'Sonar Test Count',
pattern: `${patternBase}/:metric(total_tests|skipped_tests|test_failures|test_errors)`,
namedParams: {
protocol: 'http',
host: 'sonar.petalslink.com',
component: 'org.ow2.petals:petals-log',
metric: 'total_tests',
},
queryParams: {
sonarVersion: '4.2',
},
staticPreview: this.render({
metric: 'total_tests',
value: 131,
}),
keywords,
documentation,
},
{
title: 'Sonar Test Execution Time',
pattern: `${patternBase}/test_execution_time`,
namedParams: {
protocol: 'https',
host: 'sonarcloud.io',
component: 'swellaby:azure-pipelines-templates',
},
queryParams: {
sonarVersion: '4.2',
},
staticPreview: this.render({
metric: 'test_execution_time',
value: 2,
}),
keywords,
documentation,
},
{
title: 'Sonar Test Success Rate',
pattern: `${patternBase}/test_success_density`,
namedParams: {
protocol: 'https',
host: 'sonarcloud.io',
component: 'swellaby:azure-pipelines-templates',
},
queryParams: {
sonarVersion: '4.2',
},
staticPreview: this.render({
metric: 'test_success_density',
value: 100,
}),
keywords,
documentation,
},
]
}
static get defaultBadgeData() {
return {
label: 'tests',
}
}
static render({ value, metric }) {
let color = 'blue'
let label = getLabel({ metric })
let message = metricCount(value)
if (metric === 'test_failures' || metric === 'test_errors') {
color = value === 0 ? 'brightgreen' : 'red'
} else if (metric === 'test_success_density') {
color = value === 100 ? 'brightgreen' : 'red'
label = 'tests'
message = `${value}%`
}
return {
label,
message,
color,
}
}
async handle({ protocol, host, component, metric }, { sonarVersion }) {
const json = await this.fetch({
sonarVersion,
protocol,
host,
component,
// We're using 'tests' as the metric key to provide our standard
// formatted test badge (passed, failed, skipped) that exists for other
// services. Therefore, we're exposing 'total_tests' to the user, and
// need to map that to the 'tests' metric which sonar uses to represent the
// total number of tests.
// https://docs.sonarqube.org/latest/user-guide/metric-definitions/
metricName: metric === 'total_tests' ? 'tests' : metric,
})
const metrics = this.transform({ json, sonarVersion })
return this.constructor.render({
value: metrics[metric === 'total_tests' ? 'tests' : metric],
metric,
})
}
}
module.exports = [SonarTestsSummary, SonarTests]

View File

@@ -0,0 +1,39 @@
'use strict'
const { test, given } = require('sazerac')
const SonarTests = require('./sonar-tests.service')[1]
describe('SonarTests', function() {
test(SonarTests.render, () => {
given({ value: 0, metric: 'test_failures' }).expect({
label: 'test failures',
message: '0',
color: 'brightgreen',
})
given({ value: 0, metric: 'test_errors' }).expect({
label: 'test errors',
message: '0',
color: 'brightgreen',
})
given({ value: 2, metric: 'test_failures' }).expect({
label: 'test failures',
message: '2',
color: 'red',
})
given({ value: 1, metric: 'test_errors' }).expect({
label: 'test errors',
message: '1',
color: 'red',
})
given({ value: 100, metric: 'test_success_density' }).expect({
label: 'tests',
message: '100%',
color: 'brightgreen',
})
given({ value: 93, metric: 'test_success_density' }).expect({
label: 'tests',
message: '93%',
color: 'red',
})
})
})

View File

@@ -0,0 +1,159 @@
'use strict'
const Joi = require('@hapi/joi')
const { ServiceTester } = require('../tester')
const t = (module.exports = new ServiceTester({
id: 'SonarTests',
title: 'SonarTests',
pathPrefix: '/sonar',
}))
const {
isDefaultTestTotals,
isDefaultCompactTestTotals,
isCustomTestTotals,
isCustomCompactTestTotals,
} = require('../test-validators')
const { isIntegerPercentage, isMetric } = require('../test-validators')
t.create('Tests')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azure-pipelines-templates/tests.json')
.expectBadge({
label: 'tests',
message: isDefaultTestTotals,
})
t.create('Tests (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/tests.json?sonarVersion=4.2'
)
.expectBadge({
label: 'tests',
message: isDefaultTestTotals,
})
t.create('Tests with compact message')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azure-pipelines-templates/tests.json', {
qs: { compact_message: null },
})
.expectBadge({ label: 'tests', message: isDefaultCompactTestTotals })
t.create('Tests with custom labels')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azure-pipelines-templates/tests.json', {
qs: {
passed_label: 'good',
failed_label: 'bad',
skipped_label: 'n/a',
},
})
.expectBadge({ label: 'tests', message: isCustomTestTotals })
t.create('Tests with compact message and custom labels')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azure-pipelines-templates/tests.json', {
qs: {
compact_message: null,
passed_label: '💃',
failed_label: '🤦‍♀️',
skipped_label: '🤷',
},
})
.expectBadge({
label: 'tests',
message: isCustomCompactTestTotals,
})
t.create('Total Test Count')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azdo-shellcheck/total_tests.json')
.expectBadge({
label: 'total tests',
message: isMetric,
})
t.create('Total Test Count (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/total_tests.json?sonarVersion=4.2'
)
.expectBadge({
label: 'total tests',
message: isMetric,
})
t.create('Test Failures Count')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azdo-shellcheck/test_failures.json')
.expectBadge({
label: 'test failures',
message: Joi.alternatives(isMetric, 0),
})
t.create('Test Failures Count (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/test_failures.json?sonarVersion=4.2'
)
.expectBadge({
label: 'test failures',
message: Joi.alternatives(isMetric, 0),
})
t.create('Test Errors Count')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azdo-shellcheck/test_errors.json')
.expectBadge({
label: 'test errors',
message: Joi.alternatives(isMetric, 0),
})
t.create('Test Errors Count (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/test_errors.json?sonarVersion=4.2'
)
.expectBadge({
label: 'test errors',
message: Joi.alternatives(isMetric, 0),
})
t.create('Skipped Tests Count')
.timeout(10000)
.get('/https/sonarcloud.io/swellaby:azdo-shellcheck/skipped_tests.json')
.expectBadge({
label: 'skipped tests',
message: Joi.alternatives(isMetric, 0),
})
t.create('Skipped Tests Count (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/skipped_tests.json?sonarVersion=4.2'
)
.expectBadge({
label: 'skipped tests',
message: Joi.alternatives(isMetric, 0),
})
t.create('Test Success Rate')
.timeout(10000)
.get(
'/https/sonarcloud.io/swellaby:azdo-shellcheck/test_success_density.json'
)
.expectBadge({
label: 'tests',
message: isIntegerPercentage,
})
t.create('Test Success Rate (legacy API supported)')
.timeout(10000)
.get(
'/http/sonar.petalslink.com/org.ow2.petals%3Apetals-se-ase/test_success_density.json?sonarVersion=4.2'
)
.expectBadge({
label: 'tests',
message: isIntegerPercentage,
})

View File

@@ -6,7 +6,6 @@ const SonarBase = require('./sonar-base')
const {
getLabel,
documentation,
isLegacyVersion,
keywords,
patternBase,
queryParamWithFormatSchema,
@@ -149,26 +148,12 @@ module.exports = class SonarViolations extends SonarBase {
}
transformViolations({ json, sonarVersion, metric, format }) {
// We can use the standard transform function in all cases
// except when the requested badge is the long format of violations
const metrics = this.transform({ json, sonarVersion })
if (metric !== 'violations' || format !== 'long') {
const { metricValue: violations } = this.transform({ json, sonarVersion })
return { violations }
return { violations: metrics[metric] }
}
const useLegacyApi = isLegacyVersion({ sonarVersion })
const measures = useLegacyApi ? json[0].msr : json.component.measures
const violations = {}
measures.forEach(measure => {
if (useLegacyApi) {
violations[measure.key] = measure.val
} else {
violations[measure.metric] = measure.value
}
})
return { violations }
return { violations: metrics }
}
async handle(