From 6affc4080d8b98d499174f64e2c86c84cbd2872d Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Wed, 16 Aug 2017 16:03:25 +0200 Subject: [PATCH] SONAR-9720 Issue tracking for long branches --- ...ProjectAnalysisTaskContainerPopulator.java | 2 + .../issue/IntegrateIssuesVisitor.java | 26 +++++--- .../projectanalysis/issue/IssueLifecycle.java | 23 +++++++ .../issue/IssueTrackingDelegator.java | 37 +++++++++-- .../issue/MergeBranchTrackerExecution.java | 42 ++++++++++++ .../issue/ShortBranchTrackerExecution.java | 8 +-- .../projectanalysis/issue/TrackingResult.java | 55 ++++++++++++++++ .../step/PersistIssuesStep.java | 3 +- .../issue/IntegrateIssuesVisitorTest.java | 13 +--- .../issue/IssueAssignerTest.java | 9 +-- .../issue/IssueLifecycleTest.java | 61 +++++++++++++++++ .../issue/IssueTrackingDelegatorTest.java | 34 +++++++++- .../MergeBranchTrackerExecutionTest.java | 65 +++++++++++++++++++ .../step/PersistIssuesStepTest.java | 35 ++++++++++ .../org/sonar/core/issue/DefaultIssue.java | 17 ++++- .../sonar/core/issue/DefaultIssueBuilder.java | 1 + .../sonar/core/issue/DefaultIssueTest.java | 2 + .../main/java/org/sonar/api/issue/Issue.java | 6 ++ .../DeprecatedIssueAdapterForFilter.java | 5 ++ .../scanner/issue/DeprecatedIssueWrapper.java | 5 ++ .../scanner/issue/TrackedIssueAdapter.java | 5 ++ 21 files changed, 419 insertions(+), 35 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 414920d19a3..f2be5a852fa 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -67,6 +67,7 @@ import org.sonar.server.computation.task.projectanalysis.issue.IssueTrackingDele import org.sonar.server.computation.task.projectanalysis.issue.IssueVisitors; import org.sonar.server.computation.task.projectanalysis.issue.IssuesRepositoryVisitor; import org.sonar.server.computation.task.projectanalysis.issue.LoadComponentUuidsHavingOpenIssuesVisitor; +import org.sonar.server.computation.task.projectanalysis.issue.MergeBranchTrackerExecution; import org.sonar.server.computation.task.projectanalysis.issue.MovedIssueVisitor; import org.sonar.server.computation.task.projectanalysis.issue.NewEffortAggregator; import org.sonar.server.computation.task.projectanalysis.issue.NewEffortCalculator; @@ -239,6 +240,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop Tracker.class, TrackerExecution.class, ShortBranchTrackerExecution.class, + MergeBranchTrackerExecution.class, ComponentIssuesLoader.class, BaseIssuesLoader.class, IssueTrackingDelegator.class, diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java index 528372d82e3..3a282e0663c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitor.java @@ -26,7 +26,6 @@ import java.util.List; import java.util.Map; import org.sonar.core.issue.DefaultIssue; -import org.sonar.core.issue.tracking.Tracking; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder; import org.sonar.server.computation.task.projectanalysis.component.Component; import org.sonar.server.computation.task.projectanalysis.component.Component.Status; @@ -60,13 +59,15 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter { issueVisitors.beforeComponent(component); if (isIncremental(component)) { + // no tracking needed, simply re-use existing issues List issues = issuesLoader.loadForComponentUuid(component.getUuid()); - fillIncrementalOpenIssues(component, issues, cacheAppender); + reuseOpenIssues(component, issues, cacheAppender); } else { - Tracking tracking = issueTracking.track(component); - fillNewOpenIssues(component, tracking.getUnmatchedRaws(), cacheAppender); - fillExistingOpenIssues(component, tracking.getMatchedRaws(), cacheAppender); - closeUnmatchedBaseIssues(component, tracking.getUnmatchedBases(), cacheAppender); + TrackingResult tracking = issueTracking.track(component); + fillNewOpenIssues(component, tracking.newIssues(), cacheAppender); + fillExistingOpenIssues(component, tracking.issuesToMerge(), cacheAppender); + closeIssues(component, tracking.issuesToClose(), cacheAppender); + copyIssues(component, tracking.issuesToCopy(), cacheAppender); } issueVisitors.afterComponent(component); } catch (Exception e) { @@ -85,7 +86,16 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter { } } - private void fillIncrementalOpenIssues(Component component, Collection issues, DiskCache.DiskAppender cacheAppender) { + private void copyIssues(Component component, Map matched, DiskCache.DiskAppender cacheAppender) { + for (Map.Entry entry : matched.entrySet()) { + DefaultIssue raw = entry.getKey(); + DefaultIssue base = entry.getValue(); + issueLifecycle.copyExistingOpenIssue(raw, base); + process(component, raw, cacheAppender); + } + } + + private void reuseOpenIssues(Component component, Collection issues, DiskCache.DiskAppender cacheAppender) { for (DefaultIssue issue : issues) { process(component, issue, cacheAppender); } @@ -100,7 +110,7 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter { } } - private void closeUnmatchedBaseIssues(Component component, Iterable issues, DiskCache.DiskAppender cacheAppender) { + private void closeIssues(Component component, Iterable issues, DiskCache.DiskAppender cacheAppender) { for (DefaultIssue issue : issues) { // TODO should replace flag "beingClosed" by express call to transition "automaticClose" issue.setBeingClosed(true); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycle.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycle.java index 6ee0a12e76c..15c8e0017c9 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycle.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycle.java @@ -65,6 +65,29 @@ public class IssueLifecycle { issue.setEffort(debtCalculator.calculate(issue)); } + public void copyExistingOpenIssue(DefaultIssue raw, DefaultIssue base) { + raw.setKey(Uuids.create()); + raw.setNew(false); + raw.setCopied(true); + raw.setType(base.type()); + raw.setCreationDate(base.creationDate()); + raw.setUpdateDate(base.updateDate()); + raw.setCloseDate(base.closeDate()); + raw.setResolution(base.resolution()); + raw.setStatus(base.status()); + raw.setAssignee(base.assignee()); + raw.setAuthorLogin(base.authorLogin()); + raw.setTags(base.tags()); + raw.setAttributes(base.attributes()); + raw.setEffort(debtCalculator.calculate(raw)); + raw.setOnDisabledRule(base.isOnDisabledRule()); + if (base.manualSeverity()) { + raw.setManualSeverity(true); + raw.setSeverity(base.severity()); + } + raw.setSelectedAt(base.selectedAt()); + } + public void mergeExistingOpenIssue(DefaultIssue raw, DefaultIssue base) { raw.setNew(false); raw.setKey(base.key()); diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java index c8ce3631284..655b482c58d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegator.java @@ -19,28 +19,57 @@ */ package org.sonar.server.computation.task.projectanalysis.issue; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; + +import java.util.Optional; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Tracking; +import org.sonar.db.component.BranchType; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.server.computation.task.projectanalysis.analysis.Branch; import org.sonar.server.computation.task.projectanalysis.component.Component; public class IssueTrackingDelegator { private final ShortBranchTrackerExecution shortBranchTracker; private final TrackerExecution tracker; private final AnalysisMetadataHolder analysisMetadataHolder; + private final MergeBranchTrackerExecution mergeBranchTracker; - public IssueTrackingDelegator(ShortBranchTrackerExecution shortBranchTracker, TrackerExecution tracker, AnalysisMetadataHolder analysisMetadataHolder) { + public IssueTrackingDelegator(ShortBranchTrackerExecution shortBranchTracker, MergeBranchTrackerExecution longBranchTracker, + TrackerExecution tracker, AnalysisMetadataHolder analysisMetadataHolder) { this.shortBranchTracker = shortBranchTracker; + this.mergeBranchTracker = longBranchTracker; this.tracker = tracker; this.analysisMetadataHolder = analysisMetadataHolder; } - public Tracking track(Component component) { + public TrackingResult track(Component component) { if (analysisMetadataHolder.isShortLivingBranch()) { - return shortBranchTracker.track(component); + return standardResult(shortBranchTracker.track(component)); + } else if (isFirstAnalysisSecondaryLongLivingBranch()) { + Tracking tracking = mergeBranchTracker.track(component); + return new TrackingResult(tracking.getMatchedRaws(), emptyMap(), emptyList(), tracking.getUnmatchedRaws()); } else { - return tracker.track(component); + return standardResult(tracker.track(component)); + } + } + + private static TrackingResult standardResult(Tracking tracking) { + return new TrackingResult(emptyMap(), tracking.getMatchedRaws(), tracking.getUnmatchedBases(), tracking.getUnmatchedRaws()); + } + + /** + * Special case where we want to do the issue tracking with the merge branch, and copy matched issue to the current branch. + */ + private boolean isFirstAnalysisSecondaryLongLivingBranch() { + if (analysisMetadataHolder.isFirstAnalysis()) { + Optional branch = analysisMetadataHolder.getBranch(); + if (branch.isPresent()) { + return !branch.get().isMain() && branch.get().getType() == BranchType.LONG; + } } + return false; } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java new file mode 100644 index 00000000000..ed84ffd1e2d --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecution.java @@ -0,0 +1,42 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.server.computation.task.projectanalysis.issue; + +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Tracker; +import org.sonar.core.issue.tracking.Tracking; +import org.sonar.server.computation.task.projectanalysis.component.Component; + +public class MergeBranchTrackerExecution { + private final TrackerRawInputFactory rawInputFactory; + private final TrackerMergeBranchInputFactory mergeInputFactory; + private final Tracker tracker; + + public MergeBranchTrackerExecution(TrackerRawInputFactory rawInputFactory, TrackerMergeBranchInputFactory mergeInputFactory, + Tracker tracker) { + this.rawInputFactory = rawInputFactory; + this.mergeInputFactory = mergeInputFactory; + this.tracker = tracker; + } + + public Tracking track(Component component) { + return tracker.track(rawInputFactory.create(component), mergeInputFactory.create(component)); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchTrackerExecution.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchTrackerExecution.java index f5888d5c2cb..ebc4d88b332 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchTrackerExecution.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/ShortBranchTrackerExecution.java @@ -29,10 +29,10 @@ import org.sonar.core.issue.tracking.Tracking; import org.sonar.server.computation.task.projectanalysis.component.Component; public class ShortBranchTrackerExecution { - private TrackerBaseInputFactory baseInputFactory; - private TrackerRawInputFactory rawInputFactory; - private TrackerMergeBranchInputFactory mergeInputFactory; - private Tracker tracker; + private final TrackerBaseInputFactory baseInputFactory; + private final TrackerRawInputFactory rawInputFactory; + private final TrackerMergeBranchInputFactory mergeInputFactory; + private final Tracker tracker; public ShortBranchTrackerExecution(TrackerBaseInputFactory baseInputFactory, TrackerRawInputFactory rawInputFactory, TrackerMergeBranchInputFactory mergeInputFactory, Tracker tracker) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java new file mode 100644 index 00000000000..b469301b4bd --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/issue/TrackingResult.java @@ -0,0 +1,55 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.server.computation.task.projectanalysis.issue; + +import java.util.Map; + +import org.sonar.core.issue.DefaultIssue; + +public class TrackingResult { + private final Map issuesToCopy; + private final Map issuesToMerge; + private final Iterable issuesToClose; + private final Iterable newIssues; + + public TrackingResult(Map issuesToCopy, Map issuesToMerge, + Iterable issuesToClose, Iterable newIssues) { + this.issuesToCopy = issuesToCopy; + this.issuesToMerge = issuesToMerge; + this.issuesToClose = issuesToClose; + this.newIssues = newIssues; + } + + public Map issuesToCopy() { + return issuesToCopy; + } + + public Map issuesToMerge() { + return issuesToMerge; + } + + public Iterable issuesToClose() { + return issuesToClose; + } + + public Iterable newIssues() { + return newIssues; + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStep.java index 25ee9915bf5..6276bf44b25 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStep.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStep.java @@ -73,10 +73,11 @@ public class PersistIssuesStep implements ComputationStep { } private boolean persistIssueIfRequired(IssueMapper mapper, DefaultIssue issue) { - if (issue.isNew()) { + if (issue.isNew() || issue.isCopied()) { persistNewIssue(mapper, issue); return true; } + if (issue.isChanged()) { persistChangedIssue(mapper, issue); return true; diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java index 6c663d28250..0ae5dada91c 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java @@ -119,6 +119,7 @@ public class IntegrateIssuesVisitorTest { IssueTrackingDelegator trackingDelegator; TrackerExecution tracker; ShortBranchTrackerExecution shortBranchTracker; + MergeBranchTrackerExecution mergeBranchTracker; IssueCache issueCache; TypeAwareVisitor underTest; @@ -135,8 +136,9 @@ public class IntegrateIssuesVisitorTest { TrackerMergeBranchInputFactory mergeInputFactory = new TrackerMergeBranchInputFactory(issuesLoader, analysisMetadataHolder, dbTester.getDbClient()); tracker = new TrackerExecution(baseInputFactory, rawInputFactory, new Tracker<>()); shortBranchTracker = new ShortBranchTrackerExecution(baseInputFactory, rawInputFactory, mergeInputFactory, new Tracker<>()); + mergeBranchTracker = new MergeBranchTrackerExecution(rawInputFactory, mergeInputFactory, new Tracker<>()); - trackingDelegator = new IssueTrackingDelegator(shortBranchTracker, tracker, analysisMetadataHolder); + trackingDelegator = new IssueTrackingDelegator(shortBranchTracker, mergeBranchTracker, tracker, analysisMetadataHolder); treeRootHolder.setRoot(PROJECT); issueCache = new IssueCache(temp.newFile(), System2.INSTANCE); when(analysisMetadataHolder.isIncrementalAnalysis()).thenReturn(false); @@ -165,11 +167,6 @@ public class IntegrateIssuesVisitorTest { assertThat(newArrayList(issueCache.traverse())).hasSize(1); } - @Test - public void process_short_branch_issues() { - //TODO - } - @Test public void process_new_issue() throws Exception { ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() @@ -268,10 +265,6 @@ public class IntegrateIssuesVisitorTest { underTest.visitAny(FILE); } - private void addMergeIssue(RuleKey ruleKey) { - - } - private void addBaseIssue(RuleKey ruleKey) { ComponentDto project = ComponentTesting.newPrivateProjectDto(dbTester.organizations().insert(), PROJECT_UUID).setDbKey(PROJECT_KEY); ComponentDto file = ComponentTesting.newFileDto(project, null, FILE_UUID).setDbKey(FILE_KEY); diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueAssignerTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueAssignerTest.java index a82ece85b5a..7780f069ae2 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueAssignerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueAssignerTest.java @@ -20,6 +20,7 @@ package org.sonar.server.computation.task.projectanalysis.issue; import org.junit.Test; +import org.junit.Rule; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.log.LogTester; import org.sonar.api.utils.log.LoggerLevel; @@ -40,13 +41,13 @@ public class IssueAssignerTest { static final int FILE_REF = 1; static final Component FILE = builder(Component.Type.FILE, FILE_REF).setKey("FILE_KEY").setUuid("FILE_UUID").build(); - @org.junit.Rule + @Rule public LogTester logTester = new LogTester(); - @org.junit.Rule + @Rule public ScmInfoRepositoryRule scmInfoRepository = new ScmInfoRepositoryRule(); - @org.junit.Rule + @Rule public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule().setAnalysisDate(123456789L); ScmAccountToUser scmAccountToUser = mock(ScmAccountToUser.class); @@ -185,7 +186,7 @@ public class IssueAssignerTest { "moduleUuid=,moduleUuidPath=,projectUuid=,projectKey=,ruleKey=,language=,severity=," + "manualSeverity=false,message=,line=2,gap=,effort=,status=,resolution=," + "assignee=,checksum=,attributes=,authorLogin=,comments=,tags=," + - "locations=,creationDate=,updateDate=,closeDate=,currentChange=,changes=,isNew=true," + + "locations=,creationDate=,updateDate=,closeDate=,currentChange=,changes=,isNew=true,isCopied=false," + "beingClosed=false,onDisabledRule=false,isChanged=false,sendNotifications=false,selectedAt=]"); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java index a056ccecdde..f7e3a07c7b1 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueLifecycleTest.java @@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; import static org.sonar.api.issue.Issue.STATUS_CLOSED; @@ -70,6 +71,66 @@ public class IssueLifecycleTest { assertThat(issue.updateDate()).isNotNull(); assertThat(issue.status()).isEqualTo(STATUS_OPEN); assertThat(issue.debt()).isEqualTo(DEFAULT_DURATION); + assertThat(issue.isNew()).isTrue(); + assertThat(issue.isCopied()).isFalse(); + } + + @Test + public void copiedIssue() throws Exception { + DefaultIssue raw = new DefaultIssue() + .setNew(true) + .setKey("RAW_KEY") + .setCreationDate(parseDate("2015-10-01")) + .setUpdateDate(parseDate("2015-10-02")) + .setCloseDate(parseDate("2015-10-03")); + + DbIssues.Locations issueLocations = DbIssues.Locations.newBuilder() + .setTextRange(DbCommons.TextRange.newBuilder() + .setStartLine(10) + .setEndLine(12) + .build()) + .build(); + DefaultIssue base = new DefaultIssue() + .setKey("BASE_KEY") + .setCreationDate(parseDate("2015-01-01")) + .setUpdateDate(parseDate("2015-01-02")) + .setCloseDate(parseDate("2015-01-03")) + .setResolution(RESOLUTION_FIXED) + .setStatus(STATUS_CLOSED) + .setSeverity(BLOCKER) + .setAssignee("base assignee") + .setAuthorLogin("base author") + .setTags(newArrayList("base tag")) + .setOnDisabledRule(true) + .setSelectedAt(1000L) + .setLine(10) + .setMessage("message") + .setGap(15d) + .setEffort(Duration.create(15L)) + .setManualSeverity(false) + .setLocations(issueLocations); + + when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION); + + underTest.copyExistingOpenIssue(raw, base); + + assertThat(raw.isNew()).isFalse(); + assertThat(raw.isCopied()).isTrue(); + assertThat(raw.key()).isNotNull(); + assertThat(raw.key()).isNotEqualTo(base.key()); + assertThat(raw.creationDate()).isEqualTo(base.creationDate()); + assertThat(raw.updateDate()).isEqualTo(base.updateDate()); + assertThat(raw.closeDate()).isEqualTo(base.closeDate()); + assertThat(raw.resolution()).isEqualTo(RESOLUTION_FIXED); + assertThat(raw.status()).isEqualTo(STATUS_CLOSED); + assertThat(raw.assignee()).isEqualTo("base assignee"); + assertThat(raw.authorLogin()).isEqualTo("base author"); + assertThat(raw.tags()).containsOnly("base tag"); + assertThat(raw.debt()).isEqualTo(DEFAULT_DURATION); + assertThat(raw.isOnDisabledRule()).isTrue(); + assertThat(raw.selectedAt()).isEqualTo(1000L); + + verifyZeroInteractions(updater); } @Test diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java index 4eede37eb18..966f98ef623 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/IssueTrackingDelegatorTest.java @@ -24,10 +24,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +import java.util.Optional; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Tracking; import org.sonar.db.component.BranchType; import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolder; import org.sonar.server.computation.task.projectanalysis.analysis.Branch; @@ -37,40 +41,66 @@ public class IssueTrackingDelegatorTest { @Mock private ShortBranchTrackerExecution shortBranchTracker; @Mock + private MergeBranchTrackerExecution mergeBranchTracker; + @Mock private TrackerExecution tracker; @Mock private AnalysisMetadataHolder analysisMetadataHolder; @Mock private Component component; + @Mock + private Tracking trackingResult; private IssueTrackingDelegator underTest; @Before public void setUp() { MockitoAnnotations.initMocks(this); - underTest = new IssueTrackingDelegator(shortBranchTracker, tracker, analysisMetadataHolder); + underTest = new IssueTrackingDelegator(shortBranchTracker, mergeBranchTracker, tracker, analysisMetadataHolder); + when(tracker.track(component)).thenReturn(trackingResult); + when(mergeBranchTracker.track(component)).thenReturn(trackingResult); + when(shortBranchTracker.track(component)).thenReturn(trackingResult); } @Test public void delegate_regular_tracker() { when(analysisMetadataHolder.isShortLivingBranch()).thenReturn(false); + when(analysisMetadataHolder.getBranch()).thenReturn(Optional.empty()); underTest.track(component); verify(tracker).track(component); verifyZeroInteractions(shortBranchTracker); + verifyZeroInteractions(mergeBranchTracker); + } + + @Test + public void delegate_merge_tracker() { + Branch branch = mock(Branch.class); + when(branch.getType()).thenReturn(BranchType.LONG); + when(branch.isMain()).thenReturn(false); + when(analysisMetadataHolder.getBranch()).thenReturn(Optional.of(branch)); + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true); + + underTest.track(component); + + verify(mergeBranchTracker).track(component); + verifyZeroInteractions(tracker); + verifyZeroInteractions(shortBranchTracker); + } @Test public void delegate_short_branch_tracker() { Branch branch = mock(Branch.class); when(branch.getType()).thenReturn(BranchType.SHORT); + when(analysisMetadataHolder.getBranch()).thenReturn(Optional.empty()); when(analysisMetadataHolder.isShortLivingBranch()).thenReturn(true); underTest.track(component); verify(shortBranchTracker).track(component); verifyZeroInteractions(tracker); - + verifyZeroInteractions(mergeBranchTracker); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java new file mode 100644 index 00000000000..5dc408b7fb6 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java @@ -0,0 +1,65 @@ +/* + * SonarQube + * Copyright (C) 2009-2017 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program 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. + * + * This program 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.server.computation.task.projectanalysis.issue; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.core.issue.tracking.Tracker; +import org.sonar.core.issue.tracking.Tracking; +import org.sonar.server.computation.task.projectanalysis.component.Component; + +public class MergeBranchTrackerExecutionTest { + @Mock + private TrackerRawInputFactory rawInputFactory; + @Mock + private TrackerMergeBranchInputFactory mergeInputFactory; + @Mock + private Tracker tracker; + @Mock + private Component component; + + private MergeBranchTrackerExecution underTest; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + underTest = new MergeBranchTrackerExecution(rawInputFactory, mergeInputFactory, tracker); + } + + @Test + public void testTracking() { + Input rawInput = mock(Input.class); + Input mergeInput = mock(Input.class); + Tracking result = mock(Tracking.class); + when(rawInputFactory.create(component)).thenReturn(rawInput); + when(mergeInputFactory.create(component)).thenReturn(mergeInput); + when(tracker.track(rawInput, mergeInput)).thenReturn(result); + + assertThat(underTest.track(component)).isEqualTo(result); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java index 1612e0d333c..bbb198d92ab 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/task/projectanalysis/step/PersistIssuesStepTest.java @@ -93,6 +93,41 @@ public class PersistIssuesStepTest extends BaseStepTest { session.close(); } + @Test + public void insert_copied_issue() { + RuleDefinitionDto rule = RuleTesting.newRule(RuleKey.of("xoo", "S01")); + dbTester.rules().insert(rule); + OrganizationDto organizationDto = dbTester.organizations().insert(); + ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto); + dbClient.componentDao().insert(session, project); + ComponentDto file = ComponentTesting.newFileDto(project, null); + dbClient.componentDao().insert(session, file); + session.commit(); + + issueCache.newAppender().append(new DefaultIssue() + .setKey("ISSUE") + .setType(RuleType.CODE_SMELL) + .setRuleKey(rule.getKey()) + .setComponentUuid(file.uuid()) + .setProjectUuid(project.uuid()) + .setSeverity(Severity.BLOCKER) + .setStatus(Issue.STATUS_OPEN) + .setNew(false) + .setCopied(true) + .setType(RuleType.BUG)).close(); + + step.execute(); + + IssueDto result = dbClient.issueDao().selectOrFailByKey(session, "ISSUE"); + assertThat(result.getKey()).isEqualTo("ISSUE"); + assertThat(result.getRuleKey()).isEqualTo(rule.getKey()); + assertThat(result.getComponentUuid()).isEqualTo(file.uuid()); + assertThat(result.getProjectUuid()).isEqualTo(project.uuid()); + assertThat(result.getSeverity()).isEqualTo(Severity.BLOCKER); + assertThat(result.getStatus()).isEqualTo(Issue.STATUS_OPEN); + assertThat(result.getType()).isEqualTo(RuleType.BUG.getDbConstant()); + } + @Test public void insert_new_issue() { RuleDefinitionDto rule = RuleTesting.newRule(RuleKey.of("xoo", "S01")); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java index a92b911d44f..8ac7a205d58 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -96,10 +96,13 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. // all changes private List changes = null; - // true if the the issue did not exist in the previous scan. + // true if the issue did not exist in the previous scan. private boolean isNew = true; - // True if the the issue did exist in the previous scan but not in the current one. That means + // true if the issue is being copied to a different branch + private boolean isCopied = false; + + // True if the issue did exist in the previous scan but not in the current one. That means // that this issue should be closed. private boolean beingClosed = false; @@ -412,6 +415,16 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. return isNew; } + @Override + public boolean isCopied() { + return isCopied; + } + + public DefaultIssue setCopied(boolean b) { + isCopied = b; + return this; + } + public DefaultIssue setNew(boolean b) { isNew = b; return this; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java index 9a1a2c3e5dc..29323740902 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java @@ -161,6 +161,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { issue.setStatus(Issue.STATUS_OPEN); issue.setCloseDate(null); issue.setNew(true); + issue.setCopied(false); issue.setBeingClosed(false); issue.setOnDisabledRule(false); return issue; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java index f9c36b3bc2a..247c2d32ebd 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java @@ -59,6 +59,7 @@ public class DefaultIssueTest { .setNew(true) .setBeingClosed(true) .setOnDisabledRule(true) + .setCopied(true) .setChanged(true) .setSendNotifications(true) .setCreationDate(new SimpleDateFormat("yyyy-MM-dd").parse("2013-08-19")) @@ -83,6 +84,7 @@ public class DefaultIssueTest { assertThat(issue.authorLogin()).isEqualTo("steph"); assertThat(issue.checksum()).isEqualTo("c7b5db46591806455cf082bb348631e8"); assertThat(issue.isNew()).isTrue(); + assertThat(issue.isCopied()).isTrue(); assertThat(issue.isBeingClosed()).isTrue(); assertThat(issue.isOnDisabledRule()).isTrue(); assertThat(issue.isChanged()).isTrue(); diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java index 4ed5b6750c9..9d4ffe43763 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/issue/Issue.java @@ -206,6 +206,12 @@ public interface Issue extends Serializable { */ boolean isNew(); + /** + * During a scan returns true if the issue is copied from another branch. + * @since 6.6 + */ + boolean isCopied(); + /** * @deprecated since 5.5, replaced by {@link #effort()} */ diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueAdapterForFilter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueAdapterForFilter.java index 4b45945147e..389ace4a936 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueAdapterForFilter.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueAdapterForFilter.java @@ -160,6 +160,11 @@ class DeprecatedIssueAdapterForFilter implements Issue { throw unsupported(); } + @Override + public boolean isCopied() { + throw unsupported(); + } + @Deprecated @Override public Duration debt() { diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueWrapper.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueWrapper.java index 843d7c1e7e4..5c487b5bf4b 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueWrapper.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/DeprecatedIssueWrapper.java @@ -160,6 +160,11 @@ public class DeprecatedIssueWrapper implements Issue { return false; } + @Override + public boolean isCopied() { + return false; + } + @Override public Duration debt() { return null; diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/TrackedIssueAdapter.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/TrackedIssueAdapter.java index 6db8b0abaa6..5d969e3a56e 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/TrackedIssueAdapter.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/TrackedIssueAdapter.java @@ -113,6 +113,11 @@ public class TrackedIssueAdapter implements Issue { return issue.isNew(); } + @Override + public boolean isCopied() { + return false; + } + @Override public Map attributes() { return new HashMap<>(); -- 2.39.5