]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-6990 add DuplicationRepository and LoadDuplicationsFromReportStep
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Fri, 6 Nov 2015 15:49:48 +0000 (16:49 +0100)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Mon, 9 Nov 2015 15:34:20 +0000 (16:34 +0100)
21 files changed:
server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/AbstractDuplicate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/CrossProjectDuplicate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplicate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplication.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepository.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepositoryImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InProjectDuplicate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InnerDuplicate.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/TextBlock.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/duplication/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/step/LoadDuplicationsFromReportStep.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/step/ReportComputationSteps.java
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/CrossProjectDuplicateTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicateTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryRule.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InProjectDuplicateTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InnerDuplicateTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/computation/duplication/TextBlockTest.java [new file with mode: 0644]

index e2118173965356cf35e0ec150cb51b4ca6565b9b..f74ac8ba1eca06bd1a65c62789356c10103e5014 100644 (file)
@@ -30,6 +30,7 @@ import org.sonar.server.computation.component.DbIdsRepositoryImpl;
 import org.sonar.server.computation.component.ReportTreeRootHolderImpl;
 import org.sonar.server.computation.component.SettingsRepositoryImpl;
 import org.sonar.server.computation.debt.DebtModelHolderImpl;
+import org.sonar.server.computation.duplication.DuplicationRepositoryImpl;
 import org.sonar.server.computation.event.EventRepositoryImpl;
 import org.sonar.server.computation.filesystem.ComputationTempFolderProvider;
 import org.sonar.server.computation.issue.BaseIssuesLoader;
@@ -137,6 +138,7 @@ public final class ReportComputeEngineContainerPopulator implements ContainerPop
       SourceLinesRepositoryImpl.class,
       SourceHashRepositoryImpl.class,
       ScmInfoRepositoryImpl.class,
+      DuplicationRepositoryImpl.class,
 
       // issues
       RuleRepositoryImpl.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/AbstractDuplicate.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/AbstractDuplicate.java
new file mode 100644 (file)
index 0000000..e46c568
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import javax.annotation.Nullable;
+
+import static java.util.Objects.requireNonNull;
+
+abstract class AbstractDuplicate implements Duplicate {
+  private final TextBlock textBlock;
+
+  protected AbstractDuplicate(TextBlock textBlock) {
+    this.textBlock = requireNonNull(textBlock, "textBlock of duplicate can not be null");
+  }
+
+  @Override
+  public TextBlock getTextBlock() {
+    return textBlock;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    AbstractDuplicate that = (AbstractDuplicate) o;
+    return textBlock.equals(that.textBlock);
+  }
+
+  @Override
+  public int hashCode() {
+    return textBlock.hashCode();
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/CrossProjectDuplicate.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/CrossProjectDuplicate.java
new file mode 100644 (file)
index 0000000..e469c50
--- /dev/null
@@ -0,0 +1,65 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.Objects;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class CrossProjectDuplicate extends AbstractDuplicate {
+  private final String fileKey;
+
+  public CrossProjectDuplicate(String fileKey, TextBlock textBlock) {
+    super(textBlock);
+    this.fileKey = requireNonNull(fileKey, "fileKey can not be null");
+  }
+
+  public String getFileKey() {
+    return fileKey;
+  }
+
+  @Override
+  public String toString() {
+    return "CrossProjectDuplicate{" +
+        "fileKey='" + fileKey + '\'' +
+        ", textBlock=" + getTextBlock() +
+        '}';
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass() || !super.equals(o)) {
+      return false;
+    }
+    CrossProjectDuplicate that = (CrossProjectDuplicate) o;
+    return fileKey.equals(that.fileKey);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), fileKey);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplicate.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplicate.java
new file mode 100644 (file)
index 0000000..8c45837
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+public interface Duplicate {
+  TextBlock getTextBlock();
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplication.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplication.java
new file mode 100644 (file)
index 0000000..d57e9e2
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Ordering;
+import java.util.Comparator;
+import java.util.Objects;
+import java.util.SortedSet;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.FluentIterable.from;
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public final class Duplication {
+  private static Ordering<Duplicate> DUPLICATE_ORDERING = Ordering.from(DuplicateComparatorByType.INSTANCE)
+      .compound(Ordering.natural().onResultOf(DuplicateToFileKey.INSTANCE))
+    .compound(Ordering.natural().onResultOf(DuplicateToTextBlock.INSTANCE));
+
+  private final TextBlock original;
+  private final SortedSet<Duplicate> duplicates;
+
+  /**
+   * @throws NullPointerException if {@code original} is {@code null} or {@code duplicates} is {@code null} or {@code duplicates} contains {@code null}
+   * @throws IllegalArgumentException if {@code duplicates} is empty
+   * @throws IllegalArgumentException if {@code duplicates} contains a {@link InnerDuplicate} with {@code original}
+   */
+  public Duplication(final TextBlock original, final Iterable<Duplicate> duplicates) {
+    this.original = requireNonNull(original, "original TextBlock can not be null");
+    this.duplicates = from(requireNonNull(duplicates, "duplicates can not be null"))
+      .filter(FailOnNullDuplicate.INSTANCE)
+      .filter(new EnsureInnerDuplicateIsNotOriginalTextBlock(original))
+      .toSortedSet(DUPLICATE_ORDERING);
+    checkArgument(!this.duplicates.isEmpty(), "duplicates can not be empty");
+  }
+
+  /**
+   * The duplicated block.
+   */
+  public TextBlock getOriginal() {
+    return this.original;
+  }
+
+  /**
+   * The duplicates of the original, sorted by inner duplicates, then project duplicates, then cross-project duplicates.
+   * For each category of duplicate, they are sorted by:
+   * <ul>
+   *   <li>file key (unless it's an InnerDuplicate)</li>
+   *   <li>then by TextBlocks by start line and in case of same line, by shortest first</li>
+   * </ul
+   * <p>The returned set can not be empty and no inner duplicate can contain the original {@link TextBlock}.</p>
+   */
+  public SortedSet<Duplicate> getDuplicates() {
+    return this.duplicates;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    Duplication that = (Duplication) o;
+    return original.equals(that.original) && duplicates.equals(that.duplicates);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(original, duplicates);
+  }
+
+  @Override
+  public String toString() {
+    return "Duplication{" +
+      "original=" + original +
+      ", duplicates=" + duplicates +
+      '}';
+  }
+
+  private enum FailOnNullDuplicate implements Predicate<Duplicate> {
+    INSTANCE;
+
+    @Override
+    public boolean apply(@Nullable Duplicate input) {
+      requireNonNull(input, "duplicates can not contain null");
+      return true;
+    }
+  }
+
+  private enum DuplicateComparatorByType implements Comparator<Duplicate> {
+    INSTANCE;
+
+    @Override
+    public int compare(Duplicate o1, Duplicate o2) {
+      return toIndexType(o1) - toIndexType(o2);
+    }
+
+    private static int toIndexType(Duplicate duplicate) {
+      if (duplicate instanceof InnerDuplicate) {
+        return 0;
+      }
+      if (duplicate instanceof InProjectDuplicate) {
+        return 1;
+      }
+      if (duplicate instanceof CrossProjectDuplicate) {
+        return 2;
+      }
+      throw new IllegalArgumentException("Unsupported type of Duplicate " + duplicate.getClass().getName());
+    }
+  }
+
+  private enum DuplicateToTextBlock implements Function<Duplicate, TextBlock> {
+    INSTANCE;
+
+    @Override
+    @Nonnull
+    public TextBlock apply(@Nonnull Duplicate input) {
+      return input.getTextBlock();
+    }
+  }
+
+  private static class EnsureInnerDuplicateIsNotOriginalTextBlock implements Predicate<Duplicate> {
+    private final TextBlock original;
+
+    public EnsureInnerDuplicateIsNotOriginalTextBlock(TextBlock original) {
+      this.original = original;
+    }
+
+    @Override
+    public boolean apply(@Nullable Duplicate input) {
+      if (input instanceof InnerDuplicate) {
+        checkArgument(!original.equals(input.getTextBlock()), "TextBlock of an InnerDuplicate can not be the original TextBlock");
+      }
+      return true;
+    }
+  }
+
+  private enum DuplicateToFileKey implements Function<Duplicate, String> {
+    INSTANCE;
+
+    @Override
+    @Nonnull
+    public String apply(@Nonnull Duplicate duplicate) {
+      if (duplicate instanceof InnerDuplicate) {
+        return "";
+      }
+      if (duplicate instanceof InProjectDuplicate) {
+        return ((InProjectDuplicate) duplicate).getFile().getKey();
+      }
+      if (duplicate instanceof CrossProjectDuplicate) {
+        return ((CrossProjectDuplicate) duplicate).getFileKey();
+      }
+      throw new IllegalArgumentException("Unsupported type of Duplicate " + duplicate.getClass().getName());
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepository.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepository.java
new file mode 100644 (file)
index 0000000..0bfc05a
--- /dev/null
@@ -0,0 +1,91 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.Set;
+import org.sonar.server.computation.component.Component;
+
+/**
+ * Repository of code duplications in files of the project.
+ * <p>
+ * It stores:
+ * <ul>
+ *   <li>inner duplications (ie. duplications of blocks inside the same file)</li>
+ *   <li>project duplications (ie. duplications of blocks between two files of the current project)</li>
+ *   <li>cross-project duplications (ie. duplications of blocks of code between a file of the current project and a file of another project)</li>
+ * </ul>
+ * </p>
+ */
+public interface DuplicationRepository {
+
+  /**
+   * Returns the duplications in the specified file {@link Component}, if any.
+   *
+   * @throws NullPointerException if {@code file} is {@code null}
+   * @throws IllegalArgumentException if the type of the {@link Component} argument is not {@link Component.Type#FILE}
+   */
+  Set<Duplication> getDuplications(Component file);
+
+  /**
+   * Adds a project duplication of the specified original {@link TextBlock} in the specified file {@link Component}
+   * which is duplicated by the specified duplicate {@link TextBlock} in the same file.
+   * <p>
+   * This method can be called multiple times with the same {@code file} and {@code original} but a different
+   * {@code duplicate} to add multiple duplications of the same block.
+   * </p>
+   * <p>
+   * It must not, however, be called twice with {@code original} and {@code duplicate} swapped, this would raise
+   * an {@link IllegalArgumentException} as the duplication already exists.
+   * </p>
+   *
+   * @throws NullPointerException if any argument is {@code null}
+   * @throws IllegalArgumentException if the type of the {@link Component} argument is not {@link Component.Type#FILE}
+   * @throws IllegalStateException if {@code original} and {@code duplicate} are the same
+   * @throws IllegalStateException if the specified duplication already exists in the repository
+   */
+  void addDuplication(Component file, TextBlock original, TextBlock duplicate);
+
+  /**
+   * Adds a project duplication of the specified original {@link TextBlock} in the specified file {@link Component} as
+   * duplicated by the duplicate {@link TextBlock} in the other file {@link Component}.
+   * <p>
+   * Note: the reverse duplication relationship between files is not added automatically (which leaves open the
+   * possibility of inconsistent duplication information). This means that it is the responsibility of the repository's
+   * user to call this method again with the {@link Component} arguments and the {@link TextBlock} arguments swapped.
+   * </p>
+   *
+   * @throws NullPointerException if any argument is {@code null}
+   * @throws IllegalArgumentException if the type of any of the {@link Component} arguments is not {@link Component.Type#FILE}
+   * @throws IllegalArgumentException if {@code file} and {@code otherFile} are the same
+   * @throws IllegalStateException if the specified duplication already exists in the repository
+   */
+  void addDuplication(Component file, TextBlock original, Component otherFile, TextBlock duplicate);
+
+  /**
+   * Adds a cross-project duplication of the specified original {@link TextBlock} in the specified file {@link Component},
+   * as duplicated by the specified duplicate {@link TextBlock} in a file of another project identified by its key.
+   *
+   * @throws NullPointerException if any argument is {@code null}
+   * @throws IllegalArgumentException if the type of the {@link Component} argument is not {@link Component.Type#FILE}
+   * @throws IllegalArgumentException if {@code otherFileKey} is the key of a file in the project
+   * @throws IllegalStateException if the specified duplication already exists in the repository
+   */
+  void addDuplication(Component file, TextBlock original, String otherFileKey, TextBlock duplicate);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepositoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepositoryImpl.java
new file mode 100644 (file)
index 0000000..db48356
--- /dev/null
@@ -0,0 +1,379 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.server.computation.component.Component;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.collect.FluentIterable.from;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * In-memory implementation of {@link DuplicationRepository}.
+ */
+public class DuplicationRepositoryImpl implements DuplicationRepository {
+  private final Map<String, Duplications> duplicationsByComponentUuid = new HashMap<>();
+
+  @Override
+  public Set<Duplication> getDuplications(Component file) {
+    checkFileComponentArgument(file);
+
+    if (duplicationsByComponentUuid.containsKey(file.getUuid())) {
+      return from(duplicationsByComponentUuid.get(file.getUuid()).getDuplicates())
+          .transform(DuplicatesEntryToDuplication.INSTANCE)
+          .toSet();
+    }
+    return Collections.emptySet();
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, TextBlock duplicate) {
+    checkFileComponentArgument(file);
+    checkOriginalTextBlockArgument(original);
+    checkDuplicateTextBlockArgument(duplicate);
+    checkArgument(!original.equals(duplicate), "original and duplicate TextBlocks can not be the same");
+
+    Optional<Duplicates> duplicates = getDuplicates(file, original);
+    if (duplicates.isPresent()) {
+      checkDuplicationAlreadyExists(duplicates, file, original, duplicate);
+      checkReverseDuplicationAlreadyExists(file, original, duplicate);
+
+      duplicates.get().add(duplicate);
+    } else {
+      checkReverseDuplicationAlreadyExists(file, original, duplicate);
+      getOrCreate(file, original).add(duplicate);
+    }
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, Component otherFile, TextBlock duplicate) {
+    checkFileComponentArgument(file);
+    checkOriginalTextBlockArgument(original);
+    checkComponentArgument(otherFile, "otherFile");
+    checkDuplicateTextBlockArgument(duplicate);
+    checkArgument(!file.equals(otherFile), "file and otherFile Components can not be the same");
+
+    Optional<Duplicates> duplicates = getDuplicates(file, original);
+    if (duplicates.isPresent()) {
+      checkDuplicationAlreadyExists(duplicates, file, original, otherFile, duplicate);
+
+      duplicates.get().add(otherFile, duplicate);
+    } else {
+      getOrCreate(file, original).add(otherFile, duplicate);
+    }
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, String otherFileKey, TextBlock duplicate) {
+    checkFileComponentArgument(file);
+    checkOriginalTextBlockArgument(original);
+    requireNonNull(otherFileKey, "otherFileKey can not be null");
+    checkDuplicateTextBlockArgument(duplicate);
+
+    Optional<Duplicates> duplicates = getDuplicates(file, original);
+    if (duplicates.isPresent()) {
+      checkDuplicationAlreadyExists(duplicates, file, original, otherFileKey, duplicate);
+
+      duplicates.get().add(otherFileKey, duplicate);
+    } else {
+      getOrCreate(file, original).add(otherFileKey, duplicate);
+    }
+  }
+
+  private Optional<Duplicates> getDuplicates(Component file, TextBlock original) {
+    Duplications duplications = duplicationsByComponentUuid.get(file.getUuid());
+    if (duplications == null) {
+      return Optional.absent();
+    }
+    return duplications.get(original);
+  }
+
+  private void checkDuplicationAlreadyExists(Optional<Duplicates> duplicates, Component file, TextBlock original, TextBlock duplicate) {
+    checkArgument(!duplicates.get().hasDuplicate(duplicate),
+        "Inner duplicate %s is already associated to original %s in file %s", duplicate, original, file.getKey());
+  }
+
+  private void checkReverseDuplicationAlreadyExists(Component file, TextBlock original, TextBlock duplicate) {
+    Optional<Duplicates> reverseDuplication = getDuplicates(file, duplicate);
+    if (reverseDuplication.isPresent()) {
+      checkArgument(!reverseDuplication.get().hasDuplicate(original),
+          "Inner duplicate %s is already associated to original %s in file %s", duplicate, original, file.getKey());
+    }
+  }
+
+  private static void checkDuplicationAlreadyExists(Optional<Duplicates> duplicates, Component file, TextBlock original, Component otherFile, TextBlock duplicate) {
+    checkArgument(!duplicates.get().hasDuplicate(otherFile, duplicate),
+        "In-project duplicate %s in file %s is already associated to original %s in file %s", duplicate, otherFile.getKey(), original, file.getKey());
+  }
+
+  private static void checkDuplicationAlreadyExists(Optional<Duplicates> duplicates, Component file, TextBlock original, String otherFileKey, TextBlock duplicate) {
+    checkArgument(!duplicates.get().hasDuplicate(otherFileKey, duplicate),
+        "Cross-project duplicate %s in file %s is already associated to original %s in file %s", duplicate, otherFileKey, original, file.getKey());
+  }
+
+  private Duplicates getOrCreate(Component file, TextBlock original) {
+    Duplications duplications = duplicationsByComponentUuid.get(file.getUuid());
+    if (duplications == null) {
+      duplications = new Duplications();
+      duplicationsByComponentUuid.put(file.getUuid(), duplications);
+    }
+
+    return duplications.getOrCreate(original);
+  }
+
+  /**
+   * Represents the location of the file of one or more duplicate {@link TextBlock}.
+   */
+  private interface DuplicateLocation {
+
+  }
+
+  private enum InnerDuplicationLocation implements DuplicateLocation {
+    INSTANCE
+  }
+
+  @Immutable
+  private static final class InProjectDuplicationLocation implements DuplicateLocation {
+    private final Component component;
+
+    public InProjectDuplicationLocation(Component component) {
+      this.component = component;
+    }
+
+    public Component getComponent() {
+      return component;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      InProjectDuplicationLocation that = (InProjectDuplicationLocation) o;
+      return component.equals(that.component);
+    }
+
+    @Override
+    public int hashCode() {
+      return component.hashCode();
+    }
+  }
+
+  @Immutable
+  private static final class CrossProjectDuplicationLocation implements DuplicateLocation {
+    private final String fileKey;
+
+    private CrossProjectDuplicationLocation(String fileKey) {
+      this.fileKey = fileKey;
+    }
+
+    public String getFileKey() {
+      return fileKey;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (o == null || getClass() != o.getClass()) {
+        return false;
+      }
+      CrossProjectDuplicationLocation that = (CrossProjectDuplicationLocation) o;
+      return fileKey.equals(that.fileKey);
+    }
+
+    @Override
+    public int hashCode() {
+      return fileKey.hashCode();
+    }
+  }
+
+  private static final class Duplications {
+    private final Map<TextBlock, Duplicates> duplicatesByTextBlock = new HashMap<>();
+
+    public Duplicates getOrCreate(TextBlock textBlock) {
+      if (duplicatesByTextBlock.containsKey(textBlock)) {
+        return duplicatesByTextBlock.get(textBlock);
+      }
+      Duplicates res = new Duplicates();
+      duplicatesByTextBlock.put(textBlock, res);
+      return res;
+    }
+
+    public Set<Map.Entry<TextBlock, Duplicates>> getDuplicates() {
+      return duplicatesByTextBlock.entrySet();
+    }
+
+    public Optional<Duplicates> get(TextBlock original) {
+      return Optional.fromNullable(duplicatesByTextBlock.get(original));
+    }
+  }
+
+  private static final class Duplicates {
+    private final Map<DuplicateLocation, Set<TextBlock>> duplicatesByLocation = new HashMap<>();
+
+    public Iterable<DuplicateWithLocation> getDuplicates() {
+      return from(duplicatesByLocation.entrySet())
+          .transformAndConcat(MapEntryToDuplicateWithLocation.INSTANCE);
+    }
+
+    public void add(TextBlock duplicate) {
+      add(InnerDuplicationLocation.INSTANCE, duplicate);
+    }
+
+    public void add(Component otherFile, TextBlock duplicate) {
+      InProjectDuplicationLocation key = new InProjectDuplicationLocation(otherFile);
+      add(key, duplicate);
+    }
+
+    public void add(String otherFileKey, TextBlock duplicate) {
+      CrossProjectDuplicationLocation key = new CrossProjectDuplicationLocation(otherFileKey);
+      add(key, duplicate);
+    }
+
+    private void add(DuplicateLocation duplicateLocation, TextBlock duplicate) {
+      Set<TextBlock> textBlocks = duplicatesByLocation.get(duplicateLocation);
+      if (textBlocks == null) {
+        textBlocks = new HashSet<>(1);
+        duplicatesByLocation.put(duplicateLocation, textBlocks);
+      }
+      textBlocks.add(duplicate);
+    }
+
+    public boolean hasDuplicate(TextBlock duplicate) {
+      return containsEntry(InnerDuplicationLocation.INSTANCE, duplicate);
+    }
+
+    public boolean hasDuplicate(Component otherFile, TextBlock duplicate) {
+      return containsEntry(new InProjectDuplicationLocation(otherFile), duplicate);
+    }
+
+    public boolean hasDuplicate(String otherFileKey, TextBlock duplicate) {
+      return containsEntry(new CrossProjectDuplicationLocation(otherFileKey), duplicate);
+    }
+
+    private boolean containsEntry(DuplicateLocation duplicateLocation, TextBlock duplicate) {
+      Set<TextBlock> textBlocks = duplicatesByLocation.get(duplicateLocation);
+      return textBlocks != null && textBlocks.contains(duplicate);
+    }
+
+    private enum MapEntryToDuplicateWithLocation implements Function<Map.Entry<DuplicateLocation, Set<TextBlock>>, Iterable<DuplicateWithLocation>> {
+      INSTANCE;
+
+      @Override
+      @Nonnull
+      public Iterable<DuplicateWithLocation> apply(@Nonnull final Map.Entry<DuplicateLocation, Set<TextBlock>> entry) {
+        return from(entry.getValue())
+            .transform(new Function<TextBlock, DuplicateWithLocation>() {
+              @Override
+              @Nonnull
+              public DuplicateWithLocation apply(@Nonnull TextBlock input) {
+                return new DuplicateWithLocation(entry.getKey(), input);
+              }
+            });
+      }
+    }
+  }
+
+  @Immutable
+  private static final class DuplicateWithLocation {
+    private final DuplicateLocation location;
+    private final TextBlock duplicate;
+
+    private DuplicateWithLocation(DuplicateLocation location, TextBlock duplicate) {
+      this.location = location;
+      this.duplicate = duplicate;
+    }
+
+    public DuplicateLocation getLocation() {
+      return location;
+    }
+
+    public TextBlock getDuplicate() {
+      return duplicate;
+    }
+
+  }
+
+  private static void checkFileComponentArgument(Component file) {
+    checkComponentArgument(file, "file");
+  }
+
+  private static void checkComponentArgument(Component file, String argName) {
+    requireNonNull(file, format("%s can not be null", argName));
+    checkArgument(file.getType() == Component.Type.FILE, "type of %s argument must be FILE", argName);
+  }
+
+  private static void checkDuplicateTextBlockArgument(TextBlock duplicate) {
+    requireNonNull(duplicate, "duplicate can not be null");
+  }
+
+  private static void checkOriginalTextBlockArgument(TextBlock original) {
+    requireNonNull(original, "original can not be null");
+  }
+
+  private enum DuplicatesEntryToDuplication implements Function<Map.Entry<TextBlock, Duplicates>, Duplication> {
+    INSTANCE;
+
+    @Override
+    @Nonnull
+    public Duplication apply(@Nonnull Map.Entry<TextBlock, Duplicates> entry) {
+      return new Duplication(
+          entry.getKey(),
+          from(entry.getValue().getDuplicates()).transform(DuplicateLocationEntryToDuplicate.INSTANCE)
+      );
+    }
+
+    private enum DuplicateLocationEntryToDuplicate implements Function<DuplicateWithLocation, Duplicate> {
+      INSTANCE;
+
+      @Override
+      @Nonnull
+      public Duplicate apply(@Nonnull DuplicateWithLocation input) {
+        DuplicateLocation duplicateLocation = input.getLocation();
+        if (duplicateLocation instanceof InnerDuplicationLocation) {
+          return new InnerDuplicate(input.getDuplicate());
+        }
+        if (duplicateLocation instanceof InProjectDuplicationLocation) {
+          return new InProjectDuplicate(((InProjectDuplicationLocation) duplicateLocation).getComponent(), input.getDuplicate());
+        }
+        if (duplicateLocation instanceof CrossProjectDuplicationLocation) {
+          return new CrossProjectDuplicate(((CrossProjectDuplicationLocation) duplicateLocation).getFileKey(), input.getDuplicate());
+        }
+        throw new IllegalArgumentException("Unsupported DuplicationLocation type " + duplicateLocation.getClass());
+      }
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InProjectDuplicate.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InProjectDuplicate.java
new file mode 100644 (file)
index 0000000..7ca80f2
--- /dev/null
@@ -0,0 +1,72 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.Objects;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.server.computation.component.Component;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+@Immutable
+public class InProjectDuplicate extends AbstractDuplicate {
+  private final Component file;
+
+  public InProjectDuplicate(Component file, TextBlock textBlock) {
+    super(textBlock);
+    requireNonNull(file, "file can not be null");
+    checkArgument(file.getType() == Component.Type.FILE, "file must be of type FILE");
+    this.file = file;
+  }
+
+  public Component getFile() {
+    return file;
+  }
+
+  @Override
+  public String toString() {
+    return "InProjectDuplicate{" +
+      "file=" + file +
+      ", textBlock=" + getTextBlock() +
+      '}';
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    if (!super.equals(o)) {
+      return false;
+    }
+    InProjectDuplicate that = (InProjectDuplicate) o;
+    return file.equals(that.file);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(super.hashCode(), file);
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InnerDuplicate.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InnerDuplicate.java
new file mode 100644 (file)
index 0000000..1448691
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+public final class InnerDuplicate extends AbstractDuplicate {
+
+  public InnerDuplicate(TextBlock textBlock) {
+    super(textBlock);
+  }
+
+  @Override
+  public String toString() {
+    return "InnerDuplicate{" +
+      "textBlock=" + getTextBlock() +
+      '}';
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/TextBlock.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/TextBlock.java
new file mode 100644 (file)
index 0000000..e317e7c
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.Objects;
+import javax.annotation.Nullable;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * A block of text in some file represented by its first line index (1-based) and its last line index (included).
+ * <p>
+ * This class defines a natural ordering which sorts {@link TextBlock} by lowest start line first and then, in case of
+ * same start line, by smallest size (ie. lowest end line).
+ * </p>
+ */
+public final class TextBlock implements Comparable<TextBlock> {
+  private final int start;
+  private final int end;
+
+  /**
+   * @throws IllegalArgumentException if {@code start} is 0 or less
+   * @throws IllegalStateException if {@code end} is less than {@code start}
+   */
+  public TextBlock(int start, int end) {
+    checkArgument(start > 0, "First line index must be >= 1");
+    checkArgument(end >= start, "Last line index must be >= first line index");
+    this.start = start;
+    this.end = end;
+  }
+
+  public int getStart() {
+    return start;
+  }
+
+  public int getEnd() {
+    return end;
+  }
+
+  @Override
+  public int compareTo(TextBlock other) {
+    int res = start - other.start;
+    if (res == 0) {
+      return end - other.end;
+    }
+    return res;
+  }
+
+  @Override
+  public boolean equals(@Nullable Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    TextBlock textBlock = (TextBlock) o;
+    return start == textBlock.start && end == textBlock.end;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(start, end);
+  }
+
+  @Override
+  public String toString() {
+    return "TextBlock{" +
+      "start=" + start +
+      ", end=" + end +
+      '}';
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/package-info.java
new file mode 100644 (file)
index 0000000..fbb40f5
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.
+ */
+
+@ParametersAreNonnullByDefault
+package org.sonar.server.computation.duplication;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/LoadDuplicationsFromReportStep.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/LoadDuplicationsFromReportStep.java
new file mode 100644 (file)
index 0000000..fa6534a
--- /dev/null
@@ -0,0 +1,86 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.step;
+
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.core.util.CloseableIterator;
+import org.sonar.server.computation.batch.BatchReportReader;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.CrawlerDepthLimit;
+import org.sonar.server.computation.component.DepthTraversalTypeAwareCrawler;
+import org.sonar.server.computation.component.ReportTreeRootHolder;
+import org.sonar.server.computation.component.TypeAwareVisitorAdapter;
+import org.sonar.server.computation.duplication.DuplicationRepository;
+import org.sonar.server.computation.duplication.TextBlock;
+
+import static org.sonar.server.computation.component.ComponentVisitor.Order.POST_ORDER;
+
+/**
+ * Loads duplication information from the report and loads them into the {@link DuplicationRepository}.
+ */
+public class LoadDuplicationsFromReportStep implements ComputationStep {
+  private final ReportTreeRootHolder treeRootHolder;
+  private final BatchReportReader batchReportReader;
+  private final DuplicationRepository duplicationRepository;
+
+  public LoadDuplicationsFromReportStep(ReportTreeRootHolder treeRootHolder, BatchReportReader batchReportReader, DuplicationRepository duplicationRepository) {
+    this.treeRootHolder = treeRootHolder;
+    this.batchReportReader = batchReportReader;
+    this.duplicationRepository = duplicationRepository;
+  }
+
+  @Override
+  public String getDescription() {
+    return "Load inner and project duplications";
+  }
+
+  @Override
+  public void execute() {
+    new DepthTraversalTypeAwareCrawler(
+      new TypeAwareVisitorAdapter(CrawlerDepthLimit.FILE, POST_ORDER) {
+        @Override
+        public void visitFile(Component file) {
+          CloseableIterator<BatchReport.Duplication> duplications = batchReportReader.readComponentDuplications(file.getReportAttributes().getRef());
+          try {
+            while (duplications.hasNext()) {
+              loadDuplications(file, duplications.next());
+            }
+          } finally {
+            duplications.close();
+          }
+        }
+      }).visit(treeRootHolder.getRoot());
+  }
+
+  private void loadDuplications(Component file, BatchReport.Duplication duplication) {
+    TextBlock original = convert(duplication.getOriginPosition());
+    for (BatchReport.Duplicate duplicate : duplication.getDuplicateList()) {
+      if (duplicate.hasOtherFileRef()) {
+        duplicationRepository.addDuplication(file, original, treeRootHolder.getComponentByRef(duplicate.getOtherFileRef()), convert(duplicate.getRange()));
+      } else {
+        duplicationRepository.addDuplication(file, original, convert(duplicate.getRange()));
+      }
+    }
+  }
+
+  private static TextBlock convert(BatchReport.TextRange textRange) {
+    return new TextBlock(textRange.getStartLine(), textRange.getEndLine());
+  }
+}
index 3813fa5e4b0b7d90aae0c150cf94e39e099f477f..067fd2f44ccb0005599e0a608cc26e11c0a4868e 100644 (file)
@@ -85,6 +85,7 @@ public class ReportComputationSteps implements ComputationSteps {
       PersistIssuesStep.class,
       PersistProjectLinksStep.class,
       PersistEventsStep.class,
+      LoadDuplicationsFromReportStep.class,
       PersistDuplicationsStep.class,
       PersistFileSourcesStep.class,
       PersistTestsStep.class,
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/CrossProjectDuplicateTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/CrossProjectDuplicateTest.java
new file mode 100644 (file)
index 0000000..8d30c16
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CrossProjectDuplicateTest {
+  private static final String FILE_KEY_1 = "file key 1";
+  private static final String FILE_KEY_2 = "file key 2";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void constructors_throws_NPE_if_fileKey_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("fileKey can not be null");
+
+    new CrossProjectDuplicate(null, new TextBlock(1, 1));
+  }
+
+  @Test
+  public void constructors_throws_NPE_if_textBlock_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("textBlock of duplicate can not be null");
+
+    new CrossProjectDuplicate(FILE_KEY_1, null);
+  }
+
+  @Test
+  public void getTextBlock_returns_TextBlock_constructor_argument() {
+    TextBlock textBlock = new TextBlock(2, 3);
+
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock).getTextBlock()).isSameAs(textBlock);
+  }
+
+  @Test
+  public void getFileKey_returns_constructor_argument() {
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, new TextBlock(2, 3)).getFileKey()).isEqualTo(FILE_KEY_1);
+  }
+
+  @Test
+  public void equals_compares_on_file_and_TextBlock() {
+    TextBlock textBlock1 = new TextBlock(1, 2);
+
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock1)).isEqualTo(new CrossProjectDuplicate(FILE_KEY_1, new TextBlock(1, 2)));
+
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock1)).isNotEqualTo(new CrossProjectDuplicate(FILE_KEY_1, new TextBlock(1, 1)));
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock1)).isNotEqualTo(new CrossProjectDuplicate(FILE_KEY_2, textBlock1));
+  }
+
+  @Test
+  public void hashcode_depends_on_file_and_TextBlock() {
+    TextBlock textBlock = new TextBlock(1, 2);
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock).hashCode()).isEqualTo(new CrossProjectDuplicate(FILE_KEY_1, textBlock).hashCode());
+
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock).hashCode()).isNotEqualTo(new CrossProjectDuplicate(FILE_KEY_2, textBlock).hashCode());
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, textBlock).hashCode()).isNotEqualTo(new CrossProjectDuplicate(FILE_KEY_2, new TextBlock(1, 1)).hashCode());
+  }
+
+  @Test
+  public void verify_toString() {
+    assertThat(new CrossProjectDuplicate(FILE_KEY_1, new TextBlock(1, 2)).toString()).isEqualTo("CrossProjectDuplicate{fileKey='file key 1', textBlock=TextBlock{start=1, end=2}}");
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicateTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicateTest.java
new file mode 100644 (file)
index 0000000..beef4a7
--- /dev/null
@@ -0,0 +1,41 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import org.junit.Test;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DuplicateTest {
+  @Test
+  public void duplicate_implementations_are_not_equals_to_each_other_even_if_TextBlock_is_the_same() {
+    TextBlock textBlock = new TextBlock(1, 2);
+
+    InnerDuplicate innerDuplicate = new InnerDuplicate(textBlock);
+    InProjectDuplicate inProjectDuplicate = new InProjectDuplicate(ReportComponent.builder(Component.Type.FILE, 1).build(), textBlock);
+    CrossProjectDuplicate crossProjectDuplicate = new CrossProjectDuplicate("file key", textBlock);
+
+    assertThat(innerDuplicate.equals(inProjectDuplicate)).isFalse();
+    assertThat(innerDuplicate.equals(crossProjectDuplicate)).isFalse();
+    assertThat(inProjectDuplicate.equals(crossProjectDuplicate)).isFalse();
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryImplTest.java
new file mode 100644 (file)
index 0000000..c86e202
--- /dev/null
@@ -0,0 +1,452 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+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.Set;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+import org.sonar.server.util.WrapInSingleElementArray;
+
+import static com.google.common.base.Predicates.equalTo;
+import static com.google.common.base.Predicates.not;
+import static com.google.common.collect.FluentIterable.from;
+import static java.lang.String.format;
+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 DuplicationRepositoryImplTest {
+
+  private static final Component COMPONENT_1 = ReportComponent.builder(Component.Type.FILE, 1).build();
+  private static final Component COMPONENT_2 = ReportComponent.builder(Component.Type.FILE, 2).build();
+  private static final Component COMPONENT_3 = ReportComponent.builder(Component.Type.FILE, 3).build();
+  private static final TextBlock ORIGINAL_TEXTBLOCK = new TextBlock(1, 2);
+  private static final TextBlock COPY_OF_ORIGINAL_TEXTBLOCK = new TextBlock(1, 2);
+  private static final TextBlock DUPLICATE_TEXTBLOCK_1 = new TextBlock(15, 15);
+  private static final TextBlock DUPLICATE_TEXTBLOCK_2 = new TextBlock(15, 16);
+  private static final String FILE_KEY_1 = "1";
+  private static final String FILE_KEY_2 = "2";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private DuplicationRepository underTest = new DuplicationRepositoryImpl();
+
+  @Test
+  public void getDuplications_throws_NPE_if_Component_argument_is_null() {
+    expectFileArgumentNPE();
+
+    underTest.getDuplications(null);
+  }
+
+  @Test
+  @UseDataProvider("allComponentTypesButFile")
+  public void getDuplications_throws_IAE_if_Component_type_is_not_FILE(Component.Type type) {
+    expectFileTypeIAE();
+
+    Component component = mockComponentGetType(type);
+
+    underTest.getDuplications(component);
+  }
+
+  @Test
+  public void getDuplications_returns_empty_set_when_repository_is_empty() {
+    assertNoDuplication(COMPONENT_1);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_NPE_if_file_argument_is_null() {
+    expectFileArgumentNPE();
+
+    underTest.addDuplication(null, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_NPE_if_original_argument_is_null() {
+    expectOriginalArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, null, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_NPE_if_duplicate_argument_is_null() {
+    expectDuplicateArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, null);
+  }
+
+  @Test
+  @UseDataProvider("allComponentTypesButFile")
+  public void addDuplication_inner_throws_IAE_if_file_type_is_not_FILE(Component.Type type) {
+    expectFileTypeIAE();
+
+    Component component = mockComponentGetType(type);
+
+    underTest.addDuplication(component, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_IAE_if_original_and_duplicate_are_equal() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("original and duplicate TextBlocks can not be the same");
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COPY_OF_ORIGINAL_TEXTBLOCK);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_IAE_if_duplication_already_exists() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage(format(
+        "Inner duplicate %s is already associated to original %s in file %s",
+        DUPLICATE_TEXTBLOCK_1, ORIGINAL_TEXTBLOCK, COMPONENT_1.getKey()));
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_IAE_if_reverse_duplication_already_exists() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage(format(
+        "Inner duplicate %s is already associated to original %s in file %s",
+        ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1, COMPONENT_1.getKey()));
+
+    underTest.addDuplication(COMPONENT_1, DUPLICATE_TEXTBLOCK_1, ORIGINAL_TEXTBLOCK);
+  }
+
+  @Test
+  public void addDuplication_inner_throws_IAE_if_reverse_duplication_already_exists_and_duplicate_has_duplicates_of_its_own() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+    underTest.addDuplication(COMPONENT_1, DUPLICATE_TEXTBLOCK_1, DUPLICATE_TEXTBLOCK_2);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage(format(
+        "Inner duplicate %s is already associated to original %s in file %s",
+        ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1, COMPONENT_1.getKey()));
+
+    underTest.addDuplication(COMPONENT_1, DUPLICATE_TEXTBLOCK_1, ORIGINAL_TEXTBLOCK);
+  }
+
+  @Test
+  public void addDuplication_inner_is_returned_by_getDuplications() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new InnerDuplicate(DUPLICATE_TEXTBLOCK_1));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_inner_called_multiple_times_populate_a_single_Duplication() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new InnerDuplicate(DUPLICATE_TEXTBLOCK_1), new InnerDuplicate(DUPLICATE_TEXTBLOCK_2));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_NPE_if_file_argument_is_null() {
+    expectFileArgumentNPE();
+
+    underTest.addDuplication(null, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_NPE_if_original_argument_is_null() {
+    expectOriginalArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, null, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_NPE_if_otherFile_argument_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("otherFile can not be null");
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, (Component) null, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  @UseDataProvider("allComponentTypesButFile")
+  public void addDuplication_inProject_throws_NPE_if_otherFile_type_is_not_FILE(Component.Type type) {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("type of otherFile argument must be FILE");
+
+    Component component = mockComponentGetType(type);
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, component, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_NPE_if_duplicate_argument_is_null() {
+    expectDuplicateArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, null);
+  }
+
+  @Test
+  @UseDataProvider("allComponentTypesButFile")
+  public void addDuplication_inProject_throws_NPE_if_file_type_is_not_FILE(Component.Type type) {
+    expectFileTypeIAE();
+
+    Component component = mockComponentGetType(type);
+
+    underTest.addDuplication(component, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_NPE_if_file_and_otherFile_are_the_same() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("file and otherFile Components can not be the same");
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_1, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_throws_IAE_if_duplication_already_exists() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage(format(
+        "In-project duplicate %s in file %s is already associated to original %s in file %s",
+        DUPLICATE_TEXTBLOCK_1, COMPONENT_2.getKey(), ORIGINAL_TEXTBLOCK, COMPONENT_1.getKey()));
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_inProject_is_returned_by_getDuplications() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new InProjectDuplicate(COMPONENT_2, DUPLICATE_TEXTBLOCK_1));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_inProject_called_multiple_times_populate_a_single_Duplication() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new InProjectDuplicate(COMPONENT_2, DUPLICATE_TEXTBLOCK_1), new InProjectDuplicate(COMPONENT_2, DUPLICATE_TEXTBLOCK_2));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_inProject_called_multiple_times_with_different_components_populate_a_single_Duplication() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_3, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_3, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new InProjectDuplicate(COMPONENT_2, DUPLICATE_TEXTBLOCK_2), new InProjectDuplicate(COMPONENT_3, DUPLICATE_TEXTBLOCK_1), new InProjectDuplicate(COMPONENT_3, DUPLICATE_TEXTBLOCK_2));
+
+    assertNoDuplication(COMPONENT_2);
+    assertNoDuplication(COMPONENT_3);
+  }
+
+  @Test
+  public void addDuplication_crossProject_throws_NPE_if_file_argument_is_null() {
+    expectFileArgumentNPE();
+
+    underTest.addDuplication(null, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_crossProject_throws_NPE_if_original_argument_is_null() {
+    expectOriginalArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, null, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_crossProject_throws_NPE_if_otherFileKey_argument_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("otherFileKey can not be null");
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, (String) null, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_crossProject_throws_NPE_if_duplicate_argument_is_null() {
+    expectDuplicateArgumentNPE();
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, null);
+  }
+
+  @Test
+  @Ignore
+  public void addDuplication_crossProject_throws_IAE_if_otherFileKey_is_key_of_Component_in_the_project() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("type of file argument must be FILE");
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, COMPONENT_2.getKey(), DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  @UseDataProvider("allComponentTypesButFile")
+  public void addDuplication_crossProject_throws_NPE_if_file_type_is_not_FILE(Component.Type type) {
+    expectFileTypeIAE();
+
+    Component component = mockComponentGetType(type);
+
+    underTest.addDuplication(component, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_crossProject_throws_IAE_if_duplication_already_exists() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage(format(
+        "Cross-project duplicate %s in file %s is already associated to original %s in file %s",
+        DUPLICATE_TEXTBLOCK_1, FILE_KEY_1, ORIGINAL_TEXTBLOCK, COMPONENT_1.getKey()));
+
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+  }
+
+  @Test
+  public void addDuplication_crossProject_is_returned_by_getDuplications() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new CrossProjectDuplicate(FILE_KEY_1, DUPLICATE_TEXTBLOCK_1));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_crossProject_called_multiple_times_populate_a_single_Duplication() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new CrossProjectDuplicate(FILE_KEY_1, DUPLICATE_TEXTBLOCK_1), new CrossProjectDuplicate(FILE_KEY_1, DUPLICATE_TEXTBLOCK_2));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @Test
+  public void addDuplication_crossProject_called_multiple_times_with_different_fileKeys_populate_a_single_Duplication() {
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_1, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_2, DUPLICATE_TEXTBLOCK_2);
+    underTest.addDuplication(COMPONENT_1, ORIGINAL_TEXTBLOCK, FILE_KEY_2, DUPLICATE_TEXTBLOCK_1);
+
+    Set<Duplication> duplications = underTest.getDuplications(COMPONENT_1);
+    assertThat(duplications).hasSize(1);
+    assertDuplication(
+        duplications.iterator().next(),
+        ORIGINAL_TEXTBLOCK,
+        new CrossProjectDuplicate(FILE_KEY_1, DUPLICATE_TEXTBLOCK_2), new CrossProjectDuplicate(FILE_KEY_2, DUPLICATE_TEXTBLOCK_1), new CrossProjectDuplicate(FILE_KEY_2, DUPLICATE_TEXTBLOCK_2));
+
+    assertNoDuplication(COMPONENT_2);
+  }
+
+  @DataProvider
+  public static Object[][] allComponentTypesButFile() {
+    return from(Arrays.asList(Component.Type.values()))
+        .filter(not(equalTo(Component.Type.FILE)))
+        .transform(WrapInSingleElementArray.INSTANCE)
+        .toArray(Object[].class);
+  }
+
+  private static void assertDuplication(Duplication duplication, TextBlock original, Duplicate... duplicates) {
+    assertThat(duplication.getOriginal()).isEqualTo(original);
+    assertThat(duplication.getDuplicates()).containsExactly(duplicates);
+  }
+
+  private void assertNoDuplication(Component component) {
+    assertThat(underTest.getDuplications(component)).isEmpty();
+  }
+
+  private void expectFileArgumentNPE() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("file can not be null");
+  }
+
+  private void expectOriginalArgumentNPE() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("original can not be null");
+  }
+
+  private void expectDuplicateArgumentNPE() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("duplicate can not be null");
+  }
+
+  private void expectFileTypeIAE() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("type of file argument must be FILE");
+  }
+
+  private Component mockComponentGetType(Component.Type type) {
+    Component component = mock(Component.class);
+    when(component.getType()).thenReturn(type);
+    return component;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryRule.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryRule.java
new file mode 100644 (file)
index 0000000..583fbd6
--- /dev/null
@@ -0,0 +1,111 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.Set;
+import org.junit.rules.ExternalResource;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ComponentProvider;
+import org.sonar.server.computation.component.NoComponentProvider;
+import org.sonar.server.computation.component.TreeComponentProvider;
+import org.sonar.server.computation.component.TreeRootHolderComponentProvider;
+
+public class DuplicationRepositoryRule extends ExternalResource implements DuplicationRepository {
+  private final ComponentProvider componentProvider;
+  private DuplicationRepositoryImpl delegate;
+
+  private DuplicationRepositoryRule(ComponentProvider componentProvider) {
+    this.componentProvider = componentProvider;
+  }
+
+  public static DuplicationRepositoryRule standalone() {
+    return new DuplicationRepositoryRule(NoComponentProvider.INSTANCE);
+  }
+
+  public static DuplicationRepositoryRule create(TreeRootHolderRule treeRootHolder) {
+    return new DuplicationRepositoryRule(new TreeRootHolderComponentProvider(treeRootHolder));
+  }
+
+  public static DuplicationRepositoryRule create(Component root) {
+    return new DuplicationRepositoryRule(new TreeComponentProvider(root));
+  }
+
+  @Override
+  protected void before() throws Throwable {
+    this.delegate = new DuplicationRepositoryImpl();
+  }
+
+  @Override
+  protected void after() {
+    this.componentProvider.reset();
+    this.delegate = null;
+  }
+
+  public Set<Duplication> getDuplications(int fileRef) {
+    componentProvider.ensureInitialized();
+
+    return delegate.getDuplications(componentProvider.getByRef(fileRef));
+  }
+
+  public DuplicationRepositoryRule addDuplication(int fileRef, TextBlock original, TextBlock duplicate) {
+    componentProvider.ensureInitialized();
+
+    delegate.addDuplication(componentProvider.getByRef(fileRef), original, duplicate);
+
+    return this;
+  }
+
+  public DuplicationRepositoryRule addDuplication(int fileRef, TextBlock original, int otherFileRef, TextBlock duplicate) {
+    componentProvider.ensureInitialized();
+
+    delegate.addDuplication(componentProvider.getByRef(fileRef), original, componentProvider.getByRef(otherFileRef), duplicate);
+
+    return this;
+  }
+
+  public DuplicationRepositoryRule addDuplication(int fileRef, TextBlock original, String otherFileKey, TextBlock duplicate) {
+    componentProvider.ensureInitialized();
+
+    delegate.addDuplication(componentProvider.getByRef(fileRef), original, otherFileKey, duplicate);
+
+    return this;
+  }
+
+  @Override
+  public Set<Duplication> getDuplications(Component file) {
+    return delegate.getDuplications(file);
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, TextBlock duplicate) {
+    delegate.addDuplication(file, original, duplicate);
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, Component otherFile, TextBlock duplicate) {
+    delegate.addDuplication(file, original, otherFile, duplicate);
+  }
+
+  @Override
+  public void addDuplication(Component file, TextBlock original, String otherFileKey, TextBlock duplicate) {
+    delegate.addDuplication(file, original, otherFileKey, duplicate);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationTest.java
new file mode 100644 (file)
index 0000000..2d244dd
--- /dev/null
@@ -0,0 +1,194 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class DuplicationTest {
+  private static final TextBlock SOME_ORIGINAL_TEXTBLOCK = new TextBlock(1, 2);
+  private static final TextBlock TEXT_BLOCK_1 = new TextBlock(2, 2);
+  private static final TextBlock TEXT_BLOCK_2 = new TextBlock(2, 3);
+  private static final ReportComponent FILE_COMPONENT_1 = ReportComponent.builder(Component.Type.FILE, 1).build();
+  private static final ReportComponent FILE_COMPONENT_2 = ReportComponent.builder(Component.Type.FILE, 2).build();
+  private static final String FILE_KEY_1 = "1";
+  private static final String FILE_KEY_2 = "2";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void constructor_throws_NPE_if_original_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("original TextBlock can not be null");
+
+    new Duplication(null, Collections.<Duplicate>emptySet());
+  }
+
+  @Test
+  public void constructor_throws_NPE_if_duplicates_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("duplicates can not be null");
+
+    new Duplication(SOME_ORIGINAL_TEXTBLOCK, null);
+  }
+
+  @Test
+  public void constructor_throws_IAE_if_duplicates_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("duplicates can not be empty");
+
+    new Duplication(SOME_ORIGINAL_TEXTBLOCK, Collections.<Duplicate>emptySet());
+  }
+
+  @Test
+  public void constructor_throws_NPE_if_duplicates_contains_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("duplicates can not contain null");
+
+    new Duplication(SOME_ORIGINAL_TEXTBLOCK, new HashSet<>(Arrays.asList(mock(Duplicate.class), null, mock(Duplicate.class))));
+  }
+
+  @Test
+  public void constructor_throws_IAE_if_duplicates_contains_InnerDuplicate_of_original() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("TextBlock of an InnerDuplicate can not be the original TextBlock");
+
+    new Duplication(SOME_ORIGINAL_TEXTBLOCK, new HashSet<>(Arrays.asList(mock(Duplicate.class), new InnerDuplicate(SOME_ORIGINAL_TEXTBLOCK), mock(Duplicate.class))));
+  }
+
+  @Test
+  public void constructor_throws_IAE_when_attempting_to_sort_Duplicate_of_unkown_type() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Unsupported type of Duplicate " + MyDuplicate.class.getName());
+
+    new Duplication(SOME_ORIGINAL_TEXTBLOCK, ImmutableSet.<Duplicate>of(new MyDuplicate(), new MyDuplicate()));
+  }
+
+  private static final class MyDuplicate implements Duplicate {
+
+    @Override
+    public TextBlock getTextBlock() {
+      throw new UnsupportedOperationException("getTextBlock not implemented");
+    }
+  }
+
+  @Test
+  public void getOriginal_returns_original() {
+    assertThat(new Duplication(SOME_ORIGINAL_TEXTBLOCK, ImmutableSet.of(mock(Duplicate.class))).getOriginal()).isSameAs(SOME_ORIGINAL_TEXTBLOCK);
+  }
+
+  @Test
+  public void getDuplicates_sorts_duplicates_by_Inner_then_InProject_then_CrossProject() {
+    CrossProjectDuplicate crossProjectDuplicate = new CrossProjectDuplicate("some key", TEXT_BLOCK_1);
+    InProjectDuplicate inProjectDuplicate = new InProjectDuplicate(FILE_COMPONENT_1, TEXT_BLOCK_1);
+    InnerDuplicate innerDuplicate = new InnerDuplicate(TEXT_BLOCK_1);
+
+    Duplication duplication = new Duplication(
+      SOME_ORIGINAL_TEXTBLOCK,
+      shuffledList(crossProjectDuplicate, inProjectDuplicate, innerDuplicate));
+
+    assertThat(duplication.getDuplicates()).containsExactly(innerDuplicate, inProjectDuplicate, crossProjectDuplicate);
+  }
+
+  @Test
+  public void getDuplicates_sorts_duplicates_of_InnerDuplicate_by_TextBlock() {
+    InnerDuplicate innerDuplicate1 = new InnerDuplicate(TEXT_BLOCK_2);
+    InnerDuplicate innerDuplicate2 = new InnerDuplicate(new TextBlock(3, 3));
+    InnerDuplicate innerDuplicate3 = new InnerDuplicate(new TextBlock(3, 4));
+    InnerDuplicate innerDuplicate4 = new InnerDuplicate(new TextBlock(4, 4));
+
+    assertGetDuplicatesSorting(innerDuplicate1, innerDuplicate2, innerDuplicate3, innerDuplicate4);
+  }
+
+  @Test
+  public void getDuplicates_sorts_duplicates_of_InProjectDuplicate_by_component_then_TextBlock() {
+    InProjectDuplicate innerDuplicate1 = new InProjectDuplicate(FILE_COMPONENT_1, TEXT_BLOCK_1);
+    InProjectDuplicate innerDuplicate2 = new InProjectDuplicate(FILE_COMPONENT_1, TEXT_BLOCK_2);
+    InProjectDuplicate innerDuplicate3 = new InProjectDuplicate(FILE_COMPONENT_2, TEXT_BLOCK_1);
+    InProjectDuplicate innerDuplicate4 = new InProjectDuplicate(FILE_COMPONENT_2, TEXT_BLOCK_2);
+
+    assertGetDuplicatesSorting(innerDuplicate1, innerDuplicate2, innerDuplicate3, innerDuplicate4);
+  }
+
+  @Test
+  public void getDuplicates_sorts_duplicates_of_CrossProjectDuplicate_by_fileKey_then_TextBlock() {
+    CrossProjectDuplicate innerDuplicate1 = new CrossProjectDuplicate(FILE_KEY_1, TEXT_BLOCK_1);
+    CrossProjectDuplicate innerDuplicate2 = new CrossProjectDuplicate(FILE_KEY_1, TEXT_BLOCK_2);
+    CrossProjectDuplicate innerDuplicate3 = new CrossProjectDuplicate(FILE_KEY_2, TEXT_BLOCK_1);
+    CrossProjectDuplicate innerDuplicate4 = new CrossProjectDuplicate(FILE_KEY_2, TEXT_BLOCK_2);
+
+    assertGetDuplicatesSorting(innerDuplicate1, innerDuplicate2, innerDuplicate3, innerDuplicate4);
+  }
+
+  @Test
+  public void equals_compares_on_original_and_duplicates() {
+    Duplication duplication = new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_1)));
+
+    assertThat(duplication).isEqualTo(duplication);
+    assertThat(duplication).isEqualTo(new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_1))));
+    assertThat(duplication).isNotEqualTo(new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_2))));
+    assertThat(duplication).isNotEqualTo(new Duplication(TEXT_BLOCK_1, Arrays.<Duplicate>asList(new InnerDuplicate(SOME_ORIGINAL_TEXTBLOCK))));
+  }
+
+  @Test
+  public void hashcode_is_based_on_original_only() {
+    Duplication duplication = new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_1)));
+
+    assertThat(duplication.hashCode()).isEqualTo(new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_1))).hashCode());
+    assertThat(duplication.hashCode()).isNotEqualTo(new Duplication(SOME_ORIGINAL_TEXTBLOCK, Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_2))).hashCode());
+    assertThat(duplication.hashCode()).isNotEqualTo(new Duplication(TEXT_BLOCK_1, Arrays.<Duplicate>asList(new InnerDuplicate(SOME_ORIGINAL_TEXTBLOCK))).hashCode());
+  }
+
+  @Test
+  public void verify_toString() {
+    Duplication duplication = new Duplication(
+      SOME_ORIGINAL_TEXTBLOCK,
+      Arrays.<Duplicate>asList(new InnerDuplicate(TEXT_BLOCK_1)));
+
+    assertThat(duplication.toString())
+      .isEqualTo("Duplication{original=TextBlock{start=1, end=2}, duplicates=[InnerDuplicate{textBlock=TextBlock{start=2, end=2}}]}");
+  }
+
+  @SafeVarargs
+  private final <T extends Duplicate> void assertGetDuplicatesSorting(T... expected) {
+    Duplication duplication = new Duplication(SOME_ORIGINAL_TEXTBLOCK, shuffledList(expected));
+
+    assertThat(duplication.getDuplicates()).containsExactly(expected);
+  }
+
+  private static List<Duplicate> shuffledList(Duplicate... duplicates) {
+    List<Duplicate> res = new ArrayList<>(Arrays.asList(duplicates));
+    Collections.shuffle(res);
+    return res;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InProjectDuplicateTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InProjectDuplicateTest.java
new file mode 100644 (file)
index 0000000..c0a33c1
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.ReportComponent;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InProjectDuplicateTest {
+  private static final Component FILE_1 = ReportComponent.builder(Component.Type.FILE, 1).build();
+  private static final Component FILE_2 = ReportComponent.builder(Component.Type.FILE, 2).build();
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void constructors_throws_NPE_if_file_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("file can not be null");
+
+    new InProjectDuplicate(null, new TextBlock(1, 1));
+  }
+
+  @Test
+  public void constructors_throws_NPE_if_textBlock_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("textBlock of duplicate can not be null");
+
+    new InProjectDuplicate(FILE_1, null);
+  }
+
+  @Test
+  public void constructors_throws_IAE_if_type_of_file_argument_is_not_FILE() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("file must be of type FILE");
+
+    new InProjectDuplicate(ReportComponent.builder(Component.Type.PROJECT, 1).build(), new TextBlock(1, 1));
+  }
+
+  @Test
+  public void getTextBlock_returns_TextBlock_constructor_argument() {
+    TextBlock textBlock = new TextBlock(2, 3);
+
+    assertThat(new InProjectDuplicate(FILE_1, textBlock).getTextBlock()).isSameAs(textBlock);
+  }
+
+  @Test
+  public void getFile_returns_Component_constructor_argument() {
+    assertThat(new InProjectDuplicate(FILE_1, new TextBlock(2, 3)).getFile()).isSameAs(FILE_1);
+  }
+
+  @Test
+  public void equals_compares_on_file_and_TextBlock() {
+    TextBlock textBlock1 = new TextBlock(1, 2);
+
+    assertThat(new InProjectDuplicate(FILE_1, textBlock1)).isEqualTo(new InProjectDuplicate(FILE_1, new TextBlock(1, 2)));
+
+    assertThat(new InProjectDuplicate(FILE_1, textBlock1)).isNotEqualTo(new InProjectDuplicate(FILE_1, new TextBlock(1, 1)));
+    assertThat(new InProjectDuplicate(FILE_1, textBlock1)).isNotEqualTo(new InProjectDuplicate(FILE_2, textBlock1));
+  }
+
+  @Test
+  public void hashcode_depends_on_file_and_TextBlock() {
+    TextBlock textBlock = new TextBlock(1, 2);
+    assertThat(new InProjectDuplicate(FILE_1, textBlock).hashCode()).isEqualTo(new InProjectDuplicate(FILE_1, textBlock).hashCode());
+
+    assertThat(new InProjectDuplicate(FILE_1, textBlock).hashCode()).isNotEqualTo(new InProjectDuplicate(FILE_2, textBlock).hashCode());
+    assertThat(new InProjectDuplicate(FILE_1, textBlock).hashCode()).isNotEqualTo(new InProjectDuplicate(FILE_2, new TextBlock(1, 1)).hashCode());
+  }
+
+  @Test
+  public void verify_toString() {
+    assertThat(new InProjectDuplicate(FILE_1, new TextBlock(1, 2)).toString()).isEqualTo("InProjectDuplicate{file=ReportComponent{ref=1, key='key_1', type=FILE}, textBlock=TextBlock{start=1, end=2}}");
+  }
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InnerDuplicateTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InnerDuplicateTest.java
new file mode 100644 (file)
index 0000000..1f12c26
--- /dev/null
@@ -0,0 +1,62 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InnerDuplicateTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void constructors_throws_NPE_if_textBlock_is_null() {
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("textBlock of duplicate can not be null");
+
+    new InnerDuplicate(null);
+  }
+
+  @Test
+  public void getTextBlock_returns_TextBlock_constructor_argument() {
+    TextBlock textBlock = new TextBlock(2, 3);
+    assertThat(new InnerDuplicate(textBlock).getTextBlock()).isSameAs(textBlock);
+  }
+
+  @Test
+  public void equals_compares_on_TextBlock() {
+    assertThat(new InnerDuplicate(new TextBlock(1, 2))).isEqualTo(new InnerDuplicate(new TextBlock(1, 2)));
+    assertThat(new InnerDuplicate(new TextBlock(1, 2))).isNotEqualTo(new InnerDuplicate(new TextBlock(1, 1)));
+  }
+
+  @Test
+  public void hashcode_is_TextBlock_hashcode() {
+    TextBlock textBlock = new TextBlock(1, 2);
+    assertThat(new InnerDuplicate(textBlock).hashCode()).isEqualTo(textBlock.hashCode());
+  }
+
+  @Test
+  public void verify_toString() {
+    assertThat(new InnerDuplicate(new TextBlock(1, 2)).toString()).isEqualTo("InnerDuplicate{textBlock=TextBlock{start=1, end=2}}");
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/TextBlockTest.java b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/TextBlockTest.java
new file mode 100644 (file)
index 0000000..5250bd9
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.duplication;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TextBlockTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void constructor_throws_IAE_if_start_is_0() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("First line index must be >= 1");
+
+    new TextBlock(0, 2);
+  }
+
+  @Test
+  public void constructor_throws_IAE_if_end_is_less_than_start() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Last line index must be >= first line index");
+
+    new TextBlock(1, 0);
+  }
+
+  @Test
+  public void getStart_returns_constructor_argument() {
+    TextBlock textBlock = new TextBlock(15, 300);
+
+    assertThat(textBlock.getStart()).isEqualTo(15);
+  }
+
+  @Test
+  public void getEnd_returns_constructor_argument() {
+    TextBlock textBlock = new TextBlock(15, 300);
+
+    assertThat(textBlock.getEnd()).isEqualTo(300);
+  }
+
+  @Test
+  public void equals_compares_on_start_and_end() {
+    assertThat(new TextBlock(15, 15)).isEqualTo(new TextBlock(15, 15));
+    assertThat(new TextBlock(15, 300)).isEqualTo(new TextBlock(15, 300));
+    assertThat(new TextBlock(15, 300)).isNotEqualTo(new TextBlock(15, 15));
+  }
+
+  @Test
+  public void hashcode_is_based__on_start_and_end() {
+    assertThat(new TextBlock(15, 15).hashCode()).isEqualTo(new TextBlock(15, 15).hashCode());
+    assertThat(new TextBlock(15, 300).hashCode()).isEqualTo(new TextBlock(15, 300).hashCode());
+    assertThat(new TextBlock(15, 300).hashCode()).isNotEqualTo(new TextBlock(15, 15).hashCode());
+  }
+
+  @Test
+  public void TextBlock_defines_natural_order_by_start_then_end() {
+    TextBlock textBlock1 = new TextBlock(1, 1);
+    TextBlock textBlock2 = new TextBlock(1, 2);
+    TextBlock textBlock3 = new TextBlock(2, 3);
+    TextBlock textBlock4 = new TextBlock(2, 4);
+    TextBlock textBlock5 = new TextBlock(5, 5);
+
+    List<TextBlock> shuffledList = new ArrayList<>(Arrays.asList(textBlock1, textBlock2, textBlock3, textBlock4, textBlock5));
+    Collections.shuffle(shuffledList, new Random());
+
+    Collections.sort(shuffledList);
+    assertThat(shuffledList).containsExactly(textBlock1, textBlock2, textBlock3, textBlock4, textBlock5);
+  }
+
+  @Test
+  public void verify_toString() {
+    assertThat(new TextBlock(13, 400).toString()).isEqualTo("TextBlock{start=13, end=400}");
+
+  }
+}