diff --git a/services/sonar/sonar-base.js b/services/sonar/sonar-base.js index 709a06f757f4b..e5e4d1d65a7d5 100644 --- a/services/sonar/sonar-base.js +++ b/services/sonar/sonar-base.js @@ -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 } } diff --git a/services/sonar/sonar-coverage.service.js b/services/sonar/sonar-coverage.service.js index 47b9d21442fd0..d2a3c08ee8c80 100644 --- a/services/sonar/sonar-coverage.service.js +++ b/services/sonar/sonar-coverage.service.js @@ -60,7 +60,7 @@ module.exports = class SonarCoverage extends SonarBase { component, metricName: 'coverage', }) - const { metricValue: coverage } = this.transform({ + const { coverage } = this.transform({ json, sonarVersion, }) diff --git a/services/sonar/sonar-documented-api-density.service.js b/services/sonar/sonar-documented-api-density.service.js index cc210eebcf7c0..4d6508b4b9394 100644 --- a/services/sonar/sonar-documented-api-density.service.js +++ b/services/sonar/sonar-documented-api-density.service.js @@ -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] }) } } diff --git a/services/sonar/sonar-fortify-rating.service.js b/services/sonar/sonar-fortify-rating.service.js index cdf94daf60790..8ab7f46b04b04 100644 --- a/services/sonar/sonar-fortify-rating.service.js +++ b/services/sonar/sonar-fortify-rating.service.js @@ -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'], + }) } } diff --git a/services/sonar/sonar-generic.service.js b/services/sonar/sonar-generic.service.js index 7ad88a930c807..6eaddd0322618 100644 --- a/services/sonar/sonar-generic.service.js +++ b/services/sonar/sonar-generic.service.js @@ -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], + }) } } diff --git a/services/sonar/sonar-quality-gate.service.js b/services/sonar/sonar-quality-gate.service.js index a319d84506b0f..36ec81082b6ee 100644 --- a/services/sonar/sonar-quality-gate.service.js +++ b/services/sonar/sonar-quality-gate.service.js @@ -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 }) } } diff --git a/services/sonar/sonar-tech-debt.service.js b/services/sonar/sonar-tech-debt.service.js index 2bdf1a422bc47..4d88713adfaef 100644 --- a/services/sonar/sonar-tech-debt.service.js +++ b/services/sonar/sonar-tech-debt.service.js @@ -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 }) } } diff --git a/services/sonar/sonar-tests.service.js b/services/sonar/sonar-tests.service.js new file mode 100644 index 0000000000000..c105ce216ddf8 --- /dev/null +++ b/services/sonar/sonar-tests.service.js @@ -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] diff --git a/services/sonar/sonar-tests.spec.js b/services/sonar/sonar-tests.spec.js new file mode 100644 index 0000000000000..b2043ef5c7215 --- /dev/null +++ b/services/sonar/sonar-tests.spec.js @@ -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', + }) + }) +}) diff --git a/services/sonar/sonar-tests.tester.js b/services/sonar/sonar-tests.tester.js new file mode 100644 index 0000000000000..4f07859177d7a --- /dev/null +++ b/services/sonar/sonar-tests.tester.js @@ -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, + }) diff --git a/services/sonar/sonar-violations.service.js b/services/sonar/sonar-violations.service.js index 9ce49bc396ab4..d496f32c2553c 100644 --- a/services/sonar/sonar-violations.service.js +++ b/services/sonar/sonar-violations.service.js @@ -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(