From 61c6e94bf9f7d2826de0bc8e4d5a2a1cadc237f0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Mon, 28 Oct 2024 11:33:05 +0100 Subject: [PATCH] SONAR-23299 add fallback condition to live quality gate evaluation --- .../server/metric}/StandardToMQRMetrics.java | 2 +- .../qualitygate/ConditionEvaluator.java | 4 +- .../qualitygate/EvaluatedCondition.java | 28 +++++++++ .../qualitygate/EvaluatedQualityGate.java | 12 +++- .../qualitygate/QualityGateEvaluatorImpl.java | 17 +++++- .../QualityGateFallbackManager.java | 40 +++++++++++++ .../metric}/StandardToMQRMetricsTest.java | 4 +- .../qualitygate/EvaluatedConditionTest.java | 13 +++-- .../qualitygate/EvaluatedQualityGateTest.java | 40 ++++++++----- .../QualityGateEvaluatorImplTest.java | 56 ++++++++++++++---- .../QualityGateFallbackManagerTest.java | 57 +++++++++++++++++++ .../QualityGateConditionsUpdater.java | 2 +- .../qualitygate/QualityGateModeChecker.java | 2 +- .../server/qualitygate/QualityGateModule.java | 3 +- .../qualitygate/QualityGateModuleTest.java | 2 +- 15 files changed, 238 insertions(+), 44 deletions(-) rename server/{sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws => sonar-server-common/src/main/java/org/sonar/server/metric}/StandardToMQRMetrics.java (99%) create mode 100644 server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateFallbackManager.java rename server/{sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws => sonar-server-common/src/test/java/org/sonar/server/metric}/StandardToMQRMetricsTest.java (98%) create mode 100644 server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateFallbackManagerTest.java diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/StandardToMQRMetrics.java b/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java similarity index 99% rename from server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/StandardToMQRMetrics.java rename to server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java index e50a91cf9cb..a08ec970281 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/StandardToMQRMetrics.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/metric/StandardToMQRMetrics.java @@ -17,7 +17,7 @@ * 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.qualitygate.ws; +package org.sonar.server.metric; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java index 916e79626d9..4e8e2fd3338 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/ConditionEvaluator.java @@ -50,7 +50,7 @@ class ConditionEvaluator { static EvaluatedCondition evaluate(Condition condition, QualityGateEvaluator.Measures measures) { Optional measure = measures.get(condition.getMetricKey()); if (measure.isEmpty()) { - return new EvaluatedCondition(condition, EvaluationStatus.OK, null); + return new EvaluatedCondition(condition, EvaluationStatus.OK, null, true); } Optional value = getMeasureValue(condition, measure.get()); @@ -105,7 +105,7 @@ class ConditionEvaluator { return measure.getValue().isPresent() ? getNumericValue(measure.getType(), measure.getValue().getAsDouble()) : null; } - checkArgument(ValueType.LEVEL.equals(measure.getType()), "Condition is not allowed for type %s" , measure.getType()); + checkArgument(ValueType.LEVEL.equals(measure.getType()), "Condition is not allowed for type %s", measure.getType()); return measure.getStringValue().orElse(null); } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java index db183632ec8..930e124096d 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedCondition.java @@ -29,20 +29,41 @@ import static java.util.Objects.requireNonNull; @Immutable public class EvaluatedCondition { private final Condition condition; + private Condition originalCondition; private final EvaluationStatus status; @Nullable private final String value; + private final boolean missingMeasure; public EvaluatedCondition(Condition condition, EvaluationStatus status, @Nullable String value) { + this(condition, status, value, false); + } + + public EvaluatedCondition(Condition condition, EvaluationStatus status, @Nullable String value, boolean measureMissing) { + this(condition, condition, status, value, measureMissing); + } + + public EvaluatedCondition(Condition condition, Condition originalCondition, EvaluationStatus status, @Nullable String value, boolean measureMissing) { this.condition = requireNonNull(condition, "condition can't be null"); + this.originalCondition = originalCondition; this.status = requireNonNull(status, "status can't be null"); this.value = value; + this.missingMeasure = measureMissing; } public Condition getCondition() { return condition; } + public Condition getOriginalCondition() { + return originalCondition; + } + + public EvaluatedCondition setOriginalCondition(Condition originalCondition) { + this.originalCondition = originalCondition; + return this; + } + public EvaluationStatus getStatus() { return status; } @@ -74,11 +95,16 @@ public class EvaluatedCondition { public String toString() { return "EvaluatedCondition{" + "condition=" + condition + + ", originalCondition=" + originalCondition + ", status=" + status + ", value=" + (value == null ? null : ('\'' + value + '\'')) + '}'; } + public boolean isMissingMeasure() { + return missingMeasure; + } + /** * Quality gate condition evaluation status. */ @@ -86,7 +112,9 @@ public class EvaluatedCondition { /** * No measure found or measure had no value. The condition has not been evaluated and therefor ignored in * the computation of the Quality Gate status. + * @deprecated since 10.8, the value is never used in the code */ + @Deprecated(since = "10.8") NO_VALUE, /** * Condition evaluated as OK, error thresholds hasn't been reached. diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java index 82d702223cf..eb83e0a7d06 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/EvaluatedQualityGate.java @@ -152,14 +152,20 @@ public class EvaluatedQualityGate { Set conditions = qualityGate.getConditions(); Set conditionsNotEvaluated = conditions.stream() - .filter(c -> !evaluatedConditions.containsKey(c)) + .filter(c -> !evaluatedConditions.containsKey(c) && !hasOriginalCondition(evaluatedConditions.values(), c)) .collect(Collectors.toSet()); checkArgument(conditionsNotEvaluated.isEmpty(), "Evaluation missing for the following conditions: %s", conditionsNotEvaluated); - Set unknownConditions = evaluatedConditions.keySet().stream() - .filter(c -> !conditions.contains(c)) + Set unknownConditions = evaluatedConditions.entrySet().stream() + .filter(c -> !conditions.contains(c.getValue().getCondition()) && !conditions.contains(c.getValue().getOriginalCondition())) + .map(Map.Entry::getKey) .collect(Collectors.toSet()); + checkArgument(unknownConditions.isEmpty(), "Evaluation provided for unknown conditions: %s", unknownConditions); } + + private static boolean hasOriginalCondition(Collection evaluatedConditions, Condition c) { + return evaluatedConditions.stream().anyMatch(ec -> ec.getOriginalCondition().equals(c)); + } } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java index ed13f8c86dd..b9239d9419b 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateEvaluatorImpl.java @@ -45,6 +45,12 @@ public class QualityGateEvaluatorImpl implements QualityGateEvaluator { CoreMetrics.NEW_DUPLICATED_LINES_KEY, CoreMetrics.NEW_BLOCKS_DUPLICATED_KEY); + private final QualityGateFallbackManager qualityGateFallbackManager; + + public QualityGateEvaluatorImpl(QualityGateFallbackManager qualityGateFallbackManager) { + this.qualityGateFallbackManager = qualityGateFallbackManager; + } + @Override public EvaluatedQualityGate evaluate(QualityGate gate, Measures measures, Configuration configuration) { EvaluatedQualityGate.Builder result = EvaluatedQualityGate.newBuilder() @@ -57,8 +63,15 @@ public class QualityGateEvaluatorImpl implements QualityGateEvaluator { String metricKey = condition.getMetricKey(); EvaluatedCondition evaluation = ConditionEvaluator.evaluate(condition, measures); + if (evaluation.isMissingMeasure()) { + evaluation = qualityGateFallbackManager.getFallbackCondition(evaluation.getCondition()) + .map(c -> ConditionEvaluator.evaluate(c, measures).setOriginalCondition(condition)) + .orElse(evaluation); + } + if (isSmallChangeset && evaluation.getStatus() != EvaluationStatus.OK && METRICS_TO_IGNORE_ON_SMALL_CHANGESETS.contains(metricKey)) { - result.addEvaluatedCondition(new EvaluatedCondition(evaluation.getCondition(), EvaluationStatus.OK, evaluation.getValue().orElse(null))); + result.addEvaluatedCondition(new EvaluatedCondition(evaluation.getCondition(), evaluation.getOriginalCondition(), + EvaluationStatus.OK, evaluation.getValue().orElse(null), evaluation.isMissingMeasure())); result.setIgnoredConditionsOnSmallChangeset(true); } else { result.addEvaluatedCondition(evaluation); @@ -76,6 +89,8 @@ public class QualityGateEvaluatorImpl implements QualityGateEvaluator { metricKeys.add(CoreMetrics.NEW_LINES_KEY); for (Condition condition : gate.getConditions()) { metricKeys.add(condition.getMetricKey()); + qualityGateFallbackManager.getFallbackCondition(condition) + .ifPresent(c -> metricKeys.add(c.getMetricKey())); } return metricKeys; } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateFallbackManager.java b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateFallbackManager.java new file mode 100644 index 00000000000..d81165121c8 --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/qualitygate/QualityGateFallbackManager.java @@ -0,0 +1,40 @@ +/* + * 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.qualitygate; + +import java.util.Optional; +import org.sonar.server.metric.StandardToMQRMetrics; + +/** + * This class is used to manage the fallback of the quality gate conditions, in case one of the measure has not been computed yet. + * This is to ensure continuity of the quality gate evaluation in the scenario of introduction of new measures, but the project has not been reanalyzed yet. + * The live quality gate evaluation will fallback to an equivalent metric until the project is reanalyzed + */ +public class QualityGateFallbackManager { + + public Optional getFallbackCondition(Condition condition) { + Optional equivalentMetric = StandardToMQRMetrics.getEquivalentMetric(condition.getMetricKey()); + if (StandardToMQRMetrics.isMQRMetric(condition.getMetricKey()) && equivalentMetric.isPresent()) { + return Optional.of(new Condition(equivalentMetric.get(), condition.getOperator(), condition.getErrorThreshold())); + } else { + return Optional.empty(); + } + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/StandardToMQRMetricsTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/metric/StandardToMQRMetricsTest.java similarity index 98% rename from server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/StandardToMQRMetricsTest.java rename to server/sonar-server-common/src/test/java/org/sonar/server/metric/StandardToMQRMetricsTest.java index c779311c589..66d98b9dbbb 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/StandardToMQRMetricsTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/metric/StandardToMQRMetricsTest.java @@ -17,7 +17,7 @@ * 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.qualitygate.ws; +package org.sonar.server.metric; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; @@ -25,7 +25,6 @@ import org.sonar.api.measures.CoreMetrics; import org.sonar.core.metric.SoftwareQualitiesMetrics; class StandardToMQRMetricsTest { - @Test void isStandardMetric_shouldReturnExpectedResult() { Assertions.assertThat(StandardToMQRMetrics.isStandardMetric(CoreMetrics.RELIABILITY_RATING_KEY)).isTrue(); @@ -41,5 +40,4 @@ class StandardToMQRMetricsTest { Assertions.assertThat(StandardToMQRMetrics.getEquivalentMetric(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING_KEY)).hasValue(CoreMetrics.RELIABILITY_RATING_KEY); Assertions.assertThat(StandardToMQRMetrics.getEquivalentMetric(CoreMetrics.RELIABILITY_RATING_KEY)).hasValue(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING_KEY); } - } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedConditionTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedConditionTest.java index 33402ef0969..9215eb11886 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedConditionTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedConditionTest.java @@ -64,17 +64,20 @@ public class EvaluatedConditionTest { @Test public void override_toString() { - assertThat(underTest).hasToString("EvaluatedCondition{condition=" + - "Condition{metricKey='metricKey', operator=GREATER_THAN, errorThreshold='2'}, " + - "status=ERROR, value='value'}"); + assertThat(underTest).hasToString("EvaluatedCondition{condition=Condition{metricKey='metricKey', " + + "operator=GREATER_THAN, errorThreshold='2'}, " + + "originalCondition=Condition{metricKey='metricKey', " + + "operator=GREATER_THAN, errorThreshold='2'}, status=ERROR, value='value'}"); } @Test public void toString_does_not_quote_null_value() { EvaluatedCondition underTest = new EvaluatedCondition(CONDITION_1, ERROR, null); - assertThat(underTest).hasToString("EvaluatedCondition{condition=" + - "Condition{metricKey='metricKey', operator=GREATER_THAN, errorThreshold='2'}, " + + assertThat(underTest).hasToString("EvaluatedCondition{condition=Condition{metricKey='metricKey', " + + "operator=GREATER_THAN, errorThreshold='2'}, " + + "originalCondition=Condition{metricKey='metricKey', " + + "operator=GREATER_THAN, errorThreshold='2'}, " + "status=ERROR, value=null}"); } diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java index 2b0ce22d34d..f076e163f53 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/EvaluatedQualityGateTest.java @@ -24,7 +24,8 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Random; import org.apache.commons.lang3.RandomStringUtils; -import org.junit.Test; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric.Level; @@ -34,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.sonar.server.qualitygate.EvaluatedQualityGate.newBuilder; -public class EvaluatedQualityGateTest { +class EvaluatedQualityGateTest { private static final String QUALITY_GATE_ID = "qg_id"; private static final String QUALITY_GATE_NAME = "qg_name"; private static final QualityGate NO_CONDITION_QUALITY_GATE = new QualityGate(QUALITY_GATE_ID, QUALITY_GATE_NAME, emptySet()); @@ -55,7 +56,7 @@ public class EvaluatedQualityGateTest { private EvaluatedQualityGate.Builder builder = newBuilder(); @Test - public void build_fails_with_NPE_if_status_not_set() { + void build_fails_with_NPE_if_status_not_set() { builder.setQualityGate(NO_CONDITION_QUALITY_GATE); assertThatThrownBy(() -> builder.build()) @@ -64,21 +65,21 @@ public class EvaluatedQualityGateTest { } @Test - public void addCondition_fails_with_NPE_if_condition_is_null() { + void addCondition_fails_with_NPE_if_condition_is_null() { assertThatThrownBy(() -> builder.addEvaluatedCondition(null, EvaluatedCondition.EvaluationStatus.ERROR, "a_value")) .isInstanceOf(NullPointerException.class) .hasMessage("condition can't be null"); } @Test - public void addCondition_fails_with_NPE_if_status_is_null() { + void addCondition_fails_with_NPE_if_status_is_null() { assertThatThrownBy(() -> builder.addEvaluatedCondition(new Condition("metric_key", Condition.Operator.LESS_THAN, "2"), null, "a_value")) .isInstanceOf(NullPointerException.class) .hasMessage("status can't be null"); } @Test - public void addCondition_accepts_null_value() { + void addCondition_accepts_null_value() { builder.addEvaluatedCondition(CONDITION_1, EvaluatedCondition.EvaluationStatus.NO_VALUE, null); assertThat(builder.getEvaluatedConditions()) @@ -86,12 +87,12 @@ public class EvaluatedQualityGateTest { } @Test - public void getEvaluatedConditions_returns_empty_with_no_condition_added_to_builder() { + void getEvaluatedConditions_returns_empty_with_no_condition_added_to_builder() { assertThat(builder.getEvaluatedConditions()).isEmpty(); } @Test - public void build_fails_with_IAE_if_condition_added_and_no_on_QualityGate() { + void build_fails_with_IAE_if_condition_added_and_no_on_QualityGate() { builder.setQualityGate(NO_CONDITION_QUALITY_GATE) .setStatus(randomStatus) .addEvaluatedCondition(CONDITION_1, randomEvaluationStatus, randomValue); @@ -102,7 +103,7 @@ public class EvaluatedQualityGateTest { } @Test - public void build_fails_with_IAE_if_condition_is_missing_for_one_defined_in_QualityGate() { + void build_fails_with_IAE_if_condition_is_missing_for_one_defined_in_QualityGate() { builder.setQualityGate(ONE_CONDITION_QUALITY_GATE) .setStatus(randomStatus); @@ -112,7 +113,16 @@ public class EvaluatedQualityGateTest { } @Test - public void getEvaluatedConditions_is_sorted() { + void build_whenConditionIsAFallback_shouldNotThrowException() { + builder.setQualityGate(ONE_CONDITION_QUALITY_GATE).setStatus(randomStatus) + .addEvaluatedCondition(new EvaluatedCondition(CONDITION_2, CONDITION_1, randomEvaluationStatus, randomValue, false)); + + Assertions.assertThat(builder.build() + .getEvaluatedConditions()).hasSize(1); + } + + @Test + void getEvaluatedConditions_is_sorted() { EvaluatedQualityGate underTest = builder .setQualityGate(ALL_CONDITIONS_QUALITY_GATE) .setStatus(randomStatus) @@ -128,7 +138,7 @@ public class EvaluatedQualityGateTest { } @Test - public void verify_getters() { + void verify_getters() { EvaluatedQualityGate underTest = builder .setQualityGate(ONE_CONDITION_QUALITY_GATE) .setStatus(randomStatus) @@ -142,7 +152,7 @@ public class EvaluatedQualityGateTest { } @Test - public void verify_getters_when_no_condition() { + void verify_getters_when_no_condition() { EvaluatedQualityGate underTest = builder .setQualityGate(NO_CONDITION_QUALITY_GATE) .setStatus(randomStatus) @@ -154,7 +164,7 @@ public class EvaluatedQualityGateTest { } @Test - public void verify_getters_when_multiple_conditions() { + void verify_getters_when_multiple_conditions() { QualityGate qualityGate = new QualityGate(QUALITY_GATE_ID, QUALITY_GATE_NAME, ImmutableSet.of(CONDITION_1, CONDITION_2)); EvaluatedQualityGate underTest = builder .setQualityGate(qualityGate) @@ -171,7 +181,7 @@ public class EvaluatedQualityGateTest { } @Test - public void equals_is_based_on_all_fields() { + void equals_is_based_on_all_fields() { EvaluatedQualityGate.Builder builder = this.builder .setQualityGate(ONE_CONDITION_QUALITY_GATE) .setStatus(Level.ERROR) @@ -193,7 +203,7 @@ public class EvaluatedQualityGateTest { } @Test - public void hashcode_is_based_on_all_fields() { + void hashcode_is_based_on_all_fields() { EvaluatedQualityGate.Builder builder = this.builder .setQualityGate(ONE_CONDITION_QUALITY_GATE) .setStatus(Level.ERROR) diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateEvaluatorImplTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateEvaluatorImplTest.java index 31f22433cf3..07af74eed75 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateEvaluatorImplTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateEvaluatorImplTest.java @@ -25,7 +25,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.sonar.api.CoreProperties; import org.sonar.api.config.Configuration; import org.sonar.api.config.internal.ConfigurationBridge; @@ -34,25 +34,28 @@ import org.sonar.api.measures.Metric; import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.sonar.api.measures.CoreMetrics.NEW_DUPLICATED_LINES_KEY; import static org.sonar.api.measures.CoreMetrics.NEW_LINES_KEY; import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY; -public class QualityGateEvaluatorImplTest { +class QualityGateEvaluatorImplTest { private final MapSettings settings = new MapSettings(); private final Configuration configuration = new ConfigurationBridge(settings); - private final QualityGateEvaluator underTest = new QualityGateEvaluatorImpl(); + QualityGateFallbackManager qualityGateFallbackManager = mock(QualityGateFallbackManager.class); + private final QualityGateEvaluator underTest = new QualityGateEvaluatorImpl(qualityGateFallbackManager); @Test - public void getMetricKeys_includes_by_default_new_lines() { + void getMetricKeys_includes_by_default_new_lines() { QualityGate gate = mock(QualityGate.class); assertThat(underTest.getMetricKeys(gate)).containsExactly(NEW_LINES_KEY); } @Test - public void getMetricKeys_includes_metrics_from_qgate() { + void getMetricKeys_includes_metrics_from_qgate() { Set metricKeys = ImmutableSet.of("foo", "bar", "baz"); Set conditions = metricKeys.stream().map(key -> { Condition condition = mock(Condition.class); @@ -66,7 +69,40 @@ public class QualityGateEvaluatorImplTest { } @Test - public void evaluated_conditions_are_sorted() { + void getMetricKeys_shouldIncludeFallbackConditionsMetricKeys() { + Set metricKeys = ImmutableSet.of("foo", "bar", "baz"); + when(qualityGateFallbackManager.getFallbackCondition(any())).thenReturn(Optional.of(new Condition("fallback", Condition.Operator.GREATER_THAN, "0"))); + Set conditions = metricKeys.stream().map(key -> { + Condition condition = mock(Condition.class); + when(condition.getMetricKey()).thenReturn(key); + return condition; + }).collect(Collectors.toSet()); + + QualityGate gate = mock(QualityGate.class); + when(gate.getConditions()).thenReturn(conditions); + assertThat(underTest.getMetricKeys(gate)).containsAll(metricKeys); + assertThat(underTest.getMetricKeys(gate)).contains("fallback"); + } + + @Test + void evaluate_whenConditionHasNoDataAndHasFallBack_shouldEvaluateFallbackInstead() { + when(qualityGateFallbackManager.getFallbackCondition(any())).thenReturn(Optional.of(new Condition("fallback", Condition.Operator.GREATER_THAN, "0"))); + Condition condition = mock(Condition.class); + when(condition.getMetricKey()).thenReturn("foo"); + + QualityGate gate = mock(QualityGate.class); + when(gate.getConditions()).thenReturn(Set.of(condition)); + QualityGateEvaluator.Measures measures = mock(QualityGateEvaluator.Measures.class); + when(measures.get("foo")).thenReturn(Optional.empty()); + when(measures.get("fallback")).thenReturn(Optional.of(new FakeMeasure(1))); + + assertThat(underTest.evaluate(gate, measures, configuration).getEvaluatedConditions()) + .extracting(x -> x.getCondition().getMetricKey(), x -> x.getOriginalCondition().getMetricKey()) + .containsExactly(tuple("fallback", "foo")); + } + + @Test + void evaluated_conditions_are_sorted() { Set metricKeys = ImmutableSet.of("foo", "bar", NEW_MAINTAINABILITY_RATING_KEY); Set conditions = metricKeys.stream().map(key -> { Condition condition = mock(Condition.class); @@ -79,11 +115,11 @@ public class QualityGateEvaluatorImplTest { QualityGateEvaluator.Measures measures = mock(QualityGateEvaluator.Measures.class); assertThat(underTest.evaluate(gate, measures, configuration).getEvaluatedConditions()).extracting(x -> x.getCondition().getMetricKey()) - .containsExactly(NEW_MAINTAINABILITY_RATING_KEY, "bar", "foo"); + .containsExactly(NEW_MAINTAINABILITY_RATING_KEY, "bar", "foo"); } @Test - public void evaluate_is_OK_for_empty_qgate() { + void evaluate_is_OK_for_empty_qgate() { QualityGate gate = mock(QualityGate.class); QualityGateEvaluator.Measures measures = mock(QualityGateEvaluator.Measures.class); EvaluatedQualityGate evaluatedQualityGate = underTest.evaluate(gate, measures, configuration); @@ -91,7 +127,7 @@ public class QualityGateEvaluatorImplTest { } @Test - public void evaluate_is_ERROR() { + void evaluate_is_ERROR() { Condition condition = new Condition(NEW_MAINTAINABILITY_RATING_KEY, Condition.Operator.GREATER_THAN, "0"); QualityGate gate = mock(QualityGate.class); @@ -102,7 +138,7 @@ public class QualityGateEvaluatorImplTest { } @Test - public void evaluate_for_small_changes() { + void evaluate_for_small_changes() { Condition condition = new Condition(NEW_DUPLICATED_LINES_KEY, Condition.Operator.GREATER_THAN, "0"); Map notSmallChange = new HashMap<>(); diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateFallbackManagerTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateFallbackManagerTest.java new file mode 100644 index 00000000000..2552761fff3 --- /dev/null +++ b/server/sonar-server-common/src/test/java/org/sonar/server/qualitygate/QualityGateFallbackManagerTest.java @@ -0,0 +1,57 @@ +/* + * 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.qualitygate; + +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.core.metric.SoftwareQualitiesMetrics; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class QualityGateFallbackManagerTest { + + QualityGateFallbackManager underTest = new QualityGateFallbackManager(); + + @Test + void getFallbackCondition_whenConditionIsSoftwareQualityMetricWithEquivalence_shouldReturnExpectedCondition() { + Condition condition = new Condition(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING_KEY, Condition.Operator.GREATER_THAN, "3"); + Optional fallbackCondition = underTest.getFallbackCondition(condition); + assertThat(fallbackCondition).isPresent() + .get() + .extracting(Condition::getMetricKey, Condition::getOperator, Condition::getErrorThreshold) + .containsExactly(CoreMetrics.SECURITY_RATING_KEY, Condition.Operator.GREATER_THAN, "3"); + } + + @Test + void getFallbackCondition_whenConditionIsStandardMetricWithEquivalence_shouldNotReturnCondition() { + Condition condition = new Condition(CoreMetrics.SECURITY_RATING_KEY, Condition.Operator.GREATER_THAN, "3"); + Optional fallbackCondition = underTest.getFallbackCondition(condition); + assertThat(fallbackCondition).isEmpty(); + } + + @Test + void getFallbackCondition_whenConditionIsAnyMetricWithoutEquivalence_shouldNotReturnCondition() { + Condition condition = new Condition(CoreMetrics.LINES_KEY, Condition.Operator.GREATER_THAN, "3"); + Optional fallbackCondition = underTest.getFallbackCondition(condition); + assertThat(fallbackCondition).isEmpty(); + } + +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java index 41490914c5b..42c23ed78ed 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateConditionsUpdater.java @@ -39,7 +39,7 @@ import org.sonar.db.qualitygate.QualityGateConditionDto; import org.sonar.db.qualitygate.QualityGateDto; import org.sonar.server.exceptions.NotFoundException; import org.sonar.server.measure.Rating; -import org.sonar.server.qualitygate.ws.StandardToMQRMetrics; +import org.sonar.server.metric.StandardToMQRMetrics; import static com.google.common.base.Strings.isNullOrEmpty; import static java.lang.Double.parseDouble; diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModeChecker.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModeChecker.java index e664db04910..413aabad2db 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModeChecker.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModeChecker.java @@ -23,7 +23,7 @@ import java.util.List; import java.util.Set; import java.util.stream.Collectors; import org.sonar.db.metric.MetricDto; -import org.sonar.server.qualitygate.ws.StandardToMQRMetrics; +import org.sonar.server.metric.StandardToMQRMetrics; public class QualityGateModeChecker { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java index 53b6a93cc38..24c4eb83a21 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java @@ -30,6 +30,7 @@ public class QualityGateModule extends Module { QualityGateModeChecker.class, QualityGateConditionsUpdater.class, QualityGateFinder.class, - QualityGateEvaluatorImpl.class); + QualityGateEvaluatorImpl.class, + QualityGateFallbackManager.class); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java index 1c785a45dd1..4579de0b936 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java @@ -29,6 +29,6 @@ public class QualityGateModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new QualityGateModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(6); + assertThat(container.getAddedObjects()).hasSize(7); } } -- 2.39.5