3 * Copyright (C) 2009-2024 SonarSource SA
4 * mailto:info AT sonarsource DOT com
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU Lesser General Public
8 * License as published by the Free Software Foundation; either
9 * version 3 of the License, or (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * Lesser General Public License for more details.
16 * You should have received a copy of the GNU Lesser General Public License
17 * along with this program; if not, write to the Free Software Foundation,
18 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20 package org.sonar.ce.task.projectanalysis.step;
22 import com.google.common.collect.ImmutableMap;
23 import com.google.common.collect.Multimap;
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.List;
28 import java.util.Optional;
30 import java.util.function.Function;
31 import java.util.stream.Collectors;
32 import javax.annotation.Nullable;
33 import org.apache.commons.lang.StringUtils;
34 import org.sonar.api.measures.CoreMetrics;
35 import org.sonar.ce.task.projectanalysis.component.Component;
36 import org.sonar.ce.task.projectanalysis.component.CrawlerDepthLimit;
37 import org.sonar.ce.task.projectanalysis.component.DepthTraversalTypeAwareCrawler;
38 import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
39 import org.sonar.ce.task.projectanalysis.component.TypeAwareVisitorAdapter;
40 import org.sonar.ce.task.projectanalysis.measure.Measure;
41 import org.sonar.ce.task.projectanalysis.measure.MeasureRepository;
42 import org.sonar.ce.task.projectanalysis.measure.QualityGateStatus;
43 import org.sonar.ce.task.projectanalysis.measure.qualitygatedetails.EvaluatedCondition;
44 import org.sonar.ce.task.projectanalysis.measure.qualitygatedetails.QualityGateDetailsData;
45 import org.sonar.ce.task.projectanalysis.metric.Metric;
46 import org.sonar.ce.task.projectanalysis.metric.MetricRepository;
47 import org.sonar.ce.task.projectanalysis.qualitygate.Condition;
48 import org.sonar.ce.task.projectanalysis.qualitygate.ConditionEvaluator;
49 import org.sonar.ce.task.projectanalysis.qualitygate.ConditionStatus;
50 import org.sonar.ce.task.projectanalysis.qualitygate.EvaluationResult;
51 import org.sonar.ce.task.projectanalysis.qualitygate.EvaluationResultTextConverter;
52 import org.sonar.ce.task.projectanalysis.qualitygate.MutableQualityGateStatusHolder;
53 import org.sonar.ce.task.projectanalysis.qualitygate.QualityGate;
54 import org.sonar.ce.task.projectanalysis.qualitygate.QualityGateHolder;
55 import org.sonar.ce.task.step.ComputationStep;
56 import org.sonar.core.util.stream.MoreCollectors;
58 import static java.lang.String.format;
59 import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.PRE_ORDER;
60 import static org.sonar.ce.task.projectanalysis.qualitygate.ConditionStatus.NO_VALUE_STATUS;
61 import static org.sonar.ce.task.projectanalysis.qualitygate.ConditionStatus.create;
66 * <li>updates the QualityGateStatus of all the project's measures for the metrics of the conditions of the current
67 * QualityGate (retrieved from {@link QualityGateHolder})</li>
68 * <li>computes the measures on the project for metrics {@link CoreMetrics#QUALITY_GATE_DETAILS_KEY} and
69 * {@link CoreMetrics#ALERT_STATUS_KEY}</li>
73 public class QualityGateMeasuresStep implements ComputationStep {
74 private final TreeRootHolder treeRootHolder;
75 private final QualityGateHolder qualityGateHolder;
76 private final MutableQualityGateStatusHolder qualityGateStatusHolder;
77 private final MeasureRepository measureRepository;
78 private final MetricRepository metricRepository;
79 private final EvaluationResultTextConverter evaluationResultTextConverter;
80 private final SmallChangesetQualityGateSpecialCase smallChangesetQualityGateSpecialCase;
82 public QualityGateMeasuresStep(TreeRootHolder treeRootHolder,
83 QualityGateHolder qualityGateHolder, MutableQualityGateStatusHolder qualityGateStatusHolder,
84 MeasureRepository measureRepository, MetricRepository metricRepository,
85 EvaluationResultTextConverter evaluationResultTextConverter, SmallChangesetQualityGateSpecialCase smallChangesetQualityGateSpecialCase) {
86 this.treeRootHolder = treeRootHolder;
87 this.qualityGateHolder = qualityGateHolder;
88 this.qualityGateStatusHolder = qualityGateStatusHolder;
89 this.evaluationResultTextConverter = evaluationResultTextConverter;
90 this.measureRepository = measureRepository;
91 this.metricRepository = metricRepository;
92 this.smallChangesetQualityGateSpecialCase = smallChangesetQualityGateSpecialCase;
96 public void execute(ComputationStep.Context context) {
97 new DepthTraversalTypeAwareCrawler(
98 new TypeAwareVisitorAdapter(CrawlerDepthLimit.PROJECT, PRE_ORDER) {
100 public void visitProject(Component project) {
101 executeForProject(project);
103 }).visit(treeRootHolder.getRoot());
106 private void executeForProject(Component project) {
107 Optional<QualityGate> qualityGate = qualityGateHolder.getQualityGate();
108 if (qualityGate.isPresent()) {
109 QualityGateDetailsDataBuilder builder = new QualityGateDetailsDataBuilder();
110 updateMeasures(project, qualityGate.get().getConditions(), builder);
112 addProjectMeasure(project, builder);
114 updateQualityGateStatusHolder(qualityGate.get(), builder);
118 private void updateQualityGateStatusHolder(QualityGate qualityGate, QualityGateDetailsDataBuilder builder) {
119 qualityGateStatusHolder.setStatus(convert(builder.getGlobalLevel()), createStatusPerCondition(qualityGate.getConditions(), builder.getEvaluatedConditions()));
122 private static ConditionStatus.EvaluationStatus toEvaluationStatus(Measure.Level globalLevel) {
123 switch (globalLevel) {
125 return ConditionStatus.EvaluationStatus.OK;
127 return ConditionStatus.EvaluationStatus.ERROR;
129 throw new IllegalArgumentException(format(
130 "Unsupported value '%s' of Measure.Level can not be converted to EvaluationStatus",
135 private static org.sonar.ce.task.projectanalysis.qualitygate.QualityGateStatus convert(Measure.Level globalLevel) {
136 switch (globalLevel) {
138 return org.sonar.ce.task.projectanalysis.qualitygate.QualityGateStatus.OK;
140 return org.sonar.ce.task.projectanalysis.qualitygate.QualityGateStatus.ERROR;
142 throw new IllegalArgumentException(format(
143 "Unsupported value '%s' of Measure.Level can not be converted to QualityGateStatus",
148 private static Map<Condition, ConditionStatus> createStatusPerCondition(Collection<Condition> conditions, Collection<EvaluatedCondition> evaluatedConditions) {
149 Map<Condition, EvaluatedCondition> evaluatedConditionPerCondition = evaluatedConditions.stream()
150 .collect(Collectors.toMap(EvaluatedCondition::getCondition, Function.identity()));
152 ImmutableMap.Builder<Condition, ConditionStatus> builder = ImmutableMap.builder();
153 for (Condition condition : conditions) {
154 EvaluatedCondition evaluatedCondition = evaluatedConditionPerCondition.get(condition);
156 if (evaluatedCondition == null) {
157 builder.put(condition, NO_VALUE_STATUS);
159 builder.put(condition, create(toEvaluationStatus(evaluatedCondition.getLevel()), evaluatedCondition.getActualValue()));
162 return builder.build();
165 private void updateMeasures(Component project, Set<Condition> conditions, QualityGateDetailsDataBuilder builder) {
166 Multimap<Metric, Condition> conditionsPerMetric = conditions.stream().collect(MoreCollectors.index(Condition::getMetric, Function.identity()));
167 boolean ignoredConditions = false;
168 for (Map.Entry<Metric, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) {
169 Metric metric = entry.getKey();
170 Optional<Measure> measure = measureRepository.getRawMeasure(project, metric);
171 if (!measure.isPresent()) {
175 final MetricEvaluationResult metricEvaluationResult = evaluateQualityGate(measure.get(), entry.getValue());
176 final MetricEvaluationResult finalMetricEvaluationResult;
177 if (smallChangesetQualityGateSpecialCase.appliesTo(project, metricEvaluationResult)) {
178 finalMetricEvaluationResult = smallChangesetQualityGateSpecialCase.apply(metricEvaluationResult);
179 ignoredConditions = true;
181 finalMetricEvaluationResult = metricEvaluationResult;
183 String text = evaluationResultTextConverter.asText(finalMetricEvaluationResult.condition, finalMetricEvaluationResult.evaluationResult);
184 builder.addLabel(text);
186 Measure updatedMeasure = Measure.updatedMeasureBuilder(measure.get())
187 .setQualityGateStatus(new QualityGateStatus(finalMetricEvaluationResult.evaluationResult.level(), text))
189 measureRepository.update(project, metric, updatedMeasure);
191 builder.addEvaluatedCondition(finalMetricEvaluationResult);
193 builder.setIgnoredConditions(ignoredConditions);
196 private static MetricEvaluationResult evaluateQualityGate(Measure measure, Collection<Condition> conditions) {
197 ConditionEvaluator conditionEvaluator = new ConditionEvaluator();
198 MetricEvaluationResult metricEvaluationResult = null;
199 for (Condition newCondition : conditions) {
200 EvaluationResult newEvaluationResult = conditionEvaluator.evaluate(newCondition, measure);
201 if (metricEvaluationResult == null || newEvaluationResult.level().ordinal() > metricEvaluationResult.evaluationResult.level().ordinal()) {
202 metricEvaluationResult = new MetricEvaluationResult(newEvaluationResult, newCondition);
205 return metricEvaluationResult;
208 private void addProjectMeasure(Component project, QualityGateDetailsDataBuilder builder) {
209 Measure globalMeasure = Measure.newMeasureBuilder()
210 .setQualityGateStatus(new QualityGateStatus(builder.getGlobalLevel(), StringUtils.join(builder.getLabels(), ", ")))
211 .create(builder.getGlobalLevel());
212 Metric metric = metricRepository.getByKey(CoreMetrics.ALERT_STATUS_KEY);
213 measureRepository.add(project, metric, globalMeasure);
215 String detailMeasureValue = new QualityGateDetailsData(builder.getGlobalLevel(), builder.getEvaluatedConditions(), builder.isIgnoredConditions()).toJson();
216 Measure detailsMeasure = Measure.newMeasureBuilder().create(detailMeasureValue);
217 Metric qgDetailsMetric = metricRepository.getByKey(CoreMetrics.QUALITY_GATE_DETAILS_KEY);
218 measureRepository.add(project, qgDetailsMetric, detailsMeasure);
222 public String getDescription() {
223 return "Compute Quality Gate measures";
226 private static final class QualityGateDetailsDataBuilder {
227 private Measure.Level globalLevel = Measure.Level.OK;
228 private List<String> labels = new ArrayList<>();
229 private List<EvaluatedCondition> evaluatedConditions = new ArrayList<>();
230 private boolean ignoredConditions;
232 public Measure.Level getGlobalLevel() {
236 public void addLabel(@Nullable String label) {
237 if (StringUtils.isNotBlank(label)) {
242 public List<String> getLabels() {
246 public void addEvaluatedCondition(MetricEvaluationResult metricEvaluationResult) {
247 Measure.Level level = metricEvaluationResult.evaluationResult.level();
248 if (Measure.Level.ERROR == level) {
249 globalLevel = Measure.Level.ERROR;
251 evaluatedConditions.add(
252 new EvaluatedCondition(metricEvaluationResult.condition, level, metricEvaluationResult.evaluationResult.value()));
255 public List<EvaluatedCondition> getEvaluatedConditions() {
256 return evaluatedConditions;
259 public boolean isIgnoredConditions() {
260 return ignoredConditions;
263 public QualityGateDetailsDataBuilder setIgnoredConditions(boolean ignoredConditions) {
264 this.ignoredConditions = ignoredConditions;
269 static class MetricEvaluationResult {
270 final EvaluationResult evaluationResult;
271 final Condition condition;
273 MetricEvaluationResult(EvaluationResult evaluationResult, Condition condition) {
274 this.evaluationResult = evaluationResult;
275 this.condition = condition;