]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-3321 support file move in issue tracking
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 30 May 2016 14:41:26 +0000 (16:41 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Thu, 2 Jun 2016 12:01:53 +0000 (14:01 +0200)
server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/issue/IntegrateIssuesVisitor.java
server/sonar-server/src/main/java/org/sonar/server/computation/issue/IssueLifecycle.java
server/sonar-server/src/main/java/org/sonar/server/computation/issue/MovedIssueVisitor.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/issue/TrackerBaseInputFactory.java
server/sonar-server/src/test/java/org/sonar/server/computation/issue/IntegrateIssuesVisitorTest.java
server/sonar-server/src/test/java/org/sonar/server/computation/issue/MovedIssueVisitorTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/issue/TrackerBaseInputFactoryTest.java [new file with mode: 0644]

index b79ff4bdc7e82d4bc7a2f19728a54abcbcf6df84..ab1b99878268e671ff61ecc0ed156d6453b31c6b 100644 (file)
@@ -55,6 +55,7 @@ import org.sonar.server.computation.issue.IssueCounter;
 import org.sonar.server.computation.issue.IssueLifecycle;
 import org.sonar.server.computation.issue.IssueVisitors;
 import org.sonar.server.computation.issue.LoadComponentUuidsHavingOpenIssuesVisitor;
+import org.sonar.server.computation.issue.MovedIssueVisitor;
 import org.sonar.server.computation.issue.NewEffortAggregator;
 import org.sonar.server.computation.issue.NewEffortCalculator;
 import org.sonar.server.computation.issue.RuleRepositoryImpl;
@@ -197,6 +198,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop
       NewEffortAggregator.class,
       IssueAssigner.class,
       IssueCounter.class,
+      MovedIssueVisitor.class,
 
       // visitors : order is important, measure computers must be executed at the end in order to access to every measures / issues
       LoadComponentUuidsHavingOpenIssuesVisitor.class,
index 1a1ca8f3f25d501fa1a8b344ac8cc4dc1d24bb5e..82e75c69ecc52ebeda88dc6e21dd7cf61ff80a4c 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.computation.issue;
 
+import com.google.common.base.Optional;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -27,6 +28,7 @@ import org.sonar.core.issue.tracking.Tracking;
 import org.sonar.server.computation.component.Component;
 import org.sonar.server.computation.component.CrawlerDepthLimit;
 import org.sonar.server.computation.component.TypeAwareVisitorAdapter;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
 import org.sonar.server.util.cache.DiskCache;
 
 import static org.sonar.server.computation.component.ComponentVisitor.Order.POST_ORDER;
@@ -39,11 +41,12 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
   private final IssueVisitors issueVisitors;
   private final MutableComponentIssuesRepository componentIssuesRepository;
   private final ComponentsWithUnprocessedIssues componentsWithUnprocessedIssues;
+  private final MovedFilesRepository movedFilesRepository;
 
   private final List<DefaultIssue> componentIssues = new ArrayList<>();
 
   public IntegrateIssuesVisitor(TrackerExecution tracker, IssueCache issueCache, IssueLifecycle issueLifecycle, IssueVisitors issueVisitors,
-    ComponentsWithUnprocessedIssues componentsWithUnprocessedIssues, MutableComponentIssuesRepository componentIssuesRepository) {
+    ComponentsWithUnprocessedIssues componentsWithUnprocessedIssues, MutableComponentIssuesRepository componentIssuesRepository, MovedFilesRepository movedFilesRepository) {
     super(CrawlerDepthLimit.FILE, POST_ORDER);
     this.tracker = tracker;
     this.issueCache = issueCache;
@@ -51,13 +54,19 @@ public class IntegrateIssuesVisitor extends TypeAwareVisitorAdapter {
     this.issueVisitors = issueVisitors;
     this.componentsWithUnprocessedIssues = componentsWithUnprocessedIssues;
     this.componentIssuesRepository = componentIssuesRepository;
+    this.movedFilesRepository = movedFilesRepository;
   }
 
   @Override
   public void visitAny(Component component) {
     componentIssues.clear();
     processIssues(component);
+
     componentsWithUnprocessedIssues.remove(component.getUuid());
+    Optional<MovedFilesRepository.OriginalFile> originalFile = movedFilesRepository.getOriginalFile(component);
+    if (originalFile.isPresent()) {
+      componentsWithUnprocessedIssues.remove(originalFile.get().getUuid());
+    }
     componentIssuesRepository.setIssues(component, componentIssues);
   }
 
index caba1df318d5abed0631397104aeacb74ea47b98..40f2f105bbbd4052da76cfe99e5ab8b85d98e7ec 100644 (file)
@@ -85,6 +85,12 @@ public class IssueLifecycle {
     } else {
       updater.setPastSeverity(raw, base.severity(), changeContext);
     }
+    // set component/module related fields from base in case current component has been moved
+    // (in which case base issue belongs to original file and raw issue to component)
+    raw.setComponentUuid(base.componentUuid());
+    raw.setComponentKey(base.componentKey());
+    raw.setModuleUuid(base.moduleUuid());
+    raw.setModuleUuidPath(base.moduleUuidPath());
 
     // fields coming from raw
     updater.setPastLine(raw, base.getLine());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/issue/MovedIssueVisitor.java b/server/sonar-server/src/main/java/org/sonar/server/computation/issue/MovedIssueVisitor.java
new file mode 100644 (file)
index 0000000..a5ee6d0
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.issue;
+
+import com.google.common.base.Optional;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
+import org.sonar.server.computation.filemove.MovedFilesRepository.OriginalFile;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class MovedIssueVisitor extends IssueVisitor {
+  private final MovedFilesRepository movedFilesRepository;
+
+  public MovedIssueVisitor(MovedFilesRepository movedFilesRepository) {
+    this.movedFilesRepository = movedFilesRepository;
+  }
+
+  @Override
+  public void onIssue(Component component, DefaultIssue issue) {
+    if (component.getType() != Component.Type.FILE || component.getUuid().equals(issue.componentUuid())) {
+      return;
+    }
+    Optional<OriginalFile> originalFileOptional = movedFilesRepository.getOriginalFile(component);
+    checkState(originalFileOptional.isPresent(),
+      "Issue %s for component %s has a different component key but no original file exist in MovedFilesRepository",
+      issue, component);
+    OriginalFile originalFile = originalFileOptional.get();
+    checkState(originalFile.getUuid().equals(issue.componentUuid()),
+        "Issue %s doesn't belong to file %s registered as original file of current file %s",
+        issue, originalFile.getUuid(), component);
+
+    // it's enough to change component uuid, only this field is written to table ISSUES
+    // other fields (such as module, modulePath, componentKey) are read-only and result of a join with other tables
+    issue.setComponentUuid(component.getUuid());
+    issue.setComponentKey(component.getKey());
+    issue.setModuleUuid(null);
+    issue.setModuleUuidPath(null);
+
+    // ensure issue is updated in DB
+    issue.setChanged(true);
+  }
+}
index 55da6dccd9e715f3591e470914d968518c7fac20..5714d4dbd8b9a0d9778d4b45977b3d52401e9b06 100644 (file)
@@ -21,6 +21,8 @@ package org.sonar.server.computation.issue;
 
 import java.util.Collections;
 import java.util.List;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
 import org.sonar.core.issue.DefaultIssue;
 import org.sonar.core.issue.tracking.Input;
 import org.sonar.core.issue.tracking.LazyInput;
@@ -29,6 +31,8 @@ import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.MyBatis;
 import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
+import org.sonar.server.computation.filemove.MovedFilesRepository.OriginalFile;
 
 /**
  * Factory of {@link Input} of base data for issue tracking. Data are lazy-loaded.
@@ -38,21 +42,26 @@ public class TrackerBaseInputFactory {
 
   private final BaseIssuesLoader baseIssuesLoader;
   private final DbClient dbClient;
+  private final MovedFilesRepository movedFilesRepository;
 
-  public TrackerBaseInputFactory(BaseIssuesLoader baseIssuesLoader, DbClient dbClient) {
+  public TrackerBaseInputFactory(BaseIssuesLoader baseIssuesLoader, DbClient dbClient, MovedFilesRepository movedFilesRepository) {
     this.baseIssuesLoader = baseIssuesLoader;
     this.dbClient = dbClient;
+    this.movedFilesRepository = movedFilesRepository;
   }
 
   public Input<DefaultIssue> create(Component component) {
-    return new BaseLazyInput(component);
+    return new BaseLazyInput(component, movedFilesRepository.getOriginalFile(component).orNull());
   }
 
   private class BaseLazyInput extends LazyInput<DefaultIssue> {
     private final Component component;
+    @CheckForNull
+    private final String effectiveUuid;
 
-    private BaseLazyInput(Component component) {
+    private BaseLazyInput(Component component, @Nullable OriginalFile originalFile) {
       this.component = component;
+      this.effectiveUuid = originalFile == null ? component.getUuid() : originalFile.getUuid();
     }
 
     @Override
@@ -63,7 +72,7 @@ public class TrackerBaseInputFactory {
       
       DbSession session = dbClient.openSession(false);
       try {
-        List<String> hashes = dbClient.fileSourceDao().selectLineHashes(session, component.getUuid());
+        List<String> hashes = dbClient.fileSourceDao().selectLineHashes(session, effectiveUuid);
         if (hashes == null || hashes.isEmpty()) {
           return EMPTY_LINE_HASH_SEQUENCE;
         }
@@ -75,7 +84,7 @@ public class TrackerBaseInputFactory {
 
     @Override
     protected List<DefaultIssue> loadIssues() {
-      return baseIssuesLoader.loadForComponentUuid(component.getUuid());
+      return baseIssuesLoader.loadForComponentUuid(effectiveUuid);
     }
   }
 }
index ea94c07be317a9310e49082f814d6a34e14b922c..8d6997804858046150009d5c1d4076ae89e3795d 100644 (file)
@@ -19,6 +19,7 @@
  */
 package org.sonar.server.computation.issue;
 
+import com.google.common.base.Optional;
 import java.util.Collections;
 import java.util.List;
 import org.junit.Before;
@@ -44,6 +45,7 @@ import org.sonar.server.computation.batch.BatchReportReaderRule;
 import org.sonar.server.computation.batch.TreeRootHolderRule;
 import org.sonar.server.computation.component.Component;
 import org.sonar.server.computation.component.TypeAwareVisitor;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
 import org.sonar.server.computation.issue.commonrule.CommonRuleEngineImpl;
 import org.sonar.server.computation.issue.filter.IssueFilter;
 import org.sonar.server.computation.qualityprofile.ActiveRulesHolderRule;
@@ -82,25 +84,18 @@ public class IntegrateIssuesVisitorTest {
 
   @Rule
   public TemporaryFolder temp = new TemporaryFolder();
-
   @Rule
   public DbTester dbTester = DbTester.create(System2.INSTANCE);
-
   @Rule
   public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule();
-
   @Rule
   public BatchReportReaderRule reportReader = new BatchReportReaderRule();
-
   @Rule
   public ActiveRulesHolderRule activeRulesHolderRule = new ActiveRulesHolderRule();
-
   @Rule
   public RuleRepositoryRule ruleRepositoryRule = new RuleRepositoryRule();
-
   @Rule
   public ComponentIssuesRepositoryRule componentIssuesRepository = new ComponentIssuesRepositoryRule(treeRootHolder);
-
   @Rule
   public SourceLinesRepositoryRule fileSourceRepository = new SourceLinesRepositoryRule();
 
@@ -109,8 +104,11 @@ public class IntegrateIssuesVisitorTest {
   IssueFilter issueFilter = mock(IssueFilter.class);
 
   BaseIssuesLoader baseIssuesLoader = new BaseIssuesLoader(treeRootHolder, dbTester.getDbClient(), ruleRepositoryRule, activeRulesHolderRule);
-  TrackerExecution tracker = new TrackerExecution(new TrackerBaseInputFactory(baseIssuesLoader, dbTester.getDbClient()), new TrackerRawInputFactory(treeRootHolder, reportReader,
-    fileSourceRepository, new CommonRuleEngineImpl(), issueFilter), new Tracker<DefaultIssue, DefaultIssue>());
+  MovedFilesRepository movedFilesRepository = mock(MovedFilesRepository.class);
+  TrackerExecution tracker = new TrackerExecution(new TrackerBaseInputFactory(baseIssuesLoader, dbTester.getDbClient(), movedFilesRepository),
+    new TrackerRawInputFactory(treeRootHolder, reportReader,
+      fileSourceRepository, new CommonRuleEngineImpl(), issueFilter),
+    new Tracker<>());
   IssueCache issueCache;
 
   IssueLifecycle issueLifecycle = mock(IssueLifecycle.class);
@@ -125,7 +123,8 @@ public class IntegrateIssuesVisitorTest {
     treeRootHolder.setRoot(PROJECT);
     issueCache = new IssueCache(temp.newFile(), System2.INSTANCE);
     when(issueFilter.accept(any(DefaultIssue.class), eq(FILE))).thenReturn(true);
-    underTest = new IntegrateIssuesVisitor(tracker, issueCache, issueLifecycle, issueVisitors, componentsWithUnprocessedIssues, componentIssuesRepository);
+    when(movedFilesRepository.getOriginalFile(any(Component.class))).thenReturn(Optional.<MovedFilesRepository.OriginalFile>absent());
+    underTest = new IntegrateIssuesVisitor(tracker, issueCache, issueLifecycle, issueVisitors, componentsWithUnprocessedIssues, componentIssuesRepository, movedFilesRepository);
   }
 
   @Test
@@ -264,6 +263,18 @@ public class IntegrateIssuesVisitorTest {
     assertThat(componentIssuesRepository.getIssues(PROJECT)).isEmpty();
   }
 
+  @Test
+  public void remove_uuid_of_original_file_from_componentsWithUnprocessedIssues_if_component_has_one() {
+    String originalFileUuid = "original file uuid";
+    componentsWithUnprocessedIssues.setUuids(newHashSet(FILE_UUID, originalFileUuid));
+    when(movedFilesRepository.getOriginalFile(FILE))
+      .thenReturn(Optional.of(new MovedFilesRepository.OriginalFile(4851, originalFileUuid, "original file key")));
+
+    underTest.visitAny(FILE);
+
+    assertThat(componentsWithUnprocessedIssues.getUuids()).isEmpty();
+  }
+
   private void addBaseIssue(RuleKey ruleKey) {
     ComponentDto project = ComponentTesting.newProjectDto(PROJECT_UUID).setKey(PROJECT_KEY);
     ComponentDto file = ComponentTesting.newFileDto(project, FILE_UUID).setKey(FILE_KEY);
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/issue/MovedIssueVisitorTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/issue/MovedIssueVisitorTest.java
new file mode 100644 (file)
index 0000000..3666b0d
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.issue;
+
+import com.google.common.base.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class MovedIssueVisitorTest {
+  private static final String FILE_UUID = "file uuid";
+  private static final Component FILE = ReportComponent.builder(Component.Type.FILE, 1).setUuid(FILE_UUID).build();
+
+  @org.junit.Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private MovedFilesRepository movedFilesRepository = mock(MovedFilesRepository.class);
+  private MovedIssueVisitor underTest = new MovedIssueVisitor(movedFilesRepository);
+
+  @Before
+  public void setUp() throws Exception {
+    when(movedFilesRepository.getOriginalFile(any(Component.class)))
+      .thenReturn(Optional.<MovedFilesRepository.OriginalFile>absent());
+  }
+
+  @Test
+  public void onIssue_does_not_alter_issue_if_component_is_not_a_file() {
+    DefaultIssue issue = mock(DefaultIssue.class);
+    underTest.onIssue(ReportComponent.builder(Component.Type.DIRECTORY, 1).build(), issue);
+
+    verifyZeroInteractions(issue);
+  }
+
+  @Test
+  public void onIssue_does_not_alter_issue_if_component_file_but_issue_has_the_same_component_uuid() {
+    DefaultIssue issue = mockIssue(FILE_UUID);
+    underTest.onIssue(FILE, issue);
+
+    verify(issue).componentUuid();
+    verifyNoMoreInteractions(issue);
+  }
+
+  @Test
+  public void onIssue_throws_ISE_if_issue_has_different_component_uuid_but_component_has_no_original_file() {
+    DefaultIssue issue = mockIssue("other component uuid");
+    when(issue.toString()).thenReturn("[bad issue, bad!]");
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Issue [bad issue, bad!] for component ReportComponent{ref=1, key='key_1', type=FILE} " +
+      "has a different component key but no original file exist in MovedFilesRepository");
+
+    underTest.onIssue(FILE, issue);
+  }
+
+  @Test
+  public void onIssue_throws_ISE_if_issue_has_different_component_uuid_from_component_but_it_is_not_the_one_of_original_file() {
+    DefaultIssue issue = mockIssue("other component uuid");
+    when(issue.toString()).thenReturn("[bad issue, bad!]");
+    when(movedFilesRepository.getOriginalFile(FILE))
+      .thenReturn(Optional.of(new MovedFilesRepository.OriginalFile(6451, "original uuid", "original key")));
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Issue [bad issue, bad!] doesn't belong to file original uuid registered as original " +
+      "file of current file ReportComponent{ref=1, key='key_1', type=FILE}");
+
+    underTest.onIssue(FILE, issue);
+  }
+
+  @Test
+  public void onIssue_update_component_and_module_fields_to_component_and_flag_issue_has_changed() {
+    MovedFilesRepository.OriginalFile originalFile = new MovedFilesRepository.OriginalFile(6451, "original uuid", "original key");
+    DefaultIssue issue = mockIssue(originalFile.getUuid());
+    when(movedFilesRepository.getOriginalFile(FILE))
+      .thenReturn(Optional.of(originalFile));
+
+    underTest.onIssue(FILE, issue);
+
+    verify(issue).setComponentUuid(FILE.getUuid());
+    verify(issue).setComponentKey(FILE.getKey());
+    verify(issue).setModuleUuid(null);
+    verify(issue).setModuleUuidPath(null);
+
+    verify(issue).setChanged(true);
+  }
+
+  private DefaultIssue mockIssue(String fileUuid) {
+    DefaultIssue issue = mock(DefaultIssue.class);
+    when(issue.componentUuid()).thenReturn(fileUuid);
+    return issue;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/issue/TrackerBaseInputFactoryTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/issue/TrackerBaseInputFactoryTest.java
new file mode 100644 (file)
index 0000000..f20dd96
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.computation.issue;
+
+import com.google.common.base.Optional;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.source.FileSourceDao;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+import org.sonar.server.computation.filemove.MovedFilesRepository;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class TrackerBaseInputFactoryTest {
+  private static final String FILE_UUID = "uuid";
+  private static final ReportComponent FILE = ReportComponent.builder(Component.Type.FILE, 1).setUuid(FILE_UUID).build();
+
+  private BaseIssuesLoader baseIssuesLoader = mock(BaseIssuesLoader.class);
+  private DbClient dbClient = mock(DbClient.class);
+  private DbSession dbSession = mock(DbSession.class);
+  private FileSourceDao fileSourceDao = mock(FileSourceDao.class);
+
+  private MovedFilesRepository movedFilesRepository = mock(MovedFilesRepository.class);
+
+  private TrackerBaseInputFactory underTest = new TrackerBaseInputFactory(baseIssuesLoader, dbClient, movedFilesRepository);
+
+  @Before
+  public void setUp() throws Exception {
+    when(dbClient.openSession(false)).thenReturn(dbSession);
+    when(dbClient.fileSourceDao()).thenReturn(fileSourceDao);
+    when(movedFilesRepository.getOriginalFile(any(Component.class)))
+      .thenReturn(Optional.<MovedFilesRepository.OriginalFile>absent());
+  }
+
+  @Test
+  public void create_returns_Input_which_retrieves_lines_hashes_of_specified_file_component_when_it_has_no_original_file() {
+    underTest.create(FILE).getLineHashSequence();
+
+    verify(fileSourceDao).selectLineHashes(dbSession, FILE_UUID);
+  }
+
+  @Test
+  public void create_returns_Input_which_retrieves_lines_hashes_of_original_file_of_component_when_it_has_one() {
+    String originalUuid = "original uuid";
+
+    when(movedFilesRepository.getOriginalFile(FILE)).thenReturn(
+        Optional.of(new MovedFilesRepository.OriginalFile(6542, originalUuid, "original key"))
+    );
+
+    underTest.create(FILE).getLineHashSequence();
+
+    verify(fileSourceDao).selectLineHashes(dbSession, originalUuid);
+    verify(fileSourceDao, times(0)).selectLineHashes(dbSession, FILE_UUID);
+  }
+
+  @Test
+  public void create_returns_Input_which_retrieves_issues_of_specified_file_component_when_it_has_no_original_file() {
+    underTest.create(FILE).getIssues();
+
+    verify(baseIssuesLoader).loadForComponentUuid(FILE_UUID);
+  }
+
+  @Test
+  public void create_returns_Input_which_retrieves_issues_of_original_file_of_component_when_it_has_one() {
+    String originalUuid = "original uuid";
+
+    when(movedFilesRepository.getOriginalFile(FILE)).thenReturn(
+        Optional.of(new MovedFilesRepository.OriginalFile(6542, originalUuid, "original key"))
+    );
+
+    underTest.create(FILE).getIssues();
+
+    verify(baseIssuesLoader).loadForComponentUuid(originalUuid);
+    verify(baseIssuesLoader, times(0)).loadForComponentUuid(FILE_UUID);
+  }
+}