package org.sonar.plugins.core;
import com.google.common.collect.ImmutableList;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.Properties;
-import org.sonar.api.Property;
-import org.sonar.api.PropertyType;
-import org.sonar.api.SonarPlugin;
+import org.sonar.api.*;
import org.sonar.api.checks.NoSonarFilter;
import org.sonar.api.config.PropertyDefinition;
import org.sonar.api.resources.Java;
import org.sonar.api.resources.Qualifiers;
import org.sonar.batch.components.PastSnapshotFinder;
+import org.sonar.core.issue.IssueChangelogFinder;
+import org.sonar.core.technicaldebt.TechnicalDebtCalculator;
+import org.sonar.core.technicaldebt.TechnicalDebtConverter;
import org.sonar.core.timemachine.Periods;
import org.sonar.plugins.core.batch.IndexProjectPostJob;
import org.sonar.plugins.core.charts.DistributionAreaChart;
import org.sonar.plugins.core.charts.DistributionBarChart;
import org.sonar.plugins.core.charts.XradarChart;
import org.sonar.plugins.core.colorizers.JavaColorizerFormat;
-import org.sonar.plugins.core.dashboards.GlobalDefaultDashboard;
-import org.sonar.plugins.core.dashboards.ProjectDefaultDashboard;
-import org.sonar.plugins.core.dashboards.ProjectHotspotDashboard;
-import org.sonar.plugins.core.dashboards.ProjectIssuesDashboard;
-import org.sonar.plugins.core.dashboards.ProjectTimeMachineDashboard;
-import org.sonar.plugins.core.issue.CountFalsePositivesDecorator;
-import org.sonar.plugins.core.issue.CountUnresolvedIssuesDecorator;
-import org.sonar.plugins.core.issue.InitialOpenIssuesSensor;
-import org.sonar.plugins.core.issue.InitialOpenIssuesStack;
-import org.sonar.plugins.core.issue.IssueHandlers;
-import org.sonar.plugins.core.issue.IssueTracking;
-import org.sonar.plugins.core.issue.IssueTrackingDecorator;
-import org.sonar.plugins.core.issue.IssuesDensityDecorator;
-import org.sonar.plugins.core.issue.WeightedIssuesDecorator;
+import org.sonar.plugins.core.dashboards.*;
+import org.sonar.plugins.core.issue.*;
import org.sonar.plugins.core.issue.ignore.IgnoreIssuesPlugin;
-import org.sonar.plugins.core.issue.notification.ChangesOnMyIssueNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.IssueChangesEmailTemplate;
-import org.sonar.plugins.core.issue.notification.NewFalsePositiveNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.NewIssuesEmailTemplate;
-import org.sonar.plugins.core.issue.notification.NewIssuesNotificationDispatcher;
-import org.sonar.plugins.core.issue.notification.SendIssueNotificationsPostJob;
+import org.sonar.plugins.core.issue.notification.*;
import org.sonar.plugins.core.measurefilters.MyFavouritesFilter;
import org.sonar.plugins.core.measurefilters.ProjectFilter;
import org.sonar.plugins.core.notifications.alerts.NewAlerts;
import org.sonar.plugins.core.security.ApplyProjectRolesDecorator;
-import org.sonar.plugins.core.sensors.BranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.CheckAlertThresholds;
-import org.sonar.plugins.core.sensors.CommentDensityDecorator;
-import org.sonar.plugins.core.sensors.CoverageDecorator;
-import org.sonar.plugins.core.sensors.CoverageMeasurementFilter;
-import org.sonar.plugins.core.sensors.DirectoriesDecorator;
-import org.sonar.plugins.core.sensors.FileHashSensor;
-import org.sonar.plugins.core.sensors.FilesDecorator;
-import org.sonar.plugins.core.sensors.GenerateAlertEvents;
-import org.sonar.plugins.core.sensors.ItBranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.ItCoverageDecorator;
-import org.sonar.plugins.core.sensors.ItLineCoverageDecorator;
-import org.sonar.plugins.core.sensors.LineCoverageDecorator;
-import org.sonar.plugins.core.sensors.ManualMeasureDecorator;
-import org.sonar.plugins.core.sensors.OverallBranchCoverageDecorator;
-import org.sonar.plugins.core.sensors.OverallCoverageDecorator;
-import org.sonar.plugins.core.sensors.OverallLineCoverageDecorator;
-import org.sonar.plugins.core.sensors.ProfileEventsSensor;
-import org.sonar.plugins.core.sensors.ProfileSensor;
-import org.sonar.plugins.core.sensors.ProjectLinksSensor;
-import org.sonar.plugins.core.sensors.UnitTestDecorator;
-import org.sonar.plugins.core.sensors.VersionEventsSensor;
+import org.sonar.plugins.core.sensors.*;
+import org.sonar.plugins.core.technicaldebt.NewTechnicalDebtDecorator;
import org.sonar.plugins.core.technicaldebt.TechnicalDebtDecorator;
-import org.sonar.plugins.core.timemachine.NewCoverageAggregator;
-import org.sonar.plugins.core.timemachine.NewCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.NewItCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.NewOverallCoverageFileAnalyzer;
-import org.sonar.plugins.core.timemachine.TendencyDecorator;
-import org.sonar.plugins.core.timemachine.TimeMachineConfigurationPersister;
-import org.sonar.plugins.core.timemachine.VariationDecorator;
+import org.sonar.plugins.core.timemachine.*;
import org.sonar.plugins.core.web.Lcom4Viewer;
import org.sonar.plugins.core.web.TestsViewer;
import org.sonar.plugins.core.widgets.*;
-import org.sonar.plugins.core.widgets.issues.ActionPlansWidget;
-import org.sonar.plugins.core.widgets.issues.FalsePositiveIssuesWidget;
-import org.sonar.plugins.core.widgets.issues.IssueFilterWidget;
-import org.sonar.plugins.core.widgets.issues.IssuesWidget;
-import org.sonar.plugins.core.widgets.issues.MyUnresolvedIssuesWidget;
-import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesPerAssigneeWidget;
-import org.sonar.plugins.core.widgets.issues.UnresolvedIssuesStatusesWidget;
+import org.sonar.plugins.core.widgets.issues.*;
import java.util.Arrays;
import java.util.List;
UnresolvedIssuesStatusesWidget.class,
IssueFilterWidget.class,
org.sonar.api.issue.NoSonarFilter.class,
+ IssueChangelogFinder.class,
// issue notifications
SendIssueNotificationsPostJob.class,
NewFalsePositiveNotificationDispatcher.class,
NewFalsePositiveNotificationDispatcher.newMetadata(),
+ // technical debt
+ TechnicalDebtConverter.class,
+ TechnicalDebtCalculator.class,
+ TechnicalDebtDecorator.class,
+ NewTechnicalDebtDecorator.class,
+
// batch
ProfileSensor.class,
ProfileEventsSensor.class,
extensions.addAll(IgnoreIssuesPlugin.getExtensions());
extensions.addAll(CoverageMeasurementFilter.getPropertyDefinitions());
extensions.addAll(PastSnapshotFinder.getPropertyDefinitions());
- extensions.addAll(TechnicalDebtDecorator.extensions());
+ extensions.addAll(TechnicalDebtDecorator.definitions());
extensions.addAll(propertyDefinitions());
return extensions.build();
import org.sonar.core.issue.IssueUpdater;
import org.sonar.core.technicaldebt.TechnicalDebtConverter;
+import javax.annotation.Nullable;
+
import java.io.Serializable;
-import java.util.Collection;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import static com.google.common.collect.Lists.newArrayList;
context.saveMeasure(measure);
}
- private Double calculateNewTechnicalDebtValue(Collection<Issue> issues, Multimap<Issue, FieldDiffs> changelogList, Date periodDate){
+ private Double calculateNewTechnicalDebtValue(Collection<Issue> issues, Multimap<Issue, FieldDiffs> changelogList, @Nullable Date periodDate){
double value = 0;
for (Issue issue : issues) {
WorkDayDuration currentTechnicalDebt = ((DefaultIssue) issue).technicalDebt();
List<FieldDiffs> technicalDebtChangelog = changesOnField(IssueUpdater.TECHNICAL_DEBT, changelogList.get(issue));
if (technicalDebtChangelog.isEmpty()) {
- if (issue.creationDate().after(periodDate)) {
+ if (isAfter(issue.creationDate(), periodDate)) {
value += technicalDebtConverter.toDays(currentTechnicalDebt);
}
} else {
// Changelog have to be sorted from oldest to newest to catch oldest value just before the period date -> By default it's sorted from newest to oldest
for (FieldDiffs fieldDiffs : Lists.reverse(technicalDebtChangelog)) {
- if (fieldDiffs.createdAt().after(periodDate)) {
+ if (isAfter(fieldDiffs.createdAt(), periodDate)) {
WorkDayDuration pastTechnicalDebt = newValue(IssueUpdater.TECHNICAL_DEBT, fieldDiffs);
- // Can new technical debt be null ? It shouldn't but...
double pastTechnicalDebtValue = technicalDebtConverter.toDays(pastTechnicalDebt);
return currentTechnicalDebtValue - pastTechnicalDebtValue;
}
}
- // FIXME is it possible to have null?
return null;
}
return null;
}
+ private boolean isAfter(Date currentDate, @Nullable Date pastDate) {
+ return pastDate == null || (currentDate != null && DateUtils.truncatedCompareTo(currentDate, pastDate, Calendar.SECOND) > 0);
+ }
+
private boolean shouldSaveNewMetrics(DecoratorContext context) {
return context.getMeasure(CoreMetrics.NEW_TECHNICAL_DEBT) == null;
}
}
}
- public static List<?> extensions() {
- ImmutableList.Builder<Object> extensions = ImmutableList.builder();
- extensions.addAll(definitions());
- extensions.add(
- TechnicalDebtDecorator.class, TechnicalDebtCalculator.class
- );
- return extensions.build();
- }
-
- private static List<PropertyDefinition> definitions() {
+ public static List<PropertyDefinition> definitions() {
return ImmutableList.of(
PropertyDefinition.builder(TechnicalDebtConverter.PROPERTY_HOURS_IN_DAY)
.name("Number of working hours in a day")
verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0, 4.0)));
}
+ @Test
+ public void save_on_one_issue_with_changelog_having_null_value() {
+ Issue issue = new DefaultIssue().setKey("A").setCreationDate(fiveDaysAgo).setTechnicalDebt(fiveDaysDebt);
+ when(issuable.issues()).thenReturn(newArrayList(issue));
+
+ Multimap<Issue, FieldDiffs> changelogList = ArrayListMultimap.create();
+ // Changes should be sorted from newest to oldest
+ changelogList.putAll(issue, newArrayList(
+ new FieldDiffs().setDiff("technicalDebt", null, fiveDaysDebt).setCreatedAt(rightNow),
+ new FieldDiffs().setDiff("technicalDebt", oneDaysDebt, null).setCreatedAt(fourDaysAgo),
+ new FieldDiffs().setDiff("technicalDebt", null, oneDaysDebt).setCreatedAt(nineDaysAgo)
+ ));
+ when(changelogFinder.findByIssues(anyCollection())).thenReturn(changelogList);
+
+ decorator.decorate(resource, context);
+
+ // remember : period1 is 5daysAgo, period2 is 10daysAgo
+ verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 5.0, 4.0)));
+ }
+
+ @Test
+ public void save_on_one_issue_with_changelog_and_periods_have_no_dates() {
+ when(timeMachineConfiguration.periods()).thenReturn(newArrayList(new Period(1, null, null), new Period(2, null, null)));
+
+ Issue issue = new DefaultIssue().setKey("A").setCreationDate(fiveDaysAgo).setTechnicalDebt(fiveDaysDebt);
+ when(issuable.issues()).thenReturn(newArrayList(issue));
+
+ Multimap<Issue, FieldDiffs> changelogList = ArrayListMultimap.create();
+ // Changes should be sorted from newest to oldest
+ changelogList.putAll(issue, newArrayList(
+ new FieldDiffs().setDiff("technicalDebt", null, fiveDaysDebt).setCreatedAt(rightNow),
+ new FieldDiffs().setDiff("technicalDebt", oneDaysDebt, null).setCreatedAt(fourDaysAgo),
+ new FieldDiffs().setDiff("technicalDebt", null, oneDaysDebt).setCreatedAt(nineDaysAgo)
+ ));
+ when(changelogFinder.findByIssues(anyCollection())).thenReturn(changelogList);
+
+ decorator.decorate(resource, context);
+
+ // remember : period1 is 5daysAgo, period2 is 10daysAgo
+ verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 4.0, 4.0)));
+ }
+
@Test
public void save_on_issues_with_changelog() {
Issue issue1 = new DefaultIssue().setKey("A").setCreationDate(fiveDaysAgo).setTechnicalDebt(fiveDaysDebt);
verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0, 5.0)));
}
+ @Test
+ public void save_on_one_issue_without_technical_debt_and_without_changelog() {
+ when(issuable.issues()).thenReturn(newArrayList(
+ (Issue) new DefaultIssue().setKey("A").setCreationDate(nineDaysAgo).setTechnicalDebt(null))
+ );
+
+ Multimap<Issue, FieldDiffs> changelogList = ArrayListMultimap.create();
+ when(changelogFinder.findByIssues(anyCollection())).thenReturn(changelogList);
+
+ decorator.decorate(resource, context);
+
+ // remember : period1 is 5daysAgo, period2 is 10daysAgo
+ verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0, 0.0)));
+ }
+
+ @Test
+ public void save_on_one_issue_without_changelog_and_periods_have_no_dates() {
+ when(timeMachineConfiguration.periods()).thenReturn(newArrayList(new Period(1, null, null), new Period(2, null, null)));
+
+ when(issuable.issues()).thenReturn(newArrayList(
+ (Issue) new DefaultIssue().setKey("A").setCreationDate(nineDaysAgo).setTechnicalDebt(fiveDaysDebt))
+ );
+
+ Multimap<Issue, FieldDiffs> changelogList = ArrayListMultimap.create();
+ when(changelogFinder.findByIssues(anyCollection())).thenReturn(changelogList);
+
+ decorator.decorate(resource, context);
+
+ // remember : period1 is null, period2 is null
+ verify(context).saveMeasure(argThat(new IsVariationMeasure(CoreMetrics.NEW_TECHNICAL_DEBT, 5.0, 5.0)));
+ }
+
@Test
public void save_on_issues_without_changelog() {
when(issuable.issues()).thenReturn(newArrayList(
}
@Test
- public void check_extensions() {
- assertThat(TechnicalDebtDecorator.extensions()).hasSize(1 /* properties */ + 2 /* extensions */);
+ public void check_definitions() {
+ assertThat(TechnicalDebtDecorator.definitions()).hasSize(1);
}
}
pastSnapshot.setIndex(projectPastSnapshot.getIndex());
pastSnapshot.setModeParameter(projectPastSnapshot.getModeParameter());
modulePastSnapshots.add(pastSnapshot);
+ // When no snapshot is found, date of the period is null
periods.add(new Period(pastSnapshot.getIndex(), pastSnapshot.getTargetDate(), snapshot != null ? snapshot.getCreatedAt() : null));
log(pastSnapshot);
}
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
-import org.sonar.api.BatchComponent;
+import org.sonar.api.BatchExtension;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.internal.FieldDiffs;
import org.sonar.core.issue.db.IssueChangeDao;
import static com.google.common.collect.Lists.newArrayList;
-public class IssueChangelogFinder implements BatchComponent {
+public class IssueChangelogFinder implements BatchExtension {
private final IssueChangeDao dao;