aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/BaseInputFactory.java66
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitor.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactory.java56
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoader.java108
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java19
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecution.java3
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssuesLoader.java26
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchTrackerExecution.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactory.java42
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecution.java40
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerMergeBranchInputFactory.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/CloseIssuesOnRemovedComponentsVisitorTest.java2
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ClosedIssuesInputFactoryTest.java100
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComponentIssuesLoaderTest.java85
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java13
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java10
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/MergeBranchTrackerExecutionTest.java6
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ShortBranchIssueMergerTest.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerBaseInputFactoryTest.java6
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerExecutionTest.java122
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java7
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java4
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml24
-rw-r--r--server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueMapperTest.java291
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/IssueWorkflow.java39
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/workflow/PreviousStatusWas.java50
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/workflow/IssueWorkflowTest.java155
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java9
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java19
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/AbstractTracker.java36
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/FilteringBaseInputWrapper.java54
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/NonClosedTracking.java46
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java52
-rw-r--r--sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java16
-rw-r--r--sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java32
-rw-r--r--sonar-scanner-engine/src/main/java/org/sonar/scanner/issue/tracking/LocalIssueTracking.java2
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 &lt;&gt; 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 &lt;&gt; 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);