From 5f3312ab96d92905297e5abe38c6b2377dbe5e53 Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Thu, 21 Mar 2024 11:45:12 +0100 Subject: [PATCH] SONAR-21799 fix issue with sorting by impact data metric --- .../server/measure/ImpactMeasureBuilder.java | 6 + .../measure/ImpactMeasureBuilderTest.java | 31 ++++-- .../measure/ws/ComponentTreeAction.java | 48 +++----- .../server/measure/ws/ComponentTreeSort.java | 98 +++++++++++++---- .../measure/ws/DataSupportedMetrics.java | 48 ++++++++ .../measure/ws/ComponentTreeSortTest.java | 104 ++++++++++++++---- 6 files changed, 251 insertions(+), 84 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/DataSupportedMetrics.java diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/measure/ImpactMeasureBuilder.java b/server/sonar-server-common/src/main/java/org/sonar/server/measure/ImpactMeasureBuilder.java index a60e7e10999..3167b5d03ee 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/measure/ImpactMeasureBuilder.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/measure/ImpactMeasureBuilder.java @@ -26,6 +26,7 @@ import java.lang.reflect.Type; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; +import javax.annotation.CheckForNull; import org.sonar.api.issue.impact.Severity; import static org.sonar.api.utils.Preconditions.checkArgument; @@ -85,6 +86,11 @@ public class ImpactMeasureBuilder { return this; } + @CheckForNull + public Long getTotal() { + return map.get(TOTAL_KEY); + } + public ImpactMeasureBuilder add(ImpactMeasureBuilder other) { other.buildAsMap().forEach((key, val) -> map.merge(key, val, Long::sum)); return this; diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/measure/ImpactMeasureBuilderTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/measure/ImpactMeasureBuilderTest.java index f94fb65ecd1..82f26c979c7 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/measure/ImpactMeasureBuilderTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/measure/ImpactMeasureBuilderTest.java @@ -20,16 +20,16 @@ package org.sonar.server.measure; import java.util.Map; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.sonar.api.issue.impact.Severity; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -public class ImpactMeasureBuilderTest { +class ImpactMeasureBuilderTest { @Test - public void createEmptyMeasure_shouldReturnMeasureWithAllFields() { + void createEmptyMeasure_shouldReturnMeasureWithAllFields() { ImpactMeasureBuilder builder = ImpactMeasureBuilder.createEmpty(); assertThat(builder.buildAsMap()) .containsAllEntriesOf(getImpactMap(0L, 0L, 0L, 0L)); @@ -40,7 +40,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void fromMap_shouldInitializeCorrectlyTheBuilder() { + void fromMap_shouldInitializeCorrectlyTheBuilder() { Map map = getImpactMap(6L, 3L, 2L, 1L); ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(map); assertThat(builder.buildAsMap()) @@ -48,7 +48,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void fromMap_whenMissingField_shouldThrowException() { + void fromMap_whenMissingField_shouldThrowException() { Map map = Map.of(); assertThatThrownBy(() -> ImpactMeasureBuilder.fromMap(map)) .isInstanceOf(IllegalArgumentException.class) @@ -56,7 +56,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void toString_shouldInitializeCorrectlyTheBuilder() { + void toString_shouldInitializeCorrectlyTheBuilder() { ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromString(""" { total: 6, @@ -70,7 +70,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void buildAsMap_whenIsEmpty_shouldThrowException() { + void buildAsMap_whenIsEmpty_shouldThrowException() { ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance(); assertThatThrownBy(impactMeasureBuilder::buildAsMap) .isInstanceOf(IllegalArgumentException.class) @@ -78,7 +78,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void buildAsMap_whenMissingSeverity_shouldThrowException() { + void buildAsMap_whenMissingSeverity_shouldThrowException() { ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance() .setTotal(1L) .setSeverity(Severity.HIGH, 1L) @@ -89,7 +89,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void buildAsString_whenMissingSeverity_shouldThrowException() { + void buildAsString_whenMissingSeverity_shouldThrowException() { ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance() .setTotal(1L) .setSeverity(Severity.HIGH, 1L) @@ -100,7 +100,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void setSeverity_shouldInitializeSeverityValues() { + void setSeverity_shouldInitializeSeverityValues() { ImpactMeasureBuilder builder = ImpactMeasureBuilder.newInstance() .setSeverity(Severity.HIGH, 3L) .setSeverity(Severity.MEDIUM, 2L) @@ -111,7 +111,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void add_shouldSumImpactsAndTotal() { + void add_shouldSumImpactsAndTotal() { ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(getImpactMap(6L, 3L, 2L, 1L)) .add(ImpactMeasureBuilder.newInstance().setTotal(6L).setSeverity(Severity.HIGH, 3L).setSeverity(Severity.MEDIUM, 2L).setSeverity(Severity.LOW, 1L)); assertThat(builder.buildAsMap()) @@ -119,7 +119,7 @@ public class ImpactMeasureBuilderTest { } @Test - public void add_whenOtherMapHasMissingField_shouldThrowException() { + void add_whenOtherMapHasMissingField_shouldThrowException() { ImpactMeasureBuilder impactMeasureBuilder = ImpactMeasureBuilder.newInstance(); ImpactMeasureBuilder otherBuilder = ImpactMeasureBuilder.newInstance(); assertThatThrownBy(() -> impactMeasureBuilder.add(otherBuilder)) @@ -127,4 +127,11 @@ public class ImpactMeasureBuilderTest { .hasMessage("Map must contain a total key"); } + @Test + void getTotal_shoudReturnExpectedTotal() { + ImpactMeasureBuilder builder = ImpactMeasureBuilder.fromMap(getImpactMap(6L, 3L, 2L, 1L)); + + assertThat(builder.getTotal()).isEqualTo(6L); + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java index 845cf6ccc2c..7eb02739fe1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeAction.java @@ -78,12 +78,6 @@ import static java.lang.String.format; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Optional.ofNullable; -import static org.sonar.api.measures.CoreMetrics.MAINTAINABILITY_ISSUES_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_ISSUES_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_ISSUES_KEY; -import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_ISSUES_KEY; -import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES_KEY; -import static org.sonar.api.measures.CoreMetrics.SECURITY_ISSUES_KEY; import static org.sonar.api.measures.Metric.ValueType.DATA; import static org.sonar.api.measures.Metric.ValueType.DISTRIB; import static org.sonar.api.utils.Paging.offset; @@ -179,9 +173,9 @@ public class ComponentTreeAction implements MeasuresWsAction { public void define(WebService.NewController context) { WebService.NewAction action = context.createAction(ACTION_COMPONENT_TREE) .setDescription(format("Navigate through components based on the chosen strategy with specified measures.
" + - "Requires the following permission: 'Browse' on the specified project.
" + - "For applications, it also requires 'Browse' permission on its child projects.
" + - "When limiting search with the %s parameter, directories are not returned.", Param.TEXT_QUERY)) + "Requires the following permission: 'Browse' on the specified project.
" + + "For applications, it also requires 'Browse' permission on its child projects.
" + + "When limiting search with the %s parameter, directories are not returned.", Param.TEXT_QUERY)) .setResponseExample(getClass().getResource("component_tree-example.json")) .setSince("5.4") .setHandler(this) @@ -193,7 +187,7 @@ public class ComponentTreeAction implements MeasuresWsAction { MeasuresWsModule.getDeprecatedMetricsInSonarQube105())), new Change("10.5", "Added new accepted values for the 'metricKeys' param: 'maintainability_issues', 'reliability_issues', 'security_issues'"), new Change("10.4", String.format("The metrics %s are now deprecated " + - "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.", + "without exact replacement. Use 'maintainability_issues', 'reliability_issues' and 'security_issues' instead.", MeasuresWsModule.getDeprecatedMetricsInSonarQube104())), new Change("10.4", "The metrics 'open_issues', 'reopened_issues' and 'confirmed_issues' are now deprecated in the response. Consume 'violations' instead."), new Change("10.4", "The use of 'open_issues', 'reopened_issues' and 'confirmed_issues' values in 'metricKeys' param are now deprecated. Use 'violations' instead."), @@ -226,9 +220,9 @@ public class ComponentTreeAction implements MeasuresWsAction { action.createParam(Param.TEXT_QUERY) .setDescription("Limit search to:
    " + - "
  • component names that contain the supplied string
  • " + - "
  • component keys that are exactly the same as the supplied string
  • " + - "
") + "
  • component names that contain the supplied string
  • " + + "
  • component keys that are exactly the same as the supplied string
  • " + + "") .setMinimumLength(QUERY_MINIMUM_LENGTH) .setExampleValue("FILE_NAM"); @@ -260,10 +254,10 @@ public class ComponentTreeAction implements MeasuresWsAction { action.createParam(PARAM_METRIC_SORT_FILTER) .setDescription(format("Filter components. Sort must be on a metric. Possible values are: " + - "
      " + - "
    • %s: return all components
    • " + - "
    • %s: filter out components that do not have a measure on the sorted metric
    • " + - "
    ", ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER)) + "
      " + + "
    • %s: return all components
    • " + + "
    • %s: filter out components that do not have a measure on the sorted metric
    • " + + "
    ", ALL_METRIC_SORT_FILTER, WITH_MEASURES_ONLY_METRIC_SORT_FILTER)) .setDefaultValue(ALL_METRIC_SORT_FILTER) .setPossibleValues(METRIC_SORT_FILTERS); @@ -278,11 +272,11 @@ public class ComponentTreeAction implements MeasuresWsAction { action.createParam(PARAM_STRATEGY) .setDescription("Strategy to search for base component descendants:" + - "
      " + - "
    • children: return the children components of the base component. Grandchildren components are not returned
    • " + - "
    • all: return all the descendants components of the base component. Grandchildren are returned.
    • " + - "
    • leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.
    • " + - "
    ") + "
      " + + "
    • children: return the children components of the base component. Grandchildren components are not returned
    • " + + "
    • all: return all the descendants components of the base component. Grandchildren are returned.
    • " + + "
    • leaves: return all the descendant components (files, in general) which don't have other children. They are the leaves of the component tree.
    • " + + "
    ") .setPossibleValues(STRATEGIES.keySet()) .setDefaultValue(ALL_STRATEGY); } @@ -686,15 +680,9 @@ public class ComponentTreeAction implements MeasuresWsAction { INSTANCE; static final Set FORBIDDEN_METRIC_TYPES = Set.of(DISTRIB.name()); - static final Map> PARTIALLY_SUPPORTED_METRICS= Map. of( + static final Map> PARTIALLY_SUPPORTED_METRICS = Map.of( DATA.name(), - Set.of( - SECURITY_ISSUES_KEY, - MAINTAINABILITY_ISSUES_KEY, - RELIABILITY_ISSUES_KEY, - NEW_SECURITY_ISSUES_KEY, - NEW_MAINTAINABILITY_ISSUES_KEY, - NEW_RELIABILITY_ISSUES_KEY)); + DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS); @Override public boolean test(@Nonnull MetricDto input) { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java index 1c62a7eedbe..a1b04613433 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/ComponentTreeSort.java @@ -25,16 +25,20 @@ import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.common.collect.Table; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import org.jetbrains.annotations.NotNull; import org.sonar.api.measures.Metric; import org.sonar.api.measures.Metric.ValueType; import org.sonar.db.component.ComponentDto; import org.sonar.db.metric.MetricDto; import org.sonar.server.exceptions.BadRequestException; +import org.sonar.server.measure.ImpactMeasureBuilder; import static java.lang.String.CASE_INSENSITIVE_ORDER; import static java.lang.String.format; @@ -62,7 +66,7 @@ public class ComponentTreeSort { } public static List sortComponents(List components, ComponentTreeRequest wsRequest, List metrics, - Table measuresByComponentUuidAndMetric) { + Table measuresByComponentUuidAndMetric) { List sortParameters = wsRequest.getSort(); if (sortParameters == null || sortParameters.isEmpty()) { return components; @@ -112,14 +116,14 @@ public class ComponentTreeSort { } private static Ordering metricValueOrdering(ComponentTreeRequest wsRequest, List metrics, - Table measuresByComponentUuidAndMetric) { + Table measuresByComponentUuidAndMetric) { + boolean isAscending = Optional.ofNullable(wsRequest.getAsc()).orElse(false); if (wsRequest.getMetricSort() == null) { - return componentNameOrdering(wsRequest.getAsc()); + return componentNameOrdering(isAscending); } Map metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey); MetricDto metric = metricsByKey.get(wsRequest.getMetricSort()); - boolean isAscending = wsRequest.getAsc(); ValueType metricValueType = ValueType.valueOf(metric.getValueType()); if (NUMERIC_VALUE_TYPES.contains(metricValueType)) { return numericalMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); @@ -127,22 +131,29 @@ public class ComponentTreeSort { return stringOrdering(isAscending, new ComponentDtoToTextualMeasureValue(metric, measuresByComponentUuidAndMetric)); } else if (ValueType.LEVEL.equals(ValueType.valueOf(metric.getValueType()))) { return levelMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); + } else if (ValueType.DATA.equals(ValueType.valueOf(metric.getValueType())) + && DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS.contains(metric.getKey())) { + return totalMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); } throw new IllegalStateException("Unrecognized metric value type: " + metric.getValueType()); } private static Ordering metricPeriodOrdering(ComponentTreeRequest wsRequest, List metrics, - Table measuresByComponentUuidAndMetric) { + Table measuresByComponentUuidAndMetric) { + boolean isAscending = Optional.ofNullable(wsRequest.getAsc()).orElse(false); if (wsRequest.getMetricSort() == null || wsRequest.getMetricPeriodSort() == null) { - return componentNameOrdering(wsRequest.getAsc()); + return componentNameOrdering(isAscending); } Map metricsByKey = Maps.uniqueIndex(metrics, MetricDto::getKey); MetricDto metric = metricsByKey.get(wsRequest.getMetricSort()); ValueType metricValueType = ValueType.valueOf(metric.getValueType()); if (NUMERIC_VALUE_TYPES.contains(metricValueType)) { - return numericalMetricPeriodOrdering(wsRequest, metric, measuresByComponentUuidAndMetric); + return numericalMetricPeriodOrdering(isAscending, metric, measuresByComponentUuidAndMetric); + } else if (ValueType.DATA.equals(metricValueType) + && DataSupportedMetrics.IMPACTS_SUPPORTED_METRICS.contains(metric.getKey())) { + return totalNewPeriodMetricOrdering(isAscending, metric, measuresByComponentUuidAndMetric); } throw BadRequestException.create(format("Impossible to sort metric '%s' by measure period.", metric.getKey())); @@ -150,36 +161,47 @@ public class ComponentTreeSort { private static Ordering numericalMetricOrdering(boolean isAscending, @Nullable MetricDto metric, Table measuresByComponentUuidAndMetric) { - Ordering ordering = Ordering.natural(); + Ordering ordering = getOrdering(isAscending); + + return ordering.onResultOf(new ComponentDtoToNumericalMeasureValue(metric, measuresByComponentUuidAndMetric)); + } + + @NotNull + private static > Ordering getOrdering(boolean isAscending) { + Ordering ordering = Ordering.natural(); if (!isAscending) { ordering = ordering.reverse(); } + return ordering.nullsLast(); + } - return ordering.nullsLast().onResultOf(new ComponentDtoToNumericalMeasureValue(metric, measuresByComponentUuidAndMetric)); + private static Ordering totalMetricOrdering(boolean isAscending, MetricDto metric, + Table measuresByComponentUuidAndMetric) { + Ordering ordering = getOrdering(isAscending); + return ordering.onResultOf(new ComponentDtoToTotalImpactMeasureValue(metric, measuresByComponentUuidAndMetric, false)); } - private static Ordering numericalMetricPeriodOrdering(ComponentTreeRequest request, @Nullable MetricDto metric, - Table measuresByComponentUuidAndMetric) { - Ordering ordering = Ordering.natural(); + private static Ordering totalNewPeriodMetricOrdering(boolean isAscending, MetricDto metric, + Table measuresByComponentUuidAndMetric) { + Ordering ordering = getOrdering(isAscending); + return ordering.onResultOf(new ComponentDtoToTotalImpactMeasureValue(metric, measuresByComponentUuidAndMetric, true)); + } - if (!request.getAsc()) { - ordering = ordering.reverse(); - } - return ordering.nullsLast().onResultOf(new ComponentDtoToMeasureVariationValue(metric, measuresByComponentUuidAndMetric)); + private static Ordering numericalMetricPeriodOrdering(boolean isAscending, @Nullable MetricDto metric, + Table measuresByComponentUuidAndMetric) { + Ordering ordering = getOrdering(isAscending); + + return ordering.onResultOf(new ComponentDtoToMeasureVariationValue(metric, measuresByComponentUuidAndMetric)); } private static Ordering levelMetricOrdering(boolean isAscending, @Nullable MetricDto metric, Table measuresByComponentUuidAndMetric) { - Ordering ordering = Ordering.natural(); - - // inverse the order of org.sonar.api.measures.Metric.Level - if (isAscending) { - ordering = ordering.reverse(); - } + // inverse the order of org.sonar.api.measures.Metric.Level enum + Ordering ordering = getOrdering(!isAscending); - return ordering.nullsLast().onResultOf(new ComponentDtoToLevelIndex(metric, measuresByComponentUuidAndMetric)); + return ordering.onResultOf(new ComponentDtoToLevelIndex(metric, measuresByComponentUuidAndMetric)); } private static class ComponentDtoToNumericalMeasureValue implements Function { @@ -265,4 +287,34 @@ public class ComponentTreeSort { } } + private static class ComponentDtoToTotalImpactMeasureValue implements Function { + private final MetricDto metric; + private final Table measuresByComponentUuidAndMetric; + private final boolean onlyNewPeriodMeasures; + + //Store the total value for each component to avoid multiple deserialization of the same measure + Map totalByComponentUuid = new HashMap<>(); + + private ComponentDtoToTotalImpactMeasureValue(@Nullable MetricDto metric, + Table measuresByComponentUuidAndMetric, boolean onlyNewPeriodMeasures) { + this.metric = metric; + this.measuresByComponentUuidAndMetric = measuresByComponentUuidAndMetric; + this.onlyNewPeriodMeasures = onlyNewPeriodMeasures; + } + + @Override + public Long apply(@Nonnull ComponentDto input) { + if (onlyNewPeriodMeasures && metric != null && !metric.getKey().startsWith("new_")) { + return null; + } + return totalByComponentUuid.computeIfAbsent(input.uuid(), + k -> { + ComponentTreeData.Measure measure = measuresByComponentUuidAndMetric.get(input.uuid(), metric); + return Optional.ofNullable(measure).map(ComponentTreeData.Measure::getData) + .map(data -> ImpactMeasureBuilder.fromString(measure.getData()).getTotal()) + .orElse(null); + }); + } + } + } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/DataSupportedMetrics.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/DataSupportedMetrics.java new file mode 100644 index 00000000000..6fbe87f8488 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/ws/DataSupportedMetrics.java @@ -0,0 +1,48 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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. + */ +package org.sonar.server.measure.ws; + +import java.util.Set; + +import static org.sonar.api.measures.CoreMetrics.MAINTAINABILITY_ISSUES_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_ISSUES_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_ISSUES_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_ISSUES_KEY; +import static org.sonar.api.measures.CoreMetrics.RELIABILITY_ISSUES_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_ISSUES_KEY; + + +/** + * This class contains the list of metrics that are supported in the web-api for the data type. + */ +public class DataSupportedMetrics { + + public static final Set IMPACTS_SUPPORTED_METRICS = Set.of( + SECURITY_ISSUES_KEY, + MAINTAINABILITY_ISSUES_KEY, + RELIABILITY_ISSUES_KEY, + NEW_SECURITY_ISSUES_KEY, + NEW_MAINTAINABILITY_ISSUES_KEY, + NEW_RELIABILITY_ISSUES_KEY); + + private DataSupportedMetrics() { + // only static methods + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java index 0360db6c3fd..4cc8db8ba45 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/ws/ComponentTreeSortTest.java @@ -23,8 +23,9 @@ import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import java.util.List; import javax.annotation.Nullable; -import org.junit.Before; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.sonar.api.issue.impact.Severity; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric.ValueType; import org.sonar.api.resources.Qualifiers; @@ -32,6 +33,7 @@ import org.sonar.core.util.Uuids; import org.sonar.db.component.ComponentDto; import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.metric.MetricDto; +import org.sonar.server.measure.ImpactMeasureBuilder; import static com.google.common.collect.Lists.newArrayList; import static java.util.Collections.singletonList; @@ -44,17 +46,20 @@ import static org.sonar.server.measure.ws.ComponentTreeAction.PATH_SORT; import static org.sonar.server.measure.ws.ComponentTreeAction.QUALIFIER_SORT; import static org.sonar.server.measure.ws.ComponentTreeData.Measure.createFromMeasureDto; -public class ComponentTreeSortTest { +class ComponentTreeSortTest { private static final String NUM_METRIC_KEY = "violations"; private static final String NEW_METRIC_KEY = "new_violations"; private static final String TEXT_METRIC_KEY = "sqale_index"; + private static final String DATA_IMPACT_METRIC_KEY = "reliability_issues"; + private static final String NEW_DATA_IMPACT_METRIC_KEY = "new_reliability_issues"; + private List metrics; private Table measuresByComponentUuidAndMetric; private List components; - @Before - public void setUp() { + @BeforeEach + void setUp() { components = newArrayList( newComponentWithoutSnapshotId("name-1", "qualifier-2", "path-9"), newComponentWithoutSnapshotId("name-3", "qualifier-3", "path-8"), @@ -75,8 +80,14 @@ public class ComponentTreeSortTest { MetricDto sqaleIndexMetric = newMetricDto() .setKey(TEXT_METRIC_KEY) .setValueType(ValueType.STRING.name()); + MetricDto reliabilityIssueMetric = newMetricDto() + .setKey(DATA_IMPACT_METRIC_KEY) + .setValueType(ValueType.DATA.name()); + MetricDto newReliabilityIssueMetric = newMetricDto() + .setKey(NEW_DATA_IMPACT_METRIC_KEY) + .setValueType(ValueType.DATA.name()); - metrics = newArrayList(violationsMetric, sqaleIndexMetric, newViolationsMetric); + metrics = newArrayList(violationsMetric, sqaleIndexMetric, newViolationsMetric, reliabilityIssueMetric, newReliabilityIssueMetric); measuresByComponentUuidAndMetric = HashBasedTable.create(components.size(), 2); // same number than path field @@ -85,12 +96,23 @@ public class ComponentTreeSortTest { measuresByComponentUuidAndMetric.put(component.uuid(), violationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue))); measuresByComponentUuidAndMetric.put(component.uuid(), newViolationsMetric, createFromMeasureDto(new LiveMeasureDto().setValue(currentValue))); measuresByComponentUuidAndMetric.put(component.uuid(), sqaleIndexMetric, createFromMeasureDto(new LiveMeasureDto().setData(String.valueOf(currentValue)))); + measuresByComponentUuidAndMetric.put(component.uuid(), reliabilityIssueMetric, createFromMeasureDto(new LiveMeasureDto().setData(buildJsonImpact((int) currentValue)))); + measuresByComponentUuidAndMetric.put(component.uuid(), newReliabilityIssueMetric, createFromMeasureDto(new LiveMeasureDto().setData(buildJsonImpact((int) currentValue)))); currentValue--; } } + private static String buildJsonImpact(int currentValue) { + return String.valueOf(ImpactMeasureBuilder.newInstance() + .setSeverity(Severity.HIGH, currentValue) + .setSeverity(Severity.MEDIUM, currentValue) + .setSeverity(Severity.LOW, currentValue) + .setTotal(currentValue) + .buildAsString()); + } + @Test - public void sort_by_names() { + void sort_by_names() { ComponentTreeRequest wsRequest = newRequest(singletonList(NAME_SORT), true, null); List result = sortComponents(wsRequest); @@ -99,7 +121,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_qualifier() { + void sort_by_qualifier() { ComponentTreeRequest wsRequest = newRequest(singletonList(QUALIFIER_SORT), false, null); List result = sortComponents(wsRequest); @@ -109,7 +131,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_path() { + void sort_by_path() { ComponentTreeRequest wsRequest = newRequest(singletonList(PATH_SORT), true, null); List result = sortComponents(wsRequest); @@ -119,7 +141,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_numerical_metric_key_ascending() { + void sort_by_numerical_metric_key_ascending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, NUM_METRIC_KEY); @@ -130,7 +152,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_numerical_metric_key_descending() { + void sort_by_numerical_metric_key_descending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY); @@ -141,7 +163,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_name_ascending_in_case_of_equality() { + void sort_by_name_ascending_in_case_of_equality() { components = newArrayList( newComponentWithoutSnapshotId("PROJECT 12", Qualifiers.PROJECT, "PROJECT_PATH_1"), newComponentWithoutSnapshotId("PROJECT 11", Qualifiers.PROJECT, "PROJECT_PATH_1"), @@ -155,7 +177,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_alert_status_ascending() { + void sort_by_alert_status_ascending() { components = newArrayList( newComponentWithoutSnapshotId("PROJECT OK 1", Qualifiers.PROJECT, "PROJECT_OK_PATH_1"), newComponentWithoutSnapshotId("PROJECT ERROR 1", Qualifiers.PROJECT, "PROJECT_ERROR_PATH_1"), @@ -181,7 +203,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_numerical_metric_period_1_key_descending() { + void sort_by_numerical_metric_period_1_key_descending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, NEW_METRIC_KEY).setMetricPeriodSort(1); @@ -192,7 +214,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_numerical_metric_period_1_key_ascending() { + void sort_by_numerical_metric_period_1_key_ascending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), true, NEW_METRIC_KEY).setMetricPeriodSort(1); @@ -203,7 +225,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_numerical_metric_period_5_key() { + void sort_by_numerical_metric_period_5_key() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, NUM_METRIC_KEY).setMetricPeriodSort(5); @@ -214,7 +236,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_textual_metric_key_ascending() { + void sort_by_textual_metric_key_ascending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, TEXT_METRIC_KEY); @@ -225,7 +247,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_by_textual_metric_key_descending() { + void sort_by_textual_metric_key_descending() { components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, TEXT_METRIC_KEY); @@ -236,7 +258,7 @@ public class ComponentTreeSortTest { } @Test - public void sort_on_multiple_fields() { + void sort_on_multiple_fields() { components = newArrayList( newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-2"), newComponentWithoutSnapshotId("name-1", "qualifier-1", "path-3"), @@ -249,6 +271,50 @@ public class ComponentTreeSortTest { .containsExactly("path-1", "path-2", "path-3"); } + @Test + void sortComponent_whenMetricIsImpactDataType_shouldOrderByTotalAscending() { + components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); + ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), true, DATA_IMPACT_METRIC_KEY); + + List result = sortComponents(wsRequest); + + assertThat(result).extracting("path") + .containsExactly("path-1", "path-2", "path-3", "path-4", "path-5", "path-6", "path-7", "path-8", "path-9", "path-without-measure"); + } + + @Test + void sortComponent_whenMetricIsImpactDataType_shouldOrderByTotalDescending() { + components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); + ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_SORT), false, DATA_IMPACT_METRIC_KEY); + + List result = sortComponents(wsRequest); + + assertThat(result).extracting("path") + .containsExactly("path-9", "path-8", "path-7", "path-6", "path-5", "path-4", "path-3", "path-2", "path-1", "path-without-measure"); + } + + @Test + void sortComponent_whenMetricIsNewAndMetricPeriodSort_shouldOrderByTotal() { + components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); + ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, NEW_DATA_IMPACT_METRIC_KEY).setMetricPeriodSort(1); + + List result = sortComponents(wsRequest); + + assertThat(result).extracting("path") + .containsExactly("path-9", "path-8", "path-7", "path-6", "path-5", "path-4", "path-3", "path-2", "path-1", "path-without-measure"); + } + + @Test + void sortComponent_whenMetricIsNotNewAndMetricPeriodSort_shouldNotOrder() { + components.add(newComponentWithoutSnapshotId("name-without-measure", "qualifier-without-measure", "path-without-measure")); + ComponentTreeRequest wsRequest = newRequest(singletonList(METRIC_PERIOD_SORT), false, DATA_IMPACT_METRIC_KEY).setMetricPeriodSort(1); + + List result = sortComponents(wsRequest); + + assertThat(result).extracting("path") + .containsExactly("path-9", "path-7", "path-8", "path-6", "path-3", "path-4", "path-5", "path-1", "path-2", "path-without-measure"); + } + private List sortComponents(ComponentTreeRequest wsRequest) { return ComponentTreeSort.sortComponents(components, wsRequest, metrics, measuresByComponentUuidAndMetric); } -- 2.39.5