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 f3d2d0a1e772..a6bc5c9d28a5 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 @@ -43,6 +43,7 @@ import org.sonar.ce.task.projectanalysis.duplication.DuplicationRepositoryImpl; import org.sonar.ce.task.projectanalysis.duplication.IntegrateCrossProjectDuplications; import org.sonar.ce.task.projectanalysis.event.EventRepositoryImpl; +import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepositoryImpl; import org.sonar.ce.task.projectanalysis.filemove.FileSimilarityImpl; import org.sonar.ce.task.projectanalysis.filemove.MutableMovedFilesRepositoryImpl; import org.sonar.ce.task.projectanalysis.filemove.ScoreMatrixDumperImpl; @@ -284,6 +285,7 @@ private static List componentClasses() { SourceSimilarityImpl.class, FileSimilarityImpl.class, MutableMovedFilesRepositoryImpl.class, + AddedFileRepositoryImpl.class, // duplication IntegrateCrossProjectDuplications.class, diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepository.java new file mode 100644 index 000000000000..e958d55c0f4f --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepository.java @@ -0,0 +1,30 @@ +/* + * 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.filemove; + +import org.sonar.ce.task.projectanalysis.component.Component; + +public interface AddedFileRepository { + /** + * @return {@code true} for any component on first analysis, otherwise {@code true} only if the specified component is + * a {@link Component.Type#FILE file} registered to the repository. + */ + boolean isAdded(Component component); +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImpl.java new file mode 100644 index 000000000000..162becd0aa12 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImpl.java @@ -0,0 +1,60 @@ +/* + * 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.filemove; + +import java.util.HashSet; +import java.util.Set; +import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.ce.task.projectanalysis.component.Component; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; + +public class AddedFileRepositoryImpl implements MutableAddedFileRepository { + private final Set addedComponents = new HashSet<>(); + private final AnalysisMetadataHolder analysisMetadataHolder; + + public AddedFileRepositoryImpl(AnalysisMetadataHolder analysisMetadataHolder) { + this.analysisMetadataHolder = analysisMetadataHolder; + } + + @Override + public boolean isAdded(Component component) { + checkComponent(component); + if (analysisMetadataHolder.isFirstAnalysis()) { + return true; + } + return addedComponents.contains(component); + } + + @Override + public void register(Component component) { + checkComponent(component); + checkArgument(component.getType() == Component.Type.FILE, "component must be a file"); + checkState(!analysisMetadataHolder.isFirstAnalysis(), "No file can be registered on first analysis"); + + addedComponents.add(component); + } + + private static void checkComponent(Component component) { + checkNotNull(component, "component can't be null"); + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java index 0b81525933e7..19f43618764f 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java @@ -22,7 +22,6 @@ import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.Multimap; import com.google.common.collect.Sets; import java.util.ArrayList; @@ -62,7 +61,7 @@ import static org.sonar.ce.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER; public class FileMoveDetectionStep implements ComputationStep { - protected static final int MIN_REQUIRED_SCORE = 85; + static final int MIN_REQUIRED_SCORE = 85; private static final Logger LOG = Loggers.get(FileMoveDetectionStep.class); private static final Comparator SCORE_FILE_COMPARATOR = (o1, o2) -> -1 * Integer.compare(o1.getLineCount(), o2.getLineCount()); private static final double LOWER_BOUND_RATIO = 0.84; @@ -75,10 +74,11 @@ public class FileMoveDetectionStep implements ComputationStep { private final MutableMovedFilesRepository movedFilesRepository; private final SourceLinesHashRepository sourceLinesHash; private final ScoreMatrixDumper scoreMatrixDumper; + private final MutableAddedFileRepository addedFileRepository; public FileMoveDetectionStep(AnalysisMetadataHolder analysisMetadataHolder, TreeRootHolder rootHolder, DbClient dbClient, FileSimilarity fileSimilarity, MutableMovedFilesRepository movedFilesRepository, SourceLinesHashRepository sourceLinesHash, - ScoreMatrixDumper scoreMatrixDumper) { + ScoreMatrixDumper scoreMatrixDumper, MutableAddedFileRepository addedFileRepository) { this.analysisMetadataHolder = analysisMetadataHolder; this.rootHolder = rootHolder; this.dbClient = dbClient; @@ -86,6 +86,7 @@ public FileMoveDetectionStep(AnalysisMetadataHolder analysisMetadataHolder, Tree this.movedFilesRepository = movedFilesRepository; this.sourceLinesHash = sourceLinesHash; this.scoreMatrixDumper = scoreMatrixDumper; + this.addedFileRepository = addedFileRepository; } @Override @@ -103,25 +104,30 @@ public void execute(ComputationStep.Context context) { Profiler p = Profiler.createIfTrace(LOG); p.start(); - Map dbFilesByKey = getDbFilesByKey(); - context.getStatistics().add("dbFiles", dbFilesByKey.size()); - if (dbFilesByKey.isEmpty()) { - LOG.debug("Previous snapshot has no file. Do nothing."); - return; - } - Map reportFilesByKey = getReportFilesByKey(this.rootHolder.getRoot()); + context.getStatistics().add("reportFiles", reportFilesByKey.size()); if (reportFilesByKey.isEmpty()) { - LOG.debug("No files in report. Do nothing."); + LOG.debug("No files in report. No file move detection."); return; } - Set addedFileKeys = ImmutableSet.copyOf(Sets.difference(reportFilesByKey.keySet(), dbFilesByKey.keySet())); + Map dbFilesByKey = getDbFilesByKey(); + context.getStatistics().add("dbFiles", dbFilesByKey.size()); + + Set addedFileKeys = difference(reportFilesByKey.keySet(), dbFilesByKey.keySet()); context.getStatistics().add("addedFiles", addedFileKeys.size()); - Set removedFileKeys = ImmutableSet.copyOf(Sets.difference(dbFilesByKey.keySet(), reportFilesByKey.keySet())); + + if (dbFilesByKey.isEmpty()) { + registerAddedFiles(addedFileKeys, reportFilesByKey, null); + LOG.debug("Previous snapshot has no file. No file move detection."); + return; + } + + Set removedFileKeys = difference(dbFilesByKey.keySet(), reportFilesByKey.keySet()); // can find matches if at least one of the added or removed files groups is empty => abort if (addedFileKeys.isEmpty() || removedFileKeys.isEmpty()) { + registerAddedFiles(addedFileKeys, reportFilesByKey, null); LOG.debug("Either no files added or no files removed. Do nothing."); return; } @@ -138,6 +144,8 @@ public void execute(ComputationStep.Context context) { // not a single match with score higher than MIN_REQUIRED_SCORE => abort if (scoreMatrix.getMaxScore() < MIN_REQUIRED_SCORE) { + context.getStatistics().add("movedFiles", 0); + registerAddedFiles(addedFileKeys, reportFilesByKey, null); LOG.debug("max score in matrix is less than min required score ({}). Do nothing.", MIN_REQUIRED_SCORE); return; } @@ -148,7 +156,16 @@ public void execute(ComputationStep.Context context) { ElectedMatches electedMatches = electMatches(removedFileKeys, reportFileSourcesByKey, matchesByScore); p.stopTrace("Matches elected"); + context.getStatistics().add("movedFiles", electedMatches.size()); registerMatches(dbFilesByKey, reportFilesByKey, electedMatches); + registerAddedFiles(addedFileKeys, reportFilesByKey, electedMatches); + } + + public Set difference(Set set1, Set set2) { + if (set1.isEmpty() || set2.isEmpty()) { + return set1; + } + return Sets.difference(set1, set2).immutableCopy(); } private void registerMatches(Map dbFilesByKey, Map reportFilesByKey, ElectedMatches electedMatches) { @@ -161,6 +178,22 @@ private void registerMatches(Map dbFilesByKey, Map addedFileKeys, Map reportFilesByKey, @Nullable ElectedMatches electedMatches) { + if (electedMatches == null || electedMatches.isEmpty()) { + addedFileKeys.stream() + .map(reportFilesByKey::get) + .forEach(addedFileRepository::register); + } else { + Set reallyAddedFileKeys = new HashSet<>(addedFileKeys); + for (Match electedMatch : electedMatches) { + reallyAddedFileKeys.remove(electedMatch.getReportKey()); + } + reallyAddedFileKeys.stream() + .map(reportFilesByKey::get) + .forEach(addedFileRepository::register); + } + } + private Map getDbFilesByKey() { try (DbSession dbSession = dbClient.openSession(false)) { ImmutableList.Builder builder = ImmutableList.builder(); @@ -402,5 +435,9 @@ public Iterator iterator() { public int size() { return matches.size(); } + + public boolean isEmpty() { + return matches.isEmpty(); + } } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/MutableAddedFileRepository.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/MutableAddedFileRepository.java new file mode 100644 index 000000000000..102c5defd8b8 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/MutableAddedFileRepository.java @@ -0,0 +1,32 @@ +/* + * 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.filemove; + +import org.sonar.ce.task.projectanalysis.component.Component; + +public interface MutableAddedFileRepository extends AddedFileRepository { + + /** + * @throws IllegalArgumentException if the specified component is not a {@link Component.Type#FILE File} + * @throws IllegalStateException on first analysis as all components are added on first analysis, none should be + * registered for performance reasons. + */ + void register(Component file); +} diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculator.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculator.java index 83cc898e07e2..a0350e3907fd 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculator.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculator.java @@ -30,6 +30,7 @@ import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin; import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepository; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import org.sonar.ce.task.projectanalysis.scm.Changeset; @@ -53,15 +54,18 @@ public class IssueCreationDateCalculator extends IssueVisitor { private final IssueChangeContext changeContext; private final ActiveRulesHolder activeRulesHolder; private final RuleRepository ruleRepository; + private final AddedFileRepository addedFileRepository; public IssueCreationDateCalculator(AnalysisMetadataHolder analysisMetadataHolder, ScmInfoRepository scmInfoRepository, - IssueFieldsSetter issueUpdater, ActiveRulesHolder activeRulesHolder, RuleRepository ruleRepository) { + IssueFieldsSetter issueUpdater, ActiveRulesHolder activeRulesHolder, RuleRepository ruleRepository, + AddedFileRepository addedFileRepository) { this.scmInfoRepository = scmInfoRepository; this.issueUpdater = issueUpdater; this.analysisMetadataHolder = analysisMetadataHolder; this.ruleRepository = ruleRepository; this.changeContext = createScan(new Date(analysisMetadataHolder.getAnalysisDate())); this.activeRulesHolder = activeRulesHolder; + this.addedFileRepository = addedFileRepository; } @Override @@ -69,23 +73,36 @@ public void onIssue(Component component, DefaultIssue issue) { if (!issue.isNew()) { return; } + Optional lastAnalysisOptional = lastAnalysis(); boolean firstAnalysis = !lastAnalysisOptional.isPresent(); + if (firstAnalysis || isNewFile(component)) { + backdateIssue(component, issue); + return; + } + Rule rule = ruleRepository.findByKey(issue.getRuleKey()) .orElseThrow(illegalStateException("The rule with key '%s' raised an issue, but no rule with that key was found", issue.getRuleKey())); - if (rule.isExternal()) { - getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate)); + backdateIssue(component, issue); } else { // Rule can't be inactive (see contract of IssueVisitor) ActiveRule activeRule = activeRulesHolder.get(issue.getRuleKey()).get(); - if (firstAnalysis || activeRuleIsNew(activeRule, lastAnalysisOptional.get()) + if (activeRuleIsNew(activeRule, lastAnalysisOptional.get()) || ruleImplementationChanged(activeRule.getRuleKey(), activeRule.getPluginKey(), lastAnalysisOptional.get())) { - getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate)); + backdateIssue(component, issue); } } } + private boolean isNewFile(Component component) { + return component.getType() == Component.Type.FILE && addedFileRepository.isAdded(component); + } + + private void backdateIssue(Component component, DefaultIssue issue) { + getDateOfLatestChange(component, issue).ifPresent(changeDate -> updateDate(issue, changeDate)); + } + private boolean ruleImplementationChanged(RuleKey ruleKey, @Nullable String pluginKey, long lastAnalysisDate) { if (pluginKey == null) { return false; diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImplTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImplTest.java new file mode 100644 index 000000000000..781c5fbffd69 --- /dev/null +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImplTest.java @@ -0,0 +1,129 @@ +/* + * 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.filemove; + +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 org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolder; +import org.sonar.ce.task.projectanalysis.component.Component; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(DataProviderRunner.class) +public class AddedFileRepositoryImplTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private AnalysisMetadataHolder analysisMetadataHolder = mock(AnalysisMetadataHolder.class); + private AddedFileRepositoryImpl underTest = new AddedFileRepositoryImpl(analysisMetadataHolder); + + @Test + public void isAdded_fails_with_NPE_if_component_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("component can't be null"); + + underTest.isAdded(null); + } + + @Test + public void isAdded_returns_true_for_any_component_type_on_first_analysis() { + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true); + + Arrays.stream(Component.Type.values()).forEach(type -> { + Component component = newComponent(type); + + assertThat(underTest.isAdded(component)).isTrue(); + }); + } + + @Test + public void isAdded_returns_false_for_unregistered_component_type_when_not_on_first_analysis() { + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false); + + Arrays.stream(Component.Type.values()).forEach(type -> { + Component component = newComponent(type); + + assertThat(underTest.isAdded(component)).isFalse(); + }); + } + + @Test + public void isAdded_returns_true_for_registered_file_when_not_on_first_analysis() { + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(false); + Component file1 = newComponent(Component.Type.FILE); + Component file2 = newComponent(Component.Type.FILE); + underTest.register(file1); + + assertThat(underTest.isAdded(file1)).isTrue(); + assertThat(underTest.isAdded(file2)).isFalse(); + } + + @Test + public void register_fails_with_NPE_if_component_is_null() { + expectedException.expect(NullPointerException.class); + expectedException.expectMessage("component can't be null"); + + underTest.register(null); + } + + @Test + @UseDataProvider("anyTypeButFile") + public void register_fails_with_IAE_if_component_is_not_a_file(Component.Type anyTypeButFile) { + Component component = newComponent(anyTypeButFile); + + expectedException.expect(IllegalArgumentException.class); + expectedException.expectMessage("component must be a file"); + + underTest.register(component); + } + + @DataProvider + public static Object[][] anyTypeButFile() { + return Arrays.stream(Component.Type.values()) + .filter(t -> t != Component.Type.FILE) + .map(t -> new Object[] {t}) + .toArray(Object[][]::new); + } + + @Test + public void register_fails_with_ISE_if_called_on_first_analysis() { + when(analysisMetadataHolder.isFirstAnalysis()).thenReturn(true); + Component component = newComponent(Component.Type.FILE); + + expectedException.expect(IllegalStateException.class); + expectedException.expectMessage("No file can be registered on first analysis"); + + underTest.register(component); + } + + private static Component newComponent(Component.Type type) { + Component component = mock(Component.class); + when(component.getType()).thenReturn(type); + return component; + } +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStepTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStepTest.java index 69b34878d286..b38605016af3 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStepTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStepTest.java @@ -22,7 +22,9 @@ import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.IntStream; @@ -228,9 +230,10 @@ public class FileMoveDetectionStepTest { private SourceLinesHashRepository sourceLinesHash = mock(SourceLinesHashRepository.class); private FileSimilarity fileSimilarity = new FileSimilarityImpl(new SourceSimilarityImpl()); private CapturingScoreMatrixDumper scoreMatrixDumper = new CapturingScoreMatrixDumper(); + private RecordingMutableAddedFileRepository addedFileRepository = new RecordingMutableAddedFileRepository(); private FileMoveDetectionStep underTest = new FileMoveDetectionStep(analysisMetadataHolder, treeRootHolder, dbClient, - fileSimilarity, movedFilesRepository, sourceLinesHash, scoreMatrixDumper); + fileSimilarity, movedFilesRepository, sourceLinesHash, scoreMatrixDumper, addedFileRepository); @Before public void setUp() throws Exception { @@ -245,23 +248,26 @@ public void getDescription_returns_description() { } @Test - public void execute_detects_no_move_if_baseProjectSnapshot_is_null() { + public void execute_detects_no_move_on_first_analysis() { analysisMetadataHolder.setBaseAnalysis(null); TestComputationStepContext context = new TestComputationStepContext(); underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); - verifyStatistics(context, null, null); + verifyStatistics(context, null, null, null, null); } @Test public void execute_detects_no_move_if_baseSnapshot_has_no_file_and_report_has_no_file() { analysisMetadataHolder.setBaseAnalysis(ANALYSIS); - underTest.execute(new TestComputationStepContext()); + TestComputationStepContext context = new TestComputationStepContext(); + underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 0, null, null, null); } @Test @@ -275,7 +281,8 @@ public void execute_detects_no_move_if_baseSnapshot_has_no_file() { underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); - verifyStatistics(context, 0, null); + assertThat(addedFileRepository.getComponents()).containsOnly(file1, file2); + verifyStatistics(context, 2, 0, 2, null); } @Test @@ -288,7 +295,8 @@ public void execute_detects_no_move_if_there_is_no_file_in_report() { underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); - verifyStatistics(context, 0, null); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 0, null, null, null); } @Test @@ -297,13 +305,16 @@ public void execute_detects_no_move_if_file_key_exists_in_both_DB_and_report() { Component file1 = fileComponent(FILE_1_REF, null); Component file2 = fileComponent(FILE_2_REF, null); insertFiles(file1.getDbKey(), file2.getDbKey()); + insertContentOfFileInDb(file1.getDbKey(), CONTENT1); + insertContentOfFileInDb(file2.getDbKey(), CONTENT2); setFilesInReport(file2, file1); TestComputationStepContext context = new TestComputationStepContext(); underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); - verifyStatistics(context, 0, null); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 2, 2, 0, null); } @Test @@ -323,7 +334,8 @@ public void execute_detects_move_if_content_of_file_is_same_in_DB_and_report() { assertThat(originalFile.getId()).isEqualTo(dtos[0].getId()); assertThat(originalFile.getKey()).isEqualTo(dtos[0].getDbKey()); assertThat(originalFile.getUuid()).isEqualTo(dtos[0].uuid()); - verifyStatistics(context, 1, 1); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 1, 1, 1, 1); } @Test @@ -342,7 +354,8 @@ public void execute_detects_no_move_if_content_of_file_is_not_similar_enough() { assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()) .isGreaterThan(0) .isLessThan(MIN_REQUIRED_SCORE); - verifyStatistics(context, 1, 1); + assertThat(addedFileRepository.getComponents()).contains(file2); + verifyStatistics(context, 1, 1, 1, 0); } @Test @@ -359,7 +372,8 @@ public void execute_detects_no_move_if_content_of_file_is_empty_in_DB() { assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isZero(); - verifyStatistics(context, 1, 1); + assertThat(addedFileRepository.getComponents()).contains(file2); + verifyStatistics(context, 1, 1, 1, 0); } @Test @@ -376,7 +390,8 @@ public void execute_detects_no_move_if_content_of_file_has_no_path_in_DB() { assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix).isNull(); - verifyStatistics(context, 0, null); + assertThat(addedFileRepository.getComponents()).containsOnly(file2); + verifyStatistics(context, 1, 0, 1, null); } @Test @@ -393,7 +408,8 @@ public void execute_detects_no_move_if_content_of_file_is_empty_in_report() { assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isZero(); - verifyStatistics(context, 1, 1); + assertThat(addedFileRepository.getComponents()).contains(file2); + verifyStatistics(context, 1, 1, 1, 0); assertThat(logTester.logs(LoggerLevel.DEBUG)).contains("max score in matrix is less than min required score (85). Do nothing."); } @@ -412,7 +428,8 @@ public void execute_detects_no_move_if_two_added_files_have_same_content_as_the_ assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isEqualTo(100); - verifyStatistics(context, 1, 2); + assertThat(addedFileRepository.getComponents()).containsOnly(file2, file3); + verifyStatistics(context, 2, 1, 2, 0); } @Test @@ -431,24 +448,27 @@ public void execute_detects_no_move_if_two_deleted_files_have_same_content_as_th assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isEqualTo(100); - verifyStatistics(context, 2, 1); + assertThat(addedFileRepository.getComponents()).containsOnly(file3); + verifyStatistics(context, 1, 2, 1, 0); } @Test - public void execute_detects_no_move_if_two_files_are_empty() { + public void execute_detects_no_move_if_two_files_are_empty_in_DB() { analysisMetadataHolder.setBaseAnalysis(ANALYSIS); Component file1 = fileComponent(FILE_1_REF, null); Component file2 = fileComponent(FILE_2_REF, null); insertFiles(file1.getDbKey(), file2.getDbKey()); insertContentOfFileInDb(file1.getDbKey(), null); insertContentOfFileInDb(file2.getDbKey(), null); + setFilesInReport(file1, file2); TestComputationStepContext context = new TestComputationStepContext(); underTest.execute(context); assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix).isNull(); - verifyStatistics(context, 2, null); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 2, 2, 0, null); } @Test @@ -485,7 +505,8 @@ public void execute_detects_several_moves() { assertThat(originalFile5.getKey()).isEqualTo(dtos[3].getDbKey()); assertThat(originalFile5.getUuid()).isEqualTo(dtos[3].uuid()); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isGreaterThan(MIN_REQUIRED_SCORE); - verifyStatistics(context, 4, 2); + assertThat(addedFileRepository.getComponents()).isEmpty(); + verifyStatistics(context, 3, 4, 2, 2); } @Test @@ -505,7 +526,7 @@ public void execute_does_not_compute_any_distance_if_all_files_sizes_are_all_too assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty(); assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isZero(); - verifyStatistics(context, 2, 2); + verifyStatistics(context, 2, 2, 2, 0); } /** @@ -559,7 +580,7 @@ public void real_life_use_case() throws Exception { .isEqualTo("1242_make_analysis_uuid_not_null_on_duplications_index.rb"); assertThat(movedFilesRepository.getOriginalFile(addComponentUuidAndAnalysisUuidColumnToDuplicationsIndex).get().getKey()) .isEqualTo("AddComponentUuidColumnToDuplicationsIndex.java"); - verifyStatistics(context, 12, 6); + verifyStatistics(context, comps.values().size(), 12, 6, 3); } private String[] readLines(File filename) throws IOException { @@ -640,8 +661,30 @@ public void dumpAsCsv(ScoreMatrix scoreMatrix) { } } - private static void verifyStatistics(TestComputationStepContext context, @Nullable Integer expectedDbFiles, @Nullable Integer expectedAddedFiles) { + private static void verifyStatistics(TestComputationStepContext context, + @Nullable Integer expectedReportFiles, @Nullable Integer expectedDbFiles, + @Nullable Integer expectedAddedFiles, @Nullable Integer expectedMovedFiles) { + context.getStatistics().assertValue("reportFiles", expectedReportFiles); context.getStatistics().assertValue("dbFiles", expectedDbFiles); context.getStatistics().assertValue("addedFiles", expectedAddedFiles); + context.getStatistics().assertValue("movedFiles", expectedMovedFiles); + } + + private static class RecordingMutableAddedFileRepository implements MutableAddedFileRepository { + private final List components = new ArrayList<>(); + + @Override + public void register(Component file) { + components.add(file); + } + + @Override + public boolean isAdded(Component component) { + throw new UnsupportedOperationException("isAdded should not be called"); + } + + public List getComponents() { + return components; + } } } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculatorTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculatorTest.java index 5f8b97931a9d..9c0f914c41fa 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculatorTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculatorTest.java @@ -19,18 +19,25 @@ */ package org.sonar.ce.task.projectanalysis.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.Arrays; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.function.BiConsumer; import org.junit.Before; import org.junit.Test; import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; import org.sonar.api.rule.RuleKey; import org.sonar.ce.task.projectanalysis.analysis.Analysis; import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule; import org.sonar.ce.task.projectanalysis.analysis.ScannerPlugin; import org.sonar.ce.task.projectanalysis.component.Component; +import org.sonar.ce.task.projectanalysis.filemove.AddedFileRepository; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRule; import org.sonar.ce.task.projectanalysis.qualityprofile.ActiveRulesHolder; import org.sonar.ce.task.projectanalysis.scm.Changeset; @@ -54,12 +61,12 @@ import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; +@RunWith(DataProviderRunner.class) public class IssueCreationDateCalculatorTest { private static final String COMPONENT_UUID = "ab12"; @org.junit.Rule public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule(); - @org.junit.Rule public ExpectedException exception = ExpectedException.none(); @@ -70,10 +77,13 @@ public class IssueCreationDateCalculatorTest { private RuleKey ruleKey = RuleKey.of("reop", "rule"); private DefaultIssue issue = mock(DefaultIssue.class); private ActiveRule activeRule = mock(ActiveRule.class); - private IssueCreationDateCalculator calculator; + + private IssueCreationDateCalculator underTest; + private Analysis baseAnalysis = mock(Analysis.class); private Map scannerPlugins = new HashMap<>(); private RuleRepository ruleRepository = mock(RuleRepository.class); + private AddedFileRepository addedFileRepository = mock(AddedFileRepository.class); private ScmInfo scmInfo; private Rule rule = mock(Rule.class); @@ -82,9 +92,9 @@ public void before() { analysisMetadataHolder.setScannerPluginsByKey(scannerPlugins); analysisMetadataHolder.setAnalysisDate(new Date()); when(component.getUuid()).thenReturn(COMPONENT_UUID); - calculator = new IssueCreationDateCalculator(analysisMetadataHolder, scmInfoRepository, issueUpdater, activeRulesHolder, ruleRepository); + underTest = new IssueCreationDateCalculator(analysisMetadataHolder, scmInfoRepository, issueUpdater, activeRulesHolder, ruleRepository, addedFileRepository); - when(ruleRepository.findByKey(ruleKey)).thenReturn(java.util.Optional.of(rule)); + when(ruleRepository.findByKey(ruleKey)).thenReturn(Optional.of(rule)); when(activeRulesHolder.get(any(RuleKey.class))) .thenReturn(Optional.empty()); when(activeRulesHolder.get(ruleKey)) @@ -94,13 +104,13 @@ public void before() { } @Test - public void should_not_change_date_if_no_scm_available() { + public void should_not_backdate_if_no_scm_available() { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); + makeIssueNew(); noScm(); - ruleCreatedAt(2800L); + setRuleCreatedAt(2800L); run(); @@ -108,13 +118,14 @@ public void should_not_change_date_if_no_scm_available() { } @Test - public void should_not_change_date_if_rule_and_plugin_and_base_plugin_are_old() { + @UseDataProvider("backdatingDateCases") + public void should_not_backdate_if_rule_and_plugin_and_base_plugin_are_old(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); - ruleCreatedAt(1500L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(1500L); rulePlugin("customjava"); pluginUpdatedAt("customjava", "java", 1700L); pluginUpdatedAt("java", 1700L); @@ -125,13 +136,14 @@ public void should_not_change_date_if_rule_and_plugin_and_base_plugin_are_old() } @Test - public void should_not_change_date_if_rule_and_plugin_are_old_and_no_base_plugin() { + @UseDataProvider("backdatingDateCases") + public void should_not_backdate_if_rule_and_plugin_are_old_and_no_base_plugin(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); - ruleCreatedAt(1500L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(1500L); rulePlugin("java"); pluginUpdatedAt("java", 1700L); @@ -141,13 +153,14 @@ public void should_not_change_date_if_rule_and_plugin_are_old_and_no_base_plugin } @Test - public void should_not_change_date_if_issue_existed_before() { + @UseDataProvider("backdatingDateCases") + public void should_not_backdate_if_issue_existed_before(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - existingIssue(); - withScm(1200L); - ruleCreatedAt(2800L); + makeIssueNotNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(2800L); run(); @@ -159,9 +172,8 @@ public void should_not_fail_for_issue_about_to_be_closed() { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - existingIssue(); - when(issue.getRuleKey()) - .thenReturn(RuleKey.of("repo", "disabled")); + makeIssueNotNew(); + setIssueBelongToNonExistingRule(); run(); @@ -173,8 +185,8 @@ public void should_fail_if_rule_is_not_found() { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - when(ruleRepository.findByKey(ruleKey)).thenReturn(java.util.Optional.empty()); - newIssue(); + when(ruleRepository.findByKey(ruleKey)).thenReturn(Optional.empty()); + makeIssueNew(); exception.expect(IllegalStateException.class); exception.expectMessage("The rule with key 'reop:rule' raised an issue, but no rule with that key was found"); @@ -182,155 +194,159 @@ public void should_fail_if_rule_is_not_found() { } @Test - public void should_change_date_if_scm_is_available_and_rule_is_new() { + @UseDataProvider("backdatingDateCases") + public void should_backdate_date_if_scm_is_available_and_rule_is_new(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); - ruleCreatedAt(2800L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(2800L); run(); - assertChangeOfCreationDateTo(1200L); + assertChangeOfCreationDateTo(expectedDate); } @Test - public void should_change_date_if_scm_is_available_and_first_analysis() { - analysisMetadataHolder.setBaseAnalysis(null); + @UseDataProvider("backdatingDateCases") + public void should_backdate_date_if_scm_is_available_and_first_analysis(BiConsumer configure, long expectedDate) { + currentAnalysisIsFirstAnalysis(); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); run(); - assertChangeOfCreationDateTo(1200L); + assertChangeOfCreationDateTo(expectedDate); } @Test - public void should_change_date_if_scm_is_available_and_plugin_is_new() { + @UseDataProvider("backdatingDateCases") + public void should_backdate_date_if_scm_is_available_and_current_component_is_new_file(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); - ruleCreatedAt(1500L); - rulePlugin("java"); - pluginUpdatedAt("java", 2500L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + currentComponentIsNewFile(); run(); - assertChangeOfCreationDateTo(1200L); + assertChangeOfCreationDateTo(expectedDate); } - @Test - public void should_change_date_if_scm_is_available_and_base_plugin_is_new() { + @UseDataProvider("backdatingDateCases") + public void should_backdate_if_scm_is_available_and_plugin_is_new(BiConsumer configure, long expectedDate) { previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - withScm(1200L); - ruleCreatedAt(1500L); - rulePlugin("customjava"); - pluginUpdatedAt("customjava", "java", 1000L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(1500L); + rulePlugin("java"); pluginUpdatedAt("java", 2500L); run(); - assertChangeOfCreationDateTo(1200L); + assertChangeOfCreationDateTo(expectedDate); } @Test - public void should_backdate_external_issues() { - analysisMetadataHolder.setBaseAnalysis(null); + @UseDataProvider("backdatingDateCases") + public void should_backdate_if_scm_is_available_and_base_plugin_is_new(BiConsumer configure, long expectedDate) { + previousAnalysisWas(2000L); currentAnalysisIs(3000L); - newIssue(); - when(rule.isExternal()).thenReturn(true); - when(issue.getLocations()).thenReturn(DbIssues.Locations.newBuilder().setTextRange(range(2, 3)).build()); - withScmAt(2, 1200L); - withScmAt(3, 1300L); + makeIssueNew(); + configure.accept(issue, createMockScmInfo()); + setRuleCreatedAt(1500L); + rulePlugin("customjava"); + pluginUpdatedAt("customjava", "java", 1000L); + pluginUpdatedAt("java", 2500L); run(); - assertChangeOfCreationDateTo(1300L); - verifyZeroInteractions(activeRulesHolder); + assertChangeOfCreationDateTo(expectedDate); } @Test - public void should_use_primary_location_when_backdating() { - analysisMetadataHolder.setBaseAnalysis(null); + @UseDataProvider("backdatingDateCases") + public void should_backdate_external_issues(BiConsumer configure, long expectedDate) { + currentAnalysisIsFirstAnalysis(); currentAnalysisIs(3000L); - newIssue(); - when(issue.getLocations()).thenReturn(DbIssues.Locations.newBuilder().setTextRange(range(2, 3)).build()); - withScmAt(2, 1200L); - withScmAt(3, 1300L); + makeIssueNew(); + when(rule.isExternal()).thenReturn(true); + configure.accept(issue, createMockScmInfo()); run(); - assertChangeOfCreationDateTo(1300L); + assertChangeOfCreationDateTo(expectedDate); + verifyZeroInteractions(activeRulesHolder); } - @Test - public void should_use_flows_location_when_backdating() { - analysisMetadataHolder.setBaseAnalysis(null); - currentAnalysisIs(3000L); - - newIssue(); - Builder builder = DbIssues.Locations.newBuilder() - .setTextRange(range(2, 3)); - Flow.Builder secondary = Flow.newBuilder().addLocation(Location.newBuilder().setTextRange(range(4, 5))); - builder.addFlow(secondary).build(); - Flow.Builder flow = Flow.newBuilder() - .addLocation(Location.newBuilder().setTextRange(range(6, 7)).setComponentId(COMPONENT_UUID)) - .addLocation(Location.newBuilder().setTextRange(range(8, 9)).setComponentId(COMPONENT_UUID)); - builder.addFlow(flow).build(); - when(issue.getLocations()).thenReturn(builder.build()); - withScmAt(2, 1200L); - withScmAt(3, 1300L); - withScmAt(4, 1400L); - withScmAt(5, 1500L); - // some lines missing should be ok - withScmAt(9, 1900L); - - run(); - - assertChangeOfCreationDateTo(1900L); + @DataProvider + public static Object[][] backdatingDateCases() { + return new Object[][] { + {new NoIssueLocation(), 1200L}, + {new OnlyPrimaryLocation(), 1300L}, + {new FlowOnCurrentFileOnly(), 1900L}, + {new FlowOnMultipleFiles(), 1700L} + }; } - @Test - public void should_ignore_flows_location_outside_current_file_when_backdating() { - analysisMetadataHolder.setBaseAnalysis(null); - currentAnalysisIs(3000L); - - newIssue(); - Builder builder = DbIssues.Locations.newBuilder() - .setTextRange(range(2, 3)); - Flow.Builder secondary = Flow.newBuilder().addLocation(Location.newBuilder().setTextRange(range(4, 5))); - builder.addFlow(secondary).build(); - Flow.Builder flow = Flow.newBuilder() - .addLocation(Location.newBuilder().setTextRange(range(6, 7)).setComponentId(COMPONENT_UUID)) - .addLocation(Location.newBuilder().setTextRange(range(8, 9)).setComponentId("another")); - builder.addFlow(flow).build(); - when(issue.getLocations()).thenReturn(builder.build()); - withScmAt(2, 1200L); - withScmAt(3, 1300L); - withScmAt(4, 1400L); - withScmAt(5, 1500L); - withScmAt(6, 1600L); - withScmAt(7, 1700L); - withScmAt(8, 1800L); - withScmAt(9, 1900L); + private static class NoIssueLocation implements BiConsumer { + @Override + public void accept(DefaultIssue issue, ScmInfo scmInfo) { + setDateOfLatestScmChangeset(scmInfo, 1200L); + } + } - run(); + private static class OnlyPrimaryLocation implements BiConsumer { + @Override + public void accept(DefaultIssue issue, ScmInfo scmInfo) { + when(issue.getLocations()).thenReturn(DbIssues.Locations.newBuilder().setTextRange(range(2, 3)).build()); + setDateOfChangetsetAtLine(scmInfo, 2, 1200L); + setDateOfChangetsetAtLine(scmInfo, 3, 1300L); + } + } - assertChangeOfCreationDateTo(1700L); + private static class FlowOnCurrentFileOnly implements BiConsumer { + @Override + public void accept(DefaultIssue issue, ScmInfo scmInfo) { + Builder locations = DbIssues.Locations.newBuilder() + .setTextRange(range(2, 3)) + .addFlow(newFlow(newLocation(4, 5))) + .addFlow(newFlow(newLocation(6, 7, COMPONENT_UUID), newLocation(8, 9, COMPONENT_UUID))); + when(issue.getLocations()).thenReturn(locations.build()); + setDateOfChangetsetAtLine(scmInfo, 2, 1200L); + setDateOfChangetsetAtLine(scmInfo, 3, 1300L); + setDateOfChangetsetAtLine(scmInfo, 4, 1400L); + setDateOfChangetsetAtLine(scmInfo, 5, 1500L); + // some lines missing should be ok + setDateOfChangetsetAtLine(scmInfo, 9, 1900L); + } } - private org.sonar.db.protobuf.DbCommons.TextRange.Builder range(int startLine, int endLine) { - return TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine); + private static class FlowOnMultipleFiles implements BiConsumer { + @Override + public void accept(DefaultIssue issue, ScmInfo scmInfo) { + Builder locations = DbIssues.Locations.newBuilder() + .setTextRange(range(2, 3)) + .addFlow(newFlow(newLocation(4, 5))) + .addFlow(newFlow(newLocation(6, 7, COMPONENT_UUID), newLocation(8, 9, "another"))); + when(issue.getLocations()).thenReturn(locations.build()); + setDateOfChangetsetAtLine(scmInfo, 2, 1200L); + setDateOfChangetsetAtLine(scmInfo, 3, 1300L); + setDateOfChangetsetAtLine(scmInfo, 4, 1400L); + setDateOfChangetsetAtLine(scmInfo, 5, 1500L); + setDateOfChangetsetAtLine(scmInfo, 6, 1600L); + setDateOfChangetsetAtLine(scmInfo, 7, 1700L); + setDateOfChangetsetAtLine(scmInfo, 8, 1800L); + setDateOfChangetsetAtLine(scmInfo, 9, 1900L); + } } private void previousAnalysisWas(long analysisDate) { @@ -347,47 +363,60 @@ private void pluginUpdatedAt(String pluginKey, String basePluginKey, long update scannerPlugins.put(pluginKey, new ScannerPlugin(pluginKey, basePluginKey, updatedAt)); } + private AnalysisMetadataHolderRule currentAnalysisIsFirstAnalysis() { + return analysisMetadataHolder.setBaseAnalysis(null); + } + private void currentAnalysisIs(long analysisDate) { analysisMetadataHolder.setAnalysisDate(analysisDate); } - private void newIssue() { + private void currentComponentIsNewFile() { + when(component.getType()).thenReturn(Component.Type.FILE); + when(addedFileRepository.isAdded(component)).thenReturn(true); + } + + private void makeIssueNew() { when(issue.isNew()) .thenReturn(true); } - private void existingIssue() { + private void makeIssueNotNew() { when(issue.isNew()) .thenReturn(false); } + private void setIssueBelongToNonExistingRule() { + when(issue.getRuleKey()) + .thenReturn(RuleKey.of("repo", "disabled")); + } + private void noScm() { when(scmInfoRepository.getScmInfo(component)) - .thenReturn(java.util.Optional.empty()); + .thenReturn(Optional.empty()); } - private void withScm(long blame) { - createMockScmInfo(); - Changeset changeset = Changeset.newChangesetBuilder().setDate(blame).setRevision("1").build(); + private static void setDateOfLatestScmChangeset(ScmInfo scmInfo, long date) { + Changeset changeset = Changeset.newChangesetBuilder().setDate(date).setRevision("1").build(); when(scmInfo.getLatestChangeset()).thenReturn(changeset); } - private void createMockScmInfo() { + private static void setDateOfChangetsetAtLine(ScmInfo scmInfo, int line, long date) { + Changeset changeset = Changeset.newChangesetBuilder().setDate(date).setRevision("1").build(); + when(scmInfo.hasChangesetForLine(line)).thenReturn(true); + when(scmInfo.getChangesetForLine(line)).thenReturn(changeset); + } + + private ScmInfo createMockScmInfo() { if (scmInfo == null) { scmInfo = mock(ScmInfo.class); when(scmInfoRepository.getScmInfo(component)) - .thenReturn(java.util.Optional.of(scmInfo)); + .thenReturn(Optional.of(scmInfo)); } + return scmInfo; } - private void withScmAt(int line, long blame) { - createMockScmInfo(); - Changeset changeset = Changeset.newChangesetBuilder().setDate(blame).setRevision("1").build(); - when(scmInfo.hasChangesetForLine(line)).thenReturn(true); - when(scmInfo.getChangesetForLine(line)).thenReturn(changeset); - } - - private void ruleCreatedAt(long createdAt) { + private void setRuleCreatedAt(long createdAt) { when(activeRule.getCreatedAt()).thenReturn(createdAt); } @@ -395,10 +424,28 @@ private void rulePlugin(String pluginKey) { when(activeRule.getPluginKey()).thenReturn(pluginKey); } + private static Location newLocation(int startLine, int endLine) { + return Location.newBuilder().setTextRange(range(startLine, endLine)).build(); + } + + private static Location newLocation(int startLine, int endLine, String componentUuid) { + return Location.newBuilder().setTextRange(range(startLine, endLine)).setComponentId(componentUuid).build(); + } + + private static org.sonar.db.protobuf.DbCommons.TextRange range(int startLine, int endLine) { + return TextRange.newBuilder().setStartLine(startLine).setEndLine(endLine).build(); + } + + private static Flow newFlow(Location... locations) { + Flow.Builder builder = Flow.newBuilder(); + Arrays.stream(locations).forEach(builder::addLocation); + return builder.build(); + } + private void run() { - calculator.beforeComponent(component); - calculator.onIssue(component, issue); - calculator.afterComponent(component); + underTest.beforeComponent(component); + underTest.onIssue(component, issue); + underTest.afterComponent(component); } private void assertNoChangeOfCreationDate() {