From 6f1161efb92298ace498df544e38f6f97216ec36 Mon Sep 17 00:00:00 2001 From: Simon Brandhof Date: Fri, 26 Apr 2013 15:19:16 +0200 Subject: [PATCH] SONAR-3755 new concept of automatic transitions in issue workflow --- .../org/sonar/plugins/core/CorePlugin.java | 242 +++++++++--------- .../issue/DefaultIssueHandlerContext.java | 86 +++++++ .../plugins/core/issue/IssueFilters.java | 45 ++-- .../plugins/core/issue/IssueHandlers.java | 44 ++++ .../plugins/core/issue/IssueTracking.java | 19 +- .../core/issue/IssueTrackingDecorator.java | 148 ++++------- .../issue/InitialOpenIssuesSensorTest.java | 20 +- .../plugins/core/issue/IssueFiltersTest.java | 56 ++++ .../plugins/core/issue/IssueHandlersTest.java | 55 ++++ .../issue/IssueTrackingDecoratorTest.java | 188 +++++++------- .../plugins/core/issue/IssueTrackingTest.java | 83 +++--- .../java/org/sonar/batch/index/Cache.java | 7 +- .../sonar/batch/issue/DefaultIssuable.java | 3 +- .../batch/issue/DeprecatedViolations.java | 6 +- .../org/sonar/batch/issue/IssueCache.java | 11 +- .../org/sonar/batch/issue/IssuePersister.java | 7 +- .../org/sonar/batch/issue/ScanIssues.java | 38 +-- .../org/sonar/batch/issue/IssueCacheTest.java | 14 +- .../sonar/batch/issue/IssuePersisterTest.java | 6 +- .../org/sonar/batch/issue/ScanIssuesTest.java | 48 +--- .../org/sonar/core/issue/DefaultIssue.java | 23 +- .../sonar/core/issue/DefaultIssueBuilder.java | 1 + .../java/org/sonar/core/issue/IssueDao.java | 1 + .../java/org/sonar/core/issue/IssueDto.java | 4 +- .../core/issue/workflow/HasResolution.java | 39 +++ .../sonar/core/issue/workflow/IsAlive.java | 23 +- .../{IsNotManualIssue.java => IsManual.java} | 10 +- .../core/issue/workflow/IssueWorkflow.java | 87 +++---- .../core/issue/workflow/NotCondition.java | 36 +++ .../core/issue/workflow/SetClosedAt.java | 36 +++ .../org/sonar/core/issue/workflow/State.java | 22 +- .../sonar/core/issue/workflow/Transition.java | 12 + .../sonar/core/issue/DefaultIssueTest.java | 12 +- .../org/sonar/core/issue/IssueDtoTest.java | 2 +- .../core/issue/UpdateIssueFieldsTest.java | 78 ------ .../issue/workflow/HasResolutionTest.java | 42 +++ .../core/issue/workflow/IsAliveTest.java | 43 ++++ .../core/issue/workflow/IsManualTest.java | 43 ++++ .../issue/workflow/IssueWorkflowTest.java | 75 +----- .../core/issue/workflow/NotConditionTest.java | 45 ++++ .../main/java/org/sonar/api/issue/Issue.java | 4 - .../java/org/sonar/api/issue/IssueChange.java | 175 ------------- .../{IssueChanges.java => IssueFilter.java} | 10 +- .../org/sonar/api/issue/IssueHandler.java | 58 +++++ .../main/java/org/sonar/api/issue/Paging.java | 7 +- .../org/sonar/api/issue/IssueChangeTest.java | 160 ------------ .../java/org/sonar/api/issue/PagingTest.java | 15 +- .../server/issue/DefaultJRubyIssues.java | 42 +-- ...ueChanges.java => ServerIssueActions.java} | 17 +- .../org/sonar/server/platform/Platform.java | 4 +- .../WEB-INF/db/migrate/389_create_issues.rb | 2 +- .../server/issue/DefaultJRubyIssuesTest.java | 2 +- .../server/issue/ServerIssueFinderTest.java | 33 ++- 53 files changed, 1150 insertions(+), 1139 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java rename 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 (51%) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java create mode 100644 plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java create mode 100644 plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java create mode 100644 sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java rename sonar-plugin-api/src/main/java/org/sonar/api/issue/NewIssueHandler.java => sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java (72%) rename sonar-core/src/main/java/org/sonar/core/issue/workflow/{IsNotManualIssue.java => IsManual.java} (85%) create mode 100644 sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java create mode 100644 sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java delete mode 100644 sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java create mode 100644 sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java delete mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java rename sonar-plugin-api/src/main/java/org/sonar/api/issue/{IssueChanges.java => IssueFilter.java} (79%) create mode 100644 sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java delete mode 100644 sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java rename sonar-server/src/main/java/org/sonar/server/issue/{ServerIssueChanges.java => ServerIssueActions.java} (81%) diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 396b4a3d9d2..47d9440e324 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -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)) + ); } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java new file mode 100644 index 00000000000..c9810f76603 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/DefaultIssueHandlerContext.java @@ -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; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueFilters.java similarity index 51% rename from sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java rename to plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueFilters.java index b207567906a..7b52e845618 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/UpdateIssueFields.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueFilters.java @@ -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 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; + } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java new file mode 100644 index 00000000000..3e496edcb69 --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueHandlers.java @@ -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); + } + } + +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java index 458dd20a0c8..cdf892f930d 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTracking.java @@ -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 referenceIssues, Collection newIssues) { + /** + * @return untracked issues + */ + public Set track(Resource resource, Collection dbIssues, Collection 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 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); diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java index 0d66c96f94d..8e618077815 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/issue/IssueTrackingDecorator.java @@ -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 newIssues = new ArrayList(scanIssues.issues(resource.getEffectiveKey())); + Collection 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 openIssues = initialOpenIssuesStack.selectAndRemove(resource.getId()); - - tracking.track(resource, openIssues, newIssues); - - updateIssues(newIssues); - - Set 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 dbOpenIssues = initialOpenIssues.selectAndRemove(resource.getId()); + Set 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 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 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 unmatchedDbIssues, Collection 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 openIssues) { - for (IssueDto openIssue : openIssues) { - closeAndSave(openIssue); + private void addDead(Collection 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 { - public String apply(@Nullable Issue issue) { - return (issue != null ? issue.key() : null); - } - } - - private Date getLoadedDate() { - return initialOpenIssuesStack.getLoadedDate(); - } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java index 3c88abb5fcd..90e3ae8edf7 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/InitialOpenIssuesSensorTest.java @@ -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)); } } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java new file mode 100644 index 00000000000..d6dbe9cec3d --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueFiltersTest.java @@ -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(); + } +} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java new file mode 100644 index 00000000000..dbc5e7bec7e --- /dev/null +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueHandlersTest.java @@ -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() { + @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 + } +} diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java index 9e9f775000a..110ba569fe7 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingDecoratorTest.java @@ -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.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 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.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 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 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>() { + @Override + public boolean matches(Object o) { + List issues = (List) 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.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 argument = ArgumentCaptor.forClass(DefaultIssue.class); - verify(scanIssues, times(2)).addOrUpdate(argument.capture()); - - List 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 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() { + @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.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 argument = ArgumentCaptor.forClass(DefaultIssue.class); - verify(scanIssues, times(2)).addOrUpdate(argument.capture()); - - List 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() { + @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.emptyList()); - when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(Collections.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 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); - } - } diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java index 4447eb373ef..ce9bd1868d4 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/issue/IssueTrackingTest.java @@ -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 mapping = decorator.mapIssues(newArrayList(newIssue), Lists.newArrayList()); + Map mapping = tracking.mapIssues(newArrayList(newIssue), Lists.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 mapping = decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); + Map 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 mapping = decorator.mapIssues( + Map 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 mapping = decorator.mapIssues( + Map 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 mapping = decorator.mapIssues( + Map 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 mapping = decorator.mapIssues( + Map 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 mapping = decorator.mapIssues( + Map 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 mapping = decorator.mapIssues( + Map 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; } diff --git a/sonar-batch/src/main/java/org/sonar/batch/index/Cache.java b/sonar-batch/src/main/java/org/sonar/batch/index/Cache.java index 2cd65e523c8..be5ed84357e 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/index/Cache.java +++ b/sonar-batch/src/main/java/org/sonar/batch/index/Cache.java @@ -102,18 +102,17 @@ public class Cache { 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); } diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java b/sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java index 1fa2cb1b83b..845fb6721e7 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/DefaultIssuable.java @@ -50,9 +50,10 @@ public class DefaultIssuable implements Issuable { return scanIssues.initAndAddIssue(((DefaultIssue)issue)); } + @SuppressWarnings("unchecked") @Override public Collection issues() { - return scanIssues.issues(component.key()); + return (Collection)scanIssues.issues(component.key()); } @Override diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java b/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java index 359354b66b5..b69b42c801c 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/DeprecatedViolations.java @@ -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()) diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java b/sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java index f901eaa0ce6..a0ef656bcdc 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/IssueCache.java @@ -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 cache; + private final Cache cache; public IssueCache(Caches caches) { cache = caches.createCache("issues"); } - public Collection componentIssues(String componentKey) { + public Collection 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()); + } } diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java b/sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java index ae2aeda4b8c..8b7ccdc8a7a 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/IssuePersister.java @@ -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 componentEntry : snapshotCache.snapshots()) { String componentKey = componentEntry.getKey(); Snapshot snapshot = componentEntry.getValue(); - Collection issues = issueCache.componentIssues(componentKey); + Collection 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 { diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java index 1326ca4f398..022301e650d 100644 --- a/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java +++ b/sonar-batch/src/main/java/org/sonar/batch/issue/ScanIssues.java @@ -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 issues(String componentKey) { + public Collection 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); + } } diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java index 8956e715657..85fdc6d5909 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/issue/IssueCacheTest.java @@ -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 issues = cache.componentIssues("org.struts.Action"); + Collection 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 issueKeys(Collection issues) { - return Collections2.transform(issues, new Function() { + Collection issueKeys(Collection issues) { + return Collections2.transform(issues, new Function() { @Override - public String apply(@Nullable Issue issue) { + public String apply(@Nullable DefaultIssue issue) { return issue.key(); } }); diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java index 74bc910002f..4a5a87f8b29 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/issue/IssuePersisterTest.java @@ -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")) diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java index 9cf3b7292ed..963f0618ffc 100644 --- a/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java +++ b/sonar-batch/src/test/java/org/sonar/batch/issue/ScanIssuesTest.java @@ -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 argument = ArgumentCaptor.forClass(Issue.class); - verify(cache).addOrUpdate(argument.capture()); + ArgumentCaptor 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 argument = ArgumentCaptor.forClass(Issue.class); - verify(cache).addOrUpdate(argument.capture()); + ArgumentCaptor 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); - } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java index 629dd76f5ee..bb6fe07a4c7 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -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 RESOLUTIONS = ImmutableSet.of(RESOLUTION_OPEN, RESOLUTION_FALSE_POSITIVE, RESOLUTION_FIXED); - private static final Set 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 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); } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java index 7fc4f578091..eb80c617351 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java @@ -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; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java index faa60590ef8..f134f9abaa3 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueDao.java @@ -95,6 +95,7 @@ public class IssueDao implements BatchComponent, ServerComponent { } } + // TODO rename selectOpenIssuesByProject. Is it by module or project ?? public List selectOpenIssues(Integer componentId) { SqlSession session = mybatis.openSession(); try { diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java index 47554fe215c..30f9797b367 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueDto.java @@ -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; } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java new file mode 100644 index 00000000000..536cb950428 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/HasResolution.java @@ -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 resolutions; + + public HasResolution(String first, String... others) { + this.resolutions = ImmutableSet.builder().add(first).add(others).build(); + } + + @Override + public boolean matches(Issue issue) { + return issue.resolution() != null && resolutions.contains(issue.resolution()); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/NewIssueHandler.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java similarity index 72% rename from sonar-plugin-api/src/main/java/org/sonar/api/issue/NewIssueHandler.java rename to sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java index d8361afd7c8..7a3179c5cd0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/NewIssueHandler.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsAlive.java @@ -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; + } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java similarity index 85% rename from sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java rename to sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java index ad035352dc6..d13f6e9d5e3 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsNotManualIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java @@ -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; } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java index b3444cf73d0..0d3ee8c8c7c 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java @@ -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 availableTransitions(Issue issue) { - return machine.state(issue.status()).outTransitions(issue); + public List 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 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; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java new file mode 100644 index 00000000000..8d07e2feb4c --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/NotCondition.java @@ -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); + } + +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java new file mode 100644 index 00000000000..ca2f47c268d --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosedAt.java @@ -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()); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java index 79e8b914c39..99c229501b6 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/State.java @@ -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 outTransitions(Issue issue) { + public List outManualTransitions(Issue issue) { List 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"); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java index 02decc94881..39eb470b6c7 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/Transition.java @@ -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 conditions = Lists.newArrayList(); private List 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"); diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java index ef502c9a152..64e5970e8fb 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java @@ -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"); } } diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java index b60d47d223b..8adc3718c8e 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueDtoTest.java @@ -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++) { diff --git a/sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java b/sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java deleted file mode 100644 index b211019fab3..00000000000 --- a/sonar-core/src/test/java/org/sonar/core/issue/UpdateIssueFieldsTest.java +++ /dev/null @@ -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"); - } -} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java new file mode 100644 index 00000000000..b5fcbc09fb4 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/HasResolutionTest.java @@ -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(); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java new file mode 100644 index 00000000000..6bc1b4197e8 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsAliveTest.java @@ -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(); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java new file mode 100644 index 00000000000..15ffee45752 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java @@ -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(); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java index 46c4ba0b3df..77abe017209 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java @@ -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 transitions = workflow.availableTransitions(issue); + List 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 keys(List transitions) { return Collections2.transform(transitions, new Function() { @Override diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java new file mode 100644 index 00000000000..f38b0827955 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/NotConditionTest.java @@ -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(); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java index ff32f86e54c..fca69d8d0e0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java @@ -79,8 +79,4 @@ public interface Issue extends Serializable { String authorLogin(); - /** - * Used only during project scan. - */ - boolean isNew(); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java deleted file mode 100644 index 85454d0d9bf..00000000000 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChange.java +++ /dev/null @@ -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 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(); - } - 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 attributes() { - return attributes == null ? Collections.emptyMap() : new LinkedHashMap(attributes); - } -} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChanges.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueFilter.java similarity index 79% rename from sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChanges.java rename to sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueFilter.java index 7afd7be4f14..66d3fb9e0f0 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueChanges.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueFilter.java @@ -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); } diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java new file mode 100644 index 00000000000..6ba7c0edb7c --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/IssueHandler.java @@ -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); + +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java index 95f610948f0..867cec866ca 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Paging.java @@ -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; diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java deleted file mode 100644 index 4d8458d7ce8..00000000000 --- a/sonar-plugin-api/src/test/java/org/sonar/api/issue/IssueChangeTest.java +++ /dev/null @@ -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(); - } -} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java index 5229fdad344..ca9dbf25df2 100644 --- a/sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java +++ b/sonar-plugin-api/src/test/java/org/sonar/api/issue/PagingTest.java @@ -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); + } } diff --git a/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java b/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java index b657a2dc794..4c72fe8385d 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/DefaultJRubyIssues.java @@ -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 params, @Nullable Integer currentUserId) { - String issueKey = (String) params.get("key"); - changes.change(issueKey, toChange(params), currentUserId); - } - IssueQuery toQuery(Map 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 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 toRules(Object o) { Collection result = null; diff --git a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java similarity index 81% rename from sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java rename to sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java index 1247a729bdd..dfea7610fb2 100644 --- a/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueChanges.java +++ b/sonar-server/src/main/java/org/sonar/server/issue/ServerIssueActions.java @@ -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; } } diff --git a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java index 5af1888b1ee..e2f6e298c35 100644 --- a/sonar-server/src/main/java/org/sonar/server/platform/Platform.java +++ b/sonar-server/src/main/java/org/sonar/server/platform/Platform.java @@ -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); diff --git a/sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb b/sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb index 51635e96e02..c5478fb1ea6 100644 --- a/sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb +++ b/sonar-server/src/main/webapp/WEB-INF/db/migrate/389_create_issues.rb @@ -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 diff --git a/sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java b/sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java index 39e07faa43b..38b7d784aa6 100644 --- a/sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java +++ b/sonar-server/src/test/java/org/sonar/server/issue/DefaultJRubyIssuesTest.java @@ -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 diff --git a/sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java b/sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java index 03524777c6a..b4c82f6904c 100644 --- a/sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java +++ b/sonar-server/src/test/java/org/sonar/server/issue/ServerIssueFinderTest.java @@ -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 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 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 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 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 dtoList = newArrayList(issue1, issue2); when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList); -- 2.39.5