Browse Source

SONAR-3755 new concept of automatic transitions in issue workflow

tags/3.6
Simon Brandhof 11 years ago
parent
commit
6f1161efb9
53 changed files with 1150 additions and 1139 deletions
  1. 122
    120
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java
  2. 86
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java
  3. 19
    26
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueFilters.java
  4. 44
    0
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java
  5. 15
    4
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java
  6. 47
    101
      plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java
  7. 6
    14
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java
  8. 56
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java
  9. 55
    0
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java
  10. 91
    97
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java
  11. 42
    41
      plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java
  12. 3
    4
      sonar-batch/src/main/java/org/sonar/batch/index/Cache.java
  13. 2
    1
      sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java
  14. 3
    3
      sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java
  15. 8
    3
      sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java
  16. 3
    4
      sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java
  17. 9
    29
      sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java
  18. 7
    7
      sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java
  19. 3
    3
      sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java
  20. 5
    43
      sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java
  21. 16
    7
      sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java
  22. 1
    0
      sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java
  23. 1
    0
      sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java
  24. 2
    2
      sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java
  25. 39
    0
      sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java
  26. 12
    11
      sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java
  27. 8
    2
      sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java
  28. 42
    45
      sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java
  29. 36
    0
      sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java
  30. 36
    0
      sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java
  31. 19
    3
      sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java
  32. 12
    0
      sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java
  33. 6
    6
      sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java
  34. 1
    1
      sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java
  35. 0
    78
      sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java
  36. 42
    0
      sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java
  37. 43
    0
      sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java
  38. 43
    0
      sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java
  39. 10
    65
      sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java
  40. 45
    0
      sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java
  41. 0
    4
      sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java
  42. 0
    175
      sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java
  43. 3
    7
      sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueFilter.java
  44. 58
    0
      sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java
  45. 4
    3
      sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java
  46. 0
    160
      sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java
  47. 10
    5
      sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java
  48. 2
    40
      sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java
  49. 7
    10
      sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java
  50. 2
    2
      sonar-server/src/main/java/org/sonar/server/platform/Platform.java
  51. 1
    1
      sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb
  52. 1
    1
      sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java
  53. 22
    11
      sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java

+ 122
- 120
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java View File

@@ -340,141 +340,143 @@ public final class CorePlugin extends SonarPlugin {
@SuppressWarnings("unchecked")
public List getExtensions() {
return ImmutableList.of(
DefaultResourceTypes.class,
UserManagedMetrics.class,
Periods.class,
DefaultResourceTypes.class,
UserManagedMetrics.class,
Periods.class,

// pages
Lcom4Viewer.class,
TestsViewer.class,
// pages
Lcom4Viewer.class,
TestsViewer.class,

// measure filters
ProjectFilter.class,
MyFavouritesFilter.class,
// measure filters
ProjectFilter.class,
MyFavouritesFilter.class,

// widgets
AlertsWidget.class,
CoverageWidget.class,
ItCoverageWidget.class,
CommentsDuplicationsWidget.class,
DescriptionWidget.class,
ComplexityWidget.class,
RulesWidget.class,
RulesWidget2.class,
SizeWidget.class,
EventsWidget.class,
CustomMeasuresWidget.class,
TimelineWidget.class,
TimeMachineWidget.class,
HotspotMetricWidget.class,
HotspotMostViolatedResourcesWidget.class,
HotspotMostViolatedRulesWidget.class,
MyReviewsWidget.class,
MyIssuesWidget.class,
ProjectReviewsWidget.class,
ActiveIssuesWidget.class,
FalsePositiveReviewsWidget.class,
FalsePositiveIssuesWidget.class,
ReviewsPerDeveloperWidget.class,
PlannedReviewsWidget.class,
UnplannedReviewsWidget.class,
ActionPlansWidget.class,
ReviewsMetricsWidget.class,
TreemapWidget.class,
MeasureFilterListWidget.class,
MeasureFilterTreemapWidget.class,
WelcomeWidget.class,
// widgets
AlertsWidget.class,
CoverageWidget.class,
ItCoverageWidget.class,
CommentsDuplicationsWidget.class,
DescriptionWidget.class,
ComplexityWidget.class,
RulesWidget.class,
RulesWidget2.class,
SizeWidget.class,
EventsWidget.class,
CustomMeasuresWidget.class,
TimelineWidget.class,
TimeMachineWidget.class,
HotspotMetricWidget.class,
ReviewsMetricsWidget.class,
TreemapWidget.class,
MeasureFilterListWidget.class,
MeasureFilterTreemapWidget.class,
WelcomeWidget.class,

// dashboards
ProjectDefaultDashboard.class,
ProjectHotspotDashboard.class,
ProjectReviewsDashboard.class,
ProjectTimeMachineDashboard.class,
GlobalDefaultDashboard.class,
// dashboards
ProjectDefaultDashboard.class,
ProjectHotspotDashboard.class,
ProjectReviewsDashboard.class,
ProjectTimeMachineDashboard.class,
GlobalDefaultDashboard.class,

// chart
XradarChart.class,
DistributionBarChart.class,
DistributionAreaChart.class,
// chart
XradarChart.class,
DistributionBarChart.class,
DistributionAreaChart.class,

// colorizers
JavaColorizerFormat.class,
// colorizers
JavaColorizerFormat.class,

// issues
// issues
IssueHandlers.class,
IssueFilters.class,
IssueCountersDecorator.class,
WeightedIssuesDecorator.class,
IssuesDensityDecorator.class,
InitialOpenIssuesSensor.class,
InitialOpenIssuesStack.class,
HotspotMostViolatedResourcesWidget.class,
HotspotMostViolatedRulesWidget.class,
MyReviewsWidget.class,
MyIssuesWidget.class,
ProjectReviewsWidget.class,
ActiveIssuesWidget.class,
FalsePositiveReviewsWidget.class,
FalsePositiveIssuesWidget.class,
ReviewsPerDeveloperWidget.class,
PlannedReviewsWidget.class,
UnplannedReviewsWidget.class,
ActionPlansWidget.class,

// batch
ProfileSensor.class,
ProfileEventsSensor.class,
ProjectLinksSensor.class,
UnitTestDecorator.class,
VersionEventsSensor.class,
CheckAlertThresholds.class,
GenerateAlertEvents.class,
ViolationsDecorator.class,
IssueTrackingDecorator.class,
WeightedViolationsDecorator.class,
ViolationsDensityDecorator.class,
LineCoverageDecorator.class,
CoverageDecorator.class,
BranchCoverageDecorator.class,
ItLineCoverageDecorator.class,
ItCoverageDecorator.class,
ItBranchCoverageDecorator.class,
OverallLineCoverageDecorator.class,
OverallCoverageDecorator.class,
OverallBranchCoverageDecorator.class,
ApplyProjectRolesDecorator.class,
CommentDensityDecorator.class,
NoSonarFilter.class,
DirectoriesDecorator.class,
FilesDecorator.class,
ReviewNotifications.class,
ReviewWorkflowDecorator.class,
ManualMeasureDecorator.class,
ManualViolationInjector.class,
ViolationSeverityUpdater.class,
IndexProjectPostJob.class,
ReviewsMeasuresDecorator.class,
ProfileSensor.class,
ProfileEventsSensor.class,
ProjectLinksSensor.class,
UnitTestDecorator.class,
VersionEventsSensor.class,
CheckAlertThresholds.class,
GenerateAlertEvents.class,
ViolationsDecorator.class,
IssueTrackingDecorator.class,
WeightedViolationsDecorator.class,
ViolationsDensityDecorator.class,
LineCoverageDecorator.class,
CoverageDecorator.class,
BranchCoverageDecorator.class,
ItLineCoverageDecorator.class,
ItCoverageDecorator.class,
ItBranchCoverageDecorator.class,
OverallLineCoverageDecorator.class,
OverallCoverageDecorator.class,
OverallBranchCoverageDecorator.class,
ApplyProjectRolesDecorator.class,
CommentDensityDecorator.class,
NoSonarFilter.class,
DirectoriesDecorator.class,
FilesDecorator.class,
ReviewNotifications.class,
ReviewWorkflowDecorator.class,
ManualMeasureDecorator.class,
ManualViolationInjector.class,
ViolationSeverityUpdater.class,
IndexProjectPostJob.class,
ReviewsMeasuresDecorator.class,

// time machine
TendencyDecorator.class,
VariationDecorator.class,
ViolationTrackingDecorator.class,
IssueTracking.class,
ViolationPersisterDecorator.class,
NewViolationsDecorator.class,
TimeMachineConfigurationPersister.class,
NewCoverageFileAnalyzer.class,
NewItCoverageFileAnalyzer.class,
NewOverallCoverageFileAnalyzer.class,
NewCoverageAggregator.class,
// time machine
TendencyDecorator.class,
VariationDecorator.class,
ViolationTrackingDecorator.class,
IssueTracking.class,
ViolationPersisterDecorator.class,
NewViolationsDecorator.class,
TimeMachineConfigurationPersister.class,
NewCoverageFileAnalyzer.class,
NewItCoverageFileAnalyzer.class,
NewOverallCoverageFileAnalyzer.class,
NewCoverageAggregator.class,

// notifications
// Notify incoming violations on my favourite projects
NewViolationsOnFirstDifferentialPeriod.class,
NotificationDispatcherMetadata.create("NewViolationsOnFirstDifferentialPeriod")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify alerts on my favourite projects
NewAlerts.class,
NotificationDispatcherMetadata.create("NewAlerts")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify reviews changes
ChangesInReviewAssignedToMeOrCreatedByMe.class,
NotificationDispatcherMetadata.create("ChangesInReviewAssignedToMeOrCreatedByMe")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify new false positive resolution
NewFalsePositiveReview.class,
NotificationDispatcherMetadata.create("NewFalsePositiveReview")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true))
);
// notifications
// Notify incoming violations on my favourite projects
NewViolationsOnFirstDifferentialPeriod.class,
NotificationDispatcherMetadata.create("NewViolationsOnFirstDifferentialPeriod")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify alerts on my favourite projects
NewAlerts.class,
NotificationDispatcherMetadata.create("NewAlerts")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify reviews changes
ChangesInReviewAssignedToMeOrCreatedByMe.class,
NotificationDispatcherMetadata.create("ChangesInReviewAssignedToMeOrCreatedByMe")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true)),
// Notify new false positive resolution
NewFalsePositiveReview.class,
NotificationDispatcherMetadata.create("NewFalsePositiveReview")
.setProperty(NotificationDispatcherMetadata.GLOBAL_NOTIFICATION, String.valueOf(true))
.setProperty(NotificationDispatcherMetadata.PER_PROJECT_NOTIFICATION, String.valueOf(true))
);
}
}

+ 86
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java View File

@@ -0,0 +1,86 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plugins.core.issue;

import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueHandler;
import org.sonar.core.issue.DefaultIssue;

import javax.annotation.Nullable;

class DefaultIssueHandlerContext implements IssueHandler.IssueContext {

private final DefaultIssue issue;

DefaultIssueHandlerContext(DefaultIssue issue) {
this.issue = issue;
}

@Override
public Issue issue() {
return issue;
}

@Override
public boolean isNew() {
return issue.isNew();
}

@Override
public boolean isAlive() {
return issue.isAlive();
}

@Override
public IssueHandler.IssueContext setLine(@Nullable Integer line) {
issue.setLine(line);
return this;
}

@Override
public IssueHandler.IssueContext setDescription(String description) {
issue.setDescription(description);
return this;
}

@Override
public IssueHandler.IssueContext setSeverity(String severity) {
issue.setSeverity(severity);
return this;
}

@Override
public IssueHandler.IssueContext setAuthorLogin(@Nullable String login) {
issue.setAuthorLogin(login);
return this;
}

@Override
public IssueHandler.IssueContext setAttribute(String key, @Nullable String value) {
issue.setAttribute(key, value);
return this;
}

@Override
public IssueHandler.IssueContext assignTo(@Nullable String login) {
issue.setAssignee(login);
return this;
}
}

sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java → plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueFilters.java View File

@@ -17,36 +17,29 @@
* 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.core.issue;
package org.sonar.plugins.core.issue;

import org.sonar.api.issue.IssueChange;
import org.sonar.api.BatchExtension;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueFilter;

import java.util.Map;
public class IssueFilters implements BatchExtension {
private final IssueFilter[] filters;

public class UpdateIssueFields {
public IssueFilters(IssueFilter[] filters) {
this.filters = filters;
}

public static void apply(DefaultIssue issue, IssueChange change) {
if (change.description() != null) {
issue.setDescription(change.description());
}
if (change.manualSeverity() != null) {
change.setManualSeverity(change.manualSeverity());
}
if (change.severity() != null) {
issue.setSeverity(change.severity());
}
if (change.isAssigneeChanged()) {
issue.setAssignee(change.assignee());
}
if (change.isLineChanged()) {
issue.setLine(change.line());
}
if (change.isCostChanged()) {
issue.setCost(change.cost());
}
for (Map.Entry<String, String> entry : change.attributes().entrySet()) {
issue.setAttribute(entry.getKey(), entry.getValue());
}
public IssueFilters() {
this(new IssueFilter[0]);
}

public boolean accept(Issue issue) {
for (IssueFilter filter : filters) {
if (!filter.accept(issue)) {
return false;
}
}
return true;
}
}

+ 44
- 0
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java View File

@@ -0,0 +1,44 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plugins.core.issue;

import org.sonar.api.BatchExtension;
import org.sonar.api.issue.IssueHandler;
import org.sonar.core.issue.DefaultIssue;

public class IssueHandlers implements BatchExtension {
private final IssueHandler[] handlers;

public IssueHandlers(IssueHandler[] handlers) {
this.handlers = handlers;
}

public IssueHandlers() {
this(new IssueHandler[0]);
}

public void execute(DefaultIssue issue) {
DefaultIssueHandlerContext context = new DefaultIssueHandlerContext(issue);
for (IssueHandler handler : handlers) {
handler.onIssue(context);
}
}

}

+ 15
- 4
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java View File

@@ -28,6 +28,7 @@ import org.sonar.api.batch.SonarIndex;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Resource;
import org.sonar.api.rules.RuleFinder;
import org.sonar.api.utils.KeyValueFormat;
import org.sonar.batch.scan.LastSnapshots;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueDto;
@@ -70,14 +71,19 @@ public class IssueTracking implements BatchExtension {
this.index = index;
}

public void track(Resource resource, Collection<IssueDto> referenceIssues, Collection<DefaultIssue> newIssues) {
/**
* @return untracked issues
*/
public Set<IssueDto> track(Resource resource, Collection<IssueDto> dbIssues, Collection<DefaultIssue> newIssues) {
referenceIssuesMap.clear();
unmappedLastIssues.clear();

String source = index.getSource(resource);
setChecksumOnNewIssues(newIssues, source);

// Map new issues with old ones
mapIssues(newIssues, referenceIssues, source, resource);
mapIssues(newIssues, dbIssues, source, resource);
return unmappedLastIssues;
}

private void setChecksumOnNewIssues(Collection<DefaultIssue> issues, String source) {
@@ -113,7 +119,6 @@ public class IssueTracking implements BatchExtension {
mapIssuesOnSameRule(newIssues, lastIssuesByRule);
}

unmappedLastIssues.clear();
return referenceIssuesMap;
}

@@ -342,13 +347,19 @@ public class IssueTracking implements BatchExtension {
if (pastIssue != null) {
newIssue.setKey(pastIssue.getKey());
if (pastIssue.isManualSeverity()) {
newIssue.setManualSeverity(true);
newIssue.setSeverity(pastIssue.getSeverity());
}

newIssue.setResolution(pastIssue.getResolution());
newIssue.setStatus(pastIssue.getStatus());
newIssue.setCreatedAt(pastIssue.getCreatedAt());
newIssue.setUpdatedAt(project.getAnalysisDate());
newIssue.setNew(false);
newIssue.setAlive(true);
newIssue.setAuthorLogin(pastIssue.getAuthorLogin());
if (pastIssue.getAttributes() != null) {
newIssue.setAttributes(KeyValueFormat.parse(pastIssue.getAttributes()));
}

lastIssuesByRule.remove(getRuleId(newIssue), pastIssue);
issueMap.put(newIssue, pastIssue);

+ 47
- 101
plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java View File

@@ -19,9 +19,7 @@
*/
package org.sonar.plugins.core.issue;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.Sets;
import com.google.common.collect.Lists;
import org.sonar.api.batch.Decorator;
import org.sonar.api.batch.DecoratorBarriers;
import org.sonar.api.batch.DecoratorContext;
@@ -34,25 +32,29 @@ import org.sonar.api.resources.Scopes;
import org.sonar.batch.issue.ScanIssues;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueDto;
import org.sonar.core.issue.workflow.IssueWorkflow;

import javax.annotation.Nullable;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Set;

@DependedUpon(DecoratorBarriers.END_OF_ISSUES_UPDATES)
public class IssueTrackingDecorator implements Decorator {

private final ScanIssues scanIssues;
private final InitialOpenIssuesStack initialOpenIssuesStack;
private final InitialOpenIssuesStack initialOpenIssues;
private final IssueTracking tracking;
private final IssueFilters filters;
private final IssueHandlers handlers;
private final IssueWorkflow workflow;

public IssueTrackingDecorator(ScanIssues scanIssues, InitialOpenIssuesStack initialOpenIssuesStack, IssueTracking tracking) {
public IssueTrackingDecorator(ScanIssues scanIssues, InitialOpenIssuesStack initialOpenIssues, IssueTracking tracking,
IssueFilters filters, IssueHandlers handlers, IssueWorkflow workflow) {
this.scanIssues = scanIssues;
this.initialOpenIssuesStack = initialOpenIssuesStack;
this.initialOpenIssues = initialOpenIssues;
this.tracking = tracking;
this.filters = filters;
this.handlers = handlers;
this.workflow = workflow;
}

public boolean shouldExecuteOnProject(Project project) {
@@ -60,112 +62,56 @@ public class IssueTrackingDecorator implements Decorator {
}

public void decorate(Resource resource, DecoratorContext context) {
if (isComponentSupported(resource)) {
if (canHaveIssues(resource)) {
// all the issues created by rule engines during this module scan
Collection<DefaultIssue> newIssues = new ArrayList(scanIssues.issues(resource.getEffectiveKey()));
Collection<DefaultIssue> issues = Lists.newArrayList();
for (Issue issue : scanIssues.issues(resource.getEffectiveKey())) {
if (filters.accept(issue)) {
issues.add((DefaultIssue) issue);
} else {
scanIssues.remove(issue);
}
}

// all the issues that are open in db before starting this module scan
Collection<IssueDto> openIssues = initialOpenIssuesStack.selectAndRemove(resource.getId());

tracking.track(resource, openIssues, newIssues);

updateIssues(newIssues);

Set<String> issueKeys = Sets.newHashSet(Collections2.transform(newIssues, new IssueToKeyFunction()));
for (IssueDto openIssue : openIssues) {
// not in newIssues
addManualIssuesAndCloseResolvedOnes(openIssue);

closeResolvedStandardIssues(openIssue, issueKeys);
keepFalsePositiveIssues(openIssue);
reopenUnresolvedIssues(openIssue);
Collection<IssueDto> dbOpenIssues = initialOpenIssues.selectAndRemove(resource.getId());
Set<IssueDto> unmatchedDbIssues = tracking.track(resource, dbOpenIssues, issues);
// TODO register manual issues (isAlive=true, isNew=false) ? Or are they included in unmatchedDbIssues ?
addUnmatched(unmatchedDbIssues, issues);

if (ResourceUtils.isProject(resource)) {
// issues that relate to deleted components
addDead(issues);
}

if (ResourceUtils.isRootProject(resource)) {
closeIssuesOnDeletedResources(initialOpenIssuesStack.getAllIssues());
for (DefaultIssue issue : issues) {
workflow.doAutomaticTransition(issue);
handlers.execute(issue);
scanIssues.addOrUpdate(issue);
}
}
}

private void updateIssues(Collection<DefaultIssue> newIssues) {
for (DefaultIssue issue : newIssues) {
scanIssues.addOrUpdate(issue);
}
}

private void addManualIssuesAndCloseResolvedOnes(IssueDto openIssue) {
if (openIssue.isManualIssue()) {
DefaultIssue issue = openIssue.toDefaultIssue();
if (Issue.STATUS_RESOLVED.equals(issue.status())) {
close(issue);
}
scanIssues.addOrUpdate(issue);
}
}

private void closeResolvedStandardIssues(IssueDto openIssue, Set<String> issueKeys) {
if (!openIssue.isManualIssue() && !issueKeys.contains(openIssue.getKey())) {
closeAndSave(openIssue);
}
}

private void keepFalsePositiveIssues(IssueDto openIssue) {
if (!openIssue.isManualIssue() && Issue.RESOLUTION_FALSE_POSITIVE.equals(openIssue.getResolution())) {
DefaultIssue issue = openIssue.toDefaultIssue();
issue.setResolution(openIssue.getResolution());
issue.setStatus(openIssue.getStatus());
issue.setUpdatedAt(getLoadedDate());
scanIssues.addOrUpdate(issue);
}
}

private void reopenUnresolvedIssues(IssueDto openIssue) {
if (Issue.STATUS_RESOLVED.equals(openIssue.getStatus()) && !Issue.RESOLUTION_FALSE_POSITIVE.equals(openIssue.getResolution())
&& !openIssue.isManualIssue()) {
reopenAndSave(openIssue);
private void addUnmatched(Set<IssueDto> unmatchedDbIssues, Collection<DefaultIssue> issues) {
for (IssueDto unmatchedDto : unmatchedDbIssues) {
DefaultIssue unmatched = unmatchedDto.toDefaultIssue();
unmatched.setAlive(false);
unmatched.setNew(false);
issues.add(unmatched);
}
}

/**
* Close issues that relate to resources that have been deleted or renamed.
*/
private void closeIssuesOnDeletedResources(Collection<IssueDto> openIssues) {
for (IssueDto openIssue : openIssues) {
closeAndSave(openIssue);
private void addDead(Collection<DefaultIssue> issues) {
for (IssueDto deadDto : initialOpenIssues.getAllIssues()) {
DefaultIssue dead = deadDto.toDefaultIssue();
dead.setAlive(false);
dead.setNew(false);
issues.add(dead);
}
}

private void close(DefaultIssue issue) {
issue.setStatus(Issue.STATUS_CLOSED);
issue.setUpdatedAt(getLoadedDate());
issue.setClosedAt(getLoadedDate());
}

private void closeAndSave(IssueDto openIssue) {
DefaultIssue issue = openIssue.toDefaultIssue();
close(issue);
scanIssues.addOrUpdate(issue);
}

private void reopenAndSave(IssueDto openIssue) {
DefaultIssue issue = openIssue.toDefaultIssue();
issue.setStatus(Issue.STATUS_REOPENED);
issue.setResolution(Issue.RESOLUTION_OPEN);
issue.setUpdatedAt(getLoadedDate());
scanIssues.addOrUpdate(issue);
}

private boolean isComponentSupported(Resource resource) {
private boolean canHaveIssues(Resource resource) {
// TODO check existence of perspective Issuable ?
return Scopes.isHigherThanOrEquals(resource.getScope(), Scopes.FILE);
}

private static final class IssueToKeyFunction implements Function<Issue, String> {
public String apply(@Nullable Issue issue) {
return (issue != null ? issue.key() : null);
}
}

private Date getLoadedDate() {
return initialOpenIssuesStack.getLoadedDate();
}
}

+ 6
- 14
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java View File

@@ -20,7 +20,6 @@

package org.sonar.plugins.core.issue;

import org.junit.Before;
import org.junit.Test;
import org.sonar.api.resources.Project;
import org.sonar.core.issue.IssueDao;
@@ -36,25 +35,18 @@ import static org.mockito.Mockito.verify;

public class InitialOpenIssuesSensorTest {

private InitialOpenIssuesSensor initialOpenIssuesSensor;
private InitialOpenIssuesStack initialOpenIssuesStack;
private IssueDao issueDao;
InitialOpenIssuesStack stack = mock(InitialOpenIssuesStack.class);
IssueDao issueDao = mock(IssueDao.class);
InitialOpenIssuesSensor sensor = new InitialOpenIssuesSensor(stack, issueDao);

@Before
public void before() {
initialOpenIssuesStack = mock(InitialOpenIssuesStack.class);
issueDao = mock(IssueDao.class);

initialOpenIssuesSensor = new InitialOpenIssuesSensor(initialOpenIssuesStack, issueDao);
}

@Test
public void should_analyse() {
public void should_select_module_open_issues() {
Project project = new Project("key");
project.setId(1);
initialOpenIssuesSensor.analyse(project, null);
sensor.analyse(project, null);

verify(issueDao).selectOpenIssues(1);
verify(initialOpenIssuesStack).setIssues(anyListOf(IssueDto.class), any(Date.class));
verify(stack).setIssues(anyListOf(IssueDto.class), any(Date.class));
}
}

+ 56
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java View File

@@ -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.plugins.core.issue;

import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueFilter;
import org.sonar.core.issue.DefaultIssue;

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

public class IssueFiltersTest {
@Test
public void test_accept() throws Exception {
IssueFilter ok = mock(IssueFilter.class);
when(ok.accept(any(Issue.class))).thenReturn(true);

IssueFilter ko = mock(IssueFilter.class);
when(ko.accept(any(Issue.class))).thenReturn(false);

IssueFilters filters = new IssueFilters(new IssueFilter[]{ok, ko});
assertThat(filters.accept(new DefaultIssue())).isFalse();

filters = new IssueFilters(new IssueFilter[]{ok});
assertThat(filters.accept(new DefaultIssue())).isTrue();

filters = new IssueFilters(new IssueFilter[]{ko});
assertThat(filters.accept(new DefaultIssue())).isFalse();
}

@Test
public void should_always_accept_if_no_filters() {
IssueFilters filters = new IssueFilters();
assertThat(filters.accept(new DefaultIssue())).isTrue();
}
}

+ 55
- 0
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java View File

@@ -0,0 +1,55 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.plugins.core.issue;

import org.junit.Test;
import org.mockito.ArgumentMatcher;
import org.sonar.api.issue.IssueHandler;
import org.sonar.core.issue.DefaultIssue;

import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

public class IssueHandlersTest {
@Test
public void should_execute_handlers() throws Exception {
IssueHandler h1 = mock(IssueHandler.class);
IssueHandler h2 = mock(IssueHandler.class);

IssueHandlers handlers = new IssueHandlers(new IssueHandler[]{h1, h2});
final DefaultIssue issue = new DefaultIssue();
handlers.execute(issue);

verify(h1).onIssue(argThat(new ArgumentMatcher<IssueHandler.IssueContext>() {
@Override
public boolean matches(Object o) {
return ((IssueHandler.IssueContext) o).issue() == issue;
}
}));
}

@Test
public void test_no_handlers() {
IssueHandlers handlers = new IssueHandlers();
handlers.execute(new DefaultIssue());
// do not fail
}
}

+ 91
- 97
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java View File

@@ -19,147 +19,141 @@
*/
package org.sonar.plugins.core.issue;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.api.issue.Issue;
import org.mockito.ArgumentMatcher;
import org.sonar.api.batch.DecoratorContext;
import org.sonar.api.resources.File;
import org.sonar.api.resources.Project;
import org.sonar.api.resources.Qualifiers;
import org.sonar.api.resources.Resource;
import org.sonar.batch.issue.ScanIssues;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueDto;
import org.sonar.core.issue.workflow.IssueWorkflow;
import org.sonar.core.persistence.AbstractDaoTestCase;
import org.sonar.java.api.JavaClass;

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

import static com.google.common.collect.Lists.newArrayList;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.*;

public class IssueTrackingDecoratorTest extends AbstractDaoTestCase {

IssueTrackingDecorator decorator;
ScanIssues scanIssues = mock(ScanIssues.class);
InitialOpenIssuesStack initialOpenIssuesStack = mock(InitialOpenIssuesStack.class);
InitialOpenIssuesStack initialOpenIssues = mock(InitialOpenIssuesStack.class);
IssueTracking tracking = mock(IssueTracking.class);
IssueFilters filters = mock(IssueFilters.class);
IssueHandlers handlers = mock(IssueHandlers.class);
IssueWorkflow workflow = mock(IssueWorkflow.class);
Date loadedDate = new Date();

@Before
public void init() {
when(initialOpenIssuesStack.getLoadedDate()).thenReturn(loadedDate);
decorator = new IssueTrackingDecorator(scanIssues, initialOpenIssuesStack, tracking);
when(initialOpenIssues.getLoadedDate()).thenReturn(loadedDate);
decorator = new IssueTrackingDecorator(scanIssues, initialOpenIssues, tracking, filters, handlers, workflow);
}

@Test
public void should_execute_on_project() {
Project project = mock(Project.class);
when(project.isLatestAnalysis()).thenReturn(true);
assertTrue(decorator.shouldExecuteOnProject(project));
assertThat(decorator.shouldExecuteOnProject(project)).isTrue();
}

@Test
public void should_execute_on_project_not_if_past_scan() {
public void should_not_execute_on_project_if_past_scan() {
Project project = mock(Project.class);
when(project.isLatestAnalysis()).thenReturn(false);
assertFalse(decorator.shouldExecuteOnProject(project));
assertThat(decorator.shouldExecuteOnProject(project)).isFalse();
}

@Test
public void should_close_resolved_issue() {
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList());
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList(
new IssueDto().setKey("100").setRuleId(10).setRuleKey_unit_test_only("squid", "AvoidCycle")));

decorator.decorate(mock(Resource.class), null);

ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(scanIssues).addOrUpdate(argument.capture());
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED);
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate);
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate);
public void should_not_be_executed_on_classes_not_methods() throws Exception {
DecoratorContext context = mock(DecoratorContext.class);
decorator.decorate(JavaClass.create("org.foo.Bar"), context);
verifyZeroInteractions(context, scanIssues, tracking, filters, handlers, workflow);
}

@Test
public void should_close_resolved_manual_issue() {
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList());
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList(
new IssueDto().setKey("100").setRuleId(1).setManualIssue(true).setStatus(Issue.STATUS_RESOLVED).setRuleKey_unit_test_only("squid", "AvoidCycle")));

decorator.decorate(mock(Resource.class), null);

ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(scanIssues).addOrUpdate(argument.capture());
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED);
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate);
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate);
public void should_process_open_issues() throws Exception {
Resource file = new File("Action.java").setEffectiveKey("struts:Action.java").setId(123);
final DefaultIssue issue = new DefaultIssue();

// INPUT : one issue, no open issues during previous scan, no filtering
when(scanIssues.issues("struts:Action.java")).thenReturn(Arrays.asList(issue));
when(filters.accept(issue)).thenReturn(true);
List<IssueDto> dbIssues = Collections.emptyList();
when(initialOpenIssues.selectAndRemove(123)).thenReturn(dbIssues);

decorator.decorate(file, mock(DecoratorContext.class));

// Apply filters, track, apply transitions, notify extensions then update cache
verify(filters).accept(issue);
verify(tracking).track(eq(file), eq(dbIssues), argThat(new ArgumentMatcher<Collection<DefaultIssue>>() {
@Override
public boolean matches(Object o) {
List<DefaultIssue> issues = (List<DefaultIssue>) o;
return issues.size() == 1 && issues.get(0) == issue;
}
}));
verify(workflow).doAutomaticTransition(issue);
verify(handlers).execute(issue);
verify(scanIssues).addOrUpdate(issue);
}

@Test
public void should_reopen_unresolved_issue() {
when(scanIssues.issues(anyString())).thenReturn(Lists.<Issue>newArrayList(
new DefaultIssue().setKey("100")));
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList(
new IssueDto().setKey("100").setRuleId(1).setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FIXED)
.setRuleKey_unit_test_only("squid", "AvoidCycle")));

decorator.decorate(mock(Resource.class), null);

ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(scanIssues, times(2)).addOrUpdate(argument.capture());

List<DefaultIssue> capturedDefaultIssues = argument.getAllValues();
// First call is done when updating issues after calling issue tracking and we don't care
DefaultIssue defaultIssue = capturedDefaultIssues.get(1);
assertThat(defaultIssue.status()).isEqualTo(Issue.STATUS_REOPENED);
assertThat(defaultIssue.resolution()).isEqualTo(Issue.RESOLUTION_OPEN);
assertThat(defaultIssue.updatedAt()).isEqualTo(loadedDate);
public void should_register_unmatched_issues() throws Exception {
// "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
Resource file = new File("Action.java").setEffectiveKey("struts:Action.java").setId(123);
DefaultIssue openIssue = new DefaultIssue();

// INPUT : one issue, one open issue during previous scan, no filtering
when(scanIssues.issues("struts:Action.java")).thenReturn(Arrays.asList(openIssue));
when(filters.accept(openIssue)).thenReturn(true);
IssueDto unmatchedIssue = new IssueDto().setKey("ABCDE").setResolution("OPEN").setStatus("OPEN").setRuleKey_unit_test_only("squid", "AvoidCycle");
List<IssueDto> unmatchedIssues = Arrays.asList(unmatchedIssue);
when(tracking.track(eq(file), anyCollection(), anyCollection())).thenReturn(Sets.newHashSet(unmatchedIssues));

decorator.decorate(file, mock(DecoratorContext.class));

verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class));
verify(handlers, times(2)).execute(any(DefaultIssue.class));
verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class));

verify(scanIssues).addOrUpdate(argThat(new ArgumentMatcher<DefaultIssue>() {
@Override
public boolean matches(Object o) {
DefaultIssue issue = (DefaultIssue) o;
return "ABCDE".equals(issue.key());
}
}));
}

@Test
public void should_keep_false_positive_issue() {
when(scanIssues.issues(anyString())).thenReturn(Lists.<Issue>newArrayList(
new DefaultIssue().setKey("100")));
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList(
new IssueDto().setKey("100").setRuleId(1).setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE)
.setRuleKey_unit_test_only("squid", "AvoidCycle")));

decorator.decorate(mock(Resource.class), null);

ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(scanIssues, times(2)).addOrUpdate(argument.capture());

List<DefaultIssue> capturedDefaultIssues = argument.getAllValues();
// First call is done when updating issues after calling issue tracking and we don't care
DefaultIssue defaultIssue = capturedDefaultIssues.get(1);
assertThat(defaultIssue.status()).isEqualTo(Issue.STATUS_RESOLVED);
assertThat(defaultIssue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE);
assertThat(defaultIssue.updatedAt()).isEqualTo(loadedDate);
public void should_register_issues_on_deleted_components() throws Exception {
Project project = new Project("struts");
DefaultIssue openIssue = new DefaultIssue();
when(scanIssues.issues("struts")).thenReturn(Arrays.asList(openIssue));
when(filters.accept(openIssue)).thenReturn(true);
IssueDto deadIssue = new IssueDto().setKey("ABCDE").setResolution("OPEN").setStatus("OPEN").setRuleKey_unit_test_only("squid", "AvoidCycle");
when(initialOpenIssues.getAllIssues()).thenReturn(Arrays.asList(deadIssue));

decorator.decorate(project, mock(DecoratorContext.class));

// the dead issue must be closed -> apply automatic transition, notify handlers and add to cache
verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class));
verify(handlers, times(2)).execute(any(DefaultIssue.class));
verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class));

verify(scanIssues).addOrUpdate(argThat(new ArgumentMatcher<DefaultIssue>() {
@Override
public boolean matches(Object o) {
DefaultIssue dead = (DefaultIssue) o;
return "ABCDE".equals(dead.key()) && !dead.isNew() && !dead.isAlive();
}
}));
}

@Test
public void should_close_remaining_open_issue_on_root_project() {
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList());
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(Collections.<IssueDto>emptyList());

when(initialOpenIssuesStack.getAllIssues()).thenReturn(newArrayList(new IssueDto().setKey("100").setRuleId(1).setRuleKey_unit_test_only("squid", "AvoidCycle")));

Resource resource = mock(Resource.class);
when(resource.getQualifier()).thenReturn(Qualifiers.PROJECT);
decorator.decorate(resource, null);

ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(scanIssues).addOrUpdate(argument.capture());
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED);
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate);
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate);
}

}

+ 42
- 41
plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java View File

@@ -25,6 +25,7 @@ import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.api.resources.Project;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.Rule;
@@ -47,14 +48,12 @@ import static org.mockito.Mockito.when;
public class IssueTrackingTest {

private final Date analysisDate = DateUtils.parseDate("2013-04-11");
private IssueTracking decorator;

private Project project;
private RuleFinder ruleFinder;

private LastSnapshots lastSnapshots;

private long violationId = 0;
IssueTracking tracking;
Project project;
RuleFinder ruleFinder;
LastSnapshots lastSnapshots;
long violationId = 0;

@Before
public void before() {
@@ -77,7 +76,7 @@ public class IssueTrackingTest {

project = mock(Project.class);
when(project.getAnalysisDate()).thenReturn(analysisDate);
decorator = new IssueTracking(project, ruleFinder, lastSnapshots, null);
tracking = new IssueTracking(project, ruleFinder, lastSnapshots, null);
}

@Test
@@ -88,9 +87,9 @@ public class IssueTrackingTest {
// exactly the fields of referenceIssue1 but not the same key
DefaultIssue newIssue = newDefaultIssue("message", 10, RuleKey.of("squid", "AvoidCycle"), "checksum1").setKey("200");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue1, referenceIssue2));
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue1, referenceIssue2));
// same key
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue2);
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue2);
assertThat(newIssue.isNew()).isFalse();
}

@@ -102,10 +101,10 @@ public class IssueTrackingTest {
DefaultIssue newIssue1 = newDefaultIssue("message", 3, RuleKey.of("squid", "AvoidCycle"), "checksum1");
DefaultIssue newIssue2 = newDefaultIssue("message", 5, RuleKey.of("squid", "AvoidCycle"), "checksum2");

decorator.mapIssues(newArrayList(newIssue1, newIssue2), newArrayList(referenceIssue1, referenceIssue2));
assertThat(decorator.getReferenceIssue(newIssue1)).isSameAs(referenceIssue1);
tracking.mapIssues(newArrayList(newIssue1, newIssue2), newArrayList(referenceIssue1, referenceIssue2));
assertThat(tracking.getReferenceIssue(newIssue1)).isSameAs(referenceIssue1);
assertThat(newIssue1.isNew()).isFalse();
assertThat(decorator.getReferenceIssue(newIssue2)).isSameAs(referenceIssue2);
assertThat(tracking.getReferenceIssue(newIssue2)).isSameAs(referenceIssue2);
assertThat(newIssue2.isNew()).isFalse();
}

@@ -117,8 +116,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("new message", null, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("old message", null, 1, "checksum1");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -127,8 +126,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("old message", 1, 1, "checksum1");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -137,8 +136,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum2");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -147,8 +146,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), null);
IssueDto referenceIssue = newReferenceIssue("message", 1, 2, null);

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isNull();
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isNull();
assertThat(newIssue.isNew()).isTrue();
}

@@ -157,8 +156,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("message", 2, 1, "checksum1");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -170,8 +169,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("old message", 2, 1, "checksum1");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -180,8 +179,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("message", 2, 1, "checksum2");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isNull();
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isNull();
assertThat(newIssue.isNew()).isTrue();
}

@@ -190,8 +189,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("message", 1, 2, "checksum1");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isNull();
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isNull();
assertThat(newIssue.isNew()).isTrue();
}

@@ -202,8 +201,8 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue(" message ", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum2");

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue);
assertThat(newIssue.isNew()).isFalse();
}

@@ -212,7 +211,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum");
assertThat(newIssue.createdAt()).isNull();

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(newArrayList(newIssue), Lists.<IssueDto>newArrayList());
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(newArrayList(newIssue), Lists.<IssueDto>newArrayList());
assertThat(mapping.size()).isEqualTo(0);
assertThat(newIssue.createdAt()).isEqualTo(analysisDate);
assertThat(newIssue.isNew()).isTrue();
@@ -223,7 +222,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum").setSeverity("MAJOR");
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum").setSeverity("MINOR").setManualSeverity(true);

decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(newIssue.severity()).isEqualTo("MINOR");
}

@@ -235,7 +234,7 @@ public class IssueTrackingTest {
referenceIssue.setCreatedAt(referenceDate);
assertThat(newIssue.createdAt()).isNull();

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue));
assertThat(mapping.size()).isEqualTo(1);
assertThat(newIssue.createdAt()).isEqualTo(referenceDate);
assertThat(newIssue.isNew()).isFalse();
@@ -250,7 +249,7 @@ public class IssueTrackingTest {
IssueDto referenceIssue = newReferenceIssue("2 branches need to be covered", null, 1, null);


Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
newArrayList(newIssue),
newArrayList(referenceIssue),
source, project);
@@ -267,7 +266,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), "foo");
IssueDto referenceIssue = newReferenceIssue("Indentationd", 7, 1, null);

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
newArrayList(newIssue),
newArrayList(referenceIssue),
source, project);
@@ -287,7 +286,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), null);
IssueDto referenceIssue = newReferenceIssue("2 branches need to be covered", null, 1, null);

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
newArrayList(newIssue),
newArrayList(referenceIssue),
source, project);
@@ -312,7 +311,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue3 = newDefaultIssue("Indentation", 17, RuleKey.of("squid", "AvoidCycle"), null);
DefaultIssue newIssue4 = newDefaultIssue("Indentation", 21, RuleKey.of("squid", "AvoidCycle"), null);

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4),
Arrays.asList(referenceIssue1, referenceIssue2),
source, project);
@@ -339,7 +338,7 @@ public class IssueTrackingTest {
DefaultIssue newIssue2 = newDefaultIssue("SystemPrintln", 10, RuleKey.of("squid", "AvoidCycle"), null);
DefaultIssue newIssue3 = newDefaultIssue("SystemPrintln", 14, RuleKey.of("squid", "AvoidCycle"), null);

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
Arrays.asList(newIssue1, newIssue2, newIssue3),
Arrays.asList(referenceIssue1),
source, project);
@@ -370,7 +369,7 @@ public class IssueTrackingTest {
// Same as referenceIssue1
DefaultIssue newIssue5 = newDefaultIssue("Avoid unused local variables such as 'j'.", 6, RuleKey.of("squid", "AvoidCycle"), "4432a2675ec3e1620daefe38386b51ef");

Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(
Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4, newIssue5),
Arrays.asList(referenceIssue1, referenceIssue2, referenceIssue3),
source, project);
@@ -390,7 +389,7 @@ public class IssueTrackingTest {
}

private DefaultIssue newDefaultIssue(String message, Integer line, RuleKey ruleKey, String checksum) {
return new DefaultIssue().setDescription(message).setLine(line).setRuleKey(ruleKey).setChecksum(checksum);
return new DefaultIssue().setDescription(message).setLine(line).setRuleKey(ruleKey).setChecksum(checksum).setResolution(Issue.RESOLUTION_OPEN).setStatus(Issue.STATUS_OPEN);
}

private IssueDto newReferenceIssue(String message, Integer lineId, int ruleId, String lineChecksum) {
@@ -402,6 +401,8 @@ public class IssueTrackingTest {
referenceIssue.setDescription(message);
referenceIssue.setRuleId(ruleId);
referenceIssue.setChecksum(lineChecksum);
referenceIssue.setResolution(Issue.RESOLUTION_OPEN);
referenceIssue.setStatus(Issue.STATUS_OPEN);
return referenceIssue;
}


+ 3
- 4
sonar-batch/src/main/java/org/sonar/batch/index/Cache.java View File

@@ -102,18 +102,17 @@ public class Cache<K, V extends Serializable> {
return get(DEFAULT_GROUP, key);
}

public Cache remove(String group, K key) {
public boolean remove(String group, K key) {
try {
exchange.clear();
exchange.append(group).append(key);
exchange.remove();
return this;
return exchange.remove();
} catch (Exception e) {
throw new IllegalStateException("Fail to get element from cache", e);
}
}

public Cache remove(K key) {
public boolean remove(K key) {
return remove(DEFAULT_GROUP, key);
}


+ 2
- 1
sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java View File

@@ -50,9 +50,10 @@ public class DefaultIssuable implements Issuable {
return scanIssues.initAndAddIssue(((DefaultIssue)issue));
}

@SuppressWarnings("unchecked")
@Override
public Collection<Issue> issues() {
return scanIssues.issues(component.key());
return (Collection)scanIssues.issues(component.key());
}

@Override

+ 3
- 3
sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java View File

@@ -38,9 +38,9 @@ public class DeprecatedViolations implements BatchComponent {
}

public void add(Violation violation) {
Issue issue = toIssue(violation);
DefaultIssue issue = toIssue(violation);
if (issue != null) {
cache.addOrUpdate(issue);
cache.put(issue);
}
}

@@ -48,7 +48,7 @@ public class DeprecatedViolations implements BatchComponent {
throw new UnsupportedOperationException("TODO");
}

Issue toIssue(Violation violation) {
DefaultIssue toIssue(Violation violation) {
DefaultIssue issue = new DefaultIssue()
.setComponentKey(violation.getResource().getEffectiveKey())
.setKey(UUID.randomUUID().toString())

+ 8
- 3
sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java View File

@@ -23,6 +23,7 @@ import org.sonar.api.BatchComponent;
import org.sonar.api.issue.Issue;
import org.sonar.batch.index.Cache;
import org.sonar.batch.index.Caches;
import org.sonar.core.issue.DefaultIssue;

import java.util.Collection;

@@ -32,13 +33,13 @@ import java.util.Collection;
public class IssueCache implements BatchComponent {

// component key -> issue key -> issue
private final Cache<String, Issue> cache;
private final Cache<String, DefaultIssue> cache;

public IssueCache(Caches caches) {
cache = caches.createCache("issues");
}

public Collection<Issue> componentIssues(String componentKey) {
public Collection<DefaultIssue> componentIssues(String componentKey) {
return cache.values(componentKey);
}

@@ -46,8 +47,12 @@ public class IssueCache implements BatchComponent {
return cache.get(componentKey, issueKey);
}

public IssueCache addOrUpdate(Issue issue) {
public IssueCache put(DefaultIssue issue) {
cache.put(issue.componentKey(), issue.key(), issue);
return this;
}

public boolean remove(Issue issue) {
return cache.remove(issue.componentKey(), issue.key());
}
}

+ 3
- 4
sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java View File

@@ -20,7 +20,6 @@
package org.sonar.batch.issue;

import org.sonar.api.database.model.Snapshot;
import org.sonar.api.issue.Issue;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RuleFinder;
import org.sonar.batch.index.ScanPersister;
@@ -56,15 +55,15 @@ public class IssuePersister implements ScanPersister {
for (Map.Entry<String, Snapshot> componentEntry : snapshotCache.snapshots()) {
String componentKey = componentEntry.getKey();
Snapshot snapshot = componentEntry.getValue();
Collection<Issue> issues = issueCache.componentIssues(componentKey);
Collection<DefaultIssue> issues = issueCache.componentIssues(componentKey);

for (Issue issue : issues) {
for (DefaultIssue issue : issues) {
Rule rule = ruleFinder.findByKey(issue.ruleKey().repository(), issue.ruleKey().rule());
if (rule == null) {
throw new IllegalStateException("Rule not found: " + issue.ruleKey());
}

IssueDto dto = IssueDto.toDto((DefaultIssue) issue, snapshot.getResourceId(), rule.getId());
IssueDto dto = IssueDto.toDto(issue, snapshot.getResourceId(), rule.getId());
if (issue.isNew()) {
dao.insert(dto);
} else {

+ 9
- 29
sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java View File

@@ -22,13 +22,10 @@ package org.sonar.batch.issue;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.issue.IssueChanges;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.rules.ActiveRule;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.workflow.IssueWorkflow;

import java.util.Collection;
import java.util.UUID;
@@ -36,46 +33,25 @@ import java.util.UUID;
/**
* Central component to manage issues
*/
public class ScanIssues implements IssueChanges {
public class ScanIssues {

private final RulesProfile qProfile;
private final IssueCache cache;
private final Project project;
private final IssueWorkflow workflow;

public ScanIssues(RulesProfile qProfile, IssueCache cache, Project project, IssueWorkflow workflow) {
public ScanIssues(RulesProfile qProfile, IssueCache cache, Project project) {
this.qProfile = qProfile;
this.cache = cache;
this.project = project;
this.workflow = workflow;
}

@Override
public Issue change(Issue issue, IssueChange change) {
if (!change.hasChanges()) {
return issue;
}
DefaultIssue reloaded = reload(issue);
workflow.change(reloaded, change);
cache.addOrUpdate(reloaded);
return reloaded;
}

private DefaultIssue reload(Issue issue) {
DefaultIssue reloaded = (DefaultIssue) cache.componentIssue(issue.componentKey(), issue.key());
if (reloaded == null) {
throw new IllegalStateException("Bad API usage. Unregistered issues can't be changed.");
}
return reloaded;
}

public Collection<Issue> issues(String componentKey) {
public Collection<DefaultIssue> issues(String componentKey) {
return cache.componentIssues(componentKey);
}

public ScanIssues addOrUpdate(DefaultIssue issue) {
Preconditions.checkState(!Strings.isNullOrEmpty(issue.key()), "Missing issue key");
cache.addOrUpdate(issue);
cache.put(issue);
return this;
}

@@ -92,7 +68,11 @@ public class ScanIssues implements IssueChanges {
if (issue.severity() == null) {
issue.setSeverity(activeRule.getSeverity().name());
}
cache.addOrUpdate(issue);
cache.put(issue);
return true;
}

public boolean remove(Issue issue) {
return cache.remove(issue);
}
}

+ 7
- 7
sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java View File

@@ -54,7 +54,7 @@ public class IssueCacheTest {
DefaultIssue issue1 = new DefaultIssue().setKey("111").setComponentKey("org.struts.Action");
DefaultIssue issue2 = new DefaultIssue().setKey("222").setComponentKey("org.struts.Action");
DefaultIssue issue3 = new DefaultIssue().setKey("333").setComponentKey("org.struts.Filter");
cache.addOrUpdate(issue1).addOrUpdate(issue2).addOrUpdate(issue3);
cache.put(issue1).put(issue2).put(issue3);

assertThat(issueKeys(cache.componentIssues("org.struts.Action"))).containsOnly("111", "222");
assertThat(issueKeys(cache.componentIssues("org.struts.Filter"))).containsOnly("333");
@@ -64,22 +64,22 @@ public class IssueCacheTest {
public void should_update_existing_issue() throws Exception {
IssueCache cache = new IssueCache(caches);
DefaultIssue issue = new DefaultIssue().setKey("111").setComponentKey("org.struts.Action").setSeverity(Severity.BLOCKER);
cache.addOrUpdate(issue);
cache.put(issue);

issue.setSeverity(Severity.MINOR);
cache.addOrUpdate(issue);
cache.put(issue);

Collection<Issue> issues = cache.componentIssues("org.struts.Action");
Collection<DefaultIssue> issues = cache.componentIssues("org.struts.Action");
assertThat(issues).hasSize(1);
Issue reloaded = issues.iterator().next();
assertThat(reloaded.key()).isEqualTo("111");
assertThat(reloaded.severity()).isEqualTo(Severity.MINOR);
}

Collection<String> issueKeys(Collection<Issue> issues) {
return Collections2.transform(issues, new Function<Issue, String>() {
Collection<String> issueKeys(Collection<DefaultIssue> issues) {
return Collections2.transform(issues, new Function<DefaultIssue, String>() {
@Override
public String apply(@Nullable Issue issue) {
public String apply(@Nullable DefaultIssue issue) {
return issue.key();
}
});

+ 3
- 3
sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java View File

@@ -62,7 +62,7 @@ public class IssuePersisterTest extends AbstractDaoTestCase {
snapshot.setResourceId(200);
snapshots.put("org/struts/Action.java", snapshot);

Issue issue = new DefaultIssue()
DefaultIssue issue = new DefaultIssue()
.setKey("ABCD")
.setComponentKey("org/struts/Action.java")
.setRuleKey(RuleKey.of("squid", "NullDef"))
@@ -90,7 +90,7 @@ public class IssuePersisterTest extends AbstractDaoTestCase {
snapshot.setResourceId(200);
snapshots.put("org/struts/Action.java", snapshot);

Issue issue = new DefaultIssue()
DefaultIssue issue = new DefaultIssue()
.setKey("ABCD")
.setComponentKey("org/struts/Action.java")
.setRuleKey(RuleKey.of("squid", "NullDef"))
@@ -118,7 +118,7 @@ public class IssuePersisterTest extends AbstractDaoTestCase {
snapshot.setResourceId(200);
snapshots.put("org/struts/Action.java", snapshot);

Issue issue = new DefaultIssue()
DefaultIssue issue = new DefaultIssue()
.setKey("ABCD")
.setComponentKey("org/struts/Action.java")
.setRuleKey(RuleKey.of("squid", "NullDef"))

+ 5
- 43
sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java View File

@@ -22,7 +22,6 @@ package org.sonar.batch.issue;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.resources.Project;
import org.sonar.api.rule.RuleKey;
@@ -31,21 +30,18 @@ import org.sonar.api.rules.ActiveRule;
import org.sonar.api.rules.Rule;
import org.sonar.api.rules.RulePriority;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.workflow.IssueWorkflow;

import java.util.Date;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.mockito.Mockito.*;

public class ScanIssuesTest {

IssueCache cache = mock(IssueCache.class);
IssueWorkflow workflow = mock(IssueWorkflow.class);
RulesProfile qProfile = mock(RulesProfile.class);
Project project = mock(Project.class);
ScanIssues scanIssues = new ScanIssues(qProfile, cache, project, workflow);
ScanIssues scanIssues = new ScanIssues(qProfile, cache, project);

@Test
public void should_get_issues() throws Exception {
@@ -92,8 +88,8 @@ public class ScanIssuesTest {
boolean added = scanIssues.initAndAddIssue(issue);

assertThat(added).isTrue();
ArgumentCaptor<Issue> argument = ArgumentCaptor.forClass(Issue.class);
verify(cache).addOrUpdate(argument.capture());
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(cache).put(argument.capture());
assertThat(argument.getValue().key()).isNotNull();
assertThat(argument.getValue().severity()).isEqualTo(Severity.CRITICAL);
assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate);
@@ -113,44 +109,10 @@ public class ScanIssuesTest {
DefaultIssue issue = new DefaultIssue().setRuleKey(RuleKey.of("squid", "AvoidCycle")).setSeverity(null);
scanIssues.initAndAddIssue(issue);

ArgumentCaptor<Issue> argument = ArgumentCaptor.forClass(Issue.class);
verify(cache).addOrUpdate(argument.capture());
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
verify(cache).put(argument.capture());
assertThat(argument.getValue().key()).isNotNull();
assertThat(argument.getValue().severity()).isEqualTo(Severity.INFO);
assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate);
}

@Test
public void should_ignore_empty_change() throws Exception {
Issue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(issue);
Issue changed = scanIssues.change(issue, IssueChange.create());
verifyZeroInteractions(cache);
assertThat(changed).isSameAs(issue);
assertThat(changed.updatedAt()).isNull();
}

@Test
public void unknown_issue_is_a_bad_api_usage() throws Exception {
Issue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(null);
try {
scanIssues.change(issue, IssueChange.create().setLine(200));
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Bad API usage. Unregistered issues can't be changed.");
}
}

@Test
public void should_change_fields() throws Exception {
DefaultIssue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(issue);

IssueChange change = IssueChange.create().setTransition("resolve");
scanIssues.change(issue, change);

verify(cache).addOrUpdate(issue);
verify(workflow).change(issue, change);
}
}

+ 16
- 7
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java View File

@@ -20,6 +20,7 @@
package org.sonar.core.issue;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
@@ -31,7 +32,6 @@ import org.sonar.api.rule.RuleKey;
import org.sonar.api.rule.Severity;

import javax.annotation.Nullable;

import java.io.Serializable;
import java.util.Collections;
import java.util.Date;
@@ -40,8 +40,6 @@ import java.util.Set;

public class DefaultIssue implements Issue, Serializable {

private static final Set<String> RESOLUTIONS = ImmutableSet.of(RESOLUTION_OPEN, RESOLUTION_FALSE_POSITIVE, RESOLUTION_FIXED);
private static final Set<String> STATUSES = ImmutableSet.of(STATUS_OPEN, STATUS_CLOSED, STATUS_REOPENED, STATUS_RESOLVED);
private String key;
private String componentKey;
private RuleKey ruleKey;
@@ -60,7 +58,9 @@ public class DefaultIssue implements Issue, Serializable {
private boolean manual = false;
private String checksum;
private boolean isNew = true;
private boolean isAlive = true;
private Map<String, String> attributes = null;

private String authorLogin;

public String key() {
@@ -142,8 +142,8 @@ public class DefaultIssue implements Issue, Serializable {
return status;
}

public DefaultIssue setStatus(@Nullable String s) {
Preconditions.checkArgument(s == null || STATUSES.contains(s), "Not a valid status: " + s);
public DefaultIssue setStatus(String s) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set");
this.status = s;
return this;
}
@@ -152,8 +152,8 @@ public class DefaultIssue implements Issue, Serializable {
return resolution;
}

public DefaultIssue setResolution(@Nullable String s) {
Preconditions.checkArgument(s == null || RESOLUTIONS.contains(s), "Not a valid resolution: " + s);
public DefaultIssue setResolution(String s) {
Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Resolution must be set");
this.resolution = s;
return this;
}
@@ -230,6 +230,15 @@ public class DefaultIssue implements Issue, Serializable {
return this;
}

public boolean isAlive() {
return isAlive;
}

public DefaultIssue setAlive(boolean b) {
isAlive = b;
return this;
}

public String attribute(String key) {
return attributes == null ? null : attributes.get(key);
}

+ 1
- 0
sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java View File

@@ -102,6 +102,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder {
issue.setManual(manual);
issue.setAttributes(attributes);
issue.setNew(true);
issue.setAlive(true);
issue.setResolution(Issue.RESOLUTION_OPEN);
issue.setStatus(Issue.STATUS_OPEN);
return issue;

+ 1
- 0
sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java View File

@@ -95,6 +95,7 @@ public class IssueDao implements BatchComponent, ServerComponent {
}
}

// TODO rename selectOpenIssuesByProject. Is it by module or project ??
public List<IssueDto> selectOpenIssues(Integer componentId) {
SqlSession session = mybatis.openSession();
try {

+ 2
- 2
sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java View File

@@ -210,8 +210,8 @@ public final class IssueDto {
}

public IssueDto setAttributes(@Nullable String s) {
Preconditions.checkArgument(s == null || s.length() <= 1000,
"Issue attributes must not exceed 1000 characters: " + s);
Preconditions.checkArgument(s == null || s.length() <= 4000,
"Issue attributes must not exceed 4000 characters: " + s);
this.attributes = s;
return this;
}

+ 39
- 0
sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java View File

@@ -0,0 +1,39 @@
/*
* 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.core.issue.workflow;

import com.google.common.collect.ImmutableSet;
import org.sonar.api.issue.Issue;

import java.util.Set;

public class HasResolution implements Condition {

private final Set<String> resolutions;

public HasResolution(String first, String... others) {
this.resolutions = ImmutableSet.<String>builder().add(first).add(others).build();
}

@Override
public boolean matches(Issue issue) {
return issue.resolution() != null && resolutions.contains(issue.resolution());
}
}

sonar-plugin-api/src/main/java/org/sonar/api/issue/NewIssueHandler.java → sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java View File

@@ -17,20 +17,21 @@
* 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.issue;
package org.sonar.core.issue.workflow;

import org.sonar.api.BatchExtension;
import org.sonar.api.issue.Issue;
import org.sonar.core.issue.DefaultIssue;

/**
* Observe issues considered as new during project scan. Note that it does not observe manual
* issues created by end-users
*/
public interface NewIssueHandler extends BatchExtension {
class IsAlive implements Condition {

interface NewIssueEvent {
Issue issue();
}
private final boolean alive;

void onNewIssue(NewIssueEvent event);
IsAlive(boolean alive) {
this.alive = alive;
}

@Override
public boolean matches(Issue issue) {
return ((DefaultIssue) issue).isAlive() == alive;
}
}

sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java → sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java View File

@@ -21,10 +21,16 @@ package org.sonar.core.issue.workflow;

import org.sonar.api.issue.Issue;

class IsNotManualIssue implements Condition {
class IsManual implements Condition {

private final boolean manual;

IsManual(boolean manual) {
this.manual = manual;
}

@Override
public boolean matches(Issue issue) {
return !issue.manual();
return issue.manual()==manual;
}
}

+ 42
- 45
sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java View File

@@ -24,12 +24,9 @@ import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;
import org.sonar.api.issue.DefaultTransitions;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.core.issue.DefaultIssue;

import java.util.Date;
import java.util.List;
import java.util.Map;

public class IssueWorkflow implements BatchComponent, ServerComponent, Startable {

@@ -41,16 +38,15 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable
.states(Issue.STATUS_OPEN, Issue.STATUS_REOPENED, Issue.STATUS_RESOLVED, Issue.STATUS_CLOSED)
.transition(Transition.builder(DefaultTransitions.CLOSE)
.from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED)
.functions(new SetResolution(Issue.RESOLUTION_FIXED))
// TODO set closed at
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT)
.build())
.transition(Transition.builder(DefaultTransitions.CLOSE)
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED)
// TODO set closed at
.functions(SetClosedAt.CLOSED_AT)
.build())
.transition(Transition.builder(DefaultTransitions.CLOSE)
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED)
// TODO set closed at
.functions(SetClosedAt.CLOSED_AT)
.functions(new SetResolution(Issue.RESOLUTION_FIXED))
.build())
.transition(Transition.builder(DefaultTransitions.RESOLVE)
@@ -67,18 +63,48 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable
.build())
.transition(Transition.builder(DefaultTransitions.REOPEN)
.from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED)
.functions(new SetResolution(Issue.RESOLUTION_OPEN))
.functions(new SetResolution(Issue.RESOLUTION_OPEN))// TODO new UnsetClosedAt
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED)
.conditions(new IsNotManualIssue())
.conditions(new IsManual(false))
.functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE))
.build())
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE)
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED)
.conditions(new IsNotManualIssue())
.conditions(new IsManual(false))
.functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE))
.build())

// automatic transitions

// Close the issues that do not exist anymore. Note that isAlive() is true on manual issues
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED)
.conditions(new IsAlive(false), new IsManual(false))
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT)
.automatic()
.build())
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED)
.conditions(new IsAlive(false))
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT)
.automatic()
.build())
// Close the issues marked as resolved and that do not exist anymore.
// Note that false-positives are kept resolved and are not closed.
.transition(Transition.builder("automaticclose")
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED)
.conditions(new IsAlive(false))
.functions(SetClosedAt.CLOSED_AT)
.automatic()
.build())
.transition(Transition.builder("automaticreopen")
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED)
.conditions(new IsAlive(true), new HasResolution(Issue.RESOLUTION_FIXED))
.functions(new SetResolution(Issue.RESOLUTION_OPEN))
.automatic()
.build())
.build();
}

@@ -86,46 +112,17 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable
public void stop() {
}

public List<Transition> availableTransitions(Issue issue) {
return machine.state(issue.status()).outTransitions(issue);
public List<Transition> outManualTransitions(Issue issue) {
return machine.state(issue.status()).outManualTransitions(issue);
}

public boolean change(DefaultIssue issue, IssueChange change) {
if (change.hasChanges()) {
if (change.description() != null) {
issue.setDescription(change.description());
}
if (change.manualSeverity() != null) {
change.setManualSeverity(change.manualSeverity());
}
if (change.severity() != null) {
issue.setSeverity(change.severity());
}
if (change.isAssigneeChanged()) {
issue.setAssignee(change.assignee());
}
if (change.isLineChanged()) {
issue.setLine(change.line());
}
if (change.isCostChanged()) {
issue.setCost(change.cost());
}
for (Map.Entry<String, String> entry : change.attributes().entrySet()) {
issue.setAttribute(entry.getKey(), entry.getValue());
}
if (change.transition() != null) {
move(issue, change.transition());
}
issue.setUpdatedAt(new Date());
return true;
public void doAutomaticTransition(DefaultIssue issue) {
Transition transition = machine.state(issue.status()).outAutomaticTransition(issue);
if (transition != null) {
transition.execute(issue);
}
return false;
}

private void move(DefaultIssue issue, String transition) {
State state = machine.state(issue.status());
state.move(issue, transition);
}

StateMachine machine() {
return machine;

+ 36
- 0
sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java View File

@@ -0,0 +1,36 @@
/*
* 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.core.issue.workflow;

import org.sonar.api.issue.Issue;

class NotCondition implements Condition {
private final Condition condition;

NotCondition(Condition condition) {
this.condition = condition;
}

@Override
public boolean matches(Issue issue) {
return !condition.matches(issue);
}

}

+ 36
- 0
sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java View File

@@ -0,0 +1,36 @@
/*
* 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.core.issue.workflow;

import org.sonar.core.issue.DefaultIssue;

import java.util.Date;

class SetClosedAt implements Function {
static final SetClosedAt CLOSED_AT = new SetClosedAt();

private SetClosedAt() {
}

@Override
public void execute(DefaultIssue issue) {
issue.setClosedAt(new Date());
}
}

+ 19
- 3
sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java View File

@@ -27,6 +27,7 @@ import org.apache.commons.lang.StringUtils;
import org.sonar.api.issue.Issue;
import org.sonar.core.issue.DefaultIssue;

import javax.annotation.CheckForNull;
import java.util.List;
import java.util.Set;

@@ -54,17 +55,32 @@ public class State {
}
}

public List<Transition> outTransitions(Issue issue) {
public List<Transition> outManualTransitions(Issue issue) {
List<Transition> result = Lists.newArrayList();
for (Transition transition : outTransitions) {
if (transition.supports(issue)) {
if (!transition.automatic() && transition.supports(issue)) {
result.add(transition);
}
}
return result;
}

public void move(DefaultIssue issue, String transitionKey) {
@CheckForNull
public Transition outAutomaticTransition(Issue issue) {
Transition result = null;
for (Transition transition : outTransitions) {
if (transition.automatic() && transition.supports(issue)) {
if (result == null) {
result = transition;
} else {
throw new IllegalStateException("Several automatic transitions are available for issue: " + issue);
}
}
}
return result;
}

public void doTransition(DefaultIssue issue, String transitionKey) {
Transition transition = transition(transitionKey);
if (!transition.supports(issue)) {
throw new IllegalStateException("TODO");

+ 12
- 0
sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java View File

@@ -35,6 +35,7 @@ class Transition {
private final String from, to;
private final Condition[] conditions;
private final Function[] functions;
private final boolean automatic;

private Transition(TransitionBuilder builder) {
key = builder.key;
@@ -42,6 +43,7 @@ class Transition {
to = builder.to;
conditions = builder.conditions.toArray(new Condition[builder.conditions.size()]);
functions = builder.functions.toArray(new Function[builder.functions.size()]);
automatic = builder.automatic;
}

String key() {
@@ -64,6 +66,10 @@ class Transition {
return functions;
}

boolean automatic() {
return automatic;
}

public boolean supports(Issue issue) {
for (Condition condition : conditions) {
if (!condition.matches(issue)) {
@@ -90,6 +96,7 @@ class Transition {
private String from, to;
private List<Condition> conditions = Lists.newArrayList();
private List<Function> functions = Lists.newArrayList();
private boolean automatic = false;

private TransitionBuilder(String key) {
this.key = key;
@@ -115,6 +122,11 @@ class Transition {
return this;
}

public TransitionBuilder automatic() {
this.automatic = true;
return this;
}

public Transition build() {
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Transition key must be set");
Preconditions.checkArgument(StringUtils.isAllLowerCase(key), "Transition key must be lower-case");

+ 6
- 6
sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java View File

@@ -41,22 +41,22 @@ public class DefaultIssueTest {
}

@Test
public void should_fail_on_bad_status() {
public void should_fail_on_empty_status() {
try {
issue.setStatus("FOO");
issue.setStatus("");
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Not a valid status: FOO");
assertThat(e).hasMessage("Status must be set");
}
}

@Test
public void should_fail_on_bad_resolution() {
public void should_fail_on_empty_resolution() {
try {
issue.setResolution("FOO");
issue.setResolution("");
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Not a valid resolution: FOO");
assertThat(e).hasMessage("Resolution must be set");
}
}


+ 1
- 1
sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java View File

@@ -37,7 +37,7 @@ public class IssueDtoTest {
@Test
public void set_data_check_maximal_length() {
thrown.expect(IllegalArgumentException.class);
thrown.expectMessage("Issue attributes must not exceed 1000 characters: ");
thrown.expectMessage("Issue attributes must not exceed 4000 characters: ");

StringBuilder s = new StringBuilder(4500);
for (int i = 0; i < 4500; i++) {

+ 0
- 78
sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java View File

@@ -1,78 +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.core.issue;

import org.junit.Test;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.rule.Severity;

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

public class UpdateIssueFieldsTest {

@Test
public void should_change_fields() throws Exception {
DefaultIssue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE");
UpdateIssueFields.apply(issue, IssueChange.create()
.setLine(200)
.setAttribute("JIRA", "FOO-123")
.setManualSeverity(true)
.setSeverity(Severity.CRITICAL)
.setAssignee("arthur")
.setTitle("new title")
.setDescription("new desc")
.setCost(4.2)
);
assertThat(issue.line()).isEqualTo(200);
assertThat(issue.description()).isEqualTo("new desc");
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-123");
assertThat(issue.severity()).isEqualTo(Severity.CRITICAL);
assertThat(issue.assignee()).isEqualTo("arthur");
assertThat(issue.cost()).isEqualTo(4.2);
}

@Test
public void should_not_touch_fields() throws Exception {
DefaultIssue issue = new DefaultIssue()
.setComponentKey("org/struts/Action.java")
.setKey("ABCDE")
.setLine(123)
.setDescription("the desc")
.setAssignee("karadoc")
.setCost(4.2)
.setAttribute("JIRA", "FOO-123")
.setManualSeverity(true)
.setSeverity("BLOCKER")
.setStatus("CLOSED")
.setResolution("FIXED");
UpdateIssueFields.apply(issue, IssueChange.create());

assertThat(issue.componentKey()).isEqualTo("org/struts/Action.java");
assertThat(issue.key()).isEqualTo("ABCDE");
assertThat(issue.line()).isEqualTo(123);
assertThat(issue.resolution()).isEqualTo("FIXED");
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-123");
assertThat(issue.severity()).isEqualTo("BLOCKER");
assertThat(issue.assignee()).isEqualTo("karadoc");
assertThat(issue.cost()).isEqualTo(4.2);
assertThat(issue.isManualSeverity()).isTrue();
assertThat(issue.description()).isEqualTo("the desc");
}
}

+ 42
- 0
sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java View File

@@ -0,0 +1,42 @@
/*
* 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.core.issue.workflow;

import org.junit.Test;
import org.sonar.core.issue.DefaultIssue;

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

public class HasResolutionTest {

DefaultIssue issue = new DefaultIssue();

@Test
public void should_match() throws Exception {
HasResolution condition = new HasResolution("OPEN", "FIXED", "FALSE-POSITIVE");

assertThat(condition.matches(issue.setResolution("OPEN"))).isTrue();
assertThat(condition.matches(issue.setResolution("FIXED"))).isTrue();
assertThat(condition.matches(issue.setResolution("FALSE-POSITIVE"))).isTrue();

assertThat(condition.matches(issue.setResolution("open"))).isFalse();
assertThat(condition.matches(issue.setResolution("Fixed"))).isFalse();
}
}

+ 43
- 0
sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java View File

@@ -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.core.issue.workflow;

import org.junit.Test;
import org.sonar.core.issue.DefaultIssue;

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

public class IsAliveTest {
DefaultIssue issue = new DefaultIssue();

@Test
public void should_match_alive() throws Exception {
IsAlive condition = new IsAlive(true);
assertThat(condition.matches(issue.setAlive(true))).isTrue();
assertThat(condition.matches(issue.setAlive(false))).isFalse();
}

@Test
public void should_match_dead() throws Exception {
IsAlive condition = new IsAlive(false);
assertThat(condition.matches(issue.setAlive(true))).isFalse();
assertThat(condition.matches(issue.setAlive(false))).isTrue();
}
}

+ 43
- 0
sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java View File

@@ -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.core.issue.workflow;

import org.junit.Test;
import org.sonar.core.issue.DefaultIssue;

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

public class IsManualTest {
DefaultIssue issue = new DefaultIssue();

@Test
public void should_match() throws Exception {
IsManual condition = new IsManual(true);
assertThat(condition.matches(issue.setManual(true))).isTrue();
assertThat(condition.matches(issue.setManual(false))).isFalse();
}

@Test
public void should_match_dead() throws Exception {
IsManual condition = new IsManual(false);
assertThat(condition.matches(issue.setManual(true))).isFalse();
assertThat(condition.matches(issue.setManual(false))).isTrue();
}
}

+ 10
- 65
sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java View File

@@ -23,7 +23,6 @@ import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.core.issue.DefaultIssue;

import javax.annotation.Nullable;
@@ -48,85 +47,31 @@ public class IssueWorkflowTest {
}

@Test
public void should_list_available_transitions() throws Exception {
public void should_list_out_manual_transitions() throws Exception {
workflow.start();

DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN);
List<Transition> transitions = workflow.availableTransitions(issue);
List<Transition> transitions = workflow.outManualTransitions(issue);
assertThat(transitions).hasSize(3);
assertThat(keys(transitions)).containsOnly("close", "falsepositive", "resolve");
}

@Test
public void should_not_change_anything() throws Exception {
workflow.start();

DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN);
workflow.change(issue, IssueChange.create());

assertThat(issue.updatedAt()).isNull();
}

@Test
public void should_set_fields() throws Exception {
workflow.start();

DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN);
IssueChange change = IssueChange.create()
.setAssignee("arthur")
.setAttribute("JIRA", "FOO-1234")
.setCost(4.2)
.setLine(123)
.setDescription("the desc")
.setSeverity("BLOCKER");
workflow.change(issue, change);

assertThat(issue.updatedAt()).isNotNull();
assertThat(issue.assignee()).isEqualTo("arthur");
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-1234");
assertThat(issue.cost()).isEqualTo(4.2);
assertThat(issue.line()).isEqualTo(123);
assertThat(issue.description()).isEqualTo("the desc");
assertThat(issue.severity()).isEqualTo("BLOCKER");
}

@Test
public void should_change_only_fields_with_new_values() throws Exception {
public void should_do_automatic_transition() throws Exception {
workflow.start();

DefaultIssue issue = new DefaultIssue()
.setStatus(Issue.STATUS_OPEN)
.setAssignee("karadoc")
.setAttribute("YOUTRACK", "ABC")
.setCost(3.4);
IssueChange change = IssueChange.create()
.setAttribute("JIRA", "FOO-1234")
.setLine(123)
.setSeverity("BLOCKER");
workflow.change(issue, change);

assertThat(issue.updatedAt()).isNotNull();
assertThat(issue.assignee()).isEqualTo("karadoc");
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-1234");
assertThat(issue.attribute("YOUTRACK")).isEqualTo("ABC");
assertThat(issue.cost()).isEqualTo(3.4);
assertThat(issue.line()).isEqualTo(123);
assertThat(issue.severity()).isEqualTo("BLOCKER");
}

@Test
public void should_change_issue_state() throws Exception {
workflow.start();

DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN);
IssueChange change = IssueChange.create().setTransition("close");
workflow.change(issue, change);

assertThat(issue.updatedAt()).isNotNull();
.setResolution(Issue.RESOLUTION_FIXED)
.setStatus(Issue.STATUS_RESOLVED)
.setNew(false)
.setAlive(false);
workflow.doAutomaticTransition(issue);
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED);
assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED);
assertThat(issue.closedAt()).isNotNull();
}


private Collection<String> keys(List<Transition> transitions) {
return Collections2.transform(transitions, new Function<Transition, String>() {
@Override

+ 45
- 0
sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java View File

@@ -0,0 +1,45 @@
/*
* 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.core.issue.workflow;

import org.junit.Test;
import org.sonar.api.issue.Issue;
import org.sonar.core.issue.DefaultIssue;

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

public class NotConditionTest {

Condition target = mock(Condition.class);

@Test
public void should_match_opposite() throws Exception {
NotCondition condition = new NotCondition(target);

when(target.matches(any(Issue.class))).thenReturn(true);
assertThat(condition.matches(new DefaultIssue())).isFalse();

when(target.matches(any(Issue.class))).thenReturn(false);
assertThat(condition.matches(new DefaultIssue())).isTrue();
}
}

+ 0
- 4
sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java View File

@@ -79,8 +79,4 @@ public interface Issue extends Serializable {

String authorLogin();

/**
* Used only during project scan.
*/
boolean isNew();
}

+ 0
- 175
sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java View File

@@ -1,175 +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.api.issue;

import javax.annotation.Nullable;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

/**
* @since 3.6
*/
public class IssueChange {
private String severity = null;
private String comment = null;
private String login = null;
private Boolean manualSeverity = null;
private String description = null;
private boolean lineChanged = false;
private Integer line = null;
private boolean costChanged = false;
private Double cost = null;
private String transition = null;
private boolean assigneeChanged = false;
private String assignee = null;
private String title = null;
private Map<String, String> attributes = null;

private IssueChange() {
}

public static IssueChange create() {
return new IssueChange();
}

public boolean hasChanges() {
return severity != null || comment != null || manualSeverity != null || description != null ||
lineChanged || costChanged || transition != null || assigneeChanged || attributes != null;
}

public IssueChange setSeverity(String s) {
this.severity = s;
return this;
}

public IssueChange setComment(String comment) {
this.comment = comment;
return this;
}

public IssueChange setLogin(String s) {
this.login = s;
return this;
}

public IssueChange setManualSeverity(boolean b) {
this.manualSeverity = b;
return this;
}

public IssueChange setTitle(String s) {
this.title = s;
return this;
}

public IssueChange setDescription(String s) {
this.description = s;
return this;
}

public IssueChange setLine(@Nullable Integer line) {
this.lineChanged = true;
this.line = line;
return this;
}

public IssueChange setCost(@Nullable Double cost) {
this.costChanged = true;
this.cost = cost;
return this;
}

public IssueChange setTransition(String transition) {
this.transition = transition;
return this;
}

public IssueChange setAssignee(@Nullable String assigneeLogin) {
this.assigneeChanged = true;
this.assignee = assigneeLogin;
return this;
}

public IssueChange setAttribute(String key, @Nullable String value) {
if (attributes == null) {
attributes = new LinkedHashMap<String, String>();
}
attributes.put(key, value);
return this;
}

public String severity() {
return severity;
}

public String comment() {
return comment;
}

public String login() {
return login;
}

public Boolean manualSeverity() {
return manualSeverity;
}

public String description() {
return description;
}

public String title() {
return title;
}

public Integer line() {
return line;
}

public boolean isLineChanged() {
return lineChanged;
}

public Double cost() {
return cost;
}

public boolean isCostChanged() {
return costChanged;
}

public String transition() {
return transition;
}

public String assignee() {
return assignee;
}

public boolean isAssigneeChanged() {
return assigneeChanged;
}

public Map<String, String> attributes() {
return attributes == null ? Collections.<String, String>emptyMap() : new LinkedHashMap<String, String>(attributes);
}
}

sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChanges.java → sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueFilter.java View File

@@ -19,17 +19,13 @@
*/
package org.sonar.api.issue;

import org.sonar.api.BatchComponent;
import org.sonar.api.ServerComponent;

import javax.annotation.Nullable;
import org.sonar.api.BatchExtension;

/**
* Change existing issues
* @since 3.6
*/
public interface IssueChanges extends BatchComponent {
public interface IssueFilter extends BatchExtension {

Issue change(Issue issue, IssueChange change);
boolean accept(Issue issue);

}

+ 58
- 0
sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java View File

@@ -0,0 +1,58 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2013 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.api.issue;

import org.sonar.api.BatchExtension;

import javax.annotation.Nullable;

/**
* @since 3.6
*/
public interface IssueHandler extends BatchExtension {

interface IssueContext {
Issue issue();

boolean isNew();

boolean isAlive();

IssueContext setLine(@Nullable Integer line);

IssueContext setDescription(String description);

// set manual severity ?
IssueContext setSeverity(String severity);

// TODO rename to setScmLogin ?
IssueContext setAuthorLogin(@Nullable String login);

IssueContext setAttribute(String key, @Nullable String value);

IssueContext assignTo(@Nullable String login);

//TODO IssueContext comment(String comment);

}

void onIssue(IssueContext context);

}

+ 4
- 3
sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java View File

@@ -21,13 +21,14 @@
package org.sonar.api.issue;

/**
* TODO move outside this package
* @since 3.6
*/
public class Paging {

private int pageSize;
private int pageIndex;
private int total;
private final int pageSize;
private final int pageIndex;
private final int total;

public Paging(int pageSize, int pageIndex, int total) {
this.pageSize = pageSize;

+ 0
- 160
sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java View File

@@ -1,160 +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.api.issue;

import org.junit.Test;
import org.sonar.api.rule.Severity;

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

public class IssueChangeTest {
@Test
public void should_not_have_changes_by_default() throws Exception {
IssueChange change = IssueChange.create();
assertThat(change.hasChanges()).isFalse();
assertThat(change.severity()).isNull();
assertThat(change.isCostChanged()).isFalse();
assertThat(change.cost()).isNull();
assertThat(change.isAssigneeChanged()).isFalse();
assertThat(change.assignee()).isNull();
assertThat(change.isLineChanged()).isFalse();
assertThat(change.line()).isNull();
assertThat(change.comment()).isNull();
assertThat(change.description()).isNull();
assertThat(change.transition()).isNull();
assertThat(change.manualSeverity()).isNull();
assertThat(change.attributes()).isEmpty();
}


@Test
public void should_change_line() {
IssueChange change = IssueChange.create();
change.setLine(123);
assertThat(change.isLineChanged()).isTrue();
assertThat(change.line()).isEqualTo(123);
}

@Test
public void should_reset_line() {
IssueChange change = IssueChange.create();
assertThat(change.isLineChanged()).isFalse();
assertThat(change.hasChanges()).isFalse();
change.setLine(null);
assertThat(change.isLineChanged()).isTrue();
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_change_cost() {
IssueChange change = IssueChange.create();
change.setCost(500.0);
assertThat(change.isCostChanged()).isTrue();
assertThat(change.cost()).isEqualTo(500.0);
}

@Test
public void should_reset_cost() {
IssueChange change = IssueChange.create();
assertThat(change.isCostChanged()).isFalse();
assertThat(change.hasChanges()).isFalse();
change.setCost(null);
assertThat(change.isCostChanged()).isTrue();
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_change_assignne() {
IssueChange change = IssueChange.create();
change.setAssignee("karadoc");
assertThat(change.isAssigneeChanged()).isTrue();
assertThat(change.assignee()).isEqualTo("karadoc");
}

@Test
public void should_reset_assignee() {
IssueChange change = IssueChange.create();
assertThat(change.isAssigneeChanged()).isFalse();
assertThat(change.hasChanges()).isFalse();
change.setAssignee(null);
assertThat(change.isAssigneeChanged()).isTrue();
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_change_message() {
IssueChange change = IssueChange.create();
change.setDescription("foo");
assertThat(change.description()).isEqualTo("foo");
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_add_comment() {
IssueChange change = IssueChange.create();
change.setComment("foo").setLogin("perceval");
assertThat(change.comment()).isEqualTo("foo");
assertThat(change.login()).isEqualTo("perceval");
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_change_resolution() {
IssueChange change = IssueChange.create();
change.setTransition("resolve");
assertThat(change.transition()).isEqualTo("resolve");
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_change_severity() {
IssueChange change = IssueChange.create();
change.setSeverity(Severity.INFO);
assertThat(change.severity()).isEqualTo(Severity.INFO);
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_set_manual_severity() {
IssueChange change = IssueChange.create();
change.setManualSeverity(false);
assertThat(change.manualSeverity()).isFalse();
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_set_attribute() {
IssueChange change = IssueChange.create();
change.setAttribute("JIRA", "FOO-1234");
assertThat(change.attributes()).isNotEmpty();
assertThat(change.attributes().get("JIRA")).isEqualTo("FOO-1234");
assertThat(change.hasChanges()).isTrue();
}

@Test
public void should_unset_attribute() {
IssueChange change = IssueChange.create();
change.setAttribute("JIRA", null);
assertThat(change.attributes()).hasSize(1);
assertThat(change.attributes().get("JIRA")).isNull();
assertThat(change.attributes().containsKey("JIRA")).isTrue();
assertThat(change.hasChanges()).isTrue();
}
}

+ 10
- 5
sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java View File

@@ -39,11 +39,16 @@ public class PagingTest {
}

@Test
public void test_pagination_on_second_page(){
Paging paging = new Paging(5, 2, 20);

assertThat(paging.offset()).isEqualTo(5);
assertThat(paging.pages()).isEqualTo(4);
public void test_offset(){
assertThat(new Paging(5, 1, 20).offset()).isEqualTo(0);
assertThat(new Paging(5, 2, 20).offset()).isEqualTo(5);
}

@Test
public void test_number_of_pages(){
assertThat(new Paging(5, 2, 20).pages()).isEqualTo(4);
assertThat(new Paging(5, 2, 21).pages()).isEqualTo(5);
assertThat(new Paging(5, 2, 25).pages()).isEqualTo(5);
assertThat(new Paging(5, 2, 26).pages()).isEqualTo(6);
}
}

+ 2
- 40
sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java View File

@@ -24,7 +24,6 @@ import com.google.common.base.Splitter;
import com.google.common.collect.Collections2;
import com.google.common.collect.Lists;
import com.google.common.primitives.Ints;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.issue.IssueFinder;
import org.sonar.api.issue.IssueQuery;
import org.sonar.api.issue.JRubyIssues;
@@ -34,7 +33,6 @@ import org.sonar.api.web.UserRole;
import org.sonar.server.ui.JRubyFacades;

import javax.annotation.Nullable;

import java.util.Collection;
import java.util.Date;
import java.util.List;
@@ -48,9 +46,9 @@ import java.util.Map;
public class DefaultJRubyIssues implements JRubyIssues {

private final IssueFinder finder;
private final ServerIssueChanges changes;
private final ServerIssueActions changes;

public DefaultJRubyIssues(IssueFinder f, ServerIssueChanges changes) {
public DefaultJRubyIssues(IssueFinder f, ServerIssueActions changes) {
this.finder = f;
this.changes = changes;
JRubyFacades.setIssues(this);
@@ -65,11 +63,6 @@ public class DefaultJRubyIssues implements JRubyIssues {
return finder.find(toQuery(params), currentUserId, UserRole.CODEVIEWER);
}

public void change(Map<String, Object> params, @Nullable Integer currentUserId) {
String issueKey = (String) params.get("key");
changes.change(issueKey, toChange(params), currentUserId);
}

IssueQuery toQuery(Map<String, Object> props) {
IssueQuery.Builder builder = IssueQuery.builder();
builder.keys(toStrings(props.get("keys")));
@@ -88,37 +81,6 @@ public class DefaultJRubyIssues implements JRubyIssues {
return builder.build();
}

IssueChange toChange(Map<String, Object> props) {
IssueChange change = IssueChange.create();
if (props.containsKey("newSeverity")) {
change.setSeverity((String) props.get("newSeverity"));
change.setManualSeverity(true);
}
if (props.containsKey("newDesc")) {
change.setDescription((String) props.get("newDesc"));
}
if (props.containsKey("newCost")) {
change.setCost(toDouble(props.get("newCost")));
}
if (props.containsKey("newLine")) {
change.setLine(toInteger(props.get("newLine")));
}
if (props.containsKey("newAssignee")) {
change.setAssignee((String) props.get("newAssignee"));
}
if (props.containsKey("transition")) {
change.setTransition((String) props.get("transition"));
}
if (props.containsKey("newTitle")) {
change.setTitle((String) props.get("newTitle"));
}
if (props.containsKey("comment")) {
change.setComment((String) props.get("comment"));
}
// TODO set attribute and login
return change;
}

@SuppressWarnings("unchecked")
static Collection<RuleKey> toRules(Object o) {
Collection<RuleKey> result = null;

sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java → sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java View File

@@ -21,9 +21,7 @@ package org.sonar.server.issue;

import org.sonar.api.ServerComponent;
import org.sonar.api.issue.Issue;
import org.sonar.api.issue.IssueChange;
import org.sonar.api.web.UserRole;
import org.sonar.core.issue.UpdateIssueFields;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.IssueDao;
import org.sonar.core.issue.IssueDto;
@@ -31,24 +29,23 @@ import org.sonar.core.issue.workflow.IssueWorkflow;
import org.sonar.core.user.AuthorizationDao;

import javax.annotation.Nullable;
import java.util.Arrays;

/**
* @since 3.6
*/
public class ServerIssueChanges implements ServerComponent {
public class ServerIssueActions implements ServerComponent {

private final IssueWorkflow workflow;
private final IssueDao issueDao;
private final AuthorizationDao authorizationDao;

public ServerIssueChanges(IssueWorkflow workflow, IssueDao issueDao, AuthorizationDao authorizationDao) {
public ServerIssueActions(IssueWorkflow workflow, IssueDao issueDao, AuthorizationDao authorizationDao) {
this.workflow = workflow;
this.issueDao = issueDao;
this.authorizationDao = authorizationDao;
}

public Issue change(String issueKey, IssueChange change, @Nullable Integer userId) {
public Issue executeAction(String issueKey, String action, @Nullable Integer userId) {
if (userId == null) {
// must be logged
throw new IllegalStateException("User is not logged in");
@@ -62,10 +59,10 @@ public class ServerIssueChanges implements ServerComponent {
throw new IllegalStateException("User does not have the role " + requiredRole + " required to change the issue: " + issueKey);
}
DefaultIssue issue = dto.toDefaultIssue();
if (change.hasChanges()) {
workflow.change(issue, change);
issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId())));
}
//if (change.hasChanges()) {
// workflow.change(issue, change);
// issueDao.update(Arrays.asList(IssueDto.toDto(issue, dto.getResourceId(), dto.getRuleId())));
//}
return issue;
}
}

+ 2
- 2
sonar-server/src/main/java/org/sonar/server/platform/Platform.java View File

@@ -70,7 +70,7 @@ import org.sonar.server.configuration.Backup;
import org.sonar.server.configuration.ProfilesManager;
import org.sonar.server.database.EmbeddedDatabaseFactory;
import org.sonar.server.issue.DefaultJRubyIssues;
import org.sonar.server.issue.ServerIssueChanges;
import org.sonar.server.issue.ServerIssueActions;
import org.sonar.server.issue.ServerIssueFinder;
import org.sonar.server.macro.MacroInterpreter;
import org.sonar.server.notifications.NotificationCenter;
@@ -240,7 +240,7 @@ public final class Platform {

// issues
servicesContainer.addSingleton(IssueWorkflow.class);
servicesContainer.addSingleton(ServerIssueChanges.class);
servicesContainer.addSingleton(ServerIssueActions.class);
servicesContainer.addSingleton(ServerIssueFinder.class);
servicesContainer.addSingleton(DefaultJRubyIssues.class);


+ 1
- 1
sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb View File

@@ -40,7 +40,7 @@ class CreateIssues < ActiveRecord::Migration
t.column :user_login, :string, :null => true, :limit => 40
t.column :assignee_login, :string, :null => true, :limit => 40
t.column :author_login, :string, :null => true, :limit => 100
t.column :attributes, :string, :null => true, :limit => 1000
t.column :attributes, :string, :null => true, :limit => 4000
t.column :created_at, :datetime, :null => true
t.column :updated_at, :datetime, :null => true
t.column :closed_at, :datetime, :null => true

+ 1
- 1
sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java View File

@@ -42,7 +42,7 @@ import static org.mockito.Mockito.*;
public class DefaultJRubyIssuesTest {

IssueFinder finder = mock(IssueFinder.class);
ServerIssueChanges changes = mock(ServerIssueChanges.class);
ServerIssueActions changes = mock(ServerIssueActions.class);
DefaultJRubyIssues facade = new DefaultJRubyIssues(finder, changes);

@Test

+ 22
- 11
sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java View File

@@ -79,10 +79,12 @@ public class ServerIssueFinderTest {

IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
List<IssueDto> dtoList = newArrayList(issue1, issue2);
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList);
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList);
@@ -102,10 +104,12 @@ public class ServerIssueFinderTest {

IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(135)
.setComponentKey_unit_test_only("Phases.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
List<IssueDto> dtoList = newArrayList(issue1, issue2);
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList);
when(authorizationDao.keepAuthorizedComponentIds(anySet(), anyInt(), anyString(), any(SqlSession.class))).thenReturn(newHashSet(123));
@@ -127,10 +131,12 @@ public class ServerIssueFinderTest {

IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(135)
.setComponentKey_unit_test_only("Phases.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
List<IssueDto> dtoList = newArrayList(issue1, issue2);
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList);
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList);
@@ -148,7 +154,8 @@ public class ServerIssueFinderTest {
public void should_find_by_key() {
IssueDto issueDto = new IssueDto().setId(1L).setRuleId(1).setResourceId(1)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
when(issueDao.selectByKey("ABCDE")).thenReturn(issueDto);

Issue issue = finder.findByKey("ABCDE");
@@ -167,10 +174,12 @@ public class ServerIssueFinderTest {

IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
List<IssueDto> dtoList = newArrayList(issue1, issue2);
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList);
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList);
@@ -193,10 +202,12 @@ public class ServerIssueFinderTest {

IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123)
.setComponentKey_unit_test_only("Action.java")
.setRuleKey_unit_test_only("squid", "AvoidCycle");
.setRuleKey_unit_test_only("squid", "AvoidCycle")
.setStatus("OPEN").setResolution("OPEN");
List<IssueDto> dtoList = newArrayList(issue1, issue2);
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList);
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList);

Loading…
Cancel
Save