]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17815 add isCaycCompliant flag in qualitygate endpoints
authorZipeng WU <zipeng.wu@sonarsource.com>
Wed, 4 Jan 2023 10:40:44 +0000 (11:40 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 5 Jan 2023 20:02:56 +0000 (20:02 +0000)
16 files changed:
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java
server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json [deleted file]
server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json [deleted file]
sonar-ws/src/main/protobuf/ws-qualitygates.proto

diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java
new file mode 100644 (file)
index 0000000..8a30e4d
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+
+import static java.util.stream.Collectors.toUnmodifiableMap;
+import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING;
+import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING;
+import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
+
+public class QualityGateCaycChecker {
+  private static final Map<String, Double> CAYC_REQUIREMENTS = Stream.of(
+    NEW_MAINTAINABILITY_RATING,
+    NEW_RELIABILITY_RATING,
+    NEW_SECURITY_HOTSPOTS_REVIEWED,
+    NEW_SECURITY_RATING
+  ).collect(toUnmodifiableMap(Metric::getKey, Metric::getBestValue));
+
+  private final DbClient dbClient;
+
+  public QualityGateCaycChecker(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  public boolean checkCaycCompliant(DbSession dbSession, String qualityGateUuid) {
+    var conditionsByMetricId = dbClient.gateConditionDao().selectForQualityGate(dbSession, qualityGateUuid)
+      .stream()
+      .collect(uniqueIndex(QualityGateConditionDto::getMetricUuid));
+    var metrics = dbClient.metricDao().selectByUuids(dbSession, conditionsByMetricId.keySet())
+      .stream()
+      .filter(MetricDto::isEnabled)
+      .collect(Collectors.toSet());
+    long count = metrics.stream()
+      .filter(metric -> CAYC_REQUIREMENTS.containsKey(metric.getKey()))
+      .filter(metric -> checkMetricCaycCompliant(conditionsByMetricId.get(metric.getUuid()), metric))
+      .count();
+    return count == CAYC_REQUIREMENTS.size();
+  }
+
+  public boolean checkCaycCompliantFromProject(DbSession dbSession, String projectUuid) {
+    return Optional.ofNullable(dbClient.qualityGateDao().selectByProjectUuid(dbSession, projectUuid))
+      .or(() -> Optional.ofNullable(dbClient.qualityGateDao().selectDefault(dbSession)))
+      .map(qualityGate -> checkCaycCompliant(dbSession, qualityGate.getUuid()))
+      .orElse(false);
+  }
+
+  private static boolean checkMetricCaycCompliant(QualityGateConditionDto condition, MetricDto metric) {
+    Double errorThreshold = Double.valueOf(condition.getErrorThreshold());
+    Double caycRequiredThreshold = CAYC_REQUIREMENTS.get(metric.getKey());
+    return caycRequiredThreshold.compareTo(errorThreshold) == 0;
+  }
+
+}
index 25237f6665c9782fde33b722c4798df1d681790d..de52987e164ef7491c405a604a13247205b28637 100644 (file)
@@ -26,6 +26,7 @@ public class QualityGateModule extends Module {
   protected void configureModule() {
     add(
       QualityGateUpdater.class,
+      QualityGateCaycChecker.class,
       QualityGateConditionsUpdater.class,
       QualityGateFinder.class,
       QualityGateEvaluatorImpl.class);
index e529865c4872871af44211e921ab03e1214da54d..4a722672f7af383bdd78f1f28026cc46a4efb081 100644 (file)
@@ -29,6 +29,7 @@ import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.qualitygate.QualityGateDto;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.qualitygate.QualityGateFinder;
 import org.sonarqube.ws.Qualitygates.ListWsResponse;
 import org.sonarqube.ws.Qualitygates.ListWsResponse.QualityGate;
@@ -42,11 +43,13 @@ public class ListAction implements QualityGatesWsAction {
   private final DbClient dbClient;
   private final QualityGatesWsSupport wsSupport;
   private final QualityGateFinder finder;
+  private final QualityGateCaycChecker qualityGateCaycChecker;
 
-  public ListAction(DbClient dbClient, QualityGatesWsSupport wsSupport, QualityGateFinder finder) {
+  public ListAction(DbClient dbClient, QualityGatesWsSupport wsSupport, QualityGateFinder finder, QualityGateCaycChecker qualityGateCaycChecker) {
     this.dbClient = dbClient;
     this.wsSupport = wsSupport;
     this.finder = finder;
+    this.qualityGateCaycChecker = qualityGateCaycChecker;
   }
 
   @Override
@@ -56,6 +59,7 @@ public class ListAction implements QualityGatesWsAction {
       .setSince("4.3")
       .setResponseExample(Resources.getResource(this.getClass(), "list-example.json"))
       .setChangelog(
+        new Change("9.9", "'isCaycCompliant' field is added on quality gate"),
         new Change("8.4", "Field 'id' in the response is deprecated. Format changes from integer to string."),
         new Change("7.0", "'isDefault' field is added on quality gate"),
         new Change("7.0", "'default' field on root level is deprecated"),
@@ -69,6 +73,7 @@ public class ListAction implements QualityGatesWsAction {
     try (DbSession dbSession = dbClient.openSession(false)) {
       QualityGateDto defaultQualityGate = finder.getDefault(dbSession);
       Collection<QualityGateDto> qualityGates = dbClient.qualityGateDao().selectAll(dbSession);
+
       writeProtobuf(buildResponse(dbSession, qualityGates, defaultQualityGate), request, response);
     }
   }
@@ -83,6 +88,7 @@ public class ListAction implements QualityGatesWsAction {
           .setName(qualityGate.getName())
           .setIsDefault(qualityGate.getUuid().equals(defaultUuid))
           .setIsBuiltIn(qualityGate.isBuiltIn())
+          .setIsCaycCompliant(qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid()))
           .setActions(wsSupport.getActions(dbSession, qualityGate, defaultQualityGate))
           .build())
         .collect(toList()));
index 25a6daa382548030fc5ed1b724caf4e43a2c1ab6..da2660620f9983ed9e1a919be0339b2edaa38da4 100644 (file)
@@ -41,6 +41,7 @@ import org.sonar.db.permission.GlobalPermission;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.server.component.ComponentFinder;
 import org.sonar.server.exceptions.BadRequestException;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.user.UserSession;
 import org.sonar.server.ws.KeyExamples;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
@@ -67,11 +68,13 @@ public class ProjectStatusAction implements QualityGatesWsAction {
   private final DbClient dbClient;
   private final ComponentFinder componentFinder;
   private final UserSession userSession;
+  private final QualityGateCaycChecker qualityGateCaycChecker;
 
-  public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession) {
+  public ProjectStatusAction(DbClient dbClient, ComponentFinder componentFinder, UserSession userSession, QualityGateCaycChecker qualityGateCaycChecker) {
     this.dbClient = dbClient;
     this.componentFinder = componentFinder;
     this.userSession = userSession;
+    this.qualityGateCaycChecker = qualityGateCaycChecker;
   }
 
   @Override
@@ -87,11 +90,12 @@ public class ProjectStatusAction implements QualityGatesWsAction {
         "<li>'Administer' rights on the specified project</li>" +
         "<li>'Browse' on the specified project</li>" +
         "<li>'Execute Analysis' on the specified project</li>" +
-        "</ul>",MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
+        "</ul>", MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE))
       .setResponseExample(getClass().getResource("project_status-example.json"))
       .setSince("5.3")
       .setHandler(this)
       .setChangelog(
+        new Change("9.9", "'isCaycCompliant' field is added to the response"),
         new Change("9.5", "The 'Execute Analysis' permission also allows to access the endpoint"),
         new Change("8.5", "The field 'periods' in the response is deprecated. Use 'period' instead"),
         new Change("7.7", "The parameters 'branch' and 'pullRequest' were added"),
@@ -148,9 +152,10 @@ public class ProjectStatusAction implements QualityGatesWsAction {
     ProjectAndSnapshot projectAndSnapshot = getProjectAndSnapshot(dbSession, analysisId, projectUuid, projectKey, branchKey, pullRequestId);
     checkPermission(projectAndSnapshot.project);
     Optional<String> measureData = loadQualityGateDetails(dbSession, projectAndSnapshot, analysisId != null);
+    var isCaycCompliant = qualityGateCaycChecker.checkCaycCompliantFromProject(dbSession, projectAndSnapshot.project.getUuid());
 
     return ProjectStatusResponse.newBuilder()
-      .setProjectStatus(new QualityGateDetailsFormatter(measureData, projectAndSnapshot.snapshotDto).format())
+      .setProjectStatus(new QualityGateDetailsFormatter(measureData.orElse(null), projectAndSnapshot.snapshotDto.orElse(null), isCaycCompliant).format())
       .build();
   }
 
index 20594adc9734d83ca8274afa8340fde9bb3e8085..5351687d2c00dcec83d86960dd5fd9b5e34345a1 100644 (file)
@@ -23,41 +23,28 @@ import com.google.gson.JsonArray;
 import com.google.gson.JsonElement;
 import com.google.gson.JsonObject;
 import com.google.gson.JsonParser;
-import java.util.Map;
 import java.util.Optional;
 import java.util.function.Predicate;
-import java.util.stream.Stream;
 import java.util.stream.StreamSupport;
 import javax.annotation.Nullable;
-import org.sonar.api.measures.Metric;
 import org.sonar.db.component.SnapshotDto;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.NewCodePeriod;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse.Period;
 
 import static com.google.common.base.Strings.isNullOrEmpty;
-import static java.util.stream.Collectors.toUnmodifiableMap;
-import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING;
-import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING;
 import static org.sonar.api.utils.DateUtils.formatDateTime;
 
 public class QualityGateDetailsFormatter {
-  public static final String METRIC_KEY = "metric";
-  private static final Map<String, Double> CAYC_REQUIREMENTS = Stream.of(
-    NEW_MAINTAINABILITY_RATING,
-    NEW_RELIABILITY_RATING,
-    NEW_SECURITY_HOTSPOTS_REVIEWED,
-    NEW_SECURITY_RATING
-  ).collect(toUnmodifiableMap(Metric::getKey, Metric::getBestValue));
   private final Optional<String> optionalMeasureData;
   private final Optional<SnapshotDto> optionalSnapshot;
+  private final boolean isCaycCompliant;
   private final ProjectStatusResponse.ProjectStatus.Builder projectStatusBuilder;
 
-  public QualityGateDetailsFormatter(Optional<String> measureData, Optional<SnapshotDto> snapshot) {
-    this.optionalMeasureData = measureData;
-    this.optionalSnapshot = snapshot;
+  public QualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshot, boolean isCaycCompliant) {
+    this.optionalMeasureData = Optional.ofNullable(measureData);
+    this.optionalSnapshot = Optional.ofNullable(snapshot);
+    this.isCaycCompliant = isCaycCompliant;
     this.projectStatusBuilder = ProjectStatusResponse.ProjectStatus.newBuilder();
   }
 
@@ -70,33 +57,15 @@ public class QualityGateDetailsFormatter {
 
     ProjectStatusResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString());
     projectStatusBuilder.setStatus(qualityGateStatus);
+    projectStatusBuilder.setIsCaycCompliant(isCaycCompliant);
 
     formatIgnoredConditions(json);
     formatConditions(json.getAsJsonArray("conditions"));
-    formatCleanAsYouCodeCompliant(json.getAsJsonArray("conditions"));
     formatPeriods();
 
     return projectStatusBuilder.build();
   }
 
-  private void formatCleanAsYouCodeCompliant(@Nullable JsonArray jsonConditions) {
-    if (jsonConditions == null) {
-      return;
-    }
-
-    long matchCount = jsonConditions.asList().stream()
-      .map(JsonElement::getAsJsonObject)
-      .filter(jsonObject -> CAYC_REQUIREMENTS.containsKey(jsonObject.get(METRIC_KEY).getAsString()))
-      .filter(jsonObject -> {
-        String metricKey = jsonObject.get(METRIC_KEY).getAsString();
-        Double value = jsonObject.get("error").getAsDouble();
-        return CAYC_REQUIREMENTS.get(metricKey).compareTo(value) == 0;
-      })
-      .count();
-
-    projectStatusBuilder.setIsCaycCompliant(matchCount == CAYC_REQUIREMENTS.size());
-  }
-
   private void formatIgnoredConditions(JsonObject json) {
     JsonElement ignoredConditions = json.get("ignoredConditions");
     if (ignoredConditions != null) {
@@ -201,7 +170,7 @@ public class QualityGateDetailsFormatter {
   }
 
   private static void formatConditionMetric(ProjectStatusResponse.Condition.Builder conditionBuilder, JsonObject jsonCondition) {
-    JsonElement metric = jsonCondition.get(METRIC_KEY);
+    JsonElement metric = jsonCondition.get("metric");
     if (metric != null && !isNullOrEmpty(metric.getAsString())) {
       conditionBuilder.setMetricKey(metric.getAsString());
     }
@@ -234,8 +203,8 @@ public class QualityGateDetailsFormatter {
     throw new IllegalStateException(String.format("Unknown quality gate comparator '%s'", measureOp));
   }
 
-  private static ProjectStatusResponse.ProjectStatus newResponseWithoutQualityGateDetails() {
-    return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).build();
+  private ProjectStatusResponse.ProjectStatus newResponseWithoutQualityGateDetails() {
+    return ProjectStatusResponse.ProjectStatus.newBuilder().setStatus(ProjectStatusResponse.Status.NONE).setIsCaycCompliant(isCaycCompliant).build();
   }
 
   private static Predicate<JsonObject> isConditionOnValidPeriod() {
index 0614b0e22a89c8b0a2625a3ccb694e9122a7b50b..decfc3346a64c004aaa4e2f38edb57899e928f7f 100644 (file)
@@ -34,6 +34,7 @@ import org.sonar.db.DbSession;
 import org.sonar.db.metric.MetricDto;
 import org.sonar.db.qualitygate.QualityGateConditionDto;
 import org.sonar.db.qualitygate.QualityGateDto;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.qualitygate.QualityGateFinder;
 import org.sonarqube.ws.Qualitygates.ShowWsResponse;
 
@@ -53,20 +54,20 @@ public class ShowAction implements QualityGatesWsAction {
   private final DbClient dbClient;
   private final QualityGateFinder qualityGateFinder;
   private final QualityGatesWsSupport wsSupport;
+  private final QualityGateCaycChecker qualityGateCaycChecker;
 
-  public ShowAction(DbClient dbClient, QualityGateFinder qualityGateFinder, QualityGatesWsSupport wsSupport) {
+  public ShowAction(DbClient dbClient, QualityGateFinder qualityGateFinder, QualityGatesWsSupport wsSupport, QualityGateCaycChecker qualityGateCaycChecker) {
     this.dbClient = dbClient;
     this.qualityGateFinder = qualityGateFinder;
     this.wsSupport = wsSupport;
+    this.qualityGateCaycChecker = qualityGateCaycChecker;
   }
 
-  @Override
-  public void define(WebService.NewController controller) {
-    WebService.NewAction action = controller.createAction("show")
-      .setDescription("Display the details of a quality gate")
-      .setSince("4.3")
+  @Override public void define(WebService.NewController controller) {
+    WebService.NewAction action = controller.createAction("show").setDescription("Display the details of a quality gate").setSince("4.3")
       .setResponseExample(Resources.getResource(this.getClass(), "show-example.json"))
       .setChangelog(
+        new Change("9.9", "'isCaycCompliant' field is added to the response"),
         new Change("8.4", "Parameter 'id' is deprecated. Format changes from integer to string. Use 'name' instead."),
         new Change("8.4", "Field 'id' in the response is deprecated."),
         new Change("7.6", "'period' and 'warning' fields of conditions are removed from the response"),
@@ -83,8 +84,7 @@ public class ShowAction implements QualityGatesWsAction {
       .setExampleValue("My Quality Gate");
   }
 
-  @Override
-  public void handle(Request request, Response response) {
+  @Override public void handle(Request request, Response response) {
     String id = request.param(PARAM_ID);
     String name = request.param(PARAM_NAME);
     checkOneOfIdOrNamePresent(id, name);
@@ -94,7 +94,8 @@ public class ShowAction implements QualityGatesWsAction {
       Collection<QualityGateConditionDto> conditions = getConditions(dbSession, qualityGate);
       Map<String, MetricDto> metricsByUuid = getMetricsByUuid(dbSession, conditions);
       QualityGateDto defaultQualityGate = qualityGateFinder.getDefault(dbSession);
-      writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid), request, response);
+      boolean isCaycCompliant = qualityGateCaycChecker.checkCaycCompliant(dbSession, qualityGate.getUuid());
+      writeProtobuf(buildResponse(dbSession, qualityGate, defaultQualityGate, conditions, metricsByUuid, isCaycCompliant), request, response);
     }
   }
 
@@ -114,17 +115,16 @@ public class ShowAction implements QualityGatesWsAction {
 
   private Map<String, MetricDto> getMetricsByUuid(DbSession dbSession, Collection<QualityGateConditionDto> conditions) {
     Set<String> metricUuids = conditions.stream().map(QualityGateConditionDto::getMetricUuid).collect(toSet());
-    return dbClient.metricDao().selectByUuids(dbSession, metricUuids).stream()
-      .filter(MetricDto::isEnabled)
-      .collect(uniqueIndex(MetricDto::getUuid));
+    return dbClient.metricDao().selectByUuids(dbSession, metricUuids).stream().filter(MetricDto::isEnabled).collect(uniqueIndex(MetricDto::getUuid));
   }
 
-  private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate,
-                                       Collection<QualityGateConditionDto> conditions, Map<String, MetricDto> metricsByUuid) {
+  private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate, Collection<QualityGateConditionDto> conditions,
+    Map<String, MetricDto> metricsByUuid, boolean isCaycCompliant) {
     return ShowWsResponse.newBuilder()
       .setId(qualityGate.getUuid())
       .setName(qualityGate.getName())
       .setIsBuiltIn(qualityGate.isBuiltIn())
+      .setIsCaycCompliant(isCaycCompliant)
       .addAllConditions(conditions.stream()
         .map(toWsCondition(metricsByUuid))
         .collect(toList()))
index 3ee1c35e54c00aa8f64d4754e16bce655e129d87..6119eada050ae77be57873cc62c8fc19646bd6aa 100644 (file)
@@ -2,6 +2,7 @@
   "projectStatus": {
     "status": "ERROR",
     "ignoredConditions": false,
+    "isCaycCompliant": false,
     "conditions": [
       {
         "status": "ERROR",
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java
new file mode 100644 (file)
index 0000000..e9896af
--- /dev/null
@@ -0,0 +1,98 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.utils.System2;
+import org.sonar.core.util.Uuids;
+import org.sonar.db.DbTester;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.qualitygate.QualityGateConditionDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING;
+import static org.sonar.api.measures.CoreMetrics.NEW_RELIABILITY_RATING;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_RATING;
+
+public class QualityGateCaycCheckerTest {
+
+  @Rule
+  public DbTester db = DbTester.create(System2.INSTANCE);
+  QualityGateCaycChecker underTest = new QualityGateCaycChecker(db.getDbClient());
+
+  @Test
+  public void checkCaycCompliant() {
+    String qualityGateUuid = "abcd";
+    List<Metric<Integer>> CAYC_REQUIREMENT_METRICS = List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_SECURITY_RATING);
+    CAYC_REQUIREMENT_METRICS
+      .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue()));
+    assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isTrue();
+  }
+
+  @Test
+  public void check_Cayc_NonCompliant_with_lesser_threshold_value() {
+    var metrics = List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED, NEW_SECURITY_RATING).stream()
+      .map(this::insertMetric)
+      .collect(Collectors.toList());
+
+    IntStream.range(0, metrics.size()).forEach(idx -> {
+      String qualityGateUuid = "abcd" + idx;
+      for (int i = 0; i < metrics.size(); i++) {
+        var metric = metrics.get(i);
+        insertCondition(metric, qualityGateUuid, idx == i ? metric.getWorstValue() : metric.getBestValue());
+      }
+      assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse();
+    });
+  }
+
+  @Test
+  public void check_Cayc_NonCompliant_with_missing_metric() {
+    String qualityGateUuid = "abcd";
+    List.of(NEW_MAINTAINABILITY_RATING, NEW_RELIABILITY_RATING, NEW_SECURITY_HOTSPOTS_REVIEWED)
+      .forEach(metric -> insertCondition(insertMetric(metric), qualityGateUuid, metric.getBestValue()));
+    assertThat(underTest.checkCaycCompliant(db.getSession(), qualityGateUuid)).isFalse();
+  }
+
+  private void insertCondition(MetricDto metricDto, String qualityGateUuid, Double threshold) {
+    QualityGateConditionDto newCondition = new QualityGateConditionDto().setQualityGateUuid(qualityGateUuid)
+      .setUuid(Uuids.create())
+      .setMetricUuid(metricDto.getUuid())
+      .setOperator("LT")
+      .setErrorThreshold(threshold.toString());
+    db.getDbClient().gateConditionDao().insert(newCondition, db.getSession());
+    db.commit();
+  }
+
+  private MetricDto insertMetric(Metric metric) {
+    return db.measures().insertMetric(m -> m
+      .setKey(metric.key())
+      .setValueType(metric.getType().name())
+      .setHidden(false)
+      .setBestValue(metric.getBestValue())
+      .setBestValue(metric.getWorstValue())
+      .setDirection(metric.getDirection()));
+  }
+}
index 33d2568e204ea13ff09b34fdef3468119521c0bf..0598c8834108543d6ae7583054536553c7351e1b 100644 (file)
@@ -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(4);
+    assertThat(container.getAddedObjects()).hasSize(5);
   }
 }
index d7cd28de6fb5d6c33248846bc72e7b90f20260b8..b314b63f2206f2367e6aac4cb574fbf39ee79613 100644 (file)
@@ -23,10 +23,12 @@ import org.junit.Rule;
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.qualitygate.QualityGateDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.component.TestComponentFinder;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.qualitygate.QualityGateFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
@@ -35,6 +37,10 @@ import org.sonarqube.ws.Qualitygates.ListWsResponse.QualityGate;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.AssertionsForClassTypes.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 import static org.sonar.test.JsonAssert.assertJson;
@@ -50,8 +56,10 @@ public class ListActionTest {
   private final DbClient dbClient = db.getDbClient();
   private final QualityGateFinder qualityGateFinder = new QualityGateFinder(dbClient);
 
+  private final QualityGateCaycChecker qualityGateCaycChecker = mock(QualityGateCaycChecker.class);
+
   private final WsActionTester ws = new WsActionTester(new ListAction(db.getDbClient(),
-    new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder));
+    new QualityGatesWsSupport(dbClient, userSession, TestComponentFinder.from(db)), qualityGateFinder, qualityGateCaycChecker));
 
   @Test
   public void list_quality_gates() {
@@ -85,6 +93,25 @@ public class ListActionTest {
         tuple(qualityGate2.getUuid(), false));
   }
 
+  @Test
+  public void test_isCaycCompliant_flag() {
+    QualityGateDto qualityGate1 = db.qualityGates().insertQualityGate();
+    QualityGateDto qualityGate2 = db.qualityGates().insertQualityGate();
+    when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate1.getUuid()))).thenReturn(true);
+    when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate2.getUuid()))).thenReturn(false);
+
+    db.qualityGates().setDefaultQualityGate(qualityGate1);
+
+    ListWsResponse response = ws.newRequest()
+      .executeProtobuf(ListWsResponse.class);
+
+    assertThat(response.getQualitygatesList())
+      .extracting(QualityGate::getId, QualityGate::getIsCaycCompliant)
+      .containsExactlyInAnyOrder(
+        tuple(qualityGate1.getUuid(), true),
+        tuple(qualityGate2.getUuid(), false));
+  }
+
   @Test
   public void test_deprecated_default_field() {
     QualityGateDto defaultQualityGate = db.qualityGates().insertQualityGate();
index c946273d50b295407cc725634abdffaa32220646..53ace6c0539ff636c41ebfb2539a69eeb387bda6 100644 (file)
@@ -37,10 +37,12 @@ import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.metric.MetricDto;
 import org.sonar.db.permission.GlobalPermission;
+import org.sonar.db.qualitygate.QualityGateDto;
 import org.sonar.server.component.TestComponentFinder;
 import org.sonar.server.exceptions.BadRequestException;
 import org.sonar.server.exceptions.ForbiddenException;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
@@ -51,6 +53,10 @@ import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.Assertions.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.db.component.SnapshotTesting.newAnalysis;
 import static org.sonar.db.measure.MeasureTesting.newLiveMeasure;
 import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
@@ -72,8 +78,9 @@ public class ProjectStatusActionTest {
 
   private final DbClient dbClient = db.getDbClient();
   private final DbSession dbSession = db.getSession();
+  private final QualityGateCaycChecker qualityGateCaycChecker = mock(QualityGateCaycChecker.class);
 
-  private final WsActionTester ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession));
+  private final WsActionTester ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession, qualityGateCaycChecker));
 
   @Test
   public void test_definition() {
@@ -316,6 +323,23 @@ public class ProjectStatusActionTest {
       .executeProtobuf(ProjectStatusResponse.class);
   }
 
+  @Test
+  public void check_cayc_compliant_flag() {
+    ComponentDto project = db.components().insertPrivateProject();
+    var qg = db.qualityGates().insertBuiltInQualityGate();
+    db.qualityGates().setDefaultQualityGate(qg);
+    when(qualityGateCaycChecker.checkCaycCompliantFromProject(any(DbSession.class), eq(project.uuid()))).thenReturn(true);
+    SnapshotDto snapshot = dbClient.snapshotDao().insert(dbSession, newAnalysis(project));
+    dbSession.commit();
+    userSession.addProjectPermission(UserRole.USER, project);
+
+    ProjectStatusResponse result = ws.newRequest()
+      .setParam(PARAM_ANALYSIS_ID, snapshot.getUuid())
+      .executeProtobuf(ProjectStatusResponse.class);
+
+    assertThat(result.getProjectStatus().getIsCaycCompliant()).isTrue();
+  }
+
   @Test
   public void user_with_project_scan_permission_is_allowed_to_get_project_status() {
     ComponentDto project = db.components().insertPrivateProject();
index 3c4e7b0c4d182bd2d84d89f3265bff8d1939682c..c6aaf141573d66ef857f7b1b91bf47403492bfa7 100644 (file)
@@ -21,11 +21,9 @@ package org.sonar.server.qualitygate.ws;
 
 import java.io.IOException;
 import java.util.List;
-import java.util.Map;
 import java.util.Optional;
 import javax.annotation.Nullable;
 import org.apache.commons.io.IOUtils;
-import org.apache.commons.lang.text.StrSubstitutor;
 import org.junit.Test;
 import org.sonar.db.component.SnapshotDto;
 import org.sonarqube.ws.Qualitygates.ProjectStatusResponse;
@@ -51,6 +49,7 @@ public class QualityGateDetailsFormatterTest {
     ProjectStatus result = underTest.format();
 
     assertThat(result.getStatus()).isEqualTo(ProjectStatusResponse.Status.ERROR);
+    assertThat(result.getIsCaycCompliant()).isFalse();
     // check conditions
     assertThat(result.getConditionsCount()).isEqualTo(3);
     List<ProjectStatusResponse.Condition> conditions = result.getConditionsList();
@@ -145,31 +144,7 @@ public class QualityGateDetailsFormatterTest {
       .hasMessageContaining("Unknown quality gate comparator 'UNKNOWN'");
   }
 
-  @Test
-  public void verify_cayc_quality_gate_checked() throws IOException {
-    String measureDataRaw = IOUtils.toString(getClass().getResource("QualityGateDetailsFormatterTest/cayc_compliant_qg.json"));
-
-    String measureDataCompliant = StrSubstitutor.replace(measureDataRaw, Map.of("nmr_error", "1.0"));
-    underTest = newQualityGateDetailsFormatter(measureDataCompliant, null);
-    ProjectStatus result = underTest.format();
-    assertThat(result.getIsCaycCompliant()).isTrue();
-
-    String measureDataNonCompliant = StrSubstitutor.replace(measureDataRaw, Map.of("nmr_error", "2.0"));
-    underTest = newQualityGateDetailsFormatter(measureDataNonCompliant, null);
-    result = underTest.format();
-    assertThat(result.getIsCaycCompliant()).isFalse();
-  }
-
-  @Test
-  public void verify_cayc_quality_gate_with_missing_metric() throws IOException {
-    String measureData = IOUtils.toString(getClass().getResource("QualityGateDetailsFormatterTest/cayc_missing_metric.json"));
-
-    underTest = newQualityGateDetailsFormatter(measureData, null);
-    ProjectStatus result = underTest.format();
-    assertThat(result.getIsCaycCompliant()).isFalse();
-  }
-
   private static QualityGateDetailsFormatter newQualityGateDetailsFormatter(@Nullable String measureData, @Nullable SnapshotDto snapshotDto) {
-    return new QualityGateDetailsFormatter(Optional.ofNullable(measureData), Optional.ofNullable(snapshotDto));
+    return new QualityGateDetailsFormatter(measureData, snapshotDto, false);
   }
 }
index 7ef265731b497ff94497978fc105bf51ac048b43..4bf7df81744317e945ce0ce183e8eea0b88f1935 100644 (file)
@@ -24,6 +24,7 @@ import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.server.ws.WebService.Param;
 import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.metric.MetricDto;
 import org.sonar.db.qualitygate.QualityGateConditionDto;
@@ -31,6 +32,7 @@ import org.sonar.db.qualitygate.QualityGateDto;
 import org.sonar.db.user.UserDto;
 import org.sonar.server.component.TestComponentFinder;
 import org.sonar.server.exceptions.NotFoundException;
+import org.sonar.server.qualitygate.QualityGateCaycChecker;
 import org.sonar.server.qualitygate.QualityGateFinder;
 import org.sonar.server.tester.UserSessionRule;
 import org.sonar.server.ws.WsActionTester;
@@ -41,6 +43,10 @@ import static java.lang.String.format;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.assertj.core.api.AssertionsForClassTypes.tuple;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_GATES;
 import static org.sonar.db.permission.GlobalPermission.ADMINISTER_QUALITY_PROFILES;
 import static org.sonar.test.JsonAssert.assertJson;
@@ -52,10 +58,11 @@ public class ShowActionTest {
   public UserSessionRule userSession = UserSessionRule.standalone();
   @Rule
   public DbTester db = DbTester.create(System2.INSTANCE);
+  private final QualityGateCaycChecker qualityGateCaycChecker = mock(QualityGateCaycChecker.class);
 
   private final WsActionTester ws = new WsActionTester(
     new ShowAction(db.getDbClient(), new QualityGateFinder(db.getDbClient()),
-      new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db))));
+      new QualityGatesWsSupport(db.getDbClient(), userSession, TestComponentFinder.from(db)), qualityGateCaycChecker));
 
   @Test
   public void show() {
@@ -93,6 +100,19 @@ public class ShowActionTest {
     assertThat(response.getIsBuiltIn()).isTrue();
   }
 
+  @Test
+  public void show_isCaycCompliant() {
+    QualityGateDto qualityGate = db.qualityGates().insertQualityGate();
+    when(qualityGateCaycChecker.checkCaycCompliant(any(DbSession.class), eq(qualityGate.getUuid()))).thenReturn(true);
+    db.qualityGates().setDefaultQualityGate(qualityGate);
+
+    ShowWsResponse response = ws.newRequest()
+      .setParam("name", qualityGate.getName())
+      .executeProtobuf(ShowWsResponse.class);
+
+    assertThat(response.getIsCaycCompliant()).isTrue();
+  }
+
   @Test
   public void show_by_id() {
     QualityGateDto qualityGate = db.qualityGates().insertQualityGate();
diff --git a/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json b/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json
deleted file mode 100644 (file)
index 32d4536..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-{
-  "level": "ERROR",
-  "conditions": [
-    {
-      "metric": "new_maintainability_rating",
-      "op": "LT",
-      "period": 1,
-      "warning": "",
-      "error": "${nmr_error}",
-      "actual": "2",
-      "level": "ERROR"
-    },
-    {
-      "metric": "new_reliability_rating",
-      "op": "LT",
-      "period": 1,
-      "warning": "",
-      "error": "1.0",
-      "actual": "1",
-      "level": "OK"
-    },
-    {
-      "metric": "new_security_hotspots_reviewed",
-      "op": "GT",
-      "period": 1,
-      "warning": "",
-      "error": "100.0",
-      "actual": "100.0",
-      "level": "OK"
-    },
-    {
-      "metric": "new_security_rating",
-      "op": "LT",
-      "period": 1,
-      "warning": "",
-      "error": "1.0",
-      "actual": "2",
-      "level": "ERROR"
-    }
-  ]
-}
diff --git a/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json b/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json
deleted file mode 100644 (file)
index 45ace94..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-{
-  "level": "ERROR",
-  "conditions": [
-    {
-      "metric": "new_reliability_rating",
-      "op": "LT",
-      "period": 1,
-      "warning": "",
-      "error": "1.0",
-      "actual": "1",
-      "level": "OK"
-    },
-    {
-      "metric": "new_security_hotspots_reviewed",
-      "op": "GT",
-      "period": 1,
-      "warning": "",
-      "error": "100.0",
-      "actual": "100.0",
-      "level": "OK"
-    },
-    {
-      "metric": "new_security_rating",
-      "op": "LT",
-      "period": 1,
-      "warning": "",
-      "error": "1.0",
-      "actual": "2",
-      "level": "ERROR"
-    }
-  ]
-}
index 8c6b36a99d4678adbc08dc721b02f79db33c8006..168a6b226ae085f7c31c63610dfd2afd685e8b00 100644 (file)
@@ -131,6 +131,7 @@ message ShowWsResponse {
   repeated Condition conditions = 3;
   optional bool isBuiltIn = 4;
   optional Actions actions = 5;
+  optional bool isCaycCompliant = 6;
 
   message Condition {
     optional string id = 1;
@@ -167,6 +168,7 @@ message ListWsResponse {
     optional bool isDefault = 3;
     optional bool isBuiltIn = 4;
     optional Actions actions = 5;
+    optional bool isCaycCompliant = 6;
   }
 
   message RootActions {