diff options
author | Julien Lancelot <julien.lancelot@sonarsource.com> | 2014-07-09 18:36:52 +0200 |
---|---|---|
committer | Julien Lancelot <julien.lancelot@sonarsource.com> | 2014-07-09 18:36:52 +0200 |
commit | 4d5d1f5be3d1ace0199d39a086165e1086600a26 (patch) | |
tree | f44cfe310ddf3cd277fce68fbec36e90fdb255c8 /sonar-batch | |
parent | 6a7de91d1fb7b3197da780079607a2489064d67b (diff) | |
download | sonarqube-4d5d1f5be3d1ace0199d39a086165e1086600a26.tar.gz sonarqube-4d5d1f5be3d1ace0199d39a086165e1086600a26.zip |
SONAR-5388 Introduce the SQALE Rating metric in SonarQube Core
Diffstat (limited to 'sonar-batch')
7 files changed, 762 insertions, 29 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingDecorator.java new file mode 100644 index 00000000000..dc973dd109c --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingDecorator.java @@ -0,0 +1,145 @@ +/* + * 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.batch.debt; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.Lists; +import org.sonar.api.batch.Decorator; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.batch.DependedUpon; +import org.sonar.api.batch.DependsUpon; +import org.sonar.api.batch.fs.FileSystem; +import org.sonar.api.batch.fs.InputFile; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Resource; +import org.sonar.api.resources.ResourceUtils; + +import javax.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +/** + * Decorator that computes Sqale Rating metric + */ +public final class SqaleRatingDecorator implements Decorator { + + private final SqaleRatingSettings sqaleRatingSettings; + private final Metric[] metrics; + private final FileSystem fs; + + public SqaleRatingDecorator(SqaleRatingSettings sqaleRatingSettings, Metric[] metrics, FileSystem fs) { + this.sqaleRatingSettings = sqaleRatingSettings; + this.metrics = Arrays.copyOf(metrics, metrics.length); + this.fs = fs; + } + + @VisibleForTesting + SqaleRatingDecorator() { + this.sqaleRatingSettings = null; + this.metrics = null; + this.fs = null; + } + + public boolean shouldExecuteOnProject(Project project) { + return true; + } + + @DependsUpon + public List<Metric> dependsOnMetrics() { + return Lists.<Metric>newArrayList(CoreMetrics.TECHNICAL_DEBT, + // ncloc and complexity are the two possible metrics to be used to calculate the development cost + CoreMetrics.NCLOC, CoreMetrics.COMPLEXITY); + } + + @DependedUpon + public List<Metric> generatesMetrics() { + return Lists.<Metric>newArrayList(CoreMetrics.RATING, CoreMetrics.DEVELOPMENT_COST); + } + + public void decorate(Resource resource, DecoratorContext context) { + if (ResourceUtils.isPersistable(resource) && !ResourceUtils.isUnitTestClass(resource)) { + Long developmentCost = getDevelopmentCost(context); + context.saveMeasure(new Measure(CoreMetrics.DEVELOPMENT_COST, Long.toString(developmentCost))); + + long debt = getMeasureValue(context, CoreMetrics.TECHNICAL_DEBT); + double density = computeDensity(debt, developmentCost); + SqaleRatingGrid ratingGrid = new SqaleRatingGrid(sqaleRatingSettings.getRatingGrid()); + context.saveMeasure(createRatingMeasure(ratingGrid.getRatingForDensity(density))); + } + } + + private Measure createRatingMeasure(int rating) { + return new Measure(CoreMetrics.RATING).setIntValue(rating).setData(toRatingLetter(rating)); + } + + static String toRatingLetter(@Nullable Integer rating) { + if (rating != null) { + return SqaleRatingGrid.SqaleRating.buildFromIndex(rating).name(); + } + return null; + } + + private long getDevelopmentCost(DecoratorContext context) { + InputFile file = fs.inputFile(fs.predicates().hasRelativePath(context.getResource().getKey())); + if (file != null) { + String language = file.language(); + return getMeasureValue(context, sqaleRatingSettings.getSizeMetric(language, metrics)) * sqaleRatingSettings.getWorkUnitsBySizePoint(language); + } else { + Collection<Measure> childrenMeasures = context.getChildrenMeasures(CoreMetrics.DEVELOPMENT_COST); + Double sum = sum(childrenMeasures); + return sum.longValue(); + } + } + + private static Double sum(@Nullable Collection<Measure> measures) { + if (measures == null) { + return 0d; + } + double sum = 0d; + for (Measure measure : measures) { + if (measure.getData() != null) { + sum += Double.parseDouble(measure.getData()); + } + } + return sum; + } + + private long getMeasureValue(DecoratorContext context, Metric metric) { + Measure measure = context.getMeasure(metric); + if (measure != null) { + return measure.getValue().longValue(); + } + return 0; + } + + protected double computeDensity(double debt, double developmentCost) { + if (developmentCost != 0) { + return debt / developmentCost; + } + return 0; + } + +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingGrid.java b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingGrid.java new file mode 100644 index 00000000000..5db6ab6e2ab --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingGrid.java @@ -0,0 +1,80 @@ +/* + * 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.batch.debt; + +import org.sonar.api.utils.MessageException; + +import java.util.Arrays; + +class SqaleRatingGrid { + + private final double[] gridValues; + + public SqaleRatingGrid(double[] gridValues) { + this.gridValues = Arrays.copyOf(gridValues, gridValues.length); + } + + public double getGradeLowerBound(SqaleRating rating) { + if (rating.getIndex() > 1) { + return gridValues[rating.getIndex() - 2]; + } + return 0; + } + + 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); + } + + enum SqaleRating { + + E(5), + D(4), + C(3), + B(2), + A(1); + + private final int index; + + private SqaleRating(int index) { + this.index = index; + } + + public int getIndex() { + return index; + } + + public static SqaleRating buildFromIndex(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/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingSettings.java b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingSettings.java new file mode 100644 index 00000000000..acba0266e26 --- /dev/null +++ b/sonar-batch/src/main/java/org/sonar/batch/debt/SqaleRatingSettings.java @@ -0,0 +1,127 @@ +/* + * 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.batch.debt; + +import org.sonar.api.BatchComponent; +import org.sonar.api.CoreProperties; +import org.sonar.api.config.Settings; +import org.sonar.api.measures.Metric; + +public class SqaleRatingSettings implements BatchComponent { + + private final Settings settings; + + public SqaleRatingSettings(Settings settings) { + this.settings = settings; + } + + public double[] getRatingGrid() { + try { + String[] ratingGrades = settings.getStringArray(CoreProperties.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 '" + + CoreProperties.RATING_GRID_DEF_VALUES + "' and got '" + + settings.getString(CoreProperties.RATING_GRID) + "'", e); + } + } + + public long getWorkUnitsBySizePoint(String languageKey) { + try { + LanguageSpecificConfiguration languageSpecificConfig = getSpecificParametersForLanguage(languageKey); + if (languageSpecificConfig != null && languageSpecificConfig.getManDays() != null) { + return Long.parseLong(languageSpecificConfig.getManDays()); + } + return Long.parseLong(settings.getString(CoreProperties.MAN_DAYS_BY_SIZE_POINT)); + } catch (Exception e) { + throw new IllegalArgumentException("The value of the SQALE property '" + CoreProperties.MAN_DAYS_BY_SIZE_POINT + + "' is incorrect. Expected long but got '" + settings.getString(CoreProperties.MAN_DAYS_BY_SIZE_POINT) + "'", e); + } + } + + public Metric getSizeMetric(String languageKey, Metric[] metrics) { + LanguageSpecificConfiguration languageSpecificConfig = getSpecificParametersForLanguage(languageKey); + if (languageSpecificConfig != null && languageSpecificConfig.getMetric() != null) { + return getMetricForKey(languageSpecificConfig.getMetric(), metrics); + } + return getMetricForKey(settings.getString(CoreProperties.SIZE_METRIC), metrics); + } + + private Metric getMetricForKey(String sizeMetricKey, Metric[] metrics) { + for (Metric metric : metrics) { + if (metric.getKey().equals(sizeMetricKey)) { + return metric; + } + } + throw new IllegalArgumentException("The metric key used to define the SQALE size metric is unknown : '" + sizeMetricKey + "'"); + } + + private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) { + String[] languageConfigIndexes = settings.getStringArray(CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS); + for (String languageConfigIndex : languageConfigIndexes) { + String languagePropertyKey = CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY; + if (languageKey.equals(settings.getString(languagePropertyKey))) { + return LanguageSpecificConfiguration.create(settings, languageConfigIndex); + } + } + return null; + } + + private static class LanguageSpecificConfiguration { + + private final String language; + private final String manDays; + private final String metric; + + private LanguageSpecificConfiguration(String language, String manDays, String metric) { + this.language = language; + this.manDays = manDays; + this.metric = metric; + } + + static LanguageSpecificConfiguration create(Settings settings, String configurationId) { + + String configurationPrefix = CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS + "." + configurationId + "."; + + String language = settings.getString(configurationPrefix + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY); + String manDays = settings.getString(configurationPrefix + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY); + String metric = settings.getString(configurationPrefix + CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY); + + return new LanguageSpecificConfiguration(language, manDays, metric); + } + + String getLanguage() { + return language; + } + + String getManDays() { + return manDays; + } + + String getMetric() { + return metric; + } + } +} diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java index 0935b576062..455567e5e68 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java +++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java @@ -28,20 +28,13 @@ import org.sonar.api.batch.rule.CheckFactory; import org.sonar.api.platform.ComponentContainer; import org.sonar.api.resources.Project; import org.sonar.api.scan.filesystem.FileExclusions; -import org.sonar.batch.DefaultProjectClasspath; -import org.sonar.batch.DefaultSensorContext; -import org.sonar.batch.DefaultTimeMachine; -import org.sonar.batch.ProjectTree; -import org.sonar.batch.ResourceFilters; -import org.sonar.batch.ViolationFilters; +import org.sonar.batch.*; import org.sonar.batch.bootstrap.BatchExtensionDictionnary; import org.sonar.batch.bootstrap.ExtensionInstaller; import org.sonar.batch.bootstrap.ExtensionMatcher; import org.sonar.batch.bootstrap.ExtensionUtils; import org.sonar.batch.components.TimeMachineConfiguration; -import org.sonar.batch.debt.DebtDecorator; -import org.sonar.batch.debt.IssueChangelogDebtCalculator; -import org.sonar.batch.debt.NewDebtDecorator; +import org.sonar.batch.debt.*; import org.sonar.batch.events.EventBus; import org.sonar.batch.index.DefaultIndex; import org.sonar.batch.index.ResourcePersister; @@ -60,26 +53,8 @@ import org.sonar.batch.phases.PhasesTimeProfiler; import org.sonar.batch.qualitygate.GenerateQualityGateEvents; import org.sonar.batch.qualitygate.QualityGateProvider; import org.sonar.batch.qualitygate.QualityGateVerifier; -import org.sonar.batch.rule.ActiveRulesProvider; -import org.sonar.batch.rule.ModuleQProfiles; -import org.sonar.batch.rule.QProfileDecorator; -import org.sonar.batch.rule.QProfileEventsDecorator; -import org.sonar.batch.rule.QProfileSensor; -import org.sonar.batch.rule.QProfileVerifier; -import org.sonar.batch.rule.RulesProfileProvider; -import org.sonar.batch.scan.filesystem.ComponentIndexer; -import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem; -import org.sonar.batch.scan.filesystem.DeprecatedFileFilters; -import org.sonar.batch.scan.filesystem.ExclusionFilters; -import org.sonar.batch.scan.filesystem.FileIndexer; -import org.sonar.batch.scan.filesystem.FileSystemLogger; -import org.sonar.batch.scan.filesystem.InputFileBuilderFactory; -import org.sonar.batch.scan.filesystem.LanguageDetectionFactory; -import org.sonar.batch.scan.filesystem.ModuleFileSystemInitializer; -import org.sonar.batch.scan.filesystem.ModuleInputFileCache; -import org.sonar.batch.scan.filesystem.PreviousFileHashLoader; -import org.sonar.batch.scan.filesystem.ProjectFileSystemAdapter; -import org.sonar.batch.scan.filesystem.StatusDetectionFactory; +import org.sonar.batch.rule.*; +import org.sonar.batch.scan.filesystem.*; import org.sonar.batch.scan.report.JsonReport; import org.sonar.batch.scan2.AnalyzerOptimizer; import org.sonar.core.component.ScanPerspectives; @@ -189,6 +164,8 @@ public class ModuleScanContainer extends ComponentContainer { IssueChangelogDebtCalculator.class, DebtDecorator.class, NewDebtDecorator.class, + SqaleRatingDecorator.class, + SqaleRatingSettings.class, ScanPerspectives.class); } diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingDecoratorTest.java new file mode 100644 index 00000000000..1fe1574bffe --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingDecoratorTest.java @@ -0,0 +1,189 @@ +/* + * 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.batch.debt; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.sonar.api.CoreProperties; +import org.sonar.api.batch.DecoratorContext; +import org.sonar.api.batch.fs.internal.DefaultFileSystem; +import org.sonar.api.batch.fs.internal.DefaultInputFile; +import org.sonar.api.config.Settings; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Measure; +import org.sonar.api.measures.Metric; +import org.sonar.api.resources.File; +import org.sonar.api.resources.Project; +import org.sonar.api.resources.Qualifiers; +import org.sonar.api.test.IsMeasure; + +import static com.google.common.collect.Lists.newArrayList; +import static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class SqaleRatingDecoratorTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + static final Long ONE_DAY_IN_MINUTES = 8L * 60; + + Settings settings; + Metric[] metrics = {CoreMetrics.NCLOC, CoreMetrics.ABSTRACTNESS, CoreMetrics.COMPLEXITY}; + + @Mock + DecoratorContext context; + + DefaultFileSystem fs; + + File file = File.create("src/main/java/Foo.java"); + + SqaleRatingDecorator decorator; + + @Before + public void setUp() throws Exception { + settings = new Settings(); + + fs = new DefaultFileSystem(); + fs.add(new DefaultInputFile(file.getPath()) + .setLanguage("java") + .setFile(temp.newFile("Foo.java"))); + + decorator = new SqaleRatingDecorator(new SqaleRatingSettings(settings), metrics, fs); + } + + @Test + public void generates_metrics() throws Exception { + SqaleRatingDecorator decorator = new SqaleRatingDecorator(); + assertThat(decorator.generatesMetrics()).hasSize(2); + } + + @Test + public void depends_on_metrics() { + SqaleRatingDecorator decorator = new SqaleRatingDecorator(); + assertThat(decorator.dependsOnMetrics()).containsOnly(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.NCLOC, CoreMetrics.COMPLEXITY); + } + + @Test + public void execute_on_project() throws Exception { + SqaleRatingDecorator decorator = new SqaleRatingDecorator(); + assertThat(decorator.shouldExecuteOnProject(null)).isTrue(); + } + + @Test + public void not_execute_on_unit_test() throws Exception { + File resource = mock(File.class); + when(resource.getQualifier()).thenReturn(Qualifiers.UNIT_TEST_FILE); + DecoratorContext context = mock(DecoratorContext.class); + + SqaleRatingDecorator decorator = new SqaleRatingDecorator(); + decorator.decorate(resource, context); + + verify(context, never()).saveMeasure(any(Measure.class)); + } + + @Test + public void save_total_rating_c() { + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, 2 * ONE_DAY_IN_MINUTES); + settings.setProperty(CoreProperties.SIZE_METRIC, "ncloc"); + settings.setProperty(CoreProperties.RATING_GRID, "1, 10,20,50"); + + when(context.getResource()).thenReturn(file); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 10.0)); + when(context.getMeasure(CoreMetrics.TECHNICAL_DEBT)).thenReturn(new Measure(CoreMetrics.TECHNICAL_DEBT, 300.0 * ONE_DAY_IN_MINUTES)); + + decorator.decorate(file, context); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.RATING, 3.0))); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DEVELOPMENT_COST, "9600"))); + + verify(context).getMeasure(CoreMetrics.NCLOC); + } + + @Test + public void save_total_rating_a() { + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, 2 * ONE_DAY_IN_MINUTES); + settings.setProperty(CoreProperties.SIZE_METRIC, "ncloc"); + settings.setProperty(CoreProperties.RATING_GRID, "1, 10,20,50"); + + when(context.getResource()).thenReturn(file); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 10.0)); + when(context.getMeasure(CoreMetrics.TECHNICAL_DEBT)).thenReturn(new Measure(CoreMetrics.TECHNICAL_DEBT, 0.0)); + + decorator.decorate(file, context); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.RATING, 1.0))); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DEVELOPMENT_COST, "9600"))); + + verify(context).getMeasure(CoreMetrics.NCLOC); + } + + @Test + public void save_total_rating_e() { + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, 2 * ONE_DAY_IN_MINUTES); + settings.setProperty(CoreProperties.SIZE_METRIC, "ncloc"); + settings.setProperty(CoreProperties.RATING_GRID, "1, 10,20,50"); + + when(context.getResource()).thenReturn(file); + when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 10.0)); + when(context.getMeasure(CoreMetrics.TECHNICAL_DEBT)).thenReturn(new Measure(CoreMetrics.TECHNICAL_DEBT, 100000000000.0)); + + decorator.decorate(file, context); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.RATING, 5.0))); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DEVELOPMENT_COST, "9600"))); + + verify(context).getMeasure(CoreMetrics.NCLOC); + } + + @Test + public void save_total_rating_on_project() { + settings.setProperty(CoreProperties.RATING_GRID, "1, 10,20,50"); + + when(context.getResource()).thenReturn(new Project("Sample")); + when(context.getMeasure(CoreMetrics.TECHNICAL_DEBT)).thenReturn(new Measure(CoreMetrics.TECHNICAL_DEBT, 300.0 * ONE_DAY_IN_MINUTES)); + when(context.getChildrenMeasures(CoreMetrics.DEVELOPMENT_COST)).thenReturn(newArrayList(new Measure(CoreMetrics.DEVELOPMENT_COST, Double.toString(20.0 * ONE_DAY_IN_MINUTES)))); + + decorator.decorate(mock(File.class), context); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.RATING, 3.0))); + verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DEVELOPMENT_COST, "9600"))); + + verify(context, never()).getMeasure(CoreMetrics.NCLOC); + } + + @Test + public void translate_rating_to_letter() { + assertThat(SqaleRatingDecorator.toRatingLetter(null)).isNull(); + assertThat(SqaleRatingDecorator.toRatingLetter(1)).isEqualTo("A"); + assertThat(SqaleRatingDecorator.toRatingLetter(4)).isEqualTo("D"); + } + + @Test(expected = IllegalArgumentException.class) + public void test_rating_out_of_range() { + SqaleRatingDecorator.toRatingLetter(89); + } + +} diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingGridTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingGridTest.java new file mode 100644 index 00000000000..cabf747826a --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingGridTest.java @@ -0,0 +1,72 @@ +/* + * 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.batch.debt; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import static org.fest.assertions.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_grade_lower_bound() throws Exception { + assertThat(ratingGrid.getGradeLowerBound(SqaleRatingGrid.SqaleRating.A)).isEqualTo(0); + assertThat(ratingGrid.getGradeLowerBound(SqaleRatingGrid.SqaleRating.B)).isEqualTo(0.1); + assertThat(ratingGrid.getGradeLowerBound(SqaleRatingGrid.SqaleRating.C)).isEqualTo(0.2); + assertThat(ratingGrid.getGradeLowerBound(SqaleRatingGrid.SqaleRating.D)).isEqualTo(0.5); + assertThat(ratingGrid.getGradeLowerBound(SqaleRatingGrid.SqaleRating.E)).isEqualTo(1); + } + + @Test + public void return_rating_matching_density() throws Exception { + assertThat(ratingGrid.getRatingForDensity(0)).isEqualTo(1); + assertThat(ratingGrid.getRatingForDensity(0.05)).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() throws Exception { + throwable.expect(RuntimeException.class); + + ratingGrid.getRatingForDensity(-1); + } +} 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 new file mode 100644 index 00000000000..cd784f135fc --- /dev/null +++ b/sonar-batch/src/test/java/org/sonar/batch/debt/SqaleRatingSettingsTest.java @@ -0,0 +1,143 @@ +/* + * 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.batch.debt; + +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 org.sonar.api.measures.Metric; + +import static org.fest.assertions.Assertions.assertThat; + + +public class SqaleRatingSettingsTest { + + private static final Metric[] metrics = {CoreMetrics.NCLOC, CoreMetrics.ABSTRACTNESS, CoreMetrics.COMPLEXITY}; + + private Settings settings; + + @Rule + public ExpectedException throwable = ExpectedException.none(); + + @Before + public void setUp() { + settings = new Settings(); + } + + @Test + public void load_rating_grid() throws Exception { + 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() throws Exception { + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, "50"); + SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings); + + assertThat(configurationLoader.getWorkUnitsBySizePoint("defaultLanguage")).isEqualTo(50L); + } + + @Test + public void load_size_metric_for_language() throws Exception { + settings.setProperty(CoreProperties.SIZE_METRIC, "complexity"); + SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings); + + assertThat(configurationLoader.getSizeMetric("defaultLanguage", metrics)).isEqualTo(CoreMetrics.COMPLEXITY); + } + + @Test + public void load_overridden_values_for_language() throws Exception { + + 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.getSizeMetric(aLanguage, metrics)).isEqualTo(CoreMetrics.NCLOC); + assertThat(configurationLoader.getSizeMetric(anotherLanguage, metrics)).isEqualTo(CoreMetrics.COMPLEXITY); + assertThat(configurationLoader.getWorkUnitsBySizePoint(aLanguage)).isEqualTo(30L); + assertThat(configurationLoader.getWorkUnitsBySizePoint(anotherLanguage)).isEqualTo(40L); + } + + @Test + public void fail_on_invalid_rating_grid_configuration() throws Exception { + + throwable.expect(IllegalArgumentException.class); + settings.setProperty(CoreProperties.RATING_GRID, "a b c"); + SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings); + + configurationLoader.getRatingGrid(); + } + + @Test + public void fail_on_invalid_work_unit_value() throws Exception { + throwable.expect(IllegalArgumentException.class); + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, "a"); + SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings); + + configurationLoader.getSizeMetric("aLanguage", metrics); + } + + @Test + public void fail_on_unknown_metric_key() throws Exception { + throwable.expect(IllegalArgumentException.class); + settings.setProperty(CoreProperties.SIZE_METRIC, "unknown"); + SqaleRatingSettings configurationLoader = new SqaleRatingSettings(settings); + + configurationLoader.getSizeMetric("aLanguage", metrics); + } + + @Test + public void use_generic_value_when_specific_setting_is_missing() throws Exception { + String aLanguage = "aLanguage"; + + settings.setProperty(CoreProperties.SIZE_METRIC, "complexity"); + settings.setProperty(CoreProperties.MAN_DAYS_BY_SIZE_POINT, "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.getSizeMetric(aLanguage, metrics)).isEqualTo(CoreMetrics.COMPLEXITY); + assertThat(configurationLoader.getWorkUnitsBySizePoint(aLanguage)).isEqualTo(40L); + } +} |