From 540e49d2e07c44f5ef3bc735ab4647c0563a2575 Mon Sep 17 00:00:00 2001 From: Zipeng WU Date: Wed, 4 Jan 2023 11:40:44 +0100 Subject: [PATCH] SONAR-17815 add isCaycCompliant flag in qualitygate endpoints --- .../qualitygate/QualityGateCaycChecker.java | 81 +++++++++++++++ .../server/qualitygate/QualityGateModule.java | 1 + .../server/qualitygate/ws/ListAction.java | 8 +- .../qualitygate/ws/ProjectStatusAction.java | 11 ++- .../ws/QualityGateDetailsFormatter.java | 49 ++-------- .../server/qualitygate/ws/ShowAction.java | 28 +++--- .../ws/project_status-example.json | 1 + .../QualityGateCaycCheckerTest.java | 98 +++++++++++++++++++ .../qualitygate/QualityGateModuleTest.java | 2 +- .../server/qualitygate/ws/ListActionTest.java | 29 +++++- .../ws/ProjectStatusActionTest.java | 26 ++++- .../ws/QualityGateDetailsFormatterTest.java | 29 +----- .../server/qualitygate/ws/ShowActionTest.java | 22 ++++- .../cayc_compliant_qg.json | 41 -------- .../cayc_missing_metric.json | 32 ------ .../src/main/protobuf/ws-qualitygates.proto | 2 + 16 files changed, 298 insertions(+), 162 deletions(-) create mode 100644 server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java create mode 100644 server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java delete mode 100644 server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json delete mode 100644 server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json 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 index 00000000000..8a30e4d7b51 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java @@ -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 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; + } + +} 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 25237f6665c..de52987e164 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 @@ -26,6 +26,7 @@ public class QualityGateModule extends Module { protected void configureModule() { add( QualityGateUpdater.class, + QualityGateCaycChecker.class, QualityGateConditionsUpdater.class, QualityGateFinder.class, QualityGateEvaluatorImpl.class); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java index e529865c487..4a722672f7a 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java @@ -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 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())); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java index 25a6daa3825..da2660620f9 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java @@ -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 { "
  • 'Administer' rights on the specified project
  • " + "
  • 'Browse' on the specified project
  • " + "
  • 'Execute Analysis' on the specified project
  • " + - "",MSG_ONE_PROJECT_PARAMETER_ONLY, QG_STATUSES_ONE_LINE, ProjectStatusResponse.Status.NONE)) + "", 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 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(); } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java index 20594adc973..5351687d2c0 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java @@ -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 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 optionalMeasureData; private final Optional optionalSnapshot; + private final boolean isCaycCompliant; private final ProjectStatusResponse.ProjectStatus.Builder projectStatusBuilder; - public QualityGateDetailsFormatter(Optional measureData, Optional 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 isConditionOnValidPeriod() { diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java index 0614b0e22a8..decfc3346a6 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java @@ -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 conditions = getConditions(dbSession, qualityGate); Map 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 getMetricsByUuid(DbSession dbSession, Collection conditions) { Set 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 conditions, Map metricsByUuid) { + private ShowWsResponse buildResponse(DbSession dbSession, QualityGateDto qualityGate, QualityGateDto defaultQualityGate, Collection conditions, + Map 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())) diff --git a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json index 3ee1c35e54c..6119eada050 100644 --- a/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json +++ b/server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json @@ -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 index 00000000000..e9896afd8f6 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java @@ -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> 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())); + } +} 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 33d2568e204..0598c883410 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(4); + assertThat(container.getAddedObjects()).hasSize(5); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java index d7cd28de6fb..b314b63f220 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java index c946273d50b..53ace6c0539 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java index 3c4e7b0c4d1..c6aaf141573 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java @@ -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 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); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java index 7ef265731b4..4bf7df81744 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java @@ -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 index 32d45361fa7..00000000000 --- a/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json +++ /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 index 45ace94cbc0..00000000000 --- a/server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json +++ /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" - } - ] -} diff --git a/sonar-ws/src/main/protobuf/ws-qualitygates.proto b/sonar-ws/src/main/protobuf/ws-qualitygates.proto index 8c6b36a99d4..168a6b226ae 100644 --- a/sonar-ws/src/main/protobuf/ws-qualitygates.proto +++ b/sonar-ws/src/main/protobuf/ws-qualitygates.proto @@ -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 { -- 2.39.5