]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-1352 add server side management of differential measure alerts
authorJulien Lancelot <julien.lancelot@gmail.com>
Wed, 28 Nov 2012 14:37:51 +0000 (15:37 +0100)
committerJulien Lancelot <julien.lancelot@gmail.com>
Wed, 28 Nov 2012 14:38:06 +0000 (15:38 +0100)
14 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/AlertUtils.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/CheckAlertThresholds.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/Periods.java [new file with mode: 0644]
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/TimeMachineConfigurationPersister.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/CheckAlertThresholdsTest.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/PeriodsTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java
sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql
sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl
sonar-plugin-api/src/main/java/org/sonar/api/profiles/Alert.java
sonar-server/src/main/webapp/WEB-INF/db/migrate/359_add_period_to_alerts.rb [new file with mode: 0644]
sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml
sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml

index 0592005e12ec2fdbe07ab63848fe00889c72397b..3fd67c43ea97a9927ab3fe79cc6bc5dcf0ac341c 100644 (file)
@@ -79,6 +79,7 @@ import org.sonar.plugins.core.timemachine.NewCoverageFileAnalyzer;
 import org.sonar.plugins.core.timemachine.NewItCoverageFileAnalyzer;
 import org.sonar.plugins.core.timemachine.NewOverallCoverageFileAnalyzer;
 import org.sonar.plugins.core.timemachine.NewViolationsDecorator;
+import org.sonar.plugins.core.timemachine.Periods;
 import org.sonar.plugins.core.timemachine.ReferenceAnalysis;
 import org.sonar.plugins.core.timemachine.TendencyDecorator;
 import org.sonar.plugins.core.timemachine.TimeMachineConfigurationPersister;
@@ -395,6 +396,7 @@ public final class CorePlugin extends SonarPlugin {
       UserManagedMetrics.class,
       ProjectFileSystemLogger.class,
       DatabaseSemaphoreImpl.class,
+      Periods.class,
 
       // maven
       MavenInitializer.class,
index 9fe246f5fdf53787b6d12b1e34da5b9e08d6b06f..cec2e22be177385be0bd5704af40d1fcb670db51 100644 (file)
@@ -26,6 +26,7 @@ import org.sonar.api.measures.Metric;
 import org.sonar.api.profiles.Alert;
 
 public final class AlertUtils {
+
   private AlertUtils() {
   }
 
@@ -43,73 +44,87 @@ public final class AlertUtils {
   }
 
   private static boolean evaluateAlert(Alert alert, Measure measure, Metric.Level alertLevel) {
-    String valueToEval;
-    if (alertLevel.equals(Metric.Level.ERROR)) {
-      valueToEval = alert.getValueError();
-
-    } else if (alertLevel.equals(Metric.Level.WARN)) {
-      valueToEval = alert.getValueWarning();
-
-    } else {
-      throw new IllegalStateException(alertLevel.toString());
-    }
-
+    String valueToEval = getValueToEval(alert, alertLevel);
     if (StringUtils.isEmpty(valueToEval)) {
       return false;
     }
 
     Comparable criteriaValue = getValueForComparison(alert.getMetric(), valueToEval);
-    Comparable metricValue = getMeasureValue(alert.getMetric(), measure);
+    Comparable metricValue = getMeasureValue(alert, measure);
 
     int comparison = metricValue.compareTo(criteriaValue);
     return !(// NOSONAR complexity of this boolean expression is under control
-    (alert.isNotEqualsOperator() && comparison == 0)
-      || (alert.isGreaterOperator() && comparison != 1)
-      || (alert.isSmallerOperator() && comparison != -1)
-      || (alert.isEqualsOperator() && comparison != 0));
+        (alert.isNotEqualsOperator() && comparison == 0)
+            || (alert.isGreaterOperator() && comparison != 1)
+            || (alert.isSmallerOperator() && comparison != -1)
+            || (alert.isEqualsOperator() && comparison != 0));
+  }
+
+  private static String getValueToEval(Alert alert, Metric.Level alertLevel) {
+    if (alertLevel.equals(Metric.Level.ERROR)) {
+      return alert.getValueError();
+    } else if (alertLevel.equals(Metric.Level.WARN)) {
+      return alert.getValueWarning();
+    } else {
+      throw new IllegalStateException(alertLevel.toString());
+    }
   }
 
   private static Comparable<?> getValueForComparison(Metric metric, String value) {
     if (metric.getType() == Metric.ValueType.FLOAT ||
-      metric.getType() == Metric.ValueType.PERCENT) {
+        metric.getType() == Metric.ValueType.PERCENT ||
+        metric.getType() == Metric.ValueType.RATING
+        ) {
       return Double.parseDouble(value);
     }
     if (metric.getType() == Metric.ValueType.INT ||
-      metric.getType() == Metric.ValueType.MILLISEC) {
+        metric.getType() == Metric.ValueType.MILLISEC) {
       return value.contains(".") ? Integer.parseInt(value.substring(0, value.indexOf('.'))) : Integer.parseInt(value);
     }
     if (metric.getType() == Metric.ValueType.STRING ||
-      metric.getType() == Metric.ValueType.LEVEL) {
+        metric.getType() == Metric.ValueType.LEVEL) {
       return value;
     }
     if (metric.getType() == Metric.ValueType.BOOL) {
       return Integer.parseInt(value);
     }
-    if (metric.getType() == Metric.ValueType.RATING) {
-      return Double.parseDouble(value);
-    }
     throw new NotImplementedException(metric.getType().toString());
   }
 
-  private static Comparable<?> getMeasureValue(Metric metric, Measure measure) {
+  private static Comparable<?> getMeasureValue(Alert alert, Measure measure) {
+    Metric metric = alert.getMetric();
     if (metric.getType() == Metric.ValueType.FLOAT ||
-      metric.getType() == Metric.ValueType.PERCENT) {
-      return measure.getValue();
+        metric.getType() == Metric.ValueType.PERCENT ||
+        metric.getType() == Metric.ValueType.RATING) {
+      return getValue(alert, measure);
     }
     if (metric.getType() == Metric.ValueType.INT ||
-      metric.getType() == Metric.ValueType.MILLISEC) {
-      return measure.getValue().intValue();
+        metric.getType() == Metric.ValueType.MILLISEC) {
+      return getValue(alert, measure).intValue();
     }
-    if (metric.getType() == Metric.ValueType.STRING ||
-      metric.getType() == Metric.ValueType.LEVEL) {
-      return measure.getData();
+    if (alert.getPeriod() == null) {
+      if (metric.getType() == Metric.ValueType.STRING ||
+          metric.getType() == Metric.ValueType.LEVEL) {
+        return measure.getData();
+      }
+      if (metric.getType() == Metric.ValueType.BOOL) {
+        return measure.getValue().intValue();
+      }
     }
-    if (metric.getType() == Metric.ValueType.BOOL) {
-      return measure.getValue().intValue();
-    }
-    if (metric.getType() == Metric.ValueType.RATING) {
+    throw new NotImplementedException(metric.getType().toString());
+  }
+
+  private static Double getValue(Alert alert, Measure measure) {
+    if (alert.getPeriod() == null) {
       return measure.getValue();
+    } else if (alert.getPeriod() == 1) {
+      return measure.getVariation1();
+    } else if (alert.getPeriod() == 2) {
+      return measure.getVariation2();
+    } else if (alert.getPeriod() == 3) {
+      return measure.getVariation3();
+    } else {
+      throw new IllegalStateException("Following index period is not allowed : " + Double.toString(alert.getPeriod()));
     }
-    throw new NotImplementedException(metric.getType().toString());
   }
 }
index 3b2e329755e20c7e2fcc71f75567ee38631d3e45..d8a3837ae28e4b9dc2810829b8f96eec82d7ed91 100644 (file)
@@ -21,7 +21,11 @@ package org.sonar.plugins.core.sensors;
 
 import com.google.common.collect.Lists;
 import org.apache.commons.lang.StringUtils;
-import org.sonar.api.batch.*;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorBarriers;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
 import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.measures.Measure;
 import org.sonar.api.measures.Metric;
@@ -30,15 +34,19 @@ import org.sonar.api.profiles.RulesProfile;
 import org.sonar.api.resources.Project;
 import org.sonar.api.resources.Resource;
 import org.sonar.api.resources.ResourceUtils;
+import org.sonar.plugins.core.timemachine.Periods;
 
 import java.util.List;
 
 public class CheckAlertThresholds implements Decorator {
 
   private final RulesProfile profile;
+  private final Periods periods;
 
-  public CheckAlertThresholds(RulesProfile profile) {
+
+  public CheckAlertThresholds(RulesProfile profile, Periods periods) {
     this.profile = profile;
+    this.periods = periods;
   }
 
   @DependedUpon
@@ -46,6 +54,11 @@ public class CheckAlertThresholds implements Decorator {
     return CoreMetrics.ALERT_STATUS;
   }
 
+  @DependsUpon
+  public String dependsOnVariations() {
+    return DecoratorBarriers.END_OF_TIME_MACHINE;
+  }
+
   @DependsUpon
   public List<Metric> dependsUponMetrics() {
     List<Metric> metrics = Lists.newLinkedList();
@@ -110,7 +123,12 @@ public class CheckAlertThresholds implements Decorator {
     if (level == Metric.Level.OK) {
       return null;
     }
-    return alert.getAlertLabel(level);
+    String alertLabel = alert.getAlertLabel(level);
+    Integer alertPeriod = alert.getPeriod();
+    if (alertPeriod != null){
+      alertLabel += " " + periods.getLabel(alertPeriod);
+    }
+    return alertLabel;
   }
 
 
diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/Periods.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/Periods.java
new file mode 100644 (file)
index 0000000..c8424b6
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.sonar.api.BatchExtension;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.i18n.I18n;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+public class Periods implements BatchExtension {
+
+  private final Snapshot snapshot;
+  private final I18n i18n;
+
+  public Periods(Snapshot snapshot, I18n i18n) {
+    this.snapshot = snapshot;
+    this.i18n = i18n;
+  }
+
+  public String getLabel(int periodIndex) {
+    String mode = snapshot.getPeriodMode(periodIndex);
+    String param = snapshot.getPeriodModeParameter(periodIndex);
+    Date date = snapshot.getPeriodDate(periodIndex);
+
+    if (mode.equals(CoreProperties.TIMEMACHINE_MODE_DAYS)) {
+      return message("over_x_days", param);
+    } else if (mode.equals(CoreProperties.TIMEMACHINE_MODE_VERSION)) {
+      if (date != null) {
+        return message("since_version_detailed", param, convertDate(date));
+      } else {
+        return message("since_version", param);
+      }
+    } else if (mode.equals(CoreProperties.TIMEMACHINE_MODE_PREVIOUS_ANALYSIS)) {
+      if (date != null) {
+        return message("since_previous_analysis_detailed", convertDate(date));
+      } else {
+        return message("since_previous_analysis");
+      }
+    } else if (mode.equals(CoreProperties.TIMEMACHINE_MODE_PREVIOUS_VERSION)) {
+      if (param != null) {
+        return message("since_previous_version_detailed", param);
+      } else {
+        return message("since_previous_version");
+      }
+    } else if (mode.equals(CoreProperties.TIMEMACHINE_MODE_DATE)) {
+      return message("since_x", convertDate(date));
+    } else {
+      throw new IllegalStateException("This mode is not supported : " + mode);
+    }
+  }
+
+  private String message(String key, Object... parameters) {
+    return i18n.message(getLocale(), key, null, parameters);
+  }
+
+  private String convertDate(Date date){
+    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy MMM dd");
+    return dateFormat.format(date);
+  }
+
+  private Locale getLocale() {
+    return Locale.ENGLISH;
+  }
+
+}
index 54165af632ff75dff0a1deb73dd588f6b9400d17..355feb19f5ae25f8e6ff2ea26480ec30516e6b3e 100644 (file)
@@ -20,8 +20,9 @@
 package org.sonar.plugins.core.timemachine;
 
 import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorBarriers;
 import org.sonar.api.batch.DecoratorContext;
-import org.sonar.core.DryRunIncompatible;
+import org.sonar.api.batch.DependedUpon;
 import org.sonar.api.database.DatabaseSession;
 import org.sonar.api.database.model.Snapshot;
 import org.sonar.api.resources.Project;
@@ -29,10 +30,12 @@ import org.sonar.api.resources.Resource;
 import org.sonar.api.resources.ResourceUtils;
 import org.sonar.batch.components.PastSnapshot;
 import org.sonar.batch.components.TimeMachineConfiguration;
+import org.sonar.core.DryRunIncompatible;
 
 import java.util.List;
 
 @DryRunIncompatible
+@DependedUpon(DecoratorBarriers.END_OF_TIME_MACHINE)
 public final class TimeMachineConfigurationPersister implements Decorator {
 
   private TimeMachineConfiguration configuration;
@@ -54,11 +57,10 @@ public final class TimeMachineConfigurationPersister implements Decorator {
   void persistConfiguration() {
     List<PastSnapshot> pastSnapshots = configuration.getProjectPastSnapshots();
     for (PastSnapshot pastSnapshot : pastSnapshots) {
-      projectSnapshot = session.reattach(Snapshot.class, projectSnapshot.getId());
-      projectSnapshot.setPeriodMode(pastSnapshot.getIndex(), pastSnapshot.getMode());
-      projectSnapshot.setPeriodModeParameter(pastSnapshot.getIndex(), pastSnapshot.getModeParameter());
-      projectSnapshot.setPeriodDate(pastSnapshot.getIndex(), pastSnapshot.getTargetDate());
-      session.save(projectSnapshot);
+      Snapshot snapshot = session.reattach(Snapshot.class, projectSnapshot.getId());
+      updatePeriodParams(snapshot, pastSnapshot);
+      updatePeriodParams(projectSnapshot, pastSnapshot);
+      session.save(snapshot);
     }
     session.commit();
   }
@@ -66,4 +68,11 @@ public final class TimeMachineConfigurationPersister implements Decorator {
   public boolean shouldExecuteOnProject(Project project) {
     return true;
   }
+
+  private void updatePeriodParams(Snapshot snapshot, PastSnapshot pastSnapshot) {
+    int periodIndex = pastSnapshot.getIndex();
+    snapshot.setPeriodMode(periodIndex, pastSnapshot.getMode());
+    snapshot.setPeriodModeParameter(periodIndex, pastSnapshot.getModeParameter());
+    snapshot.setPeriodDate(periodIndex, pastSnapshot.getTargetDate());
+  }
 }
index 1843350e396b8ebdc806045ab791540b2a563721..87b4590b6262f9c1ca8cc57cce53d0d2432596bc 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.plugins.core.sensors;
 
+import org.apache.commons.lang.NotImplementedException;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentMatcher;
@@ -29,39 +30,50 @@ import org.sonar.api.measures.Metric;
 import org.sonar.api.profiles.Alert;
 import org.sonar.api.profiles.RulesProfile;
 import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.resources.Resource;
 import org.sonar.api.test.IsMeasure;
+import org.sonar.plugins.core.timemachine.Periods;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 
 import static org.junit.Assert.assertFalse;
 import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.*;
+import static org.mockito.Mockito.any;
+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 CheckAlertThresholdsTest {
+
   private CheckAlertThresholds decorator;
   private DecoratorContext context;
   private RulesProfile profile;
   private Measure measureClasses;
   private Measure measureCoverage;
+  private Measure measureComplexity;
   private Resource project;
-
+  private Periods periods;
 
   @Before
   public void setup() {
     context = mock(DecoratorContext.class);
+    periods = mock(Periods.class);
 
     measureClasses = new Measure(CoreMetrics.CLASSES, 20d);
     measureCoverage = new Measure(CoreMetrics.COVERAGE, 35d);
+    measureComplexity = new Measure(CoreMetrics.COMPLEXITY, 50d);
 
     when(context.getMeasure(CoreMetrics.CLASSES)).thenReturn(measureClasses);
     when(context.getMeasure(CoreMetrics.COVERAGE)).thenReturn(measureCoverage);
+    when(context.getMeasure(CoreMetrics.COMPLEXITY)).thenReturn(measureComplexity);
 
     profile = mock(RulesProfile.class);
-    decorator = new CheckAlertThresholds(profile);
+    decorator = new CheckAlertThresholds(profile, periods);
     project = mock(Resource.class);
-    when(project.getQualifier()).thenReturn(Resource.QUALIFIER_PROJECT);
+    when(project.getQualifier()).thenReturn(Qualifiers.PROJECT);
   }
 
   @Test
@@ -71,7 +83,7 @@ public class CheckAlertThresholdsTest {
   }
 
   @Test
-  public void shouldBeOkWhenNoAlerts() {
+  public void shouldBeOkWhenNoAlert() {
     when(profile.getAlerts()).thenReturn(Arrays.asList(
         new Alert(null, CoreMetrics.CLASSES, Alert.OPERATOR_GREATER, null, "20"),
         new Alert(null, CoreMetrics.COVERAGE, Alert.OPERATOR_GREATER, null, "35.0")));
@@ -130,11 +142,13 @@ public class CheckAlertThresholdsTest {
     when(alert1.getMetric()).thenReturn(CoreMetrics.CLASSES);
     when(alert1.getValueError()).thenReturn("10000"); // there are 20 classes, error threshold is higher => alert
     when(alert1.getAlertLabel(Metric.Level.ERROR)).thenReturn("error classes");
+    when(alert1.getPeriod()).thenReturn(null);
 
     Alert alert2 = mock(Alert.class);
     when(alert2.getMetric()).thenReturn(CoreMetrics.COVERAGE);
     when(alert2.getValueWarning()).thenReturn("80"); // coverage is 35%, warning threshold is higher => alert
     when(alert2.getAlertLabel(Metric.Level.WARN)).thenReturn("warning coverage");
+    when(alert2.getPeriod()).thenReturn(null);
 
     when(profile.getAlerts()).thenReturn(Arrays.asList(alert1, alert2));
     decorator.decorate(project, context);
@@ -142,6 +156,105 @@ public class CheckAlertThresholdsTest {
     verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.ALERT_STATUS, Metric.Level.ERROR, "error classes, warning coverage")));
   }
 
+  @Test
+  public void shouldBeOkIfPeriodVariationIsEnough() {
+    measureClasses.setVariation1(0d);
+    measureCoverage.setVariation2(50d);
+    measureComplexity.setVariation3(2d);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(
+        new Alert(null, CoreMetrics.CLASSES, Alert.OPERATOR_GREATER, null, "10", 1), // ok because no variation
+        new Alert(null, CoreMetrics.COVERAGE, Alert.OPERATOR_SMALLER, null, "40.0", 2), // ok because coverage increases of 50%, which is more than 40%
+        new Alert(null, CoreMetrics.COMPLEXITY, Alert.OPERATOR_GREATER, null, "5", 3) // ok because complexity increases of 2, which is less than 5
+    ));
+
+    decorator.decorate(project, context);
+
+    verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.ALERT_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);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(
+        new Alert(null, CoreMetrics.CLASSES, Alert.OPERATOR_GREATER, null, "30", 1),  // generates warning because classes increases of 40, which is greater than 30
+        new Alert(null, CoreMetrics.COVERAGE, Alert.OPERATOR_SMALLER, null, "10.0", 2), // generates warning because coverage increases of 5%, which is smaller than 10%
+        new Alert(null, CoreMetrics.COMPLEXITY, Alert.OPERATOR_GREATER, null, "60", 3) // generates warning because complexity increases of 70, which is smaller than 60
+    ));
+
+    decorator.decorate(project, context);
+
+    verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.ALERT_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 shouldVariationPeriodValueCouldBeUsedForRatingMetric() {
+    Metric ratingMetric = new Metric.Builder("key.rating", "name.rating", Metric.ValueType.RATING).create();
+    Measure measureRatingMetric = new Measure(ratingMetric, 150d);
+    measureRatingMetric.setVariation1(50d);
+    when(context.getMeasure(ratingMetric)).thenReturn(measureRatingMetric);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(
+        new Alert(null, ratingMetric, Alert.OPERATOR_GREATER, null, "100", 1)
+    ));
+
+    decorator.decorate(project, context);
+
+    verify(context).saveMeasure(argThat(matchesMetric(CoreMetrics.ALERT_STATUS, Metric.Level.OK, null)));
+    verify(context).saveMeasure(argThat(hasLevel(measureRatingMetric, Metric.Level.OK)));
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void shouldAllowOnlyVariationPeriodOneGlobalPeriods() {
+    measureClasses.setVariation4(40d);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(
+        new Alert(null, CoreMetrics.CLASSES, Alert.OPERATOR_GREATER, null, "30", 4)
+    ));
+
+    decorator.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);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(
+        new Alert(null, CoreMetrics.SCM_AUTHORS_BY_LINE, Alert.OPERATOR_GREATER, null, "30", 1)
+    ));
+
+    decorator.decorate(project, context);
+  }
+
+  @Test
+  public void shouldLabelAlertContainsPeriod() {
+    measureClasses.setVariation1(40d);
+
+    Alert alert = mock(Alert.class);
+    when(alert.getMetric()).thenReturn(CoreMetrics.CLASSES);
+    when(alert.getValueError()).thenReturn("30");
+    when(alert.getAlertLabel(Metric.Level.ERROR)).thenReturn("error classes");
+    when(alert.getPeriod()).thenReturn(1);
+
+    when(profile.getAlerts()).thenReturn(Arrays.asList(alert));
+    decorator.decorate(project, context);
+
+    verify(periods).getLabel(1);
+  }
+
   private ArgumentMatcher<Measure> matchesMetric(final Metric metric, final Metric.Level alertStatus, final String alertText) {
     return new ArgumentMatcher<Measure>() {
       @Override
diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/PeriodsTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/PeriodsTest.java
new file mode 100644 (file)
index 0000000..da6aa2c
--- /dev/null
@@ -0,0 +1,132 @@
+/*
+ * Sonar, open source software quality management tool.
+ * Copyright (C) 2008-2012 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * Sonar 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.
+ *
+ * Sonar 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 Sonar; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+ */
+package org.sonar.plugins.core.timemachine;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.i18n.I18n;
+
+import java.util.Date;
+import java.util.Locale;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class PeriodsTest {
+
+  private Periods periods;
+
+  private Snapshot snapshot;
+  private I18n i18n;
+
+  private int periodIndex;
+  private String param;
+
+  @Before
+  public void before() {
+    periodIndex = 1;
+    param = "10";
+
+    snapshot = mock(Snapshot.class);
+    i18n = mock(I18n.class);
+
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods = new Periods(snapshot, i18n);
+  }
+
+  @Test
+  public void shouldReturnLabelInModeDays() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_DAYS);
+    when(snapshot.getPeriodDate(periodIndex)).thenReturn(new Date());
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods.getLabel(periodIndex);
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("over_x_days"), Mockito.isNull(String.class), Mockito.eq(param));
+  }
+
+  @Test
+  public void shouldReturnLabelInModeVersion() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_VERSION);
+    when(snapshot.getPeriodDate(periodIndex)).thenReturn(new Date());
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_version_detailed"), Mockito.isNull(String.class), Mockito.eq(param), Mockito.anyString());
+  }
+
+  @Test
+     public void shouldReturnLabelInModePreviousAnalysis() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_VERSION);
+    when(snapshot.getPeriodDate(periodIndex)).thenReturn(new Date());
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_version_detailed"), Mockito.isNull(String.class), Mockito.eq(param), Mockito.anyString());
+
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_VERSION);
+    when(snapshot.getPeriodDate(periodIndex)).thenReturn(null);
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_version"), Mockito.isNull(String.class), Mockito.eq(param));
+  }
+
+  @Test
+  public void shouldReturnLabelInModePreviousVersion() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_PREVIOUS_VERSION);
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(param);
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_previous_version_detailed"), Mockito.isNull(String.class), Mockito.eq(param));
+
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_PREVIOUS_VERSION);
+    when(snapshot.getPeriodModeParameter(periodIndex)).thenReturn(null);
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_previous_version"), Mockito.isNull(String.class));
+  }
+
+  @Test
+  public void shouldReturnLabelInModeDate() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn(CoreProperties.TIMEMACHINE_MODE_DATE);
+    when(snapshot.getPeriodDate(periodIndex)).thenReturn(new Date());
+
+    periods.getLabel(periodIndex);
+
+    verify(i18n).message(Mockito.any(Locale.class), Mockito.eq("since_x"), Mockito.isNull(String.class), Mockito.anyString());
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void shouldNotSupportUnknownMode() {
+    when(snapshot.getPeriodMode(periodIndex)).thenReturn("Unknown mode");
+
+    periods.getLabel(periodIndex);
+  }
+}
index 02c2f84013728c2ecd0dbfe93bc08bdf75b074b2..f2d34dcd0890b76d1ea6ca13f672ae079656ab04 100644 (file)
@@ -32,7 +32,7 @@ import java.util.List;
  */
 public class DatabaseVersion implements BatchComponent, ServerComponent {
 
-  public static final int LAST_VERSION = 358;
+  public static final int LAST_VERSION = 359;
 
   public static enum Status {
     UP_TO_DATE, REQUIRES_UPGRADE, REQUIRES_DOWNGRADE, FRESH_INSTALL
index a46a8fdb1b90220e29a7044bcb8b431fdb3520f0..aba45d45a1b7573b78e93e65c167985657eda7a2 100644 (file)
@@ -186,6 +186,7 @@ INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('355');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('356');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('357');
 INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('358');
+INSERT INTO SCHEMA_MIGRATIONS(VERSION) VALUES ('359');
 
 INSERT INTO USERS(ID, LOGIN, NAME, EMAIL, CRYPTED_PASSWORD, SALT, CREATED_AT, UPDATED_AT, REMEMBER_TOKEN, REMEMBER_TOKEN_EXPIRES_AT) VALUES (1, 'admin', 'Administrator', '', 'a373a0e667abb2604c1fd571eb4ad47fe8cc0878', '48bc4b0d93179b5103fd3885ea9119498e9d161b', '2011-09-26 22:27:48.0', '2011-09-26 22:27:48.0', null, null);
 ALTER TABLE USERS ALTER COLUMN ID RESTART WITH 2;
index ea4589cf455f3cacc83017697c61dd1534426f35..8ad5551c6d339393a18a2dc85446371ec9aee296 100644 (file)
@@ -201,7 +201,8 @@ CREATE TABLE "ALERTS" (
   "METRIC_ID" INTEGER,
   "OPERATOR" VARCHAR(3),
   "VALUE_ERROR" VARCHAR(64),
-  "VALUE_WARNING" VARCHAR(64)
+  "VALUE_WARNING" VARCHAR(64),
+  "PERIOD" INTEGER,
 );
 
 CREATE TABLE "PROPERTIES" (
index f415df3b7989b7f2d99f093de5c2994178f1bb69..891e1e955d98bedfbcf80f803b2a056886aa1612 100644 (file)
@@ -25,7 +25,12 @@ import org.hibernate.annotations.CacheConcurrencyStrategy;
 import org.sonar.api.database.BaseIdentifiable;
 import org.sonar.api.measures.Metric;
 
-import javax.persistence.*;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
 
 /**
  * Class to map alerts with hibernate model
@@ -72,6 +77,9 @@ public class Alert extends BaseIdentifiable implements Cloneable {
   @Column(name = "value_warning", updatable = false, nullable = true, length = 64)
   private String valueWarning;
 
+  @Column(name = "period", updatable = false, nullable = true)
+  private Integer period;
+
   /**
    * Default constructor
    */
@@ -96,6 +104,20 @@ public class Alert extends BaseIdentifiable implements Cloneable {
     this.valueWarning = valueWarning;
   }
 
+  /**
+   * Creates an alert
+   *
+   * @param rulesProfile the profile used to trigger the alert
+   * @param metric the metric tested for the alert
+   * @param operator the operator defined
+   * @param valueError the error value
+   * @param valueWarning the warning value
+   */
+  public Alert(RulesProfile rulesProfile, Metric metric, String operator, String valueError, String valueWarning, Integer period) {
+    this(rulesProfile, metric, operator, valueError, valueWarning);
+    this.period = period;
+  }
+
   /**
    * @return the alert profile
    */
@@ -166,6 +188,20 @@ public class Alert extends BaseIdentifiable implements Cloneable {
     this.valueWarning = valueWarning;
   }
 
+  /**
+   * @return the period
+   */
+  public Integer getPeriod() {
+    return period;
+  }
+
+  /**
+   * Sets the period if any
+   */
+  public void setPeriod(Integer period) {
+    this.period = period;
+  }
+
   /**
    * @return whether the operator is greater than
    */
@@ -205,7 +241,7 @@ public class Alert extends BaseIdentifiable implements Cloneable {
 
   @Override
   public Object clone() {
-    return new Alert(getRulesProfile(), getMetric(), getOperator(), getValueError(), getValueWarning());
+    return new Alert(getRulesProfile(), getMetric(), getOperator(), getValueError(), getValueWarning(), getPeriod());
   }
 
 }
diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/359_add_period_to_alerts.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/359_add_period_to_alerts.rb
new file mode 100644 (file)
index 0000000..9a946e3
--- /dev/null
@@ -0,0 +1,31 @@
+#
+# Sonar, entreprise quality control tool.
+# Copyright (C) 2008-2012 SonarSource
+# mailto:contact AT sonarsource DOT com
+#
+# Sonar 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.
+#
+# Sonar 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 Sonar; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02
+#
+
+#
+# Sonar 3.4
+#
+class AddPeriodToAlerts < ActiveRecord::Migration
+
+  def self.up
+    add_column 'alerts', 'period', :integer, :null => true
+  end
+
+end
+
index bf416f8256ab498e6d6a0d7649add9ba17d182ae..9acc236c08844f7c8155f389d94b13fdd51ed4e3 100644 (file)
   <rules_profiles id="2" version="1" used_profile="true" name="profile2" language="JAV" />
 
   <!-- ok -->
-  <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]"/>
-  <alerts id="2" profile_id="2" metric_id="1" operator=">" value_error="[null]" value_warning="150"/>
+  <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]" period="[null]"/>
+  <alerts id="2" profile_id="2" metric_id="1" operator=">" value_error="[null]" value_warning="150" period="[null]"/>
 
   <!-- disabled metric -->
-  <!--<alerts id="3" profile_id="1" metric_id="2" operator=">" value_error="30" value_warning="[null]"/>-->
+  <!--<alerts id="3" profile_id="1" metric_id="2" operator=">" value_error="30" value_warning="[null]" period="[null]"/>-->
 
   <!-- unknown metric -->
-  <!--<alerts id="4" profile_id="1" metric_id="999" operator=">" value_error="30" value_warning="[null]"/>-->
+  <!--<alerts id="4" profile_id="1" metric_id="999" operator=">" value_error="30" value_warning="[null]" period="[null]"/>-->
 
 </dataset>
\ No newline at end of file
index 9e033faaa2fb123c5c777782ed95e6365efc9ff3..1f12416b02f16284db7abd826d775dc295dc7f98 100644 (file)
   <rules_profiles id="2" version="1" used_profile="true" name="profile2" language="JAV" />
 
   <!-- ok -->
-  <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]"/>
-  <alerts id="2" profile_id="2" metric_id="1" operator=">" value_error="[null]" value_warning="150"/>
+  <alerts id="1" profile_id="1" metric_id="1" operator=">" value_error="30" value_warning="[null]" period="[null]"/>
+  <alerts id="2" profile_id="2" metric_id="1" operator=">" value_error="[null]" value_warning="150" period="[null]"/>
 
   <!-- disabled metric -->
-  <alerts id="3" profile_id="1" metric_id="2" operator=">" value_error="30" value_warning="[null]"/>
+  <alerts id="3" profile_id="1" metric_id="2" operator=">" value_error="30" value_warning="[null]" period="[null]"/>
 
   <!-- unknown metric -->
-  <alerts id="4" profile_id="1" metric_id="999" operator=">" value_error="30" value_warning="[null]"/>
+  <alerts id="4" profile_id="1" metric_id="999" operator=">" value_error="30" value_warning="[null]" period="[null]"/>
 
 </dataset>
\ No newline at end of file