@Immutable
public class QualityGateDetailsData {
private static final String FIELD_LEVEL = "level";
+ private static final String FIELD_IGNORED_CONDITIONS = "ignoredConditions";
private final Measure.Level level;
private final List<EvaluatedCondition> conditions;
+ private final boolean ignoredConditions;
- public QualityGateDetailsData(Measure.Level level, Iterable<EvaluatedCondition> conditions) {
+ public QualityGateDetailsData(Measure.Level level, Iterable<EvaluatedCondition> conditions, boolean ignoredConditions) {
this.level = requireNonNull(level);
this.conditions = from(conditions).toList();
+ this.ignoredConditions = ignoredConditions;
}
public String toJson() {
conditionResults.add(toJson(condition));
}
details.add("conditions", conditionResults);
+ details.addProperty(FIELD_IGNORED_CONDITIONS, ignoredConditions);
return details.toString();
}
private void updateMeasures(Component project, Set<Condition> conditions, QualityGateDetailsDataBuilder builder) {
Multimap<Metric, Condition> conditionsPerMetric = conditions.stream().collect(MoreCollectors.index(Condition::getMetric, java.util.function.Function.identity()));
+ boolean ignoredConditions = false;
for (Map.Entry<Metric, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
Metric metric = entry.getKey();
Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
}
final MetricEvaluationResult metricEvaluationResult = evaluateQualityGate(measure.get(), entry.getValue());
- final MetricEvaluationResult finalMetricEvaluationResult = smallChangesetQualityGateSpecialCase.applyIfNeeded(project, metricEvaluationResult);
+ final MetricEvaluationResult finalMetricEvaluationResult;
+ if (smallChangesetQualityGateSpecialCase.appliesTo(project, metricEvaluationResult)) {
+ finalMetricEvaluationResult = smallChangesetQualityGateSpecialCase.apply(metricEvaluationResult);
+ ignoredConditions = true;
+ } else {
+ finalMetricEvaluationResult = metricEvaluationResult;
+ }
String text = evaluationResultTextConverter.asText(finalMetricEvaluationResult.condition, finalMetricEvaluationResult.evaluationResult);
builder.addLabel(text);
builder.addEvaluatedCondition(finalMetricEvaluationResult);
}
+ builder.setIgnoredConditions(ignoredConditions);
}
private static MetricEvaluationResult evaluateQualityGate(Measure measure, Collection<Condition> conditions) {
Metric metric = metricRepository.getByKey(CoreMetrics.ALERT_STATUS_KEY);
measureRepository.add(project, metric, globalMeasure);
- String detailMeasureValue = new QualityGateDetailsData(builder.getGlobalLevel(), builder.getEvaluatedConditions()).toJson();
+ String detailMeasureValue = new QualityGateDetailsData(builder.getGlobalLevel(), builder.getEvaluatedConditions(), builder.isIgnoredConditions()).toJson();
Measure detailsMeasure = Measure.newMeasureBuilder().create(detailMeasureValue);
Metric qgDetailsMetric = metricRepository.getByKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY);
measureRepository.add(project, qgDetailsMetric, detailsMeasure);
private Measure.Level globalLevel = Measure.Level.OK;
private List<String> labels = new ArrayList<>();
private List<EvaluatedCondition> evaluatedConditions = new ArrayList<>();
+ private boolean ignoredConditions;
public Measure.Level getGlobalLevel() {
return globalLevel;
public List<EvaluatedCondition> getEvaluatedConditions() {
return evaluatedConditions;
}
+
+ public boolean isIgnoredConditions() {
+ return ignoredConditions;
+ }
+
+ public QualityGateDetailsDataBuilder setIgnoredConditions(boolean ignoredConditions) {
+ this.ignoredConditions = ignoredConditions;
+ return this;
+ }
}
private enum EvaluatedConditionToCondition implements Function<EvaluatedCondition, Condition> {
import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository;
import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository;
import org.sonar.server.computation.task.projectanalysis.qualitygate.EvaluationResult;
+import org.sonar.server.computation.task.projectanalysis.step.QualityGateMeasuresStep.MetricEvaluationResult;
import static java.util.Arrays.asList;
public class SmallChangesetQualityGateSpecialCase {
/**
- * Some metrics will only produce warnings (never errors) on very small change sets.
+ * Some metrics will be ignored on very small change sets.
*/
- private static final Collection<String> METRICS_THAT_CAN_ONLY_PRODUCE_WARNINGS_ON_SMALL_CHANGESETS = asList(
+ private static final Collection<String> METRICS_TO_IGNORE_ON_SMALL_CHANGESETS = asList(
CoreMetrics.NEW_COVERAGE_KEY,
CoreMetrics.NEW_LINE_COVERAGE_KEY,
CoreMetrics.NEW_BRANCH_COVERAGE_KEY,
this.metricRepository = metricRepository;
}
- QualityGateMeasuresStep.MetricEvaluationResult applyIfNeeded(Component project, @Nullable QualityGateMeasuresStep.MetricEvaluationResult metricEvaluationResult) {
- if (metricEvaluationResult == null) {
- return metricEvaluationResult;
- }
- if (metricEvaluationResult.evaluationResult.getLevel() == Measure.Level.OK) {
- return metricEvaluationResult;
- }
- if (!METRICS_THAT_CAN_ONLY_PRODUCE_WARNINGS_ON_SMALL_CHANGESETS.contains(metricEvaluationResult.condition.getMetric().getKey())) {
- return metricEvaluationResult;
- }
- if (!isSmallChangeset(project)) {
- return metricEvaluationResult;
- }
- return calculateModifiedResult(metricEvaluationResult);
+ public boolean appliesTo(Component project, @Nullable MetricEvaluationResult metricEvaluationResult) {
+ return metricEvaluationResult != null
+ && metricEvaluationResult.evaluationResult.getLevel() != Measure.Level.OK
+ && METRICS_TO_IGNORE_ON_SMALL_CHANGESETS.contains(metricEvaluationResult.condition.getMetric().getKey())
+ && isSmallChangeset(project);
}
- private QualityGateMeasuresStep.MetricEvaluationResult calculateModifiedResult(@Nullable QualityGateMeasuresStep.MetricEvaluationResult metricEvaluationResult) {
- return new QualityGateMeasuresStep.MetricEvaluationResult(
+ MetricEvaluationResult apply(MetricEvaluationResult metricEvaluationResult) {
+ return new MetricEvaluationResult(
new EvaluationResult(Measure.Level.OK, metricEvaluationResult.evaluationResult.getValue()), metricEvaluationResult.condition);
}
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.server.ws.Change;
import org.sonar.api.server.ws.Request;
import org.sonar.api.server.ws.Response;
import org.sonar.api.server.ws.WebService;
"</ul>", QG_STATUSES_ONE_LINE, ProjectStatusWsResponse.Status.NONE))
.setResponseExample(getClass().getResource("project_status-example.json"))
.setSince("5.3")
- .setHandler(this);
+ .setHandler(this)
+ .setChangelog(
+ new Change("6.4", "The field 'ignoredConditions' is added to the response")
+ );
action.createParam(PARAM_ANALYSIS_ID)
.setDescription("Analysis id")
ProjectStatusWsResponse.Status qualityGateStatus = measureLevelToQualityGateStatus(json.get("level").getAsString());
projectStatusBuilder.setStatus(qualityGateStatus);
+ formatIgnoredConditions(json);
formatConditions(json.getAsJsonArray("conditions"));
formatPeriods();
return projectStatusBuilder.build();
}
+ private void formatIgnoredConditions(JsonObject json) {
+ JsonElement ignoredConditions = json.get("ignoredConditions");
+ if (ignoredConditions != null) {
+ projectStatusBuilder.setIgnoredConditions(ignoredConditions.getAsBoolean());
+ } else {
+ projectStatusBuilder.setIgnoredConditions(false);
+ }
+ }
+
private void formatPeriods() {
if (!optionalSnapshot.isPresent()) {
return;
{
"projectStatus": {
"status": "ERROR",
+ "ignoredConditions": false,
"conditions": [
{
"status": "ERROR",
public class QualityGateDetailsDataTest {
@Test(expected = NullPointerException.class)
public void constructor_throws_NPE_if_Level_arg_is_null() {
- new QualityGateDetailsData(null, Collections.<EvaluatedCondition>emptyList());
+ new QualityGateDetailsData(null, Collections.<EvaluatedCondition>emptyList(), false);
}
@Test(expected = NullPointerException.class)
public void constructor_throws_NPE_if_Iterable_arg_is_null() {
- new QualityGateDetailsData(Measure.Level.OK, null);
+ new QualityGateDetailsData(Measure.Level.OK, null, false);
}
@Test
public void verify_json_when_there_is_no_condition() {
- String actualJson = new QualityGateDetailsData(Measure.Level.OK, Collections.<EvaluatedCondition>emptyList()).toJson();
+ String actualJson = new QualityGateDetailsData(Measure.Level.OK, Collections.<EvaluatedCondition>emptyList(), false).toJson();
JsonAssert.assertJson(actualJson).isSimilarTo("{" +
"\"level\":\"OK\"," +
new EvaluatedCondition(condition, Measure.Level.OK, value),
new EvaluatedCondition(condition, Measure.Level.WARN, value),
new EvaluatedCondition(condition, Measure.Level.ERROR, value));
- String actualJson = new QualityGateDetailsData(Measure.Level.OK, evaluatedConditions).toJson();
+ String actualJson = new QualityGateDetailsData(Measure.Level.OK, evaluatedConditions, false).toJson();
JsonAssert.assertJson(actualJson).isSimilarTo("{" +
"\"level\":\"OK\"," +
"]" +
"}");
}
+
+ @Test
+ public void verify_json_for_small_leak() {
+ String actualJson = new QualityGateDetailsData(Measure.Level.OK, Collections.<EvaluatedCondition>emptyList(), false).toJson();
+ JsonAssert.assertJson(actualJson).isSimilarTo("{\"ignoredConditions\": false}");
+
+ String actualJson2 = new QualityGateDetailsData(Measure.Level.OK, Collections.<EvaluatedCondition>emptyList(), true).toJson();
+ JsonAssert.assertJson(actualJson2).isSimilarTo("{\"ignoredConditions\": true}");
+ }
+
}
.hasQualityGateLevel(OK)
.hasQualityGateText("");
assertThat(getQGDetailsMeasure())
- .hasValue(new QualityGateDetailsData(OK, Collections.<EvaluatedCondition>emptyList()).toJson());
+ .hasValue(new QualityGateDetailsData(OK, Collections.<EvaluatedCondition>emptyList(), false).toJson());
QualityGateStatusHolderAssertions.assertThat(qualityGateStatusHolder)
.hasStatus(QualityGateStatus.OK)
.hasQualityGateLevel(OK)
.hasQualityGateText(dumbResultTextAnswer(equals2Condition, OK, rawValue));
assertThat(getQGDetailsMeasure().get())
- .hasValue(new QualityGateDetailsData(OK, of(new EvaluatedCondition(equals2Condition, OK, rawValue))).toJson());
+ .hasValue(new QualityGateDetailsData(OK, of(new EvaluatedCondition(equals2Condition, OK, rawValue)), false).toJson());
QualityGateStatusHolderAssertions.assertThat(qualityGateStatusHolder)
.hasStatus(QualityGateStatus.OK)
assertThat(getQGDetailsMeasure())
.hasValue(new QualityGateDetailsData(ERROR, of(
new EvaluatedCondition(equals1ErrorCondition, ERROR, rawValue),
- new EvaluatedCondition(equals1WarningCondition, WARN, rawValue))).toJson());
+ new EvaluatedCondition(equals1WarningCondition, WARN, rawValue)), false).toJson());
QualityGateStatusHolderAssertions.assertThat(qualityGateStatusHolder)
.hasStatus(QualityGateStatus.ERROR)
assertThat(getQGDetailsMeasure())
.hasValue(new QualityGateDetailsData(WARN, of(
new EvaluatedCondition(equals2Condition, OK, rawValue),
- new EvaluatedCondition(equals1WarningCondition, WARN, rawValue))).toJson());
+ new EvaluatedCondition(equals1WarningCondition, WARN, rawValue)), false).toJson());
QualityGateStatusHolderAssertions.assertThat(qualityGateStatusHolder)
.hasStatus(QualityGateStatus.WARN)
Component project = generateNewRootProject();
measureRepository.addRawMeasure(PROJECT_REF, CoreMetrics.NEW_LINES_KEY, newMeasureBuilder().setVariation(19).create(1000));
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(OK);
+ assertThat(result).isTrue();
}
@Test
Component project = generateNewRootProject();
measureRepository.addRawMeasure(PROJECT_REF, CoreMetrics.NEW_LINES_KEY, newMeasureBuilder().setVariation(19).create(1000));
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(OK);
+ assertThat(result).isTrue();
}
@Test
Component project = generateNewRootProject();
measureRepository.addRawMeasure(PROJECT_REF, CoreMetrics.NEW_LINES_KEY, newMeasureBuilder().setVariation(20).create(1000));
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(ERROR);
+ assertThat(result).isFalse();
}
@Test
Component project = generateNewRootProject();
measureRepository.addRawMeasure(PROJECT_REF, CoreMetrics.NEW_LINES_KEY, newMeasureBuilder().setVariation(19).create(1000));
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(ERROR);
+ assertThat(result).isFalse();
}
@Test
Component project = generateNewRootProject();
measureRepository.addRawMeasure(PROJECT_REF, CoreMetrics.NEW_LINES_KEY, newMeasureBuilder().setVariation(19).create(1000));
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(OK);
+ assertThat(result).isFalse();
}
@Test
QualityGateMeasuresStep.MetricEvaluationResult metricEvaluationResult = generateEvaluationResult(NEW_COVERAGE_KEY, ERROR);
Component project = generateNewRootProject();
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(project, metricEvaluationResult);
+ boolean result = underTest.appliesTo(project, metricEvaluationResult);
- assertThat(result.evaluationResult.getLevel()).isSameAs(ERROR);
+ assertThat(result).isFalse();
}
@Test
public void should_silently_ignore_null_values() throws Exception {
- QualityGateMeasuresStep.MetricEvaluationResult result = underTest.applyIfNeeded(mock(Component.class), null);
+ boolean result = underTest.appliesTo(mock(Component.class), null);
- assertThat(result).isNull();
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void apply() throws Exception {
+ Comparable<?> value = mock(Comparable.class);
+ Condition condition = mock(Condition.class);
+ QualityGateMeasuresStep.MetricEvaluationResult original = new QualityGateMeasuresStep.MetricEvaluationResult(
+ new EvaluationResult(Measure.Level.ERROR, value), condition);
+
+ QualityGateMeasuresStep.MetricEvaluationResult modified = underTest.apply(original);
+
+ assertThat(modified.evaluationResult.getLevel()).isSameAs(OK);
+ assertThat(modified.evaluationResult.getValue()).isSameAs(value);
+ assertThat(modified.condition).isSameAs(condition);
}
private Component generateNewRootProject() {
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.server.ws.Change;
+import org.sonar.api.server.ws.WebService;
import org.sonar.api.utils.System2;
import org.sonar.api.web.UserRole;
import org.sonar.db.DbClient;
import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
import static org.sonar.db.component.SnapshotTesting.newAnalysis;
import static org.sonar.db.measure.MeasureTesting.newMeasureDto;
import static org.sonar.db.metric.MetricTesting.newMetricDto;
ws = new WsActionTester(new ProjectStatusAction(dbClient, TestComponentFinder.from(db), userSession));
}
+ @Test
+ public void definition() throws Exception {
+ WebService.Action def = ws.getDef();
+ assertThat(def.params()).extracting(WebService.Param::key).containsExactlyInAnyOrder("analysisId", "projectKey", "projectId");
+ assertThat(def.changelog()).extracting(Change::getVersion, Change::getDescription).containsExactly(
+ tuple("6.4", "The field 'ignoredConditions' is added to the response")
+ );
+ }
+
@Test
public void json_example() throws IOException {
ComponentDto project = db.components().insertPrivateProject(db.organizations().insert());
optional Status status = 1;
repeated Condition conditions = 2;
repeated Period periods = 3;
+ optional bool ignoredConditions = 4;
}
message Condition {
2,user2,2014-04-01
2,user2,2014-04-01
2,user2,2014-04-01
-3,user3,2014-04-02
\ No newline at end of file
+3,user3,2014-04-03
\ No newline at end of file
import com.sonar.orchestrator.Orchestrator;
import com.sonar.orchestrator.build.SonarScanner;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.sonarqube.tests.Category6Suite;
import org.sonarqube.tests.Tester;
+import org.sonarqube.ws.MediaTypes;
import org.sonarqube.ws.Organizations;
+import org.sonarqube.ws.WsCe;
import org.sonarqube.ws.WsProjects.CreateWsResponse.Project;
import org.sonarqube.ws.WsQualityGates;
import org.sonarqube.ws.WsQualityGates.CreateWsResponse;
import org.sonarqube.ws.WsUsers;
+import org.sonarqube.ws.client.GetRequest;
+import org.sonarqube.ws.client.WsResponse;
import org.sonarqube.ws.client.qualitygate.CreateConditionRequest;
+import org.sonarqube.ws.client.qualitygate.ProjectStatusWsRequest;
+import org.sonarqube.ws.client.qualitygate.UpdateConditionRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static util.ItUtils.getMeasure;
String password = "password1";
WsUsers.CreateWsResponse.User user = tester.users().generateAdministrator(organization, u -> u.setPassword(password));
+ // no leak => use usual behaviour
SonarScanner analysis = SonarScanner
.create(projectDir("qualitygate/small-changesets/v1-1000-lines"))
.setProperty("sonar.projectKey", project.getKey())
.setDebugLogs(true);
orchestrator.executeBuild(analysis);
assertThat(getMeasure(orchestrator, project.getKey(), "alert_status").getValue()).isEqualTo("OK");
+ assertIgnoredConditions(project, "qualitygate/small-changesets/v1-1000-lines", false);
+ // small leak => ignore coverage warning or error
SonarScanner analysis2 = SonarScanner
.create(projectDir("qualitygate/small-changesets/v2-1019-lines"))
.setProperty("sonar.projectKey", project.getKey())
.setDebugLogs(true);
orchestrator.executeBuild(analysis2);
assertThat(getMeasure(orchestrator, project.getKey(), "alert_status").getValue()).isEqualTo("OK");
+ assertIgnoredConditions(project, "qualitygate/small-changesets/v2-1019-lines", true);
+ // small leak => if coverage is OK anyways, we do not have to ignore anything
+ tester.wsClient().qualityGates().updateCondition(UpdateConditionRequest.builder()
+ .setConditionId(condition.getId())
+ .setMetricKey("new_coverage")
+ .setOperator("LT")
+ .setWarning("10")
+ .setError("20")
+ .setPeriod(1)
+ .build());
+ SonarScanner analysis3 = SonarScanner
+ .create(projectDir("qualitygate/small-changesets/v2-1019-lines"))
+ .setProperty("sonar.projectKey", project.getKey())
+ .setProperty("sonar.organization", organization.getKey())
+ .setProperty("sonar.login", user.getLogin())
+ .setProperty("sonar.password", password)
+ .setProperty("sonar.scm.provider", "xoo")
+ .setProperty("sonar.scm.disabled", "false")
+ .setProperty("sonar.projectDate", "2014-04-02")
+ .setDebugLogs(true);
+ orchestrator.executeBuild(analysis3);
+ assertThat(getMeasure(orchestrator, project.getKey(), "alert_status").getValue()).isEqualTo("OK");
+ assertIgnoredConditions(project, "qualitygate/small-changesets/v2-1019-lines", false);
+
+ // big leak => use usual behaviour
+ tester.wsClient().qualityGates().updateCondition(UpdateConditionRequest.builder()
+ .setConditionId(condition.getId())
+ .setMetricKey("new_coverage")
+ .setOperator("LT")
+ .setWarning(null)
+ .setError("70")
+ .setPeriod(1)
+ .build());
SonarScanner analysis4 = SonarScanner
.create(projectDir("qualitygate/small-changesets/v2-1020-lines"))
.setProperty("sonar.projectKey", project.getKey())
.setProperty("sonar.password", password)
.setProperty("sonar.scm.provider", "xoo")
.setProperty("sonar.scm.disabled", "false")
- .setProperty("sonar.projectDate", "2014-04-02")
+ .setProperty("sonar.projectDate", "2014-04-03")
.setDebugLogs(true);
orchestrator.executeBuild(analysis4);
assertThat(getMeasure(orchestrator, project.getKey(), "alert_status").getValue()).isEqualTo("ERROR");
+ assertIgnoredConditions(project, "qualitygate/small-changesets/v2-1020-lines", false);
+ }
+
+ private void assertIgnoredConditions(Project project, String projectDir, boolean expected) throws IOException {
+ String analysisId = getAnalysisId(getTaskIdInLocalReport(projectDir(projectDir)));
+ boolean ignoredConditions = tester.wsClient().qualityGates()
+ .projectStatus(new ProjectStatusWsRequest().setAnalysisId(analysisId))
+ .getProjectStatus()
+ .getIgnoredConditions();
+ assertThat(ignoredConditions).isEqualTo(expected);
+ }
+
+ private String getAnalysisId(String taskId) throws IOException {
+ WsResponse activity = tester.wsClient()
+ .wsConnector()
+ .call(new GetRequest("api/ce/task")
+ .setParam("id", taskId)
+ .setMediaType(MediaTypes.PROTOBUF));
+ WsCe.TaskResponse activityWsResponse = WsCe.TaskResponse.parseFrom(activity.contentStream());
+ return activityWsResponse.getTask().getAnalysisId();
+ }
+
+ private String getTaskIdInLocalReport(File projectDirectory) throws IOException {
+ File metadata = new File(projectDirectory, ".sonar/report-task.txt");
+ assertThat(metadata).exists().isFile();
+ // verify properties
+ Properties props = new Properties();
+ props.load(new StringReader(FileUtils.readFileToString(metadata, StandardCharsets.UTF_8)));
+ assertThat(props.getProperty("ceTaskId")).isNotEmpty();
+
+ return props.getProperty("ceTaskId");
}
}