diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2018-08-14 18:07:10 +0200 |
---|---|---|
committer | SonarTech <sonartech@sonarsource.com> | 2018-08-21 20:21:05 +0200 |
commit | 3065f6824c72d504c8e317c7d2d6a2c682081c4f (patch) | |
tree | aa9a369907d4c31f3225e81f6073de77ba887d9e | |
parent | 61c813392f60ec29e4b628e7244593c627058b5b (diff) | |
download | sonarqube-3065f6824c72d504c8e317c7d2d6a2c682081c4f.tar.gz sonarqube-3065f6824c72d504c8e317c7d2d6a2c682081c4f.zip |
SONAR-8368 reopen closed issues (restore status)
but those from Hotspots rules and manual vulnerabilities
37 files changed, 1413 insertions, 143 deletions
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java index 22346b5606e..55b2be91c70 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java @@ -49,6 +49,7 @@ import org.sonar.ce.task.projectanalysis.filemove.SourceSimilarityImpl; import org.sonar.ce.task.projectanalysis.filesystem.ComputationTempFolderProvider; import org.sonar.ce.task.projectanalysis.issue.BaseIssuesLoader; import org.sonar.ce.task.projectanalysis.issue.CloseIssuesOnRemovedComponentsVisitor; +import org.sonar.ce.task.projectanalysis.issue.ClosedIssuesInputFactory; import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesLoader; import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesRepositoryImpl; import org.sonar.ce.task.projectanalysis.issue.ComponentsWithUnprocessedIssues; @@ -254,6 +255,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop TrackerBaseInputFactory.class, TrackerRawInputFactory.class, TrackerMergeBranchInputFactory.class, + ClosedIssuesInputFactory.class, Tracker.class, TrackerExecution.class, ShortBranchTrackerExecution.class, diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/BaseInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/BaseInputFactory.java new file mode 100644 index 00000000000..7b79d01dc53 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/BaseInputFactory.java @@ -0,0 +1,66 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.projectanalysis.issue; + +import java.util.Collections; +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.core.issue.tracking.LazyInput; +import org.sonar.core.issue.tracking.LineHashSequence; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; + +public abstract class BaseInputFactory { + private static final LineHashSequence EMPTY_LINE_HASH_SEQUENCE = new LineHashSequence(Collections.emptyList()); + + abstract Input<DefaultIssue> create(Component component); + + abstract static class BaseLazyInput extends LazyInput<DefaultIssue> { + private final DbClient dbClient; + final Component component; + final String effectiveUuid; + + BaseLazyInput(DbClient dbClient, Component component, @Nullable MovedFilesRepository.OriginalFile originalFile) { + this.dbClient = dbClient; + this.component = component; + this.effectiveUuid = originalFile == null ? component.getUuid() : originalFile.getUuid(); + } + + @Override + protected LineHashSequence loadLineHashSequence() { + if (component.getType() != Component.Type.FILE) { + return EMPTY_LINE_HASH_SEQUENCE; + } + + try (DbSession session = dbClient.openSession(false)) { + List<String> hashes = dbClient.fileSourceDao().selectLineHashes(session, effectiveUuid); + if (hashes == null || hashes.isEmpty()) { + return EMPTY_LINE_HASH_SEQUENCE; + } + return new LineHashSequence(hashes); + } + } + + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitor.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitor.java index 88649984193..506c2aedece 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitor.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitor.java @@ -57,7 +57,7 @@ public class CloseIssuesOnRemovedComponentsVisitor extends TypeAwareVisitorAdapt DiskCache<DefaultIssue>.DiskAppender cacheAppender = issueCache.newAppender(); try { for (String deletedComponentUuid : deletedComponentUuids) { - List<DefaultIssue> issues = issuesLoader.loadForComponentUuid(deletedComponentUuid); + List<DefaultIssue> issues = issuesLoader.loadOpenIssues(deletedComponentUuid); for (DefaultIssue issue : issues) { issue.setBeingClosed(true); // TODO should be renamed diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactory.java new file mode 100644 index 00000000000..d6ad6ea61b9 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactory.java @@ -0,0 +1,56 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.projectanalysis.issue; + +import java.util.List; +import javax.annotation.Nullable; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.db.DbClient; + +public class ClosedIssuesInputFactory extends BaseInputFactory { + private final ComponentIssuesLoader issuesLoader; + private final DbClient dbClient; + private final MovedFilesRepository movedFilesRepository; + + public ClosedIssuesInputFactory(ComponentIssuesLoader issuesLoader, DbClient dbClient, MovedFilesRepository movedFilesRepository) { + this.issuesLoader = issuesLoader; + this.dbClient = dbClient; + this.movedFilesRepository = movedFilesRepository; + } + + public Input<DefaultIssue> create(Component component) { + return new ClosedIssuesLazyInput(dbClient, component, movedFilesRepository.getOriginalFile(component).orNull()); + } + + private class ClosedIssuesLazyInput extends BaseLazyInput { + + ClosedIssuesLazyInput(DbClient dbClient, Component component, @Nullable MovedFilesRepository.OriginalFile originalFile) { + super(dbClient, component, originalFile); + } + + @Override + protected List<DefaultIssue> loadIssues() { + return issuesLoader.loadClosedIssues(effectiveUuid); + } + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java index 13bb7eabb58..f44149c50e9 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java @@ -19,17 +19,24 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import com.google.common.collect.ImmutableList; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; +import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.issue.IssueChangeDto; +import org.sonar.db.issue.IssueDto; import org.sonar.db.issue.IssueMapper; -import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.groupingBy; @@ -41,34 +48,48 @@ public class ComponentIssuesLoader { private final ActiveRulesHolder activeRulesHolder; public ComponentIssuesLoader(DbClient dbClient, RuleRepository ruleRepository, ActiveRulesHolder activeRulesHolder) { - this.activeRulesHolder = activeRulesHolder; this.dbClient = dbClient; + this.activeRulesHolder = activeRulesHolder; this.ruleRepository = ruleRepository; } - public List<DefaultIssue> loadForComponentUuid(String componentUuid) { + public List<DefaultIssue> loadOpenIssues(String componentUuid) { try (DbSession dbSession = dbClient.openSession(false)) { - return loadForComponentUuid(componentUuid, dbSession); + return loadOpenIssues(componentUuid, dbSession); } } - public List<DefaultIssue> loadForComponentUuidWithChanges(String componentUuid) { + public List<DefaultIssue> loadOpenIssuesWithChanges(String componentUuid) { try (DbSession dbSession = dbClient.openSession(false)) { - List<DefaultIssue> result = loadForComponentUuid(componentUuid, dbSession); + List<DefaultIssue> result = loadOpenIssues(componentUuid, dbSession); - Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao() - .selectByIssueKeys(dbSession, result.stream().map(DefaultIssue::key).collect(toList())) - .stream() - .collect(groupingBy(IssueChangeDto::getIssueKey)); + return loadChanges(dbSession, result); + } + } - return result - .stream() - .peek(i -> setChanges(changeDtoByIssueKey, i)) - .collect(toList()); + public void loadChanges(Collection<DefaultIssue> issues) { + if (issues.isEmpty()) { + return; } + + try (DbSession dbSession = dbClient.openSession(false)) { + loadChanges(dbSession, issues); + } + } + + public List<DefaultIssue> loadChanges(DbSession dbSession, Collection<DefaultIssue> issues) { + Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao() + .selectByIssueKeys(dbSession, issues.stream().map(DefaultIssue::key).collect(toList())) + .stream() + .collect(groupingBy(IssueChangeDto::getIssueKey)); + + return issues + .stream() + .peek(i -> setChanges(changeDtoByIssueKey, i)) + .collect(toList()); } - private List<DefaultIssue> loadForComponentUuid(String componentUuid, DbSession dbSession) { + private List<DefaultIssue> loadOpenIssues(String componentUuid, DbSession dbSession) { List<DefaultIssue> result = new ArrayList<>(); dbSession.getMapper(IssueMapper.class).scrollNonClosedByComponentUuid(componentUuid, resultContext -> { DefaultIssue issue = (resultContext.getResultObject()).toDefaultIssue(); @@ -84,10 +105,10 @@ public class ComponentIssuesLoader { issue.setSelectedAt(System.currentTimeMillis()); result.add(issue); }); - return result; + return ImmutableList.copyOf(result); } - public static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) { + private static void setChanges(Map<String, List<IssueChangeDto>> changeDtoByIssueKey, DefaultIssue i) { changeDtoByIssueKey.computeIfAbsent(i.key(), k -> emptyList()).forEach(c -> { switch (c.getChangeType()) { case IssueChangeDto.TYPE_FIELD_CHANGE: @@ -105,4 +126,57 @@ public class ComponentIssuesLoader { private boolean isActive(RuleKey ruleKey) { return activeRulesHolder.get(ruleKey).isPresent(); } + + /** + * Load closed issues for the specified Component, which have at least one line diff in changelog AND are + * neither hotspots nor manual vulnerabilities. + * <p> + * Closed issues do not have a line number in DB (it is unset when the issue is closed), this method + * returns {@link DefaultIssue} objects which line number is populated from the most recent diff logging + * the removal of the line. Closed issues which do not have such diff are not loaded. + */ + public List<DefaultIssue> loadClosedIssues(String componentUuid) { + try (DbSession dbSession = dbClient.openSession(false)) { + return loadClosedIssues(componentUuid, dbSession); + } + } + + private static List<DefaultIssue> loadClosedIssues(String componentUuid, DbSession dbSession) { + ClosedIssuesResultHandler handler = new ClosedIssuesResultHandler(); + dbSession.getMapper(IssueMapper.class).scrollClosedByComponentUuid(componentUuid, handler); + return ImmutableList.copyOf(handler.issues); + } + + private static class ClosedIssuesResultHandler implements ResultHandler<IssueDto> { + private final List<DefaultIssue> issues = new ArrayList<>(); + private String previousIssueKey = null; + + @Override + public void handleResult(ResultContext<? extends IssueDto> resultContext) { + IssueDto resultObject = resultContext.getResultObject(); + + // issue are ordered by most recent change first, only the first row for a given issue is of interest + if (previousIssueKey != null && previousIssueKey.equals(resultObject.getKey())) { + return; + } + + FieldDiffs fieldDiffs = FieldDiffs.parse(resultObject.getLineChangeData() + .orElseThrow(() -> new IllegalStateException("Line Change data should be populated"))); + Optional<Integer> line = Optional.ofNullable(fieldDiffs.get("line")) + .map(diff -> (String) diff.oldValue()) + .filter(str -> !str.isEmpty()) + .map(Integer::parseInt); + if (!line.isPresent()) { + return; + } + + previousIssueKey = resultObject.getKey(); + DefaultIssue issue = resultObject.toDefaultIssue(); + issue.setLine(line.get()); + // FIXME + issue.setSelectedAt(System.currentTimeMillis()); + + issues.add(issue); + } + } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java index 3d7a98e91fb..26d63bb95dc 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java @@ -88,34 +88,34 @@ public class IssueLifecycle { public void copyExistingOpenIssueFromLongLivingBranch(DefaultIssue raw, DefaultIssue base, String fromLongBranchName) { raw.setKey(Uuids.create()); raw.setNew(false); - copyIssueAttributes(raw, base); + copyAttributesOfIssueFromOtherBranch(raw, base); raw.setFieldChange(changeContext, IssueFieldsSetter.FROM_LONG_BRANCH, fromLongBranchName, analysisMetadataHolder.getBranch().getName()); } public void mergeConfirmedOrResolvedFromShortLivingBranch(DefaultIssue raw, DefaultIssue base, String fromShortBranchName) { - copyIssueAttributes(raw, base); + copyAttributesOfIssueFromOtherBranch(raw, base); raw.setFieldChange(changeContext, IssueFieldsSetter.FROM_SHORT_BRANCH, fromShortBranchName, analysisMetadataHolder.getBranch().getName()); } - private void copyIssueAttributes(DefaultIssue to, DefaultIssue from) { + private void copyAttributesOfIssueFromOtherBranch(DefaultIssue to, DefaultIssue from) { to.setCopied(true); copyFields(to, from); if (from.manualSeverity()) { to.setManualSeverity(true); to.setSeverity(from.severity()); } - copyChanges(to, from); + copyChangesOfIssueFromOtherBranch(to, from); } - private static void copyChanges(DefaultIssue raw, DefaultIssue base) { - base.defaultIssueComments().forEach(c -> raw.addComment(copy(raw.key(), c))); - base.changes().forEach(c -> copy(raw.key(), c).ifPresent(raw::addChange)); + private static void copyChangesOfIssueFromOtherBranch(DefaultIssue raw, DefaultIssue base) { + base.defaultIssueComments().forEach(c -> raw.addComment(copyComment(raw.key(), c))); + base.changes().forEach(c -> copyFieldDiffOfIssueFromOtherBranch(raw.key(), c).ifPresent(raw::addChange)); } /** * Copy a comment from another issue */ - private static DefaultIssueComment copy(String issueKey, DefaultIssueComment c) { + private static DefaultIssueComment copyComment(String issueKey, DefaultIssueComment c) { DefaultIssueComment comment = new DefaultIssueComment(); comment.setIssueKey(issueKey); comment.setKey(Uuids.create()); @@ -129,7 +129,7 @@ public class IssueLifecycle { /** * Copy a diff from another issue */ - private static Optional<FieldDiffs> copy(String issueKey, FieldDiffs c) { + private static Optional<FieldDiffs> copyFieldDiffOfIssueFromOtherBranch(String issueKey, FieldDiffs c) { FieldDiffs result = new FieldDiffs(); result.setIssueKey(issueKey); result.setUserUuid(c.userUuid()); @@ -149,6 +149,7 @@ public class IssueLifecycle { raw.setNew(false); setType(raw); copyFields(raw, base); + base.changes().forEach(raw::addChange); if (raw.isFromHotspot() != base.isFromHotspot()) { // This is to force DB update of the issue raw.setChanged(true); diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecution.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecution.java index 745e79c8ea9..dbd8b08d872 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecution.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecution.java @@ -23,7 +23,6 @@ import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Tracker; import org.sonar.core.issue.tracking.Tracking; -import org.sonar.ce.task.projectanalysis.component.Component; public class MergeBranchTrackerExecution { private final TrackerRawInputFactory rawInputFactory; @@ -38,6 +37,6 @@ public class MergeBranchTrackerExecution { } public Tracking<DefaultIssue, DefaultIssue> track(Component component) { - return tracker.track(rawInputFactory.create(component), mergeInputFactory.create(component)); + return tracker.trackNonClosed(rawInputFactory.create(component), mergeInputFactory.create(component)); } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssuesLoader.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssuesLoader.java index a75f1608c74..b943350ce1c 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssuesLoader.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssuesLoader.java @@ -31,24 +31,24 @@ import org.sonar.core.issue.DefaultIssue; import org.sonar.db.DbClient; import org.sonar.db.DbSession; import org.sonar.db.component.ComponentDto; -import org.sonar.db.issue.IssueChangeDto; import org.sonar.db.issue.IssueDto; import org.sonar.db.issue.ShortBranchIssueDto; -import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.ShortBranchComponentsWithIssues; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toMap; import static org.sonar.api.utils.DateUtils.longToDate; +import static org.sonar.core.util.stream.MoreCollectors.toList; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; public class ShortBranchIssuesLoader { private final ShortBranchComponentsWithIssues shortBranchComponentsWithIssues; private final DbClient dbClient; + private final ComponentIssuesLoader componentIssuesLoader; - public ShortBranchIssuesLoader(ShortBranchComponentsWithIssues shortBranchComponentsWithIssues, DbClient dbClient) { + public ShortBranchIssuesLoader(ShortBranchComponentsWithIssues shortBranchComponentsWithIssues, DbClient dbClient, + ComponentIssuesLoader componentIssuesLoader) { this.shortBranchComponentsWithIssues = shortBranchComponentsWithIssues; this.dbClient = dbClient; + this.componentIssuesLoader = componentIssuesLoader; } public Collection<ShortBranchIssue> loadCandidateIssuesForMergingInTargetBranch(Component component) { @@ -57,6 +57,7 @@ public class ShortBranchIssuesLoader { if (uuids.isEmpty()) { return Collections.emptyList(); } + try (DbSession session = dbClient.openSession(false)) { return dbClient.issueDao().selectOpenByComponentUuids(session, uuids) .stream() @@ -74,17 +75,16 @@ public class ShortBranchIssuesLoader { if (lightIssues.isEmpty()) { return Collections.emptyMap(); } + Map<String, ShortBranchIssue> issuesByKey = lightIssues.stream().collect(Collectors.toMap(ShortBranchIssue::getKey, i -> i)); try (DbSession session = dbClient.openSession(false)) { - - Map<String, List<IssueChangeDto>> changeDtoByIssueKey = dbClient.issueChangeDao() - .selectByIssueKeys(session, issuesByKey.keySet()).stream().collect(groupingBy(IssueChangeDto::getIssueKey)); - - return dbClient.issueDao().selectByKeys(session, issuesByKey.keySet()) + List<DefaultIssue> issues = dbClient.issueDao().selectByKeys(session, issuesByKey.keySet()) .stream() .map(IssueDto::toDefaultIssue) - .peek(i -> ComponentIssuesLoader.setChanges(changeDtoByIssueKey, i)) - .collect(toMap(i -> issuesByKey.get(i.key()), i -> i)); + .collect(toList(issuesByKey.size())); + componentIssuesLoader.loadChanges(session, issues); + return issues.stream() + .collect(uniqueIndex(i -> issuesByKey.get(i.key()), i -> i, issues.size())); } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchTrackerExecution.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchTrackerExecution.java index 6dca1decd57..6893b0d24a7 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchTrackerExecution.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchTrackerExecution.java @@ -46,12 +46,12 @@ public class ShortBranchTrackerExecution { Input<DefaultIssue> baseInput = baseInputFactory.create(component); Input<DefaultIssue> mergeInput = mergeInputFactory.create(component); - Tracking<DefaultIssue, DefaultIssue> mergeTracking = tracker.track(rawInput, mergeInput); + Tracking<DefaultIssue, DefaultIssue> mergeTracking = tracker.trackNonClosed(rawInput, mergeInput); List<DefaultIssue> unmatchedRaws = mergeTracking.getUnmatchedRaws().collect(MoreCollectors.toList()); Input<DefaultIssue> unmatchedRawInput = new DefaultTrackingInput(unmatchedRaws, rawInput.getLineHashSequence(), rawInput.getBlockHashSequence()); // do second tracking with base branch using raws issues that are still unmatched - return tracker.track(unmatchedRawInput, baseInput); + return tracker.trackNonClosed(unmatchedRawInput, baseInput); } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java index 0ba69ce041f..e55972ee647 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java @@ -19,26 +19,19 @@ */ package org.sonar.ce.task.projectanalysis.issue; -import java.util.Collections; import java.util.List; -import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository; +import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository.OriginalFile; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Input; -import org.sonar.core.issue.tracking.LazyInput; -import org.sonar.core.issue.tracking.LineHashSequence; import org.sonar.db.DbClient; -import org.sonar.db.DbSession; -import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository; -import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository.OriginalFile; /** * Factory of {@link Input} of base data for issue tracking. Data are lazy-loaded. */ -public class TrackerBaseInputFactory { - private static final LineHashSequence EMPTY_LINE_HASH_SEQUENCE = new LineHashSequence(Collections.emptyList()); +public class TrackerBaseInputFactory extends BaseInputFactory { private final ComponentIssuesLoader issuesLoader; private final DbClient dbClient; @@ -51,37 +44,18 @@ public class TrackerBaseInputFactory { } public Input<DefaultIssue> create(Component component) { - return new BaseLazyInput(component, movedFilesRepository.getOriginalFile(component).orNull()); + return new TrackerBaseLazyInput(dbClient, component, movedFilesRepository.getOriginalFile(component).orNull()); } - private class BaseLazyInput extends LazyInput<DefaultIssue> { - private final Component component; - @CheckForNull - private final String effectiveUuid; - - private BaseLazyInput(Component component, @Nullable OriginalFile originalFile) { - this.component = component; - this.effectiveUuid = originalFile == null ? component.getUuid() : originalFile.getUuid(); - } - - @Override - protected LineHashSequence loadLineHashSequence() { - if (component.getType() != Component.Type.FILE) { - return EMPTY_LINE_HASH_SEQUENCE; - } + private class TrackerBaseLazyInput extends BaseLazyInput { - try (DbSession session = dbClient.openSession(false)) { - List<String> hashes = dbClient.fileSourceDao().selectLineHashes(session, effectiveUuid); - if (hashes == null || hashes.isEmpty()) { - return EMPTY_LINE_HASH_SEQUENCE; - } - return new LineHashSequence(hashes); - } + private TrackerBaseLazyInput(DbClient dbClient, Component component, @Nullable OriginalFile originalFile) { + super(dbClient, component, originalFile); } @Override protected List<DefaultIssue> loadIssues() { - return issuesLoader.loadForComponentUuid(effectiveUuid); + return issuesLoader.loadOpenIssues(effectiveUuid); } } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecution.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecution.java index deb980b43d6..29ab66bc3aa 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecution.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecution.java @@ -19,26 +19,54 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import java.util.Set; +import org.sonar.api.issue.Issue; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.core.issue.tracking.NonClosedTracking; import org.sonar.core.issue.tracking.Tracker; import org.sonar.core.issue.tracking.Tracking; -import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.core.util.stream.MoreCollectors; public class TrackerExecution { - protected final TrackerBaseInputFactory baseInputFactory; - protected final TrackerRawInputFactory rawInputFactory; - protected final Tracker<DefaultIssue, DefaultIssue> tracker; + private final TrackerBaseInputFactory baseInputFactory; + private final TrackerRawInputFactory rawInputFactory; + private final ClosedIssuesInputFactory closedIssuesInputFactory; + private final Tracker<DefaultIssue, DefaultIssue> tracker; + private final ComponentIssuesLoader componentIssuesLoader; public TrackerExecution(TrackerBaseInputFactory baseInputFactory, TrackerRawInputFactory rawInputFactory, - Tracker<DefaultIssue, DefaultIssue> tracker) { + ClosedIssuesInputFactory closedIssuesInputFactory, Tracker<DefaultIssue, DefaultIssue> tracker, + ComponentIssuesLoader componentIssuesLoader) { this.baseInputFactory = baseInputFactory; this.rawInputFactory = rawInputFactory; + this.closedIssuesInputFactory = closedIssuesInputFactory; this.tracker = tracker; + this.componentIssuesLoader = componentIssuesLoader; } public Tracking<DefaultIssue, DefaultIssue> track(Component component) { - return tracker.track(rawInputFactory.create(component), baseInputFactory.create(component)); + Input<DefaultIssue> rawInput = rawInputFactory.create(component); + Input<DefaultIssue> openBaseIssuesInput = baseInputFactory.create(component); + NonClosedTracking<DefaultIssue, DefaultIssue> openIssueTracking = tracker.trackNonClosed(rawInput, openBaseIssuesInput); + if (openIssueTracking.isComplete()) { + return openIssueTracking; + } + + Input<DefaultIssue> closedIssuesBaseInput = closedIssuesInputFactory.create(component); + Tracking<DefaultIssue, DefaultIssue> closedIssuesTracking = tracker.trackClosed(openIssueTracking, closedIssuesBaseInput); + + // changes of closed issues need to be loaded in order to: + // - compute right transition from workflow + // - recover fields values from before they were closed + Set<DefaultIssue> matchesClosedIssues = closedIssuesTracking.getMatchedRaws().values().stream() + .filter(t -> Issue.STATUS_CLOSED.equals(t.getStatus())) + .collect(MoreCollectors.toSet()); + componentIssuesLoader.loadChanges(matchesClosedIssues); + + return closedIssuesTracking; } + } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java index 8334e26a69f..e5c2e42f5b8 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java @@ -30,8 +30,6 @@ import org.sonar.core.issue.tracking.LazyInput; import org.sonar.core.issue.tracking.LineHashSequence; import org.sonar.db.DbClient; import org.sonar.db.DbSession; -import org.sonar.ce.task.projectanalysis.component.Component; -import org.sonar.ce.task.projectanalysis.component.MergeBranchComponentUuids; public class TrackerMergeBranchInputFactory { private static final LineHashSequence EMPTY_LINE_HASH_SEQUENCE = new LineHashSequence(Collections.emptyList()); @@ -81,7 +79,7 @@ public class TrackerMergeBranchInputFactory { if (mergeBranchComponentUuid == null) { return Collections.emptyList(); } - return mergeIssuesLoader.loadForComponentUuidWithChanges(mergeBranchComponentUuid); + return mergeIssuesLoader.loadOpenIssuesWithChanges(mergeBranchComponentUuid); } } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.java index 54a1bfd47cc..ea836e621f6 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.java @@ -67,7 +67,7 @@ public class CloseIssuesOnRemovedComponentsVisitorTest { when(componentsWithUnprocessedIssues.getUuids()).thenReturn(newHashSet(fileUuid)); DefaultIssue issue = new DefaultIssue().setKey(issueUuid); - when(issuesLoader.loadForComponentUuid(fileUuid)).thenReturn(Collections.singletonList(issue)); + when(issuesLoader.loadOpenIssues(fileUuid)).thenReturn(Collections.singletonList(issue)); underTest.visit(ReportComponent.builder(PROJECT, 1).build()); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactoryTest.java new file mode 100644 index 00000000000..0c8dc4901df --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactoryTest.java @@ -0,0 +1,100 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.projectanalysis.issue; + +import com.google.common.base.Optional; +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportComponent; +import org.sonar.ce.task.projectanalysis.filemove.MovedFilesRepository; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.db.DbClient; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +public class ClosedIssuesInputFactoryTest { + private ComponentIssuesLoader issuesLoader = mock(ComponentIssuesLoader.class); + private DbClient dbClient = mock(DbClient.class); + private MovedFilesRepository movedFilesRepository = mock(MovedFilesRepository.class); + private ClosedIssuesInputFactory underTest = new ClosedIssuesInputFactory(issuesLoader, dbClient, movedFilesRepository); + + @Test + public void underTest_returns_inputFactory_loading_closed_issues_only_when_getIssues_is_called() { + String componentUuid = randomAlphanumeric(12); + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).setUuid(componentUuid).build(); + when(movedFilesRepository.getOriginalFile(component)).thenReturn(Optional.absent()); + + Input<DefaultIssue> input = underTest.create(component); + + verifyZeroInteractions(dbClient, issuesLoader); + + List<DefaultIssue> issues = ImmutableList.of(new DefaultIssue(), new DefaultIssue()); + when(issuesLoader.loadClosedIssues(componentUuid)).thenReturn(issues); + + assertThat(input.getIssues()).isSameAs(issues); + } + + @Test + public void underTest_returns_inputFactory_loading_closed_issues_from_moved_component_when_present() { + String componentUuid = randomAlphanumeric(12); + String originalComponentUuid = randomAlphanumeric(12); + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).setUuid(componentUuid).build(); + when(movedFilesRepository.getOriginalFile(component)) + .thenReturn(Optional.of(new MovedFilesRepository.OriginalFile(1, originalComponentUuid, randomAlphanumeric(2)))); + + Input<DefaultIssue> input = underTest.create(component); + + verifyZeroInteractions(dbClient, issuesLoader); + + List<DefaultIssue> issues = ImmutableList.of(); + when(issuesLoader.loadClosedIssues(originalComponentUuid)).thenReturn(issues); + + assertThat(input.getIssues()).isSameAs(issues); + } + + @Test + public void underTest_returns_inputFactory_which_caches_loaded_issues() { + String componentUuid = randomAlphanumeric(12); + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).setUuid(componentUuid).build(); + when(movedFilesRepository.getOriginalFile(component)).thenReturn(Optional.absent()); + + Input<DefaultIssue> input = underTest.create(component); + + verifyZeroInteractions(dbClient, issuesLoader); + + List<DefaultIssue> issues = ImmutableList.of(new DefaultIssue()); + when(issuesLoader.loadClosedIssues(componentUuid)).thenReturn(issues); + + assertThat(input.getIssues()).isSameAs(issues); + + reset(issuesLoader); + + assertThat(input.getIssues()).isSameAs(issues); + verifyZeroInteractions(issuesLoader); + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java new file mode 100644 index 00000000000..9bcc114976e --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java @@ -0,0 +1,85 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.projectanalysis.issue; + +import java.util.Date; +import java.util.List; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.System2; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs; +import org.sonar.db.DbClient; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.issue.IssueDto; +import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.utils.DateUtils.addDays; + +public class ComponentIssuesLoaderTest { + @Rule + public DbTester dbTester = DbTester.create(System2.INSTANCE); + + private DbClient dbClient = dbTester.getDbClient(); + private ComponentIssuesLoader underTest = new ComponentIssuesLoader(dbClient, + null /* not used in loadClosedIssues */, null /* not used in loadClosedIssues */); + + @Test + public void loadClosedIssues_returns_single_DefaultIssue_by_issue_based_on_first_row() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + RuleDefinitionDto rule = dbTester.rules().insert(t -> t.setType(RuleType.CODE_SMELL)); + IssueDto issue = dbTester.issues().insert(rule, project, file, t -> t.setStatus(Issue.STATUS_CLOSED).setIsFromHotspot(false)); + Date creationDate = new Date(); + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(addDays(creationDate, -5)).setDiff("line", 10, "")); + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(creationDate).setDiff("line", 20, "")); + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(addDays(creationDate, -10)).setDiff("line", 30, "")); + + List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid()); + + assertThat(defaultIssues).hasSize(1); + assertThat(defaultIssues.iterator().next().getLine()).isEqualTo(20); + } + + @Test + public void loadClosedIssues_returns_single_DefaultIssue_ignoring_lines_without_old_values() { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + RuleDefinitionDto rule = dbTester.rules().insert(t -> t.setType(RuleType.CODE_SMELL)); + IssueDto issue = dbTester.issues().insert(rule, project, file, t -> t.setStatus(Issue.STATUS_CLOSED).setIsFromHotspot(false)); + Date creationDate = new Date(); + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(addDays(creationDate, -5)).setDiff("line", null, "")); + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(creationDate).setDiff("line", 20, null)); // if new value is null, neither old nor new is stored in DB + dbTester.issues().insertFieldDiffs(issue, new FieldDiffs().setCreationDate(addDays(creationDate, -10)).setDiff("line", 30, "")); + + List<DefaultIssue> defaultIssues = underTest.loadClosedIssues(file.uuid()); + + assertThat(defaultIssues).hasSize(1); + assertThat(defaultIssues.iterator().next().getLine()).isEqualTo(30); + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java index c209bb445a3..bc4fe0f02b0 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java @@ -49,6 +49,7 @@ import org.sonar.ce.task.projectanalysis.source.SourceLinesHashRepository; import org.sonar.ce.task.projectanalysis.source.SourceLinesRepositoryRule; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Tracker; +import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; @@ -105,8 +106,6 @@ public class IntegrateIssuesVisitorTest { public RuleRepositoryRule ruleRepositoryRule = new RuleRepositoryRule(); @Rule public SourceLinesRepositoryRule fileSourceRepository = new SourceLinesRepositoryRule(); - @Rule - public RuleRepositoryRule ruleRepository = new RuleRepositoryRule(); private AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class); private IssueFilter issueFilter = mock(IssueFilter.class); @@ -137,11 +136,13 @@ public class IntegrateIssuesVisitorTest { defaultIssueCaptor = ArgumentCaptor.forClass(DefaultIssue.class); when(movedFilesRepository.getOriginalFile(any(Component.class))).thenReturn(Optional.absent()); - TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, new CommonRuleEngineImpl(), issueFilter, ruleRepository, + DbClient dbClient = dbTester.getDbClient(); + TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, new CommonRuleEngineImpl(), issueFilter, ruleRepositoryRule, activeRulesHolder); - TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbTester.getDbClient(), movedFilesRepository); - TrackerMergeBranchInputFactory mergeInputFactory = new TrackerMergeBranchInputFactory(issuesLoader, mergeBranchComponentsUuids, dbTester.getDbClient()); - tracker = new TrackerExecution(baseInputFactory, rawInputFactory, new Tracker<>()); + TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbClient, movedFilesRepository); + TrackerMergeBranchInputFactory mergeInputFactory = new TrackerMergeBranchInputFactory(issuesLoader, mergeBranchComponentsUuids, dbClient); + ClosedIssuesInputFactory closedIssuesInputFactory = new ClosedIssuesInputFactory(issuesLoader, dbClient, movedFilesRepository); + tracker = new TrackerExecution(baseInputFactory, rawInputFactory, closedIssuesInputFactory, new Tracker<>(), new ComponentIssuesLoader(dbClient, ruleRepositoryRule, activeRulesHolder)); shortBranchTracker = new ShortBranchTrackerExecution(baseInputFactory, rawInputFactory, mergeInputFactory, new Tracker<>()); mergeBranchTracker = new MergeBranchTrackerExecution(rawInputFactory, mergeInputFactory, new Tracker<>()); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java index d629e26f98b..2c9a3a2bc30 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java @@ -39,6 +39,7 @@ import org.sonar.server.issue.workflow.IssueWorkflow; import static com.google.common.collect.Lists.newArrayList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; import static org.assertj.core.groups.Tuple.tuple; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -274,7 +275,9 @@ public class IssueLifecycleTest { .setGap(15d) .setEffort(Duration.create(15L)) .setManualSeverity(false) - .setLocations(issueLocations); + .setLocations(issueLocations) + .addChange(new FieldDiffs().setDiff("foo", "bar", "donut")) + .addChange(new FieldDiffs().setDiff("file", "A", "B")); when(debtCalculator.calculate(raw)).thenReturn(DEFAULT_DURATION); @@ -293,6 +296,11 @@ public class IssueLifecycleTest { assertThat(raw.isOnDisabledRule()).isTrue(); assertThat(raw.selectedAt()).isEqualTo(1000L); assertThat(raw.isChanged()).isFalse(); + assertThat(raw.changes()).hasSize(2); + assertThat(raw.changes().get(0).diffs()) + .containsOnly(entry("foo", new FieldDiffs.Diff("bar", "donut"))); + assertThat(raw.changes().get(1).diffs()) + .containsOnly(entry("file", new FieldDiffs.Diff("A", "B"))); verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext); verify(updater).setPastLine(raw, 10); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java index d3b3e538ae0..ccad3e2498f 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java @@ -26,8 +26,8 @@ import org.mockito.MockitoAnnotations; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.tracking.Input; +import org.sonar.core.issue.tracking.NonClosedTracking; import org.sonar.core.issue.tracking.Tracker; -import org.sonar.core.issue.tracking.Tracking; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -55,10 +55,10 @@ public class MergeBranchTrackerExecutionTest { public void testTracking() { Input<DefaultIssue> rawInput = mock(Input.class); Input<DefaultIssue> mergeInput = mock(Input.class); - Tracking<DefaultIssue, DefaultIssue> result = mock(Tracking.class); + NonClosedTracking<DefaultIssue, DefaultIssue> result = mock(NonClosedTracking.class); when(rawInputFactory.create(component)).thenReturn(rawInput); when(mergeInputFactory.create(component)).thenReturn(mergeInput); - when(tracker.track(rawInput, mergeInput)).thenReturn(result); + when(tracker.trackNonClosed(rawInput, mergeInput)).thenReturn(result); assertThat(underTest.track(component)).isEqualTo(result); } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssueMergerTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssueMergerTest.java index 093c985529a..2fabe28f1b3 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssueMergerTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssueMergerTest.java @@ -37,6 +37,7 @@ import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule; import org.sonar.core.issue.DefaultIssue; import org.sonar.core.issue.FieldDiffs; import org.sonar.core.issue.tracking.SimpleTracker; +import org.sonar.db.DbClient; import org.sonar.db.DbTester; import org.sonar.db.component.BranchType; import org.sonar.db.component.ComponentDto; @@ -92,7 +93,8 @@ public class ShortBranchIssueMergerTest { @Before public void setUp() { MockitoAnnotations.initMocks(this); - copier = new ShortBranchIssueMerger(new ShortBranchIssuesLoader(new ShortBranchComponentsWithIssues(treeRootHolder, db.getDbClient()), db.getDbClient()), tracker, + DbClient dbClient = db.getDbClient(); + copier = new ShortBranchIssueMerger(new ShortBranchIssuesLoader(new ShortBranchComponentsWithIssues(treeRootHolder, dbClient), dbClient, new ComponentIssuesLoader(dbClient, null, null)), tracker, issueLifecycle); projectDto = db.components().insertMainBranch(p -> p.setDbKey(PROJECT_KEY).setUuid(PROJECT_UUID)); branch1Dto = db.components().insertProjectBranch(projectDto, b -> b.setKey("myBranch1") diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactoryTest.java index d91235cc066..0b747be5f25 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactoryTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactoryTest.java @@ -80,7 +80,7 @@ public class TrackerBaseInputFactoryTest { public void create_returns_Input_which_retrieves_issues_of_specified_file_component_when_it_has_no_original_file() { underTest.create(FILE).getIssues(); - verify(issuesLoader).loadForComponentUuid(FILE_UUID); + verify(issuesLoader).loadOpenIssues(FILE_UUID); } @Test @@ -92,7 +92,7 @@ public class TrackerBaseInputFactoryTest { underTest.create(FILE).getIssues(); - verify(issuesLoader).loadForComponentUuid(originalUuid); - verify(issuesLoader, times(0)).loadForComponentUuid(FILE_UUID); + verify(issuesLoader).loadOpenIssues(originalUuid); + verify(issuesLoader, times(0)).loadOpenIssues(FILE_UUID); } } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecutionTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecutionTest.java new file mode 100644 index 00000000000..d7551a1b5b0 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecutionTest.java @@ -0,0 +1,122 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.ce.task.projectanalysis.issue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Random; +import java.util.Set; +import java.util.stream.IntStream; +import org.junit.Test; +import org.sonar.api.issue.Issue; +import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.component.ReportComponent; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.tracking.Input; +import org.sonar.core.issue.tracking.NonClosedTracking; +import org.sonar.core.issue.tracking.Tracker; +import org.sonar.core.issue.tracking.Tracking; + +import static java.util.stream.Collectors.toSet; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; + +public class TrackerExecutionTest { + private final TrackerRawInputFactory rawInputFactory = mock(TrackerRawInputFactory.class); + private final TrackerBaseInputFactory baseInputFactory = mock(TrackerBaseInputFactory.class); + private final ClosedIssuesInputFactory closedIssuesInputFactory = mock(ClosedIssuesInputFactory.class); + private final Tracker<DefaultIssue, DefaultIssue> tracker = mock(Tracker.class); + private final ComponentIssuesLoader componentIssuesLoader = mock(ComponentIssuesLoader.class); + + private TrackerExecution underTest = new TrackerExecution(baseInputFactory, rawInputFactory, closedIssuesInputFactory, tracker, componentIssuesLoader); + + private Input<DefaultIssue> rawInput = mock(Input.class); + private Input<DefaultIssue> openIssuesInput = mock(Input.class); + private Input<DefaultIssue> closedIssuesInput = mock(Input.class); + private NonClosedTracking<DefaultIssue, DefaultIssue> nonClosedTracking = mock(NonClosedTracking.class); + private Tracking<DefaultIssue, DefaultIssue> closedTracking = mock(Tracking.class); + + @Test + public void track_tracks_only_nonClosed_issues_if_tracking_returns_complete_from_Tracker() { + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).build(); + when(rawInputFactory.create(component)).thenReturn(rawInput); + when(baseInputFactory.create(component)).thenReturn(openIssuesInput); + when(closedIssuesInputFactory.create(any())).thenThrow(new IllegalStateException("closedIssuesInputFactory should not be called")); + when(nonClosedTracking.isComplete()).thenReturn(true); + when(tracker.trackNonClosed(rawInput, openIssuesInput)).thenReturn(nonClosedTracking); + when(tracker.trackClosed(any(), any())).thenThrow(new IllegalStateException("trackClosed should not be called")); + + Tracking<DefaultIssue, DefaultIssue> tracking = underTest.track(component); + + assertThat(tracking).isSameAs(nonClosedTracking); + verify(tracker).trackNonClosed(rawInput, openIssuesInput); + verifyNoMoreInteractions(tracker); + } + + @Test + public void track_tracks_nonClosed_issues_and_then_closedOnes_if_tracking_returns_incomplete() { + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).build(); + when(rawInputFactory.create(component)).thenReturn(rawInput); + when(baseInputFactory.create(component)).thenReturn(openIssuesInput); + when(closedIssuesInputFactory.create(component)).thenReturn(closedIssuesInput); + when(nonClosedTracking.isComplete()).thenReturn(false); + when(tracker.trackNonClosed(rawInput, openIssuesInput)).thenReturn(nonClosedTracking); + when(tracker.trackClosed(nonClosedTracking, closedIssuesInput)).thenReturn(closedTracking); + + Tracking<DefaultIssue, DefaultIssue> tracking = underTest.track(component); + + assertThat(tracking).isSameAs(closedTracking); + verify(tracker).trackNonClosed(rawInput, openIssuesInput); + verify(tracker).trackClosed(nonClosedTracking, closedIssuesInput); + verifyNoMoreInteractions(tracker); + } + + @Test + public void track_loadChanges_on_matched_closed_issues() { + ReportComponent component = ReportComponent.builder(Component.Type.FILE, 1).build(); + when(rawInputFactory.create(component)).thenReturn(rawInput); + when(baseInputFactory.create(component)).thenReturn(openIssuesInput); + when(closedIssuesInputFactory.create(component)).thenReturn(closedIssuesInput); + when(nonClosedTracking.isComplete()).thenReturn(false); + when(tracker.trackNonClosed(rawInput, openIssuesInput)).thenReturn(nonClosedTracking); + when(tracker.trackClosed(nonClosedTracking, closedIssuesInput)).thenReturn(closedTracking); + Set<DefaultIssue> mappedClosedIssues = IntStream.range(1, 2 + new Random().nextInt(2)) + .mapToObj(i -> new DefaultIssue().setKey("closed" + i).setStatus(Issue.STATUS_CLOSED)) + .collect(toSet()); + + ArrayList<DefaultIssue> mappedBaseIssues = new ArrayList<>(mappedClosedIssues); + Issue.STATUSES.stream().filter(t -> !Issue.STATUS_CLOSED.equals(t)).forEach(s -> mappedBaseIssues.add(new DefaultIssue().setKey(s).setStatus(s))); + Collections.shuffle(mappedBaseIssues); + when(closedTracking.getMatchedRaws()).thenReturn(mappedBaseIssues.stream().collect(uniqueIndex(i -> new DefaultIssue().setKey("raw_for_" + i.key()), i -> i))); + + Tracking<DefaultIssue, DefaultIssue> tracking = underTest.track(component); + + assertThat(tracking).isSameAs(closedTracking); + verify(tracker).trackNonClosed(rawInput, openIssuesInput); + verify(tracker).trackClosed(nonClosedTracking, closedIssuesInput); + verify(componentIssuesLoader).loadChanges(mappedClosedIssues); + verifyNoMoreInteractions(tracker); + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java index df8b2f9fb07..252431d5a26 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java @@ -28,6 +28,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import java.io.Serializable; import java.util.Collection; import java.util.Date; +import java.util.Optional; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -97,6 +98,8 @@ public final class IssueDto implements Serializable { private String filePath; private String tags; private boolean isFromHotspot; + // populate only when retrieving closed issue for issue tracking + private String lineChangeData; /** * On batch side, component keys and uuid are useless @@ -698,6 +701,10 @@ public final class IssueDto implements Serializable { return this; } + public Optional<String> getLineChangeData() { + return Optional.ofNullable(lineChangeData); + } + @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index 0f80126c88c..9098c986d15 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -43,7 +43,9 @@ public interface IssueMapper { int updateIfBeforeSelectedDate(IssueDto issue); void scrollNonClosedByComponentUuid(@Param("componentUuid") String componentUuid, ResultHandler<IssueDto> handler); - + + void scrollClosedByComponentUuid(@Param("componentUuid") String componentUuid, ResultHandler<IssueDto> handler); + List<IssueDto> selectNonClosedByComponentUuidExcludingExternals(@Param("componentUuid") String componentUuid); List<IssueDto> selectNonClosedByModuleOrProject(@Param("projectUuid") String projectUuid, @Param("likeModuleUuidPath") String likeModuleUuidPath); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index 3c30811cc82..ad2ff5da259 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -216,6 +216,30 @@ i.issue_type <> 4 and (i.from_hotspot is NULL or i.from_hotspot = ${_false}) </select> + <select id="scrollClosedByComponentUuid" parameterType="String" resultType="Issue" fetchSize="${_scrollFetchSize}" resultSetType="FORWARD_ONLY"> + select + <include refid="issueColumns"/>, + ic.change_data as lineChangeData + from issues i + inner join rules r on + r.id = i.rule_id + inner join projects p on + p.uuid = i.component_uuid + inner join projects root on + root.uuid = i.project_uuid + inner join issue_changes ic on + ic.issue_key = i.kee + and ic.change_type = 'diff' + and ic.change_data like '%line=%' + where + i.component_uuid = #{componentUuid,jdbcType=VARCHAR} + and i.status = 'CLOSED' + and i.issue_type <> 4 + and (i.from_hotspot is NULL or i.from_hotspot = ${_false}) + order by + i.kee, ic.issue_change_creation_date desc + </select> + <select id="selectComponentUuidsOfOpenIssuesForProjectUuid" parameterType="string" resultType="string"> select distinct(i.component_uuid) from issues i diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java index a70313838bd..12cca63fb3f 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java @@ -19,31 +19,56 @@ */ package org.sonar.db.issue; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; +import java.util.stream.IntStream; +import javax.annotation.Nullable; +import org.apache.ibatis.session.ResultContext; +import org.apache.ibatis.session.ResultHandler; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.sonar.api.issue.Issue; +import org.sonar.api.rules.RuleType; +import org.sonar.api.utils.DateUtils; import org.sonar.api.utils.System2; +import org.sonar.core.issue.FieldDiffs; +import org.sonar.core.util.UuidFactoryFast; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; import org.sonar.db.organization.OrganizationDto; +import org.sonar.db.rule.RuleDefinitionDto; import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.RuleTesting; +import static com.google.common.base.Preconditions.checkArgument; +import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; +@RunWith(DataProviderRunner.class) public class IssueMapperTest { @Rule public DbTester dbTester = DbTester.create(System2.INSTANCE); - DbSession dbSession = dbTester.getSession(); + private DbSession dbSession = dbTester.getSession(); - IssueMapper underTest = dbSession.getMapper(IssueMapper.class); + private IssueMapper underTest = dbSession.getMapper(IssueMapper.class); - ComponentDto project, file, file2; - RuleDto rule; + private ComponentDto project, file, file2; + private RuleDto rule; + private Random random = new Random(); @Before public void setUp() throws Exception { @@ -186,7 +211,7 @@ public class IssueMapperTest { underTest.insert(newIssue()); IssueDto dto = newIssue() - .setComponentUuid(file2.uuid()) + .setComponentUuid(file2.uuid()) .setType(3) .setLine(600) .setGap(1.12d) @@ -213,6 +238,249 @@ public class IssueMapperTest { assertThat(result.getUpdatedAt()).isEqualTo(1_500_000_000_000L); } + @Test + public void scrollClosedByComponentUuid_returns_empty_when_no_issue_for_component() { + String componentUuid = randomAlphabetic(10); + RecorderResultHandler resultHandler = new RecorderResultHandler(); + + underTest.scrollClosedByComponentUuid(componentUuid, resultHandler); + + assertThat(resultHandler.issues).isEmpty(); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_returns_closed_issues_with_at_least_one_line_diff(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto expected = insertNewClosedIssue(component, ruleType); + IssueChangeDto changeDto = insertNewLineDiff(expected); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues).hasSize(1); + IssueDto issue = resultHandler.issues.iterator().next(); + assertThat(issue.getKey()).isEqualTo(issue.getKey()); + assertThat(issue.getLineChangeData()).contains(changeDto.getChangeData()); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_does_not_return_closed_issues_of_non_existing_rule(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto issueWithRule = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issueWithRule); + IssueDto issueWithoutRule = insertNewClosedIssue(component, new RuleDefinitionDto().setType(ruleType).setId(-50)); + insertNewLineDiff(issueWithoutRule); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly(tuple(issueWithRule.getKey(), issueChange.getChangeData())); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_does_not_return_closed_issues_of_orphan_component(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto issue = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issue); + IssueDto issueMissingComponent = insertNewClosedIssue(component, ruleType, t -> t.setComponentUuid("does_not_exist")); + insertNewLineDiff(issueMissingComponent); + IssueDto issueMissingProject = insertNewClosedIssue(component, ruleType, t -> t.setProjectUuid("does_not_exist")); + insertNewLineDiff(issueMissingProject); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly(tuple(issue.getKey(), issueChange.getChangeData())); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_does_not_return_closed_issues_without_any_line_diff(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto issueWithLineDiff = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issueWithLineDiff); + insertNewClosedIssue(component, ruleType); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly(tuple(issueWithLineDiff.getKey(), issueChange.getChangeData())); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_does_not_return_closed_issues_of_type_SECURITY_HOTSPOT(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto securityHotspotIssue = insertNewClosedIssue(component, RuleType.SECURITY_HOTSPOT); + insertNewLineDiff(securityHotspotIssue); + IssueDto issue = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issue); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly(tuple(issue.getKey(), issueChange.getChangeData())); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_return_closed_issues_without_isHotspot_flag(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto noHotspotFlagIssue = insertNewClosedIssue(component, ruleType); + IssueChangeDto noFlagIssueChange = insertNewLineDiff(noHotspotFlagIssue); + manuallySetToNullFromHotpotsColumn(noHotspotFlagIssue); + IssueDto issue = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issue); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly( + tuple(issue.getKey(), issueChange.getChangeData()), + tuple(noHotspotFlagIssue.getKey(), noFlagIssueChange.getChangeData())); + } + + private void manuallySetToNullFromHotpotsColumn(IssueDto fromHostSpotIssue) { + dbTester.executeUpdateSql("update issues set from_hotspot = null where kee = '" + fromHostSpotIssue.getKey() + "'"); + dbTester.commit(); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_does_not_return_closed_issues_with_isHotspot_flag_true(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto fromHostSpotIssue = insertNewClosedIssue(component, ruleType, t -> t.setIsFromHotspot(true)); + insertNewLineDiff(fromHostSpotIssue); + IssueDto issue = insertNewClosedIssue(component, ruleType); + IssueChangeDto issueChange = insertNewLineDiff(issue); + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsOnly(tuple(issue.getKey(), issueChange.getChangeData())); + } + + @Test + @UseDataProvider("closedIssuesSupportedRuleTypes") + public void scrollClosedByComponentUuid_return_one_row_per_line_diff_sorted_by_most_recent_creation_date_first(RuleType ruleType) { + OrganizationDto organization = dbTester.organizations().insert(); + ComponentDto component = randomComponent(organization); + IssueDto issue = insertNewClosedIssue(component, ruleType); + Date date = new Date(); + IssueChangeDto changes[] = new IssueChangeDto[] { + insertNewLineDiff(issue, DateUtils.addDays(date, -10)), + insertNewLineDiff(issue, DateUtils.addDays(date, -60)), + insertNewLineDiff(issue, date), + insertNewLineDiff(issue, DateUtils.addDays(date, -5)) + }; + + RecorderResultHandler resultHandler = new RecorderResultHandler(); + underTest.scrollClosedByComponentUuid(component.uuid(), resultHandler); + + assertThat(resultHandler.issues) + .hasSize(4) + .extracting(IssueDto::getKey, t -> t.getLineChangeData().get()) + .containsExactly( + tuple(issue.getKey(), changes[2].getChangeData()), + tuple(issue.getKey(), changes[3].getChangeData()), + tuple(issue.getKey(), changes[0].getChangeData()), + tuple(issue.getKey(), changes[1].getChangeData())); + } + + private IssueChangeDto insertNewLineDiff(IssueDto issueDto) { + return insertNewLineDiff(issueDto, new Date()); + } + + private IssueChangeDto insertNewLineDiff(IssueDto issueDto, Date date) { + Integer oldLine = random.nextInt(10); + Integer newLine = 10 + random.nextInt(10); + Integer[][] values = new Integer[][] { + {oldLine, newLine}, + {oldLine, null}, + {null, newLine}, + }; + Integer[] choice = values[random.nextInt(values.length)]; + return insertNewLineDiff(issueDto, date, choice[0], choice[1]); + } + + private IssueChangeDto insertNewLineDiff(IssueDto issue, Date creationDate, @Nullable Integer before, @Nullable Integer after) { + checkArgument(before != null || after != null); + + FieldDiffs diffs = new FieldDiffs() + .setCreationDate(creationDate); + IntStream.range(0, random.nextInt(3)).forEach(i -> diffs.setDiff("key_b" + i, "old_" + i, "new_" + i)); + diffs.setDiff("line", toDiffValue(before), toDiffValue(after)); + IntStream.range(0, random.nextInt(3)).forEach(i -> diffs.setDiff("key_a" + i, "old_" + i, "new_" + i)); + + IssueChangeDto changeDto = IssueChangeDto.of(issue.getKey(), diffs); + dbTester.getDbClient().issueChangeDao().insert(dbSession, changeDto); + return changeDto; + } + + private static String toDiffValue(@Nullable Integer after) { + return after == null ? "" : String.valueOf(after); + } + + @SafeVarargs + private final IssueDto insertNewClosedIssue(ComponentDto component, RuleType ruleType, Consumer<IssueDto>... consumers) { + RuleDefinitionDto rule = dbTester.rules().insert(t -> t.setType(ruleType)); + return insertNewClosedIssue(component, rule, consumers); + } + + @SafeVarargs + private final IssueDto insertNewClosedIssue(ComponentDto component, RuleDefinitionDto rule, Consumer<IssueDto>... consumers) { + IssueDto res = new IssueDto() + .setKee(UuidFactoryFast.getInstance().create()) + .setRuleId(rule.getId()) + .setType(rule.getType()) + .setComponentUuid(component.uuid()) + .setProjectUuid(component.projectUuid()) + .setStatus(Issue.STATUS_CLOSED); + Arrays.asList(consumers).forEach(c -> c.accept(res)); + underTest.insert(res); + dbSession.commit(); + return res; + } + + @DataProvider + public static Object[][] closedIssuesSupportedRuleTypes() { + return Arrays.stream(RuleType.values()) + .filter(t -> t != RuleType.SECURITY_HOTSPOT) + .map(t -> new Object[] {t}) + .toArray(Object[][]::new); + } + + private ComponentDto randomComponent(OrganizationDto organization) { + ComponentDto project = dbTester.components().insertPublicProject(organization); + ComponentDto module = dbTester.components().insertComponent(ComponentTesting.newModuleDto(project)); + ComponentDto dir = dbTester.components().insertComponent(ComponentTesting.newDirectory(project, "foo")); + ComponentDto file = dbTester.components().insertComponent(ComponentTesting.newFileDto(project)); + ComponentDto[] components = new ComponentDto[] {project, module, dir, file}; + return components[random.nextInt(components.length)]; + } + private IssueDto newIssue() { return new IssueDto() .setKee("ABCDE") @@ -237,4 +505,17 @@ public class IssueMapperTest { .setCreatedAt(1_400_000_000_000L) .setUpdatedAt(1_500_000_000_000L); } + + private static class RecorderResultHandler implements ResultHandler<IssueDto> { + private final List<IssueDto> issues = new ArrayList<>(); + + @Override + public void handleResult(ResultContext<? extends IssueDto> resultContext) { + issues.add(resultContext.getResultObject()); + } + + public List<IssueDto> getIssues() { + return issues; + } + } } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java index 5eaa9d556e6..fb27f0d5853 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java @@ -38,7 +38,7 @@ import static com.google.common.base.Preconditions.checkState; @ComputeEngineSide public class IssueWorkflow implements Startable { - public static final String AUTOMATIC_CLOSE_TRANSITION = "automaticclose"; + private static final String AUTOMATIC_CLOSE_TRANSITION = "automaticclose"; private final FunctionExecutor functionExecutor; private final IssueFieldsSetter updater; private StateMachine machine; @@ -242,7 +242,42 @@ public class IssueWorkflow implements Startable { .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED), IsNotHotspotNorManualVulnerability.INSTANCE) .functions(new SetResolution(null), UnsetCloseDate.INSTANCE) .automatic() - .build()); + .build()) + + .transition(Transition.builder("automaticuncloseopen") + .from(Issue.STATUS_CLOSED).to(Issue.STATUS_OPEN) + .conditions( + new PreviousStatusWas(Issue.STATUS_OPEN), + new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED), + IsNotHotspotNorManualVulnerability.INSTANCE) + .automatic() + .build()) + .transition(Transition.builder("automaticunclosereopen") + .from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED) + .conditions( + new PreviousStatusWas(Issue.STATUS_REOPENED), + new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED), + IsNotHotspotNorManualVulnerability.INSTANCE) + .automatic() + .build()) + .transition(Transition.builder("automaticuncloseconfirmed") + .from(Issue.STATUS_CLOSED).to(Issue.STATUS_CONFIRMED) + .conditions( + new PreviousStatusWas(Issue.STATUS_CONFIRMED), + new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED), + IsNotHotspotNorManualVulnerability.INSTANCE) + .automatic() + .build()) + .transition(Transition.builder("automaticuncloseresolved") + .from(Issue.STATUS_CLOSED).to(Issue.STATUS_RESOLVED) + .conditions( + new PreviousStatusWas(Issue.STATUS_RESOLVED), + new HasResolution(Issue.RESOLUTION_REMOVED, Issue.RESOLUTION_FIXED), + IsNotHotspotNorManualVulnerability.INSTANCE) + .automatic() + .build()) + + ; } @Override diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java new file mode 100644 index 00000000000..e3fec3623ae --- /dev/null +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java @@ -0,0 +1,50 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.issue.workflow; + +import java.util.Comparator; +import java.util.Objects; +import java.util.Optional; +import org.sonar.api.issue.Issue; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs; + +class PreviousStatusWas implements Condition { + private final String expectedPreviousStatus; + + PreviousStatusWas(String expectedPreviousStatus) { + this.expectedPreviousStatus = expectedPreviousStatus; + } + + @Override + public boolean matches(Issue issue) { + DefaultIssue defaultIssue = (DefaultIssue) issue; + Optional<String> lastPreviousStatus = defaultIssue.changes().stream() + // exclude current change (if any) + .filter(change -> change != defaultIssue.currentChange()) + .sorted(Comparator.comparing(FieldDiffs::creationDate).reversed()) + .map(change -> change.get("status")) + .filter(Objects::nonNull) + .findFirst() + .map(t -> (String) t.oldValue()); + + return lastPreviousStatus.filter(this.expectedPreviousStatus::equals).isPresent(); + } +} diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java index bc508bd450d..c515234fd08 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java @@ -21,6 +21,10 @@ package org.sonar.server.issue.workflow; import com.google.common.base.Function; import com.google.common.collect.Collections2; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; @@ -28,16 +32,21 @@ import java.util.List; import javax.annotation.Nullable; import org.apache.commons.lang.time.DateUtils; import org.junit.Test; +import org.junit.runner.RunWith; import org.sonar.api.issue.DefaultTransitions; import org.sonar.api.rule.RuleKey; +import org.sonar.api.rules.RuleType; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.FieldDiffs; import org.sonar.core.issue.IssueChangeContext; import org.sonar.server.issue.IssueFieldsSetter; +import static org.apache.commons.lang.time.DateUtils.addDays; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.sonar.api.issue.Issue.RESOLUTION_FALSE_POSITIVE; import static org.sonar.api.issue.Issue.RESOLUTION_FIXED; +import static org.sonar.api.issue.Issue.RESOLUTION_REMOVED; import static org.sonar.api.issue.Issue.RESOLUTION_WONT_FIX; import static org.sonar.api.issue.Issue.STATUS_CLOSED; import static org.sonar.api.issue.Issue.STATUS_CONFIRMED; @@ -45,6 +54,7 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN; import static org.sonar.api.issue.Issue.STATUS_REOPENED; import static org.sonar.api.issue.Issue.STATUS_RESOLVED; +@RunWith(DataProviderRunner.class) public class IssueWorkflowTest { IssueFieldsSetter updater = new IssueFieldsSetter(); @@ -148,6 +158,151 @@ public class IssueWorkflowTest { } @Test + @UseDataProvider("allStatusesLeadingToClosed") + public void automatically_reopen_closed_issue_to_its_previous_status_from_changelog(String previousStatus) { + DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(resolution -> { + DefaultIssue issue = newClosedIssue(resolution); + setStatusPreviousToClosed(issue, previousStatus); + return issue; + }) + .toArray(DefaultIssue[]::new); + Date now = new Date(); + workflow.start(); + + Arrays.stream(issues).forEach(issue -> { + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); + + assertThat(issue.status()).isEqualTo(previousStatus); + assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND)); + assertThat(issue.closeDate()).isNull(); + assertThat(issue.isChanged()).isTrue(); + }); + } + + @Test + @UseDataProvider("allStatusesLeadingToClosed") + public void automatically_reopen_closed_issue_to_most_recent_previous_status_from_changelog(String previousStatus) { + DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(resolution -> { + DefaultIssue issue = newClosedIssue(resolution); + Date now = new Date(); + addStatusChange(issue, addDays(now, -60), STATUS_OPEN, STATUS_CONFIRMED); + addStatusChange(issue, addDays(now, -10), STATUS_CONFIRMED, previousStatus); + addStatusChange(issue, now, previousStatus, STATUS_CLOSED); + return issue; + }) + .toArray(DefaultIssue[]::new); + Date now = new Date(); + workflow.start(); + + Arrays.stream(issues).forEach(issue -> { + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); + + assertThat(issue.status()).isEqualTo(previousStatus); + assertThat(issue.updateDate()).isEqualTo(DateUtils.truncate(now, Calendar.SECOND)); + }); + } + + @DataProvider + public static Object[][] allStatusesLeadingToClosed() { + return new Object[][] { + {STATUS_OPEN}, + {STATUS_REOPENED}, + {STATUS_CONFIRMED}, + {STATUS_RESOLVED} + }; + } + + private static final String[] SUPPORTED_RESOLUTIONS_FOR_UNCLOSING = new String[] {RESOLUTION_FIXED, RESOLUTION_REMOVED}; + + @DataProvider + public static Object[][] supportedResolutionsForUnClosing() { + return Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(t -> new Object[] {t}) + .toArray(Object[][]::new); + } + + @Test + public void do_not_automatically_reopen_closed_issue_which_have_no_previous_status_in_changelog() { + DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(IssueWorkflowTest::newClosedIssue) + .toArray(DefaultIssue[]::new); + Date now = new Date(); + workflow.start(); + + Arrays.stream(issues).forEach(issue -> { + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); + + assertThat(issue.status()).isEqualTo(STATUS_CLOSED); + assertThat(issue.updateDate()).isNull(); + }); + } + + @Test + @UseDataProvider("allStatusesLeadingToClosed") + public void do_not_automatically_reopen_closed_issues_of_security_hotspots(String previousStatus) { + DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(resolution -> { + DefaultIssue issue = newClosedIssue(resolution); + setStatusPreviousToClosed(issue, previousStatus); + issue.setType(RuleType.SECURITY_HOTSPOT); + return issue; + }) + .toArray(DefaultIssue[]::new); + Date now = new Date(); + workflow.start(); + + Arrays.stream(issues).forEach(issue -> { + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); + + assertThat(issue.status()).isEqualTo(STATUS_CLOSED); + assertThat(issue.updateDate()).isNull(); + }); + } + + @Test + @UseDataProvider("allStatusesLeadingToClosed") + public void do_not_automatically_reopen_closed_issues_of_manual_vulnerability(String previousStatus) { + DefaultIssue[] issues = Arrays.stream(SUPPORTED_RESOLUTIONS_FOR_UNCLOSING) + .map(resolution -> { + DefaultIssue issue = newClosedIssue(resolution); + setStatusPreviousToClosed(issue, previousStatus); + issue.setIsFromHotspot(true); + return issue; + }) + .toArray(DefaultIssue[]::new); + Date now = new Date(); + workflow.start(); + + Arrays.stream(issues).forEach(issue -> { + workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); + + assertThat(issue.status()).isEqualTo(STATUS_CLOSED); + assertThat(issue.updateDate()).isNull(); + }); + } + + private static DefaultIssue newClosedIssue(String resolution) { + DefaultIssue res = new DefaultIssue() + .setKey("ABCDE") + .setRuleKey(RuleKey.of("js", "S001")) + .setResolution(resolution) + .setStatus(STATUS_CLOSED) + .setNew(false) + .setCloseDate(new Date(5_999_999L)); + return res; + } + + private static void setStatusPreviousToClosed(DefaultIssue issue, String previousStatus) { + addStatusChange(issue, new Date(), previousStatus, STATUS_CLOSED); + } + + private static void addStatusChange(DefaultIssue issue, Date date, String previousStatus, String newStatus) { + issue.addChange(new FieldDiffs().setCreationDate(date).setDiff("status", previousStatus, newStatus)); + } + + @Test public void close_open_dead_issue() { workflow.start(); 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 b2cf1843f24..ff87e287641 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 @@ -91,12 +91,12 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. private boolean isFromHotspot = false; - // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN - // Current changes private FieldDiffs currentChange = null; // all changes + // -- contains only current change (if any) on CE side unless reopening a closed issue or copying issue from base branch + // when analyzing a long living branch from the first time private List<FieldDiffs> changes = null; // true if the issue did not exist in the previous scan. @@ -522,11 +522,6 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. return this; } - public DefaultIssue setChanges(List<FieldDiffs> changes) { - this.changes = changes; - return this; - } - public List<FieldDiffs> changes() { if (changes == null) { return Collections.emptyList(); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java index 6f579501284..47b6f194dd1 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java @@ -24,6 +24,7 @@ import com.google.common.collect.Maps; import java.io.Serializable; import java.util.Date; import java.util.Map; +import java.util.Objects; import javax.annotation.CheckForNull; import javax.annotation.Nullable; @@ -191,6 +192,24 @@ public class FieldDiffs implements Serializable { } return sb.toString(); } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Diff<?> diff = (Diff<?>) o; + return Objects.equals(oldValue, diff.oldValue) && + Objects.equals(newValue, diff.newValue); + } + + @Override + public int hashCode() { + return Objects.hash(oldValue, newValue); + } } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java index e2ca3b97e6e..feb29d55599 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java @@ -34,13 +34,13 @@ import static java.util.Comparator.comparing; public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> { protected void match(Tracking<RAW, BASE> tracking, Function<Trackable, SearchKey> searchKeyFactory) { - if (tracking.isComplete()) { return; } Multimap<SearchKey, BASE> baseSearch = ArrayListMultimap.create(); - tracking.getUnmatchedBases().forEach(base -> baseSearch.put(searchKeyFactory.apply(base), base)); + tracking.getUnmatchedBases() + .forEach(base -> baseSearch.put(searchKeyFactory.apply(base), base)); tracking.getUnmatchedRaws().forEach(raw -> { SearchKey rawKey = searchKeyFactory.apply(raw); @@ -99,6 +99,38 @@ public class AbstractTracker<RAW extends Trackable, BASE extends Trackable> { } } + protected static class LineAndLineHashAndMessage implements SearchKey { + private final RuleKey ruleKey; + private final String lineHash; + private final String message; + private final Integer line; + + protected LineAndLineHashAndMessage(Trackable trackable) { + this.ruleKey = trackable.getRuleKey(); + this.line = trackable.getLine(); + this.message = trackable.getMessage(); + this.lineHash = StringUtils.defaultString(trackable.getLineHash(), ""); + } + + @Override + public boolean equals(@Nonnull Object o) { + if (this == o) { + return true; + } + LineAndLineHashAndMessage that = (LineAndLineHashAndMessage) o; + // start with most discriminant field + return Objects.equals(line, that.line) + && lineHash.equals(that.lineHash) + && message.equals(that.message) + && ruleKey.equals(that.ruleKey); + } + + @Override + public int hashCode() { + return Objects.hash(ruleKey, lineHash, message, line != null ? line : 0); + } + } + protected static class LineHashAndMessageKey implements SearchKey { private final RuleKey ruleKey; private final String message; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/FilteringBaseInputWrapper.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/FilteringBaseInputWrapper.java new file mode 100644 index 00000000000..2fccf8b53ff --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/FilteringBaseInputWrapper.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.core.issue.tracking; + +import java.util.Collection; +import java.util.List; +import java.util.function.Predicate; + +import static org.sonar.core.util.stream.MoreCollectors.toList; + +class FilteringBaseInputWrapper<BASE extends Trackable> implements Input<BASE> { + private final Input<BASE> baseInput; + private final List<BASE> nonClosedIssues; + + public FilteringBaseInputWrapper(Input<BASE> baseInput, Predicate<BASE> baseInputFilter) { + this.baseInput = baseInput; + Collection<BASE> baseIssues = baseInput.getIssues(); + this.nonClosedIssues = baseIssues.stream() + .filter(baseInputFilter) + .collect(toList(baseIssues.size())); + } + + @Override + public LineHashSequence getLineHashSequence() { + return baseInput.getLineHashSequence(); + } + + @Override + public BlockHashSequence getBlockHashSequence() { + return baseInput.getBlockHashSequence(); + } + + @Override + public Collection<BASE> getIssues() { + return nonClosedIssues; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/NonClosedTracking.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/NonClosedTracking.java new file mode 100644 index 00000000000..cdedb2814b5 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/NonClosedTracking.java @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.core.issue.tracking; + +import org.sonar.api.issue.Issue; + +public class NonClosedTracking<RAW extends Trackable, BASE extends Trackable> extends Tracking<RAW, BASE> { + private final Input<RAW> rawInput; + private final Input<BASE> baseInput; + + private NonClosedTracking(Input<RAW> rawInput, Input<BASE> baseInput) { + super(rawInput.getIssues(), baseInput.getIssues()); + this.rawInput = rawInput; + this.baseInput = baseInput; + } + + public static <RAW extends Trackable, BASE extends Trackable> NonClosedTracking<RAW, BASE> of(Input<RAW> rawInput, Input<BASE> baseInput) { + Input<BASE> nonClosedBaseInput = new FilteringBaseInputWrapper<>(baseInput, t -> !Issue.STATUS_CLOSED.equals(t.getStatus())); + return new NonClosedTracking<>(rawInput, nonClosedBaseInput); + } + + Input<RAW> getRawInput() { + return rawInput; + } + + Input<BASE> getBaseInput() { + return baseInput; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java index 2b8734f21ef..e122fad50e7 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java @@ -19,15 +19,21 @@ */ package org.sonar.core.issue.tracking; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; import org.sonar.api.batch.InstantiationStrategy; import org.sonar.api.batch.ScannerSide; +import org.sonar.api.issue.Issue; + +import static org.sonar.core.util.stream.MoreCollectors.toList; @InstantiationStrategy(InstantiationStrategy.PER_BATCH) @ScannerSide public class Tracker<RAW extends Trackable, BASE extends Trackable> extends AbstractTracker<RAW, BASE> { - public Tracking<RAW, BASE> track(Input<RAW> rawInput, Input<BASE> baseInput) { - Tracking<RAW, BASE> tracking = new Tracking<>(rawInput.getIssues(), baseInput.getIssues()); + public NonClosedTracking<RAW, BASE> trackNonClosed(Input<RAW> rawInput, Input<BASE> baseInput) { + NonClosedTracking<RAW, BASE> tracking = NonClosedTracking.of(rawInput, baseInput); // 1. match issues with same rule, same line and same line hash, but not necessarily with same message match(tracking, LineAndLineHashKey::new); @@ -48,9 +54,51 @@ public class Tracker<RAW extends Trackable, BASE extends Trackable> extends Abst return tracking; } + public Tracking<RAW, BASE> trackClosed(NonClosedTracking<RAW, BASE> nonClosedTracking, Input<BASE> baseInput) { + ClosedTracking<RAW, BASE> closedTracking = ClosedTracking.of(nonClosedTracking, baseInput); + match(closedTracking, LineAndLineHashAndMessage::new); + + return new MergedTracking<>(nonClosedTracking, closedTracking); + } + private void detectCodeMoves(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) { if (!tracking.isComplete()) { new BlockRecognizer<RAW, BASE>().match(rawInput, baseInput, tracking); } } + + private static class ClosedTracking<RAW extends Trackable, BASE extends Trackable> extends Tracking<RAW, BASE> { + private final Input<BASE> baseInput; + + ClosedTracking(NonClosedTracking<RAW, BASE> nonClosedTracking, Input<BASE> closedBaseInput) { + super(nonClosedTracking.getRawInput().getIssues(), closedBaseInput.getIssues(), nonClosedTracking.rawToBase, nonClosedTracking.baseToRaw); + this.baseInput = closedBaseInput; + } + + public static <RAW extends Trackable, BASE extends Trackable> ClosedTracking<RAW, BASE> of(NonClosedTracking<RAW, BASE> nonClosedTracking, Input<BASE> baseInput) { + Input<BASE> closedBaseInput = new FilteringBaseInputWrapper<>(baseInput, t -> Issue.STATUS_CLOSED.equals(t.getStatus())); + return new ClosedTracking<>(nonClosedTracking, closedBaseInput); + } + + public Input<BASE> getBaseInput() { + return baseInput; + } + } + + private static class MergedTracking<RAW extends Trackable, BASE extends Trackable> extends Tracking<RAW, BASE> { + private MergedTracking(NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) { + super( + nonClosedTracking.getRawInput().getIssues(), + concatIssues(nonClosedTracking, closedTracking), + closedTracking.rawToBase, closedTracking.baseToRaw); + } + + private static <RAW extends Trackable, BASE extends Trackable> List<BASE> concatIssues( + NonClosedTracking<RAW, BASE> nonClosedTracking, ClosedTracking<RAW, BASE> closedTracking) { + Collection<BASE> nonClosedIssues = nonClosedTracking.getBaseInput().getIssues(); + Collection<BASE> closeIssues = closedTracking.getBaseInput().getIssues(); + return Stream.concat(nonClosedIssues.stream(), closeIssues.stream()) + .collect(toList(nonClosedIssues.size() + closeIssues.size())); + } + } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java index e5f9b9b34e5..cce18f9eb09 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java @@ -31,15 +31,21 @@ public class Tracking<RAW extends Trackable, BASE extends Trackable> { /** * Matched issues -> a raw issue is associated to a base issue */ - private final IdentityHashMap<RAW, BASE> rawToBase = new IdentityHashMap<>(); - private final IdentityHashMap<BASE, RAW> baseToRaw = new IdentityHashMap<>(); - + protected final IdentityHashMap<RAW, BASE> rawToBase; + protected final IdentityHashMap<BASE, RAW> baseToRaw; private final Collection<RAW> raws; private final Collection<BASE> bases; - public Tracking(Collection<RAW> rawInput, Collection<BASE> baseInput) { + Tracking(Collection<RAW> rawInput, Collection<BASE> baseInput) { + this(rawInput, baseInput, new IdentityHashMap<>(), new IdentityHashMap<>()); + } + + protected Tracking(Collection<RAW> rawInput, Collection<BASE> baseInput, + IdentityHashMap<RAW, BASE> rawToBase, IdentityHashMap<BASE, RAW> baseToRaw) { this.raws = rawInput; this.bases = baseInput; + this.rawToBase = rawToBase; + this.baseToRaw = baseToRaw; } /** @@ -78,7 +84,7 @@ public class Tracking<RAW extends Trackable, BASE extends Trackable> { } } - boolean isComplete() { + public boolean isComplete() { return rawToBase.size() == raws.size(); } diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java index e2d5a73dca9..631b09c1153 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java @@ -58,7 +58,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H1"); Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isNull(); } @@ -72,7 +72,7 @@ public class TrackerTest { Issue raw1 = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "msg"); Issue raw2 = rawInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "msg"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw1)).isSameAs(base1); assertThat(tracking.baseFor(raw2)).isSameAs(base2); } @@ -88,7 +88,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H10", "H11", "H12"); Issue raw = rawInput.createIssue(RULE_SYSTEM_PRINT, "msg2"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -100,7 +100,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H1"); Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg2"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -112,7 +112,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H2"); Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "message"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -127,7 +127,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H2"); Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -139,7 +139,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H2", "H1"); Issue raw = rawInput.createIssueOnLine(2, RULE_SYSTEM_PRINT, "msg"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -154,7 +154,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H3", "H4", "H1"); Issue raw = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "other message"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isSameAs(base); } @@ -166,7 +166,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H3", "H4", "H5"); Issue raw = rawInput.createIssue(RULE_UNUSED_LOCAL_VARIABLE, "msg2"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isNull(); assertThat(tracking.getUnmatchedBases()).containsOnly(base); } @@ -179,7 +179,7 @@ public class TrackerTest { FakeInput rawInput = new FakeInput("H3", "H4", "H5"); Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg2"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw)).isNull(); assertThat(tracking.getUnmatchedBases()).containsOnly(base); } @@ -189,7 +189,7 @@ public class TrackerTest { FakeInput baseInput = new FakeInput(); FakeInput rawInput = new FakeInput("H1").addIssue(new Issue(200, "H200", RULE_SYSTEM_PRINT, "msg", org.sonar.api.issue.Issue.STATUS_OPEN, new Date())); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.getUnmatchedRaws()).hasSize(1); } @@ -243,7 +243,7 @@ public class TrackerTest { Issue raw3 = rawInput.createIssueOnLine(17, RULE_SYSTEM_PRINT, "Indentation"); Issue raw4 = rawInput.createIssueOnLine(21, RULE_SYSTEM_PRINT, "Indentation"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw1)).isNull(); assertThat(tracking.baseFor(raw2)).isNull(); assertThat(tracking.baseFor(raw3)).isSameAs(base1); @@ -295,7 +295,7 @@ public class TrackerTest { Issue raw1 = rawInput.createIssueOnLine(11, RuleKey.of("squid", "S00103"), "Split this 139 characters long line (which is greater than 120 authorized)."); Issue raw2 = rawInput.createIssueOnLine(15, RuleKey.of("squid", "S109"), "Assign this magic number 123 to a well-named constant, and use the constant instead."); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw1)).isNull(); assertThat(tracking.baseFor(raw2)).isNull(); assertThat(tracking.getUnmatchedBases()).hasSize(2); @@ -337,7 +337,7 @@ public class TrackerTest { Issue raw2 = rawInput.createIssueOnLine(10, RULE_SYSTEM_PRINT, "SystemPrintln"); Issue raw3 = rawInput.createIssueOnLine(14, RULE_SYSTEM_PRINT, "SystemPrintln"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(raw1)).isNull(); assertThat(tracking.baseFor(raw2)).isSameAs(base1); assertThat(tracking.baseFor(raw3)).isNull(); @@ -394,7 +394,7 @@ public class TrackerTest { Issue rawSameAsBase3 = rawInput.createIssueOnLine(9, RULE_NOT_DESIGNED_FOR_EXTENSION, "Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty."); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.baseFor(newRaw)).isNull(); assertThat(tracking.baseFor(rawSameAsBase1)).isSameAs(base1); @@ -427,7 +427,7 @@ public class TrackerTest { " private final Deque<Set<Set<DataItem>>> four = new ArrayDeque<>();"); Issue raw1 = rawInput.createIssueOnLine(3, RULE_USE_DIAMOND, "Use diamond"); - Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + Tracking<Issue, Issue> tracking = tracker.trackNonClosed(rawInput, baseInput); assertThat(tracking.getUnmatchedBases()).hasSize(3); assertThat(tracking.baseFor(raw1)).isEqualTo(base1); } diff --git a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/tracking/LocalIssueTracking.java b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/tracking/LocalIssueTracking.java index 1e2913c8d08..aa4e5694ae7 100644 --- a/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/tracking/LocalIssueTracking.java +++ b/sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/tracking/LocalIssueTracking.java @@ -94,7 +94,7 @@ public class LocalIssueTracking { Input<ServerIssueFromWs> baseIssues = createBaseInput(serverIssues, sourceHashHolder); Input<TrackedIssue> rawIssues = createRawInput(rIssues, sourceHashHolder); - Tracking<TrackedIssue, ServerIssueFromWs> track = tracker.track(rawIssues, baseIssues); + Tracking<TrackedIssue, ServerIssueFromWs> track = tracker.trackNonClosed(rawIssues, baseIssues); addUnmatchedFromServer(track.getUnmatchedBases(), trackedIssues, component.key()); mergeMatched(track, trackedIssues, rIssues); |