]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-5094 Use conditions from server to generate quality gate status measure
authorJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Thu, 6 Mar 2014 16:19:27 +0000 (17:19 +0100)
committerJean-Baptiste Lievremont <jean-baptiste.lievremont@sonarsource.com>
Fri, 7 Mar 2014 07:42:46 +0000 (08:42 +0100)
sonar-batch/src/main/java/org/sonar/batch/qualitygate/ConditionUtils.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGate.java
sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateProvider.java
sonar-batch/src/main/java/org/sonar/batch/qualitygate/QualityGateVerifier.java
sonar-batch/src/main/java/org/sonar/batch/qualitygate/ResolvedCondition.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/qualitygate/ConditionUtilsTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateProviderTest.java
sonar-batch/src/test/java/org/sonar/batch/qualitygate/QualityGateVerifierTest.java

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 (file)
index 0000000..8d5cfc2
--- /dev/null
@@ -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()));
+    }
+  }
+}
index 570d7317494297d8a7c7de04412b628ab2dad5fa..4b469b4276a8888920bbd8b1450ee808770ab590 100644 (file)
@@ -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() {
index 0a7313a37dd97a20fae00e2ea62eff8cc03c0b42..3cfba31d384855c5b976a767b114ca3276991957 100644 (file)
@@ -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;
   }
 
index 32b266e97a2072e7c8b1bafe95f4dfb261985445..18b6a364ecd5a284506417738a002993d51ca9ac 100644 (file)
  */
 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 (file)
index 0000000..9cf9ecf
--- /dev/null
@@ -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 (file)
index 0000000..94239a1
--- /dev/null
@@ -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));
+  }
+}
index 746c47f3a58d523e1ccb5098e53d21d4e8af2b3f..467298667360a3a0f55309c73d0167e22d92fe81 100644 (file)
@@ -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;
@@ -49,6 +53,9 @@ public class QualityGateProviderTest {
   @Mock
   private ServerClient client;
 
+  @Mock
+  private MetricFinder metricFinder;
+
   @Mock
   private QualityGateClient qualityGateClient;
 
@@ -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);
   }
 
 }
index 4a56c238971e1f9001a76acc229fd8d0f97bf2b5..53977f6b9f74ad0ecc2a7b02cb5289f463e18dbf 100644 (file)
  */
 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;
+  }
+
 }