import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import org.sonar.server.computation.snapshot.Snapshot;
+import org.sonar.server.computation.util.InitializedProperty;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
return rootComponentRef.getProperty();
}
- private static class InitializedProperty<E> {
- private E property;
- private boolean initialized = false;
-
- public InitializedProperty setProperty(@Nullable E property) {
- this.property = property;
- this.initialized = true;
- return this;
- }
-
- @CheckForNull
- public E getProperty() {
- return property;
- }
-
- public boolean isInitialized() {
- return initialized;
- }
- }
-
}
import org.sonar.server.computation.component.SettingsRepositoryImpl;
import org.sonar.server.computation.component.TreeRootHolderImpl;
import org.sonar.server.computation.debt.DebtModelHolderImpl;
+import org.sonar.server.computation.duplication.CrossProjectDuplicationStatusHolderImpl;
import org.sonar.server.computation.duplication.DuplicationRepositoryImpl;
+import org.sonar.server.computation.duplication.IntegrateCrossProjectDuplications;
import org.sonar.server.computation.event.EventRepositoryImpl;
import org.sonar.server.computation.filesystem.ComputationTempFolderProvider;
import org.sonar.server.computation.issue.BaseIssuesLoader;
// holders
AnalysisMetadataHolderImpl.class,
+ CrossProjectDuplicationStatusHolderImpl.class,
BatchReportDirectoryHolderImpl.class,
TreeRootHolderImpl.class,
PeriodsHolderImpl.class,
TrackerExecution.class,
BaseIssuesLoader.class,
+ // duplication
+ IntegrateCrossProjectDuplications.class,
+
// views
ViewIndex.class);
}
--- /dev/null
+/*
+ * 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.sonar.server.computation.analysis.AnalysisMetadataHolder;
+
+/**
+ * A simple holder to know if the cross project duplication should be computed or not.
+ *
+ * A log will be displayed whether or not the cross project duplication is enabled or not.
+ */
+public interface CrossProjectDuplicationStatusHolder {
+
+ /**
+ * Cross project duplications is enabled when :
+ * <ui>
+ * <li>{@link AnalysisMetadataHolder#isCrossProjectDuplicationEnabled()} is true</li>
+ * <li>{@link AnalysisMetadataHolder#getBranch()} ()} is null</li>
+ * </ui>
+ */
+ boolean isEnabled();
+
+}
--- /dev/null
+/*
+ * 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.Preconditions;
+import javax.annotation.CheckForNull;
+import org.picocontainer.Startable;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.server.computation.analysis.AnalysisMetadataHolder;
+
+public class CrossProjectDuplicationStatusHolderImpl implements CrossProjectDuplicationStatusHolder, Startable {
+
+ private static final Logger LOGGER = Loggers.get(CrossProjectDuplicationStatusHolderImpl.class);
+
+ @CheckForNull
+ private Boolean enabled;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
+
+ public CrossProjectDuplicationStatusHolderImpl(AnalysisMetadataHolder analysisMetadataHolder) {
+ this.analysisMetadataHolder = analysisMetadataHolder;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ Preconditions.checkState(enabled != null, "Flag hasn't been initialized, the start() should have been called before.");
+ return enabled;
+ }
+
+ @Override
+ public void start() {
+ boolean crossProjectDuplicationIsEnabledInReport = analysisMetadataHolder.isCrossProjectDuplicationEnabled();
+ boolean branchIsUsed = analysisMetadataHolder.getBranch() != null;
+ if (crossProjectDuplicationIsEnabledInReport && !branchIsUsed) {
+ LOGGER.info("Cross project duplication is enabled");
+ this.enabled = true;
+ } else {
+ if (!crossProjectDuplicationIsEnabledInReport) {
+ LOGGER.info("Cross project duplication is disabled because it's disabled in the analysis report");
+ } else {
+ LOGGER.info("Cross project duplication is disabled because of a branch is used");
+ }
+ this.enabled = false;
+ }
+ }
+
+ @Override
+ public void stop() {
+ // nothing to do
+ }
+}
--- /dev/null
+/*
+ * 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.Predicate;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.Nonnull;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.detector.suffixtree.SuffixTreeCloneDetectionAlgorithm;
+import org.sonar.duplications.index.CloneGroup;
+import org.sonar.duplications.index.CloneIndex;
+import org.sonar.duplications.index.ClonePart;
+import org.sonar.duplications.index.PackedMemoryCloneIndex;
+import org.sonar.server.computation.component.Component;
+
+import static com.google.common.collect.FluentIterable.from;
+
+/**
+ * Transform a list of duplication blocks into clone groups, then add these clone groups into the duplication repository.
+ */
+public class IntegrateCrossProjectDuplications {
+
+ private static final Logger LOGGER = Loggers.get(IntegrateCrossProjectDuplications.class);
+
+ private static final String JAVA_KEY = "java";
+
+ private static final int MAX_CLONE_GROUP_PER_FILE = 100;
+ private static final int MAX_CLONE_PART_PER_GROUP = 100;
+
+ private final Settings settings;
+ private final DuplicationRepository duplicationRepository;
+
+ private Map<String, NumberOfUnitsNotLessThan> numberOfUnitsByLanguage = new HashMap<>();
+
+ public IntegrateCrossProjectDuplications(Settings settings, DuplicationRepository duplicationRepository) {
+ this.settings = settings;
+ this.duplicationRepository = duplicationRepository;
+ }
+
+ public void computeCpd(Component component, Collection<Block> originBlocks, Collection<Block> duplicationBlocks) {
+ CloneIndex duplicationIndex = new PackedMemoryCloneIndex();
+ populateIndex(duplicationIndex, originBlocks);
+ populateIndex(duplicationIndex, duplicationBlocks);
+
+ List<CloneGroup> duplications = SuffixTreeCloneDetectionAlgorithm.detect(duplicationIndex, originBlocks);
+ Iterable<CloneGroup> filtered = from(duplications).filter(getNumberOfUnitsNotLessThan(component.getFileAttributes().getLanguageKey()));
+ addDuplications(component, filtered);
+ }
+
+ private static void populateIndex(CloneIndex duplicationIndex, Collection<Block> duplicationBlocks) {
+ for (Block block : duplicationBlocks) {
+ duplicationIndex.insert(block);
+ }
+ }
+
+ private void addDuplications(Component file, Iterable<CloneGroup> duplications) {
+ int cloneGroupCount = 0;
+ for (CloneGroup duplication : duplications) {
+ cloneGroupCount++;
+ if (cloneGroupCount > MAX_CLONE_GROUP_PER_FILE) {
+ LOGGER.warn("Too many duplication groups on file {}. Keeping only the first {} groups.", file.getKey(), MAX_CLONE_GROUP_PER_FILE);
+ break;
+ }
+ addDuplication(file, duplication);
+ }
+ }
+
+ private void addDuplication(Component file, CloneGroup duplication) {
+ ClonePart originPart = duplication.getOriginPart();
+ TextBlock originTextBlock = new TextBlock(originPart.getStartLine(), originPart.getEndLine());
+ int clonePartCount = 0;
+ for (ClonePart part : from(duplication.getCloneParts()).filter(new DoesNotMatchOriginalPart(originPart))) {
+ clonePartCount++;
+ if (clonePartCount > MAX_CLONE_PART_PER_GROUP) {
+ LOGGER.warn("Too many duplication references on file {} for block at line {}. Keeping only the first {} references.",
+ file.getKey(), originPart.getStartLine(), MAX_CLONE_PART_PER_GROUP);
+ break;
+ }
+ duplicationRepository.addDuplication(file, originTextBlock, part.getResourceId(),
+ new TextBlock(part.getStartLine(), part.getEndLine()));
+ }
+ }
+
+ private NumberOfUnitsNotLessThan getNumberOfUnitsNotLessThan(String language) {
+ NumberOfUnitsNotLessThan numberOfUnitsNotLessThan = numberOfUnitsByLanguage.get(language);
+ if (numberOfUnitsNotLessThan == null) {
+ numberOfUnitsNotLessThan = new NumberOfUnitsNotLessThan(getMinimumTokens(language));
+ numberOfUnitsByLanguage.put(language, numberOfUnitsNotLessThan);
+ }
+ return numberOfUnitsNotLessThan;
+ }
+
+ private int getMinimumTokens(String languageKey) {
+ // The java language is an exception : it doesn't compute tokens but statement, so the settings could not be used.
+ if (languageKey.equalsIgnoreCase(JAVA_KEY)) {
+ return 0;
+ }
+ int minimumTokens = settings.getInt("sonar.cpd." + languageKey + ".minimumTokens");
+ if (minimumTokens == 0) {
+ return 100;
+ }
+ return minimumTokens;
+ }
+
+ private static class NumberOfUnitsNotLessThan implements Predicate<CloneGroup> {
+ private final int min;
+
+ public NumberOfUnitsNotLessThan(int min) {
+ this.min = min;
+ }
+
+ @Override
+ public boolean apply(@Nonnull CloneGroup input) {
+ return input.getLengthInUnits() >= min;
+ }
+ }
+
+ private static class DoesNotMatchOriginalPart implements Predicate<ClonePart> {
+ private final ClonePart originPart;
+
+ private DoesNotMatchOriginalPart(ClonePart originPart) {
+ this.originPart = originPart;
+ }
+
+ @Override
+ public boolean apply(ClonePart part) {
+ return !part.equals(originPart);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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 com.google.common.base.Function;
+import java.util.Collection;
+import java.util.List;
+import javax.annotation.Nonnull;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.batch.protocol.output.BatchReport.CpdTextBlock;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.duplication.DuplicationUnitDto;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+import org.sonar.server.computation.analysis.AnalysisMetadataHolder;
+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.TreeRootHolder;
+import org.sonar.server.computation.component.TypeAwareVisitorAdapter;
+import org.sonar.server.computation.duplication.CrossProjectDuplicationStatusHolder;
+import org.sonar.server.computation.duplication.IntegrateCrossProjectDuplications;
+import org.sonar.server.computation.snapshot.Snapshot;
+
+import static com.google.common.collect.FluentIterable.from;
+import static com.google.common.collect.Lists.newArrayList;
+import static org.sonar.server.computation.component.ComponentVisitor.Order.PRE_ORDER;
+
+/**
+ * Feed the duplications repository from the cross project duplication blocks computed with duplications blocks of the analysis report.
+ *
+ * Blocks can be empty if :
+ * - The file is excluded from the analysis using {@link org.sonar.api.CoreProperties#CPD_EXCLUSIONS}
+ * - On Java, if the number of statements of the file is too small, nothing will be sent.
+ */
+public class LoadCrossProjectDuplicationsRepositoryStep implements ComputationStep {
+
+ private static final Logger LOGGER = Loggers.get(LoadCrossProjectDuplicationsRepositoryStep.class);
+
+ private final TreeRootHolder treeRootHolder;
+ private final BatchReportReader reportReader;
+ private final AnalysisMetadataHolder analysisMetadataHolder;
+ private final IntegrateCrossProjectDuplications integrateCrossProjectDuplications;
+ private final CrossProjectDuplicationStatusHolder crossProjectDuplicationStatusHolder;
+ private final DbClient dbClient;
+
+ public LoadCrossProjectDuplicationsRepositoryStep(TreeRootHolder treeRootHolder, BatchReportReader reportReader,
+ AnalysisMetadataHolder analysisMetadataHolder, CrossProjectDuplicationStatusHolder crossProjectDuplicationStatusHolder,
+ IntegrateCrossProjectDuplications integrateCrossProjectDuplications, DbClient dbClient) {
+ this.treeRootHolder = treeRootHolder;
+ this.reportReader = reportReader;
+ this.analysisMetadataHolder = analysisMetadataHolder;
+ this.integrateCrossProjectDuplications = integrateCrossProjectDuplications;
+ this.crossProjectDuplicationStatusHolder = crossProjectDuplicationStatusHolder;
+ this.dbClient = dbClient;
+ }
+
+ @Override
+ public void execute() {
+ if (crossProjectDuplicationStatusHolder.isEnabled()) {
+ new DepthTraversalTypeAwareCrawler(new CrossProjectDuplicationVisitor()).visit(treeRootHolder.getRoot());
+ }
+ }
+
+ @Override
+ public String getDescription() {
+ return "Compute cross project duplications";
+ }
+
+ private class CrossProjectDuplicationVisitor extends TypeAwareVisitorAdapter {
+
+ private CrossProjectDuplicationVisitor() {
+ super(CrawlerDepthLimit.FILE, PRE_ORDER);
+ }
+
+ @Override
+ public void visitFile(Component file) {
+ visitComponent(file);
+ }
+
+ private void visitComponent(Component component) {
+ List<CpdTextBlock> cpdTextBlocks = newArrayList(reportReader.readCpdTextBlocks(component.getReportAttributes().getRef()));
+ LOGGER.trace("Found {} cpd blocks on file {}", cpdTextBlocks.size(), component.getKey());
+ if (cpdTextBlocks.isEmpty()) {
+ return;
+ }
+
+ Collection<String> hashes = from(cpdTextBlocks).transform(CpdTextBlockToHash.INSTANCE).toList();
+ List<DuplicationUnitDto> dtos = selectDuplicates(component, hashes);
+ Collection<Block> duplicatedBlocks = from(dtos).transform(DtoToBlock.INSTANCE).toList();
+ Collection<Block> originBlocks = from(cpdTextBlocks).transform(new CpdTextBlockToBlock(component.getKey())).toList();
+
+ integrateCrossProjectDuplications.computeCpd(component, originBlocks, duplicatedBlocks);
+ }
+
+ private List<DuplicationUnitDto> selectDuplicates(Component file, Collection<String> hashes) {
+ DbSession dbSession = dbClient.openSession(false);
+ try {
+ Snapshot projectSnapshot = analysisMetadataHolder.getBaseProjectSnapshot();
+ Long projectSnapshotId = projectSnapshot == null ? null : projectSnapshot.getId();
+ return dbClient.duplicationDao().selectCandidates(dbSession, projectSnapshotId, file.getFileAttributes().getLanguageKey(), hashes);
+ } finally {
+ dbClient.closeSession(dbSession);
+ }
+ }
+ }
+
+ private enum CpdTextBlockToHash implements Function<CpdTextBlock, String> {
+ INSTANCE;
+
+ @Override
+ public String apply(@Nonnull CpdTextBlock duplicationBlock) {
+ return duplicationBlock.getHash();
+ }
+ }
+
+ private enum DtoToBlock implements Function<DuplicationUnitDto, Block> {
+ INSTANCE;
+
+ @Override
+ public Block apply(@Nonnull DuplicationUnitDto dto) {
+ // Not that the dto doesn't contains start/end token indexes
+ return Block.builder()
+ .setResourceId(dto.getComponentKey())
+ .setBlockHash(new ByteArray(dto.getHash()))
+ .setIndexInFile(dto.getIndexInFile())
+ .setLines(dto.getStartLine(), dto.getEndLine())
+ .build();
+ }
+ }
+
+ private static class CpdTextBlockToBlock implements Function<CpdTextBlock, Block> {
+ private final String fileKey;
+ private int indexInFile = 0;
+
+ public CpdTextBlockToBlock(String fileKey) {
+ this.fileKey = fileKey;
+ }
+
+ @Override
+ public Block apply(@Nonnull CpdTextBlock duplicationBlock) {
+ return Block.builder()
+ .setResourceId(fileKey)
+ .setBlockHash(new ByteArray(duplicationBlock.getHash()))
+ .setIndexInFile(indexInFile++)
+ .setLines(duplicationBlock.getStartLine(), duplicationBlock.getEndLine())
+ .setUnit(duplicationBlock.getStartTokenIndex(), duplicationBlock.getEndTokenIndex())
+ .build();
+ }
+ }
+
+}
LoadQualityGateStep.class,
LoadPeriodsStep.class,
+ LoadCrossProjectDuplicationsRepositoryStep.class,
+
// data computation
SizeMeasuresStep.class,
NewCoverageMeasuresStep.class,
--- /dev/null
+/*
+ * 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.util;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+public class InitializedProperty<E> {
+ private E property;
+ private boolean initialized = false;
+
+ public InitializedProperty setProperty(@Nullable E property) {
+ this.property = property;
+ this.initialized = true;
+ return this;
+ }
+
+ @CheckForNull
+ public E getProperty() {
+ return property;
+ }
+
+ public boolean isInitialized() {
+ return initialized;
+ }
+}
--- /dev/null
+/*
+ * 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.analysis;
+
+import java.util.Date;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.junit.rules.ExternalResource;
+import org.sonar.server.computation.snapshot.Snapshot;
+import org.sonar.server.computation.util.InitializedProperty;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+import static com.google.common.base.Preconditions.checkState;
+
+public class AnalysisMetadataHolderRule extends ExternalResource implements AnalysisMetadataHolder {
+
+ private InitializedProperty<Long> analysisDate = new InitializedProperty<>();
+
+ private InitializedProperty<Snapshot> baseProjectSnapshot = new InitializedProperty<>();
+
+ private InitializedProperty<Boolean> crossProjectDuplicationEnabled = new InitializedProperty<>();
+
+ private InitializedProperty<String> branch = new InitializedProperty<>();
+
+ private InitializedProperty<Integer> rootComponentRef = new InitializedProperty<>();
+
+ public AnalysisMetadataHolderRule setAnalysisDate(Date date) {
+ checkNotNull(date, "Date must not be null");
+ this.analysisDate.setProperty(date.getTime());
+ return this;
+ }
+
+ @Override
+ public Date getAnalysisDate() {
+ checkState(analysisDate.isInitialized(), "Analysis date has not been set");
+ return new Date(this.analysisDate.getProperty());
+ }
+
+ @Override
+ public boolean isFirstAnalysis() {
+ return getBaseProjectSnapshot() == null;
+ }
+
+ public AnalysisMetadataHolderRule setBaseProjectSnapshot(@Nullable Snapshot baseProjectSnapshot) {
+ this.baseProjectSnapshot.setProperty(baseProjectSnapshot);
+ return this;
+ }
+
+ @Override
+ @CheckForNull
+ public Snapshot getBaseProjectSnapshot() {
+ checkState(baseProjectSnapshot.isInitialized(), "Base project snapshot has not been set");
+ return baseProjectSnapshot.getProperty();
+ }
+
+ public AnalysisMetadataHolderRule setCrossProjectDuplicationEnabled(boolean isCrossProjectDuplicationEnabled) {
+ this.crossProjectDuplicationEnabled.setProperty(isCrossProjectDuplicationEnabled);
+ return this;
+ }
+
+ @Override
+ public boolean isCrossProjectDuplicationEnabled() {
+ checkState(crossProjectDuplicationEnabled.isInitialized(), "Cross project duplication flag has not been set");
+ return crossProjectDuplicationEnabled.getProperty();
+ }
+
+ public AnalysisMetadataHolderRule setBranch(@Nullable String branch) {
+ this.branch.setProperty(branch);
+ return this;
+ }
+
+ @Override
+ public String getBranch() {
+ checkState(branch.isInitialized(), "Branch has not been set");
+ return branch.getProperty();
+ }
+
+ public AnalysisMetadataHolderRule setRootComponentRef(int rootComponentRef) {
+ this.rootComponentRef.setProperty(rootComponentRef);
+ return this;
+ }
+
+ @Override
+ public int getRootComponentRef() {
+ checkState(rootComponentRef.isInitialized(), "Root component ref has not been set");
+ return rootComponentRef.getProperty();
+ }
+}
--- /dev/null
+/*
+ * 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.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.server.computation.analysis.AnalysisMetadataHolderRule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CrossProjectDuplicationStatusHolderImplTest {
+
+ static String BRANCH = "origin/master";
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ @Rule
+ public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+ CrossProjectDuplicationStatusHolderImpl underTest = new CrossProjectDuplicationStatusHolderImpl(analysisMetadataHolder);
+
+ @Test
+ public void cross_project_duplication_is_enabled_when_enabled_in_report_and_no_branch() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(true)
+ .setBranch(null);
+ underTest.start();
+
+ assertThat(underTest.isEnabled()).isTrue();
+ assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly("Cross project duplication is enabled");
+ }
+
+ @Test
+ public void cross_project_duplication_is_disabled_when_not_enabled_in_report() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(false)
+ .setBranch(null);
+ underTest.start();
+
+ assertThat(underTest.isEnabled()).isFalse();
+ assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly("Cross project duplication is disabled because it's disabled in the analysis report");
+ }
+
+ @Test
+ public void cross_project_duplication_is_disabled_when_branch_is_used() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(true)
+ .setBranch(BRANCH);
+ underTest.start();
+
+ assertThat(underTest.isEnabled()).isFalse();
+ assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly("Cross project duplication is disabled because of a branch is used");
+ }
+
+ @Test
+ public void cross_project_duplication_is_disabled_when_not_enabled_in_report_and_when_branch_is_used() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(false)
+ .setBranch(BRANCH);
+ underTest.start();
+
+ assertThat(underTest.isEnabled()).isFalse();
+ assertThat(logTester.logs(LoggerLevel.INFO)).containsOnly("Cross project duplication is disabled because it's disabled in the analysis report");
+ }
+
+ @Test
+ public void flag_is_build_in_start() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(true)
+ .setBranch(null);
+ underTest.start();
+ assertThat(underTest.isEnabled()).isTrue();
+
+ // Change the boolean from the report. This can never happen, it's only to validate that the flag is build in the start method
+ analysisMetadataHolder.setCrossProjectDuplicationEnabled(false);
+ assertThat(underTest.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void isEnabled_throws_ISE_when_start_have_not_been_called_before() throws Exception {
+ thrown.expect(IllegalStateException.class);
+ thrown.expectMessage("Flag hasn't been initialized, the start() should have been called before");
+
+ underTest.isEnabled();
+ }
+}
--- /dev/null
+/*
+ * 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.Collection;
+import java.util.Collections;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.config.Settings;
+import org.sonar.api.utils.log.LogTester;
+import org.sonar.api.utils.log.LoggerLevel;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.FileAttributes;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.ReportComponent.builder;
+
+public class IntegrateCrossProjectDuplicationsTest {
+
+ @Rule
+ public LogTester logTester = new LogTester();
+
+ static final String XOO_LANGUAGE = "xoo";
+
+ static final String ORIGIN_FILE_KEY = "ORIGIN_FILE_KEY";
+ static final Component ORIGIN_FILE = builder(FILE, 1)
+ .setKey(ORIGIN_FILE_KEY)
+ .setFileAttributes(new FileAttributes(false, XOO_LANGUAGE))
+ .build();
+
+ static final String OTHER_FILE_KEY = "OTHER_FILE_KEY";
+
+ Settings settings = new Settings();
+
+ DuplicationRepository duplicationRepository = mock(DuplicationRepository.class);
+
+ IntegrateCrossProjectDuplications underTest = new IntegrateCrossProjectDuplications(settings, duplicationRepository);
+
+ @Test
+ public void add_duplications_from_two_blocks() {
+ settings.setProperty("sonar.cpd.xoo.minimumTokens", 10);
+
+ Collection<Block> originBlocks = asList(
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 43)
+ .setUnit(0, 5)
+ .build(),
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("2b5747f0e4c59124"))
+ .setIndexInFile(1)
+ .setLines(32, 45)
+ .setUnit(5, 20)
+ .build()
+ );
+
+ Collection<Block> duplicatedBlocks = asList(
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(40, 53)
+ .build(),
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("2b5747f0e4c59124"))
+ .setIndexInFile(1)
+ .setLines(42, 55)
+ .build());
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ verify(duplicationRepository).addDuplication(
+ ORIGIN_FILE,
+ new TextBlock(30, 45),
+ OTHER_FILE_KEY,
+ new TextBlock(40, 55)
+ );
+ }
+
+ @Test
+ public void add_duplications_from_a_single_block() {
+ settings.setProperty("sonar.cpd.xoo.minimumTokens", 10);
+
+ Collection<Block> originBlocks = singletonList(
+ // This block contains 11 tokens -> a duplication will be created
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 10)
+ .build()
+ );
+
+ Collection<Block> duplicatedBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(40, 55)
+ .build()
+ );
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ verify(duplicationRepository).addDuplication(
+ ORIGIN_FILE,
+ new TextBlock(30, 45),
+ OTHER_FILE_KEY,
+ new TextBlock(40, 55)
+ );
+ }
+
+ @Test
+ public void add_no_duplication_when_not_enough_tokens() {
+ settings.setProperty("sonar.cpd.xoo.minimumTokens", 10);
+
+ Collection<Block> originBlocks = singletonList(
+ // This block contains 5 tokens -> not enough to consider it as a duplication
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 4)
+ .build()
+ );
+
+ Collection<Block> duplicatedBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(40, 55)
+ .build()
+ );
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ verifyNoMoreInteractions(duplicationRepository);
+ }
+
+ @Test
+ public void add_no_duplication_when_no_duplicated_blocks() {
+ settings.setProperty("sonar.cpd.xoo.minimumTokens", 10);
+
+ Collection<Block> originBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 10)
+ .build()
+ );
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, Collections.<Block>emptyList());
+
+ verifyNoMoreInteractions(duplicationRepository);
+ }
+
+ @Test
+ public void add_duplication_for_java_even_when_no_token() {
+ Component javaFile = builder(FILE, 1)
+ .setKey(ORIGIN_FILE_KEY)
+ .setFileAttributes(new FileAttributes(false, "java"))
+ .build();
+
+ Collection<Block> originBlocks = singletonList(
+ // This block contains 0 token
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 0)
+ .build()
+ );
+
+ Collection<Block> duplicatedBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(40, 55)
+ .build()
+ );
+
+ underTest.computeCpd(javaFile, originBlocks, duplicatedBlocks);
+
+ verify(duplicationRepository).addDuplication(
+ ORIGIN_FILE,
+ new TextBlock(30, 45),
+ OTHER_FILE_KEY,
+ new TextBlock(40, 55)
+ );
+ }
+
+ @Test
+ public void default_minimum_tokens_is_one_hundred() {
+ settings.setProperty("sonar.cpd.xoo.minimumTokens", (Integer) null);
+
+ Collection<Block> originBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 100)
+ .build()
+ );
+
+ Collection<Block> duplicatedBlocks = singletonList(
+ new Block.Builder()
+ .setResourceId(OTHER_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(40, 55)
+ .build()
+ );
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ verify(duplicationRepository).addDuplication(
+ ORIGIN_FILE,
+ new TextBlock(30, 45),
+ OTHER_FILE_KEY,
+ new TextBlock(40, 55)
+ );
+ }
+
+ @Test
+ public void do_not_compute_more_than_one_hundred_duplications_when_too_many_duplicated_references() throws Exception {
+ Collection<Block> originBlocks = new ArrayList<>();
+ Collection<Block> duplicatedBlocks = new ArrayList<>();
+
+ Block.Builder blockBuilder = new Block.Builder()
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray("a8998353e96320ec"))
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 100);
+ originBlocks.add(blockBuilder.build());
+
+ // Generate more than 100 duplications of the same block
+ for (int i = 0; i < 110; i++) {
+ duplicatedBlocks.add(
+ blockBuilder
+ .setResourceId(randomAlphanumeric(16))
+ .build()
+ );
+ }
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly(
+ "Too many duplication references on file " + ORIGIN_FILE_KEY + " for block at line 30. Keeping only the first 100 references.");
+ verify(duplicationRepository, times(100)).addDuplication(eq(ORIGIN_FILE), any(TextBlock.class), anyString(), any(TextBlock.class));
+ }
+
+ @Test
+ public void do_not_compute_more_than_one_hundred_duplications_when_too_many_duplications() throws Exception {
+ Collection<Block> originBlocks = new ArrayList<>();
+ Collection<Block> duplicatedBlocks = new ArrayList<>();
+
+ Block.Builder blockBuilder = new Block.Builder()
+ .setIndexInFile(0)
+ .setLines(30, 45)
+ .setUnit(0, 100);
+
+ // Generate more than 100 duplication on different files
+ for (int i = 0; i < 110; i++) {
+ String hash = randomAlphanumeric(16);
+ originBlocks.add(
+ blockBuilder
+ .setResourceId(ORIGIN_FILE_KEY)
+ .setBlockHash(new ByteArray(hash))
+ .build());
+ duplicatedBlocks.add(
+ blockBuilder
+ .setResourceId(randomAlphanumeric(16))
+ .setBlockHash(new ByteArray(hash))
+ .build()
+ );
+ }
+
+ underTest.computeCpd(ORIGIN_FILE, originBlocks, duplicatedBlocks);
+
+ verify(duplicationRepository, times(100)).addDuplication(eq(ORIGIN_FILE), any(TextBlock.class), anyString(), any(TextBlock.class));
+ assertThat(logTester.logs(LoggerLevel.WARN)).containsOnly("Too many duplication groups on file " + ORIGIN_FILE_KEY + ". Keeping only the first 100 groups.");
+ }
+
+}
--- /dev/null
+/*
+ * 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 java.util.Arrays;
+import java.util.Collections;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.protocol.output.BatchReport;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.component.SnapshotTesting;
+import org.sonar.db.duplication.DuplicationUnitDto;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+import org.sonar.server.computation.analysis.AnalysisMetadataHolderRule;
+import org.sonar.server.computation.batch.BatchReportReaderRule;
+import org.sonar.server.computation.batch.TreeRootHolderRule;
+import org.sonar.server.computation.component.Component;
+import org.sonar.server.computation.component.FileAttributes;
+import org.sonar.server.computation.component.ReportComponent;
+import org.sonar.server.computation.duplication.CrossProjectDuplicationStatusHolder;
+import org.sonar.server.computation.duplication.IntegrateCrossProjectDuplications;
+import org.sonar.server.computation.snapshot.Snapshot;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.singletonList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.computation.component.Component.Type.FILE;
+import static org.sonar.server.computation.component.Component.Type.PROJECT;
+
+public class LoadCrossProjectDuplicationsRepositoryStepTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ static final String XOO_LANGUAGE = "xoo";
+
+ static final int PROJECT_REF = 1;
+ static final int FILE_REF = 2;
+ static final String CURRENT_FILE_KEY = "FILE_KEY";
+
+ static final Component CURRENT_FILE = ReportComponent.builder(FILE, FILE_REF)
+ .setKey(CURRENT_FILE_KEY)
+ .setFileAttributes(new FileAttributes(false, XOO_LANGUAGE))
+ .build();
+
+ @Rule
+ public TreeRootHolderRule treeRootHolder = new TreeRootHolderRule().setRoot(
+ ReportComponent.builder(PROJECT, PROJECT_REF)
+ .addChildren(CURRENT_FILE
+ ).build());
+
+ @Rule
+ public BatchReportReaderRule batchReportReader = new BatchReportReaderRule();
+
+ @Rule
+ public AnalysisMetadataHolderRule analysisMetadataHolder = new AnalysisMetadataHolderRule();
+
+ CrossProjectDuplicationStatusHolder crossProjectDuplicationStatusHolder = mock(CrossProjectDuplicationStatusHolder.class);
+
+ @Rule
+ public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+ DbClient dbClient = dbTester.getDbClient();
+
+ DbSession dbSession = dbTester.getSession();
+
+ IntegrateCrossProjectDuplications integrateCrossProjectDuplications = mock(IntegrateCrossProjectDuplications.class);
+
+ Snapshot baseProjectSnapshot;
+
+ ComputationStep underTest = new LoadCrossProjectDuplicationsRepositoryStep(treeRootHolder, batchReportReader, analysisMetadataHolder, crossProjectDuplicationStatusHolder,
+ integrateCrossProjectDuplications, dbClient);
+
+ @Before
+ public void setUp() throws Exception {
+ ComponentDto project = ComponentTesting.newProjectDto();
+ dbClient.componentDao().insert(dbSession, project);
+ SnapshotDto projectSnapshot = SnapshotTesting.newSnapshotForProject(project);
+ dbClient.snapshotDao().insert(dbSession, projectSnapshot);
+ dbSession.commit();
+
+ baseProjectSnapshot = new Snapshot.Builder()
+ .setId(projectSnapshot.getId())
+ .setCreatedAt(projectSnapshot.getCreatedAt())
+ .build();
+ }
+
+ @Test
+ public void call_compute_cpd() throws Exception {
+ when(crossProjectDuplicationStatusHolder.isEnabled()).thenReturn(true);
+ analysisMetadataHolder.setBaseProjectSnapshot(baseProjectSnapshot);
+
+ ComponentDto otherProject = createProject("OTHER_PROJECT_KEY");
+ SnapshotDto otherProjectSnapshot = createProjectSnapshot(otherProject);
+
+ ComponentDto otherFIle = createFile("OTHER_FILE_KEY", otherProject);
+ SnapshotDto otherFileSnapshot = createFileSnapshot(otherFIle, otherProjectSnapshot);
+
+ String hash = "a8998353e96320ec";
+ DuplicationUnitDto duplicate = new DuplicationUnitDto()
+ .setHash(hash)
+ .setStartLine(40)
+ .setEndLine(55)
+ .setIndexInFile(0)
+ .setProjectSnapshotId(otherProjectSnapshot.getId())
+ .setSnapshotId(otherFileSnapshot.getId());
+ dbClient.duplicationDao().insert(dbSession, singletonList(duplicate));
+ dbSession.commit();
+
+ BatchReport.CpdTextBlock originBlock = BatchReport.CpdTextBlock.newBuilder()
+ .setHash(hash)
+ .setStartLine(30)
+ .setEndLine(45)
+ .setStartTokenIndex(0)
+ .setEndTokenIndex(10)
+ .build();
+ batchReportReader.putDuplicationBlocks(FILE_REF, asList(originBlock));
+
+ underTest.execute();
+
+ verify(integrateCrossProjectDuplications).computeCpd(CURRENT_FILE,
+ Arrays.asList(
+ new Block.Builder()
+ .setResourceId(CURRENT_FILE_KEY)
+ .setBlockHash(new ByteArray(hash))
+ .setIndexInFile(0)
+ .setLines(originBlock.getStartLine(), originBlock.getEndLine())
+ .setUnit(originBlock.getStartTokenIndex(), originBlock.getEndTokenIndex())
+ .build()
+ ),
+ Arrays.asList(
+ new Block.Builder()
+ .setResourceId(otherFIle.getKey())
+ .setBlockHash(new ByteArray(hash))
+ .setIndexInFile(duplicate.getIndexInFile())
+ .setLines(duplicate.getStartLine(), duplicate.getEndLine())
+ .build()
+ )
+ );
+ }
+
+ @Test
+ public void nothing_to_do_when_cross_project_duplication_is_disabled() throws Exception {
+ when(crossProjectDuplicationStatusHolder.isEnabled()).thenReturn(false);
+ analysisMetadataHolder.setBaseProjectSnapshot(baseProjectSnapshot);
+
+ ComponentDto otherProject = createProject("OTHER_PROJECT_KEY");
+ SnapshotDto otherProjectSnapshot = createProjectSnapshot(otherProject);
+
+ ComponentDto otherFIle = createFile("OTHER_FILE_KEY", otherProject);
+ SnapshotDto otherFileSnapshot = createFileSnapshot(otherFIle, otherProjectSnapshot);
+
+ String hash = "a8998353e96320ec";
+ DuplicationUnitDto duplicate = new DuplicationUnitDto()
+ .setHash(hash)
+ .setStartLine(40)
+ .setEndLine(55)
+ .setIndexInFile(0)
+ .setProjectSnapshotId(otherProjectSnapshot.getId())
+ .setSnapshotId(otherFileSnapshot.getId());
+ dbClient.duplicationDao().insert(dbSession, singletonList(duplicate));
+ dbSession.commit();
+
+ BatchReport.CpdTextBlock originBlock = BatchReport.CpdTextBlock.newBuilder()
+ .setHash(hash)
+ .setStartLine(30)
+ .setEndLine(45)
+ .setStartTokenIndex(0)
+ .setEndTokenIndex(10)
+ .build();
+ batchReportReader.putDuplicationBlocks(FILE_REF, asList(originBlock));
+
+ underTest.execute();
+
+ verifyZeroInteractions(integrateCrossProjectDuplications);
+ }
+
+ @Test
+ public void nothing_to_do_when_no_cpd_text_blocks_found() throws Exception {
+ analysisMetadataHolder
+ .setCrossProjectDuplicationEnabled(true)
+ .setBranch(null)
+ .setBaseProjectSnapshot(baseProjectSnapshot);
+
+ batchReportReader.putDuplicationBlocks(FILE_REF, Collections.<BatchReport.CpdTextBlock>emptyList());
+
+ underTest.execute();
+
+ verifyZeroInteractions(integrateCrossProjectDuplications);
+ }
+
+ private ComponentDto createProject(String projectKey) {
+ ComponentDto project = ComponentTesting.newProjectDto().setKey(projectKey);
+ dbClient.componentDao().insert(dbSession, project);
+ dbSession.commit();
+ return project;
+ }
+
+ private SnapshotDto createProjectSnapshot(ComponentDto project) {
+ SnapshotDto projectSnapshot = SnapshotTesting.newSnapshotForProject(project);
+ dbClient.snapshotDao().insert(dbSession, projectSnapshot);
+ dbSession.commit();
+ return projectSnapshot;
+ }
+
+ private ComponentDto createFile(String fileKey, ComponentDto project) {
+ ComponentDto file = ComponentTesting.newFileDto(project)
+ .setKey(fileKey)
+ .setLanguage(XOO_LANGUAGE);
+ dbClient.componentDao().insert(dbSession, file);
+ dbSession.commit();
+ return file;
+ }
+
+ private SnapshotDto createFileSnapshot(ComponentDto file, SnapshotDto projectSnapshot) {
+ SnapshotDto fileSnapshot = SnapshotTesting.createForComponent(file, projectSnapshot);
+ dbClient.snapshotDao().insert(dbSession, fileSnapshot);
+ dbSession.commit();
+ return fileSnapshot;
+ }
+
+}