From 936c85405b655e7434df01b24a2728fa60c94415 Mon Sep 17 00:00:00 2001 From: Julien Lancelot Date: Wed, 28 Nov 2012 15:37:51 +0100 Subject: [PATCH] SONAR-1352 add server side management of differential measure alerts --- .../org/sonar/plugins/core/CorePlugin.java | 2 + .../plugins/core/sensors/AlertUtils.java | 85 ++++++----- .../core/sensors/CheckAlertThresholds.java | 24 +++- .../plugins/core/timemachine/Periods.java | 86 ++++++++++++ .../TimeMachineConfigurationPersister.java | 21 ++- .../sensors/CheckAlertThresholdsTest.java | 123 +++++++++++++++- .../plugins/core/timemachine/PeriodsTest.java | 132 ++++++++++++++++++ .../core/persistence/DatabaseVersion.java | 2 +- .../org/sonar/core/persistence/rows-h2.sql | 1 + .../org/sonar/core/persistence/schema-h2.ddl | 3 +- .../java/org/sonar/api/profiles/Alert.java | 40 +++++- .../db/migrate/359_add_period_to_alerts.rb | 31 ++++ .../cleanAlerts-result.xml | 8 +- .../RegisterMetricsTest/cleanAlerts.xml | 8 +- 14 files changed, 505 insertions(+), 61 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/Periods.java create mode 100644 plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/PeriodsTest.java create mode 100644 sonar-server/src/main/webapp/WEB-INF/db/migrate/359_add_period_to_alerts.rb diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 0592005e12e..3fd67c43ea9 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -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, diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/AlertUtils.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/AlertUtils.java index 9fe246f5fdf..cec2e22be17 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/AlertUtils.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/AlertUtils.java @@ -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()); } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/CheckAlertThresholds.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/CheckAlertThresholds.java index 3b2e329755e..d8a3837ae28 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/CheckAlertThresholds.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/sensors/CheckAlertThresholds.java @@ -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 dependsUponMetrics() { List 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 index 00000000000..c8424b62163 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/Periods.java @@ -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; + } + +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/TimeMachineConfigurationPersister.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/TimeMachineConfigurationPersister.java index 54165af632f..355feb19f5a 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/TimeMachineConfigurationPersister.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/timemachine/TimeMachineConfigurationPersister.java @@ -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 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()); + } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/CheckAlertThresholdsTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/CheckAlertThresholdsTest.java index 1843350e396..87b4590b626 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/CheckAlertThresholdsTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/sensors/CheckAlertThresholdsTest.java @@ -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 matchesMetric(final Metric metric, final Metric.Level alertStatus, final String alertText) { return new ArgumentMatcher() { @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 index 00000000000..da6aa2c623d --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/timemachine/PeriodsTest.java @@ -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); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java index 02c2f840137..f2d34dcd089 100644 --- a/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java +++ b/sonar-core/src/main/java/org/sonar/core/persistence/DatabaseVersion.java @@ -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 diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql b/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql index a46a8fdb1b9..aba45d45a1b 100644 --- a/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/rows-h2.sql @@ -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; diff --git a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl index ea4589cf455..8ad5551c6d3 100644 --- a/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl +++ b/sonar-core/src/main/resources/org/sonar/core/persistence/schema-h2.ddl @@ -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" ( diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/profiles/Alert.java b/sonar-plugin-api/src/main/java/org/sonar/api/profiles/Alert.java index f415df3b798..891e1e955d9 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/profiles/Alert.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/profiles/Alert.java @@ -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 index 00000000000..9a946e3b1d6 --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/359_add_period_to_alerts.rb @@ -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 + diff --git a/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml b/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml index bf416f8256a..9acc236c088 100644 --- a/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml +++ b/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts-result.xml @@ -10,13 +10,13 @@ - - + + - + - + \ No newline at end of file diff --git a/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml b/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml index 9e033faaa2f..1f12416b02f 100644 --- a/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml +++ b/sonar-server/src/test/resources/org/sonar/server/startup/RegisterMetricsTest/cleanAlerts.xml @@ -11,13 +11,13 @@ - - + + - + - + \ No newline at end of file -- 2.39.5