diff options
8 files changed, 992 insertions, 18 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ConditionUtils.java b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ConditionUtils.java new file mode 100644 index 00000000000..8d5cfc23711 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ConditionUtils.java @@ -0,0 +1,191 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.qualitygate; + +import org.apache.commons.lang.NotImplementedException; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; + +class ConditionUtils { + + private ConditionUtils() { + // only static stuff + } + + /** + * Get the matching alert level for the given measure + */ + static Metric.Level getLevel(ResolvedCondition condition, Measure measure) { + if (evaluateCondition(condition, measure, Metric.Level.ERROR)) { + return Metric.Level.ERROR; + } + if (evaluateCondition(condition, measure, Metric.Level.WARN)) { + return Metric.Level.WARN; + } + return Metric.Level.OK; + } + + private static boolean evaluateCondition(ResolvedCondition condition, Measure measure, Metric.Level alertLevel) { + String valueToEval = getValueToEval(condition, alertLevel); + if (StringUtils.isEmpty(valueToEval)) { + return false; + } + + Comparable criteriaValue = getValueForComparison(condition.metric(), valueToEval); + Comparable measureValue = getMeasureValue(condition, measure); + if (measureValue != null) { + return doesReachThresholds(measureValue, criteriaValue, condition); + } + return false; + } + + private static boolean doesReachThresholds(Comparable measureValue, Comparable criteriaValue, ResolvedCondition condition) { + int comparison = measureValue.compareTo(criteriaValue); + return !(isNotEquals(comparison, condition) + || isGreater(comparison, condition) + || isSmaller(comparison, condition) + || isEquals(comparison, condition)); + } + + private static boolean isNotEquals(int comparison, ResolvedCondition condition) { + return condition.operator().equals("NE") && comparison == 0; + } + + private static boolean isGreater(int comparison, ResolvedCondition condition) { + return condition.operator().equals("GT") && comparison != 1; + } + + private static boolean isSmaller(int comparison, ResolvedCondition condition) { + return condition.operator().equals("LT") && comparison != -1; + } + + private static boolean isEquals(int comparison, ResolvedCondition condition) { + return condition.operator().equals("EQ") && comparison != 0; + } + + private static String getValueToEval(ResolvedCondition condition, Metric.Level alertLevel) { + if (alertLevel.equals(Metric.Level.ERROR)) { + return condition.errorThreshold(); + } else if (alertLevel.equals(Metric.Level.WARN)) { + return condition.warningThreshold(); + } else { + throw new IllegalStateException(alertLevel.toString()); + } + } + + private static Comparable getValueForComparison(Metric metric, String value) { + if (isADouble(metric)) { + return Double.parseDouble(value); + } + if (isAInteger(metric)) { + return parseInteger(value); + } + if (isAString(metric)) { + return value; + } + if (isABoolean(metric)) { + return Integer.parseInt(value); + } + if (isAWorkDuration(metric)) { + return Long.parseLong(value); + } + throw new NotImplementedException(metric.getType().toString()); + } + + private static Comparable<Integer> parseInteger(String value) { + return value.contains(".") ? Integer.parseInt(value.substring(0, value.indexOf('.'))) : Integer.parseInt(value); + } + + private static Comparable getMeasureValue(ResolvedCondition condition, Measure measure) { + Metric metric = condition.metric(); + if (isADouble(metric)) { + return getValue(condition, measure); + } + if (isAInteger(metric)) { + return parseInteger(condition, measure); + } + if (isAWorkDuration(metric)) { + return parseLong(condition, measure); + } + if (condition.period() == null) { + return getMeasureValueForStringOrBoolean(metric, measure); + } + throw new NotImplementedException(metric.getType().toString()); + } + + private static Comparable getMeasureValueForStringOrBoolean(Metric metric, Measure measure) { + if (isAString(metric)) { + return measure.getData(); + } + if (isABoolean(metric)) { + return measure.getValue().intValue(); + } + throw new NotImplementedException(metric.getType().toString()); + } + + private static Comparable<Integer> parseInteger(ResolvedCondition condition, Measure measure) { + Double value = getValue(condition, measure); + return value != null ? value.intValue() : null; + } + + private static Comparable<Long> parseLong(ResolvedCondition condition, Measure measure) { + Double value = getValue(condition, measure); + return value != null ? value.longValue() : null; + } + + private static boolean isADouble(Metric metric) { + return metric.getType() == Metric.ValueType.FLOAT || + metric.getType() == Metric.ValueType.PERCENT || + metric.getType() == Metric.ValueType.RATING; + } + + private static boolean isAInteger(Metric metric) { + return metric.getType() == Metric.ValueType.INT || + metric.getType() == Metric.ValueType.MILLISEC; + } + + private static boolean isAString(Metric metric) { + return metric.getType() == Metric.ValueType.STRING || + metric.getType() == Metric.ValueType.LEVEL; + } + + private static boolean isABoolean(Metric metric) { + return metric.getType() == Metric.ValueType.BOOL; + } + + private static boolean isAWorkDuration(Metric metric) { + return metric.getType() == Metric.ValueType.WORK_DUR; + } + + private static Double getValue(ResolvedCondition condition, Measure measure) { + if (condition.period() == null) { + return measure.getValue(); + } else if (condition.period() == 1) { + return measure.getVariation1(); + } else if (condition.period() == 2) { + return measure.getVariation2(); + } else if (condition.period() == 3) { + return measure.getVariation3(); + } else { + throw new IllegalStateException("Following index period is not allowed : " + Double.toString(condition.period())); + } + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGate.java b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGate.java index 570d7317494..4b469b4276a 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGate.java +++ b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGate.java @@ -19,9 +19,9 @@ */ package org.sonar.batch.qualitygate; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import org.sonar.api.BatchComponent; -import org.sonar.wsclient.qualitygate.QualityGateCondition; import javax.annotation.Nullable; @@ -31,13 +31,17 @@ public class QualityGate implements BatchComponent { private final String name; - private final Collection<QualityGateCondition> conditions; + private final Collection<ResolvedCondition> conditions; QualityGate(@Nullable String name) { this.name = name; this.conditions = Lists.newArrayList(); } + void add(ResolvedCondition condition) { + this.conditions.add(condition); + } + static QualityGate disabled() { return new QualityGate(null); } @@ -46,8 +50,8 @@ public class QualityGate implements BatchComponent { return name; } - public Collection<QualityGateCondition> conditions() { - return conditions; + public Collection<ResolvedCondition> conditions() { + return ImmutableList.copyOf(conditions); } public boolean isEnabled() { diff --git a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateProvider.java b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateProvider.java index 0a7313a37dd..3cfba31d384 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateProvider.java +++ b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateProvider.java @@ -24,10 +24,12 @@ import org.picocontainer.injectors.ProviderAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.api.config.Settings; +import org.sonar.api.measures.MetricFinder; import org.sonar.api.utils.MessageException; import org.sonar.batch.bootstrap.ServerClient; import org.sonar.wsclient.base.HttpException; import org.sonar.wsclient.qualitygate.QualityGateClient; +import org.sonar.wsclient.qualitygate.QualityGateCondition; import org.sonar.wsclient.qualitygate.QualityGateDetails; import java.net.HttpURLConnection; @@ -38,24 +40,24 @@ public class QualityGateProvider extends ProviderAdapter { private static final String PROPERTY_QUALITY_GATE = "sonar.qualitygate"; - public QualityGate provide(Settings settings, ServerClient client) { - return init(settings, client, LOG); + public QualityGate provide(Settings settings, ServerClient client, MetricFinder metricFinder) { + return init(settings, client, metricFinder, LOG); } @VisibleForTesting - QualityGate init(Settings settings, ServerClient client, Logger logger) { + QualityGate init(Settings settings, ServerClient client, MetricFinder metricFinder, Logger logger) { QualityGate result = QualityGate.disabled(); String qualityGateSetting = settings.getString(PROPERTY_QUALITY_GATE); if (qualityGateSetting == null) { logger.info("No quality gate is configured."); } else { - result = load(qualityGateSetting, client.wsClient().qualityGateClient()); + result = load(qualityGateSetting, client.wsClient().qualityGateClient(), metricFinder); } logger.info("Loaded quality gate '{}'", result.name()); return result; } - private QualityGate load(String qualityGateSetting, QualityGateClient qualityGateClient) { + private QualityGate load(String qualityGateSetting, QualityGateClient qualityGateClient, MetricFinder metricFinder) { QualityGateDetails definitionFromServer = null; try { definitionFromServer = fetch(qualityGateSetting, qualityGateClient); @@ -69,6 +71,10 @@ public class QualityGateProvider extends ProviderAdapter { QualityGate configuredGate = new QualityGate(definitionFromServer.name()); + for (QualityGateCondition condition: definitionFromServer.conditions()) { + configuredGate.add(new ResolvedCondition(condition, metricFinder.findByKey(condition.metricKey()))); + } + return configuredGate; } diff --git a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateVerifier.java b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateVerifier.java index 32b266e97a2..18b6a364ecd 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateVerifier.java +++ b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateVerifier.java @@ -19,20 +19,45 @@ */ package org.sonar.batch.qualitygate; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import org.apache.commons.lang.StringUtils; import org.sonar.api.batch.*; +import org.sonar.api.database.model.Snapshot; +import org.sonar.api.i18n.I18n; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; import org.sonar.api.measures.Metric; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; import org.sonar.api.resources.ResourceUtils; +import org.sonar.api.utils.Duration; +import org.sonar.api.utils.Durations; +import org.sonar.core.timemachine.Periods; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Set; public class QualityGateVerifier implements Decorator { + private static final String VARIATION_METRIC_PREFIX = "new_"; + private static final String VARIATION = "variation"; + private QualityGate qualityGate; - public QualityGateVerifier(QualityGate qualityGate) { + private Snapshot snapshot; + private Periods periods; + private I18n i18n; + private Durations durations; + + public QualityGateVerifier(QualityGate qualityGate, Snapshot snapshot, Periods periods, I18n i18n, Durations durations) { this.qualityGate = qualityGate; + this.snapshot = snapshot; + this.periods = periods; + this.i18n = i18n; + this.durations = durations; } @DependedUpon @@ -45,6 +70,15 @@ public class QualityGateVerifier implements Decorator { return DecoratorBarriers.END_OF_TIME_MACHINE; } + @DependsUpon + public Collection<Metric> dependsUponMetrics() { + Set<Metric> metrics = Sets.newHashSet(); + for (ResolvedCondition condition: qualityGate.conditions()) { + metrics.add(condition.metric()); + } + return metrics; + } + @Override public boolean shouldExecuteOnProject(Project project) { return qualityGate.isEnabled(); @@ -59,12 +93,79 @@ public class QualityGateVerifier implements Decorator { private void checkProjectConditions(DecoratorContext context) { Metric.Level globalLevel = Metric.Level.OK; + List<String> labels = Lists.newArrayList(); + + for (ResolvedCondition condition: qualityGate.conditions()) { + Measure measure = context.getMeasure(condition.metric()); + if (measure != null) { + Metric.Level level = ConditionUtils.getLevel(condition, measure); + + /* + * This should probably be done only after migration from alerts + */ + //measure.setAlertStatus(level); + String text = getText(condition, level); + if (!StringUtils.isBlank(text)) { + //measure.setAlertText(text); + labels.add(text); + } + + //context.saveMeasure(measure); + + if (Metric.Level.WARN == level && globalLevel != Metric.Level.ERROR) { + globalLevel = Metric.Level.WARN; + + } else if (Metric.Level.ERROR == level) { + globalLevel = Metric.Level.ERROR; + } + } + } + Measure globalMeasure = new Measure(CoreMetrics.QUALITY_GATE_STATUS, globalLevel); globalMeasure.setAlertStatus(globalLevel); - globalMeasure.setAlertText(""); + globalMeasure.setAlertText(StringUtils.join(labels, ", ")); context.saveMeasure(globalMeasure); } + private String getText(ResolvedCondition condition, Metric.Level level) { + if (level == Metric.Level.OK) { + return null; + } + return getAlertLabel(condition, level); + } + + private String getAlertLabel(ResolvedCondition condition, Metric.Level level) { + Integer alertPeriod = condition.period(); + String metric = i18n.message(Locale.ENGLISH, "metric." + condition.metricKey() + ".name", condition.metric().getName()); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(metric); + + if (alertPeriod != null && !condition.metricKey().startsWith(VARIATION_METRIC_PREFIX)) { + String variation = i18n.message(Locale.ENGLISH, VARIATION, VARIATION).toLowerCase(); + stringBuilder.append(" ").append(variation); + } + + stringBuilder + .append(" ").append(condition.operator()).append(" ") + .append(alertValue(condition, level)); + + if (alertPeriod != null) { + stringBuilder.append(" ").append(periods.label(snapshot, alertPeriod)); + } + + return stringBuilder.toString(); + } + + private String alertValue(ResolvedCondition condition, Metric.Level level) { + String value = level.equals(Metric.Level.ERROR) ? condition.errorThreshold() : condition.warningThreshold(); + if (condition.metric().getType().equals(Metric.ValueType.WORK_DUR)) { + return durations.format(Locale.ENGLISH, Duration.create(Long.parseLong(value)), Durations.DurationFormat.SHORT); + } else { + return value; + } + } + @Override public String toString() { return getClass().getSimpleName(); diff --git a/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ResolvedCondition.java b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ResolvedCondition.java new file mode 100644 index 00000000000..9cf9ecf97e1 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/qualitygate/ResolvedCondition.java @@ -0,0 +1,69 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.qualitygate; + +import org.sonar.api.measures.Metric; +import org.sonar.wsclient.qualitygate.QualityGateCondition; + +public class ResolvedCondition implements QualityGateCondition { + + private QualityGateCondition wrapped; + + private Metric metric; + + public ResolvedCondition(QualityGateCondition condition, Metric metric) { + this.wrapped = condition; + this.metric = metric; + } + + public Metric metric() { + return metric; + } + + @Override + public Long id() { + return wrapped.id(); + } + + @Override + public String metricKey() { + return wrapped.metricKey(); + } + + @Override + public String operator() { + return wrapped.operator(); + } + + @Override + public String warningThreshold() { + return wrapped.warningThreshold(); + } + + @Override + public String errorThreshold() { + return wrapped.errorThreshold(); + } + + @Override + public Integer period() { + return wrapped.period(); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/qualitygate/ConditionUtilsTest.java b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/ConditionUtilsTest.java new file mode 100644 index 00000000000..94239a1963e --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/ConditionUtilsTest.java @@ -0,0 +1,262 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2013 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube 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. + * + * SonarQube 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.batch.qualitygate; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.core.qualitygate.db.QualityGateConditionDto; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ConditionUtilsTest { + + private Metric metric; + private Measure measure; + private ResolvedCondition condition; + + @Before + public void setup() { + metric = new Metric.Builder("test-metric", "name", Metric.ValueType.FLOAT).create(); + measure = new Measure(); + measure.setMetric(metric); + condition = mock(ResolvedCondition.class); + when(condition.period()).thenReturn(null); + } + + @Test + public void testInputNumbers() { + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_LESS_THAN); + when(condition.metric()).thenReturn(metric); + + try { + metric.setType(Metric.ValueType.FLOAT); + when(condition.errorThreshold()).thenReturn("20"); + ConditionUtils.getLevel(condition, measure); + } catch (NumberFormatException ex) { + Assert.fail(); + } + + try { + metric.setType(Metric.ValueType.INT); + when(condition.errorThreshold()).thenReturn("20.1"); + ConditionUtils.getLevel(condition, measure); + } catch (NumberFormatException ex) { + Assert.fail(); + } + + try { + metric.setType(Metric.ValueType.PERCENT); + when(condition.errorThreshold()).thenReturn("20.1"); + ConditionUtils.getLevel(condition, measure); + } catch (NumberFormatException ex) { + Assert.fail(); + } + } + + @Test + public void testEquals() { + + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.1"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + metric.setType(Metric.ValueType.STRING); + measure.setData("TEST"); + measure.setValue(null); + + when(condition.errorThreshold()).thenReturn("TEST"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("TEST2"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + } + + @Test + public void testNotEquals() { + + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_NOT_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.1"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + metric.setType(Metric.ValueType.STRING); + measure.setData("TEST"); + measure.setValue(null); + + when(condition.errorThreshold()).thenReturn("TEST"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("TEST2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + } + + @Test + public void testGreater() { + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_GREATER_THAN); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.1"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.3"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testSmaller() { + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_LESS_THAN); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.1"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.3"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testPercent() { + metric.setType(Metric.ValueType.PERCENT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testFloat() { + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testInteger() { + metric.setType(Metric.ValueType.INT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testLevel() { + metric.setType(Metric.ValueType.LEVEL); + measure.setData(Metric.Level.ERROR.toString()); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn(Metric.Level.ERROR.toString()); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn(Metric.Level.OK.toString()); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_NOT_EQUALS); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testBooleans() { + metric.setType(Metric.ValueType.BOOL); + measure.setValue(0d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("1"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("0"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_NOT_EQUALS); + when(condition.errorThreshold()).thenReturn("1"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("0"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void test_work_duration() { + metric.setType(Metric.ValueType.WORK_DUR); + measure.setValue(60.0d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("60"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + } + + @Test + public void testErrorAndWarningLevel() { + metric.setType(Metric.ValueType.FLOAT); + measure.setValue(10.2d); + when(condition.operator()).thenReturn(QualityGateConditionDto.OPERATOR_EQUALS); + when(condition.metric()).thenReturn(metric); + + when(condition.errorThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.ERROR, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.1"); + Assert.assertEquals(Metric.Level.OK, ConditionUtils.getLevel(condition, measure)); + + when(condition.errorThreshold()).thenReturn("10.3"); + when(condition.warningThreshold()).thenReturn("10.2"); + Assert.assertEquals(Metric.Level.WARN, ConditionUtils.getLevel(condition, measure)); + } +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateProviderTest.java index 746c47f3a58..46729866736 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateProviderTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateProviderTest.java @@ -19,6 +19,7 @@ */ package org.sonar.batch.qualitygate; +import com.google.common.collect.ImmutableList; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,14 +27,17 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.slf4j.Logger; import org.sonar.api.config.Settings; +import org.sonar.api.measures.MetricFinder; import org.sonar.api.utils.MessageException; import org.sonar.batch.bootstrap.ServerClient; import org.sonar.wsclient.SonarClient; import org.sonar.wsclient.base.HttpException; import org.sonar.wsclient.qualitygate.QualityGateClient; +import org.sonar.wsclient.qualitygate.QualityGateCondition; import org.sonar.wsclient.qualitygate.QualityGateDetails; import java.net.HttpURLConnection; +import java.util.Collection; import static org.fest.assertions.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -50,6 +54,9 @@ public class QualityGateProviderTest { private ServerClient client; @Mock + private MetricFinder metricFinder; + + @Mock private QualityGateClient qualityGateClient; @Mock @@ -64,8 +71,8 @@ public class QualityGateProviderTest { @Test public void should_load_empty_quality_gate_from_default_settings() { - assertThat(new QualityGateProvider().provide(settings, client).conditions()).isEmpty(); - assertThat(new QualityGateProvider().init(settings, client, logger).isEnabled()).isFalse(); + assertThat(new QualityGateProvider().provide(settings, client, metricFinder).conditions()).isEmpty(); + assertThat(new QualityGateProvider().init(settings, client, metricFinder, logger).isEnabled()).isFalse(); verify(logger).info("No quality gate is configured."); } @@ -76,7 +83,7 @@ public class QualityGateProviderTest { QualityGateDetails qGate = mock(QualityGateDetails.class); when(qualityGateClient.show(qGateName)).thenReturn(qGate); when(qGate.name()).thenReturn(qGateName); - QualityGate actualGate = new QualityGateProvider().init(settings, client, logger); + QualityGate actualGate = new QualityGateProvider().init(settings, client, metricFinder, logger); assertThat(actualGate.name()).isEqualTo(qGateName); assertThat(actualGate.isEnabled()).isTrue(); verify(logger).info("Loaded quality gate '{}'", qGateName); @@ -90,8 +97,18 @@ public class QualityGateProviderTest { QualityGateDetails qGate = mock(QualityGateDetails.class); when(qualityGateClient.show(qGateId)).thenReturn(qGate); when(qGate.name()).thenReturn(qGateName); - assertThat(new QualityGateProvider().init(settings, client, logger).name()).isEqualTo(qGateName); + String metricKey1 = "metric1"; + QualityGateCondition serverCondition1 = mock(QualityGateCondition.class); + when(serverCondition1.metricKey()).thenReturn(metricKey1); + String metricKey2 = "metric2"; + QualityGateCondition serverCondition2 = mock(QualityGateCondition.class); + when(serverCondition2.metricKey()).thenReturn(metricKey2); + Collection<QualityGateCondition> conditions = ImmutableList.of(serverCondition1, serverCondition2); + when(qGate.conditions()).thenReturn(conditions); + assertThat(new QualityGateProvider().init(settings, client, metricFinder, logger).name()).isEqualTo(qGateName); verify(logger).info("Loaded quality gate '{}'", qGateName); + verify(metricFinder).findByKey(metricKey1); + verify(metricFinder).findByKey(metricKey2); } @Test(expected = MessageException.class) @@ -99,7 +116,7 @@ public class QualityGateProviderTest { String qGateName = "Sonar way"; when(settings.getString("sonar.qualitygate")).thenReturn(qGateName); when(qualityGateClient.show(qGateName)).thenThrow(new HttpException("http://server/api/qualitygates/show?name=Sonar%20way", HttpURLConnection.HTTP_NOT_FOUND)); - new QualityGateProvider().provide(settings, client); + new QualityGateProvider().provide(settings, client, metricFinder); } @Test(expected = HttpException.class) @@ -107,7 +124,7 @@ public class QualityGateProviderTest { String qGateName = "Sonar way"; when(settings.getString("sonar.qualitygate")).thenReturn(qGateName); when(qualityGateClient.show(qGateName)).thenThrow(new HttpException("http://server/api/qualitygates/show?name=Sonar%20way", HttpURLConnection.HTTP_NOT_ACCEPTABLE)); - new QualityGateProvider().provide(settings, client); + new QualityGateProvider().provide(settings, client, metricFinder); } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateVerifierTest.java b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateVerifierTest.java index 4a56c238971..53977f6b9f7 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateVerifierTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateVerifierTest.java @@ -19,24 +19,39 @@ */ package org.sonar.batch.qualitygate; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import org.apache.commons.lang.NotImplementedException; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentMatcher; import org.sonar.api.batch.DecoratorBarriers; import org.sonar.api.batch.DecoratorContext; import org.sonar.api.database.model.Snapshot; import org.sonar.api.i18n.I18n; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.File; import org.sonar.api.resources.Project; import org.sonar.api.resources.Resource; +import org.sonar.api.test.IsMeasure; +import org.sonar.api.utils.Duration; +import org.sonar.api.utils.Durations; +import org.sonar.core.qualitygate.db.QualityGateConditionDto; import org.sonar.core.timemachine.Periods; +import java.util.ArrayList; import java.util.Locale; import static org.fest.assertions.Assertions.assertThat; import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.argThat; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; public class QualityGateVerifierTest { @@ -52,6 +67,7 @@ public class QualityGateVerifierTest { Snapshot snapshot; Periods periods; I18n i18n; + Durations durations; @Before public void before() { @@ -59,6 +75,7 @@ public class QualityGateVerifierTest { periods = mock(Periods.class); i18n = mock(I18n.class); when(i18n.message(any(Locale.class), eq("variation"), eq("variation"))).thenReturn("variation"); + durations = mock(Durations.class); measureClasses = new Measure(CoreMetrics.CLASSES, 20d); measureCoverage = new Measure(CoreMetrics.COVERAGE, 35d); @@ -71,7 +88,7 @@ public class QualityGateVerifierTest { snapshot = mock(Snapshot.class); qualityGate = mock(QualityGate.class); when(qualityGate.isEnabled()).thenReturn(true); - verifier = new QualityGateVerifier(qualityGate); + verifier = new QualityGateVerifier(qualityGate, snapshot, periods, i18n, durations); project = new Project("foo"); } @@ -97,4 +114,311 @@ public class QualityGateVerifierTest { assertThat(verifier.dependsOnVariations()).isEqualTo(DecoratorBarriers.END_OF_TIME_MACHINE); } + @Test + public void depends_upon_metrics() { + when(qualityGate.conditions()).thenReturn(ImmutableList.of(new ResolvedCondition(null, CoreMetrics.CLASSES))); + assertThat(verifier.dependsUponMetrics()).containsOnly(CoreMetrics.CLASSES); + } + + @Test + public void ok_when_no_alerts() { + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "20"), + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "35.0")); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.OK.toString()))); + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.OK))); + //verify(context).saveMeasure(argThat(hasLevel(measureCoverage, Metric.Level.OK))); + } + + @Test + public void check_root_modules_only() { + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "20"), + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "35.0")); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(File.create("src/Foo.php"), context); + + verify(context, never()).saveMeasure(any(Measure.class)); + } + + @Test + public void generate_warnings() { + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "100"), + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_LESS_THAN, null, "95.0")); // generates warning because coverage 35% < 95% + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.WARN, null))); + + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.OK))); + //verify(context).saveMeasure(argThat(hasLevel(measureCoverage, Metric.Level.WARN))); + + } + + @Test + public void globalStatusShouldBeErrorIfWarningsAndErrors() { + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_LESS_THAN, null, "100"), // generates warning because classes 20 < 100 + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_LESS_THAN, "50.0", "80.0")); // generates error because coverage 35% < 50% + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.ERROR, null))); + + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.WARN))); + //verify(context).saveMeasure(argThat(hasLevel(measureCoverage, Metric.Level.ERROR))); + } + + @Test + public void globalLabelShouldAggregateAllLabels() { + when(i18n.message(any(Locale.class), eq("metric.classes.name"), anyString())).thenReturn("Classes"); + when(i18n.message(any(Locale.class), eq("metric.coverage.name"), anyString())).thenReturn("Coverages"); + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_LESS_THAN, null, "10000"), // there are 20 classes, error threshold is higher => alert + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_LESS_THAN, "50.0", "80.0"));// coverage is 35%, warning threshold is higher => alert + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.ERROR, "Classes LT 10000, Coverages LT 50.0"))); + } + + @Test + public void alertLabelUsesL10nMetricName() { + Metric metric = new Metric.Builder("rating", "Rating", Metric.ValueType.INT).create(); + + // metric name is declared in l10n bundle + when(i18n.message(any(Locale.class), eq("metric.rating.name"), anyString())).thenReturn("THE RATING"); + + when(context.getMeasure(metric)).thenReturn(new Measure(metric, 4d)); + ArrayList<ResolvedCondition> conditions = Lists.newArrayList(mockCondition(metric, QualityGateConditionDto.OPERATOR_LESS_THAN, "10", null)); + when(qualityGate.conditions()).thenReturn(conditions); + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.ERROR, "THE RATING LT 10"))); + } + + @Test + public void alertLabelUsesMetricNameIfMissingL10nBundle() { + // the third argument is Metric#getName() + when(i18n.message(any(Locale.class), eq("metric.classes.name"), eq("Classes"))).thenReturn("Classes"); + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + // there are 20 classes, error threshold is higher => alert + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_LESS_THAN, "10000", null) + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.ERROR, "Classes LT 10000"))); + } + + @Test + public void shouldBeOkIfPeriodVariationIsEnough() { + measureClasses.setVariation1(0d); + measureCoverage.setVariation2(50d); + measureComplexity.setVariation3(2d); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "10", 1), // ok because no variation + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_LESS_THAN, null, "40.0", 2), // ok because coverage increases of 50%, which is more + // than 40% + mockCondition(CoreMetrics.COMPLEXITY, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "5", 3) // ok because complexity increases of 2, which is less + // than 5 + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.OK, null))); + + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.OK))); + //verify(context).saveMeasure(argThat(hasLevel(measureCoverage, Metric.Level.OK))); + //verify(context).saveMeasure(argThat(hasLevel(measureComplexity, Metric.Level.OK))); + } + + @Test + public void shouldGenerateWarningIfPeriodVariationIsNotEnough() { + measureClasses.setVariation1(40d); + measureCoverage.setVariation2(5d); + measureComplexity.setVariation3(70d); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "30", 1), // generates warning because classes increases of 40, + // which is greater than 30 + mockCondition(CoreMetrics.COVERAGE, QualityGateConditionDto.OPERATOR_LESS_THAN, null, "10.0", 2), // generates warning because coverage increases of 5%, + // which is smaller than 10% + mockCondition(CoreMetrics.COMPLEXITY, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "60", 3) // generates warning because complexity increases of + // 70, which is smaller than 60 + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.WARN, null))); + + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.WARN))); + //verify(context).saveMeasure(argThat(hasLevel(measureCoverage, Metric.Level.WARN))); + //verify(context).saveMeasure(argThat(hasLevel(measureComplexity, Metric.Level.WARN))); + } + + @Test + public void shouldBeOkIfVariationIsNull() { + measureClasses.setVariation1(null); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "10", 1)); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.OK, null))); + //verify(context).saveMeasure(argThat(hasLevel(measureClasses, Metric.Level.OK))); + } + + @Test + public void shouldVariationPeriodValueCouldBeUsedForRatingMetric() { + Metric ratingMetric = new Metric.Builder("key_rating_metric", "Rating metric", Metric.ValueType.RATING).create(); + Measure measureRatingMetric = new Measure(ratingMetric, 150d); + measureRatingMetric.setVariation1(50d); + when(context.getMeasure(ratingMetric)).thenReturn(measureRatingMetric); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(ratingMetric, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "100", 1) + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.OK, null))); + //verify(context).saveMeasure(argThat(hasLevel(measureRatingMetric, Metric.Level.OK))); + } + + @Test(expected = IllegalStateException.class) + public void shouldAllowOnlyVariationPeriodOneGlobalPeriods() { + measureClasses.setVariation4(40d); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "30", 4) + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + } + + @Test(expected = NotImplementedException.class) + public void shouldNotAllowPeriodVariationAlertOnStringMetric() { + Measure measure = new Measure(CoreMetrics.SCM_AUTHORS_BY_LINE, 100d); + measure.setVariation1(50d); + when(context.getMeasure(CoreMetrics.SCM_AUTHORS_BY_LINE)).thenReturn(measure); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.SCM_AUTHORS_BY_LINE, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "30", 1) + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + } + + @Test + public void shouldLabelAlertContainsPeriod() { + measureClasses.setVariation1(40d); + + when(i18n.message(any(Locale.class), eq("metric.classes.name"), anyString())).thenReturn("Classes"); + when(periods.label(snapshot, 1)).thenReturn("since someday"); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(CoreMetrics.CLASSES, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "30", 1) // generates warning because classes increases of 40, + // which is greater than 30 + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.WARN, "Classes variation GT 30 since someday"))); + } + + @Test + public void shouldLabelAlertForNewMetricDoNotContainsVariationWord() { + Metric newMetric = new Metric.Builder("new_metric_key", "New Metric", Metric.ValueType.INT).create(); + Measure measure = new Measure(newMetric, 15d); + measure.setVariation1(50d); + when(context.getMeasure(newMetric)).thenReturn(measure); + measureClasses.setVariation1(40d); + + when(i18n.message(any(Locale.class), eq("metric.new_metric_key.name"), anyString())).thenReturn("New Measure"); + when(periods.label(snapshot, 1)).thenReturn("since someday"); + + ArrayList<ResolvedCondition> conditions = Lists.newArrayList( + mockCondition(newMetric, QualityGateConditionDto.OPERATOR_GREATER_THAN, null, "30", 1) // generates warning because classes increases of 40, which is + // greater than 30 + ); + when(qualityGate.conditions()).thenReturn(conditions); + + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.WARN, "New Measure GT 30 since someday"))); + } + + @Test + public void alert_on_work_duration() { + Metric metric = new Metric.Builder("tech_debt", "Debt", Metric.ValueType.WORK_DUR).create(); + + // metric name is declared in l10n bundle + when(i18n.message(any(Locale.class), eq("metric.tech_debt.name"), anyString())).thenReturn("The Debt"); + when(durations.format(any(Locale.class), eq(Duration.create(3600L)), eq(Durations.DurationFormat.SHORT))).thenReturn("1h"); + + when(context.getMeasure(metric)).thenReturn(new Measure(metric, 1800d)); + ArrayList<ResolvedCondition> conditions = Lists.newArrayList(mockCondition(metric, QualityGateConditionDto.OPERATOR_LESS_THAN, "3600", null)); + when(qualityGate.conditions()).thenReturn(conditions); + verifier.decorate(project, context); + + verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.QUALITY_GATE_STATUS, Metric.Level.ERROR, "The Debt LT 1h"))); + } + + private ArgumentMatcher<Measure> matchesMetric(final Metric metric, final Metric.Level alertStatus, final String alertText) { + return new ArgumentMatcher<Measure>() { + @Override + public boolean matches(Object arg) { + boolean result = ((Measure) arg).getMetric().equals(metric) && ((Measure) arg).getAlertStatus() == alertStatus; + if (result && alertText != null) { + result = alertText.equals(((Measure) arg).getAlertText()); + } + return result; + } + }; + } + + private ArgumentMatcher<Measure> hasLevel(final Measure measure, final Metric.Level alertStatus) { + return new ArgumentMatcher<Measure>() { + @Override + public boolean matches(Object arg) { + return arg == measure && ((Measure) arg).getAlertStatus().equals(alertStatus); + } + }; + } + + private ResolvedCondition mockCondition(Metric metric, String operator, String error, String warning) { + return mockCondition(metric, operator, error, warning, null); + } + + private ResolvedCondition mockCondition(Metric metric, String operator, String error, String warning, Integer period) { + ResolvedCondition cond = mock(ResolvedCondition.class); + when(cond.metric()).thenReturn(metric); + when(cond.metricKey()).thenReturn(metric.getKey()); + when(cond.operator()).thenReturn(operator); + when(cond.warningThreshold()).thenReturn(warning); + when(cond.errorThreshold()).thenReturn(error); + when(cond.period()).thenReturn(period); + return cond; + } + } |