From: Sébastien Lesaint Date: Thu, 25 Jun 2015 16:40:56 +0000 (+0200) Subject: SONAR-6664 compute Sqale metrics in CE X-Git-Tag: 5.2-RC1~1306 X-Git-Url: https://source.dussan.org/?a=commitdiff_plain;h=94597925e396c970293202b23f55871e5e64e47a;p=sonarqube.git SONAR-6664 compute Sqale metrics in CE --- diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ComputeEngineContainerImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ComputeEngineContainerImpl.java index 53fbde2c698..2c7e62a7c88 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ComputeEngineContainerImpl.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ComputeEngineContainerImpl.java @@ -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 index 00000000000..c180f924b4b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingGrid.java @@ -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 index 00000000000..53add370c68 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/SqaleRatingSettings.java @@ -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 languageSpecificConfigurationByLanguageKey; + + public SqaleRatingSettings(Settings settings) { + this.settings = settings; + this.languageSpecificConfigurationByLanguageKey = buildLanguageSpecificConfigurationByLanguageKey(settings); + } + + private static Map buildLanguageSpecificConfigurationByLanguageKey(Settings settings) { + ImmutableMap.Builder 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 index 00000000000..c0e838d6e48 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/sqale/package-info.java @@ -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; diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java index 7f0f37154b1..d4e6d6fa0ff 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ComputationSteps.java @@ -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 index 00000000000..be4effc782d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/SqaleMeasuresStep.java @@ -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 { + 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() { + @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 path) { + computeAndSaveMeasures(project, path); + } + + @Override + public void visitDirectory(Component directory, Path path) { + computeAndSaveMeasures(directory, path); + } + + @Override + protected void visitModule(Component module, Path path) { + computeAndSaveMeasures(module, path); + } + + @Override + public void visitFile(Component file, Path path) { + if (!file.getFileAttributes().isUnitTest()) { + long developmentCosts = computeDevelopmentCost(file); + path.current().add(developmentCosts); + computeAndSaveMeasures(file, path); + } + } + + private void computeAndSaveMeasures(Component component, Path 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 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) { + 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 index 00000000000..e292eb63c2f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingGridTest.java @@ -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 index 00000000000..a48753620fb --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/sqale/SqaleRatingSettingsTest.java @@ -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 index 00000000000..1933feb7f12 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/step/SqaleMeasuresStepTest.java @@ -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)) + ); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java index 68cbfee5f51..464cac48d8f 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java @@ -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