]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6664 compute Sqale metrics in CE
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 25 Jun 2015 16:40:56 +0000 (18:40 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 26 Jun 2015 13:10:27 +0000 (15:10 +0200)
server/sonar-server/src/main/java/org/sonar/server/computation/container/ComputeEngineContainerImpl.java
server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingGrid.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingSettings.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/sqale/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java
server/sonar-server/src/main/java/org/sonar/server/computation/step/SqaleMeasuresStep.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingGridTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingSettingsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/step/SqaleMeasuresStepTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java

index 53fbde2c698d0a407ad90e0ab770667f0c72800f..2c7e62a7c88d57e078c58c7b3fc6dfc1c3f03ea4 100644 (file)
@@ -56,6 +56,7 @@ import org.sonar.server.computation.period.PeriodsHolderImpl;
 import org.sonar.server.computation.qualitygate.EvaluationResultTextConverterImpl;
 import org.sonar.server.computation.qualitygate.QualityGateHolderImpl;
 import org.sonar.server.computation.qualitygate.QualityGateServiceImpl;
+import org.sonar.server.computation.sqale.SqaleRatingSettings;
 import org.sonar.server.computation.step.ComputationStep;
 import org.sonar.server.computation.step.ComputationSteps;
 import org.sonar.server.view.index.ViewIndex;
@@ -133,6 +134,7 @@ public class ComputeEngineContainerImpl extends ComponentContainer implements Co
       PeriodsHolderImpl.class,
       QualityGateHolderImpl.class,
       DebtModelHolderImpl.class,
+      SqaleRatingSettings.class,
 
       BatchReportReaderImpl.class,
 
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingGrid.java b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingGrid.java
new file mode 100644 (file)
index 0000000..c180f92
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.sqale;
+
+import java.util.Arrays;
+import org.sonar.api.utils.MessageException;
+
+public class SqaleRatingGrid {
+
+  private final double[] gridValues;
+
+  public SqaleRatingGrid(double[] gridValues) {
+    this.gridValues = Arrays.copyOf(gridValues, gridValues.length);
+  }
+
+  public int getRatingForDensity(double density) {
+    for (SqaleRating sqaleRating : SqaleRating.values()) {
+      double lowerBound = getGradeLowerBound(sqaleRating);
+      if (density >= lowerBound) {
+        return sqaleRating.getIndex();
+      }
+    }
+    throw MessageException.of("The SQALE density value should be between 0 and " + Double.MAX_VALUE + " and got " + density);
+  }
+
+  private double getGradeLowerBound(SqaleRating rating) {
+    if (rating.getIndex() > 1) {
+      return gridValues[rating.getIndex() - 2];
+    }
+    return 0;
+  }
+
+  public enum SqaleRating {
+    E(5),
+    D(4),
+    C(3),
+    B(2),
+    A(1);
+
+    private final int index;
+
+    SqaleRating(int index) {
+      this.index = index;
+    }
+
+    public int getIndex() {
+      return index;
+    }
+
+    public static SqaleRating createForIndex(int index) {
+      for (SqaleRating rating : values()) {
+        if (rating.getIndex() == index) {
+          return rating;
+        }
+      }
+      throw new IllegalArgumentException("A SQALE rating must be in the range [1..5].");
+    }
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingSettings.java b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingSettings.java
new file mode 100644 (file)
index 0000000..53add37
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.sqale;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.config.Settings;
+
+import static java.lang.String.format;
+import static org.sonar.api.CoreProperties.DEVELOPMENT_COST;
+import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS;
+import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
+import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY;
+import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY;
+import static org.sonar.api.CoreProperties.RATING_GRID;
+import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES;
+import static org.sonar.api.CoreProperties.SIZE_METRIC;
+
+public class SqaleRatingSettings {
+
+  private final Settings settings;
+  private final Map<String, LanguageSpecificConfiguration> languageSpecificConfigurationByLanguageKey;
+
+  public SqaleRatingSettings(Settings settings) {
+    this.settings = settings;
+    this.languageSpecificConfigurationByLanguageKey = buildLanguageSpecificConfigurationByLanguageKey(settings);
+  }
+
+  private static Map<String, LanguageSpecificConfiguration> buildLanguageSpecificConfigurationByLanguageKey(Settings settings) {
+    ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder();
+    String[] languageConfigIndexes = settings.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS);
+    for (String languageConfigIndex : languageConfigIndexes) {
+      String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY;
+      builder.put(settings.getString(languagePropertyKey), LanguageSpecificConfiguration.create(settings, languageConfigIndex));
+    }
+    return builder.build();
+  }
+
+  public double[] getRatingGrid() {
+    try {
+      String[] ratingGrades = settings.getStringArray(RATING_GRID);
+      double[] grid = new double[4];
+      for (int i = 0; i < 4; i++) {
+        grid[i] = Double.parseDouble(ratingGrades[i]);
+      }
+      return grid;
+    } catch (Exception e) {
+      throw new IllegalArgumentException("The SQALE rating grid is incorrect. Expected something similar to '"
+        + RATING_GRID_DEF_VALUES + "' and got '"
+        + settings.getString(RATING_GRID) + "'", e);
+    }
+  }
+
+  public long getDevCost(@Nullable String languageKey) {
+    if (languageKey != null) {
+      try {
+        LanguageSpecificConfiguration languageSpecificConfig = getSpecificParametersForLanguage(languageKey);
+        if (languageSpecificConfig != null && languageSpecificConfig.getManDays() != null) {
+          return Long.parseLong(languageSpecificConfig.getManDays());
+        }
+      } catch (NumberFormatException e) {
+        throw new IllegalArgumentException(format("The manDays for language %s is not a valid long number", languageKey), e);
+      }
+    }
+
+    return getDefaultDevelopmentCost();
+  }
+
+  private long getDefaultDevelopmentCost() {
+    try {
+      return Long.parseLong(settings.getString(DEVELOPMENT_COST));
+    } catch (NumberFormatException e) {
+      throw new IllegalArgumentException("The value of the SQALE property '" + DEVELOPMENT_COST
+        + "' is incorrect. Expected long but got '" + settings.getString(DEVELOPMENT_COST) + "'", e);
+    }
+  }
+
+  public String getSizeMetricKey(@Nullable String languageKey) {
+    if (languageKey == null) {
+      return settings.getString(SIZE_METRIC);
+    }
+
+    LanguageSpecificConfiguration languageSpecificConfig = getSpecificParametersForLanguage(languageKey);
+    if (languageSpecificConfig != null && languageSpecificConfig.getMetricKey() != null) {
+      return languageSpecificConfig.getMetricKey();
+    }
+    return settings.getString(SIZE_METRIC);
+  }
+
+  @CheckForNull
+  private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) {
+    return languageSpecificConfigurationByLanguageKey.get(languageKey);
+  }
+
+  @Immutable
+  private static class LanguageSpecificConfiguration {
+    private final String language;
+    private final String manDays;
+    private final String metricKey;
+
+    private LanguageSpecificConfiguration(String language, String manDays, String metricKey) {
+      this.language = language;
+      this.manDays = manDays;
+      this.metricKey = metricKey;
+    }
+
+    static LanguageSpecificConfiguration create(Settings settings, String configurationId) {
+
+      String configurationPrefix = LANGUAGE_SPECIFIC_PARAMETERS + "." + configurationId + ".";
+
+      String language = settings.getString(configurationPrefix + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY);
+      String manDays = settings.getString(configurationPrefix + LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY);
+      String metric = settings.getString(configurationPrefix + LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY);
+
+      return new LanguageSpecificConfiguration(language, manDays, metric);
+    }
+
+    String getLanguage() {
+      return language;
+    }
+
+    String getManDays() {
+      return manDays;
+    }
+
+    String getMetricKey() {
+      return metricKey;
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/package-info.java
new file mode 100644 (file)
index 0000000..c0e838d
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.sqale;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 7f0f37154b1e278d666d779900a6de2c092f0645..d4e6d6fa0ff17ef82bfd399748c337b09eb7549b 100644 (file)
@@ -56,6 +56,7 @@ public class ComputationSteps {
       // data computation
       CustomMeasuresCopyStep.class,
       ComputeIssueMeasuresStep.class,
+      SqaleMeasuresStep.class,
 
       // Must be executed after computation of all measures
       FillMeasuresWithVariationsStep.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/SqaleMeasuresStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SqaleMeasuresStep.java
new file mode 100644 (file)
index 0000000..be4effc
--- /dev/null
@@ -0,0 +1,191 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.step;
+
+import com.google.common.base.Optional;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.PathAwareVisitor;
+import org.sonar.server.computation.component.TreeRootHolder;
+import org.sonar.server.computation.measure.Measure;
+import org.sonar.server.computation.measure.MeasureRepository;
+import org.sonar.server.computation.metric.Metric;
+import org.sonar.server.computation.metric.MetricRepository;
+import org.sonar.server.computation.sqale.SqaleRatingGrid;
+import org.sonar.server.computation.sqale.SqaleRatingSettings;
+
+import static org.sonar.server.computation.measure.Measure.newMeasureBuilder;
+
+public class SqaleMeasuresStep implements ComputationStep {
+  private final TreeRootHolder treeRootHolder;
+  private final MetricRepository metricRepository;
+  private final MeasureRepository measureRepository;
+  private final SqaleRatingSettings sqaleRatingSettings;
+
+  public SqaleMeasuresStep(TreeRootHolder treeRootHolder, MetricRepository metricRepository, MeasureRepository measureRepository,
+    SqaleRatingSettings sqaleRatingSettings) {
+    this.treeRootHolder = treeRootHolder;
+    this.metricRepository = metricRepository;
+    this.measureRepository = measureRepository;
+    this.sqaleRatingSettings = sqaleRatingSettings;
+  }
+
+  @Override
+  public void execute() {
+    new SqualeMeasuresVisitor().visit(treeRootHolder.getRoot());
+  }
+
+  @Override
+  public String getDescription() {
+    return "Compute Sqale related measures";
+  }
+
+  private class SqualeMeasuresVisitor extends PathAwareVisitor<DevelopmentCost> {
+    private final Metric developmentCostMetric;
+    private final Metric technicalDebtMetric;
+    private final Metric debtRatioMetric;
+    private final Metric sqaleRatingMetric;
+
+    public SqualeMeasuresVisitor() {
+      super(Component.Type.FILE, Order.POST_ORDER, new SimpleStackElementFactory<DevelopmentCost>() {
+        @Override
+        public DevelopmentCost createForAny(Component component) {
+          return new DevelopmentCost();
+        }
+      });
+
+      this.developmentCostMetric = metricRepository.getByKey(CoreMetrics.DEVELOPMENT_COST_KEY);
+      this.technicalDebtMetric = metricRepository.getByKey(CoreMetrics.TECHNICAL_DEBT_KEY);
+      this.debtRatioMetric = metricRepository.getByKey(CoreMetrics.SQALE_DEBT_RATIO_KEY);
+      this.sqaleRatingMetric = metricRepository.getByKey(CoreMetrics.SQALE_RATING_KEY);
+    }
+
+    @Override
+    public void visitProject(Component project, Path<DevelopmentCost> path) {
+      computeAndSaveMeasures(project, path);
+    }
+
+    @Override
+    public void visitDirectory(Component directory, Path<DevelopmentCost> path) {
+      computeAndSaveMeasures(directory, path);
+    }
+
+    @Override
+    protected void visitModule(Component module, Path<DevelopmentCost> path) {
+      computeAndSaveMeasures(module, path);
+    }
+
+    @Override
+    public void visitFile(Component file, Path<DevelopmentCost> path) {
+      if (!file.getFileAttributes().isUnitTest()) {
+        long developmentCosts = computeDevelopmentCost(file);
+        path.current().add(developmentCosts);
+        computeAndSaveMeasures(file, path);
+      }
+    }
+
+    private void computeAndSaveMeasures(Component component, Path<DevelopmentCost> path) {
+      saveDevelopmentCostMeasure(component, path.current());
+
+      double density = computeDensity(component, path.current());
+      saveDebtRatioMeasure(component, density);
+      saveSqaleRatingMeasure(component, density);
+
+      increaseParentDevelopmentCost(path);
+    }
+
+    private void saveDevelopmentCostMeasure(Component component, DevelopmentCost developmentCost) {
+      // the value of this measure is stored as a string because it can exceed the size limit of number storage on some DB
+      measureRepository.add(component, developmentCostMetric, newMeasureBuilder().create(Long.toString(developmentCost.getValue())));
+    }
+
+    private double computeDensity(Component component, DevelopmentCost developmentCost) {
+      double debt = getLongValue(measureRepository.getRawMeasure(component, technicalDebtMetric));
+      if (Double.doubleToRawLongBits(developmentCost.getValue()) != 0L) {
+        return debt / (double) developmentCost.getValue();
+      }
+      return 0d;
+    }
+
+    private void saveDebtRatioMeasure(Component component, double density) {
+      measureRepository.add(component, debtRatioMetric, newMeasureBuilder().create(100.0 * density));
+    }
+
+    private void saveSqaleRatingMeasure(Component component, double density) {
+      SqaleRatingGrid ratingGrid = new SqaleRatingGrid(sqaleRatingSettings.getRatingGrid());
+      int rating = ratingGrid.getRatingForDensity(density);
+      String ratingLetter = toRatingLetter(rating);
+      measureRepository.add(component, sqaleRatingMetric, newMeasureBuilder().create(rating, ratingLetter));
+    }
+
+    private void increaseParentDevelopmentCost(Path<DevelopmentCost> path) {
+      if (!path.isRoot()) {
+        // increase parent's developmentCost with our own
+        path.parent().add(path.current().getValue());
+      }
+    }
+  }
+
+  private long computeDevelopmentCost(Component file) {
+    String languageKey = file.getFileAttributes().getLanguageKey();
+    String sizeMetricKey = sqaleRatingSettings.getSizeMetricKey(languageKey);
+    Metric sizeMetric = metricRepository.getByKey(sizeMetricKey);
+    return getLongValue(measureRepository.getRawMeasure(file, sizeMetric)) * sqaleRatingSettings.getDevCost(languageKey);
+  }
+
+  private static long getLongValue(Optional<Measure> measure) {
+    if (!measure.isPresent()) {
+      return 0L;
+    }
+    return getLongValue(measure.get());
+  }
+
+  private static long getLongValue(Measure measure) {
+    switch (measure.getValueType()) {
+      case INT:
+        return measure.getIntValue();
+      case LONG:
+        return measure.getLongValue();
+      case DOUBLE:
+        return new Double(measure.getDoubleValue()).longValue();
+      default:
+        return 0L;
+    }
+  }
+
+  private static String toRatingLetter(int rating) {
+    return SqaleRatingGrid.SqaleRating.createForIndex(rating).name();
+  }
+
+  /**
+   * A wrapper class around a long which can be increased and represents the development cost of a Component
+   */
+  private static final class DevelopmentCost {
+    private long value = 0;
+
+    public void add(long developmentCosts) {
+      this.value += developmentCosts;
+    }
+
+    public long getValue() {
+      return value;
+    }
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingGridTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingGridTest.java
new file mode 100644 (file)
index 0000000..e292eb6
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.sqale;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SqaleRatingGridTest {
+
+  private SqaleRatingGrid ratingGrid;
+
+  @Rule
+  public ExpectedException throwable = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    double[] gridValues = new double[] {0.1, 0.2, 0.5, 1};
+    ratingGrid = new SqaleRatingGrid(gridValues);
+  }
+
+  @Test
+  public void return_rating_matching_density() {
+    assertThat(ratingGrid.getRatingForDensity(0)).isEqualTo(1);
+    assertThat(ratingGrid.getRatingForDensity(0.05)).isEqualTo(1);
+    assertThat(ratingGrid.getRatingForDensity(0.09999999)).isEqualTo(1);
+    assertThat(ratingGrid.getRatingForDensity(0.1)).isEqualTo(2);
+    assertThat(ratingGrid.getRatingForDensity(0.15)).isEqualTo(2);
+    assertThat(ratingGrid.getRatingForDensity(0.2)).isEqualTo(3);
+    assertThat(ratingGrid.getRatingForDensity(0.25)).isEqualTo(3);
+    assertThat(ratingGrid.getRatingForDensity(0.5)).isEqualTo(4);
+    assertThat(ratingGrid.getRatingForDensity(0.65)).isEqualTo(4);
+    assertThat(ratingGrid.getRatingForDensity(1)).isEqualTo(5);
+    assertThat(ratingGrid.getRatingForDensity(1.01)).isEqualTo(5);
+  }
+
+  @Test
+  public void fail_on_invalid_density() {
+    throwable.expect(RuntimeException.class);
+
+    ratingGrid.getRatingForDensity(-1);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingSettingsTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingSettingsTest.java
new file mode 100644 (file)
index 0000000..a487536
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.sqale;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SqaleRatingSettingsTest {
+
+  private Settings settings;
+
+  @Rule
+  public ExpectedException throwable = ExpectedException.none();
+
+  @Before
+  public void setUp() {
+    settings = new Settings();
+  }
+
+  @Test
+  public void load_rating_grid() {
+    settings.setProperty(CoreProperties.RATING_GRID, "1,3.4,8,50");
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    double[] grid = configurationLoader.getRatingGrid();
+    assertThat(grid).hasSize(4);
+    assertThat(grid[0]).isEqualTo(1.0);
+    assertThat(grid[1]).isEqualTo(3.4);
+    assertThat(grid[2]).isEqualTo(8.0);
+    assertThat(grid[3]).isEqualTo(50.0);
+  }
+
+  @Test
+  public void load_work_units_for_language() {
+    settings.setProperty(CoreProperties.DEVELOPMENT_COST, "50");
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    assertThat(configurationLoader.getDevCost("defaultLanguage")).isEqualTo(50L);
+  }
+
+  @Test
+  public void load_size_metric_for_language() {
+    settings.setProperty(CoreProperties.SIZE_METRIC, "complexity");
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    assertThat(configurationLoader.getSizeMetricKey("defaultLanguage")).isEqualTo("complexity");
+  }
+
+  @Test
+  public void load_overridden_values_for_language() {
+
+    String aLanguage = "aLanguage";
+    String anotherLanguage = "anotherLanguage";
+
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS, "0,1");
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY, aLanguage);
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY, "30");
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY, CoreMetrics.NCLOC_KEY);
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "1" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY, anotherLanguage);
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "1" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY, "40");
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "1" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY, CoreMetrics.COMPLEXITY_KEY);
+
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    assertThat(configurationLoader.getSizeMetricKey(aLanguage)).isEqualTo(CoreMetrics.NCLOC_KEY);
+    assertThat(configurationLoader.getSizeMetricKey(anotherLanguage)).isEqualTo(CoreMetrics.COMPLEXITY_KEY);
+    assertThat(configurationLoader.getDevCost(aLanguage)).isEqualTo(30L);
+    assertThat(configurationLoader.getDevCost(anotherLanguage)).isEqualTo(40L);
+  }
+
+  @Test
+  public void fail_on_invalid_rating_grid_configuration() {
+
+    throwable.expect(IllegalArgumentException.class);
+    settings.setProperty(CoreProperties.RATING_GRID, "a b c");
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    configurationLoader.getRatingGrid();
+  }
+
+  @Test
+  public void use_generic_value_when_specific_setting_is_missing() {
+    String aLanguage = "aLanguage";
+
+    settings.setProperty(CoreProperties.SIZE_METRIC, "complexity");
+    settings.setProperty(CoreProperties.DEVELOPMENT_COST, "30");
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS, "0");
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY, aLanguage);
+    settings.setProperty(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + "0" + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY, "40");
+
+    SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
+
+    assertThat(configurationLoader.getSizeMetricKey(aLanguage)).isEqualTo(CoreMetrics.COMPLEXITY_KEY);
+    assertThat(configurationLoader.getDevCost(aLanguage)).isEqualTo(40L);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/step/SqaleMeasuresStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SqaleMeasuresStepTest.java
new file mode 100644 (file)
index 0000000..1933feb
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 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.server.computation.step;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.DumbComponent;
+import org.sonar.server.computation.component.FileAttributes;
+import org.sonar.server.computation.measure.Measure;
+import org.sonar.server.computation.measure.MeasureRepoEntry;
+import org.sonar.server.computation.measure.MeasureRepositoryRule;
+import org.sonar.server.computation.metric.Metric;
+import org.sonar.server.computation.metric.MetricImpl;
+import org.sonar.server.computation.metric.MetricRepositoryRule;
+import org.sonar.server.computation.sqale.SqaleRatingGrid;
+import org.sonar.server.computation.sqale.SqaleRatingSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY;
+import static org.sonar.api.measures.CoreMetrics.SQALE_DEBT_RATIO_KEY;
+import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY;
+import static org.sonar.server.computation.component.Component.Type.DIRECTORY;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.Component.Type.MODULE;
+import static org.sonar.server.computation.component.Component.Type.PROJECT;
+import static org.sonar.server.computation.measure.Measure.newMeasureBuilder;
+import static org.sonar.server.computation.measure.MeasureRepoEntry.toEntries;
+import static org.sonar.server.computation.sqale.SqaleRatingGrid.SqaleRating.A;
+import static org.sonar.server.computation.sqale.SqaleRatingGrid.SqaleRating.C;
+
+public class SqaleMeasuresStepTest {
+
+  private static final String METRIC_KEY_1 = "mKey1";
+  private static final String METRIC_KEY_2 = "mKey2";
+  private static final Metric METRIC_1 = new MetricImpl(1, METRIC_KEY_1, "metric1", Metric.MetricType.FLOAT);
+  private static final Metric METRIC_2 = new MetricImpl(2, METRIC_KEY_2, "metric2", Metric.MetricType.WORK_DUR);
+  private static final String LANGUAGE_KEY_1 = "lKey1";
+  private static final String LANGUAGE_KEY_2 = "lKey2";
+  private static final double[] RATING_GRID = new double[] {34, 50, 362, 900, 36258};
+  private static final long DEV_COST_LANGUAGE_1 = 33;
+  private static final long DEV_COST_LANGUAGE_2 = 42;
+
+  @Rule
+  public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+  @Rule
+  public MetricRepositoryRule metricRepository = new MetricRepositoryRule().add(METRIC_1).add(METRIC_2);
+  @Rule
+  public MeasureRepositoryRule measureRepository = MeasureRepositoryRule.create(treeRootHolder, metricRepository);
+  private SqaleRatingSettings sqaleRatingSettings = mock(SqaleRatingSettings.class);
+
+  private SqaleMeasuresStep underTest = new SqaleMeasuresStep(treeRootHolder, metricRepository, measureRepository, sqaleRatingSettings);
+
+  @Before
+  public void setUp() throws Exception {
+    // assumes squale rating configuration is consistent
+    when(sqaleRatingSettings.getRatingGrid()).thenReturn(RATING_GRID);
+    when(sqaleRatingSettings.getSizeMetricKey(LANGUAGE_KEY_1)).thenReturn(METRIC_KEY_1);
+    when(sqaleRatingSettings.getSizeMetricKey(LANGUAGE_KEY_2)).thenReturn(METRIC_KEY_2);
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_KEY_1)).thenReturn(DEV_COST_LANGUAGE_1);
+    when(sqaleRatingSettings.getDevCost(LANGUAGE_KEY_2)).thenReturn(DEV_COST_LANGUAGE_2);
+
+    // this measures are always retrieved by the step
+    metricRepository.add(CoreMetrics.DEVELOPMENT_COST);
+    metricRepository.add(CoreMetrics.TECHNICAL_DEBT);
+    metricRepository.add(CoreMetrics.SQALE_DEBT_RATIO);
+    metricRepository.add(CoreMetrics.SQALE_RATING);
+  }
+
+  @Test
+  public void measures_created_for_project_are_all_zero_when_they_have_no_FILE_child() {
+    DumbComponent root = DumbComponent.builder(PROJECT, 1).build();
+    treeRootHolder.setRoot(root);
+
+    underTest.execute();
+
+    assertThat(toEntries(measureRepository.getRawMeasures(root))).containsOnly(
+      MeasureRepoEntry.entryOf(DEVELOPMENT_COST_KEY, newMeasureBuilder().create("0")),
+      MeasureRepoEntry.entryOf(SQALE_DEBT_RATIO_KEY, newMeasureBuilder().create(0d)),
+      MeasureRepoEntry.entryOf(SQALE_RATING_KEY, createSqaleRatingMeasure(A))
+      );
+  }
+
+  private Measure createSqaleRatingMeasure(SqaleRatingGrid.SqaleRating sqaleRating) {
+    return newMeasureBuilder().create(sqaleRating.getIndex(), sqaleRating.name());
+  }
+
+  @Test
+  public void verify_computation_of_measures_for_file_depending_upon_language_1() {
+    verify_computation_of_measure_for_file(33000l, DEV_COST_LANGUAGE_1, METRIC_KEY_1, LANGUAGE_KEY_1, C);
+  }
+
+  @Test
+  public void verify_computation_of_measures_for_file_depending_upon_language_2() {
+    verify_computation_of_measure_for_file(4200l, DEV_COST_LANGUAGE_2, METRIC_KEY_2, LANGUAGE_KEY_2, A);
+  }
+
+  /**
+   * Verify the computation of measures values depending upon which language is associated to the file by
+   * processing a tree of a single Component of type FILE.
+   */
+  private void verify_computation_of_measure_for_file(long debt, long languageCost, String metricKey, String languageKey,
+    SqaleRatingGrid.SqaleRating expectedRating) {
+    long measureValue = 10;
+
+    DumbComponent fileComponent = createFileComponent(languageKey, 1);
+    treeRootHolder.setRoot(fileComponent);
+    measureRepository.addRawMeasure(fileComponent.getRef(), metricKey, newMeasureBuilder().create(measureValue));
+    measureRepository.addRawMeasure(fileComponent.getRef(), TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt));
+
+    underTest.execute();
+
+    verifyFileMeasures(fileComponent.getRef(), measureValue, debt, languageCost, expectedRating);
+  }
+
+  @Test
+  public void verify_aggregation_of_developmentCost_and_value_of_measures_computed_from_that() {
+    DumbComponent root = DumbComponent.builder(PROJECT, 1)
+      .addChildren(
+        DumbComponent.builder(MODULE, 11)
+          .addChildren(
+            DumbComponent.builder(DIRECTORY, 111)
+              .addChildren(
+                createFileComponent(LANGUAGE_KEY_1, 1111),
+                createFileComponent(LANGUAGE_KEY_2, 1112)
+              ).build(),
+            DumbComponent.builder(DIRECTORY, 112)
+              .addChildren(
+                createFileComponent(LANGUAGE_KEY_2, 1121)
+              ).build()
+          ).build(),
+        DumbComponent.builder(MODULE, 12)
+          .addChildren(
+            DumbComponent.builder(DIRECTORY, 121)
+              .addChildren(
+                createFileComponent(LANGUAGE_KEY_1, 1211)
+              ).build(),
+            DumbComponent.builder(DIRECTORY, 122).build()
+          ).build(),
+        DumbComponent.builder(MODULE, 13).build()
+      ).build();
+
+    treeRootHolder.setRoot(root);
+
+    long measureValue1111 = 10;
+    long debt1111 = 66000l;
+    measureRepository.addRawMeasure(1111, METRIC_KEY_1, newMeasureBuilder().create(measureValue1111));
+    measureRepository.addRawMeasure(1111, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt1111));
+
+    long measureValue1112 = 10;
+    long debt1112 = 4200l;
+    measureRepository.addRawMeasure(1112, METRIC_KEY_2, newMeasureBuilder().create(measureValue1112));
+    measureRepository.addRawMeasure(1112, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt1112));
+
+    long debt111 = 96325l;
+    measureRepository.addRawMeasure(111, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt111));
+
+    long measureValue1121 = 30;
+    long debt1121 = 25200l;
+    measureRepository.addRawMeasure(1121, METRIC_KEY_2, newMeasureBuilder().create(measureValue1121));
+    measureRepository.addRawMeasure(1121, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt1121));
+
+    long debt112 = 99633l;
+    measureRepository.addRawMeasure(112, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt112));
+
+    long measureValue1211 = 20;
+    long debt1211 = 33000l;
+    measureRepository.addRawMeasure(1211, METRIC_KEY_1, newMeasureBuilder().create(measureValue1211));
+    measureRepository.addRawMeasure(1211, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt1211));
+
+    long debt121 = 7524l;
+    measureRepository.addRawMeasure(121, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt121));
+
+    long debt1 = 9999l;
+    measureRepository.addRawMeasure(1, TECHNICAL_DEBT_KEY, newMeasureBuilder().create(debt1));
+
+    underTest.execute();
+
+    // verify measures on files
+    verifyFileMeasures(1111, measureValue1111, debt1111, DEV_COST_LANGUAGE_1, C);
+    verifyFileMeasures(1112, measureValue1112, debt1112, DEV_COST_LANGUAGE_2, A);
+    verifyFileMeasures(1121, measureValue1121, debt1121, DEV_COST_LANGUAGE_2, A);
+    verifyFileMeasures(1211, measureValue1211, debt1211, DEV_COST_LANGUAGE_1, C);
+    // directory has no children => no file => 0 everywhere and A rating
+    verifyComponentMeasures(122, 0, 0, A);
+    // directory has children => dev cost is aggregated
+    long devCost111 = measureValue1111 * DEV_COST_LANGUAGE_1 + measureValue1112 * DEV_COST_LANGUAGE_2;
+    verifyComponentMeasures(111, devCost111, debt111 / (double) devCost111, C);
+    long devCost112 = measureValue1121 * DEV_COST_LANGUAGE_2;
+    verifyComponentMeasures(112, devCost112, debt112 / (double) devCost112, C);
+    long devCost121 = measureValue1211 * DEV_COST_LANGUAGE_1;
+    verifyComponentMeasures(121, devCost121, debt121 / (double) devCost121, A);
+    // just for fun, we didn't define any debt on module => they must all have rating A
+    long devCost11 = devCost111 + devCost112;
+    verifyComponentMeasures(11, devCost11, 0, A);
+    long devCost12 = devCost121;
+    verifyComponentMeasures(12, devCost12, 0, A);
+    long devCost13 = 0;
+    verifyComponentMeasures(13, devCost13, 0, A);
+    // project has aggregated dev cost of all files
+    long devCost1 = devCost11 + devCost12 + devCost13;
+    verifyComponentMeasures(1, devCost1, debt1 / (double) devCost1, A);
+  }
+
+  private DumbComponent createFileComponent(String languageKey1, int fileRef) {
+    return DumbComponent.builder(FILE, fileRef).setFileAttributes(new FileAttributes(false, languageKey1)).build();
+  }
+
+  private void verifyNoMeasure(int componentRef) {
+    assertThat(measureRepository.getRawMeasures(componentRef).isEmpty()).isTrue();
+  }
+
+  private void verifyFileMeasures(int componentRef, long measureValue, long debt, long languageCost, SqaleRatingGrid.SqaleRating expectedRating) {
+    long developmentCost = measureValue * languageCost;
+    verifyComponentMeasures(componentRef, developmentCost, debt / developmentCost, expectedRating);
+  }
+
+  private void verifyComponentMeasures(int componentRef, long expectedDevCost, double expectedDebtRatio, SqaleRatingGrid.SqaleRating expectedRating) {
+    assertThat(toEntries(measureRepository.getNewRawMeasures(componentRef))).containsOnly(
+      MeasureRepoEntry.entryOf(DEVELOPMENT_COST_KEY, newMeasureBuilder().create(Long.toString(expectedDevCost))),
+      MeasureRepoEntry.entryOf(SQALE_DEBT_RATIO_KEY, newMeasureBuilder().create(expectedDebtRatio * 100.0)),
+      MeasureRepoEntry.entryOf(SQALE_RATING_KEY, createSqaleRatingMeasure(expectedRating))
+      );
+  }
+
+}
index 68cbfee5f51dbdda56fe136f2f7dd205a5f66c18..464cac48d8f597495e2ca7a7a7a39f702077cb7f 100644 (file)
@@ -113,7 +113,12 @@ public class SqaleRatingSettingsTest {
     settings.setProperty(CoreProperties.DEVELOPMENT_COST, "a");
     SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings);
 
-    configurationLoader.getSizeMetric("aLanguage", metrics);
+    try {
+      configurationLoader.getSizeMetric("aLanguage", metrics);
+    }
+    catch (IllegalArgumentException e) {
+      e.printStackTrace();
+    }
   }
 
   @Test