]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-12256 Consistent rounding in project overview
authorJeremy Davis <jeremy.davis@sonarsource.com>
Wed, 25 Sep 2019 16:26:05 +0000 (18:26 +0200)
committerSonarTech <sonartech@sonarsource.com>
Tue, 1 Oct 2019 09:45:54 +0000 (11:45 +0200)
server/sonar-web/src/main/js/apps/overview/__tests__/utils-test.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/main/Coverage.tsx
server/sonar-web/src/main/js/apps/overview/main/Duplications.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/QualityGateCondition.tsx
server/sonar-web/src/main/js/apps/overview/qualityGate/__tests__/QualityGateCondition-test.tsx
server/sonar-web/src/main/js/apps/overview/utils.ts

diff --git a/server/sonar-web/src/main/js/apps/overview/__tests__/utils-test.ts b/server/sonar-web/src/main/js/apps/overview/__tests__/utils-test.ts
new file mode 100644 (file)
index 0000000..5e47552
--- /dev/null
@@ -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'
+        })
+      ]
+    })
+  });
+}
index dac39392ff882a8834a4e77cac708b3295fc7108..380543baa5505afc131fd469fdf35363de2384c1 100644 (file)
  */
 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>
index 8b3d03f5662971c1d85746094de36bc48e905236..026eb2b0d37afb74f9b0e821b06b47d8f6df0546 100644 (file)
 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>
index ed38b125e699601a59a35112f9b700f670222d2b..cdad14829fbd393b9372bd41a410e4a57d1c704d 100644 (file)
@@ -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(
index 13e6469e54099ba61991773c8dcd7b4ca58f6e15..ad2e8b7c4cc0097838ec7cda070175ee51441db3 100644 (file)
@@ -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(
index 034bca95fe9ccb51d75a50569759d4b5b53e532c..e8259aeba5513a27b74e3de725c9d907bd4db77d 100644 (file)
@@ -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 {};
+  }
+}