@@ -0,0 +1,68 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2019 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
import { | |||
mockMeasureEnhanced, | |||
mockMetric, | |||
mockQualityGateStatusCondition | |||
} from '../../../helpers/testMocks'; | |||
import { getThreshold } from '../utils'; | |||
describe('getThreshold', () => { | |||
it('return undefined if condition is not found', () => { | |||
expect(getThreshold([], '')).toBeUndefined(); | |||
expect(getThreshold([mockMeasure()], '')).toBeUndefined(); | |||
expect( | |||
getThreshold( | |||
[ | |||
{ | |||
metric: mockMetric({ key: 'quality_gate_details' }), | |||
value: 'badly typed json should fail' | |||
} | |||
], | |||
'' | |||
) | |||
).toBeUndefined(); | |||
}); | |||
it('should return the threshold for the right metric', () => { | |||
expect(getThreshold([mockMeasure()], 'new_coverage')).toBe(85); | |||
expect(getThreshold([mockMeasure()], 'new_duplicated_lines_density')).toBe(5); | |||
}); | |||
}); | |||
function mockMeasure() { | |||
return mockMeasureEnhanced({ | |||
metric: mockMetric({ key: 'quality_gate_details' }), | |||
value: JSON.stringify({ | |||
conditions: [ | |||
mockQualityGateStatusCondition({ | |||
metric: 'new_coverage', | |||
level: 'ERROR', | |||
error: '85' | |||
}), | |||
mockQualityGateStatusCondition({ | |||
metric: 'new_duplicated_lines_density', | |||
level: 'WARNING', | |||
warning: '5' | |||
}) | |||
] | |||
}) | |||
}); | |||
} |
@@ -19,12 +19,15 @@ | |||
*/ | |||
import * as React from 'react'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import { | |||
formatMeasure, | |||
getMinDecimalsCountToBeDistinctFromThreshold | |||
} from 'sonar-ui-common/helpers/measures'; | |||
import DocTooltip from '../../../components/docs/DocTooltip'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import CoverageRating from '../../../components/ui/CoverageRating'; | |||
import { getPeriodValue } from '../../../helpers/measures'; | |||
import { getMetricName } from '../utils'; | |||
import { getMetricName, getThreshold } from '../utils'; | |||
import enhance, { ComposedProps } from './enhance'; | |||
export class Coverage extends React.PureComponent<ComposedProps> { | |||
@@ -102,7 +105,12 @@ export class Coverage extends React.PureComponent<ComposedProps> { | |||
component={component.key} | |||
metric={newCoverageMeasure.metric.key}> | |||
<span className="js-overview-main-new-coverage"> | |||
{formatMeasure(newCoverageValue, 'PERCENT')} | |||
{formatMeasure(newCoverageValue, 'PERCENT', { | |||
decimals: getMinDecimalsCountToBeDistinctFromThreshold( | |||
parseFloat(newCoverageValue), | |||
getThreshold(measures, 'new_coverage') | |||
) | |||
})} | |||
</span> | |||
</DrilldownLink> | |||
</div> |
@@ -20,11 +20,14 @@ | |||
import * as React from 'react'; | |||
import DuplicationsRating from 'sonar-ui-common/components/ui/DuplicationsRating'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import { | |||
formatMeasure, | |||
getMinDecimalsCountToBeDistinctFromThreshold | |||
} from 'sonar-ui-common/helpers/measures'; | |||
import DocTooltip from '../../../components/docs/DocTooltip'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import { getPeriodValue } from '../../../helpers/measures'; | |||
import { getMetricName } from '../utils'; | |||
import { getMetricName, getThreshold } from '../utils'; | |||
import enhance, { ComposedProps } from './enhance'; | |||
export class Duplications extends React.PureComponent<ComposedProps> { | |||
@@ -102,7 +105,12 @@ export class Duplications extends React.PureComponent<ComposedProps> { | |||
component={component.key} | |||
metric={newDuplicationsMeasure.metric.key}> | |||
<span className="js-overview-main-new-duplications"> | |||
{formatMeasure(newDuplicationsValue, 'PERCENT')} | |||
{formatMeasure(newDuplicationsValue, 'PERCENT', { | |||
decimals: getMinDecimalsCountToBeDistinctFromThreshold( | |||
parseFloat(newDuplicationsValue), | |||
getThreshold(measures, 'new_duplicated_lines_density') | |||
) | |||
})} | |||
</span> | |||
</DrilldownLink> | |||
</div> |
@@ -22,7 +22,10 @@ import * as React from 'react'; | |||
import { Link } from 'react-router'; | |||
import IssueTypeIcon from 'sonar-ui-common/components/icons/IssueTypeIcon'; | |||
import { translate } from 'sonar-ui-common/helpers/l10n'; | |||
import { formatMeasure } from 'sonar-ui-common/helpers/measures'; | |||
import { | |||
formatMeasure, | |||
getMinDecimalsCountToBeDistinctFromThreshold | |||
} from 'sonar-ui-common/helpers/measures'; | |||
import Measure from '../../../components/measure/Measure'; | |||
import DrilldownLink from '../../../components/shared/DrilldownLink'; | |||
import { getBranchLikeQuery, isPullRequest, isShortLivingBranch } from '../../../helpers/branches'; | |||
@@ -36,16 +39,6 @@ interface Props { | |||
} | |||
export default class QualityGateCondition extends React.PureComponent<Props> { | |||
getDecimalsNumber(threshold: number, value: number) { | |||
const delta = Math.abs(threshold - value); | |||
if (delta < 0.1 && delta > 0) { | |||
const match = delta.toFixed(20).match('[^0.]'); | |||
return match && match.index ? match.index - 1 : undefined; | |||
} else { | |||
return undefined; | |||
} | |||
} | |||
getIssuesUrl = (sinceLeakPeriod: boolean, customQuery: T.Dict<string>) => { | |||
const query: T.Dict<string | undefined> = { | |||
resolved: 'false', | |||
@@ -142,7 +135,10 @@ export default class QualityGateCondition extends React.PureComponent<Props> { | |||
if (metric.type === 'RATING') { | |||
operator = translate('quality_gates.operator', condition.op, 'rating'); | |||
} else if (metric.type === 'PERCENT') { | |||
decimals = this.getDecimalsNumber(parseFloat(threshold), parseFloat(actual)); | |||
decimals = getMinDecimalsCountToBeDistinctFromThreshold( | |||
parseFloat(actual), | |||
parseFloat(threshold) | |||
); | |||
} | |||
return this.wrapWithLink( |
@@ -131,20 +131,6 @@ it('new_maintainability_rating', () => { | |||
).toMatchSnapshot(); | |||
}); | |||
it('should be able to correctly decide how much decimals to show', () => { | |||
const condition = mockRatingCondition('new_maintainability_rating'); | |||
const instance = shallow( | |||
<QualityGateCondition component={{ key: 'abcd-key' }} condition={condition} /> | |||
).instance() as QualityGateCondition; | |||
expect(instance.getDecimalsNumber(85, 80)).toBe(undefined); | |||
expect(instance.getDecimalsNumber(85, 85)).toBe(undefined); | |||
expect(instance.getDecimalsNumber(85, 85.01)).toBe(2); | |||
expect(instance.getDecimalsNumber(85, 84.95)).toBe(2); | |||
expect(instance.getDecimalsNumber(85, 84.999999999999554)).toBe('9999999999995'.length); | |||
expect(instance.getDecimalsNumber(85, 85.0000000000000954)).toBe('00000000000009'.length); | |||
expect(instance.getDecimalsNumber(85, 85.00000000000000009)).toBe(undefined); | |||
}); | |||
it('should work with branch', () => { | |||
const condition = mockRatingCondition('new_maintainability_rating'); | |||
expect( |
@@ -153,3 +153,32 @@ export function getMetricName(metricKey: string) { | |||
export function getRatingName(type: IssueType) { | |||
return translate('metric_domain', ISSUETYPE_MAP[type].ratingName); | |||
} | |||
/* | |||
* Extract a specific metric's threshold from the quality gate details | |||
*/ | |||
export function getThreshold(measures: T.MeasureEnhanced[], metricKey: string): number | undefined { | |||
const detailsMeasure = measures.find(measure => measure.metric.key === 'quality_gate_details'); | |||
if (detailsMeasure && detailsMeasure.value) { | |||
const details = safeParse(detailsMeasure.value); | |||
const conditions: T.QualityGateStatusConditionEnhanced[] = details.conditions || []; | |||
const condition = conditions.find(c => c.metric === metricKey); | |||
if (condition) { | |||
return parseFloat((condition.level === 'ERROR' | |||
? condition.error | |||
: condition.warning) as string); | |||
} | |||
} | |||
return undefined; | |||
} | |||
function safeParse(json: string) { | |||
try { | |||
return JSON.parse(json); | |||
} catch (e) { | |||
// eslint-disable-next-line no-console | |||
console.error(e); | |||
return {}; | |||
} | |||
} |