Browse Source

SONAR-4715 Compute the new "Technical Debt" metric

tags/4.0
Julien Lancelot 10 years ago
parent
commit
42cab973f7
31 changed files with 1926 additions and 32 deletions
  1. 2
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
  2. 74
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Characteristic.java
  3. 5
    13
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Characteristicable.java
  4. 89
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Requirement.java
  5. 138
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtCalculator.java
  6. 132
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java
  7. 97
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtModel.java
  8. 68
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/WorkUnit.java
  9. 58
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/WorkUnitConverter.java
  10. 44
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/AbstractFunction.java
  11. 48
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/ConstantFunction.java
  12. 34
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/Function.java
  13. 52
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/Functions.java
  14. 49
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearFunction.java
  15. 48
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithOffsetFunction.java
  16. 49
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithThresholdFunction.java
  17. 13
    0
      plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties
  18. 115
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/RequirementTest.java
  19. 153
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtCalculatorTest.java
  20. 152
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecoratorTest.java
  21. 74
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtModelTest.java
  22. 47
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/WorkUnitConverterTest.java
  23. 68
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/ConstantFunctionTest.java
  24. 37
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/FunctionsTest.java
  25. 78
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearFunctionTest.java
  26. 68
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithOffsetFunctionTest.java
  27. 71
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithThresholdFunctionTest.java
  28. 6
    0
      sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java
  29. 29
    0
      sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java
  30. 1
    1
      sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java
  31. 27
    18
      sonar-server/src/main/java/org/sonar/server/technicaldebt/XMLImporter.java

+ 2
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java View File

@@ -41,6 +41,7 @@ import org.sonar.plugins.core.measurefilters.ProjectFilter;
import org.sonar.plugins.core.notifications.alerts.NewAlerts;
import org.sonar.plugins.core.security.ApplyProjectRolesDecorator;
import org.sonar.plugins.core.sensors.*;
import org.sonar.plugins.core.technicaldebt.TechnicalDebtDecorator;
import org.sonar.plugins.core.timemachine.*;
import org.sonar.plugins.core.web.Lcom4Viewer;
import org.sonar.plugins.core.web.TestsViewer;
@@ -326,6 +327,7 @@ public final class CorePlugin extends SonarPlugin {
extensions.addAll(IgnoreIssuesPlugin.getExtensions());
extensions.addAll(CoverageMeasurementFilter.getPropertyDefinitions());
extensions.addAll(PastSnapshotFinder.getPropertyDefinitions());
extensions.addAll(TechnicalDebtDecorator.extensions());
extensions.addAll(propertyDefinitions());

return extensions.build();

+ 74
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Characteristic.java View File

@@ -0,0 +1,74 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import com.google.common.collect.Lists;

import javax.annotation.Nullable;

import java.util.List;

public final class Characteristic implements Characteristicable {

private String key;
private org.sonar.api.qualitymodel.Characteristic characteristic;
private Characteristic parent = null;
private List<Characteristic> subCharacteristics = Lists.newArrayList();
private List<Requirement> requirements = Lists.newArrayList();

public Characteristic(org.sonar.api.qualitymodel.Characteristic c) {
this(c, null);
}

public Characteristic(org.sonar.api.qualitymodel.Characteristic c, @Nullable Characteristic parent) {
this.characteristic = c;
this.key = c.getKey();
this.parent = parent;
for (org.sonar.api.qualitymodel.Characteristic subc : c.getChildren()) {
if (subc.getEnabled()) {
if (subc.getRule() != null) {
requirements.add(new Requirement(subc, this));
} else {
subCharacteristics.add(new Characteristic(subc, this));
}
}
}
}

public String getKey() {
return key;
}

public List<Characteristic> getSubCharacteristics() {
return subCharacteristics;
}

public Characteristic getParent() {
return parent;
}

public List<Requirement> getRequirements() {
return requirements;
}

public org.sonar.api.qualitymodel.Characteristic toCharacteristic() {
return characteristic;
}
}

sonar-server/src/main/java/org/sonar/server/technicaldebt/XMLConstants.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Characteristicable.java View File

@@ -17,20 +17,12 @@
* 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.technicaldebt;
package org.sonar.plugins.core.technicaldebt;

public final class XMLConstants {
import org.sonar.api.qualitymodel.Characteristic;

private XMLConstants(){
// Utility class
}
public interface Characteristicable {

public static final String CHARACTERISTIC = "chc";
public static final String CHARACTERISTIC_KEY = "key";
public static final String CHARACTERISTIC_NAME = "name";
public static final String CHARACTERISTIC_DESCRIPTION = "desc";
public static final String PROPERTY = "prop";
public static final String PROPERTY_KEY = "key";
public static final String PROPERTY_VALUE = "val";
public static final String PROPERTY_TEXT_VALUE = "txt";
Characteristic toCharacteristic();
}

+ 89
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/Requirement.java View File

@@ -0,0 +1,89 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.sonar.api.rules.Rule;
import org.sonar.plugins.core.technicaldebt.functions.LinearFunction;
import org.sonar.plugins.core.technicaldebt.functions.LinearWithOffsetFunction;
import org.sonar.plugins.core.technicaldebt.functions.LinearWithThresholdFunction;

public class Requirement implements Characteristicable {

public static final String PROPERTY_REMEDIATION_FUNCTION = "remediationFunction";
public static final String PROPERTY_REMEDIATION_FACTOR = "remediationFactor";
public static final String PROPERTY_OFFSET = "offset";

private Rule rule;
private Characteristic parent;
private org.sonar.api.qualitymodel.Characteristic characteristic;
private String function;
private WorkUnit factor;
private WorkUnit offset;

public Requirement(org.sonar.api.qualitymodel.Characteristic requirement, Characteristic parent) {
this.characteristic = requirement;
this.rule = requirement.getRule();
this.parent = parent;

initFunction();
initFactor();
initOffset();
}

private void initFunction() {
function = characteristic.getPropertyTextValue(PROPERTY_REMEDIATION_FUNCTION, LinearFunction.FUNCTION_LINEAR);
}

private void initFactor() {
factor = WorkUnit.create(characteristic.getPropertyValue(PROPERTY_REMEDIATION_FACTOR, null),
characteristic.getPropertyTextValue(PROPERTY_REMEDIATION_FACTOR, null));
}

private void initOffset() {
if (LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET.equals(function) || LinearWithThresholdFunction.FUNCTION_LINEAR_WITH_THRESHOLD.equals(function)) {
offset = WorkUnit.create(characteristic.getPropertyValue(PROPERTY_OFFSET, null),
characteristic.getPropertyTextValue(PROPERTY_OFFSET, null));
}
}

public Rule getRule() {
return rule;
}

public Characteristic getParent() {
return parent;
}

public String getRemediationFunction() {
return function;
}

public WorkUnit getRemediationFactor() {
return factor;
}

public WorkUnit getOffset() {
return offset;
}

public org.sonar.api.qualitymodel.Characteristic toCharacteristic() {
return characteristic;
}
}

+ 138
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtCalculator.java View File

@@ -0,0 +1,138 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Maps;
import org.slf4j.LoggerFactory;
import org.sonar.api.BatchExtension;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.MeasuresFilters;
import org.sonar.api.measures.Metric;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.functions.Functions;

import java.util.Collection;
import java.util.List;
import java.util.Map;

/**
* Computes the remediation cost based on the quality and analysis models.
*/
public class TechnicalDebtCalculator implements BatchExtension {

private double total = 0.0;
private Map<Characteristic, Double> characteristicCosts = Maps.newHashMap();
private Map<Requirement, Double> requirementCosts = Maps.newHashMap();

private Functions functions;
private TechnicalDebtModel technicalDebtModel;

public TechnicalDebtCalculator(TechnicalDebtModel technicalDebtModel, Functions functions) {
this.technicalDebtModel = technicalDebtModel;
this.functions = functions;
}

public void compute(DecoratorContext context) {
reset();

// group violations by requirement
ListMultimap<Requirement, Violation> violationsByRequirement = groupViolations(context);

// the total cost is: cost(violations)
for (Requirement requirement : technicalDebtModel.getAllRequirements()) {
List<Violation> violations = violationsByRequirement.get(requirement);
double allViolationsCost = computeRemediationCost(CoreMetrics.TECHNICAL_DEBT, context, requirement, violations);
updateRequirementCosts(requirement, allViolationsCost);
}
}

public double getTotal() {
return total;
}

public Map<Characteristic, Double> getCharacteristicCosts() {
return characteristicCosts;
}

public Map<Requirement, Double> getRequirementCosts() {
return requirementCosts;
}

@VisibleForTesting
protected ListMultimap<Requirement, Violation> groupViolations(DecoratorContext context) {
ListMultimap<Requirement, Violation> violationsByRequirement = ArrayListMultimap.create();
for (Violation violation : context.getViolations()) {
String repositoryKey = violation.getRule().getRepositoryKey();
String key = violation.getRule().getKey();
Requirement requirement = technicalDebtModel.getRequirementByRule(repositoryKey, key);
if (requirement == null) {
LoggerFactory.getLogger(getClass()).debug("No technical debt requirement for: " + repositoryKey + "/" + key);
} else {
violationsByRequirement.put(requirement, violation);
}
}
return violationsByRequirement;
}

@VisibleForTesting
protected void updateRequirementCosts(Requirement requirement, double cost) {
requirementCosts.put(requirement, cost);
total += cost;
propagateCostInParents(characteristicCosts, requirement.getParent(), cost);
}

private double computeRemediationCost(Metric metric, DecoratorContext context, Requirement requirement, Collection<Violation> violations) {
double cost = 0.0;
if (violations != null) {
cost = functions.calculateCost(requirement, violations);
}

for (Measure measure : context.getChildrenMeasures(MeasuresFilters.characteristic(metric, requirement.toCharacteristic()))) {
if (measure.getCharacteristic() != null && measure.getCharacteristic().equals(requirement.toCharacteristic()) && measure.getValue() != null) {
cost += measure.getValue();
}
}
return cost;
}

private void reset() {
total = 0.0;
characteristicCosts.clear();
requirementCosts.clear();
}

private void propagateCostInParents(Map<Characteristic, Double> hierarchyMap, Characteristic characteristic, double cost) {
if (characteristic != null) {
Double parentCost = hierarchyMap.get(characteristic);
if (parentCost == null) {
hierarchyMap.put(characteristic, cost);
} else {
hierarchyMap.put(characteristic, cost + parentCost);
}
propagateCostInParents(hierarchyMap, characteristic.getParent(), cost);
}
}

}

+ 132
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java View File

@@ -0,0 +1,132 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import com.google.common.collect.ImmutableList;
import org.sonar.api.CoreProperties;
import org.sonar.api.batch.*;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.measures.Metric;
import org.sonar.api.measures.PersistenceMode;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
import org.sonar.api.resources.ResourceUtils;
import org.sonar.plugins.core.technicaldebt.functions.*;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

/**
* Decorator that computes the technical debt metric
*/
@DependsUpon(DecoratorBarriers.ISSUES_TRACKED)
public final class TechnicalDebtDecorator implements Decorator {

public static final int DECIMALS_PRECISION = 5;
private TechnicalDebtCalculator costCalculator;

public TechnicalDebtDecorator(TechnicalDebtCalculator costCalculator) {
this.costCalculator = costCalculator;
}

public boolean shouldExecuteOnProject(Project project) {
return true;
}

@DependedUpon
public List<Metric> generatesMetrics() {
return Arrays.asList(CoreMetrics.TECHNICAL_DEBT);
}

@SuppressWarnings("rawtypes")
public void decorate(Resource resource, DecoratorContext context) {
if (!ResourceUtils.isUnitTestClass(resource)) {
costCalculator.compute(context);
saveCostMeasures(context);
}
}

protected void saveCostMeasures(DecoratorContext context) {
context.saveMeasure(new Measure(CoreMetrics.TECHNICAL_DEBT, costCalculator.getTotal(), DECIMALS_PRECISION));
saveCharacteristicCosts(context);
saveRequirementCosts(context);
}

private void saveCharacteristicCosts(DecoratorContext context) {
for (Map.Entry<Characteristic, Double> entry : costCalculator.getCharacteristicCosts().entrySet()) {
saveCost(context, entry.getKey().toCharacteristic(), entry.getValue(), false);
}
}

private void saveRequirementCosts(DecoratorContext context) {
for (Map.Entry<Requirement, Double> entry : costCalculator.getRequirementCosts().entrySet()) {
saveCost(context, entry.getKey().toCharacteristic(), entry.getValue(), ResourceUtils.isEntity(context.getResource()));
}
}

protected void saveCost(DecoratorContext context, org.sonar.api.qualitymodel.Characteristic characteristic, Double value, boolean inMemory) {
// we need the value on projects (root or module) even if value==0 in order to display correctly the SQALE history chart (see SQALE-122)
// BUT we don't want to save zero-values for non top-characteristics (see SQALE-147)
if (value > 0.0 || (ResourceUtils.isProject(context.getResource()) && characteristic.getDepth() == org.sonar.api.qualitymodel.Characteristic.ROOT_DEPTH)) {
Measure measure = new Measure(CoreMetrics.TECHNICAL_DEBT);
measure.setCharacteristic(characteristic);
measure.setValue(value, DECIMALS_PRECISION);
if (inMemory) {
measure.setPersistenceMode(PersistenceMode.MEMORY);
}
context.saveMeasure(measure);
}
}

public static List<?> extensions() {
ImmutableList.Builder<Object> extensions = ImmutableList.builder();
extensions.addAll(definitions());
extensions.add(
// base components
TechnicalDebtModel.class, WorkUnitConverter.class, TechnicalDebtCalculator.class,

// functions
ConstantFunction.class, LinearFunction.class, LinearWithOffsetFunction.class, LinearWithThresholdFunction.class, Functions.class,

// decorator
TechnicalDebtDecorator.class
);

return extensions.build();
}

private static List<PropertyDefinition> definitions() {
return ImmutableList.of(
PropertyDefinition.builder(WorkUnitConverter.PROPERTY_HOURS_IN_DAY)
.name("Number of working hours in a day")
.defaultValue("" + WorkUnitConverter.DEFAULT_HOURS_IN_DAY)
.category(CoreProperties.CATEGORY_TECHNICAL_DEBT)
.build()
);
}

@Override
public String toString() {
return "Technical Debt decorator";
}
}

+ 97
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtModel.java View File

@@ -0,0 +1,97 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import org.sonar.api.BatchExtension;
import org.sonar.api.qualitymodel.Model;
import org.sonar.api.qualitymodel.ModelFinder;
import org.sonar.api.rules.Rule;
import org.sonar.api.utils.SonarException;

import java.util.Collection;
import java.util.List;
import java.util.Map;

public class TechnicalDebtModel implements BatchExtension {

// FIXME Use the same as in RegisterTechnicalDebtModel
public static final String MODEL_NAME = "TECHNICAL_DEBT";

private List<Characteristic> characteristics = Lists.newArrayList();
private Map<Rule, Requirement> requirementsByRule = Maps.newHashMap();

public TechnicalDebtModel(ModelFinder modelFinder) {
Model model = modelFinder.findByName(MODEL_NAME);
if (model == null) {
throw new SonarException("Can not find the model in database: " + MODEL_NAME);
}
init(model);
}

/**
* For unit tests
*/
private TechnicalDebtModel(Model model) {
init(model);
}

/**
* For unit tests
*/
public static TechnicalDebtModel create(Model model) {
return new TechnicalDebtModel(model);
}

private void init(Model model) {
for (org.sonar.api.qualitymodel.Characteristic characteristic : model.getRootCharacteristics()) {
if (characteristic.getEnabled()) {
Characteristic sc = new Characteristic(characteristic);
characteristics.add(sc);
registerRequirements(sc);
}
}
}

private void registerRequirements(Characteristic c) {
for (Requirement requirement : c.getRequirements()) {

if (requirement.getRule() != null) {
requirementsByRule.put(requirement.getRule(), requirement);
}
}
for (Characteristic subCharacteristic : c.getSubCharacteristics()) {
registerRequirements(subCharacteristic);
}
}

public List<Characteristic> getCharacteristics() {
return characteristics;
}

public Collection<Requirement> getAllRequirements() {
return requirementsByRule.values();
}

public Requirement getRequirementByRule(String repositoryKey, String key) {
return requirementsByRule.get(Rule.create().setUniqueKey(repositoryKey, key));
}
}

+ 68
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/WorkUnit.java View File

@@ -0,0 +1,68 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import javax.annotation.Nullable;

public final class WorkUnit {

public static final String DAYS = "d";
public static final String MINUTES = "mn";
public static final String HOURS = "h";
public static final String DEFAULT_UNIT = DAYS;
private static final String[] UNITS = {DAYS, MINUTES, HOURS};

public static final double DEFAULT_VALUE = 1.0;

private double value;
private String unit;

WorkUnit(double value, String unit) {
this.value = value;
this.unit = unit;
}

public double getValue() {
return value;
}

public String getUnit() {
return unit;
}

public static WorkUnit create(@Nullable Double value, @Nullable String unit) {
unit = StringUtils.defaultIfEmpty(unit, DEFAULT_UNIT);
if (!ArrayUtils.contains(UNITS, unit)) {
throw new IllegalArgumentException("Remediation factor unit can not be: " + unit + ". Possible values are " + ArrayUtils.toString(UNITS));
}
double d = value != null ? value : DEFAULT_VALUE;
if (d < 0.0) {
throw new IllegalArgumentException("Remediation factor can not be negative: " + d);
}
return new WorkUnit(d, unit);
}

public static WorkUnit createInDays(Double value) {
return create(value, DAYS);
}
}

+ 58
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/WorkUnitConverter.java View File

@@ -0,0 +1,58 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.apache.commons.configuration.Configuration;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.BatchExtension;

public final class WorkUnitConverter implements BatchExtension {

public static final int DEFAULT_HOURS_IN_DAY = 8;

public static final String PROPERTY_HOURS_IN_DAY = "sonar.technicalDebt.hoursInDay";

private int hoursInDay = DEFAULT_HOURS_IN_DAY;

public WorkUnitConverter(Configuration configuration) {
this.hoursInDay = configuration.getInt(PROPERTY_HOURS_IN_DAY, DEFAULT_HOURS_IN_DAY);
}

public int getHoursInDay() {
return hoursInDay;
}

public double toDays(WorkUnit factor) {
double result;
if (StringUtils.equals(WorkUnit.DAYS, factor.getUnit())) {
result = factor.getValue();

} else if (StringUtils.equals(WorkUnit.HOURS, factor.getUnit())) {
result = factor.getValue() / hoursInDay;

} else if (StringUtils.equals(WorkUnit.MINUTES, factor.getUnit())) {
result = factor.getValue() / (hoursInDay * 60.0);

} else {
throw new IllegalArgumentException("Unknown remediation factor unit: " + factor.getUnit());
}
return result;
}
}

+ 44
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/AbstractFunction.java View File

@@ -0,0 +1,44 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;

public abstract class AbstractFunction implements Function {

private WorkUnitConverter converter;

public AbstractFunction(WorkUnitConverter converter) {
this.converter = converter;
}

protected WorkUnitConverter getConverter() {
return converter;
}

public abstract String getKey();

public abstract double calculateCost(Requirement requirement, Collection<Violation> violations);

}

+ 48
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/ConstantFunction.java View File

@@ -0,0 +1,48 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;

public final class ConstantFunction extends AbstractFunction {

public static final String FUNCTION_CONSTANT_RESOURCE = "constant_resource";

public ConstantFunction(WorkUnitConverter converter) {
super(converter);
}

public String getKey() {
return FUNCTION_CONSTANT_RESOURCE;
}

public double calculateCost(Requirement requirement, Collection<Violation> violations) {
double cost = 0.0;
if (!violations.isEmpty()) {
cost = getConverter().toDays(requirement.getRemediationFactor());
}
return cost;
}

}

+ 34
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/Function.java View File

@@ -0,0 +1,34 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.BatchExtension;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;

import java.util.Collection;

public interface Function extends BatchExtension {

String getKey();

double calculateCost(Requirement requirement, Collection<Violation> violations);

}

+ 52
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/Functions.java View File

@@ -0,0 +1,52 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import com.google.common.collect.Maps;
import org.sonar.api.BatchExtension;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;

import java.util.Collection;
import java.util.Map;

public class Functions implements BatchExtension {

private final Map<String, Function> functionsByKey = Maps.newHashMap();

public Functions(final Function[] functions) {
for (Function function : functions) {
functionsByKey.put(function.getKey(), function);
}
}

Function getFunction(String key) {
return functionsByKey.get(key);
}

public Function getFunction(Requirement requirement) {
return getFunction(requirement.getRemediationFunction());
}

public double calculateCost(Requirement requirement, Collection<Violation> violations) {
return getFunction(requirement).calculateCost(requirement, violations);
}

}

+ 49
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearFunction.java View File

@@ -0,0 +1,49 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;

public class LinearFunction extends AbstractFunction {

public static final String FUNCTION_LINEAR = "linear";

public static final double DEFAULT_VIOLATION_COST = 1.0;

public LinearFunction(WorkUnitConverter converter) {
super(converter);
}

public String getKey() {
return FUNCTION_LINEAR;
}

public double calculateCost(Requirement requirement, Collection<Violation> violations) {
double points = 0.0;
for (Violation violation : violations) {
points += (violation.getCost() != null ? violation.getCost() : DEFAULT_VIOLATION_COST);
}
return points * getConverter().toDays(requirement.getRemediationFactor());
}
}

+ 48
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithOffsetFunction.java View File

@@ -0,0 +1,48 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;

public final class LinearWithOffsetFunction extends LinearFunction {

public static final String FUNCTION_LINEAR_WITH_OFFSET = "linear_offset";

public LinearWithOffsetFunction(WorkUnitConverter converter) {
super(converter);
}

public String getKey() {
return FUNCTION_LINEAR_WITH_OFFSET;
}

public double calculateCost(Requirement requirement, Collection<Violation> violations) {
if (violations.isEmpty()) {
return 0.0;
}
double minimunCost = getConverter().toDays(requirement.getOffset());
return minimunCost + super.calculateCost(requirement, violations);
}

}

+ 49
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithThresholdFunction.java View File

@@ -0,0 +1,49 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;

public final class LinearWithThresholdFunction extends LinearFunction {

public static final String FUNCTION_LINEAR_WITH_THRESHOLD = "linear_threshold";

public LinearWithThresholdFunction(WorkUnitConverter converter) {
super(converter);
}

public String getKey() {
return FUNCTION_LINEAR_WITH_THRESHOLD;
}

public double calculateCost(Requirement requirement, Collection<Violation> violations) {
if (violations.isEmpty()) {
return 0.0;
}
double thresholdCost = getConverter().toDays(requirement.getOffset());
double violationsCost = super.calculateCost(requirement, violations);
return violationsCost > thresholdCost ? violationsCost : thresholdCost;
}

}

+ 13
- 0
plugins/sonar-core-plugin/src/main/resources/org/sonar/l10n/core.properties View File

@@ -841,6 +841,7 @@ property.category.exclusions.coverage=Code Coverage
property.category.exclusions.coverage.description=Configure files which should be considered for code coverage.
property.sonar.coverage.exclusions.name=Coverage Exclusions
property.sonar.coverage.exclusions.description=Patterns used to exclude some files from coverage report.
property.category.technicalDebt=Technical Debt
property.error.notBoolean=Valid options are "true" and "false"
property.error.notInteger=Only digits are allowed
property.error.notFloat=Not a floating point number
@@ -2252,6 +2253,18 @@ metric.reopened_issues.description=Reopened issues
metric.confirmed_issues.name=Confirmed issues
metric.confirmed_issues.description=Confirmed issues


#--------------------------------------------------------------------------------------------------------------------
#
# TECHNICAL DEBT METRICS
#
#--------------------------------------------------------------------------------------------------------------------

metric.technical_debt.name=Technical Debt
metric.technical_debt.description=The technical debt is the total effort required to fully adhere all quality requirements defined by a user.



#------------------------------------------------------------------------------
#
# GLOBAL PERMISSIONS

+ 115
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/RequirementTest.java View File

@@ -0,0 +1,115 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.junit.Test;
import org.sonar.api.qualitymodel.Characteristic;
import org.sonar.plugins.core.technicaldebt.functions.ConstantFunction;
import org.sonar.plugins.core.technicaldebt.functions.LinearFunction;
import org.sonar.plugins.core.technicaldebt.functions.LinearWithOffsetFunction;
import org.sonar.plugins.core.technicaldebt.functions.LinearWithThresholdFunction;

import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;

public class RequirementTest {

@Test
public void defaultFactor() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFactor().getValue(), is(WorkUnit.DEFAULT_VALUE));
assertThat(requirement.getRemediationFactor().getUnit(), is(WorkUnit.DEFAULT_UNIT));
}

@Test
public void testOverriddenFactor() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FACTOR, 3.14);
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFactor().getValue(), is(3.14));
assertThat(requirement.getRemediationFactor().getUnit(), is(WorkUnit.DAYS));
}

@Test
public void defaultFunctionIsLinear() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(LinearFunction.FUNCTION_LINEAR));
assertThat(requirement.getOffset(), is(nullValue()));
}

@Test
public void testOverriddenFunction() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FUNCTION, ConstantFunction.FUNCTION_CONSTANT_RESOURCE);
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(ConstantFunction.FUNCTION_CONSTANT_RESOURCE));
}

@Test
public void testDefaultLinearWithOffset() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FUNCTION, LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET);
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET));
assertThat(requirement.getRemediationFactor().getValue(), is(WorkUnit.DEFAULT_VALUE));
assertThat(requirement.getRemediationFactor().getUnit(), is(WorkUnit.DEFAULT_UNIT));
assertThat(requirement.getOffset().getValue(), is(WorkUnit.DEFAULT_VALUE));
assertThat(requirement.getOffset().getUnit(), is(WorkUnit.DEFAULT_UNIT));
}

@Test
public void testCustomizedLinearWithOffset() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FUNCTION, LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET);
persistedRequirement.setProperty(Requirement.PROPERTY_OFFSET, 5.0);
persistedRequirement.addProperty(persistedRequirement.getProperty(Requirement.PROPERTY_OFFSET).setTextValue("h"));
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET));
assertThat(requirement.getOffset().getValue(), is(5.0));
assertThat(requirement.getOffset().getUnit(), is(WorkUnit.HOURS));
}

@Test
public void testDefaultLinearWithThreshold() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FUNCTION, LinearWithThresholdFunction.FUNCTION_LINEAR_WITH_THRESHOLD);
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(LinearWithThresholdFunction.FUNCTION_LINEAR_WITH_THRESHOLD));
assertThat(requirement.getRemediationFactor().getValue(), is(WorkUnit.DEFAULT_VALUE));
assertThat(requirement.getRemediationFactor().getUnit(), is(WorkUnit.DEFAULT_UNIT));
assertThat(requirement.getOffset().getValue(), is(WorkUnit.DEFAULT_VALUE));
assertThat(requirement.getOffset().getUnit(), is(WorkUnit.DEFAULT_UNIT));
}

@Test
public void testCustomizedLinearWithThreshold() {
Characteristic persistedRequirement = Characteristic.createByName("Efficiency");
persistedRequirement.setProperty(Requirement.PROPERTY_REMEDIATION_FUNCTION, LinearWithThresholdFunction.FUNCTION_LINEAR_WITH_THRESHOLD);
persistedRequirement.setProperty(Requirement.PROPERTY_OFFSET, 5.0);
persistedRequirement.addProperty(persistedRequirement.getProperty(Requirement.PROPERTY_OFFSET).setTextValue("h"));
Requirement requirement = new Requirement(persistedRequirement, null);
assertThat(requirement.getRemediationFunction(), is(LinearWithThresholdFunction.FUNCTION_LINEAR_WITH_THRESHOLD));
assertThat(requirement.getOffset().getValue(), is(5.0));
assertThat(requirement.getOffset().getUnit(), is(WorkUnit.HOURS));
}
}

+ 153
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtCalculatorTest.java View File

@@ -0,0 +1,153 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import org.apache.commons.lang.time.DateUtils;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.measures.MeasuresFilter;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.functions.Functions;

import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;

public class TechnicalDebtCalculatorTest {

private static final Date NOW = new Date(System.currentTimeMillis());
private static final Date YESTERDAY = DateUtils.addDays(NOW, -1);
private static final Date LAST_MONTH = DateUtils.addMonths(NOW, -1);

private TechnicalDebtModel technicalDebtModel;
private Functions functions;
private TechnicalDebtCalculator remediationCostCalculator;

@Before
public void initMocks() {
technicalDebtModel = mock(TechnicalDebtModel.class);
functions = mock(Functions.class);
remediationCostCalculator = new TechnicalDebtCalculator(technicalDebtModel, functions);
}

@Test
public void group_violations_by_requirement() throws Exception {

Requirement requirement1 = mock(Requirement.class);
Requirement requirement2 = mock(Requirement.class);

Violation violation1 = buildViolation("rule1", "repo1", NOW);
Violation violation2 = buildViolation("rule1", "repo1", NOW);
Violation violation3 = buildViolation("rule2", "repo2", NOW);
Violation violation4 = buildViolation("unmatchable", "repo2", NOW);

List<Violation> violations = Lists.newArrayList(violation1, violation2, violation3, violation4);

stub(technicalDebtModel.getRequirementByRule("repo1", "rule1")).toReturn(requirement1);
stub(technicalDebtModel.getRequirementByRule("repo2", "rule2")).toReturn(requirement2);

DecoratorContext context = mock(DecoratorContext.class);
when(context.getViolations()).thenReturn(violations);

ListMultimap<Requirement, Violation> groupedViolations = remediationCostCalculator.groupViolations(context);

assertThat(groupedViolations.keySet().size()).isEqualTo(2);
assertThat(groupedViolations.get(requirement1)).containsExactly(violation1, violation2);
assertThat(groupedViolations.get(requirement2)).containsExactly(violation3);
}

@Test
public void add_cost_with_no_parent() throws Exception {

double requirementCost = 1.0;

Requirement requirement = mock(Requirement.class);
when(requirement.getParent()).thenReturn(null);

remediationCostCalculator.updateRequirementCosts(requirement, requirementCost);

assertThat(remediationCostCalculator.getRequirementCosts().get(requirement)).isEqualTo(requirementCost);
assertThat(remediationCostCalculator.getTotal()).isEqualTo(requirementCost);
}

@Test
public void add_cost_and_propagate_to_parents() throws Exception {

double requirementCost = 1.0;

Characteristic parentCharacteristic = new Characteristic(org.sonar.api.qualitymodel.Characteristic.create());

Characteristic characteristic = new Characteristic(org.sonar.api.qualitymodel.Characteristic.create(), parentCharacteristic);

Requirement requirement = mock(Requirement.class);
when(requirement.getParent()).thenReturn(characteristic);

remediationCostCalculator.updateRequirementCosts(requirement, requirementCost);

assertThat(remediationCostCalculator.getRequirementCosts().get(requirement)).isEqualTo(requirementCost);
assertThat(remediationCostCalculator.getCharacteristicCosts().get(characteristic)).isEqualTo(requirementCost);
assertThat(remediationCostCalculator.getCharacteristicCosts().get(parentCharacteristic)).isEqualTo(requirementCost);
}

@Test
public void compute_totals_costs() throws Exception {

Requirement requirement1 = mock(Requirement.class);
Requirement requirement2 = mock(Requirement.class);

Violation violation1 = buildViolation("rule1", "repo1", NOW);
Violation violation2 = buildViolation("rule1", "repo1", NOW);
Violation violation3 = buildViolation("rule2", "repo2", YESTERDAY);
Violation violation4 = buildViolation("rule2", "repo2", LAST_MONTH);

List<Violation> violations = Lists.newArrayList(violation1, violation2, violation3, violation4);

stub(technicalDebtModel.getRequirementByRule("repo1", "rule1")).toReturn(requirement1);
stub(technicalDebtModel.getRequirementByRule("repo2", "rule2")).toReturn(requirement2);
stub(technicalDebtModel.getAllRequirements()).toReturn(Lists.newArrayList(requirement1, requirement2));

stub(functions.calculateCost(any(Requirement.class), any(Collection.class))).toReturn(1.0);

DecoratorContext context = mock(DecoratorContext.class);
stub(context.getViolations()).toReturn(violations);
stub(context.getChildrenMeasures(any(MeasuresFilter.class))).toReturn(Collections.EMPTY_LIST);

remediationCostCalculator.compute(context);

assertThat(remediationCostCalculator.getTotal()).isEqualTo(2.0);
}

private Violation buildViolation(String ruleKey, String repositoryKey, Date creationDate) {
Violation violation = mock(Violation.class);
stub(violation.getRule()).toReturn(Rule.create(repositoryKey, ruleKey));
stub(violation.getCreatedAt()).toReturn(creationDate);
return violation;
}
}


+ 152
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecoratorTest.java View File

@@ -0,0 +1,152 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.junit.Test;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.measures.Measure;
import org.sonar.api.qualitymodel.Characteristic;
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 org.fest.assertions.Assertions.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.*;

public class TechnicalDebtDecoratorTest {

@Test
public void generates_metrics() throws Exception {
TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);
assertThat(decorator.generatesMetrics()).hasSize(1);
}

@Test
public void execute_on_project() throws Exception {
TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);
assertThat(decorator.shouldExecuteOnProject(null)).isTrue();
}

@Test
public void execute_on_source_file() throws Exception {
TechnicalDebtCalculator costCalculator = mock(TechnicalDebtCalculator.class);
File resource = mock(File.class);
DecoratorContext context = mock(DecoratorContext.class);

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(costCalculator);
decorator.decorate(resource, context);

verify(costCalculator, times(1)).compute(context);
}

@Test
public void not_execute_on_unit_test() throws Exception {
TechnicalDebtCalculator costCalculator = mock(TechnicalDebtCalculator.class);
File resource = mock(File.class);
when(resource.getQualifier()).thenReturn(Qualifiers.UNIT_TEST_FILE);
DecoratorContext context = mock(DecoratorContext.class);

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(costCalculator);
decorator.decorate(resource, context);

verify(costCalculator, never()).compute(context);
}

@Test
public void save_cost_measures() {
TechnicalDebtCalculator costCalulator = mock(TechnicalDebtCalculator.class);
when(costCalulator.getTotal()).thenReturn(60.0);

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(costCalulator);
DecoratorContext context = mock(DecoratorContext.class);

decorator.saveCostMeasures(context);

verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.TECHNICAL_DEBT, 60.0)));
}

@Test
public void always_save_cost_for_positive_values() throws Exception {
TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);

// for a project
DecoratorContext context = mock(DecoratorContext.class);
when(context.getResource()).thenReturn(new Project("foo"));
decorator.saveCost(context, null, 12.0, false);
verify(context, times(1)).saveMeasure(new Measure(CoreMetrics.TECHNICAL_DEBT));

// or for a file
context = mock(DecoratorContext.class);
when(context.getResource()).thenReturn(new File("foo"));
decorator.saveCost(context, null, 12.0, false);
verify(context, times(1)).saveMeasure(new Measure(CoreMetrics.TECHNICAL_DEBT));
}

@Test
public void always_save_cost_for_project_if_top_characteristic() throws Exception {
DecoratorContext context = mock(DecoratorContext.class);
when(context.getResource()).thenReturn(new Project("foo"));
// this is a top characteristic
Characteristic topCharacteristic = Characteristic.create();

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);

decorator.saveCost(context, topCharacteristic, 0.0, true);
verify(context, times(1)).saveMeasure(new Measure(CoreMetrics.TECHNICAL_DEBT).setCharacteristic(topCharacteristic));
}

/**
* SQALE-147
*/
@Test
public void never_save_cost_for_project_if_not_top_characteristic() throws Exception {
DecoratorContext context = mock(DecoratorContext.class);
when(context.getResource()).thenReturn(new Project("foo"));
Characteristic topCharacteristic = Characteristic.create();
Characteristic childCharacteristic = Characteristic.create();
topCharacteristic.addChild(childCharacteristic);

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);

decorator.saveCost(context, childCharacteristic, 0.0, true);
verify(context, never()).saveMeasure(any(Measure.class));
}

@Test
public void not_save_cost_for_file_if_zero() throws Exception {
DecoratorContext context = mock(DecoratorContext.class);
when(context.getResource()).thenReturn(new File("foo"));

TechnicalDebtDecorator decorator = new TechnicalDebtDecorator(null);

decorator.saveCost(context, null, 0.0, true);
verify(context, never()).saveMeasure(new Measure(CoreMetrics.TECHNICAL_DEBT));
}

@Test
public void check_extensions() {
assertThat(TechnicalDebtDecorator.extensions()).hasSize(1 /* properties */ + 9 /* extensions */);
}

}

+ 74
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtModelTest.java View File

@@ -0,0 +1,74 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.junit.Test;
import org.sonar.api.qualitymodel.Characteristic;
import org.sonar.api.qualitymodel.Model;
import org.sonar.api.qualitymodel.ModelFinder;
import org.sonar.api.rules.Rule;

import static org.fest.assertions.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class TechnicalDebtModelTest {

private Characteristic disabledCharacteristic = Characteristic.createByKey("DISABLED", "Disabled").setEnabled(false);

@Test
public void load_model() {
ModelFinder modelFinder = mock(ModelFinder.class);
Model persistedModel = createModel();
when(modelFinder.findByName(TechnicalDebtModel.MODEL_NAME)).thenReturn(persistedModel);

TechnicalDebtModel technicalDebtModel = new TechnicalDebtModel(modelFinder);

assertThat(technicalDebtModel.getCharacteristics()).hasSize(2);
assertThat(technicalDebtModel.getAllRequirements()).hasSize(1);
assertThat(technicalDebtModel.getRequirementByRule("repo", "check1").getRule().getKey()).isEqualTo("check1");
assertThat(technicalDebtModel.getRequirementByRule("repo", "check1").getParent().getKey()).isEqualTo("CPU_EFFICIENCY");

// ignore disabled characteristics/requirements
assertThat(technicalDebtModel.getCharacteristics()).excludes(disabledCharacteristic);
assertThat(technicalDebtModel.getRequirementByRule("repo", "check2")).isNull();
}

private Model createModel() {
Model model = Model.createByName(TechnicalDebtModel.MODEL_NAME);
Characteristic efficiency = model.createCharacteristicByKey("EFFICIENCY", "Efficiency");
model.createCharacteristicByKey("TESTABILITY", "Testability");

Characteristic cpuEfficiency = model.createCharacteristicByKey("CPU_EFFICIENCY", "CPU Efficiency");
efficiency.addChild(cpuEfficiency);

Characteristic requirement1 = model.createCharacteristicByRule(Rule.create("repo", "check1", "Check One"));
cpuEfficiency.addChild(requirement1);

// disabled requirement
Characteristic requirement2 = model.createCharacteristicByRule(Rule.create("repo", "check2", "Check Two"));
requirement2.setEnabled(false);
cpuEfficiency.addChild(requirement2);

// disabled characteristic
model.addCharacteristic(disabledCharacteristic);
return model;
}
}

+ 47
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/WorkUnitConverterTest.java View File

@@ -0,0 +1,47 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt;

import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class WorkUnitConverterTest {

@Test
public void shouldUseDefaultConfiguration() {
WorkUnitConverter converter = new WorkUnitConverter(new PropertiesConfiguration());
assertThat(converter.getHoursInDay(), is(WorkUnitConverter.DEFAULT_HOURS_IN_DAY));
}

@Test
public void shouldConvertValueToDays() {
PropertiesConfiguration configuration = new PropertiesConfiguration();
configuration.setProperty(WorkUnitConverter.PROPERTY_HOURS_IN_DAY, "12");
WorkUnitConverter converter = new WorkUnitConverter(configuration);

assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.DAYS)), is(6.0));
assertThat(converter.toDays(WorkUnit.create(6.0, WorkUnit.HOURS)), is(6.0 / 12.0));
assertThat(converter.toDays(WorkUnit.create(60.0 , WorkUnit.MINUTES)), is(1.0/12.0));
}

}

+ 68
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/ConstantFunctionTest.java View File

@@ -0,0 +1,68 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import com.google.common.collect.Lists;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnit;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;
import java.util.Collections;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class ConstantFunctionTest {

private Requirement requirement;
private Function function;

@Before
public void before() {
function = new ConstantFunction(new WorkUnitConverter(new PropertiesConfiguration()));
requirement = mock(Requirement.class);
when(requirement.getRemediationFactor()).thenReturn(WorkUnit.createInDays(3.14));
}

@Test
public void zeroIfNoViolations() {
assertThat(function.calculateCost(requirement, Collections.<Violation>emptyList()), is(0.0));
}

@Test
public void countAsIfSingleViolation() {
Collection<Violation> violations = Lists.newArrayList();

Rule rule = Rule.create("checkstyle", "foo", "Foo");
violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(3.14));

violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(3.14));
}
}

+ 37
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/FunctionsTest.java View File

@@ -0,0 +1,37 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import org.junit.Test;

import static org.fest.assertions.Assertions.assertThat;

public class FunctionsTest {

@Test
public void registerFunctions() {
Functions functions = new Functions(new Function[]{new LinearFunction(null), new LinearWithOffsetFunction(null),
new ConstantFunction(null)});
assertThat(functions.getFunction(LinearFunction.FUNCTION_LINEAR)).isInstanceOf(LinearFunction.class);
assertThat(functions.getFunction(LinearWithOffsetFunction.FUNCTION_LINEAR_WITH_OFFSET)).isInstanceOf(LinearWithOffsetFunction.class);
assertThat(functions.getFunction(ConstantFunction.FUNCTION_CONSTANT_RESOURCE)).isInstanceOf(ConstantFunction.class);
assertThat(functions.getFunction("foo")).isNull();
}
}

+ 78
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearFunctionTest.java View File

@@ -0,0 +1,78 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import com.google.common.collect.Lists;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnit;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;
import java.util.Collections;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class LinearFunctionTest {

private Requirement requirement;
private Function function = new LinearFunction(new WorkUnitConverter(new PropertiesConfiguration()));

@Before
public void before() {
requirement = mock(Requirement.class);
when(requirement.getRemediationFactor()).thenReturn(WorkUnit.createInDays(3.14));
}

@Test
public void zeroIfNoViolations() {
assertThat(function.calculateCost(requirement, Collections.<Violation>emptyList()), is(0.0));
}

@Test
public void countEveryViolation() {
Collection<Violation> violations = Lists.newArrayList();

Rule rule = Rule.create("checkstyle", "foo", "Foo");
violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(3.14));

violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(3.14 * 2));
}

@Test
public void usePointsWhenAvailable() {
Collection<Violation> violations = Lists.newArrayList();

Rule rule = Rule.create("checkstyle", "foo", "Foo");
violations.add(new Violation(rule).setCost(20.5));
violations.add(new Violation(rule).setCost(3.8));
violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(3.14 * (20.5 + 3.8 + 1)));
}
}

+ 68
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithOffsetFunctionTest.java View File

@@ -0,0 +1,68 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import com.google.common.collect.Lists;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnit;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;
import java.util.Collections;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class LinearWithOffsetFunctionTest {

private Requirement requirement;
private Function function = new LinearWithOffsetFunction(new WorkUnitConverter(new PropertiesConfiguration()));

@Before
public void before() {
requirement = mock(Requirement.class);
when(requirement.getRemediationFactor()).thenReturn(WorkUnit.createInDays(3.14));
when(requirement.getOffset()).thenReturn(WorkUnit.createInDays(2.12));
}

@Test
public void zeroIfNoViolations() {
assertThat(function.calculateCost(requirement, Collections.<Violation>emptyList()), is(0.0));
}

@Test
public void countEveryViolation() {
Collection<Violation> violations = Lists.newArrayList();

Rule rule = Rule.create("checkstyle", "foo", "Foo");
violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(2.12 + 3.14));

violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(2.12 + 3.14 * 2));
}
}

+ 71
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/functions/LinearWithThresholdFunctionTest.java View File

@@ -0,0 +1,71 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 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.plugins.core.technicaldebt.functions;

import com.google.common.collect.Lists;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.Violation;
import org.sonar.plugins.core.technicaldebt.Requirement;
import org.sonar.plugins.core.technicaldebt.WorkUnit;
import org.sonar.plugins.core.technicaldebt.WorkUnitConverter;

import java.util.Collection;
import java.util.Collections;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class LinearWithThresholdFunctionTest {

private Requirement requirement;
private Function function = new LinearWithThresholdFunction(new WorkUnitConverter(new PropertiesConfiguration()));

@Before
public void before() {
requirement = mock(Requirement.class);
when(requirement.getRemediationFactor()).thenReturn(WorkUnit.createInDays(2.0));
when(requirement.getOffset()).thenReturn(WorkUnit.createInDays(5.0));
}

@Test
public void zeroIfNoViolations() {
assertThat(function.calculateCost(requirement, Collections.<Violation>emptyList()), is(0.0));
}

@Test
public void countEveryViolationAndCheckThreshold() {
Collection<Violation> violations = Lists.newArrayList();

Rule rule = Rule.create("checkstyle", "foo", "Foo");
violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(5.0));

violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(5.0));

violations.add(new Violation(rule));
assertThat(function.calculateCost(requirement, violations), is(6.0));
}
}

+ 6
- 0
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java View File

@@ -121,6 +121,12 @@ public interface CoreProperties {
*/
String CATEGORY_LICENSES = "licenses";

/**
* @since 4.0
*/
String CATEGORY_TECHNICAL_DEBT = "technicalDebt";


/* Global settings */
String SONAR_HOME = "SONAR_HOME";
String PROJECT_BRANCH_PROPERTY = "sonar.branch";

+ 29
- 0
sonar-plugin-api/src/main/java/org/sonar/api/measures/CoreMetrics.java View File

@@ -55,6 +55,11 @@ public final class CoreMetrics {
public static String DOMAIN_DUPLICATION = "Duplication";
public static String DOMAIN_DESIGN = "Design";

/**
* @since 4.0
*/
public static String DOMAIN_TECHNICAL_DEBT = "Technical Debt";

public static final String LINES_KEY = "lines";
public static final Metric LINES = new Metric.Builder(LINES_KEY, "Lines", Metric.ValueType.INT)
.setDescription("Lines")
@@ -2009,6 +2014,30 @@ public final class CoreMetrics {
.setHidden(true)
.create();


// --------------------------------------------------------------------------------------------------------------------
//
// TECHNICAL DEBT
//
// --------------------------------------------------------------------------------------------------------------------

/**
* @since 4.0
*/
public static final String TECHNICAL_DEBT_KEY = "technical_debt";

/**
* @since 4.0
*/
public static final Metric TECHNICAL_DEBT = new Metric.Builder(TECHNICAL_DEBT_KEY, "Technical Debt", Metric.ValueType.FLOAT)
.setDomain(DOMAIN_TECHNICAL_DEBT)
.setDirection(Metric.DIRECTION_WORST)
.setOptimizedBestValue(true)
.setBestValue(0.0)
.setQualitative(true)
.create();


// --------------------------------------------------------------------------------------------------------------------
//
// FILE DATA

+ 1
- 1
sonar-plugin-api/src/test/java/org/sonar/api/resources/CoreMetricsTest.java View File

@@ -31,7 +31,7 @@ public class CoreMetricsTest {
@Test
public void shouldReadMetricsFromClassReflection() {
List<Metric> metrics = CoreMetrics.getMetrics();
assertThat(metrics).hasSize(148);
assertThat(metrics).hasSize(149);
assertThat(metrics).contains(CoreMetrics.NCLOC, CoreMetrics.DIRECTORIES);
}
}

+ 27
- 18
sonar-server/src/main/java/org/sonar/server/technicaldebt/XMLImporter.java View File

@@ -46,22 +46,31 @@ public class XMLImporter implements ServerExtension {

private static final Logger LOG = LoggerFactory.getLogger(XMLImporter.class);

private static final String CHARACTERISTIC = "chc";
private static final String CHARACTERISTIC_KEY = "key";
private static final String CHARACTERISTIC_NAME = "name";
private static final String CHARACTERISTIC_DESCRIPTION = "desc";
private static final String PROPERTY = "prop";
private static final String PROPERTY_KEY = "key";
private static final String PROPERTY_VALUE = "val";
private static final String PROPERTY_TEXT_VALUE = "txt";

public Model importXML(String xml, ValidationMessages messages, RuleCache ruleCache) {
return importXML(new StringReader(xml), messages, ruleCache);
}

public Model importXML(Reader xml, ValidationMessages messages, RuleCache repositoryCache) {
Model sqale = Model.createByName(RegisterTechnicalDebtModel.TECHNICAL_DEBT_MODEL);
Model model = Model.createByName(RegisterTechnicalDebtModel.TECHNICAL_DEBT_MODEL);
try {
SMInputFactory inputFactory = initStax();
SMHierarchicCursor cursor = inputFactory.rootElementCursor(xml);

// advance to <sqale>
cursor.advance();
SMInputCursor chcCursor = cursor.childElementCursor(XMLConstants.CHARACTERISTIC);
SMInputCursor chcCursor = cursor.childElementCursor(CHARACTERISTIC);

while (chcCursor.getNext() != null) {
processCharacteristic(sqale, chcCursor, messages, repositoryCache);
processCharacteristic(model, chcCursor, messages, repositoryCache);
}

cursor.getStreamReader().closeCompletely();
@@ -70,7 +79,7 @@ public class XMLImporter implements ServerExtension {
LOG.error("XML is not valid", e);
messages.addErrorText("XML is not valid: " + e.getMessage());
}
return sqale;
return model;
}

private SMInputFactory initStax() {
@@ -82,7 +91,7 @@ public class XMLImporter implements ServerExtension {
return new SMInputFactory(xmlFactory);
}

private Characteristic processCharacteristic(Model sqale, SMInputCursor chcCursor, ValidationMessages messages, RuleCache ruleCache) throws XMLStreamException {
private Characteristic processCharacteristic(Model model, SMInputCursor chcCursor, ValidationMessages messages, RuleCache ruleCache) throws XMLStreamException {
Characteristic characteristic = Characteristic.create();
SMInputCursor cursor = chcCursor.childElementCursor();

@@ -90,20 +99,20 @@ public class XMLImporter implements ServerExtension {
List<Characteristic> children = Lists.newArrayList();
while (cursor.getNext() != null) {
String node = cursor.getLocalName();
if (StringUtils.equals(node, XMLConstants.CHARACTERISTIC_KEY)) {
if (StringUtils.equals(node, CHARACTERISTIC_KEY)) {
characteristic.setKey(cursor.collectDescendantText().trim());

} else if (StringUtils.equals(node, XMLConstants.CHARACTERISTIC_NAME)) {
} else if (StringUtils.equals(node, CHARACTERISTIC_NAME)) {
characteristic.setName(cursor.collectDescendantText().trim(), false);

} else if (StringUtils.equals(node, XMLConstants.CHARACTERISTIC_DESCRIPTION)) {
} else if (StringUtils.equals(node, CHARACTERISTIC_DESCRIPTION)) {
characteristic.setDescription(cursor.collectDescendantText().trim());

} else if (StringUtils.equals(node, XMLConstants.PROPERTY)) {
} else if (StringUtils.equals(node, PROPERTY)) {
processProperty(characteristic, cursor, messages);

} else if (StringUtils.equals(node, XMLConstants.CHARACTERISTIC)) {
children.add(processCharacteristic(sqale, cursor, messages, ruleCache));
} else if (StringUtils.equals(node, CHARACTERISTIC)) {
children.add(processCharacteristic(model, cursor, messages, ruleCache));

} else if (StringUtils.equals(node, "rule-repo")) {
ruleRepositoryKey = cursor.collectDescendantText().trim();
@@ -115,7 +124,7 @@ public class XMLImporter implements ServerExtension {
fillRule(characteristic, ruleRepositoryKey, ruleKey, messages, ruleCache);

if (StringUtils.isNotBlank(characteristic.getKey()) || characteristic.getRule() != null) {
addCharacteristicToModel(sqale, characteristic, children);
addCharacteristicToModel(model, characteristic, children);
return characteristic;
}
return null;
@@ -133,11 +142,11 @@ public class XMLImporter implements ServerExtension {
}
}

private void addCharacteristicToModel(Model sqale, Characteristic characteristic, List<Characteristic> children) {
sqale.addCharacteristic(characteristic);
private void addCharacteristicToModel(Model model, Characteristic characteristic, List<Characteristic> children) {
model.addCharacteristic(characteristic);
for (Characteristic child : children) {
if (child != null) {
sqale.addCharacteristic(child);
model.addCharacteristic(child);
characteristic.addChild(child);
}
}
@@ -150,10 +159,10 @@ public class XMLImporter implements ServerExtension {
String textValue = null;
while (c.getNext() != null) {
String node = c.getLocalName();
if (StringUtils.equals(node, XMLConstants.PROPERTY_KEY)) {
if (StringUtils.equals(node, PROPERTY_KEY)) {
key = c.collectDescendantText().trim();

} else if (StringUtils.equals(node, XMLConstants.PROPERTY_VALUE)) {
} else if (StringUtils.equals(node, PROPERTY_VALUE)) {
String s = c.collectDescendantText().trim();
try {
value = NumberUtils.createDouble(s);
@@ -162,7 +171,7 @@ public class XMLImporter implements ServerExtension {
LOG.error(message, ex);
messages.addErrorText(message);
}
} else if (StringUtils.equals(node, XMLConstants.PROPERTY_TEXT_VALUE)) {
} else if (StringUtils.equals(node, PROPERTY_TEXT_VALUE)) {
textValue = c.collectDescendantText().trim();
}
}

Loading…
Cancel
Save