]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11513 backdate issues on new files
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 22 Nov 2018 14:23:23 +0000 (15:23 +0100)
committerSonarTech <sonartech@sonarsource.com>
Mon, 3 Dec 2018 19:20:58 +0000 (20:20 +0100)
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepository.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImpl.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStep.java
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/filemove/MutableAddedFileRepository.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculator.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/AddedFileRepositoryImplTest.java [new file with mode: 0644]
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/filemove/FileMoveDetectionStepTest.java
server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueCreationDateCalculatorTest.java

index f3d2d0a1e7728820008a48e3f5d76caca0b6c6fb..a6bc5c9d28a5b7ead0c2272b02ddadf863adabeb 100644 (file)
@@ -43,6 +43,7 @@ import org.sonar.ce.task.projectanalysis.duplication.DuplicationMeasures;
 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 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
       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 (file)
index 0000000..e958d55
--- /dev/null
@@ -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 (file)
index 0000000..162becd
--- /dev/null
@@ -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<Component> 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");
+  }
+}
index 0b81525933e7c0f9756017eee45caa6c421bfd7d..19f43618764fc8177cd3800caae4d86d08fe0654 100644 (file)
@@ -22,7 +22,6 @@ package org.sonar.ce.task.projectanalysis.filemove;
 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 com.google.common.collect.FluentIterable.from;
 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<ScoreMatrix.ScoreFile> 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 class FileMoveDetectionStep implements ComputationStep {
     this.movedFilesRepository = movedFilesRepository;
     this.sourceLinesHash = sourceLinesHash;
     this.scoreMatrixDumper = scoreMatrixDumper;
+    this.addedFileRepository = addedFileRepository;
   }
 
   @Override
@@ -103,25 +104,30 @@ public class FileMoveDetectionStep implements ComputationStep {
     Profiler p = Profiler.createIfTrace(LOG);
 
     p.start();
-    Map<String, DbComponent> dbFilesByKey = getDbFilesByKey();
-    context.getStatistics().add("dbFiles", dbFilesByKey.size());
-    if (dbFilesByKey.isEmpty()) {
-      LOG.debug("Previous snapshot has no file. Do nothing.");
-      return;
-    }
-
     Map<String, Component> 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<String> addedFileKeys = ImmutableSet.copyOf(Sets.difference(reportFilesByKey.keySet(), dbFilesByKey.keySet()));
+    Map<String, DbComponent> dbFilesByKey = getDbFilesByKey();
+    context.getStatistics().add("dbFiles", dbFilesByKey.size());
+
+    Set<String> addedFileKeys = difference(reportFilesByKey.keySet(), dbFilesByKey.keySet());
     context.getStatistics().add("addedFiles", addedFileKeys.size());
-    Set<String> 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<String> 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 class FileMoveDetectionStep implements ComputationStep {
 
     // 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 class FileMoveDetectionStep implements ComputationStep {
     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<String> difference(Set<String> set1, Set<String> set2) {
+    if (set1.isEmpty() || set2.isEmpty()) {
+      return set1;
+    }
+    return Sets.difference(set1, set2).immutableCopy();
   }
 
   private void registerMatches(Map<String, DbComponent> dbFilesByKey, Map<String, Component> reportFilesByKey, ElectedMatches electedMatches) {
@@ -161,6 +178,22 @@ public class FileMoveDetectionStep implements ComputationStep {
     }
   }
 
+  private void registerAddedFiles(Set<String> addedFileKeys, Map<String, Component> reportFilesByKey, @Nullable ElectedMatches electedMatches) {
+    if (electedMatches == null || electedMatches.isEmpty()) {
+      addedFileKeys.stream()
+        .map(reportFilesByKey::get)
+        .forEach(addedFileRepository::register);
+    } else {
+      Set<String> reallyAddedFileKeys = new HashSet<>(addedFileKeys);
+      for (Match electedMatch : electedMatches) {
+        reallyAddedFileKeys.remove(electedMatch.getReportKey());
+      }
+      reallyAddedFileKeys.stream()
+        .map(reportFilesByKey::get)
+        .forEach(addedFileRepository::register);
+    }
+  }
+
   private Map<String, DbComponent> getDbFilesByKey() {
     try (DbSession dbSession = dbClient.openSession(false)) {
       ImmutableList.Builder<DbComponent> builder = ImmutableList.builder();
@@ -402,5 +435,9 @@ public class FileMoveDetectionStep implements ComputationStep {
     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 (file)
index 0000000..102c5de
--- /dev/null
@@ -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);
+}
index 83cc898e07e2bf11b5cf73b4d1421055c719a336..a0350e3907fdc5f002bdf51b1f3c8d1432bde90c 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.ce.task.projectanalysis.analysis.Analysis;
 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 class IssueCreationDateCalculator extends IssueVisitor {
     if (!issue.isNew()) {
       return;
     }
+
     Optional<Long> 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 (file)
index 0000000..781c5fb
--- /dev/null
@@ -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;
+  }
+}
index 69b34878d28629295091ef39419b3c6986885647..b38605016af320482ddff4d93b8c1a5abd444ca9 100644 (file)
@@ -22,7 +22,9 @@ package org.sonar.ce.task.projectanalysis.filemove;
 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 class FileMoveDetectionStepTest {
   }
 
   @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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
 
     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 class FileMoveDetectionStepTest {
 
     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 class FileMoveDetectionStepTest {
 
     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 class FileMoveDetectionStepTest {
 
     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 class FileMoveDetectionStepTest {
 
     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 class FileMoveDetectionStepTest {
     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 class FileMoveDetectionStepTest {
 
     assertThat(movedFilesRepository.getComponentsWithOriginal()).isEmpty();
     assertThat(scoreMatrixDumper.scoreMatrix.getMaxScore()).isZero();
-    verifyStatistics(context, 2, 2);
+    verifyStatistics(context, 2, 2, 2, 0);
   }
 
   /**
@@ -559,7 +580,7 @@ public class FileMoveDetectionStepTest {
       .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 class FileMoveDetectionStepTest {
     }
   }
 
-  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<Component> 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<Component> getComponents() {
+      return components;
+    }
   }
 }
index 5f8b97931a9d7a41770b2c153ca3b53069883230..9c0f914c41fa6e68c60520b00197b77ccb92fd9b 100644 (file)
  */
 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.verify;
 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<String, ScannerPlugin> 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 class IssueCreationDateCalculatorTest {
     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 class IssueCreationDateCalculatorTest {
   }
 
   @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 class IssueCreationDateCalculatorTest {
   }
 
   @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<DefaultIssue, ScmInfo> 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 class IssueCreationDateCalculatorTest {
   }
 
   @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<DefaultIssue, ScmInfo> 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 class IssueCreationDateCalculatorTest {
   }
 
   @Test
-  public void should_not_change_date_if_issue_existed_before() {
+  @UseDataProvider("backdatingDateCases")
+  public void should_not_backdate_if_issue_existed_before(BiConsumer<DefaultIssue, ScmInfo> 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 class IssueCreationDateCalculatorTest {
     previousAnalysisWas(2000L);
     currentAnalysisIs(3000L);
 
-    existingIssue();
-    when(issue.getRuleKey())
-      .thenReturn(RuleKey.of("repo", "disabled"));
+    makeIssueNotNew();
+    setIssueBelongToNonExistingRule();
 
     run();
 
@@ -173,8 +185,8 @@ public class IssueCreationDateCalculatorTest {
     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 class IssueCreationDateCalculatorTest {
   }
 
   @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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> 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<DefaultIssue, ScmInfo> {
+    @Override
+    public void accept(DefaultIssue issue, ScmInfo scmInfo) {
+      setDateOfLatestScmChangeset(scmInfo, 1200L);
+    }
+  }
 
-    run();
+  private static class OnlyPrimaryLocation implements BiConsumer<DefaultIssue, ScmInfo> {
+    @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<DefaultIssue, ScmInfo> {
+    @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<DefaultIssue, ScmInfo> {
+    @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 @@ public class IssueCreationDateCalculatorTest {
     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 @@ public class IssueCreationDateCalculatorTest {
     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() {