Browse Source

SONAR-17815 add isCaycCompliant flag in qualitygate endpoints

tags/9.9.0.65466
Zipeng WU 1 year ago
parent
commit
540e49d2e0
16 changed files with 298 additions and 162 deletions
  1. 81
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java
  2. 1
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java
  3. 7
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java
  4. 8
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java
  5. 9
    40
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java
  6. 14
    14
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java
  7. 1
    0
      server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json
  8. 98
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java
  9. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java
  10. 28
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java
  11. 25
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java
  12. 2
    27
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java
  13. 21
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java
  14. 0
    41
      server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json
  15. 0
    32
      server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json
  16. 2
    0
      sonar-ws/src/main/protobuf/ws-qualitygates.proto

+ 81
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateCaycChecker.java View File

@@ -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;
}

}

+ 1
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/QualityGateModule.java View File

@@ -26,6 +26,7 @@ public class QualityGateModule extends Module {
protected void configureModule() {
add(
QualityGateUpdater.class,
QualityGateCaycChecker.class,
QualityGateConditionsUpdater.class,
QualityGateFinder.class,
QualityGateEvaluatorImpl.class);

+ 7
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ListAction.java View 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()));

+ 8
- 3
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ProjectStatusAction.java View 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();
}


+ 9
- 40
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatter.java View 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() {

+ 14
- 14
server/sonar-webserver-webapi/src/main/java/org/sonar/server/qualitygate/ws/ShowAction.java View 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()))

+ 1
- 0
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/qualitygate/ws/project_status-example.json View File

@@ -2,6 +2,7 @@
"projectStatus": {
"status": "ERROR",
"ignoredConditions": false,
"isCaycCompliant": false,
"conditions": [
{
"status": "ERROR",

+ 98
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateCaycCheckerTest.java View File

@@ -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()));
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/QualityGateModuleTest.java View 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);
}
}

+ 28
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ListActionTest.java View 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();

+ 25
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ProjectStatusActionTest.java View 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();

+ 2
- 27
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest.java View 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);
}
}

+ 21
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/qualitygate/ws/ShowActionTest.java View 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();

+ 0
- 41
server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_compliant_qg.json View File

@@ -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"
}
]
}

+ 0
- 32
server/sonar-webserver-webapi/src/test/resources/org/sonar/server/qualitygate/ws/QualityGateDetailsFormatterTest/cayc_missing_metric.json View File

@@ -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"
}
]
}

+ 2
- 0
sonar-ws/src/main/protobuf/ws-qualitygates.proto View 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 {

Loading…
Cancel
Save