@@ -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)) | |||
); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -17,36 +17,29 @@ | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.core.issue; | |||
package org.sonar.plugins.core.issue; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.api.BatchExtension; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.issue.IssueFilter; | |||
import java.util.Map; | |||
public class IssueFilters implements BatchExtension { | |||
private final IssueFilter[] filters; | |||
public class UpdateIssueFields { | |||
public IssueFilters(IssueFilter[] filters) { | |||
this.filters = filters; | |||
} | |||
public static void apply(DefaultIssue issue, IssueChange change) { | |||
if (change.description() != null) { | |||
issue.setDescription(change.description()); | |||
} | |||
if (change.manualSeverity() != null) { | |||
change.setManualSeverity(change.manualSeverity()); | |||
} | |||
if (change.severity() != null) { | |||
issue.setSeverity(change.severity()); | |||
} | |||
if (change.isAssigneeChanged()) { | |||
issue.setAssignee(change.assignee()); | |||
} | |||
if (change.isLineChanged()) { | |||
issue.setLine(change.line()); | |||
} | |||
if (change.isCostChanged()) { | |||
issue.setCost(change.cost()); | |||
} | |||
for (Map.Entry<String, String> entry : change.attributes().entrySet()) { | |||
issue.setAttribute(entry.getKey(), entry.getValue()); | |||
} | |||
public IssueFilters() { | |||
this(new IssueFilter[0]); | |||
} | |||
public boolean accept(Issue issue) { | |||
for (IssueFilter filter : filters) { | |||
if (!filter.accept(issue)) { | |||
return false; | |||
} | |||
} | |||
return true; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -28,6 +28,7 @@ import org.sonar.api.batch.SonarIndex; | |||
import org.sonar.api.resources.Project; | |||
import org.sonar.api.resources.Resource; | |||
import org.sonar.api.rules.RuleFinder; | |||
import org.sonar.api.utils.KeyValueFormat; | |||
import org.sonar.batch.scan.LastSnapshots; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueDto; | |||
@@ -70,14 +71,19 @@ public class IssueTracking implements BatchExtension { | |||
this.index = index; | |||
} | |||
public void track(Resource resource, Collection<IssueDto> referenceIssues, Collection<DefaultIssue> newIssues) { | |||
/** | |||
* @return untracked issues | |||
*/ | |||
public Set<IssueDto> track(Resource resource, Collection<IssueDto> dbIssues, Collection<DefaultIssue> newIssues) { | |||
referenceIssuesMap.clear(); | |||
unmappedLastIssues.clear(); | |||
String source = index.getSource(resource); | |||
setChecksumOnNewIssues(newIssues, source); | |||
// Map new issues with old ones | |||
mapIssues(newIssues, referenceIssues, source, resource); | |||
mapIssues(newIssues, dbIssues, source, resource); | |||
return unmappedLastIssues; | |||
} | |||
private void setChecksumOnNewIssues(Collection<DefaultIssue> issues, String source) { | |||
@@ -113,7 +119,6 @@ public class IssueTracking implements BatchExtension { | |||
mapIssuesOnSameRule(newIssues, lastIssuesByRule); | |||
} | |||
unmappedLastIssues.clear(); | |||
return referenceIssuesMap; | |||
} | |||
@@ -342,13 +347,19 @@ public class IssueTracking implements BatchExtension { | |||
if (pastIssue != null) { | |||
newIssue.setKey(pastIssue.getKey()); | |||
if (pastIssue.isManualSeverity()) { | |||
newIssue.setManualSeverity(true); | |||
newIssue.setSeverity(pastIssue.getSeverity()); | |||
} | |||
newIssue.setResolution(pastIssue.getResolution()); | |||
newIssue.setStatus(pastIssue.getStatus()); | |||
newIssue.setCreatedAt(pastIssue.getCreatedAt()); | |||
newIssue.setUpdatedAt(project.getAnalysisDate()); | |||
newIssue.setNew(false); | |||
newIssue.setAlive(true); | |||
newIssue.setAuthorLogin(pastIssue.getAuthorLogin()); | |||
if (pastIssue.getAttributes() != null) { | |||
newIssue.setAttributes(KeyValueFormat.parse(pastIssue.getAttributes())); | |||
} | |||
lastIssuesByRule.remove(getRuleId(newIssue), pastIssue); | |||
issueMap.put(newIssue, pastIssue); |
@@ -19,9 +19,7 @@ | |||
*/ | |||
package org.sonar.plugins.core.issue; | |||
import com.google.common.base.Function; | |||
import com.google.common.collect.Collections2; | |||
import com.google.common.collect.Sets; | |||
import com.google.common.collect.Lists; | |||
import org.sonar.api.batch.Decorator; | |||
import org.sonar.api.batch.DecoratorBarriers; | |||
import org.sonar.api.batch.DecoratorContext; | |||
@@ -34,25 +32,29 @@ import org.sonar.api.resources.Scopes; | |||
import org.sonar.batch.issue.ScanIssues; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueDto; | |||
import org.sonar.core.issue.workflow.IssueWorkflow; | |||
import javax.annotation.Nullable; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.Set; | |||
@DependedUpon(DecoratorBarriers.END_OF_ISSUES_UPDATES) | |||
public class IssueTrackingDecorator implements Decorator { | |||
private final ScanIssues scanIssues; | |||
private final InitialOpenIssuesStack initialOpenIssuesStack; | |||
private final InitialOpenIssuesStack initialOpenIssues; | |||
private final IssueTracking tracking; | |||
private final IssueFilters filters; | |||
private final IssueHandlers handlers; | |||
private final IssueWorkflow workflow; | |||
public IssueTrackingDecorator(ScanIssues scanIssues, InitialOpenIssuesStack initialOpenIssuesStack, IssueTracking tracking) { | |||
public IssueTrackingDecorator(ScanIssues scanIssues, InitialOpenIssuesStack initialOpenIssues, IssueTracking tracking, | |||
IssueFilters filters, IssueHandlers handlers, IssueWorkflow workflow) { | |||
this.scanIssues = scanIssues; | |||
this.initialOpenIssuesStack = initialOpenIssuesStack; | |||
this.initialOpenIssues = initialOpenIssues; | |||
this.tracking = tracking; | |||
this.filters = filters; | |||
this.handlers = handlers; | |||
this.workflow = workflow; | |||
} | |||
public boolean shouldExecuteOnProject(Project project) { | |||
@@ -60,112 +62,56 @@ public class IssueTrackingDecorator implements Decorator { | |||
} | |||
public void decorate(Resource resource, DecoratorContext context) { | |||
if (isComponentSupported(resource)) { | |||
if (canHaveIssues(resource)) { | |||
// all the issues created by rule engines during this module scan | |||
Collection<DefaultIssue> newIssues = new ArrayList(scanIssues.issues(resource.getEffectiveKey())); | |||
Collection<DefaultIssue> issues = Lists.newArrayList(); | |||
for (Issue issue : scanIssues.issues(resource.getEffectiveKey())) { | |||
if (filters.accept(issue)) { | |||
issues.add((DefaultIssue) issue); | |||
} else { | |||
scanIssues.remove(issue); | |||
} | |||
} | |||
// all the issues that are open in db before starting this module scan | |||
Collection<IssueDto> openIssues = initialOpenIssuesStack.selectAndRemove(resource.getId()); | |||
tracking.track(resource, openIssues, newIssues); | |||
updateIssues(newIssues); | |||
Set<String> issueKeys = Sets.newHashSet(Collections2.transform(newIssues, new IssueToKeyFunction())); | |||
for (IssueDto openIssue : openIssues) { | |||
// not in newIssues | |||
addManualIssuesAndCloseResolvedOnes(openIssue); | |||
closeResolvedStandardIssues(openIssue, issueKeys); | |||
keepFalsePositiveIssues(openIssue); | |||
reopenUnresolvedIssues(openIssue); | |||
Collection<IssueDto> dbOpenIssues = initialOpenIssues.selectAndRemove(resource.getId()); | |||
Set<IssueDto> unmatchedDbIssues = tracking.track(resource, dbOpenIssues, issues); | |||
// TODO register manual issues (isAlive=true, isNew=false) ? Or are they included in unmatchedDbIssues ? | |||
addUnmatched(unmatchedDbIssues, issues); | |||
if (ResourceUtils.isProject(resource)) { | |||
// issues that relate to deleted components | |||
addDead(issues); | |||
} | |||
if (ResourceUtils.isRootProject(resource)) { | |||
closeIssuesOnDeletedResources(initialOpenIssuesStack.getAllIssues()); | |||
for (DefaultIssue issue : issues) { | |||
workflow.doAutomaticTransition(issue); | |||
handlers.execute(issue); | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
} | |||
} | |||
private void updateIssues(Collection<DefaultIssue> newIssues) { | |||
for (DefaultIssue issue : newIssues) { | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
} | |||
private void addManualIssuesAndCloseResolvedOnes(IssueDto openIssue) { | |||
if (openIssue.isManualIssue()) { | |||
DefaultIssue issue = openIssue.toDefaultIssue(); | |||
if (Issue.STATUS_RESOLVED.equals(issue.status())) { | |||
close(issue); | |||
} | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
} | |||
private void closeResolvedStandardIssues(IssueDto openIssue, Set<String> issueKeys) { | |||
if (!openIssue.isManualIssue() && !issueKeys.contains(openIssue.getKey())) { | |||
closeAndSave(openIssue); | |||
} | |||
} | |||
private void keepFalsePositiveIssues(IssueDto openIssue) { | |||
if (!openIssue.isManualIssue() && Issue.RESOLUTION_FALSE_POSITIVE.equals(openIssue.getResolution())) { | |||
DefaultIssue issue = openIssue.toDefaultIssue(); | |||
issue.setResolution(openIssue.getResolution()); | |||
issue.setStatus(openIssue.getStatus()); | |||
issue.setUpdatedAt(getLoadedDate()); | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
} | |||
private void reopenUnresolvedIssues(IssueDto openIssue) { | |||
if (Issue.STATUS_RESOLVED.equals(openIssue.getStatus()) && !Issue.RESOLUTION_FALSE_POSITIVE.equals(openIssue.getResolution()) | |||
&& !openIssue.isManualIssue()) { | |||
reopenAndSave(openIssue); | |||
private void addUnmatched(Set<IssueDto> unmatchedDbIssues, Collection<DefaultIssue> issues) { | |||
for (IssueDto unmatchedDto : unmatchedDbIssues) { | |||
DefaultIssue unmatched = unmatchedDto.toDefaultIssue(); | |||
unmatched.setAlive(false); | |||
unmatched.setNew(false); | |||
issues.add(unmatched); | |||
} | |||
} | |||
/** | |||
* Close issues that relate to resources that have been deleted or renamed. | |||
*/ | |||
private void closeIssuesOnDeletedResources(Collection<IssueDto> openIssues) { | |||
for (IssueDto openIssue : openIssues) { | |||
closeAndSave(openIssue); | |||
private void addDead(Collection<DefaultIssue> issues) { | |||
for (IssueDto deadDto : initialOpenIssues.getAllIssues()) { | |||
DefaultIssue dead = deadDto.toDefaultIssue(); | |||
dead.setAlive(false); | |||
dead.setNew(false); | |||
issues.add(dead); | |||
} | |||
} | |||
private void close(DefaultIssue issue) { | |||
issue.setStatus(Issue.STATUS_CLOSED); | |||
issue.setUpdatedAt(getLoadedDate()); | |||
issue.setClosedAt(getLoadedDate()); | |||
} | |||
private void closeAndSave(IssueDto openIssue) { | |||
DefaultIssue issue = openIssue.toDefaultIssue(); | |||
close(issue); | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
private void reopenAndSave(IssueDto openIssue) { | |||
DefaultIssue issue = openIssue.toDefaultIssue(); | |||
issue.setStatus(Issue.STATUS_REOPENED); | |||
issue.setResolution(Issue.RESOLUTION_OPEN); | |||
issue.setUpdatedAt(getLoadedDate()); | |||
scanIssues.addOrUpdate(issue); | |||
} | |||
private boolean isComponentSupported(Resource resource) { | |||
private boolean canHaveIssues(Resource resource) { | |||
// TODO check existence of perspective Issuable ? | |||
return Scopes.isHigherThanOrEquals(resource.getScope(), Scopes.FILE); | |||
} | |||
private static final class IssueToKeyFunction implements Function<Issue, String> { | |||
public String apply(@Nullable Issue issue) { | |||
return (issue != null ? issue.key() : null); | |||
} | |||
} | |||
private Date getLoadedDate() { | |||
return initialOpenIssuesStack.getLoadedDate(); | |||
} | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -0,0 +1,55 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2013 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.plugins.core.issue; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentMatcher; | |||
import org.sonar.api.issue.IssueHandler; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import static org.mockito.Matchers.argThat; | |||
import static org.mockito.Mockito.mock; | |||
import static org.mockito.Mockito.verify; | |||
public class IssueHandlersTest { | |||
@Test | |||
public void should_execute_handlers() throws Exception { | |||
IssueHandler h1 = mock(IssueHandler.class); | |||
IssueHandler h2 = mock(IssueHandler.class); | |||
IssueHandlers handlers = new IssueHandlers(new IssueHandler[]{h1, h2}); | |||
final DefaultIssue issue = new DefaultIssue(); | |||
handlers.execute(issue); | |||
verify(h1).onIssue(argThat(new ArgumentMatcher<IssueHandler.IssueContext>() { | |||
@Override | |||
public boolean matches(Object o) { | |||
return ((IssueHandler.IssueContext) o).issue() == issue; | |||
} | |||
})); | |||
} | |||
@Test | |||
public void test_no_handlers() { | |||
IssueHandlers handlers = new IssueHandlers(); | |||
handlers.execute(new DefaultIssue()); | |||
// do not fail | |||
} | |||
} |
@@ -19,147 +19,141 @@ | |||
*/ | |||
package org.sonar.plugins.core.issue; | |||
import com.google.common.collect.Lists; | |||
import com.google.common.collect.Sets; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.sonar.api.issue.Issue; | |||
import org.mockito.ArgumentMatcher; | |||
import org.sonar.api.batch.DecoratorContext; | |||
import org.sonar.api.resources.File; | |||
import org.sonar.api.resources.Project; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.resources.Resource; | |||
import org.sonar.batch.issue.ScanIssues; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueDto; | |||
import org.sonar.core.issue.workflow.IssueWorkflow; | |||
import org.sonar.core.persistence.AbstractDaoTestCase; | |||
import org.sonar.java.api.JavaClass; | |||
import java.util.Collections; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.*; | |||
import static com.google.common.collect.Lists.newArrayList; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.junit.Assert.assertFalse; | |||
import static org.junit.Assert.assertTrue; | |||
import static org.mockito.Matchers.anyInt; | |||
import static org.mockito.Matchers.anyString; | |||
import static org.mockito.Mockito.*; | |||
public class IssueTrackingDecoratorTest extends AbstractDaoTestCase { | |||
IssueTrackingDecorator decorator; | |||
ScanIssues scanIssues = mock(ScanIssues.class); | |||
InitialOpenIssuesStack initialOpenIssuesStack = mock(InitialOpenIssuesStack.class); | |||
InitialOpenIssuesStack initialOpenIssues = mock(InitialOpenIssuesStack.class); | |||
IssueTracking tracking = mock(IssueTracking.class); | |||
IssueFilters filters = mock(IssueFilters.class); | |||
IssueHandlers handlers = mock(IssueHandlers.class); | |||
IssueWorkflow workflow = mock(IssueWorkflow.class); | |||
Date loadedDate = new Date(); | |||
@Before | |||
public void init() { | |||
when(initialOpenIssuesStack.getLoadedDate()).thenReturn(loadedDate); | |||
decorator = new IssueTrackingDecorator(scanIssues, initialOpenIssuesStack, tracking); | |||
when(initialOpenIssues.getLoadedDate()).thenReturn(loadedDate); | |||
decorator = new IssueTrackingDecorator(scanIssues, initialOpenIssues, tracking, filters, handlers, workflow); | |||
} | |||
@Test | |||
public void should_execute_on_project() { | |||
Project project = mock(Project.class); | |||
when(project.isLatestAnalysis()).thenReturn(true); | |||
assertTrue(decorator.shouldExecuteOnProject(project)); | |||
assertThat(decorator.shouldExecuteOnProject(project)).isTrue(); | |||
} | |||
@Test | |||
public void should_execute_on_project_not_if_past_scan() { | |||
public void should_not_execute_on_project_if_past_scan() { | |||
Project project = mock(Project.class); | |||
when(project.isLatestAnalysis()).thenReturn(false); | |||
assertFalse(decorator.shouldExecuteOnProject(project)); | |||
assertThat(decorator.shouldExecuteOnProject(project)).isFalse(); | |||
} | |||
@Test | |||
public void should_close_resolved_issue() { | |||
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList()); | |||
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList( | |||
new IssueDto().setKey("100").setRuleId(10).setRuleKey_unit_test_only("squid", "AvoidCycle"))); | |||
decorator.decorate(mock(Resource.class), null); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(scanIssues).addOrUpdate(argument.capture()); | |||
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED); | |||
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate); | |||
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate); | |||
public void should_not_be_executed_on_classes_not_methods() throws Exception { | |||
DecoratorContext context = mock(DecoratorContext.class); | |||
decorator.decorate(JavaClass.create("org.foo.Bar"), context); | |||
verifyZeroInteractions(context, scanIssues, tracking, filters, handlers, workflow); | |||
} | |||
@Test | |||
public void should_close_resolved_manual_issue() { | |||
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList()); | |||
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList( | |||
new IssueDto().setKey("100").setRuleId(1).setManualIssue(true).setStatus(Issue.STATUS_RESOLVED).setRuleKey_unit_test_only("squid", "AvoidCycle"))); | |||
decorator.decorate(mock(Resource.class), null); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(scanIssues).addOrUpdate(argument.capture()); | |||
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED); | |||
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate); | |||
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate); | |||
public void should_process_open_issues() throws Exception { | |||
Resource file = new File("Action.java").setEffectiveKey("struts:Action.java").setId(123); | |||
final DefaultIssue issue = new DefaultIssue(); | |||
// INPUT : one issue, no open issues during previous scan, no filtering | |||
when(scanIssues.issues("struts:Action.java")).thenReturn(Arrays.asList(issue)); | |||
when(filters.accept(issue)).thenReturn(true); | |||
List<IssueDto> dbIssues = Collections.emptyList(); | |||
when(initialOpenIssues.selectAndRemove(123)).thenReturn(dbIssues); | |||
decorator.decorate(file, mock(DecoratorContext.class)); | |||
// Apply filters, track, apply transitions, notify extensions then update cache | |||
verify(filters).accept(issue); | |||
verify(tracking).track(eq(file), eq(dbIssues), argThat(new ArgumentMatcher<Collection<DefaultIssue>>() { | |||
@Override | |||
public boolean matches(Object o) { | |||
List<DefaultIssue> issues = (List<DefaultIssue>) o; | |||
return issues.size() == 1 && issues.get(0) == issue; | |||
} | |||
})); | |||
verify(workflow).doAutomaticTransition(issue); | |||
verify(handlers).execute(issue); | |||
verify(scanIssues).addOrUpdate(issue); | |||
} | |||
@Test | |||
public void should_reopen_unresolved_issue() { | |||
when(scanIssues.issues(anyString())).thenReturn(Lists.<Issue>newArrayList( | |||
new DefaultIssue().setKey("100"))); | |||
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList( | |||
new IssueDto().setKey("100").setRuleId(1).setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FIXED) | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"))); | |||
decorator.decorate(mock(Resource.class), null); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(scanIssues, times(2)).addOrUpdate(argument.capture()); | |||
List<DefaultIssue> capturedDefaultIssues = argument.getAllValues(); | |||
// First call is done when updating issues after calling issue tracking and we don't care | |||
DefaultIssue defaultIssue = capturedDefaultIssues.get(1); | |||
assertThat(defaultIssue.status()).isEqualTo(Issue.STATUS_REOPENED); | |||
assertThat(defaultIssue.resolution()).isEqualTo(Issue.RESOLUTION_OPEN); | |||
assertThat(defaultIssue.updatedAt()).isEqualTo(loadedDate); | |||
public void should_register_unmatched_issues() throws Exception { | |||
// "Unmatched" issues existed in previous scan but not in current one -> they have to be closed | |||
Resource file = new File("Action.java").setEffectiveKey("struts:Action.java").setId(123); | |||
DefaultIssue openIssue = new DefaultIssue(); | |||
// INPUT : one issue, one open issue during previous scan, no filtering | |||
when(scanIssues.issues("struts:Action.java")).thenReturn(Arrays.asList(openIssue)); | |||
when(filters.accept(openIssue)).thenReturn(true); | |||
IssueDto unmatchedIssue = new IssueDto().setKey("ABCDE").setResolution("OPEN").setStatus("OPEN").setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
List<IssueDto> unmatchedIssues = Arrays.asList(unmatchedIssue); | |||
when(tracking.track(eq(file), anyCollection(), anyCollection())).thenReturn(Sets.newHashSet(unmatchedIssues)); | |||
decorator.decorate(file, mock(DecoratorContext.class)); | |||
verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class)); | |||
verify(handlers, times(2)).execute(any(DefaultIssue.class)); | |||
verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class)); | |||
verify(scanIssues).addOrUpdate(argThat(new ArgumentMatcher<DefaultIssue>() { | |||
@Override | |||
public boolean matches(Object o) { | |||
DefaultIssue issue = (DefaultIssue) o; | |||
return "ABCDE".equals(issue.key()); | |||
} | |||
})); | |||
} | |||
@Test | |||
public void should_keep_false_positive_issue() { | |||
when(scanIssues.issues(anyString())).thenReturn(Lists.<Issue>newArrayList( | |||
new DefaultIssue().setKey("100"))); | |||
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(newArrayList( | |||
new IssueDto().setKey("100").setRuleId(1).setStatus(Issue.STATUS_RESOLVED).setResolution(Issue.RESOLUTION_FALSE_POSITIVE) | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"))); | |||
decorator.decorate(mock(Resource.class), null); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(scanIssues, times(2)).addOrUpdate(argument.capture()); | |||
List<DefaultIssue> capturedDefaultIssues = argument.getAllValues(); | |||
// First call is done when updating issues after calling issue tracking and we don't care | |||
DefaultIssue defaultIssue = capturedDefaultIssues.get(1); | |||
assertThat(defaultIssue.status()).isEqualTo(Issue.STATUS_RESOLVED); | |||
assertThat(defaultIssue.resolution()).isEqualTo(Issue.RESOLUTION_FALSE_POSITIVE); | |||
assertThat(defaultIssue.updatedAt()).isEqualTo(loadedDate); | |||
public void should_register_issues_on_deleted_components() throws Exception { | |||
Project project = new Project("struts"); | |||
DefaultIssue openIssue = new DefaultIssue(); | |||
when(scanIssues.issues("struts")).thenReturn(Arrays.asList(openIssue)); | |||
when(filters.accept(openIssue)).thenReturn(true); | |||
IssueDto deadIssue = new IssueDto().setKey("ABCDE").setResolution("OPEN").setStatus("OPEN").setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
when(initialOpenIssues.getAllIssues()).thenReturn(Arrays.asList(deadIssue)); | |||
decorator.decorate(project, mock(DecoratorContext.class)); | |||
// the dead issue must be closed -> apply automatic transition, notify handlers and add to cache | |||
verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class)); | |||
verify(handlers, times(2)).execute(any(DefaultIssue.class)); | |||
verify(scanIssues, times(2)).addOrUpdate(any(DefaultIssue.class)); | |||
verify(scanIssues).addOrUpdate(argThat(new ArgumentMatcher<DefaultIssue>() { | |||
@Override | |||
public boolean matches(Object o) { | |||
DefaultIssue dead = (DefaultIssue) o; | |||
return "ABCDE".equals(dead.key()) && !dead.isNew() && !dead.isAlive(); | |||
} | |||
})); | |||
} | |||
@Test | |||
public void should_close_remaining_open_issue_on_root_project() { | |||
when(scanIssues.issues(anyString())).thenReturn(Collections.<Issue>emptyList()); | |||
when(initialOpenIssuesStack.selectAndRemove(anyInt())).thenReturn(Collections.<IssueDto>emptyList()); | |||
when(initialOpenIssuesStack.getAllIssues()).thenReturn(newArrayList(new IssueDto().setKey("100").setRuleId(1).setRuleKey_unit_test_only("squid", "AvoidCycle"))); | |||
Resource resource = mock(Resource.class); | |||
when(resource.getQualifier()).thenReturn(Qualifiers.PROJECT); | |||
decorator.decorate(resource, null); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(scanIssues).addOrUpdate(argument.capture()); | |||
assertThat(argument.getValue().status()).isEqualTo(Issue.STATUS_CLOSED); | |||
assertThat(argument.getValue().updatedAt()).isEqualTo(loadedDate); | |||
assertThat(argument.getValue().closedAt()).isEqualTo(loadedDate); | |||
} | |||
} |
@@ -25,6 +25,7 @@ import com.google.common.collect.Lists; | |||
import com.google.common.io.Resources; | |||
import org.junit.Before; | |||
import org.junit.Test; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.resources.Project; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rules.Rule; | |||
@@ -47,14 +48,12 @@ import static org.mockito.Mockito.when; | |||
public class IssueTrackingTest { | |||
private final Date analysisDate = DateUtils.parseDate("2013-04-11"); | |||
private IssueTracking decorator; | |||
private Project project; | |||
private RuleFinder ruleFinder; | |||
private LastSnapshots lastSnapshots; | |||
private long violationId = 0; | |||
IssueTracking tracking; | |||
Project project; | |||
RuleFinder ruleFinder; | |||
LastSnapshots lastSnapshots; | |||
long violationId = 0; | |||
@Before | |||
public void before() { | |||
@@ -77,7 +76,7 @@ public class IssueTrackingTest { | |||
project = mock(Project.class); | |||
when(project.getAnalysisDate()).thenReturn(analysisDate); | |||
decorator = new IssueTracking(project, ruleFinder, lastSnapshots, null); | |||
tracking = new IssueTracking(project, ruleFinder, lastSnapshots, null); | |||
} | |||
@Test | |||
@@ -88,9 +87,9 @@ public class IssueTrackingTest { | |||
// exactly the fields of referenceIssue1 but not the same key | |||
DefaultIssue newIssue = newDefaultIssue("message", 10, RuleKey.of("squid", "AvoidCycle"), "checksum1").setKey("200"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue1, referenceIssue2)); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue1, referenceIssue2)); | |||
// same key | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue2); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue2); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -102,10 +101,10 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue1 = newDefaultIssue("message", 3, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
DefaultIssue newIssue2 = newDefaultIssue("message", 5, RuleKey.of("squid", "AvoidCycle"), "checksum2"); | |||
decorator.mapIssues(newArrayList(newIssue1, newIssue2), newArrayList(referenceIssue1, referenceIssue2)); | |||
assertThat(decorator.getReferenceIssue(newIssue1)).isSameAs(referenceIssue1); | |||
tracking.mapIssues(newArrayList(newIssue1, newIssue2), newArrayList(referenceIssue1, referenceIssue2)); | |||
assertThat(tracking.getReferenceIssue(newIssue1)).isSameAs(referenceIssue1); | |||
assertThat(newIssue1.isNew()).isFalse(); | |||
assertThat(decorator.getReferenceIssue(newIssue2)).isSameAs(referenceIssue2); | |||
assertThat(tracking.getReferenceIssue(newIssue2)).isSameAs(referenceIssue2); | |||
assertThat(newIssue2.isNew()).isFalse(); | |||
} | |||
@@ -117,8 +116,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("new message", null, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("old message", null, 1, "checksum1"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -127,8 +126,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("old message", 1, 1, "checksum1"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -137,8 +136,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum2"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -147,8 +146,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), null); | |||
IssueDto referenceIssue = newReferenceIssue("message", 1, 2, null); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isNull(); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isNull(); | |||
assertThat(newIssue.isNew()).isTrue(); | |||
} | |||
@@ -157,8 +156,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 2, 1, "checksum1"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -170,8 +169,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("old message", 2, 1, "checksum1"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -180,8 +179,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 2, 1, "checksum2"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isNull(); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isNull(); | |||
assertThat(newIssue.isNew()).isTrue(); | |||
} | |||
@@ -190,8 +189,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 1, 2, "checksum1"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isNull(); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isNull(); | |||
assertThat(newIssue.isNew()).isTrue(); | |||
} | |||
@@ -202,8 +201,8 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue(" message ", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum2"); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(decorator.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(tracking.getReferenceIssue(newIssue)).isSameAs(referenceIssue); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
} | |||
@@ -212,7 +211,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum"); | |||
assertThat(newIssue.createdAt()).isNull(); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(newArrayList(newIssue), Lists.<IssueDto>newArrayList()); | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(newArrayList(newIssue), Lists.<IssueDto>newArrayList()); | |||
assertThat(mapping.size()).isEqualTo(0); | |||
assertThat(newIssue.createdAt()).isEqualTo(analysisDate); | |||
assertThat(newIssue.isNew()).isTrue(); | |||
@@ -223,7 +222,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum").setSeverity("MAJOR"); | |||
IssueDto referenceIssue = newReferenceIssue("message", 1, 1, "checksum").setSeverity("MINOR").setManualSeverity(true); | |||
decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(newIssue.severity()).isEqualTo("MINOR"); | |||
} | |||
@@ -235,7 +234,7 @@ public class IssueTrackingTest { | |||
referenceIssue.setCreatedAt(referenceDate); | |||
assertThat(newIssue.createdAt()).isNull(); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue)); | |||
assertThat(mapping.size()).isEqualTo(1); | |||
assertThat(newIssue.createdAt()).isEqualTo(referenceDate); | |||
assertThat(newIssue.isNew()).isFalse(); | |||
@@ -250,7 +249,7 @@ public class IssueTrackingTest { | |||
IssueDto referenceIssue = newReferenceIssue("2 branches need to be covered", null, 1, null); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
newArrayList(newIssue), | |||
newArrayList(referenceIssue), | |||
source, project); | |||
@@ -267,7 +266,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), "foo"); | |||
IssueDto referenceIssue = newReferenceIssue("Indentationd", 7, 1, null); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
newArrayList(newIssue), | |||
newArrayList(referenceIssue), | |||
source, project); | |||
@@ -287,7 +286,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), null); | |||
IssueDto referenceIssue = newReferenceIssue("2 branches need to be covered", null, 1, null); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
newArrayList(newIssue), | |||
newArrayList(referenceIssue), | |||
source, project); | |||
@@ -312,7 +311,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue3 = newDefaultIssue("Indentation", 17, RuleKey.of("squid", "AvoidCycle"), null); | |||
DefaultIssue newIssue4 = newDefaultIssue("Indentation", 21, RuleKey.of("squid", "AvoidCycle"), null); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4), | |||
Arrays.asList(referenceIssue1, referenceIssue2), | |||
source, project); | |||
@@ -339,7 +338,7 @@ public class IssueTrackingTest { | |||
DefaultIssue newIssue2 = newDefaultIssue("SystemPrintln", 10, RuleKey.of("squid", "AvoidCycle"), null); | |||
DefaultIssue newIssue3 = newDefaultIssue("SystemPrintln", 14, RuleKey.of("squid", "AvoidCycle"), null); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
Arrays.asList(newIssue1, newIssue2, newIssue3), | |||
Arrays.asList(referenceIssue1), | |||
source, project); | |||
@@ -370,7 +369,7 @@ public class IssueTrackingTest { | |||
// Same as referenceIssue1 | |||
DefaultIssue newIssue5 = newDefaultIssue("Avoid unused local variables such as 'j'.", 6, RuleKey.of("squid", "AvoidCycle"), "4432a2675ec3e1620daefe38386b51ef"); | |||
Map<DefaultIssue, IssueDto> mapping = decorator.mapIssues( | |||
Map<DefaultIssue, IssueDto> mapping = tracking.mapIssues( | |||
Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4, newIssue5), | |||
Arrays.asList(referenceIssue1, referenceIssue2, referenceIssue3), | |||
source, project); | |||
@@ -390,7 +389,7 @@ public class IssueTrackingTest { | |||
} | |||
private DefaultIssue newDefaultIssue(String message, Integer line, RuleKey ruleKey, String checksum) { | |||
return new DefaultIssue().setDescription(message).setLine(line).setRuleKey(ruleKey).setChecksum(checksum); | |||
return new DefaultIssue().setDescription(message).setLine(line).setRuleKey(ruleKey).setChecksum(checksum).setResolution(Issue.RESOLUTION_OPEN).setStatus(Issue.STATUS_OPEN); | |||
} | |||
private IssueDto newReferenceIssue(String message, Integer lineId, int ruleId, String lineChecksum) { | |||
@@ -402,6 +401,8 @@ public class IssueTrackingTest { | |||
referenceIssue.setDescription(message); | |||
referenceIssue.setRuleId(ruleId); | |||
referenceIssue.setChecksum(lineChecksum); | |||
referenceIssue.setResolution(Issue.RESOLUTION_OPEN); | |||
referenceIssue.setStatus(Issue.STATUS_OPEN); | |||
return referenceIssue; | |||
} | |||
@@ -102,18 +102,17 @@ public class Cache<K, V extends Serializable> { | |||
return get(DEFAULT_GROUP, key); | |||
} | |||
public Cache remove(String group, K key) { | |||
public boolean remove(String group, K key) { | |||
try { | |||
exchange.clear(); | |||
exchange.append(group).append(key); | |||
exchange.remove(); | |||
return this; | |||
return exchange.remove(); | |||
} catch (Exception e) { | |||
throw new IllegalStateException("Fail to get element from cache", e); | |||
} | |||
} | |||
public Cache remove(K key) { | |||
public boolean remove(K key) { | |||
return remove(DEFAULT_GROUP, key); | |||
} | |||
@@ -50,9 +50,10 @@ public class DefaultIssuable implements Issuable { | |||
return scanIssues.initAndAddIssue(((DefaultIssue)issue)); | |||
} | |||
@SuppressWarnings("unchecked") | |||
@Override | |||
public Collection<Issue> issues() { | |||
return scanIssues.issues(component.key()); | |||
return (Collection)scanIssues.issues(component.key()); | |||
} | |||
@Override |
@@ -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()) |
@@ -23,6 +23,7 @@ import org.sonar.api.BatchComponent; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.batch.index.Cache; | |||
import org.sonar.batch.index.Caches; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import java.util.Collection; | |||
@@ -32,13 +33,13 @@ import java.util.Collection; | |||
public class IssueCache implements BatchComponent { | |||
// component key -> issue key -> issue | |||
private final Cache<String, Issue> cache; | |||
private final Cache<String, DefaultIssue> cache; | |||
public IssueCache(Caches caches) { | |||
cache = caches.createCache("issues"); | |||
} | |||
public Collection<Issue> componentIssues(String componentKey) { | |||
public Collection<DefaultIssue> componentIssues(String componentKey) { | |||
return cache.values(componentKey); | |||
} | |||
@@ -46,8 +47,12 @@ public class IssueCache implements BatchComponent { | |||
return cache.get(componentKey, issueKey); | |||
} | |||
public IssueCache addOrUpdate(Issue issue) { | |||
public IssueCache put(DefaultIssue issue) { | |||
cache.put(issue.componentKey(), issue.key(), issue); | |||
return this; | |||
} | |||
public boolean remove(Issue issue) { | |||
return cache.remove(issue.componentKey(), issue.key()); | |||
} | |||
} |
@@ -20,7 +20,6 @@ | |||
package org.sonar.batch.issue; | |||
import org.sonar.api.database.model.Snapshot; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.rules.Rule; | |||
import org.sonar.api.rules.RuleFinder; | |||
import org.sonar.batch.index.ScanPersister; | |||
@@ -56,15 +55,15 @@ public class IssuePersister implements ScanPersister { | |||
for (Map.Entry<String, Snapshot> componentEntry : snapshotCache.snapshots()) { | |||
String componentKey = componentEntry.getKey(); | |||
Snapshot snapshot = componentEntry.getValue(); | |||
Collection<Issue> issues = issueCache.componentIssues(componentKey); | |||
Collection<DefaultIssue> issues = issueCache.componentIssues(componentKey); | |||
for (Issue issue : issues) { | |||
for (DefaultIssue issue : issues) { | |||
Rule rule = ruleFinder.findByKey(issue.ruleKey().repository(), issue.ruleKey().rule()); | |||
if (rule == null) { | |||
throw new IllegalStateException("Rule not found: " + issue.ruleKey()); | |||
} | |||
IssueDto dto = IssueDto.toDto((DefaultIssue) issue, snapshot.getResourceId(), rule.getId()); | |||
IssueDto dto = IssueDto.toDto(issue, snapshot.getResourceId(), rule.getId()); | |||
if (issue.isNew()) { | |||
dao.insert(dto); | |||
} else { |
@@ -22,13 +22,10 @@ package org.sonar.batch.issue; | |||
import com.google.common.base.Preconditions; | |||
import com.google.common.base.Strings; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.api.issue.IssueChanges; | |||
import org.sonar.api.profiles.RulesProfile; | |||
import org.sonar.api.resources.Project; | |||
import org.sonar.api.rules.ActiveRule; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.workflow.IssueWorkflow; | |||
import java.util.Collection; | |||
import java.util.UUID; | |||
@@ -36,46 +33,25 @@ import java.util.UUID; | |||
/** | |||
* Central component to manage issues | |||
*/ | |||
public class ScanIssues implements IssueChanges { | |||
public class ScanIssues { | |||
private final RulesProfile qProfile; | |||
private final IssueCache cache; | |||
private final Project project; | |||
private final IssueWorkflow workflow; | |||
public ScanIssues(RulesProfile qProfile, IssueCache cache, Project project, IssueWorkflow workflow) { | |||
public ScanIssues(RulesProfile qProfile, IssueCache cache, Project project) { | |||
this.qProfile = qProfile; | |||
this.cache = cache; | |||
this.project = project; | |||
this.workflow = workflow; | |||
} | |||
@Override | |||
public Issue change(Issue issue, IssueChange change) { | |||
if (!change.hasChanges()) { | |||
return issue; | |||
} | |||
DefaultIssue reloaded = reload(issue); | |||
workflow.change(reloaded, change); | |||
cache.addOrUpdate(reloaded); | |||
return reloaded; | |||
} | |||
private DefaultIssue reload(Issue issue) { | |||
DefaultIssue reloaded = (DefaultIssue) cache.componentIssue(issue.componentKey(), issue.key()); | |||
if (reloaded == null) { | |||
throw new IllegalStateException("Bad API usage. Unregistered issues can't be changed."); | |||
} | |||
return reloaded; | |||
} | |||
public Collection<Issue> issues(String componentKey) { | |||
public Collection<DefaultIssue> issues(String componentKey) { | |||
return cache.componentIssues(componentKey); | |||
} | |||
public ScanIssues addOrUpdate(DefaultIssue issue) { | |||
Preconditions.checkState(!Strings.isNullOrEmpty(issue.key()), "Missing issue key"); | |||
cache.addOrUpdate(issue); | |||
cache.put(issue); | |||
return this; | |||
} | |||
@@ -92,7 +68,11 @@ public class ScanIssues implements IssueChanges { | |||
if (issue.severity() == null) { | |||
issue.setSeverity(activeRule.getSeverity().name()); | |||
} | |||
cache.addOrUpdate(issue); | |||
cache.put(issue); | |||
return true; | |||
} | |||
public boolean remove(Issue issue) { | |||
return cache.remove(issue); | |||
} | |||
} |
@@ -54,7 +54,7 @@ public class IssueCacheTest { | |||
DefaultIssue issue1 = new DefaultIssue().setKey("111").setComponentKey("org.struts.Action"); | |||
DefaultIssue issue2 = new DefaultIssue().setKey("222").setComponentKey("org.struts.Action"); | |||
DefaultIssue issue3 = new DefaultIssue().setKey("333").setComponentKey("org.struts.Filter"); | |||
cache.addOrUpdate(issue1).addOrUpdate(issue2).addOrUpdate(issue3); | |||
cache.put(issue1).put(issue2).put(issue3); | |||
assertThat(issueKeys(cache.componentIssues("org.struts.Action"))).containsOnly("111", "222"); | |||
assertThat(issueKeys(cache.componentIssues("org.struts.Filter"))).containsOnly("333"); | |||
@@ -64,22 +64,22 @@ public class IssueCacheTest { | |||
public void should_update_existing_issue() throws Exception { | |||
IssueCache cache = new IssueCache(caches); | |||
DefaultIssue issue = new DefaultIssue().setKey("111").setComponentKey("org.struts.Action").setSeverity(Severity.BLOCKER); | |||
cache.addOrUpdate(issue); | |||
cache.put(issue); | |||
issue.setSeverity(Severity.MINOR); | |||
cache.addOrUpdate(issue); | |||
cache.put(issue); | |||
Collection<Issue> issues = cache.componentIssues("org.struts.Action"); | |||
Collection<DefaultIssue> issues = cache.componentIssues("org.struts.Action"); | |||
assertThat(issues).hasSize(1); | |||
Issue reloaded = issues.iterator().next(); | |||
assertThat(reloaded.key()).isEqualTo("111"); | |||
assertThat(reloaded.severity()).isEqualTo(Severity.MINOR); | |||
} | |||
Collection<String> issueKeys(Collection<Issue> issues) { | |||
return Collections2.transform(issues, new Function<Issue, String>() { | |||
Collection<String> issueKeys(Collection<DefaultIssue> issues) { | |||
return Collections2.transform(issues, new Function<DefaultIssue, String>() { | |||
@Override | |||
public String apply(@Nullable Issue issue) { | |||
public String apply(@Nullable DefaultIssue issue) { | |||
return issue.key(); | |||
} | |||
}); |
@@ -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")) |
@@ -22,7 +22,6 @@ package org.sonar.batch.issue; | |||
import org.junit.Test; | |||
import org.mockito.ArgumentCaptor; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.api.profiles.RulesProfile; | |||
import org.sonar.api.resources.Project; | |||
import org.sonar.api.rule.RuleKey; | |||
@@ -31,21 +30,18 @@ import org.sonar.api.rules.ActiveRule; | |||
import org.sonar.api.rules.Rule; | |||
import org.sonar.api.rules.RulePriority; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.workflow.IssueWorkflow; | |||
import java.util.Date; | |||
import static org.fest.assertions.Assertions.assertThat; | |||
import static org.fest.assertions.Fail.fail; | |||
import static org.mockito.Mockito.*; | |||
public class ScanIssuesTest { | |||
IssueCache cache = mock(IssueCache.class); | |||
IssueWorkflow workflow = mock(IssueWorkflow.class); | |||
RulesProfile qProfile = mock(RulesProfile.class); | |||
Project project = mock(Project.class); | |||
ScanIssues scanIssues = new ScanIssues(qProfile, cache, project, workflow); | |||
ScanIssues scanIssues = new ScanIssues(qProfile, cache, project); | |||
@Test | |||
public void should_get_issues() throws Exception { | |||
@@ -92,8 +88,8 @@ public class ScanIssuesTest { | |||
boolean added = scanIssues.initAndAddIssue(issue); | |||
assertThat(added).isTrue(); | |||
ArgumentCaptor<Issue> argument = ArgumentCaptor.forClass(Issue.class); | |||
verify(cache).addOrUpdate(argument.capture()); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(cache).put(argument.capture()); | |||
assertThat(argument.getValue().key()).isNotNull(); | |||
assertThat(argument.getValue().severity()).isEqualTo(Severity.CRITICAL); | |||
assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate); | |||
@@ -113,44 +109,10 @@ public class ScanIssuesTest { | |||
DefaultIssue issue = new DefaultIssue().setRuleKey(RuleKey.of("squid", "AvoidCycle")).setSeverity(null); | |||
scanIssues.initAndAddIssue(issue); | |||
ArgumentCaptor<Issue> argument = ArgumentCaptor.forClass(Issue.class); | |||
verify(cache).addOrUpdate(argument.capture()); | |||
ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class); | |||
verify(cache).put(argument.capture()); | |||
assertThat(argument.getValue().key()).isNotNull(); | |||
assertThat(argument.getValue().severity()).isEqualTo(Severity.INFO); | |||
assertThat(argument.getValue().createdAt()).isEqualTo(analysisDate); | |||
} | |||
@Test | |||
public void should_ignore_empty_change() throws Exception { | |||
Issue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE"); | |||
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(issue); | |||
Issue changed = scanIssues.change(issue, IssueChange.create()); | |||
verifyZeroInteractions(cache); | |||
assertThat(changed).isSameAs(issue); | |||
assertThat(changed.updatedAt()).isNull(); | |||
} | |||
@Test | |||
public void unknown_issue_is_a_bad_api_usage() throws Exception { | |||
Issue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE"); | |||
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(null); | |||
try { | |||
scanIssues.change(issue, IssueChange.create().setLine(200)); | |||
fail(); | |||
} catch (IllegalStateException e) { | |||
assertThat(e).hasMessage("Bad API usage. Unregistered issues can't be changed."); | |||
} | |||
} | |||
@Test | |||
public void should_change_fields() throws Exception { | |||
DefaultIssue issue = new DefaultIssue().setComponentKey("org/struts/Action.java").setKey("ABCDE"); | |||
when(cache.componentIssue("org/struts/Action.java", "ABCDE")).thenReturn(issue); | |||
IssueChange change = IssueChange.create().setTransition("resolve"); | |||
scanIssues.change(issue, change); | |||
verify(cache).addOrUpdate(issue); | |||
verify(workflow).change(issue, change); | |||
} | |||
} |
@@ -20,6 +20,7 @@ | |||
package org.sonar.core.issue; | |||
import com.google.common.base.Preconditions; | |||
import com.google.common.base.Strings; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Maps; | |||
@@ -31,7 +32,6 @@ import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rule.Severity; | |||
import javax.annotation.Nullable; | |||
import java.io.Serializable; | |||
import java.util.Collections; | |||
import java.util.Date; | |||
@@ -40,8 +40,6 @@ import java.util.Set; | |||
public class DefaultIssue implements Issue, Serializable { | |||
private static final Set<String> RESOLUTIONS = ImmutableSet.of(RESOLUTION_OPEN, RESOLUTION_FALSE_POSITIVE, RESOLUTION_FIXED); | |||
private static final Set<String> STATUSES = ImmutableSet.of(STATUS_OPEN, STATUS_CLOSED, STATUS_REOPENED, STATUS_RESOLVED); | |||
private String key; | |||
private String componentKey; | |||
private RuleKey ruleKey; | |||
@@ -60,7 +58,9 @@ public class DefaultIssue implements Issue, Serializable { | |||
private boolean manual = false; | |||
private String checksum; | |||
private boolean isNew = true; | |||
private boolean isAlive = true; | |||
private Map<String, String> attributes = null; | |||
private String authorLogin; | |||
public String key() { | |||
@@ -142,8 +142,8 @@ public class DefaultIssue implements Issue, Serializable { | |||
return status; | |||
} | |||
public DefaultIssue setStatus(@Nullable String s) { | |||
Preconditions.checkArgument(s == null || STATUSES.contains(s), "Not a valid status: " + s); | |||
public DefaultIssue setStatus(String s) { | |||
Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set"); | |||
this.status = s; | |||
return this; | |||
} | |||
@@ -152,8 +152,8 @@ public class DefaultIssue implements Issue, Serializable { | |||
return resolution; | |||
} | |||
public DefaultIssue setResolution(@Nullable String s) { | |||
Preconditions.checkArgument(s == null || RESOLUTIONS.contains(s), "Not a valid resolution: " + s); | |||
public DefaultIssue setResolution(String s) { | |||
Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Resolution must be set"); | |||
this.resolution = s; | |||
return this; | |||
} | |||
@@ -230,6 +230,15 @@ public class DefaultIssue implements Issue, Serializable { | |||
return this; | |||
} | |||
public boolean isAlive() { | |||
return isAlive; | |||
} | |||
public DefaultIssue setAlive(boolean b) { | |||
isAlive = b; | |||
return this; | |||
} | |||
public String attribute(String key) { | |||
return attributes == null ? null : attributes.get(key); | |||
} |
@@ -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; |
@@ -95,6 +95,7 @@ public class IssueDao implements BatchComponent, ServerComponent { | |||
} | |||
} | |||
// TODO rename selectOpenIssuesByProject. Is it by module or project ?? | |||
public List<IssueDto> selectOpenIssues(Integer componentId) { | |||
SqlSession session = mybatis.openSession(); | |||
try { |
@@ -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; | |||
} |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2013 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.core.issue.workflow; | |||
import com.google.common.collect.ImmutableSet; | |||
import org.sonar.api.issue.Issue; | |||
import java.util.Set; | |||
public class HasResolution implements Condition { | |||
private final Set<String> resolutions; | |||
public HasResolution(String first, String... others) { | |||
this.resolutions = ImmutableSet.<String>builder().add(first).add(others).build(); | |||
} | |||
@Override | |||
public boolean matches(Issue issue) { | |||
return issue.resolution() != null && resolutions.contains(issue.resolution()); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -24,12 +24,9 @@ import org.sonar.api.BatchComponent; | |||
import org.sonar.api.ServerComponent; | |||
import org.sonar.api.issue.DefaultTransitions; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import java.util.Date; | |||
import java.util.List; | |||
import java.util.Map; | |||
public class IssueWorkflow implements BatchComponent, ServerComponent, Startable { | |||
@@ -41,16 +38,15 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable | |||
.states(Issue.STATUS_OPEN, Issue.STATUS_REOPENED, Issue.STATUS_RESOLVED, Issue.STATUS_CLOSED) | |||
.transition(Transition.builder(DefaultTransitions.CLOSE) | |||
.from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) | |||
.functions(new SetResolution(Issue.RESOLUTION_FIXED)) | |||
// TODO set closed at | |||
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.CLOSE) | |||
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) | |||
// TODO set closed at | |||
.functions(SetClosedAt.CLOSED_AT) | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.CLOSE) | |||
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED) | |||
// TODO set closed at | |||
.functions(SetClosedAt.CLOSED_AT) | |||
.functions(new SetResolution(Issue.RESOLUTION_FIXED)) | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.RESOLVE) | |||
@@ -67,18 +63,48 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.REOPEN) | |||
.from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED) | |||
.functions(new SetResolution(Issue.RESOLUTION_OPEN)) | |||
.functions(new SetResolution(Issue.RESOLUTION_OPEN))// TODO new UnsetClosedAt | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) | |||
.from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED) | |||
.conditions(new IsNotManualIssue()) | |||
.conditions(new IsManual(false)) | |||
.functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE)) | |||
.build()) | |||
.transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) | |||
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED) | |||
.conditions(new IsNotManualIssue()) | |||
.conditions(new IsManual(false)) | |||
.functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE)) | |||
.build()) | |||
// automatic transitions | |||
// Close the issues that do not exist anymore. Note that isAlive() is true on manual issues | |||
.transition(Transition.builder("automaticclose") | |||
.from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) | |||
.conditions(new IsAlive(false), new IsManual(false)) | |||
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) | |||
.automatic() | |||
.build()) | |||
.transition(Transition.builder("automaticclose") | |||
.from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED) | |||
.conditions(new IsAlive(false)) | |||
.functions(new SetResolution(Issue.RESOLUTION_FIXED), SetClosedAt.CLOSED_AT) | |||
.automatic() | |||
.build()) | |||
// Close the issues marked as resolved and that do not exist anymore. | |||
// Note that false-positives are kept resolved and are not closed. | |||
.transition(Transition.builder("automaticclose") | |||
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) | |||
.conditions(new IsAlive(false)) | |||
.functions(SetClosedAt.CLOSED_AT) | |||
.automatic() | |||
.build()) | |||
.transition(Transition.builder("automaticreopen") | |||
.from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED) | |||
.conditions(new IsAlive(true), new HasResolution(Issue.RESOLUTION_FIXED)) | |||
.functions(new SetResolution(Issue.RESOLUTION_OPEN)) | |||
.automatic() | |||
.build()) | |||
.build(); | |||
} | |||
@@ -86,46 +112,17 @@ public class IssueWorkflow implements BatchComponent, ServerComponent, Startable | |||
public void stop() { | |||
} | |||
public List<Transition> availableTransitions(Issue issue) { | |||
return machine.state(issue.status()).outTransitions(issue); | |||
public List<Transition> outManualTransitions(Issue issue) { | |||
return machine.state(issue.status()).outManualTransitions(issue); | |||
} | |||
public boolean change(DefaultIssue issue, IssueChange change) { | |||
if (change.hasChanges()) { | |||
if (change.description() != null) { | |||
issue.setDescription(change.description()); | |||
} | |||
if (change.manualSeverity() != null) { | |||
change.setManualSeverity(change.manualSeverity()); | |||
} | |||
if (change.severity() != null) { | |||
issue.setSeverity(change.severity()); | |||
} | |||
if (change.isAssigneeChanged()) { | |||
issue.setAssignee(change.assignee()); | |||
} | |||
if (change.isLineChanged()) { | |||
issue.setLine(change.line()); | |||
} | |||
if (change.isCostChanged()) { | |||
issue.setCost(change.cost()); | |||
} | |||
for (Map.Entry<String, String> entry : change.attributes().entrySet()) { | |||
issue.setAttribute(entry.getKey(), entry.getValue()); | |||
} | |||
if (change.transition() != null) { | |||
move(issue, change.transition()); | |||
} | |||
issue.setUpdatedAt(new Date()); | |||
return true; | |||
public void doAutomaticTransition(DefaultIssue issue) { | |||
Transition transition = machine.state(issue.status()).outAutomaticTransition(issue); | |||
if (transition != null) { | |||
transition.execute(issue); | |||
} | |||
return false; | |||
} | |||
private void move(DefaultIssue issue, String transition) { | |||
State state = machine.state(issue.status()); | |||
state.move(issue, transition); | |||
} | |||
StateMachine machine() { | |||
return machine; |
@@ -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); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -27,6 +27,7 @@ import org.apache.commons.lang.StringUtils; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import javax.annotation.CheckForNull; | |||
import java.util.List; | |||
import java.util.Set; | |||
@@ -54,17 +55,32 @@ public class State { | |||
} | |||
} | |||
public List<Transition> outTransitions(Issue issue) { | |||
public List<Transition> outManualTransitions(Issue issue) { | |||
List<Transition> result = Lists.newArrayList(); | |||
for (Transition transition : outTransitions) { | |||
if (transition.supports(issue)) { | |||
if (!transition.automatic() && transition.supports(issue)) { | |||
result.add(transition); | |||
} | |||
} | |||
return result; | |||
} | |||
public void move(DefaultIssue issue, String transitionKey) { | |||
@CheckForNull | |||
public Transition outAutomaticTransition(Issue issue) { | |||
Transition result = null; | |||
for (Transition transition : outTransitions) { | |||
if (transition.automatic() && transition.supports(issue)) { | |||
if (result == null) { | |||
result = transition; | |||
} else { | |||
throw new IllegalStateException("Several automatic transitions are available for issue: " + issue); | |||
} | |||
} | |||
} | |||
return result; | |||
} | |||
public void doTransition(DefaultIssue issue, String transitionKey) { | |||
Transition transition = transition(transitionKey); | |||
if (!transition.supports(issue)) { | |||
throw new IllegalStateException("TODO"); |
@@ -35,6 +35,7 @@ class Transition { | |||
private final String from, to; | |||
private final Condition[] conditions; | |||
private final Function[] functions; | |||
private final boolean automatic; | |||
private Transition(TransitionBuilder builder) { | |||
key = builder.key; | |||
@@ -42,6 +43,7 @@ class Transition { | |||
to = builder.to; | |||
conditions = builder.conditions.toArray(new Condition[builder.conditions.size()]); | |||
functions = builder.functions.toArray(new Function[builder.functions.size()]); | |||
automatic = builder.automatic; | |||
} | |||
String key() { | |||
@@ -64,6 +66,10 @@ class Transition { | |||
return functions; | |||
} | |||
boolean automatic() { | |||
return automatic; | |||
} | |||
public boolean supports(Issue issue) { | |||
for (Condition condition : conditions) { | |||
if (!condition.matches(issue)) { | |||
@@ -90,6 +96,7 @@ class Transition { | |||
private String from, to; | |||
private List<Condition> conditions = Lists.newArrayList(); | |||
private List<Function> functions = Lists.newArrayList(); | |||
private boolean automatic = false; | |||
private TransitionBuilder(String key) { | |||
this.key = key; | |||
@@ -115,6 +122,11 @@ class Transition { | |||
return this; | |||
} | |||
public TransitionBuilder automatic() { | |||
this.automatic = true; | |||
return this; | |||
} | |||
public Transition build() { | |||
Preconditions.checkArgument(!Strings.isNullOrEmpty(key), "Transition key must be set"); | |||
Preconditions.checkArgument(StringUtils.isAllLowerCase(key), "Transition key must be lower-case"); |
@@ -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"); | |||
} | |||
} | |||
@@ -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++) { |
@@ -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"); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -23,7 +23,6 @@ import com.google.common.base.Function; | |||
import com.google.common.collect.Collections2; | |||
import org.junit.Test; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import javax.annotation.Nullable; | |||
@@ -48,85 +47,31 @@ public class IssueWorkflowTest { | |||
} | |||
@Test | |||
public void should_list_available_transitions() throws Exception { | |||
public void should_list_out_manual_transitions() throws Exception { | |||
workflow.start(); | |||
DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN); | |||
List<Transition> transitions = workflow.availableTransitions(issue); | |||
List<Transition> transitions = workflow.outManualTransitions(issue); | |||
assertThat(transitions).hasSize(3); | |||
assertThat(keys(transitions)).containsOnly("close", "falsepositive", "resolve"); | |||
} | |||
@Test | |||
public void should_not_change_anything() throws Exception { | |||
workflow.start(); | |||
DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN); | |||
workflow.change(issue, IssueChange.create()); | |||
assertThat(issue.updatedAt()).isNull(); | |||
} | |||
@Test | |||
public void should_set_fields() throws Exception { | |||
workflow.start(); | |||
DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN); | |||
IssueChange change = IssueChange.create() | |||
.setAssignee("arthur") | |||
.setAttribute("JIRA", "FOO-1234") | |||
.setCost(4.2) | |||
.setLine(123) | |||
.setDescription("the desc") | |||
.setSeverity("BLOCKER"); | |||
workflow.change(issue, change); | |||
assertThat(issue.updatedAt()).isNotNull(); | |||
assertThat(issue.assignee()).isEqualTo("arthur"); | |||
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-1234"); | |||
assertThat(issue.cost()).isEqualTo(4.2); | |||
assertThat(issue.line()).isEqualTo(123); | |||
assertThat(issue.description()).isEqualTo("the desc"); | |||
assertThat(issue.severity()).isEqualTo("BLOCKER"); | |||
} | |||
@Test | |||
public void should_change_only_fields_with_new_values() throws Exception { | |||
public void should_do_automatic_transition() throws Exception { | |||
workflow.start(); | |||
DefaultIssue issue = new DefaultIssue() | |||
.setStatus(Issue.STATUS_OPEN) | |||
.setAssignee("karadoc") | |||
.setAttribute("YOUTRACK", "ABC") | |||
.setCost(3.4); | |||
IssueChange change = IssueChange.create() | |||
.setAttribute("JIRA", "FOO-1234") | |||
.setLine(123) | |||
.setSeverity("BLOCKER"); | |||
workflow.change(issue, change); | |||
assertThat(issue.updatedAt()).isNotNull(); | |||
assertThat(issue.assignee()).isEqualTo("karadoc"); | |||
assertThat(issue.attribute("JIRA")).isEqualTo("FOO-1234"); | |||
assertThat(issue.attribute("YOUTRACK")).isEqualTo("ABC"); | |||
assertThat(issue.cost()).isEqualTo(3.4); | |||
assertThat(issue.line()).isEqualTo(123); | |||
assertThat(issue.severity()).isEqualTo("BLOCKER"); | |||
} | |||
@Test | |||
public void should_change_issue_state() throws Exception { | |||
workflow.start(); | |||
DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_OPEN); | |||
IssueChange change = IssueChange.create().setTransition("close"); | |||
workflow.change(issue, change); | |||
assertThat(issue.updatedAt()).isNotNull(); | |||
.setResolution(Issue.RESOLUTION_FIXED) | |||
.setStatus(Issue.STATUS_RESOLVED) | |||
.setNew(false) | |||
.setAlive(false); | |||
workflow.doAutomaticTransition(issue); | |||
assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); | |||
assertThat(issue.status()).isEqualTo(Issue.STATUS_CLOSED); | |||
assertThat(issue.closedAt()).isNotNull(); | |||
} | |||
private Collection<String> keys(List<Transition> transitions) { | |||
return Collections2.transform(transitions, new Function<Transition, String>() { | |||
@Override |
@@ -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(); | |||
} | |||
} |
@@ -79,8 +79,4 @@ public interface Issue extends Serializable { | |||
String authorLogin(); | |||
/** | |||
* Used only during project scan. | |||
*/ | |||
boolean isNew(); | |||
} |
@@ -1,175 +0,0 @@ | |||
/* | |||
* SonarQube, open source software quality management tool. | |||
* Copyright (C) 2008-2013 SonarSource | |||
* mailto:contact AT sonarsource DOT com | |||
* | |||
* SonarQube is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* SonarQube is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.api.issue; | |||
import javax.annotation.Nullable; | |||
import java.util.Collections; | |||
import java.util.LinkedHashMap; | |||
import java.util.Map; | |||
/** | |||
* @since 3.6 | |||
*/ | |||
public class IssueChange { | |||
private String severity = null; | |||
private String comment = null; | |||
private String login = null; | |||
private Boolean manualSeverity = null; | |||
private String description = null; | |||
private boolean lineChanged = false; | |||
private Integer line = null; | |||
private boolean costChanged = false; | |||
private Double cost = null; | |||
private String transition = null; | |||
private boolean assigneeChanged = false; | |||
private String assignee = null; | |||
private String title = null; | |||
private Map<String, String> attributes = null; | |||
private IssueChange() { | |||
} | |||
public static IssueChange create() { | |||
return new IssueChange(); | |||
} | |||
public boolean hasChanges() { | |||
return severity != null || comment != null || manualSeverity != null || description != null || | |||
lineChanged || costChanged || transition != null || assigneeChanged || attributes != null; | |||
} | |||
public IssueChange setSeverity(String s) { | |||
this.severity = s; | |||
return this; | |||
} | |||
public IssueChange setComment(String comment) { | |||
this.comment = comment; | |||
return this; | |||
} | |||
public IssueChange setLogin(String s) { | |||
this.login = s; | |||
return this; | |||
} | |||
public IssueChange setManualSeverity(boolean b) { | |||
this.manualSeverity = b; | |||
return this; | |||
} | |||
public IssueChange setTitle(String s) { | |||
this.title = s; | |||
return this; | |||
} | |||
public IssueChange setDescription(String s) { | |||
this.description = s; | |||
return this; | |||
} | |||
public IssueChange setLine(@Nullable Integer line) { | |||
this.lineChanged = true; | |||
this.line = line; | |||
return this; | |||
} | |||
public IssueChange setCost(@Nullable Double cost) { | |||
this.costChanged = true; | |||
this.cost = cost; | |||
return this; | |||
} | |||
public IssueChange setTransition(String transition) { | |||
this.transition = transition; | |||
return this; | |||
} | |||
public IssueChange setAssignee(@Nullable String assigneeLogin) { | |||
this.assigneeChanged = true; | |||
this.assignee = assigneeLogin; | |||
return this; | |||
} | |||
public IssueChange setAttribute(String key, @Nullable String value) { | |||
if (attributes == null) { | |||
attributes = new LinkedHashMap<String, String>(); | |||
} | |||
attributes.put(key, value); | |||
return this; | |||
} | |||
public String severity() { | |||
return severity; | |||
} | |||
public String comment() { | |||
return comment; | |||
} | |||
public String login() { | |||
return login; | |||
} | |||
public Boolean manualSeverity() { | |||
return manualSeverity; | |||
} | |||
public String description() { | |||
return description; | |||
} | |||
public String title() { | |||
return title; | |||
} | |||
public Integer line() { | |||
return line; | |||
} | |||
public boolean isLineChanged() { | |||
return lineChanged; | |||
} | |||
public Double cost() { | |||
return cost; | |||
} | |||
public boolean isCostChanged() { | |||
return costChanged; | |||
} | |||
public String transition() { | |||
return transition; | |||
} | |||
public String assignee() { | |||
return assignee; | |||
} | |||
public boolean isAssigneeChanged() { | |||
return assigneeChanged; | |||
} | |||
public Map<String, String> attributes() { | |||
return attributes == null ? Collections.<String, String>emptyMap() : new LinkedHashMap<String, String>(attributes); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} |
@@ -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; |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -24,7 +24,6 @@ import com.google.common.base.Splitter; | |||
import com.google.common.collect.Collections2; | |||
import com.google.common.collect.Lists; | |||
import com.google.common.primitives.Ints; | |||
import org.sonar.api.issue.IssueChange; | |||
import org.sonar.api.issue.IssueFinder; | |||
import org.sonar.api.issue.IssueQuery; | |||
import org.sonar.api.issue.JRubyIssues; | |||
@@ -34,7 +33,6 @@ import org.sonar.api.web.UserRole; | |||
import org.sonar.server.ui.JRubyFacades; | |||
import javax.annotation.Nullable; | |||
import java.util.Collection; | |||
import java.util.Date; | |||
import java.util.List; | |||
@@ -48,9 +46,9 @@ import java.util.Map; | |||
public class DefaultJRubyIssues implements JRubyIssues { | |||
private final IssueFinder finder; | |||
private final ServerIssueChanges changes; | |||
private final ServerIssueActions changes; | |||
public DefaultJRubyIssues(IssueFinder f, ServerIssueChanges changes) { | |||
public DefaultJRubyIssues(IssueFinder f, ServerIssueActions changes) { | |||
this.finder = f; | |||
this.changes = changes; | |||
JRubyFacades.setIssues(this); | |||
@@ -65,11 +63,6 @@ public class DefaultJRubyIssues implements JRubyIssues { | |||
return finder.find(toQuery(params), currentUserId, UserRole.CODEVIEWER); | |||
} | |||
public void change(Map<String, Object> params, @Nullable Integer currentUserId) { | |||
String issueKey = (String) params.get("key"); | |||
changes.change(issueKey, toChange(params), currentUserId); | |||
} | |||
IssueQuery toQuery(Map<String, Object> props) { | |||
IssueQuery.Builder builder = IssueQuery.builder(); | |||
builder.keys(toStrings(props.get("keys"))); | |||
@@ -88,37 +81,6 @@ public class DefaultJRubyIssues implements JRubyIssues { | |||
return builder.build(); | |||
} | |||
IssueChange toChange(Map<String, Object> props) { | |||
IssueChange change = IssueChange.create(); | |||
if (props.containsKey("newSeverity")) { | |||
change.setSeverity((String) props.get("newSeverity")); | |||
change.setManualSeverity(true); | |||
} | |||
if (props.containsKey("newDesc")) { | |||
change.setDescription((String) props.get("newDesc")); | |||
} | |||
if (props.containsKey("newCost")) { | |||
change.setCost(toDouble(props.get("newCost"))); | |||
} | |||
if (props.containsKey("newLine")) { | |||
change.setLine(toInteger(props.get("newLine"))); | |||
} | |||
if (props.containsKey("newAssignee")) { | |||
change.setAssignee((String) props.get("newAssignee")); | |||
} | |||
if (props.containsKey("transition")) { | |||
change.setTransition((String) props.get("transition")); | |||
} | |||
if (props.containsKey("newTitle")) { | |||
change.setTitle((String) props.get("newTitle")); | |||
} | |||
if (props.containsKey("comment")) { | |||
change.setComment((String) props.get("comment")); | |||
} | |||
// TODO set attribute and login | |||
return change; | |||
} | |||
@SuppressWarnings("unchecked") | |||
static Collection<RuleKey> toRules(Object o) { | |||
Collection<RuleKey> result = null; |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
@@ -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 |
@@ -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 |
@@ -79,10 +79,12 @@ public class ServerIssueFinderTest { | |||
IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
List<IssueDto> dtoList = newArrayList(issue1, issue2); | |||
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); | |||
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList); | |||
@@ -102,10 +104,12 @@ public class ServerIssueFinderTest { | |||
IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(135) | |||
.setComponentKey_unit_test_only("Phases.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
List<IssueDto> dtoList = newArrayList(issue1, issue2); | |||
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); | |||
when(authorizationDao.keepAuthorizedComponentIds(anySet(), anyInt(), anyString(), any(SqlSession.class))).thenReturn(newHashSet(123)); | |||
@@ -127,10 +131,12 @@ public class ServerIssueFinderTest { | |||
IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(135) | |||
.setComponentKey_unit_test_only("Phases.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
List<IssueDto> dtoList = newArrayList(issue1, issue2); | |||
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); | |||
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList); | |||
@@ -148,7 +154,8 @@ public class ServerIssueFinderTest { | |||
public void should_find_by_key() { | |||
IssueDto issueDto = new IssueDto().setId(1L).setRuleId(1).setResourceId(1) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
when(issueDao.selectByKey("ABCDE")).thenReturn(issueDto); | |||
Issue issue = finder.findByKey("ABCDE"); | |||
@@ -167,10 +174,12 @@ public class ServerIssueFinderTest { | |||
IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
List<IssueDto> dtoList = newArrayList(issue1, issue2); | |||
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); | |||
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList); | |||
@@ -193,10 +202,12 @@ public class ServerIssueFinderTest { | |||
IssueDto issue1 = new IssueDto().setId(1L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
IssueDto issue2 = new IssueDto().setId(2L).setRuleId(50).setResourceId(123) | |||
.setComponentKey_unit_test_only("Action.java") | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle"); | |||
.setRuleKey_unit_test_only("squid", "AvoidCycle") | |||
.setStatus("OPEN").setResolution("OPEN"); | |||
List<IssueDto> dtoList = newArrayList(issue1, issue2); | |||
when(issueDao.selectIssueIdsAndComponentsId(eq(issueQuery), any(SqlSession.class))).thenReturn(dtoList); | |||
when(issueDao.selectByIds(anyCollection(), any(SqlSession.class))).thenReturn(dtoList); |