]> source.dussan.org Git - sonarqube.git/commitdiff
Replace WorkUnit by WorkDuration for technical debt
authorJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 12 Feb 2014 17:14:50 +0000 (18:14 +0100)
committerJulien Lancelot <julien.lancelot@sonarsource.com>
Wed, 12 Feb 2014 17:14:50 +0000 (18:14 +0100)
58 files changed:
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/NewTechnicalDebtDecorator.java
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecorator.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/NewTechnicalDebtDecoratorTest.java
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/technicaldebt/TechnicalDebtDecoratorTest.java
sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelLoader.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelProvider.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/debt/IssueChangelogDebtCalculator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/debt/RuleDebtCalculator.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/debt/package-info.java [new file with mode: 0644]
sonar-batch/src/main/java/org/sonar/batch/issue/ModuleIssues.java
sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculator.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoader.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProvider.java [deleted file]
sonar-batch/src/main/java/org/sonar/batch/technicaldebt/package-info.java [deleted file]
sonar-batch/src/test/java/org/sonar/batch/debt/DebtModelLoaderTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/debt/RuleDebtCalculatorTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/debt/TechnicalDebtModelProviderTest.java [new file with mode: 0644]
sonar-batch/src/test/java/org/sonar/batch/issue/ModuleIssuesTest.java
sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculatorTest.java [deleted file]
sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoaderTest.java [deleted file]
sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProviderTest.java [deleted file]
sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java
sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java
sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java
sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java
sonar-core/src/test/java/org/sonar/core/technicaldebt/DefaultTechnicalDebtManagerTest.java
sonar-core/src/test/java/org/sonar/core/technicaldebt/DefaultTechnicalDebtModelTest.java
sonar-core/src/test/java/org/sonar/core/technicaldebt/TechnicalDebtModelSynchronizerTest.java
sonar-core/src/test/java/org/sonar/core/technicaldebt/TechnicalDebtXMLImporterTest.java
sonar-plugin-api/src/main/java/org/sonar/api/issue/internal/DefaultIssue.java
sonar-plugin-api/src/main/java/org/sonar/api/technicaldebt/batch/internal/DefaultRequirement.java
sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDuration.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDurationFactory.java [new file with mode: 0644]
sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkUnit.java
sonar-plugin-api/src/test/java/org/sonar/api/issue/internal/DefaultIssueTest.java
sonar-plugin-api/src/test/java/org/sonar/api/technicaldebt/batch/internal/DefaultRequirementTest.java
sonar-plugin-api/src/test/java/org/sonar/api/technicaldebt/server/internal/DefaultCharacteristicTest.java
sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationFactoryTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationTest.java [new file with mode: 0644]
sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkUnitTest.java
sonar-server/src/main/java/org/sonar/server/issue/ActionPlanService.java
sonar-server/src/main/java/org/sonar/server/issue/DefaultIssueFinder.java
sonar-server/src/main/java/org/sonar/server/issue/IssueChangelogFormatter.java
sonar-server/src/main/java/org/sonar/server/issue/ws/IssueShowWsHandler.java
sonar-server/src/main/java/org/sonar/server/platform/Platform.java
sonar-server/src/main/java/org/sonar/server/technicaldebt/DebtFormatter.java
sonar-server/src/main/java/org/sonar/server/technicaldebt/DebtService.java
sonar-server/src/test/java/org/sonar/server/issue/ActionPlanServiceTest.java
sonar-server/src/test/java/org/sonar/server/issue/DefaultIssueFinderTest.java
sonar-server/src/test/java/org/sonar/server/issue/IssueChangelogFormatterTest.java
sonar-server/src/test/java/org/sonar/server/issue/ws/IssueShowWsHandlerTest.java
sonar-server/src/test/java/org/sonar/server/technicaldebt/DebtFormatterTest.java
sonar-server/src/test/java/org/sonar/server/technicaldebt/DebtServiceTest.java

index a4539278b059e73118e8aca8e68c25e29562e5a5..57f1b91037b063c9629bf5ff18cad00955a7f84a 100644 (file)
@@ -24,7 +24,9 @@ import org.sonar.api.*;
 import org.sonar.api.checks.NoSonarFilter;
 import org.sonar.api.config.PropertyDefinition;
 import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.components.PastSnapshotFinder;
+import org.sonar.batch.debt.IssueChangelogDebtCalculator;
 import org.sonar.core.timemachine.Periods;
 import org.sonar.plugins.core.batch.IndexProjectPostJob;
 import org.sonar.plugins.core.charts.DistributionAreaChart;
@@ -291,6 +293,8 @@ public final class CorePlugin extends SonarPlugin {
       // technical debt
       TechnicalDebtDecorator.class,
       NewTechnicalDebtDecorator.class,
+      IssueChangelogDebtCalculator.class,
+      WorkDurationFactory.class,
 
       // batch
       ProfileEventsSensor.class,
index 3c568ae78f91dfd0e04078da2d1604a85347bcff..28ca54cb9ad2d98784e0995a5d98d992a4c331a5 100644 (file)
@@ -39,7 +39,8 @@ import org.sonar.api.rules.ActiveRule;
 import org.sonar.api.rules.Rule;
 import org.sonar.api.rules.RuleFinder;
 import org.sonar.api.utils.KeyValueFormat;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.issue.IssueCache;
 import org.sonar.batch.scan.LastSnapshots;
 import org.sonar.core.issue.IssueUpdater;
@@ -67,6 +68,7 @@ public class IssueTrackingDecorator implements Decorator {
   private final ResourcePerspectives perspectives;
   private final RulesProfile rulesProfile;
   private final RuleFinder ruleFinder;
+  private final WorkDurationFactory workDurationFactory;
 
   public IssueTrackingDecorator(IssueCache issueCache, InitialOpenIssuesStack initialOpenIssues, IssueTracking tracking,
                                 LastSnapshots lastSnapshots, SonarIndex index,
@@ -75,7 +77,7 @@ public class IssueTrackingDecorator implements Decorator {
                                 Project project,
                                 ResourcePerspectives perspectives,
                                 RulesProfile rulesProfile,
-                                RuleFinder ruleFinder) {
+                                RuleFinder ruleFinder, WorkDurationFactory workDurationFactory) {
     this.issueCache = issueCache;
     this.initialOpenIssues = initialOpenIssues;
     this.tracking = tracking;
@@ -84,6 +86,7 @@ public class IssueTrackingDecorator implements Decorator {
     this.handlers = handlers;
     this.workflow = workflow;
     this.updater = updater;
+    this.workDurationFactory = workDurationFactory;
     this.changeContext = IssueChangeContext.createScan(project.getAnalysisDate());
     this.perspectives = perspectives;
     this.rulesProfile = rulesProfile;
@@ -178,14 +181,14 @@ public class IssueTrackingDecorator implements Decorator {
       updater.setPastMessage(issue, ref.getMessage(), changeContext);
       updater.setPastEffortToFix(issue, ref.getEffortToFix(), changeContext);
       Long technicalDebt = ref.getTechnicalDebt();
-      WorkUnit previousTechnicalDebt = technicalDebt != null ? WorkUnit.fromLong(technicalDebt) : null;
+      WorkDuration previousTechnicalDebt = technicalDebt != null ? workDurationFactory.createFromWorkingLong(technicalDebt) : null;
       updater.setPastTechnicalDebt(issue, previousTechnicalDebt, changeContext);
     }
   }
 
   private void addUnmatched(Collection<IssueDto> unmatchedIssues, SourceHashHolder sourceHashHolder, Collection<DefaultIssue> issues) {
     for (IssueDto unmatchedDto : unmatchedIssues) {
-      DefaultIssue unmatched = unmatchedDto.toDefaultIssue();
+      DefaultIssue unmatched = unmatchedDto.toDefaultIssue(workDurationFactory.createFromWorkingLong(unmatchedDto.getTechnicalDebt()));
       if (StringUtils.isNotBlank(unmatchedDto.getReporter()) && !Issue.STATUS_CLOSED.equals(unmatchedDto.getStatus())) {
         relocateManualIssue(unmatched, unmatchedDto, sourceHashHolder);
       }
@@ -196,7 +199,7 @@ public class IssueTrackingDecorator implements Decorator {
 
   private void addIssuesOnDeletedComponents(Collection<DefaultIssue> issues) {
     for (IssueDto deadDto : initialOpenIssues.selectAllIssues()) {
-      DefaultIssue dead = deadDto.toDefaultIssue();
+      DefaultIssue dead = deadDto.toDefaultIssue(workDurationFactory.createFromWorkingLong(deadDto.getTechnicalDebt()));
       updateUnmatchedIssue(dead, true);
       issues.add(dead);
     }
index cd540b555257c43e2f4c40595fb9886b22a8c1ba..b81e27125d206486361e3558d0f0ae2c7a69de92 100644 (file)
 
 package org.sonar.plugins.core.technicaldebt;
 
-import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Ordering;
-import org.apache.commons.lang.time.DateUtils;
-import org.sonar.api.CoreProperties;
 import org.sonar.api.batch.*;
 import org.sonar.api.component.ResourcePerspectives;
-import org.sonar.api.config.Settings;
 import org.sonar.api.issue.Issuable;
 import org.sonar.api.issue.Issue;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.measures.Measure;
 import org.sonar.api.measures.MeasureUtils;
 import org.sonar.api.measures.Metric;
 import org.sonar.api.resources.Project;
 import org.sonar.api.resources.Resource;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.components.Period;
 import org.sonar.batch.components.TimeMachineConfiguration;
-import org.sonar.core.issue.IssueUpdater;
+import org.sonar.batch.debt.IssueChangelogDebtCalculator;
 
 import javax.annotation.Nullable;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
 
 import static com.google.common.collect.Lists.newArrayList;
 
@@ -57,13 +53,15 @@ public final class NewTechnicalDebtDecorator implements Decorator {
 
   private final ResourcePerspectives perspectives;
   private final TimeMachineConfiguration timeMachineConfiguration;
+  private final IssueChangelogDebtCalculator issueChangelogDebtCalculator;
+  private final WorkDurationFactory workDurationFactory;
 
-  private final int hoursInDay;
-
-  public NewTechnicalDebtDecorator(ResourcePerspectives perspectives, TimeMachineConfiguration timeMachineConfiguration, Settings settings) {
+  public NewTechnicalDebtDecorator(ResourcePerspectives perspectives, TimeMachineConfiguration timeMachineConfiguration,
+                                   IssueChangelogDebtCalculator issueChangelogDebtCalculator, WorkDurationFactory workDurationFactory) {
     this.perspectives = perspectives;
     this.timeMachineConfiguration = timeMachineConfiguration;
-    this.hoursInDay = settings.getInt(CoreProperties.HOURS_IN_DAY);
+    this.issueChangelogDebtCalculator = issueChangelogDebtCalculator;
+    this.workDurationFactory = workDurationFactory;
   }
 
   public boolean shouldExecuteOnProject(Project project) {
@@ -98,93 +96,14 @@ public final class NewTechnicalDebtDecorator implements Decorator {
   }
 
   private Double calculateNewTechnicalDebtValue(Collection<Issue> issues, @Nullable Date periodDate) {
-    double value = 0;
+    WorkDuration duration = workDurationFactory.createFromWorkingLong(0l);
     for (Issue issue : issues) {
-      double currentTechnicalDebtValue = 0d;
-      WorkUnit currentTechnicalDebt = ((DefaultIssue) issue).technicalDebt();
-      if (currentTechnicalDebt != null) {
-        currentTechnicalDebtValue = currentTechnicalDebt.toDays(hoursInDay);
-      }
-
-      Date periodDatePlusOneSecond = periodDate != null ? DateUtils.addSeconds(periodDate, 1) : null;
-      if (isAfter(issue.creationDate(), periodDatePlusOneSecond)) {
-        value += currentTechnicalDebtValue;
-      } else {
-        value += calculateNewTechnicalDebtValueFromChangelog(currentTechnicalDebtValue, issue, periodDate);
-      }
-    }
-    return value;
-  }
-
-  private double calculateNewTechnicalDebtValueFromChangelog(double currentTechnicalDebtValue, Issue issue, Date periodDate) {
-    List<FieldDiffs> changelog = technicalDebtHistory(issue);
-    for (Iterator<FieldDiffs> iterator = changelog.iterator(); iterator.hasNext(); ) {
-      FieldDiffs diff = iterator.next();
-      Date date = diff.creationDate();
-      if (isLesserOrEqual(date, periodDate)) {
-        // return new value from the change that is just before the period date
-        return currentTechnicalDebtValue - newValue(diff).toDays(hoursInDay);
+      WorkDuration debt = issueChangelogDebtCalculator.calculateNewTechnicalDebt(issue, periodDate);
+      if (debt != null) {
+        duration = duration.add(debt);
       }
-      if (!iterator.hasNext()) {
-        // return old value from the change that is just after the period date when there's no more element in changelog
-        return currentTechnicalDebtValue - oldValue(diff).toDays(hoursInDay);
-      }
-    }
-    // Return 0 when no changelog
-    return 0d;
-  }
-
-  private List<FieldDiffs> technicalDebtHistory(Issue issue) {
-    List<FieldDiffs> technicalDebtChangelog = changesOnField(((DefaultIssue) issue).changes());
-    if (!technicalDebtChangelog.isEmpty()) {
-      // Changelog have to be sorted from newest to oldest.
-      // Null date should be the latest as this happen when technical debt has changed since previous analysis.
-      Ordering<FieldDiffs> ordering = Ordering.natural().reverse().nullsFirst().onResultOf(new Function<FieldDiffs, Date>() {
-        public Date apply(FieldDiffs diff) {
-          return diff.creationDate();
-        }
-      });
-      return ordering.immutableSortedCopy(technicalDebtChangelog);
     }
-    return Collections.emptyList();
-  }
-
-  private List<FieldDiffs> changesOnField(Collection<FieldDiffs> fieldDiffs) {
-    List<FieldDiffs> diffs = newArrayList();
-    for (FieldDiffs fieldDiff : fieldDiffs) {
-      if (fieldDiff.diffs().containsKey(IssueUpdater.TECHNICAL_DEBT)) {
-        diffs.add(fieldDiff);
-      }
-    }
-    return diffs;
-  }
-
-  private WorkUnit newValue(FieldDiffs fieldDiffs) {
-    for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
-      if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
-        Long newValue = entry.getValue().newValueLong();
-        return newValue != null ? WorkUnit.fromLong(newValue) : WorkUnit.fromLong(0);
-      }
-    }
-    return WorkUnit.fromLong(0);
-  }
-
-  private WorkUnit oldValue(FieldDiffs fieldDiffs) {
-    for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
-      if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
-        Long value = entry.getValue().oldValueLong();
-        return value != null ? WorkUnit.fromLong(value) : WorkUnit.fromLong(0);
-      }
-    }
-    return WorkUnit.fromLong(0);
-  }
-
-  private boolean isAfter(@Nullable Date currentDate, @Nullable Date pastDate) {
-    return pastDate == null || (currentDate != null && DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) > 0);
-  }
-
-  private boolean isLesserOrEqual(@Nullable Date currentDate, @Nullable Date pastDate) {
-    return (currentDate != null) && (pastDate == null || (DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) <= 0));
+    return duration.toWorkingDays();
   }
 
   private boolean shouldSaveNewMetrics(DecoratorContext context) {
index 44eb76e6ba105e6d55666e7cacd09125f009ecbd..6813e1f197101298b3f2886d868e622c2dd14c3a 100644 (file)
@@ -30,7 +30,6 @@ import org.sonar.api.PropertyType;
 import org.sonar.api.batch.*;
 import org.sonar.api.component.ResourcePerspectives;
 import org.sonar.api.config.PropertyDefinition;
-import org.sonar.api.config.Settings;
 import org.sonar.api.issue.Issuable;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.internal.DefaultIssue;
@@ -41,7 +40,8 @@ import org.sonar.api.resources.ResourceUtils;
 import org.sonar.api.technicaldebt.batch.Characteristic;
 import org.sonar.api.technicaldebt.batch.Requirement;
 import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -61,13 +61,12 @@ public final class TechnicalDebtDecorator implements Decorator {
 
   private final ResourcePerspectives perspectives;
   private final TechnicalDebtModel model;
+  private final WorkDurationFactory workDurationFactory;
 
-  private final int hoursInDay;
-
-  public TechnicalDebtDecorator(ResourcePerspectives perspectives, TechnicalDebtModel model, Settings settings) {
+  public TechnicalDebtDecorator(ResourcePerspectives perspectives, TechnicalDebtModel model, WorkDurationFactory workDurationFactory) {
     this.perspectives = perspectives;
     this.model = model;
-    this.hoursInDay = settings.getInt(CoreProperties.HOURS_IN_DAY);
+    this.workDurationFactory = workDurationFactory;
   }
 
   public boolean shouldExecuteOnProject(Project project) {
@@ -168,18 +167,18 @@ public final class TechnicalDebtDecorator implements Decorator {
   }
 
   private double computeTechnicalDebt(Metric metric, DecoratorContext context, Requirement requirement, Collection<Issue> issues) {
-    double value = 0.0;
+    WorkDuration debt = workDurationFactory.createFromWorkingLong(0l);
+//    double value = 0d;
     if (issues != null) {
       for (Issue issue : issues) {
-        WorkUnit debt = ((DefaultIssue) issue).technicalDebt();
-        if (debt != null) {
-          value += debt.toDays(hoursInDay);
-        } else {
-          value += 0d;
-        }
+//        if (debt != null) {
+//          value += debt.toWorkingDays();
+//        }
+        debt = debt.add(((DefaultIssue) issue).technicalDebt());
       }
     }
 
+    double value = debt.toWorkingDays();
     for (Measure measure : context.getChildrenMeasures(MeasuresFilters.requirement(metric, requirement))) {
       Requirement measureRequirement = measure.getRequirement();
       if (measureRequirement != null && measureRequirement.equals(requirement) && measure.getValue() != null) {
index 0cb689bb9d8000dc499d3b4b29b3c6a4e3054e1f..f3227c11ae2a83cc383d13804e7b8e961a18703b 100644 (file)
@@ -23,9 +23,11 @@ import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatcher;
+import org.sonar.api.CoreProperties;
 import org.sonar.api.batch.DecoratorContext;
 import org.sonar.api.batch.SonarIndex;
 import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.config.Settings;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.IssueChangeContext;
@@ -36,7 +38,8 @@ import org.sonar.api.resources.Resource;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rules.Rule;
 import org.sonar.api.rules.RuleFinder;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.issue.IssueCache;
 import org.sonar.batch.scan.LastSnapshots;
 import org.sonar.core.issue.IssueUpdater;
@@ -78,6 +81,9 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase {
 
   @Before
   public void init() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, 8);
+
     decorator = new IssueTrackingDecorator(
       issueCache,
       initialOpenIssues,
@@ -90,7 +96,7 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase {
       mock(Project.class),
       perspectives,
       profile,
-      ruleFinder);
+      ruleFinder, new WorkDurationFactory(settings));
   }
 
   @Test
@@ -520,7 +526,7 @@ public class IssueTrackingDecoratorTest extends AbstractDaoTestCase {
     verify(updater).setPastLine(eq(issue), eq(10));
     verify(updater).setPastMessage(eq(issue), eq("Message"), any(IssueChangeContext.class));
     verify(updater).setPastEffortToFix(eq(issue), eq(1.5), any(IssueChangeContext.class));
-    verify(updater).setPastTechnicalDebt(eq(issue), eq(new WorkUnit.Builder().setMinutes(1).build()), any(IssueChangeContext.class));
+    verify(updater).setPastTechnicalDebt(eq(issue), eq(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.MINUTES, 8)), any(IssueChangeContext.class));
   }
 
   @Test
index 4b9689381360a3aadbf8d371dd2c448d335c29ee..9416ff3253005760a51d7d009bc1eb327465b9e1 100644 (file)
@@ -40,9 +40,11 @@ import org.sonar.api.measures.Measure;
 import org.sonar.api.measures.Metric;
 import org.sonar.api.resources.Resource;
 import org.sonar.api.test.IsMeasure;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.components.Period;
 import org.sonar.batch.components.TimeMachineConfiguration;
+import org.sonar.batch.debt.IssueChangelogDebtCalculator;
 
 import java.util.Date;
 
@@ -75,9 +77,9 @@ public class NewTechnicalDebtDecoratorTest {
   Date fiveDaysAgo;
   Date fourDaysAgo;
 
-  WorkUnit oneDaysDebt = new WorkUnit.Builder().setDays(1).build();
-  WorkUnit twoDaysDebt = new WorkUnit.Builder().setDays(2).build();
-  WorkUnit fiveDaysDebt = new WorkUnit.Builder().setDays(5).build();
+  WorkDuration oneDaysDebt = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8);
+  WorkDuration twoDaysDebt = WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, 8);
+  WorkDuration fiveDaysDebt = WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.DAYS, 8);
 
   @Before
   public void setup() {
@@ -96,7 +98,8 @@ public class NewTechnicalDebtDecoratorTest {
 
     when(timeMachineConfiguration.periods()).thenReturn(newArrayList(new Period(1, fiveDaysAgo), new Period(2, tenDaysAgo)));
 
-    decorator = new NewTechnicalDebtDecorator(perspectives, timeMachineConfiguration, settings);
+    WorkDurationFactory workDurationFactory = new WorkDurationFactory(settings);
+    decorator = new NewTechnicalDebtDecorator(perspectives, timeMachineConfiguration, new IssueChangelogDebtCalculator(workDurationFactory), workDurationFactory);
   }
 
   @Test
@@ -325,7 +328,7 @@ public class NewTechnicalDebtDecoratorTest {
     verify(context, never()).saveMeasure(argThat(new IsMeasure(CoreMetrics.NEW_TECHNICAL_DEBT)));
   }
 
-  private Long fromWorkDayDuration(WorkUnit workDayDuration) {
+  private Long fromWorkDayDuration(WorkDuration workDayDuration) {
     return workDayDuration.toLong();
   }
 
index 3c13be9a8a30bd0251935e0d662de3d5966f2703..1d7784d6b0c440298b7a83acfde9fa1e3fbeb513 100644 (file)
@@ -49,7 +49,8 @@ import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
 import org.sonar.api.technicaldebt.batch.internal.DefaultCharacteristic;
 import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
 import org.sonar.api.test.IsMeasure;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 
 import java.util.List;
 
@@ -83,7 +84,7 @@ public class TechnicalDebtDecoratorTest {
     Settings settings = new Settings();
     settings.setProperty(CoreProperties.HOURS_IN_DAY, "8");
 
-    decorator = new TechnicalDebtDecorator(perspectives, defaultTechnicalDebtModel, settings);
+    decorator = new TechnicalDebtDecorator(perspectives, defaultTechnicalDebtModel, new WorkDurationFactory(settings));
   }
 
   @Test
@@ -129,7 +130,7 @@ public class TechnicalDebtDecoratorTest {
 
   @Test
   public void add_technical_debt_from_one_issue_and_no_parent() throws Exception {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setDays(1).build();
+    WorkDuration technicalDebt = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8);
 
     Issue issue = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt);
     when(issuable.issues()).thenReturn(newArrayList(issue));
@@ -160,7 +161,7 @@ public class TechnicalDebtDecoratorTest {
 
   @Test
   public void add_technical_debt_from_one_issue_and_propagate_to_parents() throws Exception {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setDays(1).build();
+    WorkDuration technicalDebt = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8);
 
     Issue issue = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt);
     when(issuable.issues()).thenReturn(newArrayList(issue));
@@ -183,8 +184,8 @@ public class TechnicalDebtDecoratorTest {
 
   @Test
   public void add_technical_debt_from_issues() throws Exception {
-    WorkUnit technicalDebt1 = new WorkUnit.Builder().setDays(1).build();
-    WorkUnit technicalDebt2 = new WorkUnit.Builder().setDays(2).build();
+    WorkDuration technicalDebt1 = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8);
+    WorkDuration technicalDebt2 = WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, 8);
 
     Issue issue1 = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt1);
     Issue issue2 = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt1);
@@ -212,7 +213,7 @@ public class TechnicalDebtDecoratorTest {
 
   @Test
   public void add_technical_debt_from_children_measures() throws Exception {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setDays(1).build();
+    WorkDuration technicalDebt = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8);
 
     Issue issue1 = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt);
     Issue issue2 = createIssue("rule1", "repo1").setTechnicalDebt(technicalDebt);
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelLoader.java b/sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelLoader.java
new file mode 100644 (file)
index 0000000..1cbf38d
--- /dev/null
@@ -0,0 +1,103 @@
+/*
+ * 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.batch.debt;
+
+import org.sonar.api.BatchComponent;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RuleQuery;
+import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
+import org.sonar.api.technicaldebt.batch.internal.DefaultCharacteristic;
+import org.sonar.core.technicaldebt.DefaultTechnicalDebtModel;
+import org.sonar.core.technicaldebt.db.CharacteristicDao;
+import org.sonar.core.technicaldebt.db.CharacteristicDto;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static com.google.common.collect.Maps.newHashMap;
+
+public class DebtModelLoader implements BatchComponent {
+
+  private final CharacteristicDao dao;
+  private final RuleFinder ruleFinder;
+
+  public DebtModelLoader(CharacteristicDao dao, RuleFinder ruleFinder) {
+    this.dao = dao;
+    this.ruleFinder = ruleFinder;
+  }
+
+  public TechnicalDebtModel load() {
+    DefaultTechnicalDebtModel model = new DefaultTechnicalDebtModel();
+    List<CharacteristicDto> dtos = dao.selectEnabledCharacteristics();
+    Map<Integer, DefaultCharacteristic> characteristicsById = newHashMap();
+
+    addRootCharacteristics(model, dtos, characteristicsById);
+    addCharacteristics(dtos, characteristicsById);
+    addRequirements(dtos, characteristicsById);
+    return model;
+  }
+
+  private void addRootCharacteristics(DefaultTechnicalDebtModel model, List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
+    for (CharacteristicDto dto : dtos) {
+      if (dto.getParentId() == null) {
+        DefaultCharacteristic rootCharacteristic = dto.toCharacteristic(null);
+        model.addRootCharacteristic(rootCharacteristic);
+        characteristicsById.put(dto.getId(), rootCharacteristic);
+      }
+    }
+  }
+
+  private void addCharacteristics(List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
+    for (CharacteristicDto dto : dtos) {
+      if (dto.getParentId() != null && dto.getRuleId() == null) {
+        DefaultCharacteristic parent = characteristicsById.get(dto.getParentId());
+        DefaultCharacteristic characteristic = dto.toCharacteristic(parent);
+        characteristicsById.put(dto.getId(), characteristic);
+      }
+    }
+  }
+
+  private void addRequirements(List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
+    Map<Integer, Rule> rulesById = rulesById(ruleFinder.findAll(RuleQuery.create()));
+    for (CharacteristicDto dto : dtos) {
+      Integer ruleId = dto.getRuleId();
+      if (ruleId != null) {
+        DefaultCharacteristic characteristic = characteristicsById.get(dto.getParentId());
+        DefaultCharacteristic rootCharacteristic = characteristicsById.get(dto.getRootId());
+        Rule rule = rulesById.get(ruleId);
+        RuleKey ruleKey = RuleKey.of(rule.getRepositoryKey(), rule.getKey());
+        dto.toRequirement(ruleKey, characteristic, rootCharacteristic);
+      }
+    }
+  }
+
+  private Map<Integer, Rule> rulesById(Collection<Rule> rules) {
+    Map<Integer, Rule> rulesById = newHashMap();
+    for (Rule rule : rules) {
+      rulesById.put(rule.getId(), rule);
+    }
+    return rulesById;
+  }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelProvider.java b/sonar-batch/src/main/java/org/sonar/batch/debt/DebtModelProvider.java
new file mode 100644 (file)
index 0000000..fafeade
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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.batch.debt;
+
+import org.picocontainer.injectors.ProviderAdapter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
+import org.sonar.api.utils.TimeProfiler;
+
+public class DebtModelProvider extends ProviderAdapter {
+
+  private static final Logger LOG = LoggerFactory.getLogger(DebtModelProvider.class);
+
+  private TechnicalDebtModel model;
+
+  public TechnicalDebtModel provide(DebtModelLoader loader) {
+    if (model == null) {
+      TimeProfiler profiler = new TimeProfiler(LOG).start("Loading technical debt model");
+      model = loader.load();
+      profiler.stop();
+    }
+    return model;
+  }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/IssueChangelogDebtCalculator.java b/sonar-batch/src/main/java/org/sonar/batch/debt/IssueChangelogDebtCalculator.java
new file mode 100644 (file)
index 0000000..04bb3b2
--- /dev/null
@@ -0,0 +1,133 @@
+/*
+ * 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.batch.debt;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Ordering;
+import org.apache.commons.lang.time.DateUtils;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.FieldDiffs;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
+import org.sonar.core.issue.IssueUpdater;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.util.*;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+public class IssueChangelogDebtCalculator implements BatchComponent {
+
+  private final WorkDurationFactory workDurationFactory;
+
+  public IssueChangelogDebtCalculator(WorkDurationFactory workDurationFactory) {
+    this.workDurationFactory = workDurationFactory;
+  }
+
+  @CheckForNull
+  public WorkDuration calculateNewTechnicalDebt(Issue issue, @Nullable Date periodDate) {
+    WorkDuration currentTechnicalDebt = ((DefaultIssue) issue).technicalDebt();
+    Date periodDatePlusOneSecond = periodDate != null ? DateUtils.addSeconds(periodDate, 1) : null;
+    if (isAfter(issue.creationDate(), periodDatePlusOneSecond)) {
+      return currentTechnicalDebt;
+    } else {
+      return calculateNewTechnicalDebtValueFromChangelog(currentTechnicalDebt, issue, periodDate);
+    }
+  }
+
+  @CheckForNull
+  private WorkDuration calculateNewTechnicalDebtValueFromChangelog(WorkDuration currentTechnicalDebtValue, Issue issue, Date periodDate) {
+    List<FieldDiffs> changelog = technicalDebtHistory(issue);
+    for (Iterator<FieldDiffs> iterator = changelog.iterator(); iterator.hasNext(); ) {
+      FieldDiffs diff = iterator.next();
+      Date date = diff.creationDate();
+      if (isLesserOrEqual(date, periodDate)) {
+        // return new value from the change that is just before the period date
+        return currentTechnicalDebtValue.subtract(newValue(diff));
+      }
+      if (!iterator.hasNext()) {
+        // return old value from the change that is just after the period date when there's no more element in changelog
+        return currentTechnicalDebtValue.subtract(oldValue(diff));
+      }
+    }
+    // Return null when no changelog
+    return null;
+  }
+
+  private List<FieldDiffs> technicalDebtHistory(Issue issue) {
+    List<FieldDiffs> technicalDebtChangelog = changesOnField(((DefaultIssue) issue).changes());
+    if (!technicalDebtChangelog.isEmpty()) {
+      // Changelog have to be sorted from newest to oldest.
+      // Null date should be the first as this happen when technical debt has changed since previous analysis.
+      Ordering<FieldDiffs> ordering = Ordering.natural().reverse().nullsFirst().onResultOf(new Function<FieldDiffs, Date>() {
+        public Date apply(FieldDiffs diff) {
+          return diff.creationDate();
+        }
+      });
+      return ordering.immutableSortedCopy(technicalDebtChangelog);
+    }
+    return Collections.emptyList();
+  }
+
+  private List<FieldDiffs> changesOnField(Collection<FieldDiffs> fieldDiffs) {
+    List<FieldDiffs> diffs = newArrayList();
+    for (FieldDiffs fieldDiff : fieldDiffs) {
+      if (fieldDiff.diffs().containsKey(IssueUpdater.TECHNICAL_DEBT)) {
+        diffs.add(fieldDiff);
+      }
+    }
+    return diffs;
+  }
+
+  private WorkDuration newValue(FieldDiffs fieldDiffs) {
+    for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
+      if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
+        Long newValue = entry.getValue().newValueLong();
+        return newValue != null ? workDurationFactory.createFromWorkingLong(newValue) : workDurationFactory.createFromWorkingLong(0l);
+      }
+    }
+    return workDurationFactory.createFromWorkingLong(0l);
+  }
+
+  private WorkDuration oldValue(FieldDiffs fieldDiffs) {
+    for (Map.Entry<String, FieldDiffs.Diff> entry : fieldDiffs.diffs().entrySet()) {
+      if (entry.getKey().equals(IssueUpdater.TECHNICAL_DEBT)) {
+        Long value = entry.getValue().oldValueLong();
+        return value != null ? workDurationFactory.createFromWorkingLong(value) : workDurationFactory.createFromWorkingLong(0l);
+      }
+    }
+    return workDurationFactory.createFromWorkingLong(0l);
+  }
+
+  private boolean isAfter(@Nullable Date currentDate, @Nullable Date pastDate) {
+    return pastDate == null || (currentDate != null && DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) > 0);
+  }
+
+  private boolean isLesserOrEqual(@Nullable Date currentDate, @Nullable Date pastDate) {
+    return (currentDate != null) && (pastDate == null || (DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) <= 0));
+  }
+
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/RuleDebtCalculator.java b/sonar-batch/src/main/java/org/sonar/batch/debt/RuleDebtCalculator.java
new file mode 100644 (file)
index 0000000..d828a4d
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * 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.batch.debt;
+
+import com.google.common.base.Objects;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.technicaldebt.batch.Requirement;
+import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
+import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
+import org.sonar.api.utils.WorkUnit;
+
+import javax.annotation.Nullable;
+
+/**
+ * Computes the remediation cost based on the quality and analysis models.
+ */
+public class RuleDebtCalculator implements BatchExtension {
+
+  private TechnicalDebtModel model;
+  private final WorkDurationFactory workDurationFactory;
+
+  public RuleDebtCalculator(TechnicalDebtModel model, WorkDurationFactory workDurationFactory) {
+    this.model = model;
+    this.workDurationFactory = workDurationFactory;
+  }
+
+  /**
+   * Calculate the technical debt from a requirement
+   */
+  public WorkDuration calculateTechnicalDebt(RuleKey ruleKey, Double effortToFix) {
+    Requirement requirement = model.requirementsByRule(ruleKey);
+    if (requirement != null) {
+      if (requirement.function().equals(DefaultRequirement.CONSTANT_ISSUE) && effortToFix != null) {
+        throw new IllegalArgumentException("Requirement for '" + ruleKey + "' can not use 'Constant/issue' remediation function " +
+          "because this rule does not have a fixed remediation cost.");
+      }
+      return calculateTechnicalDebt(requirement, effortToFix);
+    }
+    return null;
+  }
+
+  private WorkDuration calculateTechnicalDebt(Requirement requirement, @Nullable Double effortToFix) {
+    WorkDuration result = workDurationFactory.createFromWorkingValue(0, WorkDuration.UNIT.DAYS);
+
+    WorkUnit factorUnit = requirement.factor();
+    if (factorUnit != null) {
+      int factorValue = (int) requirement.factor().getValue();
+      int effortToFixValue = Objects.firstNonNull(effortToFix, 1).intValue();
+      result = workDurationFactory.createFromWorkingValue(factorValue, toUnit(requirement.factor().getUnit())).multiply(effortToFixValue);
+    }
+
+    WorkUnit offsetUnit = requirement.offset();
+    if (offsetUnit != null) {
+      int offsetValue = (int) requirement.offset().getValue();
+      result = result.add(workDurationFactory.createFromWorkingValue(offsetValue, toUnit(requirement.offset().getUnit())));
+    }
+
+    return result;
+  }
+
+  private static WorkDuration.UNIT toUnit(String requirementUnit){
+    if (requirementUnit.equals(WorkUnit.DAYS)) {
+      return WorkDuration.UNIT.DAYS;
+    } else if (requirementUnit.equals(WorkUnit.HOURS)) {
+      return WorkDuration.UNIT.HOURS;
+    } else if (requirementUnit.equals(WorkUnit.MINUTES)) {
+      return WorkDuration.UNIT.MINUTES;
+    }
+    throw new IllegalStateException("Invalid unit : " + requirementUnit);
+  }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/debt/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/debt/package-info.java
new file mode 100644 (file)
index 0000000..1e643df
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.batch.debt;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 453f1fc43b65b6c9bb4f9f43e14636d67eeac494..516770d1c3ac302997463f13c27ff4aafaf68664 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.api.rules.Rule;
 import org.sonar.api.rules.RuleFinder;
 import org.sonar.api.rules.Violation;
 import org.sonar.api.utils.MessageException;
-import org.sonar.batch.technicaldebt.TechnicalDebtCalculator;
+import org.sonar.batch.debt.RuleDebtCalculator;
 import org.sonar.core.issue.DefaultIssueBuilder;
 
 import javax.annotation.Nullable;
@@ -43,10 +43,10 @@ public class ModuleIssues {
   private final IssueCache cache;
   private final Project project;
   private final IssueFilters filters;
-  private final TechnicalDebtCalculator technicalDebtCalculator;
+  private final RuleDebtCalculator technicalDebtCalculator;
   private final RuleFinder ruleFinder;
 
-  public ModuleIssues(RulesProfile qProfile, IssueCache cache, Project project, IssueFilters filters, TechnicalDebtCalculator technicalDebtCalculator, RuleFinder ruleFinder) {
+  public ModuleIssues(RulesProfile qProfile, IssueCache cache, Project project, IssueFilters filters, RuleDebtCalculator technicalDebtCalculator, RuleFinder ruleFinder) {
     this.qProfile = qProfile;
     this.cache = cache;
     this.project = project;
@@ -111,7 +111,7 @@ public class ModuleIssues {
     if (issue.severity() == null) {
       issue.setSeverity(activeRule.getSeverity().name());
     }
-    issue.setTechnicalDebt(technicalDebtCalculator.calculTechnicalDebt(issue));
+    issue.setTechnicalDebt(technicalDebtCalculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix()));
   }
 
 }
index 821de088059037898a90f3f7d9064c497c3c4017..b564ea4ca62ad21af1b08b175523973901b9d71c 100644 (file)
@@ -31,12 +31,17 @@ import org.sonar.api.resources.Languages;
 import org.sonar.api.resources.Project;
 import org.sonar.api.scan.filesystem.PathResolver;
 import org.sonar.api.utils.SonarException;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.batch.DefaultFileLinesContextFactory;
 import org.sonar.batch.DefaultResourceCreationLock;
 import org.sonar.batch.ProjectConfigurator;
 import org.sonar.batch.ProjectTree;
 import org.sonar.batch.bootstrap.*;
 import org.sonar.batch.components.PeriodsDefinition;
+import org.sonar.batch.debt.DebtModelLoader;
+import org.sonar.batch.debt.DebtModelProvider;
+import org.sonar.batch.debt.IssueChangelogDebtCalculator;
+import org.sonar.batch.debt.RuleDebtCalculator;
 import org.sonar.batch.index.*;
 import org.sonar.batch.issue.*;
 import org.sonar.batch.phases.GraphPersister;
@@ -46,9 +51,6 @@ import org.sonar.batch.scan.maven.FakeMavenPluginExecutor;
 import org.sonar.batch.scan.maven.MavenPluginExecutor;
 import org.sonar.batch.source.HighlightableBuilder;
 import org.sonar.batch.source.SymbolizableBuilder;
-import org.sonar.batch.technicaldebt.TechnicalDebtCalculator;
-import org.sonar.batch.technicaldebt.TechnicalDebtModelLoader;
-import org.sonar.batch.technicaldebt.TechnicalDebtModelProvider;
 import org.sonar.core.component.ScanGraph;
 import org.sonar.core.issue.IssueNotifications;
 import org.sonar.core.issue.IssueUpdater;
@@ -138,6 +140,7 @@ public class ProjectScanContainer extends ComponentContainer {
       IssuePersister.class,
       IssueNotifications.class,
       DefaultProjectIssues.class,
+      IssueChangelogDebtCalculator.class,
 
       // tests
       TestPlanPerspectiveLoader.class,
@@ -153,9 +156,10 @@ public class ProjectScanContainer extends ComponentContainer {
       SymbolizableBuilder.class,
 
       // technical debt
-      TechnicalDebtModelLoader.class,
-      TechnicalDebtCalculator.class,
-      new TechnicalDebtModelProvider(),
+      DebtModelLoader.class,
+      RuleDebtCalculator.class,
+      WorkDurationFactory.class,
+      new DebtModelProvider(),
 
       // Differential periods
       PeriodsDefinition.class,
diff --git a/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculator.java b/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculator.java
deleted file mode 100644 (file)
index 3cfefd8..0000000
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import com.google.common.base.Objects;
-import org.sonar.api.BatchExtension;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.config.Settings;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.technicaldebt.batch.Requirement;
-import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
-import org.sonar.api.utils.WorkUnit;
-
-/**
- * Computes the remediation cost based on the quality and analysis models.
- */
-public class TechnicalDebtCalculator implements BatchExtension {
-
-  private int hoursInDay;
-
-  private TechnicalDebtModel model;
-
-  public TechnicalDebtCalculator(TechnicalDebtModel model, Settings settings) {
-    this.model = model;
-    this.hoursInDay = settings.getInt(CoreProperties.HOURS_IN_DAY);
-  }
-
-  /**
-   * Get the technical debt from the requirement
-   */
-  public WorkUnit calculTechnicalDebt(Issue issue) {
-    Requirement requirement = model.requirementsByRule(issue.ruleKey());
-    if (requirement != null) {
-      if (requirement.function().equals(DefaultRequirement.CONSTANT_ISSUE) && issue.effortToFix() != null) {
-        throw new IllegalArgumentException("Requirement for '" + issue.ruleKey() + "' can not use 'Constant/issue' remediation function " +
-          "because this rule does not have a fixed remediation cost.");
-      }
-      return fromMinutes(calculTechnicalDebt(requirement, issue));
-    }
-    return null;
-  }
-
-  private long calculTechnicalDebt(Requirement requirement, Issue issue) {
-    long effortToFix = Objects.firstNonNull(issue.effortToFix(), 1L).longValue();
-
-    WorkUnit factorUnit = requirement.factor();
-    long factor = factorUnit != null ? toMinutes(factorUnit) : 0L;
-
-    WorkUnit offsetUnit = requirement.offset();
-    long offset = offsetUnit != null ? toMinutes(offsetUnit) : 0L;
-
-    return effortToFix * factor + offset;
-  }
-
-  private long toMinutes(WorkUnit factor) {
-    if (factor.days() > 0) {
-      return Double.valueOf(factor.days() * hoursInDay * 60d).longValue();
-    } else if (factor.hours() > 0) {
-      return Double.valueOf(factor.hours() * 60d).longValue();
-    } else {
-      return Double.valueOf(factor.minutes()).longValue();
-    }
-  }
-
-  private WorkUnit fromMinutes(Long inMinutes) {
-    int oneHourInMinute = 60;
-    int days = 0;
-    int hours = 0;
-    int minutes = 0;
-
-    int oneWorkingDay = hoursInDay * oneHourInMinute;
-    if (inMinutes >= oneWorkingDay) {
-      Long nbDays = inMinutes / oneWorkingDay;
-      days = nbDays.shortValue();
-      inMinutes = inMinutes - (nbDays * oneWorkingDay);
-    }
-
-    if (inMinutes >= oneHourInMinute) {
-      Long nbHours = inMinutes / oneHourInMinute;
-      hours = nbHours.shortValue();
-      inMinutes = inMinutes - (nbHours * oneHourInMinute);
-    }
-
-    minutes = inMinutes.shortValue();
-
-    return new WorkUnit.Builder().setDays(days).setHours(hours).setMinutes(minutes).build();
-  }
-}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoader.java b/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoader.java
deleted file mode 100644 (file)
index cf11256..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import org.sonar.api.BatchComponent;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rules.Rule;
-import org.sonar.api.rules.RuleFinder;
-import org.sonar.api.rules.RuleQuery;
-import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-import org.sonar.api.technicaldebt.batch.internal.DefaultCharacteristic;
-import org.sonar.core.technicaldebt.DefaultTechnicalDebtModel;
-import org.sonar.core.technicaldebt.db.CharacteristicDao;
-import org.sonar.core.technicaldebt.db.CharacteristicDto;
-
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-import static com.google.common.collect.Maps.newHashMap;
-
-public class TechnicalDebtModelLoader implements BatchComponent {
-
-  private final CharacteristicDao dao;
-  private final RuleFinder ruleFinder;
-
-  public TechnicalDebtModelLoader(CharacteristicDao dao, RuleFinder ruleFinder) {
-    this.dao = dao;
-    this.ruleFinder = ruleFinder;
-  }
-
-  public TechnicalDebtModel load() {
-    DefaultTechnicalDebtModel model = new DefaultTechnicalDebtModel();
-    List<CharacteristicDto> dtos = dao.selectEnabledCharacteristics();
-    Map<Integer, DefaultCharacteristic> characteristicsById = newHashMap();
-
-    addRootCharacteristics(model, dtos, characteristicsById);
-    addCharacteristics(dtos, characteristicsById);
-    addRequirements(dtos, characteristicsById);
-    return model;
-  }
-
-  private void addRootCharacteristics(DefaultTechnicalDebtModel model, List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
-    for (CharacteristicDto dto : dtos) {
-      if (dto.getParentId() == null) {
-        DefaultCharacteristic rootCharacteristic = dto.toCharacteristic(null);
-        model.addRootCharacteristic(rootCharacteristic);
-        characteristicsById.put(dto.getId(), rootCharacteristic);
-      }
-    }
-  }
-
-  private void addCharacteristics(List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
-    for (CharacteristicDto dto : dtos) {
-      if (dto.getParentId() != null && dto.getRuleId() == null) {
-        DefaultCharacteristic parent = characteristicsById.get(dto.getParentId());
-        DefaultCharacteristic characteristic = dto.toCharacteristic(parent);
-        characteristicsById.put(dto.getId(), characteristic);
-      }
-    }
-  }
-
-  private void addRequirements(List<CharacteristicDto> dtos, Map<Integer, DefaultCharacteristic> characteristicsById) {
-    Map<Integer, Rule> rulesById = rulesById(ruleFinder.findAll(RuleQuery.create()));
-    for (CharacteristicDto dto : dtos) {
-      Integer ruleId = dto.getRuleId();
-      if (ruleId != null) {
-        DefaultCharacteristic characteristic = characteristicsById.get(dto.getParentId());
-        DefaultCharacteristic rootCharacteristic = characteristicsById.get(dto.getRootId());
-        Rule rule = rulesById.get(ruleId);
-        RuleKey ruleKey = RuleKey.of(rule.getRepositoryKey(), rule.getKey());
-        dto.toRequirement(ruleKey, characteristic, rootCharacteristic);
-      }
-    }
-  }
-
-  private Map<Integer, Rule> rulesById(Collection<Rule> rules) {
-    Map<Integer, Rule> rulesById = newHashMap();
-    for (Rule rule : rules) {
-      rulesById.put(rule.getId(), rule);
-    }
-    return rulesById;
-  }
-
-}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProvider.java b/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProvider.java
deleted file mode 100644 (file)
index 24ea9b6..0000000
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import org.picocontainer.injectors.ProviderAdapter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-import org.sonar.api.utils.TimeProfiler;
-
-public class TechnicalDebtModelProvider extends ProviderAdapter {
-
-  private static final Logger LOG = LoggerFactory.getLogger(TechnicalDebtModelProvider.class);
-
-  private TechnicalDebtModel model;
-
-  public TechnicalDebtModel provide(TechnicalDebtModelLoader loader) {
-    if (model == null) {
-      TimeProfiler profiler = new TimeProfiler(LOG).start("Loading technical debt model");
-      model = loader.load();
-      profiler.stop();
-    }
-    return model;
-  }
-}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/technicaldebt/package-info.java
deleted file mode 100644 (file)
index f450854..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-/*
- * 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.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.batch.technicaldebt;
-
-import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/DebtModelLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/DebtModelLoaderTest.java
new file mode 100644 (file)
index 0000000..1640363
--- /dev/null
@@ -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.batch.debt;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.rules.RuleQuery;
+import org.sonar.api.technicaldebt.batch.internal.DefaultCharacteristic;
+import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
+import org.sonar.api.utils.WorkUnit;
+import org.sonar.core.technicaldebt.DefaultTechnicalDebtModel;
+import org.sonar.core.technicaldebt.db.CharacteristicDao;
+import org.sonar.core.technicaldebt.db.CharacteristicDto;
+
+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.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class DebtModelLoaderTest {
+
+  @Mock
+  CharacteristicDao dao;
+
+  @Mock
+  RuleFinder ruleFinder;
+
+  DebtModelLoader loader;
+
+  @Before
+  public void before() {
+    loader = new DebtModelLoader(dao, ruleFinder);
+  }
+
+  @Test
+  public void find_all() throws Exception {
+    CharacteristicDto rootCharacteristicDto = new CharacteristicDto()
+      .setId(1)
+      .setKey("MEMORY_EFFICIENCY")
+      .setName("Memory use");
+
+    CharacteristicDto characteristicDto = new CharacteristicDto()
+      .setId(2)
+      .setKey("EFFICIENCY")
+      .setName("Efficiency")
+      .setParentId(1);
+
+    CharacteristicDto requirementDto = new CharacteristicDto()
+      .setId(3)
+      .setParentId(2)
+      .setRuleId(100)
+      .setFunction("linear")
+      .setFactorValue(2d)
+      .setFactorUnit(WorkUnit.DAYS)
+      .setOffsetValue(0d)
+      .setOffsetUnit(WorkUnit.DEFAULT_UNIT);
+
+    RuleKey ruleKey = RuleKey.of("checkstyle", "Regexp");
+    Rule rule = Rule.create(ruleKey.repository(), ruleKey.rule());
+    rule.setId(100);
+    when(ruleFinder.findAll(any(RuleQuery.class))).thenReturn(newArrayList(rule));
+    when(dao.selectEnabledCharacteristics()).thenReturn(newArrayList(rootCharacteristicDto, characteristicDto, requirementDto));
+
+    DefaultTechnicalDebtModel result = (DefaultTechnicalDebtModel) loader.load();
+    assertThat(result.rootCharacteristics()).hasSize(1);
+
+    DefaultCharacteristic rootCharacteristic = result.characteristicByKey("MEMORY_EFFICIENCY");
+    assertThat(rootCharacteristic.key()).isEqualTo("MEMORY_EFFICIENCY");
+    assertThat(rootCharacteristic.name()).isEqualTo("Memory use");
+    assertThat(rootCharacteristic.parent()).isNull();
+    assertThat(rootCharacteristic.requirements()).isEmpty();
+    assertThat(rootCharacteristic.children()).hasSize(1);
+    assertThat(rootCharacteristic.children().get(0).key()).isEqualTo("EFFICIENCY");
+
+    DefaultCharacteristic characteristic = result.characteristicByKey("EFFICIENCY");
+    assertThat(characteristic.key()).isEqualTo("EFFICIENCY");
+    assertThat(characteristic.name()).isEqualTo("Efficiency");
+    assertThat(characteristic.parent().key()).isEqualTo("MEMORY_EFFICIENCY");
+    assertThat(characteristic.children()).isEmpty();
+    assertThat(characteristic.requirements()).hasSize(1);
+    assertThat(characteristic.requirements().get(0).ruleKey()).isEqualTo(ruleKey);
+
+    DefaultRequirement requirement = result.requirementsByRule(ruleKey);
+    assertThat(requirement.ruleKey()).isEqualTo(ruleKey);
+    assertThat(requirement.function()).isEqualTo("linear");
+    assertThat(requirement.factor()).isEqualTo(WorkUnit.create(2d, WorkUnit.DAYS));
+    assertThat(requirement.offset()).isEqualTo(WorkUnit.create(0d, WorkUnit.DAYS));
+  }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/RuleDebtCalculatorTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/RuleDebtCalculatorTest.java
new file mode 100644 (file)
index 0000000..820ea6f
--- /dev/null
@@ -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.batch.debt;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
+import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
+import org.sonar.api.utils.WorkUnit;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+@RunWith(MockitoJUnitRunner.class)
+public class RuleDebtCalculatorTest {
+
+  private static final int HOURS_IN_DAY = 8;
+  @Mock
+  TechnicalDebtModel model;
+
+  WorkUnit tenMinutes = WorkUnit.create(10d, WorkUnit.MINUTES);
+  WorkUnit fiveMinutes = WorkUnit.create(5d, WorkUnit.MINUTES);
+
+  RuleDebtCalculator calculator;
+
+  @Before
+  public void before() {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, HOURS_IN_DAY);
+    calculator = new RuleDebtCalculator(model, new WorkDurationFactory(settings));
+  }
+
+  @Test
+  public void calculate_technical_debt() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
+
+    DefaultRequirement requirement = mock(DefaultRequirement.class);
+    Mockito.when(requirement.function()).thenReturn("constant_issue");
+    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
+    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
+    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
+
+    assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isEqualTo(
+      WorkDuration.createFromValueAndUnit(15, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+  }
+
+  @Test
+  public void calculate_technical_debt_with_effort_to_fix() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
+
+    DefaultRequirement requirement = mock(DefaultRequirement.class);
+    Mockito.when(requirement.function()).thenReturn("linear_offset");
+    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
+    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
+    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
+
+    assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isEqualTo(
+      WorkDuration.createFromValueAndUnit((10 * 2) + 5, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+  }
+
+  @Test
+  public void calculate_technical_debt_with_no_offset() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
+
+    DefaultRequirement requirement = mock(DefaultRequirement.class);
+    Mockito.when(requirement.function()).thenReturn("linear");
+    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
+    Mockito.when(requirement.offset()).thenReturn(null);
+    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
+
+    assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isEqualTo(
+      WorkDuration.createFromValueAndUnit((10 * 2), WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+  }
+
+  @Test
+  public void calculate_technical_debt_with_no_factor() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
+
+    DefaultRequirement requirement = mock(DefaultRequirement.class);
+    Mockito.when(requirement.function()).thenReturn("constant_issue");
+    Mockito.when(requirement.factor()).thenReturn(null);
+    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
+    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
+
+    assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isEqualTo(
+      WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+  }
+
+  @Test
+  public void no_technical_debt_if_requirement_not_found() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
+    when(model.requirementsByRule(ruleKey)).thenReturn(null);
+
+    assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isNull();
+  }
+
+  @Test
+  public void fail_to_calculate_technical_debt_on_constant_issue_function_with_effort_to_fix() throws Exception {
+    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
+    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
+
+    DefaultRequirement requirement = mock(DefaultRequirement.class);
+    Mockito.when(requirement.function()).thenReturn("constant_issue");
+    Mockito.when(requirement.factor()).thenReturn(null);
+    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
+    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
+
+    try {
+      assertThat(calculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).isEqualTo(
+        WorkDuration.createFromValueAndUnit(15, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+      fail();
+    } catch (Exception e) {
+      assertThat(e).isInstanceOf(IllegalArgumentException.class)
+        .hasMessage("Requirement for 'squid:AvoidCycle' can not use 'Constant/issue' remediation function because this rule does not have a fixed remediation cost.");
+    }
+  }
+
+}
+
diff --git a/sonar-batch/src/test/java/org/sonar/batch/debt/TechnicalDebtModelProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/debt/TechnicalDebtModelProviderTest.java
new file mode 100644 (file)
index 0000000..05747f8
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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.batch.debt;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.*;
+
+@RunWith(MockitoJUnitRunner.class)
+public class TechnicalDebtModelProviderTest {
+
+  @Mock
+  DebtModelLoader loader;
+
+  @Test
+  public void load_model() {
+    TechnicalDebtModel model = mock(TechnicalDebtModel.class);
+    when(loader.load()).thenReturn(model);
+
+    DebtModelProvider provider = new DebtModelProvider();
+    TechnicalDebtModel result = provider.provide(loader);
+    assertThat(result).isNotNull();
+  }
+
+  @Test
+  public void load_model_only_once() {
+    TechnicalDebtModel model = mock(TechnicalDebtModel.class);
+    when(loader.load()).thenReturn(model);
+
+    DebtModelProvider provider = new DebtModelProvider();
+    provider.provide(loader);
+    verify(loader).load();
+
+    provider.provide(loader);
+    verifyZeroInteractions(loader);
+  }
+}
index 532d056a306bebd1477371beeb6242f3a8458274..e0bb79cccddf92016f374ae73c1145741db68548 100644 (file)
@@ -35,8 +35,8 @@ import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
 import org.sonar.api.rules.*;
 import org.sonar.api.utils.MessageException;
-import org.sonar.api.utils.WorkUnit;
-import org.sonar.batch.technicaldebt.TechnicalDebtCalculator;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.batch.debt.RuleDebtCalculator;
 
 import java.util.Calendar;
 import java.util.Date;
@@ -67,7 +67,7 @@ public class ModuleIssuesTest {
   IssueFilters filters;
 
   @Mock
-  TechnicalDebtCalculator technicalDebtCalculator;
+  RuleDebtCalculator technicalDebtCalculator;
 
   @Mock
   RuleFinder ruleFinder;
@@ -284,14 +284,15 @@ public class ModuleIssuesTest {
       .setRuleKey(SQUID_RULE_KEY)
       .setSeverity(Severity.CRITICAL);
 
-    when(technicalDebtCalculator.calculTechnicalDebt(issue)).thenReturn(new WorkUnit.Builder().setDays(10).build());
+    WorkDuration debt = WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.DAYS, 8);
+    when(technicalDebtCalculator.calculateTechnicalDebt(issue.ruleKey(), issue.effortToFix())).thenReturn(debt);
     when(filters.accept(issue, null)).thenReturn(true);
 
     moduleIssues.initAndAddIssue(issue);
 
     ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
     verify(cache).put(argument.capture());
-    assertThat(argument.getValue().technicalDebt()).isEqualTo(new WorkUnit.Builder().setDays(10).build());
+    assertThat(argument.getValue().technicalDebt()).isEqualTo(debt);
   }
 
 }
diff --git a/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculatorTest.java b/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtCalculatorTest.java
deleted file mode 100644 (file)
index 684f442..0000000
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.config.Settings;
-import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
-import org.sonar.api.utils.WorkUnit;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TechnicalDebtCalculatorTest {
-
-  @Mock
-  TechnicalDebtModel model;
-
-  WorkUnit tenMinutes = new WorkUnit.Builder().setMinutes(10).build();
-  WorkUnit fiveMinutes = new WorkUnit.Builder().setMinutes(5).build();
-
-  TechnicalDebtCalculator remediationCostCalculator;
-
-  @Before
-  public void before() {
-    Settings settings = new Settings();
-    settings.setProperty(CoreProperties.HOURS_IN_DAY, 8);
-
-    remediationCostCalculator = new TechnicalDebtCalculator(model, settings);
-  }
-
-  @Test
-  public void calcul_technical_debt() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
-
-    DefaultRequirement requirement = mock(DefaultRequirement.class);
-    Mockito.when(requirement.function()).thenReturn("constant_issue");
-    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
-    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
-    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
-
-    assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isEqualTo(new WorkUnit.Builder().setMinutes(15).build());
-  }
-
-  @Test
-  public void calcul_technical_debt_with_effort_to_fix() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
-
-    DefaultRequirement requirement = mock(DefaultRequirement.class);
-    Mockito.when(requirement.function()).thenReturn("linear_offset");
-    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
-    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
-    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
-
-    assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isEqualTo(new WorkUnit.Builder().setMinutes((10 * 2) + 5).build());
-  }
-
-  @Test
-  public void calcul_technical_debt_with_no_offset() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
-
-    DefaultRequirement requirement = mock(DefaultRequirement.class);
-    Mockito.when(requirement.function()).thenReturn("linear");
-    Mockito.when(requirement.factor()).thenReturn(tenMinutes);
-    Mockito.when(requirement.offset()).thenReturn(null);
-    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
-
-    assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isEqualTo(new WorkUnit.Builder().setMinutes(10 * 2).build());
-  }
-
-  @Test
-  public void calcul_technical_debt_with_no_factor() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
-
-    DefaultRequirement requirement = mock(DefaultRequirement.class);
-    Mockito.when(requirement.function()).thenReturn("constant_issue");
-    Mockito.when(requirement.factor()).thenReturn(null);
-    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
-    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
-
-    assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isEqualTo(new WorkUnit.Builder().setMinutes(5).build());
-  }
-
-  @Test
-  public void no_technical_debt_if_requirement_not_found() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey);
-    when(model.requirementsByRule(ruleKey)).thenReturn(null);
-
-    assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isNull();
-  }
-
-  @Test
-  public void fail_to_calcul_technical_debt_on_constant_issue_function_with_effort_to_fix() throws Exception {
-    RuleKey ruleKey = RuleKey.of("squid", "AvoidCycle");
-    DefaultIssue issue = new DefaultIssue().setKey("ABCDE").setRuleKey(ruleKey).setEffortToFix(2d);
-
-    DefaultRequirement requirement = mock(DefaultRequirement.class);
-    Mockito.when(requirement.function()).thenReturn("constant_issue");
-    Mockito.when(requirement.factor()).thenReturn(null);
-    Mockito.when(requirement.offset()).thenReturn(fiveMinutes);
-    when(model.requirementsByRule(ruleKey)).thenReturn(requirement);
-
-    try {
-      assertThat(remediationCostCalculator.calculTechnicalDebt(issue)).isEqualTo(new WorkUnit.Builder().setMinutes(15).build());
-      fail();
-    } catch (Exception e) {
-      assertThat(e).isInstanceOf(IllegalArgumentException.class)
-        .hasMessage("Requirement for 'squid:AvoidCycle' can not use 'Constant/issue' remediation function because this rule does not have a fixed remediation cost.");
-    }
-  }
-
-}
-
diff --git a/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelLoaderTest.java
deleted file mode 100644 (file)
index b60bb58..0000000
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.sonar.api.rule.RuleKey;
-import org.sonar.api.rules.Rule;
-import org.sonar.api.rules.RuleFinder;
-import org.sonar.api.rules.RuleQuery;
-import org.sonar.api.technicaldebt.batch.internal.DefaultCharacteristic;
-import org.sonar.api.technicaldebt.batch.internal.DefaultRequirement;
-import org.sonar.api.utils.WorkUnit;
-import org.sonar.core.technicaldebt.DefaultTechnicalDebtModel;
-import org.sonar.core.technicaldebt.db.CharacteristicDao;
-import org.sonar.core.technicaldebt.db.CharacteristicDto;
-
-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.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TechnicalDebtModelLoaderTest {
-
-  @Mock
-  CharacteristicDao dao;
-
-  @Mock
-  RuleFinder ruleFinder;
-
-  TechnicalDebtModelLoader loader;
-
-  @Before
-  public void before() {
-    loader = new TechnicalDebtModelLoader(dao, ruleFinder);
-  }
-
-  @Test
-  public void find_all() throws Exception {
-    CharacteristicDto rootCharacteristicDto = new CharacteristicDto()
-      .setId(1)
-      .setKey("MEMORY_EFFICIENCY")
-      .setName("Memory use");
-
-    CharacteristicDto characteristicDto = new CharacteristicDto()
-      .setId(2)
-      .setKey("EFFICIENCY")
-      .setName("Efficiency")
-      .setParentId(1);
-
-    CharacteristicDto requirementDto = new CharacteristicDto()
-      .setId(3)
-      .setParentId(2)
-      .setRuleId(100)
-      .setFunction("linear")
-      .setFactorValue(2d)
-      .setFactorUnit(WorkUnit.DAYS)
-      .setOffsetValue(0d)
-      .setOffsetUnit(WorkUnit.DEFAULT_UNIT);
-
-    RuleKey ruleKey = RuleKey.of("checkstyle", "Regexp");
-    Rule rule = Rule.create(ruleKey.repository(), ruleKey.rule());
-    rule.setId(100);
-    when(ruleFinder.findAll(any(RuleQuery.class))).thenReturn(newArrayList(rule));
-    when(dao.selectEnabledCharacteristics()).thenReturn(newArrayList(rootCharacteristicDto, characteristicDto, requirementDto));
-
-    DefaultTechnicalDebtModel result = (DefaultTechnicalDebtModel) loader.load();
-    assertThat(result.rootCharacteristics()).hasSize(1);
-
-    DefaultCharacteristic rootCharacteristic = result.characteristicByKey("MEMORY_EFFICIENCY");
-    assertThat(rootCharacteristic.key()).isEqualTo("MEMORY_EFFICIENCY");
-    assertThat(rootCharacteristic.name()).isEqualTo("Memory use");
-    assertThat(rootCharacteristic.parent()).isNull();
-    assertThat(rootCharacteristic.requirements()).isEmpty();
-    assertThat(rootCharacteristic.children()).hasSize(1);
-    assertThat(rootCharacteristic.children().get(0).key()).isEqualTo("EFFICIENCY");
-
-    DefaultCharacteristic characteristic = result.characteristicByKey("EFFICIENCY");
-    assertThat(characteristic.key()).isEqualTo("EFFICIENCY");
-    assertThat(characteristic.name()).isEqualTo("Efficiency");
-    assertThat(characteristic.parent().key()).isEqualTo("MEMORY_EFFICIENCY");
-    assertThat(characteristic.children()).isEmpty();
-    assertThat(characteristic.requirements()).hasSize(1);
-    assertThat(characteristic.requirements().get(0).ruleKey()).isEqualTo(ruleKey);
-
-    DefaultRequirement requirement = result.requirementsByRule(ruleKey);
-    assertThat(requirement.ruleKey()).isEqualTo(ruleKey);
-    assertThat(requirement.function()).isEqualTo("linear");
-    assertThat(requirement.factor()).isEqualTo(new WorkUnit.Builder().setDays(2).build());
-    assertThat(requirement.offset()).isEqualTo(new WorkUnit.Builder().setDays(0).build());
-  }
-
-}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProviderTest.java b/sonar-batch/src/test/java/org/sonar/batch/technicaldebt/TechnicalDebtModelProviderTest.java
deleted file mode 100644 (file)
index e4bee8d..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * 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.batch.technicaldebt;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-import org.sonar.api.technicaldebt.batch.TechnicalDebtModel;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.mockito.Mockito.*;
-
-@RunWith(MockitoJUnitRunner.class)
-public class TechnicalDebtModelProviderTest {
-
-  @Mock
-  TechnicalDebtModelLoader loader;
-
-  @Test
-  public void load_model() {
-    TechnicalDebtModel model = mock(TechnicalDebtModel.class);
-    when(loader.load()).thenReturn(model);
-
-    TechnicalDebtModelProvider provider = new TechnicalDebtModelProvider();
-    TechnicalDebtModel result = provider.provide(loader);
-    assertThat(result).isNotNull();
-  }
-
-  @Test
-  public void load_model_only_once() {
-    TechnicalDebtModel model = mock(TechnicalDebtModel.class);
-    when(loader.load()).thenReturn(model);
-
-    TechnicalDebtModelProvider provider = new TechnicalDebtModelProvider();
-    provider.provide(loader);
-    verify(loader).load();
-
-    provider.provide(loader);
-    verifyZeroInteractions(loader);
-  }
-}
index 538104db87cfa50e81ec8fc2a0ab49db9c182580..4d25d12d296efc1965257d3c9227d7d3879b2f9f 100644 (file)
@@ -29,7 +29,7 @@ import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.DefaultIssueComment;
 import org.sonar.api.issue.internal.IssueChangeContext;
 import org.sonar.api.user.User;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 
 import javax.annotation.Nullable;
 
@@ -200,8 +200,8 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
     return setEffortToFix(issue, currentEffort, context);
   }
 
-  public boolean setTechnicalDebt(DefaultIssue issue, @Nullable WorkUnit value, IssueChangeContext context) {
-    WorkUnit oldValue = issue.technicalDebt();
+  public boolean setTechnicalDebt(DefaultIssue issue, @Nullable WorkDuration value, IssueChangeContext context) {
+    WorkDuration oldValue = issue.technicalDebt();
     if (!Objects.equal(value, oldValue)) {
       issue.setTechnicalDebt(value);
       issue.setFieldChange(context, TECHNICAL_DEBT, oldValue != null ? oldValue.toLong() : null, value != null ? value.toLong() : null);
@@ -212,8 +212,8 @@ public class IssueUpdater implements BatchComponent, ServerComponent {
     return false;
   }
 
-  public boolean setPastTechnicalDebt(DefaultIssue issue, @Nullable WorkUnit previousTechnicalDebt, IssueChangeContext context) {
-    WorkUnit currentTechnicalDebt = issue.technicalDebt();
+  public boolean setPastTechnicalDebt(DefaultIssue issue, @Nullable WorkDuration previousTechnicalDebt, IssueChangeContext context) {
+    WorkDuration currentTechnicalDebt = issue.technicalDebt();
     issue.setTechnicalDebt(previousTechnicalDebt);
     return setTechnicalDebt(issue, currentTechnicalDebt, context);
   }
index 3e34ad2c738888891bb2aef71bb238146670be00..46ef306b3bd803db44c8e1373702b85eccf5147b 100644 (file)
@@ -26,7 +26,7 @@ import org.apache.commons.lang.builder.ToStringStyle;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.utils.KeyValueFormat;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
@@ -363,12 +363,13 @@ public final class IssueDto implements Serializable {
   }
 
   public static IssueDto toDtoForInsert(DefaultIssue issue, Long componentId, Long rootComponentId, Integer ruleId, Date now) {
+    WorkDuration debt = issue.technicalDebt();
     return new IssueDto()
       .setKee(issue.key())
       .setLine(issue.line())
       .setMessage(issue.message())
       .setEffortToFix(issue.effortToFix())
-      .setTechnicalDebt(issue.technicalDebt() != null ? issue.technicalDebt().toLong() : null)
+      .setTechnicalDebt(debt != null ? debt.toLong() : null)
       .setResolution(issue.resolution())
       .setStatus(issue.status())
       .setSeverity(issue.severity())
@@ -392,12 +393,13 @@ public final class IssueDto implements Serializable {
 
   public static IssueDto toDtoForUpdate(DefaultIssue issue, Date now) {
     // Invariant fields, like key and rule, can't be updated
+    WorkDuration debt = issue.technicalDebt();
     return new IssueDto()
       .setKee(issue.key())
       .setLine(issue.line())
       .setMessage(issue.message())
       .setEffortToFix(issue.effortToFix())
-      .setTechnicalDebt(issue.technicalDebt() != null ? issue.technicalDebt().toLong() : null)
+      .setTechnicalDebt(debt != null ? debt.toLong() : null)
       .setResolution(issue.resolution())
       .setStatus(issue.status())
       .setSeverity(issue.severity())
@@ -415,14 +417,14 @@ public final class IssueDto implements Serializable {
       .setUpdatedAt(now);
   }
 
-  public DefaultIssue toDefaultIssue() {
+  public DefaultIssue toDefaultIssue(@Nullable WorkDuration debt) {
     DefaultIssue issue = new DefaultIssue();
     issue.setKey(kee);
     issue.setStatus(status);
     issue.setResolution(resolution);
     issue.setMessage(message);
     issue.setEffortToFix(effortToFix);
-    issue.setTechnicalDebt(technicalDebt != null ? WorkUnit.fromLong(technicalDebt) : null);
+    issue.setTechnicalDebt(debt);
     issue.setLine(line);
     issue.setSeverity(severity);
     issue.setReporter(reporter);
index 3a78a4430109a2cc6ae866318c1cb2a736f2bf63..ff668b918f00a0663e0ac95ca2a819659aa517f9 100644 (file)
@@ -25,7 +25,7 @@ import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.FieldDiffs;
 import org.sonar.api.issue.internal.IssueChangeContext;
 import org.sonar.api.user.User;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.core.user.DefaultUser;
 
 import java.util.Date;
@@ -366,42 +366,44 @@ public class IssueUpdaterTest {
 
   @Test
   public void set_past_technical_debt() throws Exception {
-    issue.setTechnicalDebt(new WorkUnit.Builder().setDays(15).build());
-    WorkUnit previousDebt = new WorkUnit.Builder().setDays(10).build();
+    WorkDuration newDebt = WorkDuration.createFromValueAndUnit(15, WorkDuration.UNIT.DAYS, 8);
+    WorkDuration previousDebt = WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.DAYS, 8);
+    issue.setTechnicalDebt(newDebt);
     boolean updated = updater.setPastTechnicalDebt(issue, previousDebt, context);
     assertThat(updated).isTrue();
-    assertThat(issue.technicalDebt()).isEqualTo(new WorkUnit.Builder().setDays(15).build());
+    assertThat(issue.technicalDebt()).isEqualTo(newDebt);
     assertThat(issue.mustSendNotifications()).isFalse();
 
     FieldDiffs.Diff diff = issue.currentChange().get(TECHNICAL_DEBT);
-    assertThat(diff.oldValue()).isEqualTo(new WorkUnit.Builder().setDays(10).build().toLong());
-    assertThat(diff.newValue()).isEqualTo(new WorkUnit.Builder().setDays(15).build().toLong());
+    assertThat(diff.oldValue()).isEqualTo(previousDebt.toLong());
+    assertThat(diff.newValue()).isEqualTo(newDebt.toLong());
   }
 
   @Test
   public void set_past_technical_debt_without_previous_value() throws Exception {
-    issue.setTechnicalDebt(new WorkUnit.Builder().setDays(15).build());
+    WorkDuration newDebt = WorkDuration.createFromValueAndUnit(15, WorkDuration.UNIT.DAYS, 8);
+    issue.setTechnicalDebt(newDebt);
     boolean updated = updater.setPastTechnicalDebt(issue, null, context);
     assertThat(updated).isTrue();
-    assertThat(issue.technicalDebt()).isEqualTo(new WorkUnit.Builder().setDays(15).build());
+    assertThat(issue.technicalDebt()).isEqualTo(newDebt);
     assertThat(issue.mustSendNotifications()).isFalse();
 
     FieldDiffs.Diff diff = issue.currentChange().get(TECHNICAL_DEBT);
     assertThat(diff.oldValue()).isNull();
-    assertThat(diff.newValue()).isEqualTo(new WorkUnit.Builder().setDays(15).build().toLong());
+    assertThat(diff.newValue()).isEqualTo(newDebt.toLong());
   }
 
   @Test
   public void set_past_technical_debt_with_null_new_value() throws Exception {
     issue.setTechnicalDebt(null);
-    WorkUnit previousDebt = new WorkUnit.Builder().setDays(10).build();
+    WorkDuration previousDebt = WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.DAYS, 8);
     boolean updated = updater.setPastTechnicalDebt(issue, previousDebt, context);
     assertThat(updated).isTrue();
     assertThat(issue.technicalDebt()).isNull();
     assertThat(issue.mustSendNotifications()).isFalse();
 
     FieldDiffs.Diff diff = issue.currentChange().get(TECHNICAL_DEBT);
-    assertThat(diff.oldValue()).isEqualTo(new WorkUnit.Builder().setDays(10).build().toLong());
+    assertThat(diff.oldValue()).isEqualTo(previousDebt.toLong());
     assertThat(diff.newValue()).isNull();
   }
 
index e87ffb98153c3c6339795fee0b0b6401e34508f5..5d505dea49cfa919010c139e6b50ab1146f85510 100644 (file)
@@ -25,7 +25,7 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.internal.DefaultIssue;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 
 import java.util.Calendar;
 import java.util.Date;
@@ -79,7 +79,7 @@ public class IssueDtoTest {
       .setIssueUpdateDate(updatedAt)
       .setIssueCloseDate(closedAt);
 
-    DefaultIssue issue = dto.toDefaultIssue();
+    DefaultIssue issue = dto.toDefaultIssue(WorkDuration.create(10, 10, 10, 8));
     assertThat(issue.key()).isEqualTo("100");
     assertThat(issue.ruleKey().toString()).isEqualTo("squid:AvoidCycle");
     assertThat(issue.componentKey()).isEqualTo("org.sonar.sample:Sample");
@@ -87,7 +87,7 @@ public class IssueDtoTest {
     assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED);
     assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
     assertThat(issue.effortToFix()).isEqualTo(15.0);
-    assertThat(issue.technicalDebt()).isEqualTo(new WorkUnit.Builder().setDays(10).setHours(10).setMinutes(10).build());
+    assertThat(issue.technicalDebt()).isNotNull();
     assertThat(issue.line()).isEqualTo(6);
     assertThat(issue.severity()).isEqualTo("BLOCKER");
     assertThat(issue.message()).isEqualTo("message");
index 5c8b3487f8ef11044a38626e57dfb216eef66719..ba9b2eddc7d9a0c00e7cef4929a9c0585ff9742f 100644 (file)
@@ -28,7 +28,7 @@ import org.sonar.api.rules.Rule;
 import org.sonar.api.rules.RuleFinder;
 import org.sonar.api.rules.RuleQuery;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.core.persistence.AbstractDaoTestCase;
 import org.sonar.core.persistence.MyBatis;
 
@@ -54,7 +54,7 @@ public class IssueStorageTest extends AbstractDaoTestCase {
 
       .setRuleKey(RuleKey.of("squid", "AvoidCycle"))
       .setLine(5000)
-      .setTechnicalDebt(new WorkUnit.Builder().setMinutes(10).build())
+      .setTechnicalDebt(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, 8))
       .setReporter("emmerik")
       .setResolution("OPEN")
       .setStatus("OPEN")
@@ -88,7 +88,7 @@ public class IssueStorageTest extends AbstractDaoTestCase {
 
         // updated fields
       .setLine(5000)
-      .setTechnicalDebt(new WorkUnit.Builder().setMinutes(10).build())
+      .setTechnicalDebt(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, 8))
       .setChecksum("FFFFF")
       .setAuthorLogin("simon")
       .setAssignee("loic")
index 69877f818984fd71ace6a9d186013878406d5b2c..2e66f1ebd55818e4c4c2d8cbfc49c516809df860 100644 (file)
@@ -89,8 +89,8 @@ public class DefaultTechnicalDebtManagerTest {
     assertThat(result.rootId()).isEqualTo(1);
     assertThat(result.ruleKey()).isEqualTo(RuleKey.of("repo", "key"));
     assertThat(result.function()).isEqualTo("linear");
-    assertThat(result.factor()).isEqualTo(new WorkUnit.Builder().setMinutes(30).build());
-    assertThat(result.offset()).isEqualTo(new WorkUnit.Builder().setDays(0).build());
+    assertThat(result.factor()).isEqualTo(WorkUnit.create(30d, WorkUnit.MINUTES));
+    assertThat(result.offset()).isEqualTo(WorkUnit.create(0d, WorkUnit.DAYS));
   }
 
   @Test
@@ -149,8 +149,8 @@ public class DefaultTechnicalDebtManagerTest {
     assertThat(result.rootId()).isEqualTo(1);
     assertThat(result.ruleKey()).isEqualTo(RuleKey.of("repo", "key"));
     assertThat(result.function()).isEqualTo("linear");
-    assertThat(result.factor()).isEqualTo(new WorkUnit.Builder().setMinutes(30).build());
-    assertThat(result.offset()).isEqualTo(new WorkUnit.Builder().setDays(0).build());
+    assertThat(result.factor()).isEqualTo(WorkUnit.create(30d, WorkUnit.MINUTES));
+    assertThat(result.offset()).isEqualTo(WorkUnit.create(0d, WorkUnit.DAYS));
   }
 
   @Test
index 4177c144c6440f5a1af5fa9a9e9391ef144764e2..dcf61654fc317a882dc706c60189ae308dd1ab86 100644 (file)
@@ -92,8 +92,8 @@ public class DefaultTechnicalDebtModelTest {
       .setCharacteristic(characteristic)
       .setRuleKey(ruleKey)
       .setFunction("linear")
-      .setFactor(new WorkUnit.Builder().setHours(2).build())
-      .setOffset(new WorkUnit.Builder().setHours(0).build());
+      .setFactor(WorkUnit.create(2d, WorkUnit.HOURS))
+      .setOffset(WorkUnit.create(0d, WorkUnit.HOURS));
 
     sqaleModel.addRootCharacteristic(rootCharacteristic);
 
index 17e4bee2e8aeaa510ea0e46e065fa3c8a73be9f0..774f80d890d43590c3c2c998ea16b85f8a8fc50c 100644 (file)
@@ -142,7 +142,7 @@ public class TechnicalDebtModelSynchronizerTest {
     RuleKey ruleKey = RuleKey.of("checkstyle", "import");
     when(ruleCache.getByRuleKey(ruleKey)).thenReturn(rule);
     new DefaultRequirement().setRuleKey(ruleKey)
-      .setFunction("linear").setFactor(new WorkUnit.Builder().setMinutes(30).build()).setCharacteristic(javaCharacteristic).setRootCharacteristic(javaRootCharacteristic);
+      .setFunction("linear").setFactor(WorkUnit.create(30d, WorkUnit.MINUTES)).setCharacteristic(javaCharacteristic).setRootCharacteristic(javaRootCharacteristic);
 
     Reader javaModelReader = mock(Reader.class);
     when(xmlImporter.importXML(eq(javaModelReader), any(ValidationMessages.class), eq(ruleCache))).thenReturn(javaModel);
@@ -195,7 +195,7 @@ public class TechnicalDebtModelSynchronizerTest {
 
     // New requirement
     new DefaultRequirement().setRuleKey(ruleKey2)
-      .setFunction("linear").setFactor(new WorkUnit.Builder().setHours(1).build()).setCharacteristic(javaCharacteristic).setRootCharacteristic(javaRootCharacteristic);
+      .setFunction("linear").setFactor(WorkUnit.create(1d, WorkUnit.HOURS)).setCharacteristic(javaCharacteristic).setRootCharacteristic(javaRootCharacteristic);
 
     Reader javaModelReader = mock(Reader.class);
     when(technicalDebtModelRepository.createReaderForXMLFile("java")).thenReturn(javaModelReader);
index d614d7588a0d9a2070a7fd09da5ba3b16223b51d..5dad54e04308c83a48e62ae39e8543b26591e734 100644 (file)
@@ -189,7 +189,7 @@ public class TechnicalDebtXMLImporterTest {
   }
 
   private void checkXmlCorrectlyImported(DefaultTechnicalDebtModel sqale, ValidationMessages messages) {
-    checkXmlCorrectlyImported(sqale, new WorkUnit.Builder().setDays(0).build(), messages);
+    checkXmlCorrectlyImported(sqale, WorkUnit.create(0d, WorkUnit.DAYS), messages);
   }
 
   private void checkXmlCorrectlyImported(DefaultTechnicalDebtModel sqale, WorkUnit offset, ValidationMessages messages) {
index dcb7e1a7bebca34476afdb20be5ea52a8d3d8032..6c13c90c9b93ec2abf0b6a255894249dbca3c221 100644 (file)
@@ -33,7 +33,7 @@ import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueComment;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.rule.Severity;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
@@ -59,7 +59,7 @@ public class DefaultIssue implements Issue {
   private String message;
   private Integer line;
   private Double effortToFix;
-  private WorkUnit technicalDebt;
+  private WorkDuration technicalDebt;
   private String status;
   private String resolution;
   private String reporter;
@@ -196,11 +196,11 @@ public class DefaultIssue implements Issue {
    * Elapsed time to fix the issue
    */
   @CheckForNull
-  public WorkUnit technicalDebt() {
+  public WorkDuration technicalDebt() {
     return technicalDebt;
   }
 
-  public DefaultIssue setTechnicalDebt(@Nullable WorkUnit t) {
+  public DefaultIssue setTechnicalDebt(@Nullable WorkDuration t) {
     this.technicalDebt = t;
     return this;
   }
index 8f80e3fc8ecc48383ea266924a50e9a4e8bc4a27..d8fca31d1f27efb0d477e51e8c4c211c4ff128e7 100644 (file)
@@ -24,6 +24,7 @@ import org.apache.commons.lang.builder.ToStringBuilder;
 import org.apache.commons.lang.builder.ToStringStyle;
 import org.sonar.api.rule.RuleKey;
 import org.sonar.api.technicaldebt.batch.Requirement;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.api.utils.WorkUnit;
 
 import java.util.Date;
@@ -40,6 +41,10 @@ public class DefaultRequirement implements Requirement {
   private DefaultCharacteristic rootCharacteristic;
 
   private String function;
+  private int factorValue;
+  private WorkDuration.UNIT factorUnit;
+  private int offsetValue;
+  private WorkDuration.UNIT offsetUnit;
   private WorkUnit factor;
   private WorkUnit offset;
 
@@ -47,8 +52,8 @@ public class DefaultRequirement implements Requirement {
   private Date updatedAt;
 
   public DefaultRequirement() {
-    this.factor = new WorkUnit.Builder().setDays(0).build();
-    this.offset = new WorkUnit.Builder().setDays(0).build();
+    this.factor = WorkUnit.create(0d, WorkUnit.DAYS);
+    this.offset = WorkUnit.create(0d, WorkUnit.DAYS);
   }
 
   public Integer id() {
@@ -97,24 +102,77 @@ public class DefaultRequirement implements Requirement {
     return this;
   }
 
+  /**
+   * @deprecated since 4.2
+   */
+  @Deprecated
   public WorkUnit factor() {
     return factor;
+//    return WorkUnit.create((double) factorValue, fromUnit(factorUnit));
   }
 
+  /**
+   * @deprecated since 4.2
+   */
+  @Deprecated
   public DefaultRequirement setFactor(WorkUnit factor) {
     this.factor = factor;
     return this;
   }
 
+  /**
+   * @deprecated since 4.2
+   */
+  @Deprecated
   public WorkUnit offset() {
     return offset;
   }
 
+  /**
+   * @deprecated since 4.2
+   */
+  @Deprecated
   public DefaultRequirement setOffset(WorkUnit offset) {
     this.offset = offset;
     return this;
   }
 
+  public int factorValue() {
+    return factorValue;
+  }
+
+  public DefaultRequirement setFactorValue(int factorValue) {
+    this.factorValue = factorValue;
+    return this;
+  }
+
+  public WorkDuration.UNIT factorUnit() {
+    return factorUnit;
+  }
+
+  public DefaultRequirement setFactorUnit(WorkDuration.UNIT factorUnit) {
+    this.factorUnit = factorUnit;
+    return this;
+  }
+
+  public int offsetValue() {
+    return offsetValue;
+  }
+
+  public DefaultRequirement setOffsetValue(int offsetValue) {
+    this.offsetValue = offsetValue;
+    return this;
+  }
+
+  public WorkDuration.UNIT offsetUnit() {
+    return offsetUnit;
+  }
+
+  public DefaultRequirement setOffsetUnit(WorkDuration.UNIT offsetUnit) {
+    this.offsetUnit = offsetUnit;
+    return this;
+  }
+
   public Date createdAt() {
     return createdAt;
   }
@@ -133,6 +191,27 @@ public class DefaultRequirement implements Requirement {
     return this;
   }
 
+  private static WorkDuration.UNIT toUnit(String requirementUnit){
+    if (requirementUnit.equals(WorkUnit.DAYS)) {
+      return WorkDuration.UNIT.DAYS;
+    } else if (requirementUnit.equals(WorkUnit.HOURS)) {
+      return WorkDuration.UNIT.HOURS;
+    } else if (requirementUnit.equals(WorkUnit.MINUTES)) {
+      return WorkDuration.UNIT.MINUTES;
+    }
+    throw new IllegalStateException("Invalid unit : " + requirementUnit);
+  }
+
+  private static String fromUnit(WorkDuration.UNIT unit){
+    if (unit.equals(WorkDuration.UNIT.DAYS)) {
+      return WorkUnit.DAYS;
+    } else if (unit.equals(WorkDuration.UNIT.HOURS)) {
+      return WorkUnit.HOURS;
+    } else if (unit.equals(WorkDuration.UNIT.MINUTES)) {
+      return WorkUnit.MINUTES;
+    }
+    throw new IllegalStateException("Invalid unit : " + unit);
+  }
 
   @Override
   public String toString() {
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDuration.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDuration.java
new file mode 100644 (file)
index 0000000..e5d38b5
--- /dev/null
@@ -0,0 +1,197 @@
+/*
+ * 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.api.utils;
+
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+
+import javax.annotation.Nullable;
+
+import java.io.Serializable;
+
+/**
+ * @since 4.2
+ */
+public class WorkDuration implements Serializable {
+
+  static final int DAY_POSITION_IN_LONG = 10000;
+  static final int HOUR_POSITION_IN_LONG = 100;
+  static final int MINUTE_POSITION_IN_LONG = 1;
+
+  public static enum UNIT {DAYS, HOURS, MINUTES}
+
+  private int hoursInDay;
+
+  private long durationInSeconds;
+  private int days;
+  private int hours;
+  private int minutes;
+
+  private WorkDuration(long durationInSeconds, int days, int hours, int minutes, int hoursInDay) {
+    this.durationInSeconds = durationInSeconds;
+    this.days = days;
+    this.hours = hours;
+    this.minutes = minutes;
+    this.hoursInDay = hoursInDay;
+  }
+
+  public static WorkDuration create(int days, int hours, int minutes, int hoursInDay) {
+    long durationInSeconds = days * hoursInDay * 60 * 60;
+    durationInSeconds += hours * 60 * 60;
+    durationInSeconds += minutes * 60;
+    return new WorkDuration(durationInSeconds, days, hours, minutes, hoursInDay);
+  }
+
+  public static WorkDuration createFromValueAndUnit(int value, UNIT unit, int hoursInDay) {
+    switch (unit) {
+      case DAYS:
+        return create(value, 0, 0, hoursInDay);
+      case HOURS:
+        return create(0, value, 0, hoursInDay);
+      case MINUTES:
+        return create(0, 0, value, hoursInDay);
+      default:
+        throw new IllegalStateException("Cannot create work duration");
+    }
+  }
+
+  static WorkDuration createFromLong(long duration, int hoursInDay) {
+    int days = 0, hours = 0, minutes = 0;
+
+    long time = duration;
+    Long currentTime = time / WorkDuration.DAY_POSITION_IN_LONG;
+    if (currentTime > 0) {
+      days = (currentTime.intValue());
+      time = time - (currentTime * WorkDuration.DAY_POSITION_IN_LONG);
+    }
+
+    currentTime = time / WorkDuration.HOUR_POSITION_IN_LONG;
+    if (currentTime > 0) {
+      hours = currentTime.intValue();
+      time = time - (currentTime * WorkDuration.HOUR_POSITION_IN_LONG);
+    }
+
+    currentTime = time / WorkDuration.MINUTE_POSITION_IN_LONG;
+    if (currentTime > 0) {
+      minutes = currentTime.intValue();
+    }
+    return WorkDuration.create(days, hours, minutes, hoursInDay);
+  }
+
+  static WorkDuration createFromSeconds(long seconds, int hoursInDay) {
+    int days = (int) (seconds / hoursInDay / 60d / 60d);
+    long currentDurationInSeconds = seconds - (days * hoursInDay * 60 * 60);
+    int hours = (int) (currentDurationInSeconds / 60d / 60d);
+    currentDurationInSeconds = currentDurationInSeconds - (hours * 60 * 60);
+    int minutes = (int) (currentDurationInSeconds / 60d);
+    return new WorkDuration(seconds, days, hours, minutes, hoursInDay);
+  }
+
+  /**
+   * Return the duration in number of working days.
+   * For instance, 3 days and 4 hours will return 3.5 days (if hoursIndDay is 8).
+   */
+  public double toWorkingDays() {
+    return durationInSeconds / 60d / 60d / hoursInDay;
+  }
+
+  /**
+   * Return the duration using the following format DDHHMM, where DD is the number of days, HH is the number of months, and MM the number of minutes.
+   * For instance, 3 days and 4 hours will return 030400 (if hoursIndDay is 8).
+   */
+  public long toLong() {
+    int workingDays = days;
+    int workingHours = hours;
+    if (hours >= hoursInDay) {
+      int nbAdditionalDays = hours / hoursInDay;
+      workingDays += nbAdditionalDays;
+      workingHours = hours - (nbAdditionalDays * hoursInDay);
+    }
+    return workingDays * DAY_POSITION_IN_LONG + workingHours * HOUR_POSITION_IN_LONG + minutes * MINUTE_POSITION_IN_LONG;
+  }
+
+  public long toSeconds() {
+    return durationInSeconds;
+  }
+
+  public WorkDuration add(@Nullable WorkDuration with) {
+    if (with != null) {
+      return WorkDuration.createFromSeconds(this.toSeconds() + with.toSeconds(), this.hoursInDay);
+    } else {
+      return this;
+    }
+  }
+
+  public WorkDuration subtract(@Nullable WorkDuration with) {
+    if (with != null) {
+      return WorkDuration.createFromSeconds(this.toSeconds() - with.toSeconds(), this.hoursInDay);
+    } else {
+      return this;
+    }
+  }
+
+  public WorkDuration multiply(int factor) {
+    return WorkDuration.createFromSeconds(this.toSeconds() * factor, this.hoursInDay);
+  }
+
+  public int days() {
+    return days;
+  }
+
+  public int hours() {
+    return hours;
+  }
+
+  public int minutes() {
+    return minutes;
+  }
+
+  public int hoursInDay() {
+    return hoursInDay;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+
+    WorkDuration that = (WorkDuration) o;
+    if (durationInSeconds != that.durationInSeconds) {
+      return false;
+    }
+
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return (int) (durationInSeconds ^ (durationInSeconds >>> 32));
+  }
+
+  @Override
+  public String toString() {
+    return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
+  }
+}
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDurationFactory.java b/sonar-plugin-api/src/main/java/org/sonar/api/utils/WorkDurationFactory.java
new file mode 100644 (file)
index 0000000..c1b859d
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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.api.utils;
+
+import org.sonar.api.BatchComponent;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.ServerComponent;
+import org.sonar.api.config.Settings;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+/**
+ * @since 4.2
+ */
+public final class WorkDurationFactory implements BatchComponent, ServerComponent {
+
+  private final int hoursInDay;
+
+  public WorkDurationFactory(Settings settings) {
+    this.hoursInDay = settings.getInt(CoreProperties.HOURS_IN_DAY);
+  }
+
+  public WorkDuration createFromWorkingValue(int value, WorkDuration.UNIT unit) {
+    return WorkDuration.createFromValueAndUnit(value, unit, hoursInDay);
+  }
+
+  @CheckForNull
+  public WorkDuration createFromWorkingLong(@Nullable Long duration) {
+    if (duration == null) {
+      return null;
+    }
+    return WorkDuration.createFromLong(duration, hoursInDay);
+  }
+
+}
index 2686663765c3530d42665cf42faf3492c1c9d14b..0e62693d296e307ad7b5171aa88c6928d2fb3588 100644 (file)
@@ -29,39 +29,33 @@ import javax.annotation.Nullable;
 import java.io.Serializable;
 
 /**
- * @since 4.0
+ * @deprecated since 4.2. Use WorkDuration instead
  */
+@Deprecated
 public final class WorkUnit implements Serializable {
 
   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 = 0.0;
 
-  private static final String[] UNITS = {DAYS, MINUTES, HOURS};
-
-  private static final int DAY = 10000;
-  private static final int HOUR = 100;
-  private static final int MINUTE = 1;
+  private double value = 0d;
+  private String unit = DEFAULT_UNIT;
 
-  private int days;
-  private int hours;
-  private int minutes;
+  WorkUnit(double value, String unit) {
+    this.value = value;
+    this.unit = unit;
+  }
 
-  private WorkUnit(int days, int hours, int minutes) {
-    this.minutes = minutes;
-    this.hours = hours;
-    this.days = days;
+  public double getValue() {
+    return value;
   }
 
-  /**
-   * @deprecated since 4.2.
-   */
-  @Deprecated
-  public static WorkUnit create() {
-    return create(0d, DEFAULT_UNIT);
+  public String getUnit() {
+    return unit;
   }
 
   public static WorkUnit create(@Nullable Double value, @Nullable String unit) {
@@ -69,114 +63,15 @@ public final class WorkUnit implements Serializable {
     if (!ArrayUtils.contains(UNITS, defaultIfEmptyUnit)) {
       throw new IllegalArgumentException("Unit can not be: " + defaultIfEmptyUnit + ". Possible values are " + ArrayUtils.toString(UNITS));
     }
-    Double d = value != null ? value : DEFAULT_VALUE;
+    double d = value != null ? value : DEFAULT_VALUE;
     if (d < 0.0) {
       throw new IllegalArgumentException("Value can not be negative: " + d);
     }
-
-    int days = 0;
-    int hours = 0;
-    int minutes = 0;
-    if (DAYS.equals(unit)) {
-      days = d.intValue();
-    } else if (HOURS.equals(unit)) {
-      hours = d.intValue();
-    } else if (MINUTES.equals(unit)) {
-      minutes = d.intValue();
-    }
-    return new WorkUnit(days, hours, minutes);
-  }
-
-  public double getValue() {
-    if (days > 0) {
-      return days + (hours / 24) + (minutes / 60 / 24);
-    } else if (hours > 0) {
-      return hours + (minutes / 60);
-    } else {
-      return minutes;
-    }
-  }
-
-  public String getUnit() {
-    if (days > 0) {
-      return DAYS;
-    } else if (hours > 0) {
-      return HOURS;
-    } else {
-      return MINUTES;
-    }
-  }
-
-  /**
-   * @since 4.2
-   */
-  public int days() {
-    return days;
-  }
-
-  /**
-   * @since 4.2
-   */
-  public int hours() {
-    return hours;
-  }
-
-  /**
-   * @since 4.2
-   */
-  public int minutes() {
-    return minutes;
-  }
-
-  /**
-   *
-   * @since 4.2
-   */
-  public static WorkUnit fromLong(long durationInLong) {
-    Builder builder = new Builder();
-
-    long time = durationInLong;
-    Long currentTime = time / DAY;
-    if (currentTime > 0) {
-      builder.setDays(currentTime.intValue());
-      time = time - (currentTime * DAY);
-    }
-
-    currentTime = time / HOUR;
-    if (currentTime > 0) {
-      builder.setHours(currentTime.intValue());
-      time = time - (currentTime * HOUR);
-    }
-
-    currentTime = time / MINUTE;
-    if (currentTime > 0) {
-      builder.setMinutes(currentTime.intValue());
-    }
-
-    return builder.build();
+    return new WorkUnit(d, defaultIfEmptyUnit);
   }
 
-  /**
-   * Return the duration using the following format DDHHMM, where DD is the number of days, HH is the number of months, and MM the number of minutes.
-   * For instance, 5 days and 2 hours will return 050200.
-   *
-   * @since 4.2
-   */
-  public long toLong() {
-    return days * DAY + hours * HOUR + minutes * MINUTE;
-  }
-
-  /**
-   * Return the duration in number of days.
-   * For instance, 5 days and 4 hours will return 5.5 hours (if hoursIndDay is 8).
-   *
-   * @since 4.2
-   */
-  public double toDays(int hoursInDay) {
-    double resultDays = days;
-    resultDays += (double) hours / hoursInDay;
-    resultDays += (double) minutes / (hoursInDay * 60.0);
-    return resultDays;
+  public static WorkUnit create() {
+    return create(0d, DEFAULT_UNIT);
   }
 
   @Override
@@ -187,24 +82,26 @@ public final class WorkUnit implements Serializable {
     if (o == null || getClass() != o.getClass()) {
       return false;
     }
-    WorkUnit workDayDuration = (WorkUnit) o;
-    if (days != workDayDuration.days) {
-      return false;
-    }
-    if (hours != workDayDuration.hours) {
+
+    WorkUnit workUnit = (WorkUnit) o;
+
+    if (Double.compare(workUnit.value, value) != 0) {
       return false;
     }
-    if (minutes != workDayDuration.minutes) {
+    if (!unit.equals(workUnit.unit)) {
       return false;
     }
+
     return true;
   }
 
   @Override
   public int hashCode() {
-    int result = Integer.valueOf(days).hashCode();
-    result = 29 * result + Integer.valueOf(hours).hashCode();
-    result = 27 * result + Integer.valueOf(minutes).hashCode();
+    int result;
+    long temp;
+    temp = Double.doubleToLongBits(value);
+    result = (int) (temp ^ (temp >>> 32));
+    result = 31 * result + unit.hashCode();
     return result;
   }
 
@@ -212,32 +109,4 @@ public final class WorkUnit implements Serializable {
   public String toString() {
     return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
   }
-
-  /**
-   * @since 4.2
-   */
-  public static class Builder {
-    private int days;
-    private int hours;
-    private int minutes;
-
-    public Builder setDays(int days) {
-      this.days = days;
-      return this;
-    }
-
-    public Builder setHours(int hours) {
-      this.hours = hours;
-      return this;
-    }
-
-    public Builder setMinutes(int minutes) {
-      this.minutes = minutes;
-      return this;
-    }
-
-    public WorkUnit build() {
-      return new WorkUnit(days, hours, minutes);
-    }
-  }
 }
index 1b385766ac42c1df035258b866a61d797979a7b8..0df487f777cda3d2732712dea48ffc3432c7c05f 100644 (file)
@@ -25,7 +25,7 @@ import org.junit.Test;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueComment;
 import org.sonar.api.rule.RuleKey;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 
 import java.text.SimpleDateFormat;
 import java.util.List;
@@ -50,7 +50,7 @@ public class DefaultIssueTest {
       .setMessage("a message")
       .setLine(7)
       .setEffortToFix(1.2d)
-      .setTechnicalDebt(new WorkUnit.Builder().setDays(1).build())
+      .setTechnicalDebt(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8))
       .setActionPlanKey("BCDE")
       .setStatus(Issue.STATUS_CLOSED)
       .setResolution(Issue.RESOLUTION_FIXED)
@@ -78,7 +78,7 @@ public class DefaultIssueTest {
     assertThat(issue.message()).isEqualTo("a message");
     assertThat(issue.line()).isEqualTo(7);
     assertThat(issue.effortToFix()).isEqualTo(1.2d);
-    assertThat(issue.technicalDebt()).isEqualTo(new WorkUnit.Builder().setDays(1).build());
+    assertThat(issue.technicalDebt()).isEqualTo(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8));
     assertThat(issue.actionPlanKey()).isEqualTo("BCDE");
     assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED);
     assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
index f3bda18103803b3b5d6d7281af64c3180ae6a3a1..918b5918f183a921996ca02b31e42ab2d1d959ef 100644 (file)
@@ -47,8 +47,8 @@ public class DefaultRequirementTest {
       .setCharacteristic(characteristic)
       .setRootCharacteristic(root)
       .setFunction("linear_offset")
-      .setFactor(new WorkUnit.Builder().setMinutes(2).build())
-      .setOffset(new WorkUnit.Builder().setHours(1).build())
+      .setFactor(WorkUnit.create(2d, WorkUnit.MINUTES))
+      .setOffset(WorkUnit.create(1d, WorkUnit.HOURS))
       .setCreatedAt(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"))
       .setUpdatedAt(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"));
 
@@ -57,8 +57,8 @@ public class DefaultRequirementTest {
     assertThat(requirement.characteristic()).isEqualTo(characteristic);
     assertThat(requirement.rootCharacteristic()).isEqualTo(root);
     assertThat(requirement.function()).isEqualTo("linear_offset");
-    assertThat(requirement.factor()).isEqualTo(new WorkUnit.Builder().setMinutes(2).build());
-    assertThat(requirement.offset()).isEqualTo(new WorkUnit.Builder().setHours(1).build());
+    assertThat(requirement.factor()).isEqualTo(WorkUnit.create(2d, WorkUnit.MINUTES));
+    assertThat(requirement.offset()).isEqualTo(WorkUnit.create(1d, WorkUnit.HOURS));
     assertThat(requirement.createdAt()).isEqualTo(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"));
     assertThat(requirement.updatedAt()).isEqualTo(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19"));
   }
index 72835e0705ba5b604cad22a2d24ef3830f70cf07..6cdb209c18fec05a71836831a16b21328038324f 100644 (file)
@@ -56,8 +56,8 @@ public class DefaultCharacteristicTest {
       .setId(1)
       .setRuleKey(RuleKey.of("repo", "rule"))
       .setFunction("linear_offset")
-      .setFactor(new WorkUnit.Builder().setMinutes(2).build())
-      .setOffset(new WorkUnit.Builder().setHours(1).build())
+      .setFactor(WorkUnit.create(2d, WorkUnit.MINUTES))
+      .setOffset(WorkUnit.create(1d, WorkUnit.HOURS))
       .setRootId(3)
       .setParentId(2);
 
@@ -67,8 +67,8 @@ public class DefaultCharacteristicTest {
     assertThat(requirement.order()).isNull();
     assertThat(requirement.ruleKey()).isEqualTo(RuleKey.of("repo", "rule"));
     assertThat(requirement.function()).isEqualTo("linear_offset");
-    assertThat(requirement.factor()).isEqualTo(new WorkUnit.Builder().setMinutes(2).build());
-    assertThat(requirement.offset()).isEqualTo(new WorkUnit.Builder().setHours(1).build());
+    assertThat(requirement.factor()).isEqualTo(WorkUnit.create(2d, WorkUnit.MINUTES));
+    assertThat(requirement.offset()).isEqualTo(WorkUnit.create(1d, WorkUnit.HOURS));
     assertThat(requirement.parentId()).isEqualTo(2);
     assertThat(requirement.rootId()).isEqualTo(3);
   }
@@ -92,8 +92,8 @@ public class DefaultCharacteristicTest {
       .setId(1)
       .setRuleKey(RuleKey.of("repo", "rule"))
       .setFunction("linear_offset")
-      .setFactor(new WorkUnit.Builder().setMinutes(2).build())
-      .setOffset(new WorkUnit.Builder().setHours(1).build())
+      .setFactor(WorkUnit.create(2d, WorkUnit.MINUTES))
+      .setOffset(WorkUnit.create(1d, WorkUnit.HOURS))
       .setRootId(3)
       .setParentId(2);
 
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationFactoryTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationFactoryTest.java
new file mode 100644 (file)
index 0000000..88e21e9
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * 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.api.utils;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class WorkDurationFactoryTest {
+
+  WorkDurationFactory factory;
+
+  @Before
+  public void setUp() throws Exception {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, 8);
+    factory = new WorkDurationFactory(settings);
+  }
+
+  @Test
+  public void create_from_working_value() throws Exception {
+    // 1 working day -> 8 hours
+    assertThat(factory.createFromWorkingValue(1, WorkDuration.UNIT.DAYS).toSeconds()).isEqualTo(8*60*60);
+    // 8 hours
+    assertThat(factory.createFromWorkingValue(8, WorkDuration.UNIT.HOURS).toSeconds()).isEqualTo(8*60*60);
+  }
+
+  @Test
+  public void create_from_working_long() throws Exception {
+    WorkDuration workDuration = factory.createFromWorkingLong(1l);
+    assertThat(workDuration.days()).isEqualTo(0);
+    assertThat(workDuration.hours()).isEqualTo(0);
+    assertThat(workDuration.minutes()).isEqualTo(1);
+  }
+}
diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/utils/WorkDurationTest.java
new file mode 100644 (file)
index 0000000..affb52a
--- /dev/null
@@ -0,0 +1,172 @@
+/*
+ * 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.api.utils;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class WorkDurationTest {
+
+  private static final int HOURS_IN_DAY = 8;
+
+  @Test
+  public void create_from_days_hours_minutes() throws Exception {
+    WorkDuration workDuration = WorkDuration.create(1, 1, 1, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(1);
+    assertThat(workDuration.hours()).isEqualTo(1);
+    assertThat(workDuration.minutes()).isEqualTo(1);
+    assertThat(workDuration.toSeconds()).isEqualTo(1 * HOURS_IN_DAY * 60 * 60 + 1 * 60 * 60 + 60);
+    assertThat(workDuration.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+  }
+
+  @Test
+  public void create_from_value_and_unit() throws Exception {
+    WorkDuration result = WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, HOURS_IN_DAY);
+    assertThat(result.days()).isEqualTo(1);
+    assertThat(result.hours()).isEqualTo(0);
+    assertThat(result.minutes()).isEqualTo(0);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+    assertThat(result.toSeconds()).isEqualTo(1 * HOURS_IN_DAY * 60 * 60);
+
+    assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toSeconds()).isEqualTo(1 * HOURS_IN_DAY * 60 * 60);
+    assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toSeconds()).isEqualTo(1 * 60 * 60);
+    assertThat(WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toSeconds()).isEqualTo(60);
+  }
+
+  @Test
+  public void create_from_seconds() throws Exception {
+    WorkDuration workDuration = WorkDuration.createFromSeconds(60, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(0);
+    assertThat(workDuration.hours()).isEqualTo(0);
+    assertThat(workDuration.minutes()).isEqualTo(1);
+
+    workDuration = WorkDuration.createFromSeconds(60 * 60, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(0);
+    assertThat(workDuration.hours()).isEqualTo(1);
+    assertThat(workDuration.minutes()).isEqualTo(0);
+
+    workDuration = WorkDuration.createFromSeconds(HOURS_IN_DAY * 60 * 60, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(1);
+    assertThat(workDuration.hours()).isEqualTo(0);
+    assertThat(workDuration.minutes()).isEqualTo(0);
+  }
+
+  @Test
+  public void create_from_working_long() throws Exception {
+    // 1 minute
+    WorkDuration workDuration = WorkDuration.createFromLong(1l, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(0);
+    assertThat(workDuration.hours()).isEqualTo(0);
+    assertThat(workDuration.minutes()).isEqualTo(1);
+
+    // 1 hour
+    workDuration = WorkDuration.createFromLong(100l, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(0);
+    assertThat(workDuration.hours()).isEqualTo(1);
+    assertThat(workDuration.minutes()).isEqualTo(0);
+
+    // 1 day
+    workDuration = WorkDuration.createFromLong(10000l, HOURS_IN_DAY);
+    assertThat(workDuration.days()).isEqualTo(1);
+    assertThat(workDuration.hours()).isEqualTo(0);
+    assertThat(workDuration.minutes()).isEqualTo(0);
+  }
+
+  @Test
+  public void convert_to_seconds() throws Exception {
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toSeconds()).isEqualTo(2 * 60);
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toSeconds()).isEqualTo(2 * 60 * 60);
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toSeconds()).isEqualTo(2 * HOURS_IN_DAY * 60 * 60);
+  }
+
+  @Test
+  public void convert_to_working_days() throws Exception {
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d / 60d / 8d);
+    assertThat(WorkDuration.createFromValueAndUnit(240, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toWorkingDays()).isEqualTo(0.5);
+    assertThat(WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(0.5);
+    assertThat(WorkDuration.createFromValueAndUnit(8, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(1d);
+    assertThat(WorkDuration.createFromValueAndUnit(16, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d);
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toWorkingDays()).isEqualTo(2d);
+  }
+
+  @Test
+  public void convert_to_working_long() throws Exception {
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).toLong()).isEqualTo(2l);
+    assertThat(WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(400l);
+    assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(10200l);
+    assertThat(WorkDuration.createFromValueAndUnit(8, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).toLong()).isEqualTo(10000l);
+    assertThat(WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.DAYS, HOURS_IN_DAY).toLong()).isEqualTo(20000l);
+  }
+
+  @Test
+  public void add() throws Exception {
+    // 4h + 5h = 1d 1h
+    WorkDuration result = WorkDuration.createFromValueAndUnit(4, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).add(WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY));
+    assertThat(result.days()).isEqualTo(1);
+    assertThat(result.hours()).isEqualTo(1);
+    assertThat(result.minutes()).isEqualTo(0);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+
+    // 40 m + 30m = 1h 10m
+    result = WorkDuration.createFromValueAndUnit(40, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).add(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+    assertThat(result.days()).isEqualTo(0);
+    assertThat(result.hours()).isEqualTo(1);
+    assertThat(result.minutes()).isEqualTo(10);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+
+    // 10 m + 20m = 30m
+    assertThat(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).add(
+      WorkDuration.createFromValueAndUnit(20, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY)
+    ).minutes()).isEqualTo(30);
+  }
+
+  @Test
+  public void subtract() throws Exception {
+    // 1d 1h - 5h = 4h
+    WorkDuration result = WorkDuration.create(1, 1, 0, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY));
+    assertThat(result.days()).isEqualTo(0);
+    assertThat(result.hours()).isEqualTo(4);
+    assertThat(result.minutes()).isEqualTo(0);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+
+    // 1h 10m - 30m = 40m
+    result = WorkDuration.create(0, 1, 10, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
+    assertThat(result.days()).isEqualTo(0);
+    assertThat(result.hours()).isEqualTo(0);
+    assertThat(result.minutes()).isEqualTo(40);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+
+    // 30m - 20m = 10m
+    assertThat(WorkDuration.createFromValueAndUnit(30, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY).subtract(WorkDuration.createFromValueAndUnit(20, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY))
+      .minutes()).isEqualTo(10);
+  }
+
+  @Test
+  public void multiply() throws Exception {
+    // 5h * 2 = 1d 2h
+    WorkDuration result = WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY).multiply(2);
+    assertThat(result.days()).isEqualTo(1);
+    assertThat(result.hours()).isEqualTo(2);
+    assertThat(result.minutes()).isEqualTo(0);
+    assertThat(result.hoursInDay()).isEqualTo(HOURS_IN_DAY);
+  }
+}
index c77e0b8bbf6e440198904e75b7a01c314ffba2cf..108f7aa57c277bdf0da5e36645c3f61b8bc880d5 100644 (file)
@@ -36,9 +36,17 @@ public class WorkUnitTest {
   @Test
   public void create_default() throws Exception {
     WorkUnit workUnit = WorkUnit.create();
+    assertThat(workUnit.getUnit()).isEqualTo("d");
     assertThat(workUnit.getValue()).isEqualTo(0.0);
   }
 
+  @Test
+  public void test_equals() throws Exception {
+    assertThat(WorkUnit.create(2.0, "mn")).isEqualTo(WorkUnit.create(2.0, "mn"));
+    assertThat(WorkUnit.create(3.0, "mn")).isNotEqualTo(WorkUnit.create(2.0, "mn"));
+    assertThat(WorkUnit.create(2.0, "h")).isNotEqualTo(WorkUnit.create(2.0, "mn"));
+  }
+
   @Test
   public void fail_with_bad_unit() throws Exception {
     try {
@@ -57,49 +65,4 @@ public class WorkUnitTest {
     }
   }
 
-  @Test
-  public void from_long_on_simple_values() {
-    checkTimes(WorkUnit.fromLong(1L), 0, 0, 1);
-    checkTimes(WorkUnit.fromLong(100L), 0, 1, 0);
-    checkTimes(WorkUnit.fromLong(10000L), 1, 0, 0);
-  }
-
-  @Test
-  public void from_long_on_complex_values() {
-    checkTimes(WorkUnit.fromLong(10101L), 1, 1, 1);
-    checkTimes(WorkUnit.fromLong(101L), 0, 1, 1);
-    checkTimes(WorkUnit.fromLong(10001L), 1, 0, 1);
-    checkTimes(WorkUnit.fromLong(10100L), 1, 1, 0);
-
-    checkTimes(WorkUnit.fromLong(112233L), 11, 22, 33);
-  }
-
-  @Test
-  public void to_long() {
-    assertThat(new WorkUnit.Builder().setDays(1).setHours(1).setMinutes(1).build().toLong()).isEqualTo(10101L);
-  }
-
-  @Test
-  public void test_equals_and_hashCode() throws Exception {
-    WorkUnit oneMinute = WorkUnit.fromLong(1L);
-    WorkUnit oneHours = WorkUnit.fromLong(100L);
-    WorkUnit oneDay = WorkUnit.fromLong(10000L);
-
-    assertThat(oneMinute).isEqualTo(oneMinute);
-    assertThat(oneMinute).isEqualTo(WorkUnit.fromLong(1L));
-    assertThat(oneHours).isEqualTo(WorkUnit.fromLong(100L));
-    assertThat(oneDay).isEqualTo(WorkUnit.fromLong(10000L));
-
-    assertThat(oneMinute).isNotEqualTo(oneHours);
-    assertThat(oneHours).isNotEqualTo(oneDay);
-
-    assertThat(oneMinute.hashCode()).isEqualTo(oneMinute.hashCode());
-  }
-
-  private void checkTimes(WorkUnit technicalDebt, int expectedDays, int expectedHours, int expectedMinutes) {
-    assertThat(technicalDebt.days()).isEqualTo(expectedDays);
-    assertThat(technicalDebt.hours()).isEqualTo(expectedHours);
-    assertThat(technicalDebt.minutes()).isEqualTo(expectedMinutes);
-  }
-
 }
index 37325365b554336fac261348b164b11a0f304989..6ba3bb545d16762560f5c3292c11ab056cffc873 100644 (file)
@@ -27,18 +27,13 @@ import org.sonar.api.issue.ActionPlan;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.ActionPlanDeadlineComparator;
 import org.sonar.core.issue.ActionPlanStats;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.IssueUpdater;
-import org.sonar.core.issue.db.ActionPlanDao;
-import org.sonar.core.issue.db.ActionPlanDto;
-import org.sonar.core.issue.db.ActionPlanStatsDao;
-import org.sonar.core.issue.db.ActionPlanStatsDto;
-import org.sonar.core.issue.db.IssueDao;
-import org.sonar.core.issue.db.IssueDto;
-import org.sonar.core.issue.db.IssueStorage;
+import org.sonar.core.issue.db.*;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
@@ -47,11 +42,7 @@ import org.sonar.server.user.UserSession;
 
 import javax.annotation.CheckForNull;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
 
 import static com.google.common.collect.Lists.newArrayList;
 
@@ -67,9 +58,10 @@ public class ActionPlanService implements ServerComponent {
   private final IssueDao issueDao;
   private final IssueUpdater issueUpdater;
   private final IssueStorage issueStorage;
+  private final WorkDurationFactory workDurationFactory;
 
   public ActionPlanService(ActionPlanDao actionPlanDao, ActionPlanStatsDao actionPlanStatsDao, ResourceDao resourceDao, AuthorizationDao authorizationDao,
-    IssueDao issueDao, IssueUpdater issueUpdater, IssueStorage issueStorage) {
+                           IssueDao issueDao, IssueUpdater issueUpdater, IssueStorage issueStorage, WorkDurationFactory workDurationFactory) {
     this.actionPlanDao = actionPlanDao;
     this.actionPlanStatsDao = actionPlanStatsDao;
     this.resourceDao = resourceDao;
@@ -77,6 +69,7 @@ public class ActionPlanService implements ServerComponent {
     this.issueDao = issueDao;
     this.issueUpdater = issueUpdater;
     this.issueStorage = issueStorage;
+    this.workDurationFactory = workDurationFactory;
   }
 
   public ActionPlan create(ActionPlan actionPlan, UserSession userSession) {
@@ -110,7 +103,7 @@ public class ActionPlanService implements ServerComponent {
     IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.login());
     List<DefaultIssue> issues = newArrayList();
     for (IssueDto issueDto : dtos) {
-      DefaultIssue issue = issueDto.toDefaultIssue();
+      DefaultIssue issue = issueDto.toDefaultIssue(workDurationFactory.createFromWorkingLong(issueDto.getTechnicalDebt()));
       // Unplan issue
       if (issueUpdater.plan(issue, null, context)) {
         issues.add(issue);
index eeeae4fdf1875b5afc393afc412919993f338969..f5a3e2faa7f7f35214ebf21f244217b33df760d2 100644 (file)
@@ -32,6 +32,7 @@ import org.sonar.api.rules.Rule;
 import org.sonar.api.user.User;
 import org.sonar.api.user.UserFinder;
 import org.sonar.api.utils.Paging;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.issue.DefaultIssueQueryResult;
 import org.sonar.core.issue.db.IssueChangeDao;
 import org.sonar.core.issue.db.IssueDao;
@@ -59,13 +60,14 @@ public class DefaultIssueFinder implements IssueFinder {
   private final UserFinder userFinder;
   private final ResourceDao resourceDao;
   private final ActionPlanService actionPlanService;
+  private final WorkDurationFactory workDurationFactory;
 
   public DefaultIssueFinder(MyBatis myBatis,
-    IssueDao issueDao, IssueChangeDao issueChangeDao,
-    DefaultRuleFinder ruleFinder,
-    UserFinder userFinder,
-    ResourceDao resourceDao,
-    ActionPlanService actionPlanService) {
+                            IssueDao issueDao, IssueChangeDao issueChangeDao,
+                            DefaultRuleFinder ruleFinder,
+                            UserFinder userFinder,
+                            ResourceDao resourceDao,
+                            ActionPlanService actionPlanService, WorkDurationFactory workDurationFactory) {
     this.myBatis = myBatis;
     this.issueDao = issueDao;
     this.issueChangeDao = issueChangeDao;
@@ -73,6 +75,7 @@ public class DefaultIssueFinder implements IssueFinder {
     this.userFinder = userFinder;
     this.resourceDao = resourceDao;
     this.actionPlanService = actionPlanService;
+    this.workDurationFactory = workDurationFactory;
   }
 
   DefaultIssue findByKey(String issueKey, String requiredRole) {
@@ -83,7 +86,8 @@ public class DefaultIssueFinder implements IssueFinder {
     if (!UserSession.get().hasProjectPermission(requiredRole, dto.getRootComponentKey())) {
       throw new IllegalStateException("User does not have the required role required to change the issue: " + issueKey);
     }
-    return dto.toDefaultIssue();
+
+    return dto.toDefaultIssue(workDurationFactory.createFromWorkingLong(dto.getTechnicalDebt()));
   }
 
   @Override
@@ -114,7 +118,7 @@ public class DefaultIssueFinder implements IssueFinder {
       Set<String> actionPlanKeys = Sets.newHashSet();
       Set<String> users = Sets.newHashSet();
       for (IssueDto dto : pagedSortedIssues) {
-        DefaultIssue defaultIssue = dto.toDefaultIssue();
+        DefaultIssue defaultIssue = dto.toDefaultIssue(workDurationFactory.createFromWorkingLong(dto.getTechnicalDebt()));
         issuesByKey.put(dto.getKee(), defaultIssue);
         issues.add(defaultIssue);
         ruleIds.add(dto.getRuleId());
@@ -196,7 +200,7 @@ public class DefaultIssueFinder implements IssueFinder {
 
   public Issue findByKey(String key) {
     IssueDto dto = issueDao.selectByKey(key);
-    return dto != null ? dto.toDefaultIssue() : null;
+    return dto != null ? dto.toDefaultIssue(workDurationFactory.createFromWorkingLong(dto.getTechnicalDebt())) : null;
   }
 
 }
index d92ea0f1ceea5f75e3c708baf34223012c0cd842..0c58b1a1a464df80c89b9fdb4dbc446fa5f572cd 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.server.issue;
 
 import org.sonar.api.ServerComponent;
 import org.sonar.api.issue.internal.FieldDiffs;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.i18n.DefaultI18n;
 import org.sonar.core.issue.IssueUpdater;
 import org.sonar.server.technicaldebt.DebtFormatter;
@@ -39,10 +39,12 @@ public class IssueChangelogFormatter implements ServerComponent {
 
   private final DefaultI18n defaultI18n;
   private final DebtFormatter debtFormatter;
+  private final WorkDurationFactory workDurationFactory;
 
-  public IssueChangelogFormatter(DefaultI18n defaultI18n, DebtFormatter debtFormatter) {
+  public IssueChangelogFormatter(DefaultI18n defaultI18n, DebtFormatter debtFormatter, WorkDurationFactory workDurationFactory) {
     this.defaultI18n = defaultI18n;
     this.debtFormatter = debtFormatter;
+    this.workDurationFactory = workDurationFactory;
   }
 
   public List<String> format(Locale locale, FieldDiffs diffs) {
@@ -74,10 +76,10 @@ public class IssueChangelogFormatter implements ServerComponent {
     String oldValueString = oldValue != null && !"".equals(oldValue) ? oldValue.toString() : null;
     if (IssueUpdater.TECHNICAL_DEBT.equals(key)) {
       if (newValueString != null) {
-        newValueString = debtFormatter.format(locale, WorkUnit.fromLong(Long.parseLong(newValueString)));
+        newValueString = debtFormatter.format(locale, workDurationFactory.createFromWorkingLong(Long.parseLong(newValueString)));
       }
       if (oldValueString != null) {
-        oldValueString = debtFormatter.format(locale, WorkUnit.fromLong(Long.parseLong(oldValueString)));
+        oldValueString = debtFormatter.format(locale, workDurationFactory.createFromWorkingLong(Long.parseLong(oldValueString)));
       }
     }
     return new IssueChangelogDiffFormat(oldValueString, newValueString);
index 096ca6e225524eea6e6b285e6573af9d4cb7998e..9229483d07f6b548ff03ff356379f945fe2e604f 100644 (file)
@@ -31,7 +31,7 @@ import org.sonar.api.server.ws.Response;
 import org.sonar.api.technicaldebt.server.Characteristic;
 import org.sonar.api.user.User;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.workflow.Transition;
@@ -101,7 +101,7 @@ public class IssueShowWsHandler implements RequestHandler {
     Component project = result.project(issue);
     String actionPlanKey = issue.actionPlanKey();
     ActionPlan actionPlan = result.actionPlan(issue);
-    WorkUnit technicalDebt = issue.technicalDebt();
+    WorkDuration technicalDebt = issue.technicalDebt();
     Date updateDate = issue.updateDate();
     Date closeDate = issue.closeDate();
 
index 85bc93c8f53d95976460b0bd039604ee093aa624..4265044367fa67c819c82fde283a165745613011 100644 (file)
@@ -35,6 +35,7 @@ import org.sonar.api.rules.XMLRuleParser;
 import org.sonar.api.utils.HttpDownloader;
 import org.sonar.api.utils.TimeProfiler;
 import org.sonar.api.utils.UriReader;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.api.utils.internal.TempFolderCleaner;
 import org.sonar.core.component.SnapshotPerspectives;
 import org.sonar.core.config.Logback;
@@ -370,6 +371,7 @@ public final class Platform {
     servicesContainer.addSingleton(TechnicalDebtXMLImporter.class);
     servicesContainer.addSingleton(DebtFormatter.class);
     servicesContainer.addSingleton(DefaultTechnicalDebtManager.class);
+    servicesContainer.addSingleton(WorkDurationFactory.class);
 
     // source
     servicesContainer.addSingleton(HtmlSourceDecorator.class);
index faa2f8b71753878ea9c92372f366466ff7198856..720bda3d741e930b47609167b2bec36cd2652a21 100644 (file)
@@ -21,7 +21,7 @@
 package org.sonar.server.technicaldebt;
 
 import org.sonar.api.ServerComponent;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.core.i18n.DefaultI18n;
 
 import java.util.Locale;
@@ -34,23 +34,23 @@ public class DebtFormatter implements ServerComponent {
     this.defaultI18n = defaultI18n;
   }
 
-  public String format(Locale locale, WorkUnit technicalDebt) {
+  public String format(Locale locale, WorkDuration debt) {
     StringBuilder message = new StringBuilder();
-    if (technicalDebt.days() > 0) {
-      message.append(defaultI18n.message(locale, "issue.technical_debt.x_days", null, technicalDebt.days()));
+    if (debt.days() > 0) {
+      message.append(defaultI18n.message(locale, "issue.technical_debt.x_days", null, debt.days()));
     }
-    if (technicalDebt.hours() > 0) {
+    if (debt.hours() > 0) {
       if (message.length() > 0) {
         message.append(" ");
       }
-      message.append(defaultI18n.message(locale, "issue.technical_debt.x_hours", null, technicalDebt.hours()));
+      message.append(defaultI18n.message(locale, "issue.technical_debt.x_hours", null, debt.hours()));
     }
     // Do not display minutes if days is not null to not have too much information
-    if (technicalDebt.minutes() > 0 && technicalDebt.days() == 0) {
+    if (debt.minutes() > 0 && debt.days() == 0) {
       if (message.length() > 0) {
         message.append(" ");
       }
-      message.append(defaultI18n.message(locale, "issue.technical_debt.x_minutes", null, technicalDebt.minutes()));
+      message.append(defaultI18n.message(locale, "issue.technical_debt.x_minutes", null, debt.minutes()));
     }
     return message.toString();
   }
index cb079c12dae10b465f7e22958987f05ff718419b..f05964841082b6e475735f1311c016ff59a1b9e0 100644 (file)
@@ -22,7 +22,8 @@ package org.sonar.server.technicaldebt;
 
 import org.sonar.api.ServerComponent;
 import org.sonar.api.technicaldebt.server.Characteristic;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.technicaldebt.DefaultTechnicalDebtManager;
 import org.sonar.server.user.UserSession;
 
@@ -34,18 +35,20 @@ public class DebtService implements ServerComponent {
 
   private final DebtFormatter debtFormatter;
   private final DefaultTechnicalDebtManager finder;
+  private final WorkDurationFactory workDurationFactory;
 
-  public DebtService(DebtFormatter debtFormatter, DefaultTechnicalDebtManager finder) {
+  public DebtService(DebtFormatter debtFormatter, DefaultTechnicalDebtManager finder, WorkDurationFactory workDurationFactory) {
     this.debtFormatter = debtFormatter;
     this.finder = finder;
+    this.workDurationFactory = workDurationFactory;
   }
 
-  public String format(WorkUnit technicalDebt) {
+  public String format(WorkDuration technicalDebt) {
     return debtFormatter.format(UserSession.get().locale(), technicalDebt);
   }
 
-  public WorkUnit toTechnicalDebt(String technicalDebtInLong) {
-    return WorkUnit.fromLong(Long.parseLong(technicalDebtInLong));
+  public WorkDuration toTechnicalDebt(String technicalDebtInLong) {
+    return workDurationFactory.createFromWorkingLong(Long.parseLong(technicalDebtInLong));
   }
 
   public List<Characteristic> findRootCharacteristics() {
index 9bcd00ec64402489d52e91206634bf56ab41263c..f9587ae72fc06f7613b794d33c3f80513729fa9e 100644 (file)
@@ -23,22 +23,19 @@ package org.sonar.server.issue;
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
 import org.sonar.api.issue.ActionPlan;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
 import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.ActionPlanStats;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.IssueUpdater;
-import org.sonar.core.issue.db.ActionPlanDao;
-import org.sonar.core.issue.db.ActionPlanDto;
-import org.sonar.core.issue.db.ActionPlanStatsDao;
-import org.sonar.core.issue.db.ActionPlanStatsDto;
-import org.sonar.core.issue.db.IssueDao;
-import org.sonar.core.issue.db.IssueDto;
-import org.sonar.core.issue.db.IssueStorage;
+import org.sonar.core.issue.db.*;
 import org.sonar.core.resource.ResourceDao;
 import org.sonar.core.resource.ResourceDto;
 import org.sonar.core.resource.ResourceQuery;
@@ -54,10 +51,7 @@ import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
+import static org.mockito.Mockito.*;
 
 public class ActionPlanServiceTest {
 
@@ -78,7 +72,10 @@ public class ActionPlanServiceTest {
     when(userSession.userId()).thenReturn(10);
     when(authorizationDao.isAuthorizedComponentKey(anyString(), eq(10), anyString())).thenReturn(true);
 
-    actionPlanService = new ActionPlanService(actionPlanDao, actionPlanStatsDao, resourceDao, authorizationDao, issueDao, issueUpdater, issueStorage);
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, 8);
+    actionPlanService = new ActionPlanService(actionPlanDao, actionPlanStatsDao, resourceDao, authorizationDao, issueDao, issueUpdater, issueStorage,
+      new WorkDurationFactory(settings));
   }
 
   @Test
index 754a06a61b52ec9a0cf4df467ec1b3057b07605b..d53fdd5b8cfce4536b56fb80ec44828a57fb547e 100644 (file)
@@ -21,8 +21,11 @@ package org.sonar.server.issue;
 
 import com.google.common.collect.Lists;
 import org.apache.ibatis.session.SqlSession;
+import org.junit.Before;
 import org.junit.Test;
+import org.sonar.api.CoreProperties;
 import org.sonar.api.component.Component;
+import org.sonar.api.config.Settings;
 import org.sonar.api.issue.ActionPlan;
 import org.sonar.api.issue.Issue;
 import org.sonar.api.issue.IssueQuery;
@@ -31,7 +34,8 @@ import org.sonar.api.issue.internal.DefaultIssue;
 import org.sonar.api.rules.Rule;
 import org.sonar.api.user.User;
 import org.sonar.api.user.UserFinder;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.component.ComponentDto;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.db.IssueChangeDao;
@@ -57,6 +61,8 @@ import static org.mockito.Mockito.*;
 
 public class DefaultIssueFinderTest {
 
+  private static final int HOURS_IN_DAY = 8;
+
   MyBatis mybatis = mock(MyBatis.class);
   IssueDao issueDao = mock(IssueDao.class);
   IssueChangeDao issueChangeDao = mock(IssueChangeDao.class);
@@ -64,7 +70,14 @@ public class DefaultIssueFinderTest {
   ResourceDao resourceDao = mock(ResourceDao.class);
   ActionPlanService actionPlanService = mock(ActionPlanService.class);
   UserFinder userFinder = mock(UserFinder.class);
-  DefaultIssueFinder finder = new DefaultIssueFinder(mybatis, issueDao, issueChangeDao, ruleFinder, userFinder, resourceDao, actionPlanService);
+  DefaultIssueFinder finder;
+
+  @Before
+  public void setUp() throws Exception {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, HOURS_IN_DAY);
+    finder = new DefaultIssueFinder(mybatis, issueDao, issueChangeDao, ruleFinder, userFinder, resourceDao, actionPlanService, new WorkDurationFactory(settings));
+  }
 
   @Test
   public void find_issues() {
@@ -322,7 +335,7 @@ public class DefaultIssueFinderTest {
 
     assertThat(results.issues()).hasSize(1);
     DefaultIssue result = (DefaultIssue) results.issues().iterator().next();
-    assertThat(result.technicalDebt()).isEqualTo(new WorkUnit.Builder().setMinutes(10).build());
+    assertThat(result.technicalDebt()).isEqualTo(WorkDuration.createFromValueAndUnit(10, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY));
   }
 
 }
index bb84b7a2b8f1a6186641484e8d88344c9dd2d13c..a60ea6445468126d2c2f66c3858c74503d63200e 100644 (file)
@@ -24,8 +24,11 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.runners.MockitoJUnitRunner;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
 import org.sonar.api.issue.internal.FieldDiffs;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.i18n.DefaultI18n;
 import org.sonar.server.technicaldebt.DebtFormatter;
 
@@ -40,6 +43,8 @@ public class IssueChangelogFormatterTest {
 
   private static final Locale DEFAULT_LOCALE = Locale.getDefault();
 
+  private static final int HOURS_IN_DAY = 8;
+
   @Mock
   private DefaultI18n i18n;
 
@@ -50,7 +55,9 @@ public class IssueChangelogFormatterTest {
 
   @Before
   public void before() {
-    formatter = new IssueChangelogFormatter(i18n, debtFormatter);
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, HOURS_IN_DAY);
+    formatter = new IssueChangelogFormatter(i18n, debtFormatter, new WorkDurationFactory(settings));
   }
 
   @Test
@@ -130,8 +137,8 @@ public class IssueChangelogFormatterTest {
     FieldDiffs diffs = new FieldDiffs();
     diffs.setDiff("technicalDebt", "500", "10000");
 
-    when(debtFormatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setHours(5).build())).thenReturn("5 hours");
-    when(debtFormatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setDays(1).build())).thenReturn("1 days");
+    when(debtFormatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY))).thenReturn("5 hours");
+    when(debtFormatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, HOURS_IN_DAY))).thenReturn("1 days");
 
     when(i18n.message(DEFAULT_LOCALE, "issue.changelog.field.technicalDebt", null)).thenReturn("Technical Debt");
     when(i18n.message(DEFAULT_LOCALE, "issue.changelog.changed_to", null, "Technical Debt", "1 days")).thenReturn("Technical Debt changed to 1 days");
@@ -148,7 +155,7 @@ public class IssueChangelogFormatterTest {
     FieldDiffs diffs = new FieldDiffs();
     diffs.setDiff("technicalDebt", null, "10000");
 
-    when(debtFormatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setDays(1).build())).thenReturn("1 days");
+    when(debtFormatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.DAYS, 8))).thenReturn("1 days");
 
     when(i18n.message(DEFAULT_LOCALE, "issue.changelog.field.technicalDebt", null)).thenReturn("Technical Debt");
     when(i18n.message(DEFAULT_LOCALE, "issue.changelog.changed_to", null, "Technical Debt", "1 days")).thenReturn("Technical Debt changed to 1 days");
index 73bdd1ba6561c5312dbfbf408eec72d88b5cda03..d1dcd4b15fb2ccf1a48afb5613d441c7a8ca58f9 100644 (file)
@@ -42,7 +42,7 @@ import org.sonar.api.technicaldebt.server.Characteristic;
 import org.sonar.api.technicaldebt.server.internal.DefaultCharacteristic;
 import org.sonar.api.user.User;
 import org.sonar.api.utils.DateUtils;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.api.web.UserRole;
 import org.sonar.core.issue.DefaultActionPlan;
 import org.sonar.core.issue.DefaultIssueQueryResult;
@@ -209,7 +209,7 @@ public class IssueShowWsHandlerTest {
 
   @Test
   public void show_issue_with_technical_debt() throws Exception {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setHours(2).setMinutes(1).build();
+    WorkDuration technicalDebt = WorkDuration.create(0, 2, 1, 8);
     Issue issue = createStandardIssue().setTechnicalDebt(technicalDebt);
     issues.add(issue);
 
@@ -222,8 +222,7 @@ public class IssueShowWsHandlerTest {
 
   @Test
   public void show_issue_with_characteristics() throws Exception {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setHours(2).setMinutes(1).build();
-    ;
+    WorkDuration technicalDebt = WorkDuration.create(0, 2, 1, 8);
     Issue issue = createStandardIssue().setTechnicalDebt(technicalDebt);
     issues.add(issue);
 
index c9f12afd90e6eee3f61e4be005fa1903eb30b927..321b6914e38951af24540530aa20d858f04f0b45 100644 (file)
@@ -21,7 +21,7 @@
 package org.sonar.server.technicaldebt;
 
 import org.junit.Test;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
 import org.sonar.core.i18n.DefaultI18n;
 
 import java.util.Locale;
@@ -43,13 +43,13 @@ public class DebtFormatterTest {
     when(i18n.message(DEFAULT_LOCALE, "issue.technical_debt.x_hours", null, 2)).thenReturn("2 hours");
     when(i18n.message(DEFAULT_LOCALE, "issue.technical_debt.x_minutes", null, 1)).thenReturn("1 minutes");
 
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setDays(5).build())).isEqualTo("5 days");
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setHours(2).build())).isEqualTo("2 hours");
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setMinutes(1).build())).isEqualTo("1 minutes");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.DAYS, 8))).isEqualTo("5 days");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(2, WorkDuration.UNIT.HOURS, 8))).isEqualTo("2 hours");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.createFromValueAndUnit(1, WorkDuration.UNIT.MINUTES, 8))).isEqualTo("1 minutes");
 
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setDays(5).setHours(2).build())).isEqualTo("5 days 2 hours");
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setHours(2).setMinutes(1).build())).isEqualTo("2 hours 1 minutes");
-    assertThat(formatter.format(DEFAULT_LOCALE, new WorkUnit.Builder().setDays(5).setHours(2).setMinutes(10).build())).isEqualTo("5 days 2 hours");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.create(5, 2, 0, 8))).isEqualTo("5 days 2 hours");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.create(0, 2, 1, 8))).isEqualTo("2 hours 1 minutes");
+    assertThat(formatter.format(DEFAULT_LOCALE, WorkDuration.create(5, 2, 10, 8))).isEqualTo("5 days 2 hours");
   }
 
 }
index 75684adf2db02f126918968a43986dd71a4eeab8..fccf16801385e308e34fa4f73c6e88e7a05c5842 100644 (file)
  */
 package org.sonar.server.technicaldebt;
 
+import org.junit.Before;
 import org.junit.Test;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
 import org.sonar.api.technicaldebt.server.Characteristic;
 import org.sonar.api.technicaldebt.server.internal.DefaultCharacteristic;
-import org.sonar.api.utils.WorkUnit;
+import org.sonar.api.utils.WorkDuration;
+import org.sonar.api.utils.WorkDurationFactory;
 import org.sonar.core.technicaldebt.DefaultTechnicalDebtManager;
 
 import java.util.List;
@@ -36,20 +40,29 @@ import static org.mockito.Mockito.*;
 
 public class DebtServiceTest {
 
+  private static final int HOURS_IN_DAY = 8;
   DebtFormatter debtFormatter = mock(DebtFormatter.class);
   DefaultTechnicalDebtManager finder = mock(DefaultTechnicalDebtManager.class);
-  DebtService service = new DebtService(debtFormatter, finder);
+
+  DebtService service;
+
+  @Before
+  public void setUp() throws Exception {
+    Settings settings = new Settings();
+    settings.setProperty(CoreProperties.HOURS_IN_DAY, HOURS_IN_DAY);
+    service = new DebtService(debtFormatter, finder, new WorkDurationFactory(settings));
+  }
 
   @Test
   public void format() {
-    WorkUnit technicalDebt = new WorkUnit.Builder().setMinutes(5).build();
+    WorkDuration technicalDebt = WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.MINUTES, HOURS_IN_DAY);
     service.format(technicalDebt);
     verify(debtFormatter).format(any(Locale.class), eq(technicalDebt));
   }
 
   @Test
   public void to_technical_debt() {
-    assertThat(service.toTechnicalDebt("500")).isEqualTo(new WorkUnit.Builder().setHours(5).build());
+    assertThat(service.toTechnicalDebt("500")).isEqualTo(WorkDuration.createFromValueAndUnit(5, WorkDuration.UNIT.HOURS, HOURS_IN_DAY));
   }
 
   @Test