aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-batch
diff options
context:
space:
mode:
authorJulien HENRY <julien.henry@sonarsource.com>2015-02-16 11:28:21 +0100
committerJulien HENRY <julien.henry@sonarsource.com>2015-02-16 18:32:02 +0100
commit2660b61c7d0c0aee191ab719bf672f7902e78c5e (patch)
tree3fc4ecdca84ae66e2d4d623e1840f32e99b5e7d1 /sonar-batch
parent1230b06fa26f9cc032c15d39f3563867d9d2914b (diff)
downloadsonarqube-2660b61c7d0c0aee191ab719bf672f7902e78c5e.tar.gz
sonarqube-2660b61c7d0c0aee191ab719bf672f7902e78c5e.zip
SONAR-6000 Merge cpd core plugin into the batch
Diffstat (limited to 'sonar-batch')
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java3
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java78
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java49
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java51
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java100
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java193
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java257
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java92
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java55
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java25
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java137
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java90
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java83
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java25
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java25
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java32
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java91
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java110
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java166
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java82
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java71
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java86
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java173
23 files changed, 2074 insertions, 0 deletions
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java
index ea8dac835fb..31c78a9d8b7 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java
@@ -20,6 +20,7 @@
package org.sonar.batch.bootstrap;
import com.google.common.collect.Lists;
+import org.sonar.batch.cpd.CpdComponents;
import org.sonar.batch.design.DirectoryDsmDecorator;
import org.sonar.batch.design.DirectoryTangleIndexDecorator;
import org.sonar.batch.design.FileTangleIndexDecorator;
@@ -89,6 +90,8 @@ public class BatchComponents {
DefaultPurgeTask.class
);
components.addAll(CorePropertyDefinitions.all());
+ // CPD
+ components.addAll(CpdComponents.all());
if (!analysisMode.isMediumTest()) {
components.add(MavenDependenciesSensor.class);
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java
new file mode 100644
index 00000000000..6032e60b9b6
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdComponents.java
@@ -0,0 +1,78 @@
+/*
+ * 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.batch.cpd;
+
+import com.google.common.collect.ImmutableList;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.PropertyType;
+import org.sonar.api.config.PropertyDefinition;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.batch.cpd.decorators.DuplicationDensityDecorator;
+import org.sonar.batch.cpd.decorators.SumDuplicationsDecorator;
+import org.sonar.batch.cpd.index.IndexFactory;
+
+import java.util.List;
+
+public final class CpdComponents {
+
+ public static List all() {
+ return ImmutableList.of(
+ PropertyDefinition.builder(CoreProperties.CPD_CROSS_PROJECT)
+ .defaultValue(CoreProperties.CPD_CROSS_RPOJECT_DEFAULT_VALUE + "")
+ .name("Cross project duplication detection")
+ .description("By default, SonarQube detects duplications at sub-project level. This means that a block "
+ + "duplicated on two sub-projects of the same project won't be reported. Setting this parameter to \"true\" "
+ + "allows to detect duplicates across sub-projects and more generally across projects. Note that activating "
+ + "this property will slightly increase each SonarQube analysis time.")
+ .onQualifiers(Qualifiers.PROJECT, Qualifiers.MODULE)
+ .category(CoreProperties.CATEGORY_GENERAL)
+ .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS)
+ .type(PropertyType.BOOLEAN)
+ .build(),
+ PropertyDefinition.builder(CoreProperties.CPD_SKIP_PROPERTY)
+ .defaultValue("false")
+ .name("Skip")
+ .description("Disable detection of duplications")
+ .hidden()
+ .category(CoreProperties.CATEGORY_GENERAL)
+ .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS)
+ .type(PropertyType.BOOLEAN)
+ .build(),
+ PropertyDefinition.builder(CoreProperties.CPD_EXCLUSIONS)
+ .defaultValue("")
+ .name("Duplication Exclusions")
+ .description("Patterns used to exclude some source files from the duplication detection mechanism. " +
+ "See below to know how to use wildcards to specify this property.")
+ .onQualifiers(Qualifiers.PROJECT, Qualifiers.MODULE)
+ .category(CoreProperties.CATEGORY_EXCLUSIONS)
+ .subCategory(CoreProperties.SUBCATEGORY_DUPLICATIONS_EXCLUSIONS)
+ .multiValues(true)
+ .build(),
+
+ CpdSensor.class,
+ CpdMappings.class,
+ SumDuplicationsDecorator.class,
+ DuplicationDensityDecorator.class,
+ IndexFactory.class,
+ JavaCpdEngine.class,
+ DefaultCpdEngine.class);
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java
new file mode 100644
index 00000000000..45964262241
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdEngine.java
@@ -0,0 +1,49 @@
+/*
+ * 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.batch.cpd;
+
+import org.slf4j.Logger;
+import org.sonar.api.BatchExtension;
+import org.sonar.api.batch.sensor.SensorContext;
+
+public abstract class CpdEngine implements BatchExtension {
+
+ abstract boolean isLanguageSupported(String language);
+
+ abstract void analyse(String language, SensorContext context);
+
+ protected void logExclusions(String[] exclusions, Logger logger) {
+ if (exclusions.length > 0) {
+ StringBuilder message = new StringBuilder("Copy-paste detection exclusions:");
+ for (String exclusion : exclusions) {
+ message.append("\n ");
+ message.append(exclusion);
+ }
+
+ logger.info(message.toString());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java
new file mode 100644
index 00000000000..ff4b7a81beb
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdMappings.java
@@ -0,0 +1,51 @@
+/*
+ * 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.batch.cpd;
+
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.CpdMapping;
+
+import javax.annotation.CheckForNull;
+
+public class CpdMappings implements BatchComponent {
+
+ private final CpdMapping[] mappings;
+
+ public CpdMappings(CpdMapping[] mappings) {
+ this.mappings = mappings;
+ }
+
+ public CpdMappings() {
+ this(new CpdMapping[0]);
+ }
+
+ @CheckForNull
+ public CpdMapping getMapping(String language) {
+ if (mappings != null) {
+ for (CpdMapping cpdMapping : mappings) {
+ if (cpdMapping.getLanguage().getKey().equals(language)) {
+ return cpdMapping;
+ }
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java
new file mode 100644
index 00000000000..d4ba5397783
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/CpdSensor.java
@@ -0,0 +1,100 @@
+/*
+ * 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.batch.cpd;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.Phase;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.sensor.Sensor;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorDescriptor;
+import org.sonar.api.config.Settings;
+
+@Phase(name = Phase.Name.POST)
+public class CpdSensor implements Sensor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(CpdSensor.class);
+
+ private CpdEngine sonarEngine;
+ private CpdEngine sonarBridgeEngine;
+ private Settings settings;
+ private FileSystem fs;
+
+ public CpdSensor(JavaCpdEngine sonarEngine, DefaultCpdEngine sonarBridgeEngine, Settings settings, FileSystem fs) {
+ this.sonarEngine = sonarEngine;
+ this.sonarBridgeEngine = sonarBridgeEngine;
+ this.settings = settings;
+ this.fs = fs;
+ }
+
+ @Override
+ public void describe(SensorDescriptor descriptor) {
+ descriptor.name("CPD Sensor")
+ .disabledInPreview();
+ }
+
+ @VisibleForTesting
+ CpdEngine getEngine(String language) {
+ if (sonarEngine.isLanguageSupported(language)) {
+ return sonarEngine;
+ }
+ return sonarBridgeEngine;
+ }
+
+ @VisibleForTesting
+ boolean isSkipped(String language) {
+ String key = "sonar.cpd." + language + ".skip";
+ if (settings.hasKey(key)) {
+ return settings.getBoolean(key);
+ }
+ return settings.getBoolean(CoreProperties.CPD_SKIP_PROPERTY);
+ }
+
+ @Override
+ public void execute(SensorContext context) {
+ if (settings.hasKey(CoreProperties.CPD_SKIP_PROPERTY)) {
+ LOG.warn("\"sonar.cpd.skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism.");
+ }
+
+ for (String language : fs.languages()) {
+ if (settings.hasKey("sonar.cpd." + language + ".skip")) {
+ LOG
+ .warn("\"sonar.cpd." + language + ".skip\" property is deprecated and will be removed. Please set \"sonar.cpd.exclusions=**\" instead to disable duplication mechanism.");
+ }
+
+ if (isSkipped(language)) {
+ LOG.info("Detection of duplicated code is skipped for {}", language);
+ continue;
+ }
+
+ CpdEngine engine = getEngine(language);
+ if (!engine.isLanguageSupported(language)) {
+ LOG.debug("Detection of duplicated code is not supported for {}", language);
+ continue;
+ }
+ LOG.info("{} is used for {}", engine, language);
+ engine.analyse(language, context);
+ }
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java
new file mode 100644
index 00000000000..6f95c3e47f3
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/DefaultCpdEngine.java
@@ -0,0 +1,193 @@
+/*
+ * 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.batch.cpd;
+
+import org.sonar.batch.cpd.index.IndexFactory;
+import org.sonar.batch.cpd.index.SonarDuplicationsIndex;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.CpdMapping;
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.SonarException;
+import org.sonar.batch.duplication.BlockCache;
+import org.sonar.duplications.DuplicationPredicates;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.FileBlocks;
+import org.sonar.duplications.index.CloneGroup;
+import org.sonar.duplications.internal.pmd.TokenizerBridge;
+
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+public class DefaultCpdEngine extends CpdEngine {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DefaultCpdEngine.class);
+
+ /**
+ * Limit of time to analyse one file (in seconds).
+ */
+ private static final int TIMEOUT = 5 * 60;
+
+ private final IndexFactory indexFactory;
+ private final CpdMappings mappings;
+ private final FileSystem fs;
+ private final Settings settings;
+ private final BlockCache blockCache;
+ private final Project project;
+
+ public DefaultCpdEngine(@Nullable Project project, IndexFactory indexFactory, CpdMappings mappings, FileSystem fs, Settings settings, BlockCache duplicationCache) {
+ this.project = project;
+ this.indexFactory = indexFactory;
+ this.mappings = mappings;
+ this.fs = fs;
+ this.settings = settings;
+ this.blockCache = duplicationCache;
+ }
+
+ public DefaultCpdEngine(IndexFactory indexFactory, CpdMappings mappings, FileSystem fs, Settings settings, BlockCache duplicationCache) {
+ this(null, indexFactory, mappings, fs, settings, duplicationCache);
+ }
+
+ @Override
+ public boolean isLanguageSupported(String language) {
+ return true;
+ }
+
+ @Override
+ public void analyse(String languageKey, SensorContext context) {
+ String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS);
+ logExclusions(cpdExclusions, LOG);
+ FilePredicates p = fs.predicates();
+ List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and(
+ p.hasType(InputFile.Type.MAIN),
+ p.hasLanguage(languageKey),
+ p.doesNotMatchPathPatterns(cpdExclusions)
+ )));
+ if (sourceFiles.isEmpty()) {
+ return;
+ }
+
+ CpdMapping mapping = mappings.getMapping(languageKey);
+
+ // Create index
+ SonarDuplicationsIndex index = indexFactory.create(project, languageKey);
+ populateIndex(languageKey, sourceFiles, mapping, index);
+
+ // Detect
+ Predicate<CloneGroup> minimumTokensPredicate = DuplicationPredicates.numberOfUnitsNotLessThan(getMinimumTokens(languageKey));
+
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ for (InputFile inputFile : sourceFiles) {
+ LOG.debug("Detection of duplications for {}", inputFile);
+ String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key();
+ Collection<Block> fileBlocks = index.getByInputFile(inputFile, resourceEffectiveKey);
+
+ Iterable<CloneGroup> filtered;
+ try {
+ List<CloneGroup> duplications = executorService.submit(new JavaCpdEngine.Task(index, fileBlocks)).get(TIMEOUT, TimeUnit.SECONDS);
+ filtered = Iterables.filter(duplications, minimumTokensPredicate);
+ } catch (TimeoutException e) {
+ filtered = null;
+ LOG.warn("Timeout during detection of duplications for " + inputFile, e);
+ } catch (InterruptedException e) {
+ throw new SonarException("Fail during detection of duplication for " + inputFile, e);
+ } catch (ExecutionException e) {
+ throw new SonarException("Fail during detection of duplication for " + inputFile, e);
+ }
+
+ JavaCpdEngine.save(context, inputFile, filtered);
+ }
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ private void populateIndex(String languageKey, List<InputFile> sourceFiles, CpdMapping mapping, SonarDuplicationsIndex index) {
+ TokenizerBridge bridge = null;
+ if (mapping != null) {
+ bridge = new TokenizerBridge(mapping.getTokenizer(), fs.encoding().name(), getBlockSize(languageKey));
+ }
+ for (InputFile inputFile : sourceFiles) {
+ LOG.debug("Populating index from {}", inputFile);
+ String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key();
+ FileBlocks fileBlocks = blockCache.byComponent(resourceEffectiveKey);
+ if (fileBlocks != null) {
+ index.insert(inputFile, fileBlocks.blocks());
+ } else if (bridge != null) {
+ List<Block> blocks2 = bridge.chunk(resourceEffectiveKey, inputFile.file());
+ index.insert(inputFile, blocks2);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ int getBlockSize(String languageKey) {
+ int blockSize = settings.getInt("sonar.cpd." + languageKey + ".minimumLines");
+ if (blockSize == 0) {
+ blockSize = getDefaultBlockSize(languageKey);
+ }
+ return blockSize;
+ }
+
+ @VisibleForTesting
+ static int getDefaultBlockSize(String languageKey) {
+ if ("cobol".equals(languageKey)) {
+ return 30;
+ } else if ("abap".equals(languageKey) || "natur".equals(languageKey)) {
+ return 20;
+ } else {
+ return 10;
+ }
+ }
+
+ @VisibleForTesting
+ int getMinimumTokens(String languageKey) {
+ int minimumTokens = settings.getInt("sonar.cpd." + languageKey + ".minimumTokens");
+ if (minimumTokens == 0) {
+ minimumTokens = settings.getInt(CoreProperties.CPD_MINIMUM_TOKENS_PROPERTY);
+ }
+ if (minimumTokens == 0) {
+ minimumTokens = 100;
+ }
+
+ return minimumTokens;
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java
new file mode 100644
index 00000000000..37bae21c422
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/JavaCpdEngine.java
@@ -0,0 +1,257 @@
+/*
+ * 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.batch.cpd;
+
+import org.sonar.batch.cpd.index.IndexFactory;
+import org.sonar.batch.cpd.index.SonarDuplicationsIndex;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.batch.fs.FilePredicates;
+import org.sonar.api.batch.fs.FileSystem;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.duplication.DuplicationBuilder;
+import org.sonar.api.batch.sensor.duplication.internal.DefaultDuplicationBuilder;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.config.Settings;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.resources.Project;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.api.utils.SonarException;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.BlockChunker;
+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.java.JavaStatementBuilder;
+import org.sonar.duplications.java.JavaTokenProducer;
+import org.sonar.duplications.statement.Statement;
+import org.sonar.duplications.statement.StatementChunker;
+import org.sonar.duplications.token.TokenChunker;
+
+import javax.annotation.Nullable;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.*;
+
+public class JavaCpdEngine extends CpdEngine {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JavaCpdEngine.class);
+
+ private static final int BLOCK_SIZE = 10;
+
+ /**
+ * Limit of time to analyse one file (in seconds).
+ */
+ private static final int TIMEOUT = 5 * 60;
+
+ private final IndexFactory indexFactory;
+ private final FileSystem fs;
+ private final Settings settings;
+ private final Project project;
+
+ public JavaCpdEngine(@Nullable Project project, IndexFactory indexFactory, FileSystem fs, Settings settings) {
+ this.project = project;
+ this.indexFactory = indexFactory;
+ this.fs = fs;
+ this.settings = settings;
+ }
+
+ public JavaCpdEngine(IndexFactory indexFactory, FileSystem fs, Settings settings) {
+ this(null, indexFactory, fs, settings);
+ }
+
+ @Override
+ public boolean isLanguageSupported(String language) {
+ return "java".equals(language);
+ }
+
+ @Override
+ public void analyse(String languageKey, SensorContext context) {
+ String[] cpdExclusions = settings.getStringArray(CoreProperties.CPD_EXCLUSIONS);
+ logExclusions(cpdExclusions, LOG);
+ FilePredicates p = fs.predicates();
+ List<InputFile> sourceFiles = Lists.newArrayList(fs.inputFiles(p.and(
+ p.hasType(InputFile.Type.MAIN),
+ p.hasLanguage(languageKey),
+ p.doesNotMatchPathPatterns(cpdExclusions)
+ )));
+ if (sourceFiles.isEmpty()) {
+ return;
+ }
+ SonarDuplicationsIndex index = createIndex(project, languageKey, sourceFiles);
+ detect(index, context, sourceFiles);
+ }
+
+ private SonarDuplicationsIndex createIndex(@Nullable Project project, String language, Iterable<InputFile> sourceFiles) {
+ final SonarDuplicationsIndex index = indexFactory.create(project, language);
+
+ TokenChunker tokenChunker = JavaTokenProducer.build();
+ StatementChunker statementChunker = JavaStatementBuilder.build();
+ BlockChunker blockChunker = new BlockChunker(BLOCK_SIZE);
+
+ for (InputFile inputFile : sourceFiles) {
+ LOG.debug("Populating index from {}", inputFile);
+ String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key();
+
+ List<Statement> statements;
+
+ Reader reader = null;
+ try {
+ reader = new InputStreamReader(new FileInputStream(inputFile.file()), fs.encoding());
+ statements = statementChunker.chunk(tokenChunker.chunk(reader));
+ } catch (FileNotFoundException e) {
+ throw new SonarException("Cannot find file " + inputFile.file(), e);
+ } finally {
+ IOUtils.closeQuietly(reader);
+ }
+
+ List<Block> blocks = blockChunker.chunk(resourceEffectiveKey, statements);
+ index.insert(inputFile, blocks);
+ }
+
+ return index;
+ }
+
+ private void detect(SonarDuplicationsIndex index, org.sonar.api.batch.sensor.SensorContext context, List<InputFile> sourceFiles) {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ for (InputFile inputFile : sourceFiles) {
+ LOG.debug("Detection of duplications for {}", inputFile);
+ String resourceEffectiveKey = ((DeprecatedDefaultInputFile) inputFile).key();
+
+ Collection<Block> fileBlocks = index.getByInputFile(inputFile, resourceEffectiveKey);
+
+ List<CloneGroup> clones;
+ try {
+ clones = executorService.submit(new Task(index, fileBlocks)).get(TIMEOUT, TimeUnit.SECONDS);
+ } catch (TimeoutException e) {
+ clones = null;
+ LOG.warn("Timeout during detection of duplications for " + inputFile, e);
+ } catch (InterruptedException e) {
+ throw new SonarException("Fail during detection of duplication for " + inputFile, e);
+ } catch (ExecutionException e) {
+ throw new SonarException("Fail during detection of duplication for " + inputFile, e);
+ }
+
+ save(context, inputFile, clones);
+ }
+ } finally {
+ executorService.shutdown();
+ }
+ }
+
+ static class Task implements Callable<List<CloneGroup>> {
+ private final CloneIndex index;
+ private final Collection<Block> fileBlocks;
+
+ public Task(CloneIndex index, Collection<Block> fileBlocks) {
+ this.index = index;
+ this.fileBlocks = fileBlocks;
+ }
+
+ @Override
+ public List<CloneGroup> call() {
+ return SuffixTreeCloneDetectionAlgorithm.detect(index, fileBlocks);
+ }
+ }
+
+ static void save(org.sonar.api.batch.sensor.SensorContext context, InputFile inputFile, @Nullable Iterable<CloneGroup> duplications) {
+ if (duplications == null || Iterables.isEmpty(duplications)) {
+ return;
+ }
+ Set<Integer> duplicatedLines = new HashSet<Integer>();
+ int duplicatedBlocks = computeBlockAndLineCount(duplications, duplicatedLines);
+ Map<Integer, Integer> duplicationByLine = new HashMap<Integer, Integer>();
+ for (int i = 1; i <= inputFile.lines(); i++) {
+ duplicationByLine.put(i, duplicatedLines.contains(i) ? 1 : 0);
+ }
+ ((DefaultMeasure<String>) context.<String>newMeasure()
+ .forMetric(CoreMetrics.DUPLICATION_LINES_DATA)
+ .onFile(inputFile)
+ .withValue(KeyValueFormat.format(duplicationByLine)))
+ .setFromCore()
+ .save();
+ // Save
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .forMetric(CoreMetrics.DUPLICATED_FILES)
+ .onFile(inputFile)
+ .withValue(1))
+ .setFromCore()
+ .save();
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .forMetric(CoreMetrics.DUPLICATED_LINES)
+ .onFile(inputFile)
+ .withValue(duplicatedLines.size()))
+ .setFromCore()
+ .save();
+ ((DefaultMeasure<Integer>) context.<Integer>newMeasure()
+ .forMetric(CoreMetrics.DUPLICATED_BLOCKS)
+ .onFile(inputFile)
+ .withValue(duplicatedBlocks))
+ .setFromCore()
+ .save();
+
+ DuplicationBuilder builder = context.duplicationBuilder(inputFile);
+ for (CloneGroup duplication : duplications) {
+ builder.originBlock(duplication.getOriginPart().getStartLine(), duplication.getOriginPart().getEndLine());
+ for (ClonePart part : duplication.getCloneParts()) {
+ if (!part.equals(duplication.getOriginPart())) {
+ ((DefaultDuplicationBuilder) builder).isDuplicatedBy(part.getResourceId(), part.getStartLine(), part.getEndLine());
+ }
+ }
+ }
+ context.saveDuplications(inputFile, builder.build());
+ }
+
+ private static int computeBlockAndLineCount(Iterable<CloneGroup> duplications, Set<Integer> duplicatedLines) {
+ int duplicatedBlocks = 0;
+ for (CloneGroup clone : duplications) {
+ ClonePart origin = clone.getOriginPart();
+ for (ClonePart part : clone.getCloneParts()) {
+ if (part.getResourceId().equals(origin.getResourceId())) {
+ duplicatedBlocks++;
+ for (int duplicatedLine = part.getStartLine(); duplicatedLine < part.getStartLine() + part.getLines(); duplicatedLine++) {
+ duplicatedLines.add(duplicatedLine);
+ }
+ }
+ }
+ }
+ return duplicatedBlocks;
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java
new file mode 100644
index 00000000000..d122eeb3b2b
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecorator.java
@@ -0,0 +1,92 @@
+/*
+ * 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.batch.cpd.decorators;
+
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class DuplicationDensityDecorator implements Decorator {
+
+ @DependsUpon
+ public List<Metric> dependsUponMetrics() {
+ return Arrays.<Metric>asList(
+ CoreMetrics.NCLOC,
+ CoreMetrics.COMMENT_LINES,
+ CoreMetrics.DUPLICATED_LINES,
+ CoreMetrics.LINES);
+ }
+
+ @DependedUpon
+ public Metric generatesMetric() {
+ return CoreMetrics.DUPLICATED_LINES_DENSITY;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+
+ @Override
+ public void decorate(Resource resource, DecoratorContext context) {
+ Measure nbDuplicatedLines = context.getMeasure(CoreMetrics.DUPLICATED_LINES);
+ if (nbDuplicatedLines == null) {
+ return;
+ }
+
+ Double divisor = getNbLinesFromLocOrNcloc(context);
+ if (divisor != null && divisor > 0.0) {
+ context.saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, calculate(nbDuplicatedLines.getValue(), divisor));
+ }
+ }
+
+ private Double getNbLinesFromLocOrNcloc(DecoratorContext context) {
+ Measure nbLoc = context.getMeasure(CoreMetrics.LINES);
+ if (nbLoc != null) {
+ // TODO test this branch
+ return nbLoc.getValue();
+ }
+ Measure nbNcloc = context.getMeasure(CoreMetrics.NCLOC);
+ if (nbNcloc != null) {
+ Measure nbComments = context.getMeasure(CoreMetrics.COMMENT_LINES);
+ Double nbLines = nbNcloc.getValue();
+ return nbComments != null ? nbLines + nbComments.getValue() : nbLines;
+ }
+ return null;
+ }
+
+ protected Double calculate(Double dividend, Double divisor) {
+ Double result = 100.0 * dividend / divisor;
+ if (result < 100.0) {
+ return result;
+ }
+ return 100.0;
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.java
new file mode 100644
index 00000000000..af7f4b79e60
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecorator.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.batch.cpd.decorators;
+
+import org.sonar.api.batch.AbstractSumChildrenDecorator;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class SumDuplicationsDecorator extends AbstractSumChildrenDecorator {
+
+ @Override
+ @DependedUpon
+ public List<Metric> generatesMetrics() {
+ return Arrays.<Metric>asList(CoreMetrics.DUPLICATED_BLOCKS, CoreMetrics.DUPLICATED_FILES, CoreMetrics.DUPLICATED_LINES);
+ }
+
+ @Override
+ protected boolean shouldSaveZeroIfNoChildMeasures() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+
+ @Override
+ public boolean shouldDecorateResource(Resource resource) {
+ return !ResourceUtils.isUnitTestClass(resource);
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java
new file mode 100644
index 00000000000..51735f6c8dc
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/decorators/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.batch.cpd.decorators;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java
new file mode 100644
index 00000000000..4318e0fa8ad
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/DbDuplicationsIndex.java
@@ -0,0 +1,137 @@
+/*
+ * 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.batch.cpd.index;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.database.DatabaseSession;
+import org.sonar.api.database.model.Snapshot;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.index.ResourceCache;
+import org.sonar.core.duplication.DuplicationDao;
+import org.sonar.core.duplication.DuplicationUnitDto;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+
+import javax.persistence.Query;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class DbDuplicationsIndex {
+
+ private static final String RESOURCE_ID = "resourceId";
+ private static final String LAST = "last";
+
+ private final Map<ByteArray, Collection<Block>> cache = Maps.newHashMap();
+
+ private final int currentProjectSnapshotId;
+ private final Integer lastSnapshotId;
+ private final String languageKey;
+ private final DuplicationDao dao;
+ private final DatabaseSession session;
+ private final ResourceCache resourceCache;
+
+ public DbDuplicationsIndex(Project currentProject, DuplicationDao dao,
+ String language, DatabaseSession session, ResourceCache resourceCache) {
+ this.dao = dao;
+ this.session = session;
+ this.resourceCache = resourceCache;
+ Snapshot lastSnapshot = getLastSnapshot(currentProject.getId());
+ this.currentProjectSnapshotId = resourceCache.get(currentProject.getEffectiveKey()).snapshotId();
+ this.lastSnapshotId = lastSnapshot == null ? null : lastSnapshot.getId();
+ this.languageKey = language;
+ }
+
+ private Snapshot getLastSnapshot(int resourceId) {
+ String hql = "SELECT s FROM " + Snapshot.class.getSimpleName() + " s WHERE s.last=:last AND s.resourceId=:resourceId";
+ Query query = session.createQuery(hql);
+ query.setParameter(LAST, true);
+ query.setParameter(RESOURCE_ID, resourceId);
+ return session.getSingleResult(query, null);
+ }
+
+ int getSnapshotIdFor(InputFile inputFile) {
+ return resourceCache.get(((DefaultInputFile) inputFile).key()).snapshotId();
+ }
+
+ public void prepareCache(InputFile inputFile) {
+ int resourceSnapshotId = getSnapshotIdFor(inputFile);
+ List<DuplicationUnitDto> units = dao.selectCandidates(resourceSnapshotId, lastSnapshotId, languageKey);
+ cache.clear();
+ // TODO Godin: maybe remove conversion of units to blocks?
+ for (DuplicationUnitDto unit : units) {
+ String hash = unit.getHash();
+ String resourceKey = unit.getResourceKey();
+ int indexInFile = unit.getIndexInFile();
+ int startLine = unit.getStartLine();
+ int endLine = unit.getEndLine();
+
+ // TODO Godin: in fact we could work directly with id instead of key - this will allow to decrease memory consumption
+ Block block = Block.builder()
+ .setResourceId(resourceKey)
+ .setBlockHash(new ByteArray(hash))
+ .setIndexInFile(indexInFile)
+ .setLines(startLine, endLine)
+ .build();
+
+ // Group blocks by hash
+ Collection<Block> sameHash = cache.get(block.getBlockHash());
+ if (sameHash == null) {
+ sameHash = Lists.newArrayList();
+ cache.put(block.getBlockHash(), sameHash);
+ }
+ sameHash.add(block);
+ }
+ }
+
+ public Collection<Block> getByHash(ByteArray hash) {
+ Collection<Block> result = cache.get(hash);
+ if (result != null) {
+ return result;
+ } else {
+ return Collections.emptyList();
+ }
+ }
+
+ public void insert(InputFile inputFile, Collection<Block> blocks) {
+ int resourceSnapshotId = getSnapshotIdFor(inputFile);
+
+ // TODO Godin: maybe remove conversion of blocks to units?
+ List<DuplicationUnitDto> units = Lists.newArrayList();
+ for (Block block : blocks) {
+ DuplicationUnitDto unit = new DuplicationUnitDto(
+ currentProjectSnapshotId,
+ resourceSnapshotId,
+ block.getBlockHash().toString(),
+ block.getIndexInFile(),
+ block.getStartLine(),
+ block.getEndLine());
+ units.add(unit);
+ }
+
+ dao.insert(units);
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.java
new file mode 100644
index 00000000000..a11f94a0fde
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/IndexFactory.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.batch.cpd.index;
+
+import com.google.common.annotations.VisibleForTesting;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.database.DatabaseSession;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.bootstrap.DefaultAnalysisMode;
+import org.sonar.batch.index.ResourceCache;
+import org.sonar.core.duplication.DuplicationDao;
+
+import javax.annotation.Nullable;
+
+public class IndexFactory implements BatchComponent {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IndexFactory.class);
+
+ private final Settings settings;
+ private final DuplicationDao dao;
+ private final DefaultAnalysisMode mode;
+ private final DatabaseSession session;
+ private final ResourceCache resourceCache;
+
+ public IndexFactory(DefaultAnalysisMode mode, Settings settings, @Nullable DuplicationDao dao, @Nullable DatabaseSession session, ResourceCache resourceCache) {
+ this.mode = mode;
+ this.settings = settings;
+ this.dao = dao;
+ this.session = session;
+ this.resourceCache = resourceCache;
+ }
+
+ /**
+ * Used by new sensor mode
+ */
+ public IndexFactory(DefaultAnalysisMode mode, Settings settings, ResourceCache resourceCache) {
+ this(mode, settings, null, null, resourceCache);
+ }
+
+ public SonarDuplicationsIndex create(@Nullable Project project, String languageKey) {
+ if (verifyCrossProject(project, LOG) && dao != null && session != null) {
+ return new SonarDuplicationsIndex(new DbDuplicationsIndex(project, dao, languageKey, session, resourceCache));
+ }
+ return new SonarDuplicationsIndex();
+ }
+
+ @VisibleForTesting
+ boolean verifyCrossProject(@Nullable Project project, Logger logger) {
+ boolean crossProject = false;
+
+ if (settings.getBoolean(CoreProperties.CPD_CROSS_PROJECT)) {
+ if (mode.isPreview()) {
+ logger.info("Cross-project analysis disabled. Not supported in preview mode.");
+ } else if (StringUtils.isNotBlank(settings.getString(CoreProperties.PROJECT_BRANCH_PROPERTY))) {
+ logger.info("Cross-project analysis disabled. Not supported on project branches.");
+ } else if (project == null) {
+ // New sensor mode
+ logger.info("Cross-project analysis disabled. Not supported in new sensor mode.");
+ } else {
+ logger.info("Cross-project analysis enabled");
+ crossProject = true;
+ }
+ } else {
+ logger.info("Cross-project analysis disabled");
+ }
+ return crossProject;
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java
new file mode 100644
index 00000000000..dacedc06b74
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/SonarDuplicationsIndex.java
@@ -0,0 +1,83 @@
+/*
+ * 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.batch.cpd.index;
+
+import com.google.common.collect.Lists;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.duplications.block.Block;
+import org.sonar.duplications.block.ByteArray;
+import org.sonar.duplications.index.AbstractCloneIndex;
+import org.sonar.duplications.index.CloneIndex;
+import org.sonar.duplications.index.PackedMemoryCloneIndex;
+
+import java.util.Collection;
+import java.util.List;
+
+public class SonarDuplicationsIndex extends AbstractCloneIndex {
+
+ private final CloneIndex mem = new PackedMemoryCloneIndex();
+ private final DbDuplicationsIndex db;
+
+ public SonarDuplicationsIndex() {
+ this.db = null;
+ }
+
+ public SonarDuplicationsIndex(DbDuplicationsIndex db) {
+ this.db = db;
+ }
+
+ public void insert(InputFile inputFile, Collection<Block> blocks) {
+ for (Block block : blocks) {
+ mem.insert(block);
+ }
+ if (db != null) {
+ db.insert(inputFile, blocks);
+ }
+ }
+
+ public Collection<Block> getByInputFile(InputFile inputFile, String resourceKey) {
+ if (db != null) {
+ db.prepareCache(inputFile);
+ }
+ return mem.getByResourceId(resourceKey);
+ }
+
+ @Override
+ public Collection<Block> getBySequenceHash(ByteArray hash) {
+ if (db == null) {
+ return mem.getBySequenceHash(hash);
+ } else {
+ List<Block> result = Lists.newArrayList(mem.getBySequenceHash(hash));
+ result.addAll(db.getByHash(hash));
+ return result;
+ }
+ }
+
+ @Override
+ public Collection<Block> getByResourceId(String resourceId) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void insert(Block block) {
+ throw new UnsupportedOperationException();
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java
new file mode 100644
index 00000000000..5d53db6190b
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/index/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.batch.cpd.index;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java
new file mode 100644
index 00000000000..8be46d698af
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/cpd/package-info.java
@@ -0,0 +1,25 @@
+/*
+ * 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.batch.cpd;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java
new file mode 100644
index 00000000000..90632426ee3
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdComponentsTest.java
@@ -0,0 +1,32 @@
+/*
+ * 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.batch.cpd;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CpdComponentsTest {
+
+ @Test
+ public void getExtensions() {
+ assertThat(CpdComponents.all()).hasSize(10);
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.java
new file mode 100644
index 00000000000..2c6f195b880
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/CpdSensorTest.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.batch.cpd;
+
+import org.sonar.batch.cpd.CpdMappings;
+import org.sonar.batch.cpd.CpdComponents;
+import org.sonar.batch.cpd.CpdSensor;
+import org.sonar.batch.cpd.DefaultCpdEngine;
+import org.sonar.batch.cpd.JavaCpdEngine;
+import org.sonar.batch.cpd.index.IndexFactory;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.internal.DefaultFileSystem;
+import org.sonar.api.config.PropertyDefinitions;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Java;
+import org.sonar.batch.duplication.BlockCache;
+
+import java.io.IOException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+public class CpdSensorTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ JavaCpdEngine sonarEngine;
+ DefaultCpdEngine sonarBridgeEngine;
+ CpdSensor sensor;
+ Settings settings;
+
+ @Before
+ public void setUp() throws IOException {
+ IndexFactory indexFactory = mock(IndexFactory.class);
+ sonarEngine = new JavaCpdEngine(indexFactory, null, null);
+ sonarBridgeEngine = new DefaultCpdEngine(indexFactory, new CpdMappings(), null, null, mock(BlockCache.class));
+ settings = new Settings(new PropertyDefinitions(CpdComponents.class));
+
+ DefaultFileSystem fs = new DefaultFileSystem(temp.newFolder().toPath());
+ sensor = new CpdSensor(sonarEngine, sonarBridgeEngine, settings, fs);
+ }
+
+ @Test
+ public void test_global_skip() {
+ settings.setProperty("sonar.cpd.skip", true);
+ assertThat(sensor.isSkipped(Java.KEY)).isTrue();
+ }
+
+ @Test
+ public void should_not_skip_by_default() {
+ assertThat(sensor.isSkipped(Java.KEY)).isFalse();
+ }
+
+ @Test
+ public void should_skip_by_language() {
+ settings.setProperty("sonar.cpd.skip", false);
+ settings.setProperty("sonar.cpd.php.skip", true);
+
+ assertThat(sensor.isSkipped("php")).isTrue();
+ assertThat(sensor.isSkipped(Java.KEY)).isFalse();
+ }
+
+ @Test
+ public void test_engine() {
+ assertThat(sensor.getEngine(Java.KEY)).isSameAs(sonarEngine);
+ assertThat(sensor.getEngine("PHP")).isSameAs(sonarBridgeEngine);
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java
new file mode 100644
index 00000000000..9ce86ce3ee4
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/DefaultCpdEngineTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.batch.cpd;
+
+import org.sonar.batch.cpd.DefaultCpdEngine;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.sonar.api.config.Settings;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.duplication.BlockCache;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+public class DefaultCpdEngineTest {
+
+ private DefaultCpdEngine engine;
+ private Settings settings;
+
+ @Before
+ public void init() {
+ settings = new Settings();
+ engine = new DefaultCpdEngine(null, null, null, settings, mock(BlockCache.class));
+ }
+
+ @Test
+ public void shouldLogExclusions() {
+ Logger logger = mock(Logger.class);
+ engine.logExclusions(new String[0], logger);
+ verify(logger, never()).info(anyString());
+
+ logger = mock(Logger.class);
+ engine.logExclusions(new String[] {"Foo*", "**/Bar*"}, logger);
+
+ String message = "Copy-paste detection exclusions:"
+ + "\n Foo*"
+ + "\n **/Bar*";
+ verify(logger, times(1)).info(message);
+ }
+
+ @Test
+ public void shouldReturnDefaultBlockSize() {
+ assertThat(DefaultCpdEngine.getDefaultBlockSize("cobol")).isEqualTo(30);
+ assertThat(DefaultCpdEngine.getDefaultBlockSize("natur")).isEqualTo(20);
+ assertThat(DefaultCpdEngine.getDefaultBlockSize("abap")).isEqualTo(20);
+ assertThat(DefaultCpdEngine.getDefaultBlockSize("other")).isEqualTo(10);
+ }
+
+ @Test
+ public void defaultBlockSize() {
+
+ assertThat(engine.getBlockSize("java")).isEqualTo(10);
+ }
+
+ @Test
+ public void blockSizeForCobol() {
+ settings.setProperty("sonar.cpd.cobol.minimumLines", "42");
+
+ assertThat(engine.getBlockSize("cobol")).isEqualTo(42);
+ }
+
+ @Test
+ public void defaultMinimumTokens() {
+ assertThat(engine.getMinimumTokens("java")).isEqualTo(100);
+ }
+
+ @Test
+ public void generalMinimumTokens() {
+ settings.setProperty("sonar.cpd.minimumTokens", 33);
+
+ assertThat(engine.getMinimumTokens("java")).isEqualTo(33);
+ }
+
+ @Test
+ public void minimumTokensByLanguage() {
+ settings.setProperty("sonar.cpd.java.minimumTokens", "42");
+ settings.setProperty("sonar.cpd.php.minimumTokens", "33");
+ assertThat(engine.getMinimumTokens("java")).isEqualTo(42);
+
+ settings.setProperty("sonar.cpd.java.minimumTokens", "42");
+ settings.setProperty("sonar.cpd.php.minimumTokens", "33");
+ assertThat(engine.getMinimumTokens("php")).isEqualTo(33);
+ }
+
+ private static Project newProject(String key) {
+ return new Project(key).setAnalysisType(Project.AnalysisType.DYNAMIC);
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java
new file mode 100644
index 00000000000..711f70f9be4
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/JavaCpdEngineTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.batch.cpd;
+
+import org.sonar.batch.cpd.JavaCpdEngine;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.InOrder;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DeprecatedDefaultInputFile;
+import org.sonar.api.batch.sensor.SensorContext;
+import org.sonar.api.batch.sensor.SensorStorage;
+import org.sonar.api.batch.sensor.duplication.DuplicationGroup;
+import org.sonar.api.batch.sensor.duplication.internal.DefaultDuplicationBuilder;
+import org.sonar.api.batch.sensor.measure.Measure;
+import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.duplications.index.CloneGroup;
+import org.sonar.duplications.index.ClonePart;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class JavaCpdEngineTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ SensorContext context = mock(SensorContext.class);
+ DeprecatedDefaultInputFile inputFile;
+ private DefaultDuplicationBuilder duplicationBuilder;
+ private SensorStorage storage = mock(SensorStorage.class);
+
+ @Before
+ public void before() throws IOException {
+ when(context.newMeasure()).then(new Answer<Measure>() {
+ @Override
+ public Measure answer(InvocationOnMock invocation) throws Throwable {
+ return new DefaultMeasure(storage);
+ }
+ });
+ inputFile = new DeprecatedDefaultInputFile("foo", "src/main/java/Foo.java");
+ duplicationBuilder = spy(new DefaultDuplicationBuilder(inputFile));
+ when(context.duplicationBuilder(any(InputFile.class))).thenReturn(duplicationBuilder);
+ inputFile.setModuleBaseDir(temp.newFolder().toPath());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Test
+ public void testNothingToSave() {
+ JavaCpdEngine.save(context, inputFile, null);
+ JavaCpdEngine.save(context, inputFile, Collections.EMPTY_LIST);
+
+ verifyZeroInteractions(context);
+ }
+
+ @Test
+ public void testOneSimpleDuplicationBetweenTwoFiles() {
+ inputFile.setLines(5);
+ List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 2, 4), new ClonePart("key2", 0, 15, 17)));
+ JavaCpdEngine.save(context, inputFile, groups);
+
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(3));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATION_LINES_DATA).onFile(inputFile).withValue("1=0;2=1;3=1;4=1;5=0"));
+
+ InOrder inOrder = Mockito.inOrder(duplicationBuilder);
+ inOrder.verify(duplicationBuilder).originBlock(2, 4);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 17);
+ inOrder.verify(duplicationBuilder).build();
+ }
+
+ @Test
+ public void testDuplicationOnSameFile() throws Exception {
+ List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key1", 0, 215, 414)));
+ JavaCpdEngine.save(context, inputFile, groups);
+
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(2));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(400));
+
+ InOrder inOrder = Mockito.inOrder(duplicationBuilder);
+ inOrder.verify(duplicationBuilder).originBlock(5, 204);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key1", 215, 414);
+ inOrder.verify(duplicationBuilder).build();
+ }
+
+ @Test
+ public void testOneDuplicatedGroupInvolvingMoreThanTwoFiles() throws Exception {
+ List<CloneGroup> groups = Arrays.asList(newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key2", 0, 15, 214), new ClonePart("key3", 0, 25, 224)));
+ JavaCpdEngine.save(context, inputFile, groups);
+
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(200));
+
+ InOrder inOrder = Mockito.inOrder(duplicationBuilder);
+ inOrder.verify(duplicationBuilder).originBlock(5, 204);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 214);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key3", 25, 224);
+ inOrder.verify(duplicationBuilder).build();
+
+ verify(context).saveDuplications(inputFile, Arrays.asList(
+ new DuplicationGroup(new DuplicationGroup.Block("foo:src/main/java/Foo.java", 5, 200))
+ .addDuplicate(new DuplicationGroup.Block("key2", 15, 200))
+ .addDuplicate(new DuplicationGroup.Block("key3", 25, 200))
+ ));
+ }
+
+ @Test
+ public void testTwoDuplicatedGroupsInvolvingThreeFiles() throws Exception {
+ List<CloneGroup> groups = Arrays.asList(
+ newCloneGroup(new ClonePart("key1", 0, 5, 204), new ClonePart("key2", 0, 15, 214)),
+ newCloneGroup(new ClonePart("key1", 0, 15, 214), new ClonePart("key3", 0, 15, 214)));
+ JavaCpdEngine.save(context, inputFile, groups);
+
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_FILES).onFile(inputFile).withValue(1));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_BLOCKS).onFile(inputFile).withValue(2));
+ verify(storage).store(new DefaultMeasure().forMetric(CoreMetrics.DUPLICATED_LINES).onFile(inputFile).withValue(210));
+
+ InOrder inOrder = Mockito.inOrder(duplicationBuilder);
+ inOrder.verify(duplicationBuilder).originBlock(5, 204);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key2", 15, 214);
+ inOrder.verify(duplicationBuilder).originBlock(15, 214);
+ inOrder.verify(duplicationBuilder).isDuplicatedBy("key3", 15, 214);
+ inOrder.verify(duplicationBuilder).build();
+ }
+
+ private CloneGroup newCloneGroup(ClonePart... parts) {
+ return CloneGroup.builder().setLength(0).setOrigin(parts[0]).setParts(Arrays.asList(parts)).build();
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java
new file mode 100644
index 00000000000..629a1cf6d5d
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/DuplicationDensityDecoratorTest.java
@@ -0,0 +1,82 @@
+/*
+ * 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.batch.cpd.decorators;
+
+import org.sonar.batch.cpd.decorators.DuplicationDensityDecorator;
+
+import org.junit.Test;
+import static org.mockito.Mockito.*;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+
+public class DuplicationDensityDecoratorTest {
+
+ @Test
+ public void densityIsBalancedByNclocAndCommentLines() {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 40.0));
+ when(context.getMeasure(CoreMetrics.COMMENT_LINES)).thenReturn(new Measure(CoreMetrics.COMMENT_LINES, 10.0));
+ when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0));
+
+ DuplicationDensityDecorator decorator = new DuplicationDensityDecorator();
+ decorator.decorate(null, context);
+
+ verify(context).saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, 20.0);
+ }
+
+
+ @Test
+ public void densityEvenIfNoComments() {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 40.0));
+ when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0));
+
+ DuplicationDensityDecorator decorator = new DuplicationDensityDecorator();
+ decorator.decorate(null, context);
+
+ verify(context).saveMeasure(CoreMetrics.DUPLICATED_LINES_DENSITY, 25.0);
+ }
+
+ @Test
+ public void noDensityIfNoDuplicationMeasure() {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 45.0));
+
+ DuplicationDensityDecorator decorator = new DuplicationDensityDecorator();
+ decorator.decorate(null, context);
+
+ verify(context, never()).saveMeasure(eq(CoreMetrics.DUPLICATED_LINES_DENSITY), anyDouble());
+ }
+
+ @Test
+ public void noDensityWhenZeroNclocAndComments() {
+ DecoratorContext context = mock(DecoratorContext.class);
+ when(context.getMeasure(CoreMetrics.NCLOC)).thenReturn(new Measure(CoreMetrics.NCLOC, 0.0));
+ when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.COMMENT_LINES, 0.0));
+ when(context.getMeasure(CoreMetrics.DUPLICATED_LINES)).thenReturn(new Measure(CoreMetrics.DUPLICATED_LINES, 10.0));
+
+ DuplicationDensityDecorator decorator = new DuplicationDensityDecorator();
+ decorator.decorate(null, context);
+
+ verify(context, never()).saveMeasure(eq(CoreMetrics.DUPLICATED_LINES_DENSITY), anyDouble());
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java
new file mode 100644
index 00000000000..a86cfb423ed
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/decorators/SumDuplicationsDecoratorTest.java
@@ -0,0 +1,71 @@
+/*
+ * 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.batch.cpd.decorators;
+
+import org.sonar.batch.cpd.decorators.SumDuplicationsDecorator;
+
+import org.junit.Test;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Measure;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Qualifiers;
+import org.sonar.api.test.IsMeasure;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+public class SumDuplicationsDecoratorTest {
+
+ @Test
+ public void parapets() {
+ SumDuplicationsDecorator decorator = new SumDuplicationsDecorator();
+ assertThat(decorator.generatesMetrics().size(), greaterThan(0));
+ assertThat(decorator.shouldSaveZeroIfNoChildMeasures(), is(true));
+ }
+
+ @Test
+ public void doNotSetDuplicationsOnUnitTests() {
+ SumDuplicationsDecorator decorator = new SumDuplicationsDecorator();
+ File unitTest = File.create("org/foo/BarTest.java");
+ unitTest.setQualifier(Qualifiers.UNIT_TEST_FILE);
+ DecoratorContext context = mock(DecoratorContext.class);
+
+ decorator.decorate(unitTest, context);
+
+ verify(context, never()).saveMeasure(any(Measure.class));
+ }
+
+ @Test
+ public void saveZeroIfNoDuplications() {
+ SumDuplicationsDecorator decorator = new SumDuplicationsDecorator();
+ File file = File.create("org/foo/BarTest.java");
+ DecoratorContext context = mock(DecoratorContext.class);
+
+ decorator.decorate(file, context);
+
+ verify(context).saveMeasure(argThat(new IsMeasure(CoreMetrics.DUPLICATED_LINES, 0.0)));
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java b/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.java
new file mode 100644
index 00000000000..d60ace37875
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/cpd/index/IndexFactoryTest.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.batch.cpd.index;
+
+import org.sonar.batch.cpd.index.IndexFactory;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.config.Settings;
+import org.sonar.api.database.DatabaseSession;
+import org.sonar.api.resources.Project;
+import org.sonar.batch.bootstrap.DefaultAnalysisMode;
+import org.sonar.batch.index.ResourceCache;
+import org.sonar.core.duplication.DuplicationDao;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class IndexFactoryTest {
+
+ Project project;
+ Settings settings;
+ IndexFactory factory;
+ Logger logger;
+ private DefaultAnalysisMode analysisMode;
+
+ @Before
+ public void setUp() {
+ project = new Project("foo");
+ settings = new Settings();
+ analysisMode = mock(DefaultAnalysisMode.class);
+ factory = new IndexFactory(analysisMode, settings, mock(DuplicationDao.class), mock(DatabaseSession.class), new ResourceCache());
+ logger = mock(Logger.class);
+ }
+
+ @Test
+ public void crossProjectEnabled() {
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true");
+ assertThat(factory.verifyCrossProject(project, logger)).isTrue();
+ verify(logger).info("Cross-project analysis enabled");
+ }
+
+ @Test
+ public void noCrossProjectWithBranch() {
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true");
+ settings.setProperty(CoreProperties.PROJECT_BRANCH_PROPERTY, "branch");
+ assertThat(factory.verifyCrossProject(project, logger)).isFalse();
+ verify(logger).info("Cross-project analysis disabled. Not supported on project branches.");
+ }
+
+ @Test
+ public void cross_project_should_be_disabled_on_preview() {
+ when(analysisMode.isPreview()).thenReturn(true);
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "true");
+ assertThat(factory.verifyCrossProject(project, logger)).isFalse();
+ verify(logger).info("Cross-project analysis disabled. Not supported in preview mode.");
+ }
+
+ @Test
+ public void crossProjectDisabled() {
+ settings.setProperty(CoreProperties.CPD_CROSS_PROJECT, "false");
+ assertThat(factory.verifyCrossProject(project, logger)).isFalse();
+ verify(logger).info("Cross-project analysis disabled");
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java
new file mode 100644
index 00000000000..e5e6f6c1a1c
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/cpd/CpdMediumTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.batch.mediumtest.cpd;
+
+import com.google.common.collect.ImmutableMap;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.sensor.duplication.DuplicationGroup;
+import org.sonar.batch.mediumtest.BatchMediumTester;
+import org.sonar.batch.mediumtest.TaskResult;
+import org.sonar.xoo.XooPlugin;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CpdMediumTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ public BatchMediumTester tester = BatchMediumTester.builder()
+ .registerPlugin("xoo", new XooPlugin())
+ .addDefaultQProfile("xoo", "Sonar Way")
+ .build();
+
+ private File baseDir;
+
+ private ImmutableMap.Builder<String, String> builder;
+
+ @Before
+ public void prepare() throws IOException {
+ tester.start();
+
+ baseDir = temp.newFolder();
+
+ builder = ImmutableMap.<String, String>builder()
+ .put("sonar.task", "scan")
+ .put("sonar.projectBaseDir", baseDir.getAbsolutePath())
+ .put("sonar.projectKey", "com.foo.project")
+ .put("sonar.projectName", "Foo Project")
+ .put("sonar.projectVersion", "1.0-SNAPSHOT")
+ .put("sonar.projectDescription", "Description of Foo Project");
+ }
+
+ @After
+ public void stop() {
+ tester.stop();
+ }
+
+ @Test
+ public void testCrossFileDuplications() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String duplicatedStuff = "Sample xoo\ncontent\nfoo\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti\nbar\ntoto\ntiti\nfoo\nbar\ntoto\ntiti";
+
+ File xooFile1 = new File(srcDir, "sample1.xoo");
+ FileUtils.write(xooFile1, duplicatedStuff);
+
+ File xooFile2 = new File(srcDir, "sample2.xoo");
+ FileUtils.write(xooFile2, duplicatedStuff);
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "10")
+ .put("sonar.verbose", "true")
+ .build())
+ .start();
+
+ assertThat(result.inputFiles()).hasSize(2);
+
+ // 5 measures per file + quality profile measure
+ assertThat(result.measures()).hasSize(11);
+
+ InputFile inputFile1 = result.inputFile("src/sample1.xoo");
+ InputFile inputFile2 = result.inputFile("src/sample2.xoo");
+ // One clone group on each file
+ List<DuplicationGroup> duplicationGroupsFile1 = result.duplicationsFor(inputFile1);
+ assertThat(duplicationGroupsFile1).hasSize(1);
+
+ DuplicationGroup cloneGroupFile1 = duplicationGroupsFile1.get(0);
+ assertThat(cloneGroupFile1.duplicates()).hasSize(1);
+ assertThat(cloneGroupFile1.originBlock().startLine()).isEqualTo(1);
+ assertThat(cloneGroupFile1.originBlock().length()).isEqualTo(17);
+ assertThat(cloneGroupFile1.originBlock().resourceKey()).isEqualTo(((DefaultInputFile) inputFile1).key());
+ assertThat(cloneGroupFile1.duplicates()).hasSize(1);
+ assertThat(cloneGroupFile1.duplicates().get(0).resourceKey()).isEqualTo(((DefaultInputFile) inputFile2).key());
+
+ List<DuplicationGroup> duplicationGroupsFile2 = result.duplicationsFor(inputFile2);
+ assertThat(duplicationGroupsFile2).hasSize(1);
+
+ DuplicationGroup cloneGroupFile2 = duplicationGroupsFile2.get(0);
+ assertThat(cloneGroupFile2.duplicates()).hasSize(1);
+ assertThat(cloneGroupFile2.originBlock().startLine()).isEqualTo(1);
+ assertThat(cloneGroupFile2.originBlock().length()).isEqualTo(17);
+ assertThat(cloneGroupFile2.originBlock().resourceKey()).isEqualTo(((DefaultInputFile) inputFile2).key());
+ assertThat(cloneGroupFile2.duplicates()).hasSize(1);
+ assertThat(cloneGroupFile2.duplicates().get(0).resourceKey()).isEqualTo(((DefaultInputFile) inputFile1).key());
+ }
+
+ @Test
+ public void testIntraFileDuplications() throws IOException {
+ File srcDir = new File(baseDir, "src");
+ srcDir.mkdir();
+
+ String content = "Sample xoo\ncontent\nfoo\nbar\nSample xoo\ncontent\n";
+
+ File xooFile = new File(srcDir, "sample.xoo");
+ FileUtils.write(xooFile, content);
+
+ TaskResult result = tester.newTask()
+ .properties(builder
+ .put("sonar.sources", "src")
+ .put("sonar.cpd.xoo.minimumTokens", "2")
+ .put("sonar.cpd.xoo.minimumLines", "2")
+ .put("sonar.verbose", "true")
+ .build())
+ .start();
+
+ // 5 measures per file + QP measure
+ assertThat(result.measures()).hasSize(6);
+
+ InputFile inputFile = result.inputFile("src/sample.xoo");
+ // One clone group
+ List<DuplicationGroup> duplicationGroups = result.duplicationsFor(inputFile);
+ assertThat(duplicationGroups).hasSize(1);
+
+ DuplicationGroup cloneGroup = duplicationGroups.get(0);
+ assertThat(cloneGroup.duplicates()).hasSize(1);
+ assertThat(cloneGroup.originBlock().startLine()).isEqualTo(1);
+ assertThat(cloneGroup.originBlock().length()).isEqualTo(2);
+ assertThat(cloneGroup.duplicates()).hasSize(1);
+ assertThat(cloneGroup.duplicates().get(0).startLine()).isEqualTo(5);
+ assertThat(cloneGroup.duplicates().get(0).length()).isEqualTo(2);
+
+ // assertThat(result.measures()).contains(new DefaultMeasure<String>()
+ // .forMetric(CoreMetrics.DUPLICATION_LINES_DATA)
+ // .onFile(inputFile)
+ // .withValue("1=1;2=1;3=0;4=0;5=1;6=1;7=0"));
+ }
+
+}