aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorDuarte Meneses <duarte.meneses@sonarsource.com>2022-07-26 11:25:34 -0500
committersonartech <sonartech@sonarsource.com>2022-08-02 20:04:05 +0000
commit6a32765903d0d2162f04172f811206ab9a78a435 (patch)
tree2c7282f7516686a1b515a70d066744c7fc4ed202 /server
parentb43d59a860d38e0bd9399f43a3d028ed96fc85b9 (diff)
downloadsonarqube-6a32765903d0d2162f04172f811206ab9a78a435.tar.gz
sonarqube-6a32765903d0d2162f04172f811206ab9a78a435.zip
SONAR-16583 Collection of hashes of issue locations takes too long
Diffstat (limited to 'server')
-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/ComputeLocationHashesVisitor.java215
-rw-r--r--server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java49
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java266
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IntegrateIssuesVisitorTest.java4
-rw-r--r--server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java167
-rw-r--r--server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java42
-rw-r--r--server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java87
8 files changed, 618 insertions, 214 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 36beabb12e4..a90a512cd8a 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
@@ -55,6 +55,7 @@ import org.sonar.ce.task.projectanalysis.filesystem.ComputationTempFolderProvide
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.ComputeLocationHashesVisitor;
import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesLoader;
import org.sonar.ce.task.projectanalysis.issue.ComponentIssuesRepositoryImpl;
import org.sonar.ce.task.projectanalysis.issue.ComponentsWithUnprocessedIssues;
@@ -271,6 +272,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
// debt)
RuleTagsCopier.class,
IssueCreationDateCalculator.class,
+ ComputeLocationHashesVisitor.class,
DebtCalculator.class,
EffortAggregator.class,
NewEffortAggregator.class,
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java
new file mode 100644
index 00000000000..7a9f4fdf220
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitor.java
@@ -0,0 +1,215 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolder;
+import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.issue.TaintChecker;
+
+/**
+ * This visitor will update the locations field of issues, by filling the hashes for all locations.
+ * It only applies to issues that are taint vulnerabilities and that are new or were changed.
+ * For performance reasons, it will read each source code file once and feed the lines to all locations in that file.
+ */
+public class ComputeLocationHashesVisitor extends IssueVisitor {
+ private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s");
+ private final List<DefaultIssue> issues = new LinkedList<>();
+ private final SourceLinesRepository sourceLinesRepository;
+ private final TreeRootHolder treeRootHolder;
+ private final TaintChecker taintChecker;
+
+ public ComputeLocationHashesVisitor(TaintChecker taintChecker, SourceLinesRepository sourceLinesRepository, TreeRootHolder treeRootHolder) {
+ this.taintChecker = taintChecker;
+ this.sourceLinesRepository = sourceLinesRepository;
+ this.treeRootHolder = treeRootHolder;
+ }
+
+ @Override
+ public void beforeComponent(Component component) {
+ issues.clear();
+ }
+
+ @Override
+ public void onIssue(Component component, DefaultIssue issue) {
+ if (taintChecker.isTaintVulnerability(issue) && !issue.isFromExternalRuleEngine() && (issue.isNew() || issue.locationsChanged())) {
+ issues.add(issue);
+ }
+ }
+
+ @Override
+ public void afterComponent(Component component) {
+ Map<Component, List<Location>> locationsByComponent = new HashMap<>();
+ List<LocationToSet> locationsToSet = new LinkedList<>();
+
+ for (DefaultIssue issue : issues) {
+ if (issue.getLocations() == null) {
+ continue;
+ }
+
+ DbIssues.Locations.Builder primaryLocationBuilder = ((DbIssues.Locations) issue.getLocations()).toBuilder();
+ boolean hasTextRange = addLocations(component, locationsByComponent, primaryLocationBuilder);
+
+ // If any location was added (because it had a text range), we'll need to update the issue at the end with the new object containing the hashes
+ if (hasTextRange) {
+ locationsToSet.add(new LocationToSet(issue, primaryLocationBuilder));
+ }
+ }
+
+ // Feed lines to locations, component by component
+ locationsByComponent.forEach(this::updateLocationsInComponent);
+
+ // Finalize by setting hashes
+ locationsByComponent.values().forEach(list -> list.forEach(Location::afterAllLines));
+
+ // set new locations to issues
+ locationsToSet.forEach(LocationToSet::set);
+
+ issues.clear();
+ }
+
+ private boolean addLocations(Component component, Map<Component, List<Location>> locationsByComponent, DbIssues.Locations.Builder primaryLocationBuilder) {
+ boolean hasTextRange = false;
+
+ // Add primary location
+ if (primaryLocationBuilder.hasTextRange()) {
+ hasTextRange = true;
+ PrimaryLocation primaryLocation = new PrimaryLocation(primaryLocationBuilder);
+ locationsByComponent.computeIfAbsent(component, c -> new LinkedList<>()).add(primaryLocation);
+ }
+
+ // Add secondary locations
+ for (DbIssues.Flow.Builder flowBuilder : primaryLocationBuilder.getFlowBuilderList()) {
+ for (DbIssues.Location.Builder locationBuilder : flowBuilder.getLocationBuilderList()) {
+ if (locationBuilder.hasTextRange()) {
+ hasTextRange = true;
+ Component locationComponent = treeRootHolder.getComponentByUuid(locationBuilder.getComponentId());
+ locationsByComponent.computeIfAbsent(locationComponent, c -> new LinkedList<>()).add(new SecondaryLocation(locationBuilder));
+ }
+ }
+ }
+
+ return hasTextRange;
+ }
+
+ private void updateLocationsInComponent(Component component, List<Location> locations) {
+ try (CloseableIterator<String> linesIterator = sourceLinesRepository.readLines(component)) {
+ int lineNumber = 1;
+ while (linesIterator.hasNext()) {
+ String line = linesIterator.next();
+ for (Location location : locations) {
+ location.processLine(lineNumber, line);
+ }
+ lineNumber++;
+ }
+ }
+ }
+
+ private static class LocationToSet {
+ private final DefaultIssue issue;
+ private final DbIssues.Locations.Builder locationsBuilder;
+
+ public LocationToSet(DefaultIssue issue, DbIssues.Locations.Builder locationsBuilder) {
+ this.issue = issue;
+ this.locationsBuilder = locationsBuilder;
+ }
+
+ void set() {
+ issue.setLocations(locationsBuilder.build());
+ }
+ }
+
+ private static class PrimaryLocation extends Location {
+ private final DbIssues.Locations.Builder locationsBuilder;
+
+ public PrimaryLocation(DbIssues.Locations.Builder locationsBuilder) {
+ this.locationsBuilder = locationsBuilder;
+ }
+
+ @Override
+ DbCommons.TextRange getTextRange() {
+ return locationsBuilder.getTextRange();
+ }
+
+ @Override
+ void setHash(String hash) {
+ locationsBuilder.setChecksum(hash);
+ }
+ }
+
+ private static class SecondaryLocation extends Location {
+ private final DbIssues.Location.Builder locationBuilder;
+
+ public SecondaryLocation(DbIssues.Location.Builder locationBuilder) {
+ this.locationBuilder = locationBuilder;
+ }
+
+ @Override
+ DbCommons.TextRange getTextRange() {
+ return locationBuilder.getTextRange();
+ }
+
+ @Override
+ void setHash(String hash) {
+ locationBuilder.setChecksum(hash);
+ }
+ }
+
+ private abstract static class Location {
+ private final StringBuilder hashBuilder = new StringBuilder();
+
+ abstract DbCommons.TextRange getTextRange();
+
+ abstract void setHash(String hash);
+
+ public void processLine(int lineNumber, String line) {
+ DbCommons.TextRange textRange = getTextRange();
+ if (lineNumber > textRange.getEndLine() || lineNumber < textRange.getStartLine()) {
+ return;
+ }
+
+ if (lineNumber == textRange.getStartLine() && lineNumber == textRange.getEndLine()) {
+ hashBuilder.append(line, textRange.getStartOffset(), textRange.getEndOffset());
+ } else if (lineNumber == textRange.getStartLine()) {
+ hashBuilder.append(line, textRange.getStartOffset(), line.length());
+ } else if (lineNumber < textRange.getEndLine()) {
+ hashBuilder.append(line);
+ } else {
+ hashBuilder.append(line, 0, textRange.getEndOffset());
+ }
+ }
+
+ void afterAllLines() {
+ String issueContentWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(hashBuilder.toString()).replaceAll("");
+ String hash = DigestUtils.md5Hex(issueContentWithoutWhitespaces);
+ setHash(hash);
+ }
+ }
+}
diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
index 577c15076a2..5b2b75b16b4 100644
--- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
+++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java
@@ -27,7 +27,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
-import org.apache.commons.codec.digest.DigestUtils;
import org.sonar.api.rule.RuleKey;
import org.sonar.api.rules.RuleType;
import org.sonar.api.utils.Duration;
@@ -58,25 +57,20 @@ import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.api.issue.Issue.STATUS_TO_REVIEW;
public class TrackerRawInputFactory {
- private static final Logger LOGGER = Loggers.get(TrackerRawInputFactory.class);
- private static final Pattern MATCH_ALL_WHITESPACES = Pattern.compile("\\s");
private static final long DEFAULT_EXTERNAL_ISSUE_EFFORT = 0L;
private final TreeRootHolder treeRootHolder;
private final BatchReportReader reportReader;
private final CommonRuleEngine commonRuleEngine;
private final IssueFilter issueFilter;
private final SourceLinesHashRepository sourceLinesHash;
- private final SourceLinesRepository sourceLinesRepository;
private final RuleRepository ruleRepository;
private final ActiveRulesHolder activeRulesHolder;
- public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader, SourceLinesHashRepository sourceLinesHash,
- SourceLinesRepository sourceLinesRepository, CommonRuleEngine commonRuleEngine, IssueFilter issueFilter, RuleRepository ruleRepository,
- ActiveRulesHolder activeRulesHolder) {
+ public TrackerRawInputFactory(TreeRootHolder treeRootHolder, BatchReportReader reportReader, SourceLinesHashRepository sourceLinesHash, CommonRuleEngine commonRuleEngine,
+ IssueFilter issueFilter, RuleRepository ruleRepository, ActiveRulesHolder activeRulesHolder) {
this.treeRootHolder = treeRootHolder;
this.reportReader = reportReader;
this.sourceLinesHash = sourceLinesHash;
- this.sourceLinesRepository = sourceLinesRepository;
this.commonRuleEngine = commonRuleEngine;
this.issueFilter = issueFilter;
this.ruleRepository = ruleRepository;
@@ -195,7 +189,6 @@ public class TrackerRawInputFactory {
if (reportIssue.hasTextRange()) {
DbCommons.TextRange.Builder textRange = convertTextRange(reportIssue.getTextRange());
dbLocationsBuilder.setTextRange(textRange);
- dbLocationsBuilder.setChecksum(calculateLocationHash(textRange, component));
}
for (ScannerReport.Flow flow : reportIssue.getFlowList()) {
if (flow.getLocationCount() > 0) {
@@ -305,48 +298,10 @@ public class TrackerRawInputFactory {
ScannerReport.TextRange sourceRange = source.getTextRange();
DbCommons.TextRange.Builder targetRange = convertTextRange(sourceRange);
target.setTextRange(targetRange);
- target.setChecksum(calculateLocationHash(targetRange, source.getComponentRef()));
}
return Optional.of(target.build());
}
- private String calculateLocationHash(DbCommons.TextRange.Builder textRange, int componentRef) {
- if (this.component.getReportAttributes().getRef() == null) {
- LOGGER.warn("Line hash for one of the issues in component" + component.getName() + " will not be calculated");
- return "";
- }
- if (componentRef == this.component.getReportAttributes().getRef()) {
- return calculateLocationHash(textRange, this.component);
- }
- Component textRangeComponent = treeRootHolder.getComponentByRef(componentRef);
- return calculateLocationHash(textRange, textRangeComponent);
- }
-
- private String calculateLocationHash(DbCommons.TextRange.Builder textRange, Component component) {
- try (CloseableIterator<String> linesIterator = sourceLinesRepository.readLines(component)) {
- StringBuilder toHash = new StringBuilder();
- int lineNumber = 1;
- while (linesIterator.hasNext()) {
- String line = linesIterator.next();
- if (lineNumber == textRange.getStartLine() && lineNumber == textRange.getEndLine()) {
- toHash.append(line, textRange.getStartOffset(), textRange.getEndOffset());
- } else if (lineNumber == textRange.getStartLine()) {
- toHash.append(line, textRange.getStartOffset(), line.length());
- } else if (lineNumber > textRange.getStartLine() && lineNumber < textRange.getEndLine()) {
- toHash.append(line);
- } else if (lineNumber == textRange.getEndLine()) {
- toHash.append(line, 0, textRange.getEndOffset());
- } else if (lineNumber > textRange.getEndLine()) {
- break;
- }
- lineNumber++;
- }
- String issueContentWithoutWhitespaces = MATCH_ALL_WHITESPACES.matcher(toHash.toString()).replaceAll("");
- return DigestUtils.md5Hex(issueContentWithoutWhitespaces);
- }
-
- }
-
private DbCommons.TextRange.Builder convertTextRange(ScannerReport.TextRange sourceRange) {
DbCommons.TextRange.Builder targetRange = DbCommons.TextRange.newBuilder();
targetRange.setStartLine(sourceRange.getStartLine());
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java
new file mode 100644
index 00000000000..5fdb62f1238
--- /dev/null
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/ComputeLocationHashesVisitorTest.java
@@ -0,0 +1,266 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.IntStream;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.ce.task.projectanalysis.component.Component;
+import org.sonar.ce.task.projectanalysis.component.ReportComponent;
+import org.sonar.ce.task.projectanalysis.component.TreeRootHolderRule;
+import org.sonar.ce.task.projectanalysis.source.SourceLinesRepository;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
+import org.sonar.server.issue.TaintChecker;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+
+public class ComputeLocationHashesVisitorTest {
+ private static final String EXAMPLE_LINE_OF_CODE_FORMAT = "int example = line + of + code + %d; ";
+ private static final String LINE_IN_THE_MAIN_FILE = "String string = 'line-in-the-main-file';";
+ private static final String LINE_IN_ANOTHER_FILE = "String string = 'line-in-the-another-file';";
+
+ private static final RuleKey RULE_KEY = RuleKey.of("javasecurity", "S001");
+ private static final Component FILE_1 = ReportComponent.builder(Component.Type.FILE, 2).build();
+ private static final Component FILE_2 = ReportComponent.builder(Component.Type.FILE, 3).build();
+ private static final Component ROOT = ReportComponent.builder(Component.Type.PROJECT, 1)
+ .addChildren(FILE_1, FILE_2)
+ .build();
+
+ private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class);
+ private final MutableConfiguration configuration = new MutableConfiguration();
+ private final TaintChecker taintChecker = new TaintChecker(configuration);
+ @Rule
+ public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
+ private final ComputeLocationHashesVisitor underTest = new ComputeLocationHashesVisitor(taintChecker, sourceLinesRepository, treeRootHolder);
+
+ @Before
+ public void before() {
+ Iterator<String> stringIterator = IntStream.rangeClosed(1, 9)
+ .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i))
+ .iterator();
+ when(sourceLinesRepository.readLines(FILE_1)).thenReturn(CloseableIterator.from(stringIterator));
+ when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE));
+ treeRootHolder.setRoot(ROOT);
+ }
+
+ @Test
+ public void do_nothing_if_issue_is_unchanged() {
+ DefaultIssue issue = createIssue()
+ .setLocationsChanged(false)
+ .setNew(false)
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
+
+ underTest.beforeComponent(FILE_1);
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ DbIssues.Locations locations = issue.getLocations();
+ assertThat(locations.getChecksum()).isEmpty();
+ verifyNoInteractions(sourceLinesRepository);
+ }
+
+ @Test
+ public void do_nothing_if_issue_is_not_taint_vulnerability() {
+ DefaultIssue issue = createIssue()
+ .setRuleKey(RuleKey.of("repo", "rule"))
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ DbIssues.Locations locations = issue.getLocations();
+ assertThat(locations.getChecksum()).isEmpty();
+ verifyNoInteractions(sourceLinesRepository);
+ }
+
+ @Test
+ public void calculates_hash_for_multiple_lines() {
+ DefaultIssue issue = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ assertLocationHashIsMadeOf(issue, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
+ }
+
+ @Test
+ public void calculates_hash_for_multiple_files() {
+ DefaultIssue issue1 = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 3, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
+ DefaultIssue issue2 = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length())).build());
+
+ underTest.onIssue(FILE_1, issue1);
+ underTest.afterComponent(FILE_1);
+
+ underTest.onIssue(FILE_2, issue2);
+ underTest.afterComponent(FILE_2);
+
+ assertLocationHashIsMadeOf(issue1, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
+ assertLocationHashIsMadeOf(issue2, "Stringstring='line-in-the-another-file';");
+ }
+
+ @Test
+ public void calculates_hash_for_partial_line() {
+ DefaultIssue issue = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 1, EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)).build());
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ assertLocationHashIsMadeOf(issue, "line+of+code+1;");
+ }
+
+ @Test
+ public void calculates_hash_for_partial_multiple_lines() {
+ DefaultIssue issue = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder().setTextRange(createRange(1, 13, 3, 11)).build());
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ assertLocationHashIsMadeOf(issue, "line+of+code+1;intexample=line+of+code+2;intexample");
+ }
+
+ @Test
+ public void dont_calculate_hash_if_no_textRange() {
+ // primary location and one of the secondary locations have no text range
+ DefaultIssue issue = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder()
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder()
+ .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
+ .setComponentId(FILE_1.getUuid())
+ .build())
+ .addLocation(DbIssues.Location.newBuilder()
+ .setComponentId(FILE_2.getUuid())
+ .build())
+ .build())
+ .build());
+
+ when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ verify(sourceLinesRepository).readLines(FILE_1);
+ verifyNoMoreInteractions(sourceLinesRepository);
+ DbIssues.Locations locations = issue.getLocations();
+ assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
+ assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEmpty();
+ }
+
+ @Test
+ public void calculates_hash_for_multiple_locations() {
+ DefaultIssue issue = createIssue()
+ .setLocations(DbIssues.Locations.newBuilder()
+ .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder()
+ .setTextRange(createRange(1, 0, 1, LINE_IN_THE_MAIN_FILE.length()))
+ .setComponentId(FILE_1.getUuid())
+ .build())
+ .addLocation(DbIssues.Location.newBuilder()
+ .setTextRange(createRange(1, 0, 1, LINE_IN_ANOTHER_FILE.length()))
+ .setComponentId(FILE_2.getUuid())
+ .build())
+ .build())
+ .build());
+
+ when(sourceLinesRepository.readLines(FILE_1)).thenReturn(newOneLineIterator(LINE_IN_THE_MAIN_FILE));
+ when(sourceLinesRepository.readLines(FILE_2)).thenReturn(newOneLineIterator(LINE_IN_ANOTHER_FILE));
+
+ underTest.onIssue(FILE_1, issue);
+ underTest.afterComponent(FILE_1);
+
+ DbIssues.Locations locations = issue.getLocations();
+
+ assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
+ assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-another-file';"));
+ }
+
+ private DbCommons.TextRange createRange(int startLine, int startOffset, int endLine, int endOffset) {
+ return DbCommons.TextRange.newBuilder()
+ .setStartLine(startLine).setStartOffset(startOffset)
+ .setEndLine(endLine).setEndOffset(endOffset)
+ .build();
+ }
+
+ private DefaultIssue createIssue() {
+ return new DefaultIssue()
+ .setLocationsChanged(true)
+ .setRuleKey(RULE_KEY)
+ .setIsFromExternalRuleEngine(false)
+ .setType(RuleType.CODE_SMELL);
+ }
+
+ private void assertLocationHashIsMadeOf(DefaultIssue issue, String stringToHash) {
+ String expectedHash = DigestUtils.md5Hex(stringToHash);
+ DbIssues.Locations locations = issue.getLocations();
+ assertThat(locations.getChecksum()).isEqualTo(expectedHash);
+ }
+
+ private CloseableIterator<String> newOneLineIterator(String lineContent) {
+ return CloseableIterator.from(List.of(lineContent).iterator());
+ }
+
+ private static class MutableConfiguration implements Configuration {
+ private final Map<String, String> keyValues = new HashMap<>();
+
+ public Configuration put(String key, String value) {
+ keyValues.put(key, value.trim());
+ return this;
+ }
+
+ @Override
+ public Optional<String> get(String key) {
+ return Optional.ofNullable(keyValues.get(key));
+ }
+
+ @Override
+ public boolean hasKey(String key) {
+ return keyValues.containsKey(key);
+ }
+
+ @Override
+ public String[] getStringArray(String key) {
+ throw new UnsupportedOperationException("getStringArray not implemented");
+ }
+ }
+}
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 7e261b7ad72..454a56f4074 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
@@ -150,8 +150,8 @@ public class IntegrateIssuesVisitorTest {
when(movedFilesRepository.getOriginalFile(any(Component.class))).thenReturn(Optional.empty());
DbClient dbClient = dbTester.getDbClient();
- TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, sourceLinesRepository,
- new CommonRuleEngineImpl(), issueFilter, ruleRepositoryRule, activeRulesHolder);
+ TrackerRawInputFactory rawInputFactory = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash, new CommonRuleEngineImpl(), issueFilter,
+ ruleRepositoryRule, activeRulesHolder);
TrackerBaseInputFactory baseInputFactory = new TrackerBaseInputFactory(issuesLoader, dbClient, movedFilesRepository, mock(ReportModulesPath.class), analysisMetadataHolder,
new IssueFieldsSetter(), mock(ComponentsWithUnprocessedIssues.class));
TrackerTargetBranchInputFactory targetInputFactory = new TrackerTargetBranchInputFactory(issuesLoader, targetBranchComponentUuids, dbClient);
diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
index 1392fd64f86..3f0b1f4c289 100644
--- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
+++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java
@@ -75,8 +75,7 @@ public class TrackerRawInputFactoryTest {
private static final String FILE_UUID = "fake_uuid";
private static final String ANOTHER_FILE_UUID = "another_fake_uuid";
private static final String EXAMPLE_LINE_OF_CODE_FORMAT = "int example = line + of + code + %d; ";
- private static final String LINE_IN_THE_MAIN_FILE = "String string = 'line-in-the-main-file';";
- private static final String LINE_IN_ANOTHER_FILE = "String string = 'line-in-the-another-file';";
+
private static final int FILE_REF = 2;
private static final int NOT_IN_REPORT_FILE_REF = 3;
private static final int ANOTHER_FILE_REF = 4;
@@ -96,19 +95,10 @@ public class TrackerRawInputFactoryTest {
private static final ReportComponent PROJECT = ReportComponent.builder(Component.Type.PROJECT, 1).addChildren(FILE, ANOTHER_FILE).build();
private final SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class);
- private final SourceLinesRepository sourceLinesRepository = mock(SourceLinesRepository.class);
private final CommonRuleEngine commonRuleEngine = mock(CommonRuleEngine.class);
private final IssueFilter issueFilter = mock(IssueFilter.class);
private final TrackerRawInputFactory underTest = new TrackerRawInputFactory(treeRootHolder, reportReader, sourceLinesHash,
- sourceLinesRepository, commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder);
-
- @Before
- public void before() {
- Iterator<String> stringIterator = IntStream.rangeClosed(1, 9)
- .mapToObj(i -> String.format(EXAMPLE_LINE_OF_CODE_FORMAT, i))
- .iterator();
- when(sourceLinesRepository.readLines(any())).thenReturn(CloseableIterator.from(stringIterator));
- }
+ commonRuleEngine, issueFilter, ruleRepository, activeRulesHolder);
@Test
public void load_source_hash_sequences() {
@@ -168,8 +158,6 @@ public class TrackerRawInputFactoryTest {
assertInitializedIssue(issue);
assertThat(issue.effort()).isNull();
assertThat(issue.getRuleDescriptionContextKey()).isEmpty();
-
- assertLocationHashIsMadeOf(input, "intexample=line+of+code+2;");
}
@Test
@@ -197,121 +185,6 @@ public class TrackerRawInputFactoryTest {
}
@Test
- public void calculateLocationHash_givenIssueOn3Lines_calculateHashOn3Lines() {
- RuleKey ruleKey = RuleKey.of("java", "S001");
- markRuleAsActive(ruleKey);
- when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
-
- ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
- .setTextRange(TextRange.newBuilder()
- .setStartLine(1)
- .setEndLine(3)
- .setStartOffset(0)
- .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
- .build())
- .setMsg("the message")
- .setRuleRepository(ruleKey.repository())
- .setRuleKey(ruleKey.rule())
- .build();
- reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
-
- Input<DefaultIssue> input = underTest.create(FILE);
-
- assertLocationHashIsMadeOf(input, "intexample=line+of+code+1;intexample=line+of+code+2;intexample=line+of+code+3;");
- }
-
- @Test
- public void calculateLocationHash_givenIssuePartiallyOn1Line_calculateHashOnAPartOfLine() {
- RuleKey ruleKey = RuleKey.of("java", "S001");
- markRuleAsActive(ruleKey);
- when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
-
- ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
- .setTextRange(TextRange.newBuilder()
- .setStartLine(1)
- .setEndLine(1)
- .setStartOffset(13)
- .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
- .build())
- .setMsg("the message")
- .setRuleRepository(ruleKey.repository())
- .setRuleKey(ruleKey.rule())
- .build();
- reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
-
- Input<DefaultIssue> input = underTest.create(FILE);
-
- assertLocationHashIsMadeOf(input, "line+of+code+1;");
- }
-
- @Test
- public void calculateLocationHash_givenIssuePartiallyOn1LineAndPartiallyOnThirdLine_calculateHashAccordingly() {
- RuleKey ruleKey = RuleKey.of("java", "S001");
- markRuleAsActive(ruleKey);
- when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
-
- ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
- .setTextRange(TextRange.newBuilder()
- .setStartLine(1)
- .setEndLine(3)
- .setStartOffset(13)
- .setEndOffset(11)
- .build())
- .setMsg("the message")
- .setRuleRepository(ruleKey.repository())
- .setRuleKey(ruleKey.rule())
- .build();
- reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
-
- Input<DefaultIssue> input = underTest.create(FILE);
-
- assertLocationHashIsMadeOf(input, "line+of+code+1;intexample=line+of+code+2;intexample");
- }
-
- @Test
- public void calculateLocationHash_givenIssueOn2Components_calculateHashesByReading2Files() {
- when(sourceLinesRepository.readLines(any())).thenReturn(
- newOneLineIterator(LINE_IN_THE_MAIN_FILE),
- newOneLineIterator(LINE_IN_THE_MAIN_FILE),
- newOneLineIterator(LINE_IN_ANOTHER_FILE));
- RuleKey ruleKey = RuleKey.of("java", "S001");
- markRuleAsActive(ruleKey);
- when(issueFilter.accept(any(), eq(FILE))).thenReturn(true);
-
- ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder()
- .setTextRange(newTextRange(1, LINE_IN_THE_MAIN_FILE.length()))
- .setMsg("the message")
- .setRuleRepository(ruleKey.repository())
- .setRuleKey(ruleKey.rule())
- .setSeverity(Constants.Severity.BLOCKER)
- .setGap(3.14)
- .addFlow(ScannerReport.Flow.newBuilder()
- .addLocation(ScannerReport.IssueLocation.newBuilder()
- .setComponentRef(FILE_REF)
- .setMsg("Secondary location in same file")
- .setTextRange(newTextRange(1, LINE_IN_THE_MAIN_FILE.length())))
- .addLocation(ScannerReport.IssueLocation.newBuilder()
- .setComponentRef(ANOTHER_FILE_REF)
- .setMsg("Secondary location in other file")
- .setTextRange(newTextRange(1, LINE_IN_ANOTHER_FILE.length())))
- .build())
- .build();
- reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue));
-
- Input<DefaultIssue> input = underTest.create(FILE);
- DefaultIssue issue = Iterators.getOnlyElement(input.getIssues().iterator());
-
- DbIssues.Locations locations = issue.getLocations();
-
- assertThat(locations.getFlow(0).getLocation(0).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-main-file';"));
- assertThat(locations.getFlow(0).getLocation(1).getChecksum()).isEqualTo(DigestUtils.md5Hex("Stringstring='line-in-the-another-file';"));
- }
-
- private CloseableIterator<String> newOneLineIterator(String lineContent) {
- return CloseableIterator.from(List.of(lineContent).iterator());
- }
-
- @Test
public void set_rule_name_as_message_when_issue_message_from_report_is_empty() {
RuleKey ruleKey = RuleKey.of("java", "S001");
markRuleAsActive(ruleKey);
@@ -556,6 +429,15 @@ public class TrackerRawInputFactoryTest {
assertThat(input.getIssues()).isEmpty();
}
+ private ScannerReport.TextRange newTextRange(int issueOnLine) {
+ return ScannerReport.TextRange.newBuilder()
+ .setStartLine(issueOnLine)
+ .setEndLine(issueOnLine)
+ .setStartOffset(0)
+ .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
+ .build();
+ }
+
private void assertInitializedIssue(DefaultIssue issue) {
assertInitializedExternalIssue(issue, STATUS_OPEN);
assertThat(issue.effort()).isNull();
@@ -581,31 +463,4 @@ public class TrackerRawInputFactoryTest {
dumbRule.setName(name);
ruleRepository.add(dumbRule);
}
-
- private TextRange newTextRange(int issueOnLine, int endOffset) {
- return TextRange.newBuilder()
- .setStartLine(issueOnLine)
- .setEndLine(issueOnLine)
- .setStartOffset(0)
- .setEndOffset(endOffset)
- .build();
- }
-
- private TextRange newTextRange(int issueOnLine) {
- return TextRange.newBuilder()
- .setStartLine(issueOnLine)
- .setEndLine(issueOnLine)
- .setStartOffset(0)
- .setEndOffset(EXAMPLE_LINE_OF_CODE_FORMAT.length() - 1)
- .build();
- }
-
- private void assertLocationHashIsMadeOf(Input<DefaultIssue> input, String stringToHash) {
- DefaultIssue defaultIssue = Iterators.getOnlyElement(input.getIssues().iterator());
- String expectedHash = DigestUtils.md5Hex(stringToHash);
- DbIssues.Locations locations = defaultIssue.getLocations();
-
- assertThat(locations.getChecksum()).isEqualTo(expectedHash);
- }
-
}
diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
index 2b027eb37ac..1b7afe26d32 100644
--- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
+++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java
@@ -35,6 +35,7 @@ import org.sonar.api.utils.Duration;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.DefaultIssueComment;
import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.db.protobuf.DbIssues;
import org.sonar.db.user.UserDto;
import static com.google.common.base.Preconditions.checkState;
@@ -171,15 +172,54 @@ public class IssueFieldsSetter {
return false;
}
+ /**
+ * New value will be set if the locations are different, ignoring the hashes. If that's the case, we mark the issue as changed,
+ * and we also flag that the locations have changed, so that we calculate all the hashes later, in an efficient way.
+ * WARNING: It is possible that the hashes changes without the text ranges changing, but for optimization we take that risk.
+ *
+ * @see ComputeLocationHashesVisitor
+ */
public boolean setLocations(DefaultIssue issue, @Nullable Object locations) {
- if (!Objects.equals(locations, issue.getLocations())) {
+ if (!locationsEqualsIgnoreHashes(locations, issue.getLocations())) {
issue.setLocations(locations);
issue.setChanged(true);
+ issue.setLocationsChanged(true);
return true;
}
return false;
}
+ private static boolean locationsEqualsIgnoreHashes(@Nullable Object l1, @Nullable DbIssues.Locations l2) {
+ if (l1 == null && l2 == null) {
+ return true;
+ }
+
+ if (l2 == null || !(l1 instanceof DbIssues.Locations)) {
+ return false;
+ }
+
+ DbIssues.Locations l1c = (DbIssues.Locations) l1;
+ if (!Objects.equals(l1c.getTextRange(), l2.getTextRange()) || l1c.getFlowCount() != l2.getFlowCount()) {
+ return false;
+ }
+
+ for (int i = 0; i < l1c.getFlowCount(); i++) {
+ if (l1c.getFlow(i).getLocationCount() != l2.getFlow(i).getLocationCount()) {
+ return false;
+ }
+ for (int j = 0; j < l1c.getFlow(i).getLocationCount(); j++) {
+ if (!locationEqualsIgnoreHashes(l1c.getFlow(i).getLocation(j), l2.getFlow(i).getLocation(j))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ private static boolean locationEqualsIgnoreHashes(DbIssues.Location l1, DbIssues.Location l2) {
+ return Objects.equals(l1.getComponentId(), l2.getComponentId()) && Objects.equals(l1.getTextRange(), l2.getTextRange()) && Objects.equals(l1.getMsg(), l2.getMsg());
+ }
+
public boolean setPastLocations(DefaultIssue issue, @Nullable Object previousLocations) {
Object currentLocations = issue.getLocations();
issue.setLocations(previousLocations);
diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
index c0835a1b701..65801c0e591 100644
--- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
+++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java
@@ -28,6 +28,8 @@ import org.sonar.api.utils.Duration;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.FieldDiffs;
import org.sonar.core.issue.IssueChangeContext;
+import org.sonar.db.protobuf.DbCommons;
+import org.sonar.db.protobuf.DbIssues;
import org.sonar.db.user.UserDto;
import static org.assertj.core.api.Assertions.assertThat;
@@ -269,21 +271,90 @@ public class IssueFieldsSetterTest {
}
@Test
- public void change_locations() {
- issue.setLocations("[1-3]");
- boolean updated = underTest.setLocations(issue, "[1-4]");
+ public void change_locations_if_primary_text_rage_changed() {
+ DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build();
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .setTextRange(range)
+ .build();
+ DbIssues.Locations locations2 = locations.toBuilder().setTextRange(range.toBuilder().setEndLine(2).build()).build();
+ issue.setLocations(locations);
+ boolean updated = underTest.setLocations(issue, locations2);
assertThat(updated).isTrue();
- assertThat(issue.getLocations().toString()).isEqualTo("[1-4]");
+ assertThat((Object) issue.getLocations()).isEqualTo(locations2);
+ assertThat(issue.locationsChanged()).isTrue();
assertThat(issue.currentChange()).isNull();
assertThat(issue.mustSendNotifications()).isFalse();
}
@Test
- public void do_not_change_locations() {
- issue.setLocations("[1-3]");
- boolean updated = underTest.setLocations(issue, "[1-3]");
+ public void change_locations_if_secondary_text_rage_changed() {
+ DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build();
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder().setTextRange(range))
+ .build())
+ .build();
+ issue.setLocations(locations);
+ DbIssues.Locations.Builder builder = locations.toBuilder();
+ builder.getFlowBuilder(0).getLocationBuilder(0).setTextRange(range.toBuilder().setEndLine(2));
+ boolean updated = underTest.setLocations(issue, builder.build());
+ assertThat(updated).isTrue();
+ }
+
+ @Test
+ public void change_locations_if_secondary_message_changed() {
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder().setMsg("msg1"))
+ .build())
+ .build();
+ issue.setLocations(locations);
+ DbIssues.Locations.Builder builder = locations.toBuilder();
+ builder.getFlowBuilder(0).getLocationBuilder(0).setMsg("msg2");
+ boolean updated = underTest.setLocations(issue, builder.build());
+ assertThat(updated).isTrue();
+ }
+
+ @Test
+ public void change_locations_if_different_flow_count() {
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder())
+ .build())
+ .build();
+ issue.setLocations(locations);
+ DbIssues.Locations.Builder builder = locations.toBuilder();
+ builder.clearFlow();
+ boolean updated = underTest.setLocations(issue, builder.build());
+ assertThat(updated).isTrue();
+ }
+
+ @Test
+ public void do_not_change_locations_if_primary_hash_changed() {
+ DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build();
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .setTextRange(range)
+ .setChecksum("1")
+ .build();
+ issue.setLocations(locations);
+ boolean updated = underTest.setLocations(issue, locations.toBuilder().setChecksum("2").build());
+ assertThat(updated).isFalse();
+ }
+
+ @Test
+ public void do_not_change_locations_if_secondary_hash_changed() {
+ DbCommons.TextRange range = DbCommons.TextRange.newBuilder().setStartLine(1).build();
+ DbIssues.Locations locations = DbIssues.Locations.newBuilder()
+ .addFlow(DbIssues.Flow.newBuilder()
+ .addLocation(DbIssues.Location.newBuilder().setTextRange(range))
+ .build())
+ .setChecksum("1")
+ .build();
+ issue.setLocations(locations);
+ DbIssues.Locations.Builder builder = locations.toBuilder();
+ builder.getFlowBuilder(0).getLocationBuilder(0).setChecksum("2");
+ boolean updated = underTest.setLocations(issue, builder.build());
assertThat(updated).isFalse();
- assertThat(issue.getLocations().toString()).isEqualTo("[1-3]");
assertThat(issue.currentChange()).isNull();
assertThat(issue.mustSendNotifications()).isFalse();
}