diff options
author | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2015-11-06 16:49:48 +0100 |
---|---|---|
committer | Sébastien Lesaint <sebastien.lesaint@sonarsource.com> | 2015-11-09 16:34:20 +0100 |
commit | 6ec797b49144e1ab5899a105eaa98c917cacc28f (patch) | |
tree | 06c546b2746098bd2c4e6587cb9ce1349bdd24a4 /server | |
parent | e56313c708502cf6af836fed195836c7b43eaa3c (diff) | |
download | sonarqube-6ec797b49144e1ab5899a105eaa98c917cacc28f.tar.gz sonarqube-6ec797b49144e1ab5899a105eaa98c917cacc28f.zip |
SONAR-6990 add DuplicationRepository and LoadDuplicationsFromReportStep
Diffstat (limited to 'server')
21 files changed, 2251 insertions, 0 deletions
diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java index e2118173965..f74ac8ba1ec 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/container/ReportComputeEngineContainerPopulator.java @@ -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 index 00000000000..e46c5687711 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/AbstractDuplicate.java @@ -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 index 00000000000..e469c50c7a9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/CrossProjectDuplicate.java @@ -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 index 00000000000..8c45837d329 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplicate.java @@ -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 index 00000000000..d57e9e27853 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/Duplication.java @@ -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 index 00000000000..0bfc05a004a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepository.java @@ -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 index 00000000000..db48356be6a --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/DuplicationRepositoryImpl.java @@ -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 index 00000000000..7ca80f24e62 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InProjectDuplicate.java @@ -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 index 00000000000..1448691873f --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/InnerDuplicate.java @@ -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 index 00000000000..e317e7c9225 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/TextBlock.java @@ -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 index 00000000000..fbb40f500f4 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/duplication/package-info.java @@ -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 index 00000000000..fa6534aecfb --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/LoadDuplicationsFromReportStep.java @@ -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()); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ReportComputationSteps.java b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ReportComputationSteps.java index 3813fa5e4b0..067fd2f44cc 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/computation/step/ReportComputationSteps.java +++ b/server/sonar-server/src/main/java/org/sonar/server/computation/step/ReportComputationSteps.java @@ -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 index 00000000000..8d30c16880f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/CrossProjectDuplicateTest.java @@ -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 index 00000000000..beef4a7c8fa --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicateTest.java @@ -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 index 00000000000..c86e2021d48 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryImplTest.java @@ -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 index 00000000000..583fbd62410 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationRepositoryRule.java @@ -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 index 00000000000..2d244dde873 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/DuplicationTest.java @@ -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 index 00000000000..c0a33c1767f --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InProjectDuplicateTest.java @@ -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 index 00000000000..1f12c26aecd --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/InnerDuplicateTest.java @@ -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 index 00000000000..5250bd96f04 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/computation/duplication/TextBlockTest.java @@ -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}"); + + } +} |