aboutsummaryrefslogtreecommitdiffstats
path: root/sonar-batch
diff options
context:
space:
mode:
authorJulien HENRY <julien.henry@sonarsource.com>2015-01-21 11:22:44 +0100
committerJulien HENRY <julien.henry@sonarsource.com>2015-01-23 09:59:47 +0100
commit1340ee7da7a1688ebb059812504e117d041e0124 (patch)
tree171deefeac6057e876106131ac333bd4914d9253 /sonar-batch
parent97ca9e16fc27e19f021558502fdce23fe9a77460 (diff)
downloadsonarqube-1340ee7da7a1688ebb059812504e117d041e0124.tar.gz
sonarqube-1340ee7da7a1688ebb059812504e117d041e0124.zip
SONAR-6012 Local issue tracking
Diffstat (limited to 'sonar-batch')
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/BatchComponents.java4
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapContainer.java17
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java2
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/index/ResourceCache.java11
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java77
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensor.java86
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStack.java85
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueHandlers.java140
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTracking.java337
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizer.java69
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingDecorator.java280
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingResult.java111
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java227
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssue.java47
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromDb.java62
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromWs.java61
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueRepository.java84
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java89
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java78
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/issue/tracking/package-info.java23
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/mediumtest/BatchMediumTester.java40
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/phases/PreviewPhaseExecutor.java15
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/DefaultGlobalReferentialsLoader.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/DefaultGlobalReferentialsLoader.java)2
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/DefaultPreviousIssuesLoader.java55
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/DefaultProjectReferentialsLoader.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/DefaultProjectReferentialsLoader.java)10
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsLoader.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsLoader.java)2
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsProvider.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsProvider.java)2
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/PreviousIssuesLoader.java30
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsLoader.java)8
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsProvider.java)12
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/repository/package-info.java (renamed from sonar-batch/src/main/java/org/sonar/batch/referential/package-info.java)2
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java8
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java4
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java13
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/ModuleSettings.java6
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java16
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/ProjectSettings.java6
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java6
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java6
-rw-r--r--sonar-batch/src/main/java/org/sonar/batch/scm/ScmSensor.java6
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensorTest.java65
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStackTest.java142
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueHandlersTest.java62
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizerTest.java49
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingDecoratorTest.java582
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingTest.java372
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java43
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java107
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java6
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/ReportsMediumTest.java11
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/repository/DefaultProjectReferentialsLoaderTest.java (renamed from sonar-batch/src/test/java/org/sonar/batch/referential/DefaultProjectReferentialsLoaderTest.java)5
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java6
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java11
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java6
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/FileMetadataTest.java12
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java4
-rw-r--r--sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java4
-rw-r--r--sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures1
-rw-r--r--sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures1
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v1.txt12
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v2.txt22
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v1.txt7
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v2.txt16
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v1.txt16
-rw-r--r--sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v2.txt20
65 files changed, 3631 insertions, 90 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 333a041f13f..dca499c6869 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
@@ -26,6 +26,7 @@ import org.sonar.batch.design.FileTangleIndexDecorator;
import org.sonar.batch.design.MavenDependenciesSensor;
import org.sonar.batch.design.ProjectDsmDecorator;
import org.sonar.batch.design.SubProjectDsmDecorator;
+import org.sonar.batch.issue.tracking.IssueTracking;
import org.sonar.batch.maven.DefaultMavenPluginExecutor;
import org.sonar.batch.maven.MavenProjectBootstrapper;
import org.sonar.batch.maven.MavenProjectBuilder;
@@ -66,6 +67,9 @@ public class BatchComponents {
LinesSensor.class,
+ // Issues tracking
+ IssueTracking.class,
+
// Reports
ConsoleReport.class,
JSONReport.class,
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapContainer.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapContainer.java
index 75071b09ef1..d3749c4cf7f 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapContainer.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/BootstrapContainer.java
@@ -35,11 +35,13 @@ import org.sonar.batch.components.PastSnapshotFinderByDays;
import org.sonar.batch.components.PastSnapshotFinderByPreviousAnalysis;
import org.sonar.batch.components.PastSnapshotFinderByPreviousVersion;
import org.sonar.batch.components.PastSnapshotFinderByVersion;
-import org.sonar.batch.referential.DefaultGlobalReferentialsLoader;
-import org.sonar.batch.referential.DefaultProjectReferentialsLoader;
-import org.sonar.batch.referential.GlobalReferentialsLoader;
-import org.sonar.batch.referential.GlobalReferentialsProvider;
-import org.sonar.batch.referential.ProjectReferentialsLoader;
+import org.sonar.batch.repository.DefaultGlobalReferentialsLoader;
+import org.sonar.batch.repository.DefaultPreviousIssuesLoader;
+import org.sonar.batch.repository.DefaultProjectReferentialsLoader;
+import org.sonar.batch.repository.GlobalReferentialsLoader;
+import org.sonar.batch.repository.GlobalReferentialsProvider;
+import org.sonar.batch.repository.PreviousIssuesLoader;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
import org.sonar.batch.user.UserRepository;
import org.sonar.core.cluster.NullQueue;
import org.sonar.core.config.Logback;
@@ -113,9 +115,12 @@ public class BootstrapContainer extends ComponentContainer {
if (getComponentByType(GlobalReferentialsLoader.class) == null) {
add(DefaultGlobalReferentialsLoader.class);
}
- if (getComponentByType(ProjectReferentialsLoader.class) == null) {
+ if (getComponentByType(ProjectRepositoriesLoader.class) == null) {
add(DefaultProjectReferentialsLoader.class);
}
+ if (getComponentByType(PreviousIssuesLoader.class) == null) {
+ add(DefaultPreviousIssuesLoader.class);
+ }
}
private void addDatabaseComponents() {
diff --git a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java
index 470f0f79686..3b53ae97c19 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/bootstrap/ServerClient.java
@@ -103,7 +103,7 @@ public class ServerClient implements BatchComponent {
}
}
- private InputSupplier<InputStream> doRequest(String pathStartingWithSlash, String requestMethod, @Nullable Integer timeoutMillis) {
+ public InputSupplier<InputStream> doRequest(String pathStartingWithSlash, String requestMethod, @Nullable Integer timeoutMillis) {
Preconditions.checkArgument(pathStartingWithSlash.startsWith("/"), "Path must start with slash /");
String path = StringEscapeUtils.escapeHtml(pathStartingWithSlash);
diff --git a/sonar-batch/src/main/java/org/sonar/batch/index/ResourceCache.java b/sonar-batch/src/main/java/org/sonar/batch/index/ResourceCache.java
index afa2ed7fee3..4a97a6c8ab8 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/index/ResourceCache.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/index/ResourceCache.java
@@ -38,6 +38,8 @@ public class ResourceCache implements BatchComponent {
// dedicated cache for libraries
private final Map<Library, BatchResource> libraries = Maps.newLinkedHashMap();
+ private BatchResource root;
+
@CheckForNull
public BatchResource get(String componentKey) {
return resources.get(componentKey);
@@ -56,10 +58,13 @@ public class ResourceCache implements BatchComponent {
String componentKey = resource.getEffectiveKey();
Preconditions.checkState(!Strings.isNullOrEmpty(componentKey), "Missing resource effective key");
BatchResource parent = parentResource != null ? get(parentResource.getEffectiveKey()) : null;
- BatchResource batchResource = new BatchResource((long) resources.size() + 1, resource, parent);
+ BatchResource batchResource = new BatchResource(resources.size() + 1, resource, parent);
if (!(resource instanceof Library)) {
// Libraries can have the same effective key than a project so we can't cache by effectiveKey
resources.put(componentKey, batchResource);
+ if (parent == null) {
+ root = batchResource;
+ }
} else {
libraries.put((Library) resource, batchResource);
}
@@ -73,4 +78,8 @@ public class ResourceCache implements BatchComponent {
public Collection<BatchResource> allLibraries() {
return libraries.values();
}
+
+ public BatchResource getRoot() {
+ return root;
+ }
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java
new file mode 100644
index 00000000000..49b0405787b
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/FileHashes.java
@@ -0,0 +1,77 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import org.apache.commons.codec.binary.Hex;
+import org.apache.commons.lang.ObjectUtils;
+
+import java.util.Collection;
+
+/**
+ * Wraps a {@link Sequence} to assign hash codes to elements.
+ */
+public final class FileHashes {
+
+ private final String[] hashes;
+ private final Multimap<String, Integer> linesByHash;
+
+ private FileHashes(String[] hashes, Multimap<String, Integer> linesByHash) {
+ this.hashes = hashes;
+ this.linesByHash = linesByHash;
+ }
+
+ public static FileHashes create(String[] hashes) {
+ int size = hashes.length;
+ Multimap<String, Integer> linesByHash = LinkedHashMultimap.create();
+ for (int i = 0; i < size; i++) {
+ // indices in array are shifted one line before
+ linesByHash.put(hashes[i], i + 1);
+ }
+ return new FileHashes(hashes, linesByHash);
+ }
+
+ public static FileHashes create(byte[][] hashes) {
+ int size = hashes.length;
+ Multimap<String, Integer> linesByHash = LinkedHashMultimap.create();
+ String[] hexHashes = new String[size];
+ for (int i = 0; i < size; i++) {
+ String hash = hashes[i] != null ? Hex.encodeHexString(hashes[i]) : "";
+ hexHashes[i] = hash;
+ // indices in array are shifted one line before
+ linesByHash.put(hash, i + 1);
+ }
+ return new FileHashes(hexHashes, linesByHash);
+ }
+
+ public int length() {
+ return hashes.length;
+ }
+
+ public Collection<Integer> getLinesForHash(String hash) {
+ return linesByHash.get(hash);
+ }
+
+ public String getHash(int line) {
+ // indices in array are shifted one line before
+ return (String) ObjectUtils.defaultIfNull(hashes[line - 1], "");
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensor.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensor.java
new file mode 100644
index 00000000000..a58f5c93a80
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensor.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.issue.tracking;
+
+import org.apache.commons.lang.time.DateUtils;
+import org.apache.ibatis.session.ResultContext;
+import org.apache.ibatis.session.ResultHandler;
+import org.sonar.api.batch.Sensor;
+import org.sonar.api.batch.SensorContext;
+import org.sonar.api.resources.Project;
+import org.sonar.core.DryRunIncompatible;
+import org.sonar.core.issue.db.IssueChangeDao;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueDao;
+import org.sonar.core.issue.db.IssueDto;
+
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Load all the issues referenced during the previous scan.
+ */
+@DryRunIncompatible
+public class InitialOpenIssuesSensor implements Sensor {
+
+ private final InitialOpenIssuesStack initialOpenIssuesStack;
+ private final IssueDao issueDao;
+ private final IssueChangeDao issueChangeDao;
+
+ public InitialOpenIssuesSensor(InitialOpenIssuesStack initialOpenIssuesStack, IssueDao issueDao, IssueChangeDao issueChangeDao) {
+ this.initialOpenIssuesStack = initialOpenIssuesStack;
+ this.issueDao = issueDao;
+ this.issueChangeDao = issueChangeDao;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+
+ @Override
+ public void analyse(Project project, SensorContext context) {
+ // Adding one second is a hack for resolving conflicts with concurrent user
+ // changes during issue persistence
+ final Date now = DateUtils.addSeconds(DateUtils.truncate(new Date(), Calendar.MILLISECOND), 1);
+
+ issueDao.selectNonClosedIssuesByModule(project.getId(), new ResultHandler() {
+ @Override
+ public void handleResult(ResultContext rc) {
+ IssueDto dto = (IssueDto) rc.getResultObject();
+ dto.setSelectedAt(now.getTime());
+ initialOpenIssuesStack.addIssue(dto);
+ }
+ });
+
+ issueChangeDao.selectChangelogOnNonClosedIssuesByModuleAndType(project.getId(), new ResultHandler() {
+ @Override
+ public void handleResult(ResultContext rc) {
+ IssueChangeDto dto = (IssueChangeDto) rc.getResultObject();
+ initialOpenIssuesStack.addChangelog(dto);
+ }
+ });
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStack.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStack.java
new file mode 100644
index 00000000000..acc9ebf21b8
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStack.java
@@ -0,0 +1,85 @@
+/*
+ * 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.issue.tracking;
+
+import org.sonar.api.BatchExtension;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.batch.index.Cache;
+import org.sonar.batch.index.Caches;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueDto;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class InitialOpenIssuesStack implements BatchExtension {
+
+ private final Cache<IssueDto> issuesCache;
+ private final Cache<ArrayList<IssueChangeDto>> issuesChangelogCache;
+
+ public InitialOpenIssuesStack(Caches caches) {
+ issuesCache = caches.createCache("last-open-issues");
+ issuesChangelogCache = caches.createCache("issues-changelog");
+ }
+
+ public InitialOpenIssuesStack addIssue(IssueDto issueDto) {
+ issuesCache.put(issueDto.getComponentKey(), issueDto.getKee(), issueDto);
+ return this;
+ }
+
+ public List<PreviousIssue> selectAndRemoveIssues(String componentKey) {
+ Iterable<IssueDto> issues = issuesCache.values(componentKey);
+ List<PreviousIssue> result = newArrayList();
+ for (IssueDto issue : issues) {
+ result.add(new PreviousIssueFromDb(issue));
+ }
+ issuesCache.clear(componentKey);
+ return result;
+ }
+
+ public Iterable<IssueDto> selectAllIssues() {
+ return issuesCache.values();
+ }
+
+ public InitialOpenIssuesStack addChangelog(IssueChangeDto issueChangeDto) {
+ List<IssueChangeDto> changeDtos = issuesChangelogCache.get(issueChangeDto.getIssueKey());
+ if (changeDtos == null) {
+ changeDtos = newArrayList();
+ }
+ changeDtos.add(issueChangeDto);
+ issuesChangelogCache.put(issueChangeDto.getIssueKey(), newArrayList(changeDtos));
+ return this;
+ }
+
+ public List<IssueChangeDto> selectChangelog(String issueKey) {
+ List<IssueChangeDto> changeDtos = issuesChangelogCache.get(issueKey);
+ return changeDtos != null ? changeDtos : Collections.<IssueChangeDto>emptyList();
+ }
+
+ public void clear() {
+ issuesCache.clear();
+ issuesChangelogCache.clear();
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueHandlers.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueHandlers.java
new file mode 100644
index 00000000000..f8a98c29b00
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueHandlers.java
@@ -0,0 +1,140 @@
+/*
+ * 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.issue.tracking;
+
+import org.sonar.api.BatchExtension;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.IssueHandler;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.user.User;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.user.DefaultUser;
+
+import javax.annotation.Nullable;
+
+public class IssueHandlers implements BatchExtension {
+ private final IssueHandler[] handlers;
+ private final DefaultContext context;
+
+ public IssueHandlers(IssueUpdater updater, IssueHandler[] handlers) {
+ this.handlers = handlers;
+ this.context = new DefaultContext(updater);
+ }
+
+ public IssueHandlers(IssueUpdater updater) {
+ this(updater, new IssueHandler[0]);
+ }
+
+ public void execute(DefaultIssue issue, IssueChangeContext changeContext) {
+ context.reset(issue, changeContext);
+ for (IssueHandler handler : handlers) {
+ handler.onIssue(context);
+ }
+ }
+
+ static class DefaultContext implements IssueHandler.Context {
+ private final IssueUpdater updater;
+ private DefaultIssue issue;
+ private IssueChangeContext changeContext;
+
+ private DefaultContext(IssueUpdater updater) {
+ this.updater = updater;
+ }
+
+ private void reset(DefaultIssue i, IssueChangeContext changeContext) {
+ this.issue = i;
+ this.changeContext = changeContext;
+ }
+
+ @Override
+ public Issue issue() {
+ return issue;
+ }
+
+ @Override
+ public boolean isNew() {
+ return issue.isNew();
+ }
+
+ @Override
+ public boolean isEndOfLife() {
+ return issue.isEndOfLife();
+ }
+
+ @Override
+ public IssueHandler.Context setLine(@Nullable Integer line) {
+ updater.setLine(issue, line);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context setMessage(@Nullable String s) {
+ updater.setMessage(issue, s, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context setSeverity(String severity) {
+ updater.setSeverity(issue, severity, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context setAuthorLogin(@Nullable String login) {
+ updater.setAuthorLogin(issue, login, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context setEffortToFix(@Nullable Double d) {
+ updater.setEffortToFix(issue, d, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context setAttribute(String key, @Nullable String value) {
+ throw new UnsupportedOperationException("TODO");
+ }
+
+ @Override
+ public IssueHandler.Context assign(@Nullable String assignee) {
+ User user = null;
+ if(assignee != null) {
+ user = new DefaultUser().setLogin(assignee).setName(assignee);
+ }
+ updater.assign(issue, user, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context assign(@Nullable User user) {
+ updater.assign(issue, user, changeContext);
+ return this;
+ }
+
+ @Override
+ public IssueHandler.Context addComment(String text) {
+ updater.addComment(issue, text, changeContext);
+ return this;
+ }
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTracking.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTracking.java
new file mode 100644
index 00000000000..2c1c49afa97
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTracking.java
@@ -0,0 +1,337 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Objects;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.issue.internal.DefaultIssue;
+
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class IssueTracking implements BatchComponent {
+
+ /**
+ * @param sourceHashHolder Null when working on resource that is not a file (directory/project)
+ */
+ public IssueTrackingResult track(@Nullable SourceHashHolder sourceHashHolder, Collection<PreviousIssue> previousIssues, Collection<DefaultIssue> newIssues) {
+ IssueTrackingResult result = new IssueTrackingResult();
+
+ if (sourceHashHolder != null) {
+ setChecksumOnNewIssues(newIssues, sourceHashHolder);
+ }
+
+ // Map new issues with old ones
+ mapIssues(newIssues, previousIssues, sourceHashHolder, result);
+ return result;
+ }
+
+ private void setChecksumOnNewIssues(Collection<DefaultIssue> issues, SourceHashHolder sourceHashHolder) {
+ if (issues.isEmpty()) {
+ return;
+ }
+ for (DefaultIssue issue : issues) {
+ Integer line = issue.line();
+ if (line != null) {
+ issue.setChecksum(sourceHashHolder.getHashedSource().getHash(line));
+ }
+ }
+ }
+
+ @VisibleForTesting
+ void mapIssues(Collection<DefaultIssue> newIssues, @Nullable Collection<PreviousIssue> previousIssues, @Nullable SourceHashHolder sourceHashHolder, IssueTrackingResult result) {
+ boolean hasLastScan = false;
+
+ if (previousIssues != null) {
+ hasLastScan = true;
+ mapLastIssues(newIssues, previousIssues, result);
+ }
+
+ // If each new issue matches an old one we can stop the matching mechanism
+ if (result.matched().size() != newIssues.size()) {
+ if (sourceHashHolder != null && hasLastScan) {
+ FileHashes hashedReference = sourceHashHolder.getHashedReference();
+ if (hashedReference != null) {
+ mapNewissues(hashedReference, sourceHashHolder.getHashedSource(), newIssues, result);
+ }
+ }
+ mapIssuesOnSameRule(newIssues, result);
+ }
+ }
+
+ private void mapLastIssues(Collection<DefaultIssue> newIssues, Collection<PreviousIssue> previousIssues, IssueTrackingResult result) {
+ for (PreviousIssue lastIssue : previousIssues) {
+ result.addUnmatched(lastIssue);
+ }
+
+ // Match the key of the issue. (For manual issues)
+ for (DefaultIssue newIssue : newIssues) {
+ mapIssue(newIssue, result.unmatchedByKeyForRule(newIssue.ruleKey()).get(newIssue.key()), result);
+ }
+
+ // Try first to match issues on same rule with same line and with same checksum (but not necessarily with same message)
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result)) {
+ mapIssue(
+ newIssue,
+ findLastIssueWithSameLineAndChecksum(newIssue, result),
+ result);
+ }
+ }
+ }
+
+ private void mapNewissues(FileHashes hashedReference, FileHashes hashedSource, Collection<DefaultIssue> newIssues, IssueTrackingResult result) {
+
+ IssueTrackingBlocksRecognizer rec = new IssueTrackingBlocksRecognizer(hashedReference, hashedSource);
+
+ RollingFileHashes a = RollingFileHashes.create(hashedReference, 5);
+ RollingFileHashes b = RollingFileHashes.create(hashedSource, 5);
+
+ Multimap<Integer, DefaultIssue> newIssuesByLines = newIssuesByLines(newIssues, rec, result);
+ Multimap<Integer, PreviousIssue> lastIssuesByLines = lastIssuesByLines(result.unmatched(), rec);
+
+ Map<Integer, HashOccurrence> map = Maps.newHashMap();
+
+ for (Integer line : lastIssuesByLines.keySet()) {
+ int hash = a.getHash(line);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence == null) {
+ // first occurrence in A
+ hashOccurrence = new HashOccurrence();
+ hashOccurrence.lineA = line;
+ hashOccurrence.countA = 1;
+ map.put(hash, hashOccurrence);
+ } else {
+ hashOccurrence.countA++;
+ }
+ }
+
+ for (Integer line : newIssuesByLines.keySet()) {
+ int hash = b.getHash(line);
+ HashOccurrence hashOccurrence = map.get(hash);
+ if (hashOccurrence != null) {
+ hashOccurrence.lineB = line;
+ hashOccurrence.countB++;
+ }
+ }
+
+ for (HashOccurrence hashOccurrence : map.values()) {
+ if (hashOccurrence.countA == 1 && hashOccurrence.countB == 1) {
+ // Guaranteed that lineA has been moved to lineB, so we can map all issues on lineA to all issues on lineB
+ map(newIssuesByLines.get(hashOccurrence.lineB), lastIssuesByLines.get(hashOccurrence.lineA), result);
+ lastIssuesByLines.removeAll(hashOccurrence.lineA);
+ newIssuesByLines.removeAll(hashOccurrence.lineB);
+ }
+ }
+
+ // Check if remaining number of lines exceeds threshold
+ if (lastIssuesByLines.keySet().size() * newIssuesByLines.keySet().size() < 250000) {
+ List<LinePair> possibleLinePairs = Lists.newArrayList();
+ for (Integer oldLine : lastIssuesByLines.keySet()) {
+ for (Integer newLine : newIssuesByLines.keySet()) {
+ int weight = rec.computeLengthOfMaximalBlock(oldLine, newLine);
+ possibleLinePairs.add(new LinePair(oldLine, newLine, weight));
+ }
+ }
+ Collections.sort(possibleLinePairs, LINE_PAIR_COMPARATOR);
+ for (LinePair linePair : possibleLinePairs) {
+ // High probability that lineA has been moved to lineB, so we can map all Issues on lineA to all Issues on lineB
+ map(newIssuesByLines.get(linePair.lineB), lastIssuesByLines.get(linePair.lineA), result);
+ }
+ }
+ }
+
+ private void mapIssuesOnSameRule(Collection<DefaultIssue> newIssues, IssueTrackingResult result) {
+ // Try then to match issues on same rule with same message and with same checksum
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result)) {
+ mapIssue(
+ newIssue,
+ findLastIssueWithSameChecksumAndMessage(newIssue, result.unmatchedByKeyForRule(newIssue.ruleKey()).values()),
+ result);
+ }
+ }
+
+ // Try then to match issues on same rule with same line and with same message
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result)) {
+ mapIssue(
+ newIssue,
+ findLastIssueWithSameLineAndMessage(newIssue, result.unmatchedByKeyForRule(newIssue.ruleKey()).values()),
+ result);
+ }
+ }
+
+ // Last check: match issue if same rule and same checksum but different line and different message
+ // See SONAR-2812
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result)) {
+ mapIssue(
+ newIssue,
+ findLastIssueWithSameChecksum(newIssue, result.unmatchedByKeyForRule(newIssue.ruleKey()).values()),
+ result);
+ }
+ }
+ }
+
+ private void map(Collection<DefaultIssue> newIssues, Collection<PreviousIssue> previousIssues, IssueTrackingResult result) {
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result)) {
+ for (PreviousIssue previousIssue : previousIssues) {
+ if (isNotAlreadyMapped(previousIssue, result) && Objects.equal(newIssue.ruleKey(), previousIssue.ruleKey())) {
+ mapIssue(newIssue, previousIssue, result);
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ private Multimap<Integer, DefaultIssue> newIssuesByLines(Collection<DefaultIssue> newIssues, IssueTrackingBlocksRecognizer rec, IssueTrackingResult result) {
+ Multimap<Integer, DefaultIssue> newIssuesByLines = LinkedHashMultimap.create();
+ for (DefaultIssue newIssue : newIssues) {
+ if (isNotAlreadyMapped(newIssue, result) && rec.isValidLineInSource(newIssue.line())) {
+ newIssuesByLines.put(newIssue.line(), newIssue);
+ }
+ }
+ return newIssuesByLines;
+ }
+
+ private Multimap<Integer, PreviousIssue> lastIssuesByLines(Collection<PreviousIssue> previousIssues, IssueTrackingBlocksRecognizer rec) {
+ Multimap<Integer, PreviousIssue> previousIssuesByLines = LinkedHashMultimap.create();
+ for (PreviousIssue previousIssue : previousIssues) {
+ if (rec.isValidLineInReference(previousIssue.line())) {
+ previousIssuesByLines.put(previousIssue.line(), previousIssue);
+ }
+ }
+ return previousIssuesByLines;
+ }
+
+ private PreviousIssue findLastIssueWithSameChecksum(DefaultIssue newIssue, Collection<PreviousIssue> previousIssues) {
+ for (PreviousIssue previousIssue : previousIssues) {
+ if (isSameChecksum(newIssue, previousIssue)) {
+ return previousIssue;
+ }
+ }
+ return null;
+ }
+
+ private PreviousIssue findLastIssueWithSameLineAndMessage(DefaultIssue newIssue, Collection<PreviousIssue> previousIssues) {
+ for (PreviousIssue previousIssue : previousIssues) {
+ if (isSameLine(newIssue, previousIssue) && isSameMessage(newIssue, previousIssue)) {
+ return previousIssue;
+ }
+ }
+ return null;
+ }
+
+ private PreviousIssue findLastIssueWithSameChecksumAndMessage(DefaultIssue newIssue, Collection<PreviousIssue> previousIssues) {
+ for (PreviousIssue previousIssue : previousIssues) {
+ if (isSameChecksum(newIssue, previousIssue) && isSameMessage(newIssue, previousIssue)) {
+ return previousIssue;
+ }
+ }
+ return null;
+ }
+
+ private PreviousIssue findLastIssueWithSameLineAndChecksum(DefaultIssue newIssue, IssueTrackingResult result) {
+ Collection<PreviousIssue> sameRuleAndSameLineAndSameChecksum = result.unmatchedForRuleAndForLineAndForChecksum(newIssue.ruleKey(), newIssue.line(), newIssue.checksum());
+ if (!sameRuleAndSameLineAndSameChecksum.isEmpty()) {
+ return sameRuleAndSameLineAndSameChecksum.iterator().next();
+ }
+ return null;
+ }
+
+ private boolean isNotAlreadyMapped(PreviousIssue previousIssue, IssueTrackingResult result) {
+ return result.unmatched().contains(previousIssue);
+ }
+
+ private boolean isNotAlreadyMapped(DefaultIssue newIssue, IssueTrackingResult result) {
+ return !result.isMatched(newIssue);
+ }
+
+ private boolean isSameChecksum(DefaultIssue newIssue, PreviousIssue previousIssue) {
+ return Objects.equal(previousIssue.checksum(), newIssue.checksum());
+ }
+
+ private boolean isSameLine(DefaultIssue newIssue, PreviousIssue previousIssue) {
+ return Objects.equal(previousIssue.line(), newIssue.line());
+ }
+
+ private boolean isSameMessage(DefaultIssue newIssue, PreviousIssue previousIssue) {
+ return Objects.equal(newIssue.message(), previousIssue.message());
+ }
+
+ private void mapIssue(DefaultIssue issue, @Nullable PreviousIssue ref, IssueTrackingResult result) {
+ if (ref != null) {
+ result.setMatch(issue, ref);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName();
+ }
+
+ private static class LinePair {
+ int lineA;
+ int lineB;
+ int weight;
+
+ public LinePair(int lineA, int lineB, int weight) {
+ this.lineA = lineA;
+ this.lineB = lineB;
+ this.weight = weight;
+ }
+ }
+
+ private static class HashOccurrence {
+ int lineA;
+ int lineB;
+ int countA;
+ int countB;
+ }
+
+ private static final Comparator<LinePair> LINE_PAIR_COMPARATOR = new Comparator<LinePair>() {
+ @Override
+ public int compare(LinePair o1, LinePair o2) {
+ int weightDiff = o2.weight - o1.weight;
+ if (weightDiff != 0) {
+ return weightDiff;
+ } else {
+ return Math.abs(o1.lineA - o1.lineB) - Math.abs(o2.lineA - o2.lineB);
+ }
+ }
+ };
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizer.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizer.java
new file mode 100644
index 00000000000..c7e01c9020d
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizer.java
@@ -0,0 +1,69 @@
+/*
+ * 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.issue.tracking;
+
+import javax.annotation.Nullable;
+
+public class IssueTrackingBlocksRecognizer {
+
+ private final FileHashes a;
+ private final FileHashes b;
+
+ public IssueTrackingBlocksRecognizer(FileHashes a, FileHashes b) {
+ this.a = a;
+ this.b = b;
+ }
+
+ public boolean isValidLineInReference(@Nullable Integer line) {
+ return (line != null) && (0 <= line - 1) && (line - 1 < a.length());
+ }
+
+ public boolean isValidLineInSource(@Nullable Integer line) {
+ return (line != null) && (0 <= line - 1) && (line - 1 < b.length());
+ }
+
+ /**
+ * @param startA number of line from first version of text (numbering starts from 1)
+ * @param startB number of line from second version of text (numbering starts from 1)
+ */
+ public int computeLengthOfMaximalBlock(int startA, int startB) {
+ if (!a.getHash(startA).equals(b.getHash(startB))) {
+ return 0;
+ }
+ int length = 0;
+ int ai = startA;
+ int bi = startB;
+ while (ai <= a.length() && bi <= b.length() && a.getHash(ai).equals(b.getHash(bi))) {
+ ai++;
+ bi++;
+ length++;
+ }
+ ai = startA;
+ bi = startB;
+ while (ai > 0 && bi > 0 && a.getHash(ai).equals(b.getHash(bi))) {
+ ai--;
+ bi--;
+ length++;
+ }
+ // Note that position (startA, startB) was counted twice
+ return length - 1;
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingDecorator.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingDecorator.java
new file mode 100644
index 00000000000..8bc86e35fa7
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingDecorator.java
@@ -0,0 +1,280 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.batch.Decorator;
+import org.sonar.api.batch.DecoratorBarriers;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.DependedUpon;
+import org.sonar.api.batch.DependsUpon;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.issue.Issuable;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.api.rules.ActiveRule;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.KeyValueFormat;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.scan.LastLineHashes;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.core.DryRunIncompatible;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueDto;
+import org.sonar.core.issue.workflow.IssueWorkflow;
+
+import java.util.Collection;
+
+@DependsUpon(DecoratorBarriers.ISSUES_ADDED)
+@DependedUpon(DecoratorBarriers.ISSUES_TRACKED)
+@DryRunIncompatible
+public class IssueTrackingDecorator implements Decorator {
+
+ private static final Logger LOG = LoggerFactory.getLogger(IssueTrackingDecorator.class);
+
+ private final IssueCache issueCache;
+ private final InitialOpenIssuesStack initialOpenIssues;
+ private final IssueTracking tracking;
+ private final LastLineHashes lastLineHashes;
+ private final IssueHandlers handlers;
+ private final IssueWorkflow workflow;
+ private final IssueUpdater updater;
+ private final IssueChangeContext changeContext;
+ private final ResourcePerspectives perspectives;
+ private final RulesProfile rulesProfile;
+ private final RuleFinder ruleFinder;
+ private final InputPathCache inputPathCache;
+ private final Project project;
+
+ public IssueTrackingDecorator(IssueCache issueCache, InitialOpenIssuesStack initialOpenIssues, IssueTracking tracking,
+ LastLineHashes lastLineHashes,
+ IssueHandlers handlers, IssueWorkflow workflow,
+ IssueUpdater updater,
+ Project project,
+ ResourcePerspectives perspectives,
+ RulesProfile rulesProfile,
+ RuleFinder ruleFinder, InputPathCache inputPathCache) {
+ this.issueCache = issueCache;
+ this.initialOpenIssues = initialOpenIssues;
+ this.tracking = tracking;
+ this.lastLineHashes = lastLineHashes;
+ this.handlers = handlers;
+ this.workflow = workflow;
+ this.updater = updater;
+ this.project = project;
+ this.inputPathCache = inputPathCache;
+ this.changeContext = IssueChangeContext.createScan(project.getAnalysisDate());
+ this.perspectives = perspectives;
+ this.rulesProfile = rulesProfile;
+ this.ruleFinder = ruleFinder;
+ }
+
+ @Override
+ public boolean shouldExecuteOnProject(Project project) {
+ return true;
+ }
+
+ @Override
+ public void decorate(Resource resource, DecoratorContext context) {
+ Issuable issuable = perspectives.as(Issuable.class, resource);
+ if (issuable != null) {
+ doDecorate(resource);
+ }
+ }
+
+ @VisibleForTesting
+ void doDecorate(Resource resource) {
+ Collection<DefaultIssue> issues = Lists.newArrayList();
+ for (Issue issue : issueCache.byComponent(resource.getEffectiveKey())) {
+ issues.add((DefaultIssue) issue);
+ }
+ issueCache.clear(resource.getEffectiveKey());
+ // issues = all the issues created by rule engines during this module scan and not excluded by filters
+
+ // all the issues that are not closed in db before starting this module scan, including manual issues
+ Collection<PreviousIssue> dbOpenIssues = initialOpenIssues.selectAndRemoveIssues(resource.getEffectiveKey());
+
+ SourceHashHolder sourceHashHolder = null;
+ if (ResourceUtils.isFile(resource)) {
+ File sonarFile = (File) resource;
+ InputFile file = inputPathCache.getFile(project.getEffectiveKey(), sonarFile.getPath());
+ if (file == null) {
+ throw new IllegalStateException("Resource " + resource + " was not found in InputPath cache");
+ }
+ sourceHashHolder = new SourceHashHolder((DefaultInputFile) file, lastLineHashes);
+ }
+
+ IssueTrackingResult trackingResult = tracking.track(sourceHashHolder, dbOpenIssues, issues);
+
+ // unmatched = issues that have been resolved + issues on disabled/removed rules + manual issues
+ addUnmatched(trackingResult.unmatched(), sourceHashHolder, issues);
+
+ mergeMatched(trackingResult);
+
+ if (ResourceUtils.isProject(resource)) {
+ // issues that relate to deleted components
+ addIssuesOnDeletedComponents(issues);
+ }
+
+ for (DefaultIssue issue : issues) {
+ workflow.doAutomaticTransition(issue, changeContext);
+ handlers.execute(issue, changeContext);
+ issueCache.put(issue);
+ }
+ }
+
+ @VisibleForTesting
+ protected void mergeMatched(IssueTrackingResult result) {
+ for (DefaultIssue issue : result.matched()) {
+ IssueDto ref = ((PreviousIssueFromDb) result.matching(issue)).getDto();
+
+ // invariant fields
+ issue.setKey(ref.getKee());
+ issue.setCreationDate(ref.getIssueCreationDate());
+ issue.setUpdateDate(ref.getIssueUpdateDate());
+ issue.setCloseDate(ref.getIssueCloseDate());
+
+ // non-persisted fields
+ issue.setNew(false);
+ issue.setEndOfLife(false);
+ issue.setOnDisabledRule(false);
+ issue.setSelectedAt(ref.getSelectedAt());
+
+ // fields to update with old values
+ issue.setActionPlanKey(ref.getActionPlanKey());
+ issue.setResolution(ref.getResolution());
+ issue.setStatus(ref.getStatus());
+ issue.setAssignee(ref.getAssignee());
+ issue.setAuthorLogin(ref.getAuthorLogin());
+ issue.setTags(ref.getTags());
+
+ if (ref.getIssueAttributes() != null) {
+ issue.setAttributes(KeyValueFormat.parse(ref.getIssueAttributes()));
+ }
+
+ // populate existing changelog
+ Collection<IssueChangeDto> issueChangeDtos = initialOpenIssues.selectChangelog(issue.key());
+ for (IssueChangeDto issueChangeDto : issueChangeDtos) {
+ issue.addChange(issueChangeDto.toFieldDiffs());
+ }
+
+ // fields to update with current values
+ if (ref.isManualSeverity()) {
+ issue.setManualSeverity(true);
+ issue.setSeverity(ref.getSeverity());
+ } else {
+ updater.setPastSeverity(issue, ref.getSeverity(), changeContext);
+ }
+ updater.setPastLine(issue, ref.getLine());
+ updater.setPastMessage(issue, ref.getMessage(), changeContext);
+ updater.setPastEffortToFix(issue, ref.getEffortToFix(), changeContext);
+ Long debtInMinutes = ref.getDebt();
+ Duration previousTechnicalDebt = debtInMinutes != null ? Duration.create(debtInMinutes) : null;
+ updater.setPastTechnicalDebt(issue, previousTechnicalDebt, changeContext);
+ updater.setPastProject(issue, ref.getProjectKey(), changeContext);
+ }
+ }
+
+ private void addUnmatched(Collection<PreviousIssue> unmatchedIssues, SourceHashHolder sourceHashHolder, Collection<DefaultIssue> issues) {
+ for (PreviousIssue unmatchedIssue : unmatchedIssues) {
+ IssueDto unmatchedDto = ((PreviousIssueFromDb) unmatchedIssue).getDto();
+ DefaultIssue unmatched = unmatchedDto.toDefaultIssue();
+ if (StringUtils.isNotBlank(unmatchedDto.getReporter()) && !Issue.STATUS_CLOSED.equals(unmatchedDto.getStatus())) {
+ relocateManualIssue(unmatched, unmatchedDto, sourceHashHolder);
+ }
+ updateUnmatchedIssue(unmatched, false /* manual issues can be kept open */);
+ issues.add(unmatched);
+ }
+ }
+
+ private void addIssuesOnDeletedComponents(Collection<DefaultIssue> issues) {
+ for (IssueDto deadDto : initialOpenIssues.selectAllIssues()) {
+ DefaultIssue dead = deadDto.toDefaultIssue();
+ updateUnmatchedIssue(dead, true);
+ issues.add(dead);
+ }
+ initialOpenIssues.clear();
+ }
+
+ private void updateUnmatchedIssue(DefaultIssue issue, boolean forceEndOfLife) {
+ issue.setNew(false);
+
+ boolean manualIssue = !Strings.isNullOrEmpty(issue.reporter());
+ Rule rule = ruleFinder.findByKey(issue.ruleKey());
+ if (manualIssue) {
+ // Manual rules are not declared in Quality profiles, so no need to check ActiveRule
+ boolean isRemovedRule = rule == null || Rule.STATUS_REMOVED.equals(rule.getStatus());
+ issue.setEndOfLife(forceEndOfLife || isRemovedRule);
+ issue.setOnDisabledRule(isRemovedRule);
+ } else {
+ ActiveRule activeRule = rulesProfile.getActiveRule(issue.ruleKey().repository(), issue.ruleKey().rule());
+ issue.setEndOfLife(true);
+ issue.setOnDisabledRule(activeRule == null || rule == null || Rule.STATUS_REMOVED.equals(rule.getStatus()));
+ }
+ }
+
+ private void relocateManualIssue(DefaultIssue newIssue, IssueDto oldIssue, SourceHashHolder sourceHashHolder) {
+ LOG.debug("Trying to relocate manual issue {}", oldIssue.getKee());
+
+ Integer previousLine = oldIssue.getLine();
+ if (previousLine == null) {
+ LOG.debug("Cannot relocate issue at resource level");
+ return;
+ }
+
+ Collection<Integer> newLinesWithSameHash = sourceHashHolder.getNewLinesMatching(previousLine);
+ LOG.debug("Found the following lines with same hash: {}", newLinesWithSameHash);
+ if (newLinesWithSameHash.isEmpty()) {
+ if (previousLine > sourceHashHolder.getHashedSource().length()) {
+ LOG.debug("Old issue line {} is out of new source, closing and removing line number", previousLine);
+ newIssue.setLine(null);
+ updater.setStatus(newIssue, Issue.STATUS_CLOSED, changeContext);
+ updater.setResolution(newIssue, Issue.RESOLUTION_REMOVED, changeContext);
+ updater.setPastLine(newIssue, previousLine);
+ updater.setPastMessage(newIssue, oldIssue.getMessage(), changeContext);
+ updater.setPastEffortToFix(newIssue, oldIssue.getEffortToFix(), changeContext);
+ }
+ } else if (newLinesWithSameHash.size() == 1) {
+ Integer newLine = newLinesWithSameHash.iterator().next();
+ LOG.debug("Relocating issue to line {}", newLine);
+
+ newIssue.setLine(newLine);
+ updater.setPastLine(newIssue, previousLine);
+ updater.setPastMessage(newIssue, oldIssue.getMessage(), changeContext);
+ updater.setPastEffortToFix(newIssue, oldIssue.getEffortToFix(), changeContext);
+ }
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingResult.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingResult.java
new file mode 100644
index 00000000000..f7b4e8d119e
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/IssueTrackingResult.java
@@ -0,0 +1,111 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.rule.RuleKey;
+
+import javax.annotation.Nullable;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+class IssueTrackingResult {
+ private final Map<String, PreviousIssue> unmatchedByKey = new HashMap<>();
+ private final Map<RuleKey, Map<String, PreviousIssue>> unmatchedByRuleAndKey = new HashMap<>();
+ private final Map<RuleKey, Map<Integer, Multimap<String, PreviousIssue>>> unmatchedByRuleAndLineAndChecksum = new HashMap<>();
+ private final Map<DefaultIssue, PreviousIssue> matched = Maps.newIdentityHashMap();
+
+ Collection<PreviousIssue> unmatched() {
+ return unmatchedByKey.values();
+ }
+
+ Map<String, PreviousIssue> unmatchedByKeyForRule(RuleKey ruleKey) {
+ return unmatchedByRuleAndKey.containsKey(ruleKey) ? unmatchedByRuleAndKey.get(ruleKey) : Collections.<String, PreviousIssue>emptyMap();
+ }
+
+ Collection<PreviousIssue> unmatchedForRuleAndForLineAndForChecksum(RuleKey ruleKey, @Nullable Integer line, @Nullable String checksum) {
+ if (!unmatchedByRuleAndLineAndChecksum.containsKey(ruleKey)) {
+ return Collections.emptyList();
+ }
+ Map<Integer, Multimap<String, PreviousIssue>> unmatchedForRule = unmatchedByRuleAndLineAndChecksum.get(ruleKey);
+ Integer lineNotNull = line != null ? line : 0;
+ if (!unmatchedForRule.containsKey(lineNotNull)) {
+ return Collections.emptyList();
+ }
+ Multimap<String, PreviousIssue> unmatchedForRuleAndLine = unmatchedForRule.get(lineNotNull);
+ String checksumNotNull = StringUtils.defaultString(checksum, "");
+ if (!unmatchedForRuleAndLine.containsKey(checksumNotNull)) {
+ return Collections.emptyList();
+ }
+ return unmatchedForRuleAndLine.get(checksumNotNull);
+ }
+
+ Collection<DefaultIssue> matched() {
+ return matched.keySet();
+ }
+
+ boolean isMatched(DefaultIssue issue) {
+ return matched.containsKey(issue);
+ }
+
+ PreviousIssue matching(DefaultIssue issue) {
+ return matched.get(issue);
+ }
+
+ void addUnmatched(PreviousIssue i) {
+ unmatchedByKey.put(i.key(), i);
+ RuleKey ruleKey = i.ruleKey();
+ if (!unmatchedByRuleAndKey.containsKey(ruleKey)) {
+ unmatchedByRuleAndKey.put(ruleKey, new HashMap<String, PreviousIssue>());
+ unmatchedByRuleAndLineAndChecksum.put(ruleKey, new HashMap<Integer, Multimap<String, PreviousIssue>>());
+ }
+ unmatchedByRuleAndKey.get(ruleKey).put(i.key(), i);
+ Map<Integer, Multimap<String, PreviousIssue>> unmatchedForRule = unmatchedByRuleAndLineAndChecksum.get(ruleKey);
+ Integer lineNotNull = lineNotNull(i);
+ if (!unmatchedForRule.containsKey(lineNotNull)) {
+ unmatchedForRule.put(lineNotNull, HashMultimap.<String, PreviousIssue>create());
+ }
+ Multimap<String, PreviousIssue> unmatchedForRuleAndLine = unmatchedForRule.get(lineNotNull);
+ String checksumNotNull = StringUtils.defaultString(i.checksum(), "");
+ unmatchedForRuleAndLine.put(checksumNotNull, i);
+ }
+
+ private Integer lineNotNull(PreviousIssue i) {
+ Integer line = i.line();
+ return line != null ? line : 0;
+ }
+
+ void setMatch(DefaultIssue issue, PreviousIssue matching) {
+ matched.put(issue, matching);
+ RuleKey ruleKey = matching.ruleKey();
+ unmatchedByRuleAndKey.get(ruleKey).remove(matching.key());
+ unmatchedByKey.remove(matching.key());
+ Integer lineNotNull = lineNotNull(matching);
+ String checksumNotNull = StringUtils.defaultString(matching.checksum(), "");
+ unmatchedByRuleAndLineAndChecksum.get(ruleKey).get(lineNotNull).get(checksumNotNull).remove(matching);
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java
new file mode 100644
index 00000000000..99542db8cb6
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/LocalIssueTracking.java
@@ -0,0 +1,227 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Lists;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.batch.rule.ActiveRule;
+import org.sonar.api.batch.rule.ActiveRules;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.ResourceUtils;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.index.BatchResource;
+import org.sonar.batch.index.ResourceCache;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.scan.LastLineHashes;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.workflow.IssueWorkflow;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+public class LocalIssueTracking implements BatchComponent {
+
+ private final IssueCache issueCache;
+ private final IssueTracking tracking;
+ private final LastLineHashes lastLineHashes;
+ private final IssueWorkflow workflow;
+ private final IssueUpdater updater;
+ private final IssueChangeContext changeContext;
+ private final ActiveRules activeRules;
+ private final InputPathCache inputPathCache;
+ private final ResourceCache resourceCache;
+ private final PreviousIssueRepository previousIssueCache;
+
+ public LocalIssueTracking(ResourceCache resourceCache, IssueCache issueCache, IssueTracking tracking,
+ LastLineHashes lastLineHashes, IssueWorkflow workflow, IssueUpdater updater,
+ ActiveRules activeRules, InputPathCache inputPathCache, PreviousIssueRepository previousIssueCache) {
+ this.resourceCache = resourceCache;
+ this.issueCache = issueCache;
+ this.tracking = tracking;
+ this.lastLineHashes = lastLineHashes;
+ this.workflow = workflow;
+ this.updater = updater;
+ this.inputPathCache = inputPathCache;
+ this.previousIssueCache = previousIssueCache;
+ this.changeContext = IssueChangeContext.createScan(((Project) resourceCache.getRoot().resource()).getAnalysisDate());
+ this.activeRules = activeRules;
+ }
+
+ public void execute() {
+ previousIssueCache.load();
+
+ for (BatchResource component : resourceCache.all()) {
+ trackIssues(component);
+ }
+ }
+
+ public void trackIssues(BatchResource component) {
+
+ Collection<DefaultIssue> issues = Lists.newArrayList();
+ for (Issue issue : issueCache.byComponent(component.resource().getEffectiveKey())) {
+ issues.add((DefaultIssue) issue);
+ }
+ issueCache.clear(component.resource().getEffectiveKey());
+ // issues = all the issues created by rule engines during this module scan and not excluded by filters
+
+ // all the issues that are not closed in db before starting this module scan, including manual issues
+ Collection<PreviousIssue> previousIssues = new ArrayList<>();
+ for (org.sonar.batch.protocol.input.issues.PreviousIssue previousIssue : previousIssueCache.byComponent(component)) {
+ previousIssues.add(new PreviousIssueFromWs(previousIssue));
+ }
+
+ SourceHashHolder sourceHashHolder = null;
+ if (ResourceUtils.isFile(component.resource())) {
+ File sonarFile = (File) component.resource();
+ InputFile file = inputPathCache.getFile(component.parent().parent().resource().getEffectiveKey(), sonarFile.getPath());
+ if (file == null) {
+ throw new IllegalStateException("Resource " + component.resource() + " was not found in InputPath cache");
+ }
+ sourceHashHolder = new SourceHashHolder((DefaultInputFile) file, lastLineHashes);
+ }
+
+ IssueTrackingResult trackingResult = tracking.track(sourceHashHolder, previousIssues, issues);
+
+ // unmatched = issues that have been resolved + issues on disabled/removed rules + manual issues
+ addUnmatched(trackingResult.unmatched(), sourceHashHolder, issues);
+
+ mergeMatched(trackingResult);
+
+ if (ResourceUtils.isRootProject(component.resource())) {
+ // issues that relate to deleted components
+ addIssuesOnDeletedComponents(issues);
+ }
+
+ for (DefaultIssue issue : issues) {
+ workflow.doAutomaticTransition(issue, changeContext);
+ issueCache.put(issue);
+ }
+ }
+
+ @VisibleForTesting
+ protected void mergeMatched(IssueTrackingResult result) {
+ for (DefaultIssue issue : result.matched()) {
+ org.sonar.batch.protocol.input.issues.PreviousIssue ref = ((PreviousIssueFromWs) result.matching(issue)).getDto();
+
+ // invariant fields
+ issue.setKey(ref.key());
+
+ // non-persisted fields
+ issue.setNew(false);
+ issue.setEndOfLife(false);
+ issue.setOnDisabledRule(false);
+
+ // fields to update with old values
+ issue.setResolution(ref.resolution());
+ issue.setStatus(ref.status());
+ issue.setAssignee(ref.assigneeLogin());
+
+ String overriddenSeverity = ref.overriddenSeverity();
+ if (overriddenSeverity != null) {
+ // Severity overriden by user
+ issue.setSeverity(overriddenSeverity);
+ }
+ }
+ }
+
+ private void addUnmatched(Collection<PreviousIssue> unmatchedIssues, SourceHashHolder sourceHashHolder, Collection<DefaultIssue> issues) {
+ for (PreviousIssue unmatchedIssue : unmatchedIssues) {
+ org.sonar.batch.protocol.input.issues.PreviousIssue unmatchedPreviousIssue = ((PreviousIssueFromWs) unmatchedIssue).getDto();
+ ActiveRule activeRule = activeRules.find(unmatchedIssue.ruleKey());
+ DefaultIssue unmatched = toUnmatchedIssue(unmatchedPreviousIssue);
+ if (activeRule != null && !Issue.STATUS_CLOSED.equals(unmatchedPreviousIssue.status())) {
+ relocateManualIssue(unmatched, unmatchedIssue, sourceHashHolder);
+ }
+ updateUnmatchedIssue(unmatched, false /* manual issues can be kept open */);
+ issues.add(unmatched);
+ }
+ }
+
+ private void addIssuesOnDeletedComponents(Collection<DefaultIssue> issues) {
+ for (org.sonar.batch.protocol.input.issues.PreviousIssue previous : previousIssueCache.issuesOnMissingComponents()) {
+ DefaultIssue dead = toUnmatchedIssue(previous);
+ updateUnmatchedIssue(dead, true);
+ issues.add(dead);
+ }
+ }
+
+ private DefaultIssue toUnmatchedIssue(org.sonar.batch.protocol.input.issues.PreviousIssue previous) {
+ DefaultIssue issue = new DefaultIssue();
+ issue.setKey(previous.key());
+ issue.setStatus(previous.status());
+ issue.setResolution(previous.resolution());
+ issue.setMessage(previous.message());
+ issue.setLine(previous.line());
+ String overriddenSeverity = previous.overriddenSeverity();
+ if (overriddenSeverity != null) {
+ issue.setSeverity(overriddenSeverity);
+ } else {
+ ActiveRule activeRule = activeRules.find(RuleKey.of(previous.ruleRepo(), previous.ruleKey()));
+ if (activeRule != null) {
+ // FIXME if rule was removed we can't guess what was the severity of the issue
+ issue.setSeverity(activeRule.severity());
+ }
+ }
+ issue.setAssignee(previous.assigneeLogin());
+ issue.setComponentKey(previous.componentKey());
+ issue.setManualSeverity(overriddenSeverity != null);
+ issue.setRuleKey(RuleKey.of(previous.ruleRepo(), previous.ruleKey()));
+ issue.setNew(false);
+ return issue;
+ }
+
+ private void updateUnmatchedIssue(DefaultIssue issue, boolean forceEndOfLife) {
+ ActiveRule activeRule = activeRules.find(issue.ruleKey());
+ boolean isRemovedRule = activeRule == null;
+ issue.setEndOfLife(forceEndOfLife || isRemovedRule);
+ issue.setOnDisabledRule(isRemovedRule);
+ }
+
+ private void relocateManualIssue(DefaultIssue newIssue, PreviousIssue oldIssue, SourceHashHolder sourceHashHolder) {
+ Integer previousLine = oldIssue.line();
+ if (previousLine == null) {
+ return;
+ }
+
+ Collection<Integer> newLinesWithSameHash = sourceHashHolder.getNewLinesMatching(previousLine);
+ if (newLinesWithSameHash.isEmpty()) {
+ if (previousLine > sourceHashHolder.getHashedSource().length()) {
+ newIssue.setLine(null);
+ updater.setStatus(newIssue, Issue.STATUS_CLOSED, changeContext);
+ updater.setResolution(newIssue, Issue.RESOLUTION_REMOVED, changeContext);
+ updater.setPastLine(newIssue, previousLine);
+ updater.setPastMessage(newIssue, oldIssue.message(), changeContext);
+ }
+ } else if (newLinesWithSameHash.size() == 1) {
+ Integer newLine = newLinesWithSameHash.iterator().next();
+ newIssue.setLine(newLine);
+ updater.setPastLine(newIssue, previousLine);
+ updater.setPastMessage(newIssue, oldIssue.message(), changeContext);
+ }
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssue.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssue.java
new file mode 100644
index 00000000000..dcef8caaa5a
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssue.java
@@ -0,0 +1,47 @@
+/*
+ * 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.issue.tracking;
+
+import org.sonar.api.rule.RuleKey;
+
+import javax.annotation.CheckForNull;
+
+public interface PreviousIssue {
+
+ String key();
+
+ RuleKey ruleKey();
+
+ /**
+ * Null for issue with no line
+ */
+ @CheckForNull
+ String checksum();
+
+ /**
+ * Global issues have no line
+ */
+ @CheckForNull
+ Integer line();
+
+ @CheckForNull
+ String message();
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromDb.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromDb.java
new file mode 100644
index 00000000000..f20f420def4
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromDb.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.api.rule.RuleKey;
+import org.sonar.core.issue.db.IssueDto;
+
+public class PreviousIssueFromDb implements PreviousIssue {
+
+ private IssueDto dto;
+
+ public PreviousIssueFromDb(IssueDto dto) {
+ this.dto = dto;
+ }
+
+ public IssueDto getDto() {
+ return dto;
+ }
+
+ @Override
+ public String key() {
+ return dto.getKee();
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return dto.getRuleKey();
+ }
+
+ @Override
+ public String checksum() {
+ return dto.getChecksum();
+ }
+
+ @Override
+ public Integer line() {
+ return dto.getLine();
+ }
+
+ @Override
+ public String message() {
+ return dto.getMessage();
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromWs.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromWs.java
new file mode 100644
index 00000000000..a9a435abb8c
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueFromWs.java
@@ -0,0 +1,61 @@
+/*
+ * 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.issue.tracking;
+
+import org.sonar.api.rule.RuleKey;
+
+public class PreviousIssueFromWs implements PreviousIssue {
+
+ private org.sonar.batch.protocol.input.issues.PreviousIssue dto;
+
+ public PreviousIssueFromWs(org.sonar.batch.protocol.input.issues.PreviousIssue dto) {
+ this.dto = dto;
+ }
+
+ public org.sonar.batch.protocol.input.issues.PreviousIssue getDto() {
+ return dto;
+ }
+
+ @Override
+ public String key() {
+ return dto.key();
+ }
+
+ @Override
+ public RuleKey ruleKey() {
+ return RuleKey.of(dto.ruleRepo(), dto.ruleKey());
+ }
+
+ @Override
+ public String checksum() {
+ return dto.checksum();
+ }
+
+ @Override
+ public Integer line() {
+ return dto.line();
+ }
+
+ @Override
+ public String message() {
+ return dto.message();
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueRepository.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueRepository.java
new file mode 100644
index 00000000000..6bad06cf818
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/PreviousIssueRepository.java
@@ -0,0 +1,84 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.base.Function;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.api.BatchComponent;
+import org.sonar.api.batch.InstantiationStrategy;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.api.utils.TimeProfiler;
+import org.sonar.batch.index.BatchResource;
+import org.sonar.batch.index.Cache;
+import org.sonar.batch.index.Caches;
+import org.sonar.batch.index.ResourceCache;
+import org.sonar.batch.protocol.input.issues.PreviousIssue;
+import org.sonar.batch.repository.PreviousIssuesLoader;
+
+@InstantiationStrategy(InstantiationStrategy.PER_BATCH)
+public class PreviousIssueRepository implements BatchComponent {
+
+ private static final Logger LOG = LoggerFactory.getLogger(PreviousIssueRepository.class);
+
+ private final Caches caches;
+ private Cache<PreviousIssue> issuesCache;
+ private final PreviousIssuesLoader previousIssuesLoader;
+ private final ProjectReactor reactor;
+ private final ResourceCache resourceCache;
+
+ public PreviousIssueRepository(Caches caches, PreviousIssuesLoader previousIssuesLoader, ProjectReactor reactor, ResourceCache resourceCache) {
+ this.caches = caches;
+ this.previousIssuesLoader = previousIssuesLoader;
+ this.reactor = reactor;
+ this.resourceCache = resourceCache;
+ }
+
+ public void load() {
+ TimeProfiler profiler = new TimeProfiler(LOG).start("Load previous issues");
+ try {
+ this.issuesCache = caches.createCache("previousIssues");
+ previousIssuesLoader.load(reactor, new Function<PreviousIssue, Void>() {
+
+ @Override
+ public Void apply(PreviousIssue issue) {
+ String componentKey = issue.componentKey();
+ BatchResource r = resourceCache.get(componentKey);
+ if (r == null) {
+ // Deleted resource
+ issuesCache.put(0, issue.key(), issue);
+ }
+ issuesCache.put(r.batchId(), issue.key(), issue);
+ return null;
+ }
+ });
+ } finally {
+ profiler.stop();
+ }
+ }
+
+ public Iterable<PreviousIssue> byComponent(BatchResource component) {
+ return issuesCache.values(component.batchId());
+ }
+
+ public Iterable<PreviousIssue> issuesOnMissingComponents() {
+ return issuesCache.values(0);
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java
new file mode 100644
index 00000000000..84beecd42ed
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/RollingFileHashes.java
@@ -0,0 +1,89 @@
+/*
+ * 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.issue.tracking;
+
+/**
+ * Compute hashes of block around each line
+ */
+public class RollingFileHashes {
+
+ final int[] rollingHashes;
+
+ public static RollingFileHashes create(FileHashes hashes, int halfBlockSize) {
+ int size = hashes.length();
+ int[] rollingHashes = new int[size];
+
+ RollingHashCalculator hashCalulator = new RollingHashCalculator(halfBlockSize * 2 + 1);
+ for (int i = 1; i <= Math.min(size, halfBlockSize + 1); i++) {
+ hashCalulator.add(hashes.getHash(i).hashCode());
+ }
+ for (int i = 1; i <= size; i++) {
+ rollingHashes[i - 1] = hashCalulator.getHash();
+ if (i - halfBlockSize > 0) {
+ hashCalulator.remove(hashes.getHash(i - halfBlockSize).hashCode());
+ }
+ if (i + 1 + halfBlockSize <= size) {
+ hashCalulator.add(hashes.getHash(i + 1 + halfBlockSize).hashCode());
+ } else {
+ hashCalulator.add(0);
+ }
+ }
+
+ return new RollingFileHashes(rollingHashes);
+ }
+
+ public int getHash(int line) {
+ return rollingHashes[line - 1];
+ }
+
+ private RollingFileHashes(int[] hashes) {
+ this.rollingHashes = hashes;
+ }
+
+ private static class RollingHashCalculator {
+
+ private static final int PRIME_BASE = 31;
+
+ private final int power;
+ private int hash;
+
+ public RollingHashCalculator(int size) {
+ int pow = 1;
+ for (int i = 0; i < size - 1; i++) {
+ pow = pow * PRIME_BASE;
+ }
+ this.power = pow;
+ }
+
+ public void add(int value) {
+ hash = hash * PRIME_BASE + value;
+ }
+
+ public void remove(int value) {
+ hash = hash - power * value;
+ }
+
+ public int getHash() {
+ return hash;
+ }
+
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.java
new file mode 100644
index 00000000000..38f0af7745a
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/SourceHashHolder.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.issue.tracking;
+
+import com.google.common.collect.ImmutableSet;
+import org.sonar.api.batch.fs.InputFile.Status;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.batch.scan.LastLineHashes;
+
+import javax.annotation.CheckForNull;
+
+import java.util.Collection;
+
+public class SourceHashHolder {
+
+ private final LastLineHashes lastSnapshots;
+
+ private FileHashes hashedReference;
+ private FileHashes hashedSource;
+ private DefaultInputFile inputFile;
+
+ public SourceHashHolder(DefaultInputFile inputFile, LastLineHashes lastSnapshots) {
+ this.inputFile = inputFile;
+ this.lastSnapshots = lastSnapshots;
+ }
+
+ private void initHashes() {
+ if (hashedSource == null) {
+ hashedSource = FileHashes.create(inputFile.lineHashes());
+ Status status = inputFile.status();
+ if (status == Status.ADDED) {
+ hashedReference = null;
+ } else if (status == Status.SAME) {
+ hashedReference = hashedSource;
+ } else {
+ String[] lineHashes = lastSnapshots.getLineHashes(inputFile.key());
+ hashedReference = lineHashes != null ? FileHashes.create(lineHashes) : null;
+ }
+ }
+ }
+
+ @CheckForNull
+ public FileHashes getHashedReference() {
+ initHashes();
+ return hashedReference;
+ }
+
+ public FileHashes getHashedSource() {
+ initHashes();
+ return hashedSource;
+ }
+
+ public Collection<Integer> getNewLinesMatching(Integer originLine) {
+ FileHashes reference = getHashedReference();
+ if (reference == null) {
+ return ImmutableSet.of();
+ } else {
+ return getHashedSource().getLinesForHash(reference.getHash(originLine));
+ }
+ }
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/package-info.java
new file mode 100644
index 00000000000..8df05a16129
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/issue/tracking/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.issue.tracking;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/mediumtest/BatchMediumTester.java b/sonar-batch/src/main/java/org/sonar/batch/mediumtest/BatchMediumTester.java
index 1afdd61be6d..27bbc64ef0a 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/mediumtest/BatchMediumTester.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/mediumtest/BatchMediumTester.java
@@ -19,6 +19,7 @@
*/
package org.sonar.batch.mediumtest;
+import com.google.common.base.Function;
import org.apache.commons.io.Charsets;
import org.sonar.api.SonarPlugin;
import org.sonar.api.batch.bootstrap.ProjectReactor;
@@ -32,9 +33,11 @@ import org.sonar.batch.bootstrapper.Batch;
import org.sonar.batch.bootstrapper.EnvironmentInformation;
import org.sonar.batch.protocol.input.ActiveRule;
import org.sonar.batch.protocol.input.GlobalReferentials;
-import org.sonar.batch.protocol.input.ProjectReferentials;
-import org.sonar.batch.referential.GlobalReferentialsLoader;
-import org.sonar.batch.referential.ProjectReferentialsLoader;
+import org.sonar.batch.protocol.input.ProjectRepository;
+import org.sonar.batch.protocol.input.issues.PreviousIssue;
+import org.sonar.batch.repository.GlobalReferentialsLoader;
+import org.sonar.batch.repository.PreviousIssuesLoader;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
import org.sonar.core.plugins.DefaultPluginMetadata;
import org.sonar.core.plugins.RemotePlugin;
@@ -65,6 +68,7 @@ public class BatchMediumTester {
private final FakeGlobalReferentialsLoader globalRefProvider = new FakeGlobalReferentialsLoader();
private final FakeProjectReferentialsLoader projectRefProvider = new FakeProjectReferentialsLoader();
private final FakePluginsReferential pluginsReferential = new FakePluginsReferential();
+ private final FakePreviousIssuesLoader previousIssues = new FakePreviousIssuesLoader();
private final Map<String, String> bootstrapProperties = new HashMap<String, String>();
public BatchMediumTester build() {
@@ -114,6 +118,11 @@ public class BatchMediumTester {
return this;
}
+ public BatchMediumTesterBuilder addPreviousIssue(PreviousIssue issue) {
+ previousIssues.getPreviousIssues().add(issue);
+ return this;
+ }
+
}
public void start() {
@@ -132,6 +141,7 @@ public class BatchMediumTester {
builder.pluginsReferential,
builder.globalRefProvider,
builder.projectRefProvider,
+ builder.previousIssues,
new DefaultDebtModel())
.setBootstrapProperties(builder.bootstrapProperties)
.build();
@@ -217,12 +227,12 @@ public class BatchMediumTester {
}
}
- private static class FakeProjectReferentialsLoader implements ProjectReferentialsLoader {
+ private static class FakeProjectReferentialsLoader implements ProjectRepositoriesLoader {
- private ProjectReferentials ref = new ProjectReferentials();
+ private ProjectRepository ref = new ProjectRepository();
@Override
- public ProjectReferentials load(ProjectReactor reactor, TaskProperties taskProperties) {
+ public ProjectRepository load(ProjectReactor reactor, TaskProperties taskProperties) {
return ref;
}
@@ -272,4 +282,22 @@ public class BatchMediumTester {
}
+ private static class FakePreviousIssuesLoader implements PreviousIssuesLoader {
+
+ List<PreviousIssue> previousIssues = new ArrayList<>();
+
+ public List<PreviousIssue> getPreviousIssues() {
+ return previousIssues;
+ }
+
+ @Override
+ public void load(ProjectReactor reactor, Function<PreviousIssue, Void> consumer) {
+ for (PreviousIssue previousIssue : previousIssues) {
+ consumer.apply(previousIssue);
+ }
+
+ }
+
+ }
+
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/phases/PreviewPhaseExecutor.java b/sonar-batch/src/main/java/org/sonar/batch/phases/PreviewPhaseExecutor.java
index de4bd65a3b4..80c2fe8c078 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/phases/PreviewPhaseExecutor.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/phases/PreviewPhaseExecutor.java
@@ -25,6 +25,7 @@ import org.sonar.batch.events.BatchStepEvent;
import org.sonar.batch.events.EventBus;
import org.sonar.batch.index.DefaultIndex;
import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
+import org.sonar.batch.issue.tracking.LocalIssueTracking;
import org.sonar.batch.rule.QProfileVerifier;
import org.sonar.batch.scan.filesystem.DefaultModuleFileSystem;
import org.sonar.batch.scan.filesystem.FileSystemLogger;
@@ -46,13 +47,14 @@ public final class PreviewPhaseExecutor implements PhaseExecutor {
private final QProfileVerifier profileVerifier;
private final IssueExclusionsLoader issueExclusionsLoader;
private final IssuesReports issuesReport;
+ private final LocalIssueTracking localIssueTracking;
public PreviewPhaseExecutor(Phases phases,
MavenPluginsConfigurator mavenPluginsConfigurator, InitializersExecutor initializersExecutor,
SensorsExecutor sensorsExecutor,
SensorContext sensorContext, DefaultIndex index,
EventBus eventBus, ProjectInitializer pi, FileSystemLogger fsLogger, IssuesReports jsonReport, DefaultModuleFileSystem fs, QProfileVerifier profileVerifier,
- IssueExclusionsLoader issueExclusionsLoader) {
+ IssueExclusionsLoader issueExclusionsLoader, LocalIssueTracking localIssueTracking) {
this.phases = phases;
this.mavenPluginsConfigurator = mavenPluginsConfigurator;
this.initializersExecutor = initializersExecutor;
@@ -66,6 +68,7 @@ public final class PreviewPhaseExecutor implements PhaseExecutor {
this.fs = fs;
this.profileVerifier = profileVerifier;
this.issueExclusionsLoader = issueExclusionsLoader;
+ this.localIssueTracking = localIssueTracking;
}
/**
@@ -95,6 +98,9 @@ public final class PreviewPhaseExecutor implements PhaseExecutor {
}
if (module.isRoot()) {
+
+ localIssueTracking();
+
issuesReport();
}
@@ -102,6 +108,13 @@ public final class PreviewPhaseExecutor implements PhaseExecutor {
eventBus.fireEvent(new ProjectAnalysisEvent(module, false));
}
+ private void localIssueTracking() {
+ String stepName = "Local Issue Tracking";
+ eventBus.fireEvent(new BatchStepEvent(stepName, true));
+ localIssueTracking.execute();
+ eventBus.fireEvent(new BatchStepEvent(stepName, false));
+ }
+
private void issuesReport() {
String stepName = "Issues Reports";
eventBus.fireEvent(new BatchStepEvent(stepName, true));
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/DefaultGlobalReferentialsLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultGlobalReferentialsLoader.java
index 17fd6821a6c..91012f9a6e3 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/DefaultGlobalReferentialsLoader.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultGlobalReferentialsLoader.java
@@ -17,7 +17,7 @@
* 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.referential;
+package org.sonar.batch.repository;
import org.sonar.batch.bootstrap.ServerClient;
import org.sonar.batch.protocol.input.GlobalReferentials;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultPreviousIssuesLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultPreviousIssuesLoader.java
new file mode 100644
index 00000000000..d2831e4a8f2
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultPreviousIssuesLoader.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.repository;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.io.InputSupplier;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.batch.bootstrap.ServerClient;
+import org.sonar.batch.protocol.input.issues.PreviousIssue;
+import org.sonar.batch.protocol.input.issues.PreviousIssueHelper;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+public class DefaultPreviousIssuesLoader implements PreviousIssuesLoader {
+
+ private final ServerClient serverClient;
+
+ public DefaultPreviousIssuesLoader(ServerClient serverClient) {
+ this.serverClient = serverClient;
+ }
+
+ @Override
+ public void load(ProjectReactor reactor, Function<PreviousIssue, Void> consumer) {
+ InputSupplier<InputStream> request = serverClient.doRequest("/batch/issues?key=" + ServerClient.encodeForUrl(reactor.getRoot().getKeyWithBranch()), "GET", null);
+ try (InputStream is = request.getInput(); Reader reader = new InputStreamReader(is, Charsets.UTF_8)) {
+ for (PreviousIssue issue : PreviousIssueHelper.getIssues(reader)) {
+ consumer.apply(issue);
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Unable to get previous issues", e);
+ }
+ }
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/DefaultProjectReferentialsLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultProjectReferentialsLoader.java
index 90a10bc1191..de45aeaf428 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/DefaultProjectReferentialsLoader.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/DefaultProjectReferentialsLoader.java
@@ -17,7 +17,7 @@
* 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.referential;
+package org.sonar.batch.repository;
import com.google.common.collect.Maps;
import org.slf4j.Logger;
@@ -35,7 +35,7 @@ import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.ServerClient;
import org.sonar.batch.bootstrap.TaskProperties;
import org.sonar.batch.protocol.input.FileData;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import org.sonar.batch.rule.ModuleQProfiles;
import javax.annotation.CheckForNull;
@@ -48,7 +48,7 @@ import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
-public class DefaultProjectReferentialsLoader implements ProjectReferentialsLoader {
+public class DefaultProjectReferentialsLoader implements ProjectRepositoriesLoader {
private static final Logger LOG = LoggerFactory.getLogger(DefaultProjectReferentialsLoader.class);
@@ -71,7 +71,7 @@ public class DefaultProjectReferentialsLoader implements ProjectReferentialsLoad
}
@Override
- public ProjectReferentials load(ProjectReactor reactor, TaskProperties taskProperties) {
+ public ProjectRepository load(ProjectReactor reactor, TaskProperties taskProperties) {
String projectKey = reactor.getRoot().getKeyWithBranch();
String url = BATCH_PROJECT_URL + "?key=" + ServerClient.encodeForUrl(projectKey);
if (taskProperties.properties().containsKey(ModuleQProfiles.SONAR_PROFILE_PROP)) {
@@ -80,7 +80,7 @@ public class DefaultProjectReferentialsLoader implements ProjectReferentialsLoad
url += "&profile=" + ServerClient.encodeForUrl(taskProperties.properties().get(ModuleQProfiles.SONAR_PROFILE_PROP));
}
url += "&preview=" + analysisMode.isPreview();
- ProjectReferentials ref = ProjectReferentials.fromJson(serverClient.request(url));
+ ProjectRepository ref = ProjectRepository.fromJson(serverClient.request(url));
if (session != null) {
for (ProjectDefinition module : reactor.getProjects()) {
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsLoader.java
index d355867d433..1dbbb520882 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsLoader.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsLoader.java
@@ -17,7 +17,7 @@
* 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.referential;
+package org.sonar.batch.repository;
import org.sonar.batch.protocol.input.GlobalReferentials;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsProvider.java b/sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsProvider.java
index 5e10e6c61e4..d3c99dc6552 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/GlobalReferentialsProvider.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/GlobalReferentialsProvider.java
@@ -17,7 +17,7 @@
* 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.referential;
+package org.sonar.batch.repository;
import org.picocontainer.injectors.ProviderAdapter;
import org.slf4j.Logger;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/repository/PreviousIssuesLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/PreviousIssuesLoader.java
new file mode 100644
index 00000000000..fe706da3e73
--- /dev/null
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/PreviousIssuesLoader.java
@@ -0,0 +1,30 @@
+/*
+ * 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.repository;
+
+import com.google.common.base.Function;
+import org.sonar.api.batch.bootstrap.ProjectReactor;
+import org.sonar.batch.protocol.input.issues.PreviousIssue;
+
+public interface PreviousIssuesLoader {
+
+ void load(ProjectReactor reactor, Function<PreviousIssue, Void> consumer);
+
+}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsLoader.java b/sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java
index e4bad9f1e13..75caa6f3b4f 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsLoader.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesLoader.java
@@ -17,14 +17,14 @@
* 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.referential;
+package org.sonar.batch.repository;
import org.sonar.api.batch.bootstrap.ProjectReactor;
import org.sonar.batch.bootstrap.TaskProperties;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
-public interface ProjectReferentialsLoader {
+public interface ProjectRepositoriesLoader {
- ProjectReferentials load(ProjectReactor reactor, TaskProperties taskProperties);
+ ProjectRepository load(ProjectReactor reactor, TaskProperties taskProperties);
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsProvider.java b/sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java
index 03486bfdcee..c9cf9e40537 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/ProjectReferentialsProvider.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/ProjectRepositoriesProvider.java
@@ -17,7 +17,7 @@
* 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.referential;
+package org.sonar.batch.repository;
import org.picocontainer.injectors.ProviderAdapter;
import org.slf4j.Logger;
@@ -25,15 +25,15 @@ import org.slf4j.LoggerFactory;
import org.sonar.api.batch.bootstrap.ProjectReactor;
import org.sonar.api.utils.TimeProfiler;
import org.sonar.batch.bootstrap.TaskProperties;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
-public class ProjectReferentialsProvider extends ProviderAdapter {
+public class ProjectRepositoriesProvider extends ProviderAdapter {
- private static final Logger LOG = LoggerFactory.getLogger(ProjectReferentialsProvider.class);
+ private static final Logger LOG = LoggerFactory.getLogger(ProjectRepositoriesProvider.class);
- private ProjectReferentials projectReferentials;
+ private ProjectRepository projectReferentials;
- public ProjectReferentials provide(ProjectReferentialsLoader loader, ProjectReactor reactor, TaskProperties taskProps) {
+ public ProjectRepository provide(ProjectRepositoriesLoader loader, ProjectReactor reactor, TaskProperties taskProps) {
if (projectReferentials == null) {
TimeProfiler profiler = new TimeProfiler(LOG).start("Load project referentials");
try {
diff --git a/sonar-batch/src/main/java/org/sonar/batch/referential/package-info.java b/sonar-batch/src/main/java/org/sonar/batch/repository/package-info.java
index 1a50ff50cdf..353f019b08f 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/referential/package-info.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/repository/package-info.java
@@ -18,6 +18,6 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
-package org.sonar.batch.referential;
+package org.sonar.batch.repository;
import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java b/sonar-batch/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java
index a539b744073..4ae65303f0e 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/rule/ActiveRulesProvider.java
@@ -25,26 +25,26 @@ import org.sonar.api.batch.rule.internal.ActiveRulesBuilder;
import org.sonar.api.batch.rule.internal.NewActiveRule;
import org.sonar.api.rule.RuleKey;
import org.sonar.batch.protocol.input.ActiveRule;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import java.util.Map.Entry;
/**
* Loads the rules that are activated on the Quality profiles
- * used by the current module and build {@link org.sonar.api.batch.rule.ActiveRules}.
+ * used by the current project and build {@link org.sonar.api.batch.rule.ActiveRules}.
*/
public class ActiveRulesProvider extends ProviderAdapter {
private ActiveRules singleton = null;
- public ActiveRules provide(ProjectReferentials ref) {
+ public ActiveRules provide(ProjectRepository ref) {
if (singleton == null) {
singleton = load(ref);
}
return singleton;
}
- private ActiveRules load(ProjectReferentials ref) {
+ private ActiveRules load(ProjectRepository ref) {
ActiveRulesBuilder builder = new ActiveRulesBuilder();
for (ActiveRule activeRule : ref.activeRules()) {
NewActiveRule newActiveRule = builder.create(RuleKey.of(activeRule.repositoryKey(), activeRule.ruleKey()));
diff --git a/sonar-batch/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java b/sonar-batch/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java
index dba6cd74a9b..badcc7878c6 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/rule/ModuleQProfiles.java
@@ -21,7 +21,7 @@ package org.sonar.batch.rule;
import com.google.common.collect.ImmutableMap;
import org.sonar.api.BatchComponent;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import javax.annotation.CheckForNull;
@@ -36,7 +36,7 @@ public class ModuleQProfiles implements BatchComponent {
public static final String SONAR_PROFILE_PROP = "sonar.profile";
private final Map<String, QProfile> byLanguage;
- public ModuleQProfiles(ProjectReferentials ref) {
+ public ModuleQProfiles(ProjectRepository ref) {
ImmutableMap.Builder<String, QProfile> builder = ImmutableMap.builder();
for (org.sonar.batch.protocol.input.QProfile qProfile : ref.qProfiles()) {
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
index 59dc45098d5..c54806dd347 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleScanContainer.java
@@ -19,8 +19,6 @@
*/
package org.sonar.batch.scan;
-import org.sonar.batch.sensor.AnalyzerOptimizer;
-
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.BatchComponent;
@@ -59,6 +57,9 @@ import org.sonar.batch.issue.ignore.pattern.IssueExclusionPatternInitializer;
import org.sonar.batch.issue.ignore.pattern.IssueInclusionPatternInitializer;
import org.sonar.batch.issue.ignore.scanner.IssueExclusionsLoader;
import org.sonar.batch.issue.ignore.scanner.IssueExclusionsRegexpScanner;
+import org.sonar.batch.issue.tracking.InitialOpenIssuesSensor;
+import org.sonar.batch.issue.tracking.IssueHandlers;
+import org.sonar.batch.issue.tracking.IssueTrackingDecorator;
import org.sonar.batch.language.LanguageDistributionDecorator;
import org.sonar.batch.phases.DecoratorsExecutor;
import org.sonar.batch.phases.DefaultPhaseExecutor;
@@ -75,7 +76,6 @@ import org.sonar.batch.qualitygate.QualityGateVerifier;
import org.sonar.batch.report.ComponentsPublisher;
import org.sonar.batch.report.IssuesPublisher;
import org.sonar.batch.report.PublishReportJob;
-import org.sonar.batch.rule.ActiveRulesProvider;
import org.sonar.batch.rule.ModuleQProfiles;
import org.sonar.batch.rule.QProfileDecorator;
import org.sonar.batch.rule.QProfileEventsDecorator;
@@ -97,6 +97,7 @@ import org.sonar.batch.scan.filesystem.ProjectFileSystemAdapter;
import org.sonar.batch.scan.filesystem.StatusDetectionFactory;
import org.sonar.batch.scan.maven.MavenPluginsConfigurator;
import org.sonar.batch.scan.report.IssuesReports;
+import org.sonar.batch.sensor.AnalyzerOptimizer;
import org.sonar.batch.sensor.DefaultSensorContext;
import org.sonar.batch.sensor.DefaultSensorStorage;
import org.sonar.batch.sensor.coverage.CoverageExclusions;
@@ -185,7 +186,6 @@ public class ModuleScanContainer extends ComponentContainer {
// rules
ModuleQProfiles.class,
- new ActiveRulesProvider(),
new RulesProfileProvider(),
QProfileSensor.class,
QProfileDecorator.class,
@@ -229,6 +229,11 @@ public class ModuleScanContainer extends ComponentContainer {
SqaleRatingDecorator.class,
SqaleRatingSettings.class,
+ // Issue tracking
+ IssueTrackingDecorator.class,
+ IssueHandlers.class,
+ InitialOpenIssuesSensor.class,
+
QProfileEventsDecorator.class,
TimeMachineConfiguration.class);
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleSettings.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleSettings.java
index f7c2d9fa27d..83b1c328deb 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleSettings.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ModuleSettings.java
@@ -27,7 +27,7 @@ import org.sonar.api.config.Settings;
import org.sonar.api.utils.MessageException;
import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.GlobalSettings;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import java.util.List;
@@ -36,10 +36,10 @@ import java.util.List;
*/
public class ModuleSettings extends Settings {
- private final ProjectReferentials projectReferentials;
+ private final ProjectRepository projectReferentials;
private AnalysisMode analysisMode;
- public ModuleSettings(GlobalSettings batchSettings, ProjectDefinition project, ProjectReferentials projectReferentials,
+ public ModuleSettings(GlobalSettings batchSettings, ProjectDefinition project, ProjectRepository projectReferentials,
AnalysisMode analysisMode) {
super(batchSettings.getDefinitions());
this.projectReferentials = projectReferentials;
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
index 0a3c22ef77a..d6a47de9952 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectScanContainer.java
@@ -59,11 +59,15 @@ import org.sonar.batch.index.ResourcePersister;
import org.sonar.batch.index.SourcePersister;
import org.sonar.batch.issue.DefaultProjectIssues;
import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.issue.tracking.InitialOpenIssuesStack;
+import org.sonar.batch.issue.tracking.LocalIssueTracking;
+import org.sonar.batch.issue.tracking.PreviousIssueRepository;
import org.sonar.batch.languages.DefaultLanguagesReferential;
import org.sonar.batch.mediumtest.ScanTaskObservers;
import org.sonar.batch.phases.GraphPersister;
import org.sonar.batch.profiling.PhasesSumUpTimeProfiler;
-import org.sonar.batch.referential.ProjectReferentialsProvider;
+import org.sonar.batch.repository.ProjectRepositoriesProvider;
+import org.sonar.batch.rule.ActiveRulesProvider;
import org.sonar.batch.rule.RulesProvider;
import org.sonar.batch.scan.filesystem.InputPathCache;
import org.sonar.batch.scan.maven.FakeMavenPluginExecutor;
@@ -131,7 +135,7 @@ public class ProjectScanContainer extends ComponentContainer {
private void addBatchComponents() {
add(
- new ProjectReferentialsProvider(),
+ new ProjectRepositoriesProvider(),
DefaultResourceCreationLock.class,
CodeColorizers.class,
DefaultNotificationManager.class,
@@ -148,6 +152,9 @@ public class ProjectScanContainer extends ComponentContainer {
InputPathCache.class,
PathResolver.class,
+ // rules
+ new ActiveRulesProvider(),
+
// issues
IssueUpdater.class,
FunctionExecutor.class,
@@ -155,6 +162,8 @@ public class ProjectScanContainer extends ComponentContainer {
IssueCache.class,
DefaultProjectIssues.class,
IssueChangelogDebtCalculator.class,
+ LocalIssueTracking.class,
+ PreviousIssueRepository.class,
// tests
TestPlanPerspectiveLoader.class,
@@ -203,6 +212,9 @@ public class ProjectScanContainer extends ComponentContainer {
// technical debt
DefaultTechnicalDebtModel.class,
+ // Issue tracking
+ InitialOpenIssuesStack.class,
+
ProjectLock.class);
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectSettings.java b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectSettings.java
index 72218e95ce3..f6539dbfa7f 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectSettings.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/ProjectSettings.java
@@ -28,18 +28,18 @@ import org.sonar.api.config.Settings;
import org.sonar.api.utils.MessageException;
import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.GlobalSettings;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
public class ProjectSettings extends Settings {
private static final Logger LOG = LoggerFactory.getLogger(ProjectSettings.class);
private final GlobalSettings globalSettings;
- private final ProjectReferentials projectReferentials;
+ private final ProjectRepository projectReferentials;
private final AnalysisMode mode;
public ProjectSettings(ProjectReactor reactor, GlobalSettings globalSettings, PropertyDefinitions propertyDefinitions,
- ProjectReferentials projectReferentials, AnalysisMode mode) {
+ ProjectRepository projectReferentials, AnalysisMode mode) {
super(propertyDefinitions);
this.mode = mode;
getEncryption().setPathToSecretKey(globalSettings.getString(CoreProperties.ENCRYPTION_SECRET_KEY_PATH));
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java
index 8843e604989..883268fcc01 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetection.java
@@ -22,13 +22,13 @@ package org.sonar.batch.scan.filesystem;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.batch.protocol.input.FileData;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
class StatusDetection {
- private final ProjectReferentials projectReferentials;
+ private final ProjectRepository projectReferentials;
- StatusDetection(ProjectReferentials projectReferentials) {
+ StatusDetection(ProjectRepository projectReferentials) {
this.projectReferentials = projectReferentials;
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java
index 830cb346c09..15e19d6457b 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scan/filesystem/StatusDetectionFactory.java
@@ -20,13 +20,13 @@
package org.sonar.batch.scan.filesystem;
import org.sonar.api.BatchComponent;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
public class StatusDetectionFactory implements BatchComponent {
- private final ProjectReferentials projectReferentials;
+ private final ProjectRepository projectReferentials;
- public StatusDetectionFactory(ProjectReferentials projectReferentials) {
+ public StatusDetectionFactory(ProjectRepository projectReferentials) {
this.projectReferentials = projectReferentials;
}
diff --git a/sonar-batch/src/main/java/org/sonar/batch/scm/ScmSensor.java b/sonar-batch/src/main/java/org/sonar/batch/scm/ScmSensor.java
index 00f6839bfdb..fc42a20e18c 100644
--- a/sonar-batch/src/main/java/org/sonar/batch/scm/ScmSensor.java
+++ b/sonar-batch/src/main/java/org/sonar/batch/scm/ScmSensor.java
@@ -34,7 +34,7 @@ import org.sonar.api.batch.sensor.measure.internal.DefaultMeasure;
import org.sonar.api.measures.CoreMetrics;
import org.sonar.api.utils.TimeProfiler;
import org.sonar.batch.protocol.input.FileData;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import org.sonar.core.DryRunIncompatible;
import java.util.LinkedList;
@@ -48,10 +48,10 @@ public final class ScmSensor implements Sensor {
private final ProjectDefinition projectDefinition;
private final ScmConfiguration configuration;
private final FileSystem fs;
- private final ProjectReferentials projectReferentials;
+ private final ProjectRepository projectReferentials;
public ScmSensor(ProjectDefinition projectDefinition, ScmConfiguration configuration,
- ProjectReferentials projectReferentials, FileSystem fs) {
+ ProjectRepository projectReferentials, FileSystem fs) {
this.projectDefinition = projectDefinition;
this.configuration = configuration;
this.projectReferentials = projectReferentials;
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensorTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensorTest.java
new file mode 100644
index 00000000000..85c1e43c51a
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesSensorTest.java
@@ -0,0 +1,65 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.apache.ibatis.session.ResultHandler;
+import org.junit.Test;
+import org.sonar.api.resources.Project;
+import org.sonar.core.issue.db.IssueChangeDao;
+import org.sonar.core.issue.db.IssueDao;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+public class InitialOpenIssuesSensorTest {
+
+ InitialOpenIssuesStack stack = mock(InitialOpenIssuesStack.class);
+ IssueDao issueDao = mock(IssueDao.class);
+ IssueChangeDao issueChangeDao = mock(IssueChangeDao.class);
+
+ InitialOpenIssuesSensor sensor = new InitialOpenIssuesSensor(stack, issueDao, issueChangeDao);
+
+ @Test
+ public void should_select_module_open_issues() {
+ Project project = new Project("key");
+ project.setId(1);
+ sensor.analyse(project, null);
+
+ verify(issueDao).selectNonClosedIssuesByModule(eq(1), any(ResultHandler.class));
+ }
+
+ @Test
+ public void should_select_module_open_issues_changelog() {
+ Project project = new Project("key");
+ project.setId(1);
+ sensor.analyse(project, null);
+
+ verify(issueChangeDao).selectChangelogOnNonClosedIssuesByModuleAndType(eq(1), any(ResultHandler.class));
+ }
+
+ @Test
+ public void test_toString() throws Exception {
+ assertThat(sensor.toString()).isEqualTo("InitialOpenIssuesSensor");
+
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStackTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStackTest.java
new file mode 100644
index 00000000000..34adce38fa1
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/InitialOpenIssuesStackTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.issue.tracking;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.api.CoreProperties;
+import org.sonar.batch.bootstrap.BootstrapProperties;
+import org.sonar.batch.bootstrap.TempFolderProvider;
+import org.sonar.batch.index.Caches;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueDto;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class InitialOpenIssuesStackTest {
+
+ @ClassRule
+ public static TemporaryFolder temp = new TemporaryFolder();
+
+ public static Caches createCacheOnTemp(TemporaryFolder temp) {
+ BootstrapProperties bootstrapSettings = new BootstrapProperties(Collections.<String, String>emptyMap());
+ try {
+ bootstrapSettings.properties().put(CoreProperties.WORKING_DIRECTORY, temp.newFolder().getAbsolutePath());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return new Caches(new TempFolderProvider().provide(bootstrapSettings));
+ }
+
+ InitialOpenIssuesStack stack;
+ Caches caches;
+
+ @Before
+ public void before() throws Exception {
+ caches = createCacheOnTemp(temp);
+ caches.start();
+ stack = new InitialOpenIssuesStack(caches);
+ }
+
+ @After
+ public void after() {
+ caches.stop();
+ }
+
+ @Test
+ public void get_and_remove_issues() {
+ IssueDto issueDto = new IssueDto().setComponentKey("org.struts.Action").setKee("ISSUE-1");
+ stack.addIssue(issueDto);
+
+ List<PreviousIssue> issueDtos = stack.selectAndRemoveIssues("org.struts.Action");
+ assertThat(issueDtos).hasSize(1);
+ assertThat(issueDtos.get(0).key()).isEqualTo("ISSUE-1");
+
+ assertThat(stack.selectAllIssues()).isEmpty();
+ }
+
+ @Test
+ public void get_and_remove_with_many_issues_on_same_resource() {
+ stack.addIssue(new IssueDto().setComponentKey("org.struts.Action").setKee("ISSUE-1"));
+ stack.addIssue(new IssueDto().setComponentKey("org.struts.Action").setKee("ISSUE-2"));
+
+ List<PreviousIssue> issueDtos = stack.selectAndRemoveIssues("org.struts.Action");
+ assertThat(issueDtos).hasSize(2);
+
+ assertThat(stack.selectAllIssues()).isEmpty();
+ }
+
+ @Test
+ public void get_and_remove_do_nothing_if_resource_not_found() {
+ stack.addIssue(new IssueDto().setComponentKey("org.struts.Action").setKee("ISSUE-1"));
+
+ List<PreviousIssue> issueDtos = stack.selectAndRemoveIssues("Other");
+ assertThat(issueDtos).hasSize(0);
+
+ assertThat(stack.selectAllIssues()).hasSize(1);
+ }
+
+ @Test
+ public void select_changelog() {
+ stack.addChangelog(new IssueChangeDto().setKey("CHANGE-1").setIssueKey("ISSUE-1"));
+ stack.addChangelog(new IssueChangeDto().setKey("CHANGE-2").setIssueKey("ISSUE-1"));
+
+ List<IssueChangeDto> issueChangeDtos = stack.selectChangelog("ISSUE-1");
+ assertThat(issueChangeDtos).hasSize(2);
+ assertThat(issueChangeDtos.get(0).getKey()).isEqualTo("CHANGE-1");
+ assertThat(issueChangeDtos.get(1).getKey()).isEqualTo("CHANGE-2");
+ }
+
+ @Test
+ public void return_empty_changelog() {
+ assertThat(stack.selectChangelog("ISSUE-1")).isEmpty();
+ }
+
+ @Test
+ public void clear_issues() {
+ stack.addIssue(new IssueDto().setComponentKey("org.struts.Action").setKee("ISSUE-1"));
+
+ assertThat(stack.selectAllIssues()).hasSize(1);
+
+ // issues are not removed
+ assertThat(stack.selectAllIssues()).hasSize(1);
+
+ stack.clear();
+ assertThat(stack.selectAllIssues()).isEmpty();
+ }
+
+ @Test
+ public void clear_issues_changelog() {
+ stack.addChangelog(new IssueChangeDto().setKey("CHANGE-1").setIssueKey("ISSUE-1"));
+
+ assertThat(stack.selectChangelog("ISSUE-1")).hasSize(1);
+
+ stack.clear();
+ assertThat(stack.selectChangelog("ISSUE-1")).isEmpty();
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueHandlersTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueHandlersTest.java
new file mode 100644
index 00000000000..8b210bd731d
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueHandlersTest.java
@@ -0,0 +1,62 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * SonarQube is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.batch.issue.tracking;
+
+import org.sonar.batch.issue.tracking.IssueHandlers;
+
+import org.junit.Test;
+import org.mockito.ArgumentMatcher;
+import org.sonar.api.issue.IssueHandler;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.core.issue.IssueUpdater;
+
+import java.util.Date;
+
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Mockito.*;
+
+public class IssueHandlersTest {
+ @Test
+ public void should_execute_handlers() throws Exception {
+ IssueHandler h1 = mock(IssueHandler.class);
+ IssueHandler h2 = mock(IssueHandler.class);
+ IssueUpdater updater = mock(IssueUpdater.class);
+
+ IssueHandlers handlers = new IssueHandlers(updater, new IssueHandler[]{h1, h2});
+ final DefaultIssue issue = new DefaultIssue();
+ handlers.execute(issue, IssueChangeContext.createScan(new Date()));
+
+ verify(h1).onIssue(argThat(new ArgumentMatcher<IssueHandler.Context>() {
+ @Override
+ public boolean matches(Object o) {
+ return ((IssueHandler.Context) o).issue() == issue;
+ }
+ }));
+ }
+
+ @Test
+ public void test_no_handlers() {
+ IssueUpdater updater = mock(IssueUpdater.class);
+ IssueHandlers handlers = new IssueHandlers(updater);
+ handlers.execute(new DefaultIssue(), IssueChangeContext.createScan(new Date()));
+ verifyZeroInteractions(updater);
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizerTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizerTest.java
new file mode 100644
index 00000000000..30bf045483d
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingBlocksRecognizerTest.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.issue.tracking;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class IssueTrackingBlocksRecognizerTest {
+
+ @Test
+ public void test() {
+ assertThat(compute(t("abcde"), t("abcde"), 4, 4)).isEqualTo(5);
+ assertThat(compute(t("abcde"), t("abcd"), 4, 4)).isEqualTo(4);
+ assertThat(compute(t("bcde"), t("abcde"), 4, 4)).isEqualTo(0);
+ assertThat(compute(t("bcde"), t("abcde"), 3, 4)).isEqualTo(4);
+ }
+
+ private static int compute(FileHashes a, FileHashes b, int ai, int bi) {
+ IssueTrackingBlocksRecognizer rec = new IssueTrackingBlocksRecognizer(a, b);
+ return rec.computeLengthOfMaximalBlock(ai, bi);
+ }
+
+ private static FileHashes t(String text) {
+ String[] array = new String[text.length()];
+ for (int i = 0; i < text.length(); i++) {
+ array[i] = "" + text.charAt(i);
+ }
+ return FileHashes.create(array);
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingDecoratorTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingDecoratorTest.java
new file mode 100644
index 00000000000..7691dbf6538
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingDecoratorTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.issue.tracking;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatcher;
+import org.sonar.api.batch.DecoratorContext;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.component.ResourcePerspectives;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.issue.internal.IssueChangeContext;
+import org.sonar.api.profiles.RulesProfile;
+import org.sonar.api.resources.File;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.Rule;
+import org.sonar.api.rules.RuleFinder;
+import org.sonar.api.utils.Duration;
+import org.sonar.api.utils.System2;
+import org.sonar.batch.issue.IssueCache;
+import org.sonar.batch.scan.LastLineHashes;
+import org.sonar.batch.scan.filesystem.InputPathCache;
+import org.sonar.core.issue.IssueUpdater;
+import org.sonar.core.issue.db.IssueChangeDto;
+import org.sonar.core.issue.db.IssueDto;
+import org.sonar.core.issue.workflow.IssueWorkflow;
+import org.sonar.java.api.JavaClass;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyCollection;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.argThat;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.RETURNS_MOCKS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class IssueTrackingDecoratorTest {
+
+ IssueTrackingDecorator decorator;
+ IssueCache issueCache = mock(IssueCache.class, RETURNS_MOCKS);
+ InitialOpenIssuesStack initialOpenIssues = mock(InitialOpenIssuesStack.class);
+ IssueTracking tracking = mock(IssueTracking.class, RETURNS_MOCKS);
+ LastLineHashes lastSnapshots = mock(LastLineHashes.class);
+ IssueHandlers handlers = mock(IssueHandlers.class);
+ IssueWorkflow workflow = mock(IssueWorkflow.class);
+ IssueUpdater updater = mock(IssueUpdater.class);
+ ResourcePerspectives perspectives = mock(ResourcePerspectives.class);
+ RulesProfile profile = mock(RulesProfile.class);
+ RuleFinder ruleFinder = mock(RuleFinder.class);
+ InputPathCache inputPathCache = mock(InputPathCache.class);
+
+ @Before
+ public void init() {
+ decorator = new IssueTrackingDecorator(
+ issueCache,
+ initialOpenIssues,
+ tracking,
+ lastSnapshots,
+ handlers,
+ workflow,
+ updater,
+ new Project("foo"),
+ perspectives,
+ profile,
+ ruleFinder,
+ inputPathCache);
+ }
+
+ @Test
+ public void should_execute_on_project() {
+ Project project = mock(Project.class);
+ assertThat(decorator.shouldExecuteOnProject(project)).isTrue();
+ }
+
+ @Test
+ public void should_not_be_executed_on_classes_not_methods() throws Exception {
+ DecoratorContext context = mock(DecoratorContext.class);
+ decorator.decorate(JavaClass.create("org.foo.Bar"), context);
+ verifyZeroInteractions(context, issueCache, tracking, handlers, workflow);
+ }
+
+ @Test
+ public void should_process_open_issues() throws Exception {
+ Resource file = File.create("Action.java").setEffectiveKey("struts:Action.java").setId(123);
+ final DefaultIssue issue = new DefaultIssue();
+
+ // INPUT : one issue, no open issues during previous scan, no filtering
+ when(issueCache.byComponent("struts:Action.java")).thenReturn(Arrays.asList(issue));
+ List<PreviousIssue> dbIssues = Collections.emptyList();
+ when(initialOpenIssues.selectAndRemoveIssues("struts:Action.java")).thenReturn(dbIssues);
+ when(inputPathCache.getFile("foo", "Action.java")).thenReturn(mock(DefaultInputFile.class));
+ decorator.doDecorate(file);
+
+ // Apply filters, track, apply transitions, notify extensions then update cache
+ verify(tracking).track(isA(SourceHashHolder.class), eq(dbIssues), argThat(new ArgumentMatcher<Collection<DefaultIssue>>() {
+ @Override
+ public boolean matches(Object o) {
+ List<DefaultIssue> issues = (List<DefaultIssue>) o;
+ return issues.size() == 1 && issues.get(0) == issue;
+ }
+ }));
+ verify(workflow).doAutomaticTransition(eq(issue), any(IssueChangeContext.class));
+ verify(handlers).execute(eq(issue), any(IssueChangeContext.class));
+ verify(issueCache).put(issue);
+ }
+
+ @Test
+ public void should_register_unmatched_issues_as_end_of_life() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+ Resource file = File.create("Action.java").setEffectiveKey("struts:Action.java").setId(123);
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setResolution(null).setStatus("OPEN").setRuleKey("squid", "AvoidCycle"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+ when(inputPathCache.getFile("foo", "Action.java")).thenReturn(mock(DefaultInputFile.class));
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isTrue();
+ }
+
+ @Test
+ public void manual_issues_should_be_moved_if_matching_line_found() throws Exception {
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(6).setStatus("OPEN").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public interface Action {\n"
+ + " void method1();\n"
+ + " void method2();\n"
+ + " void method3();\n"
+ + " void method4();\n"
+ + " void method5();\n" // Original issue here
+ + "}";
+ String newSource = "public interface Action {\n"
+ + " void method5();\n" // New issue here
+ + " void method1();\n"
+ + " void method2();\n"
+ + " void method3();\n"
+ + " void method4();\n"
+ + "}";
+ Resource file = mockHashes(originalSource, newSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.line()).isEqualTo(2);
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isFalse();
+ assertThat(issue.isOnDisabledRule()).isFalse();
+ }
+
+ private Resource mockHashes(String originalSource, String newSource) {
+ DefaultInputFile inputFile = mock(DefaultInputFile.class);
+ byte[][] hashes = computeHashes(newSource);
+ when(inputFile.lineHashes()).thenReturn(hashes);
+ when(inputFile.key()).thenReturn("foo:Action.java");
+ when(inputPathCache.getFile("foo", "Action.java")).thenReturn(inputFile);
+ when(lastSnapshots.getLineHashes("foo:Action.java")).thenReturn(computeHexHashes(originalSource));
+ Resource file = File.create("Action.java");
+ return file;
+ }
+
+ @Test
+ public void manual_issues_should_be_untouched_if_already_closed() throws Exception {
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(1).setStatus("CLOSED").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public interface Action {}";
+ Resource file = mockHashes(originalSource, originalSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.line()).isEqualTo(1);
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isFalse();
+ assertThat(issue.isOnDisabledRule()).isFalse();
+ assertThat(issue.status()).isEqualTo("CLOSED");
+ }
+
+ @Test
+ public void manual_issues_should_be_untouched_if_line_is_null() throws Exception {
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(null).setStatus("OPEN").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public interface Action {}";
+ Resource file = mockHashes(originalSource, originalSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.line()).isEqualTo(null);
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isFalse();
+ assertThat(issue.isOnDisabledRule()).isFalse();
+ assertThat(issue.status()).isEqualTo("OPEN");
+ }
+
+ @Test
+ public void manual_issues_should_be_kept_if_matching_line_not_found() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+
+ // INPUT : one issue existing during previous scan
+ final int issueOnLine = 6;
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(issueOnLine).setStatus("OPEN")
+ .setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public interface Action {\n"
+ + " void method1();\n"
+ + " void method2();\n"
+ + " void method3();\n"
+ + " void method4();\n"
+ + " void method5();\n" // Original issue here
+ + "}";
+ String newSource = "public interface Action {\n"
+ + " void method1();\n"
+ + " void method2();\n"
+ + " void method3();\n"
+ + " void method4();\n"
+ + " void method6();\n" // Poof, no method5 anymore
+ + "}";
+
+ Resource file = mockHashes(originalSource, newSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.line()).isEqualTo(issueOnLine);
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isFalse();
+ assertThat(issue.isOnDisabledRule()).isFalse();
+ }
+
+ @Test
+ public void manual_issues_should_be_kept_if_multiple_matching_lines_found() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+
+ // INPUT : one issue existing during previous scan
+ final int issueOnLine = 3;
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(issueOnLine).setStatus("OPEN")
+ .setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance"));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public class Action {\n"
+ + " void method1() {\n"
+ + " notify();\n" // initial issue
+ + " }\n"
+ + "}";
+ String newSource = "public class Action {\n"
+ + " \n"
+ + " void method1() {\n" // new issue will appear here
+ + " notify();\n"
+ + " }\n"
+ + " void method2() {\n"
+ + " notify();\n"
+ + " }\n"
+ + "}";
+ Resource file = mockHashes(originalSource, newSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.line()).isEqualTo(issueOnLine);
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isFalse();
+ assertThat(issue.isOnDisabledRule()).isFalse();
+ }
+
+ @Test
+ public void manual_issues_should_be_closed_if_manual_rule_is_removed() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(1).setStatus("OPEN").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(new Rule("manual", "Performance").setStatus(Rule.STATUS_REMOVED));
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String source = "public interface Action {}";
+ Resource file = mockHashes(source, source);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isTrue();
+ assertThat(issue.isOnDisabledRule()).isTrue();
+ }
+
+ @Test
+ public void manual_issues_should_be_closed_if_manual_rule_is_not_found() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(1).setStatus("OPEN").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(null);
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String source = "public interface Action {}";
+ Resource file = mockHashes(source, source);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isTrue();
+ assertThat(issue.isOnDisabledRule()).isTrue();
+ }
+
+ @Test
+ public void manual_issues_should_be_closed_if_new_source_is_shorter() throws Exception {
+ // "Unmatched" issues existed in previous scan but not in current one -> they have to be closed
+
+ // INPUT : one issue existing during previous scan
+ PreviousIssue unmatchedIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setReporter("freddy").setLine(6).setStatus("OPEN").setRuleKey("manual", "Performance"));
+ when(ruleFinder.findByKey(RuleKey.of("manual", "Performance"))).thenReturn(null);
+
+ IssueTrackingResult trackingResult = new IssueTrackingResult();
+ trackingResult.addUnmatched(unmatchedIssue);
+
+ String originalSource = "public interface Action {\n"
+ + " void method1();\n"
+ + " void method2();\n"
+ + " void method3();\n"
+ + " void method4();\n"
+ + " void method5();\n"
+ + "}";
+ String newSource = "public interface Action {\n"
+ + " void method1();\n"
+ + " void method2();\n"
+ + "}";
+ Resource file = mockHashes(originalSource, newSource);
+
+ when(tracking.track(isA(SourceHashHolder.class), anyCollection(), anyCollection())).thenReturn(trackingResult);
+
+ decorator.doDecorate(file);
+
+ verify(workflow, times(1)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(1)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+
+ ArgumentCaptor<DefaultIssue> argument = ArgumentCaptor.forClass(DefaultIssue.class);
+ verify(issueCache).put(argument.capture());
+
+ DefaultIssue issue = argument.getValue();
+ verify(updater).setResolution(eq(issue), eq(Issue.RESOLUTION_REMOVED), any(IssueChangeContext.class));
+ verify(updater).setStatus(eq(issue), eq(Issue.STATUS_CLOSED), any(IssueChangeContext.class));
+
+ assertThat(issue.key()).isEqualTo("ABCDE");
+ assertThat(issue.isNew()).isFalse();
+ assertThat(issue.isEndOfLife()).isTrue();
+ assertThat(issue.isOnDisabledRule()).isTrue();
+ }
+
+ @Test
+ public void should_register_issues_on_deleted_components() throws Exception {
+ Project project = new Project("struts");
+ DefaultIssue openIssue = new DefaultIssue();
+ when(issueCache.byComponent("struts")).thenReturn(Arrays.asList(openIssue));
+ IssueDto deadIssue = new IssueDto().setKee("ABCDE").setResolution(null).setStatus("OPEN").setRuleKey("squid", "AvoidCycle");
+ when(initialOpenIssues.selectAllIssues()).thenReturn(Arrays.asList(deadIssue));
+
+ decorator.doDecorate(project);
+
+ // the dead issue must be closed -> apply automatic transition, notify handlers and add to cache
+ verify(workflow, times(2)).doAutomaticTransition(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(handlers, times(2)).execute(any(DefaultIssue.class), any(IssueChangeContext.class));
+ verify(issueCache, times(2)).put(any(DefaultIssue.class));
+
+ verify(issueCache).put(argThat(new ArgumentMatcher<DefaultIssue>() {
+ @Override
+ public boolean matches(Object o) {
+ DefaultIssue dead = (DefaultIssue) o;
+ return "ABCDE".equals(dead.key()) && !dead.isNew() && dead.isEndOfLife();
+ }
+ }));
+ }
+
+ @Test
+ public void merge_matched_issue() throws Exception {
+ PreviousIssue previousIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setResolution(null).setStatus("OPEN").setRuleKey("squid", "AvoidCycle")
+ .setLine(10).setSeverity("MAJOR").setMessage("Message").setEffortToFix(1.5).setDebt(1L).setProjectKey("sample"));
+ DefaultIssue issue = new DefaultIssue();
+
+ IssueTrackingResult trackingResult = mock(IssueTrackingResult.class);
+ when(trackingResult.matched()).thenReturn(newArrayList(issue));
+ when(trackingResult.matching(eq(issue))).thenReturn(previousIssue);
+ decorator.mergeMatched(trackingResult);
+
+ verify(updater).setPastSeverity(eq(issue), eq("MAJOR"), any(IssueChangeContext.class));
+ verify(updater).setPastLine(eq(issue), eq(10));
+ verify(updater).setPastMessage(eq(issue), eq("Message"), any(IssueChangeContext.class));
+ verify(updater).setPastEffortToFix(eq(issue), eq(1.5), any(IssueChangeContext.class));
+ verify(updater).setPastTechnicalDebt(eq(issue), eq(Duration.create(1L)), any(IssueChangeContext.class));
+ verify(updater).setPastProject(eq(issue), eq("sample"), any(IssueChangeContext.class));
+ }
+
+ @Test
+ public void merge_matched_issue_on_manual_severity() throws Exception {
+ PreviousIssue previousIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setResolution(null).setStatus("OPEN").setRuleKey("squid", "AvoidCycle")
+ .setLine(10).setManualSeverity(true).setSeverity("MAJOR").setMessage("Message").setEffortToFix(1.5).setDebt(1L));
+ DefaultIssue issue = new DefaultIssue();
+
+ IssueTrackingResult trackingResult = mock(IssueTrackingResult.class);
+ when(trackingResult.matched()).thenReturn(newArrayList(issue));
+ when(trackingResult.matching(eq(issue))).thenReturn(previousIssue);
+ decorator.mergeMatched(trackingResult);
+
+ assertThat(issue.manualSeverity()).isTrue();
+ assertThat(issue.severity()).isEqualTo("MAJOR");
+ verify(updater, never()).setPastSeverity(eq(issue), anyString(), any(IssueChangeContext.class));
+ }
+
+ @Test
+ public void merge_issue_changelog_with_previous_changelog() throws Exception {
+ when(initialOpenIssues.selectChangelog("ABCDE")).thenReturn(newArrayList(new IssueChangeDto().setIssueKey("ABCD").setCreatedAt(System2.INSTANCE.now())));
+
+ PreviousIssue previousIssue = new PreviousIssueFromDb(new IssueDto().setKee("ABCDE").setResolution(null).setStatus("OPEN").setRuleKey("squid", "AvoidCycle")
+ .setLine(10).setMessage("Message").setEffortToFix(1.5).setDebt(1L).setCreatedAt(System2.INSTANCE.now()));
+ DefaultIssue issue = new DefaultIssue();
+
+ IssueTrackingResult trackingResult = mock(IssueTrackingResult.class);
+ when(trackingResult.matched()).thenReturn(newArrayList(issue));
+ when(trackingResult.matching(eq(issue))).thenReturn(previousIssue);
+ decorator.mergeMatched(trackingResult);
+
+ assertThat(issue.changes()).hasSize(1);
+ }
+
+ private byte[][] computeHashes(String source) {
+ String[] lines = source.split("\n");
+ byte[][] hashes = new byte[lines.length][];
+ for (int i = 0; i < lines.length; i++) {
+ hashes[i] = DigestUtils.md5(lines[i].replaceAll("[\t ]", ""));
+ }
+ return hashes;
+ }
+
+ private String[] computeHexHashes(String source) {
+ String[] lines = source.split("\n");
+ String[] hashes = new String[lines.length];
+ for (int i = 0; i < lines.length; i++) {
+ hashes[i] = DigestUtils.md5Hex(lines[i].replaceAll("[\t ]", ""));
+ }
+ return hashes;
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingTest.java
new file mode 100644
index 00000000000..2efb24c13eb
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/IssueTrackingTest.java
@@ -0,0 +1,372 @@
+/*
+ * 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.issue.tracking;
+
+import com.google.common.base.Charsets;
+import com.google.common.collect.Lists;
+import com.google.common.io.Resources;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.junit.Before;
+import org.junit.Test;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.issue.internal.DefaultIssue;
+import org.sonar.api.resources.Project;
+import org.sonar.api.resources.Resource;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.batch.scan.LastLineHashes;
+import org.sonar.core.issue.db.IssueDto;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static com.google.common.collect.Lists.newArrayList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class IssueTrackingTest {
+
+ IssueTracking tracking;
+ Resource project;
+ SourceHashHolder sourceHashHolder;
+ LastLineHashes lastSnapshots;
+ long violationId = 0;
+
+ @Before
+ public void before() {
+ lastSnapshots = mock(LastLineHashes.class);
+
+ project = mock(Project.class);
+ tracking = new IssueTracking();
+ }
+
+ @Test
+ public void key_should_be_the_prioritary_field_to_check() {
+ PreviousIssueFromDb referenceIssue1 = newReferenceIssue("message", 10, "squid", "AvoidCycle", "checksum1");
+ referenceIssue1.getDto().setKee("100");
+ PreviousIssueFromDb referenceIssue2 = newReferenceIssue("message", 10, "squid", "AvoidCycle", "checksum2");
+ referenceIssue2.getDto().setKee("200");
+
+ // exactly the fields of referenceIssue1 but not the same key
+ DefaultIssue newIssue = newDefaultIssue("message", 10, RuleKey.of("squid", "AvoidCycle"), "checksum1").setKey("200");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), Lists.<PreviousIssue>newArrayList(referenceIssue1, referenceIssue2), null, result);
+ // same key
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue2);
+ }
+
+ @Test
+ public void checksum_should_have_greater_priority_than_line() {
+ PreviousIssue referenceIssue1 = newReferenceIssue("message", 1, "squid", "AvoidCycle", "checksum1");
+ PreviousIssue referenceIssue2 = newReferenceIssue("message", 3, "squid", "AvoidCycle", "checksum2");
+
+ DefaultIssue newIssue1 = newDefaultIssue("message", 3, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ DefaultIssue newIssue2 = newDefaultIssue("message", 5, RuleKey.of("squid", "AvoidCycle"), "checksum2");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue1, newIssue2), newArrayList(referenceIssue1, referenceIssue2), null, result);
+ assertThat(result.matching(newIssue1)).isSameAs(referenceIssue1);
+ assertThat(result.matching(newIssue2)).isSameAs(referenceIssue2);
+ }
+
+ /**
+ * SONAR-2928
+ */
+ @Test
+ public void same_rule_and_null_line_and_checksum_but_different_messages() {
+ DefaultIssue newIssue = newDefaultIssue("new message", null, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("old message", null, "squid", "AvoidCycle", "checksum1");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ @Test
+ public void same_rule_and_line_and_checksum_but_different_messages() {
+ DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("old message", 1, "squid", "AvoidCycle", "checksum1");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ @Test
+ public void same_rule_and_line_message() {
+ DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("message", 1, "squid", "AvoidCycle", "checksum2");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ @Test
+ public void should_ignore_reference_measure_without_checksum() {
+ DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), null);
+ PreviousIssue referenceIssue = newReferenceIssue("message", 1, "squid", "NullDeref", null);
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isNull();
+ }
+
+ @Test
+ public void same_rule_and_message_and_checksum_but_different_line() {
+ DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("message", 2, "squid", "AvoidCycle", "checksum1");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ /**
+ * SONAR-2812
+ */
+ @Test
+ public void same_checksum_and_rule_but_different_line_and_different_message() {
+ DefaultIssue newIssue = newDefaultIssue("new message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("old message", 2, "squid", "AvoidCycle", "checksum1");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ @Test
+ public void should_create_new_issue_when_same_rule_same_message_but_different_line_and_checksum() {
+ DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("message", 2, "squid", "AvoidCycle", "checksum2");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isNull();
+ }
+
+ @Test
+ public void should_not_track_issue_if_different_rule() {
+ DefaultIssue newIssue = newDefaultIssue("message", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("message", 1, "squid", "NullDeref", "checksum1");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isNull();
+ }
+
+ @Test
+ public void should_compare_issues_with_database_format() {
+ // issue messages are trimmed and can be abbreviated when persisted in database.
+ // Comparing issue messages must use the same format.
+ DefaultIssue newIssue = newDefaultIssue(" message ", 1, RuleKey.of("squid", "AvoidCycle"), "checksum1");
+ PreviousIssue referenceIssue = newReferenceIssue("message", 1, "squid", "AvoidCycle", "checksum2");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(newArrayList(newIssue), newArrayList(referenceIssue), null, result);
+ assertThat(result.matching(newIssue)).isSameAs(referenceIssue);
+ }
+
+ @Test
+ public void past_issue_not_associated_with_line_should_not_cause_npe() throws Exception {
+ initLastHashes("example2-v1", "example2-v2");
+
+ DefaultIssue newIssue = newDefaultIssue("Indentation", 9, RuleKey.of("squid", "AvoidCycle"), "foo");
+ PreviousIssue referenceIssue = newReferenceIssue("2 branches need to be covered", null, "squid", "AvoidCycle", null);
+
+ IssueTrackingResult result = tracking.track(sourceHashHolder, newArrayList(referenceIssue), newArrayList(newIssue));
+
+ assertThat(result.matched()).isEmpty();
+ }
+
+ @Test
+ public void new_issue_not_associated_with_line_should_not_cause_npe() throws Exception {
+ initLastHashes("example2-v1", "example2-v2");
+
+ DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), "foo");
+ PreviousIssue referenceIssue = newReferenceIssue("Indentationd", 7, "squid", "AvoidCycle", null);
+
+ IssueTrackingResult result = tracking.track(sourceHashHolder, newArrayList(referenceIssue), newArrayList(newIssue));
+
+ assertThat(result.matched()).isEmpty();
+ }
+
+ /**
+ * SONAR-2928
+ */
+ @Test
+ public void issue_not_associated_with_line() throws Exception {
+ initLastHashes("example2-v1", "example2-v2");
+
+ DefaultIssue newIssue = newDefaultIssue("1 branch need to be covered", null, RuleKey.of("squid", "AvoidCycle"), null);
+ PreviousIssue referenceIssue = newReferenceIssue("2 branches need to be covered", null, "squid", "AvoidCycle", null);
+
+ IssueTrackingResult result = tracking.track(sourceHashHolder, newArrayList(referenceIssue), newArrayList(newIssue));
+
+ assertThat(result.matching(newIssue)).isEqualTo(referenceIssue);
+ }
+
+ /**
+ * SONAR-3072
+ */
+ @Test
+ public void should_track_issues_based_on_blocks_recognition_on_example1() throws Exception {
+ initLastHashes("example1-v1", "example1-v2");
+
+ PreviousIssue referenceIssue1 = newReferenceIssue("Indentation", 7, "squid", "AvoidCycle", null);
+ PreviousIssue referenceIssue2 = newReferenceIssue("Indentation", 11, "squid", "AvoidCycle", null);
+
+ DefaultIssue newIssue1 = newDefaultIssue("Indentation", 9, RuleKey.of("squid", "AvoidCycle"), null);
+ DefaultIssue newIssue2 = newDefaultIssue("Indentation", 13, RuleKey.of("squid", "AvoidCycle"), null);
+ DefaultIssue newIssue3 = newDefaultIssue("Indentation", 17, RuleKey.of("squid", "AvoidCycle"), null);
+ DefaultIssue newIssue4 = newDefaultIssue("Indentation", 21, RuleKey.of("squid", "AvoidCycle"), null);
+
+ IssueTrackingResult result = tracking.track(sourceHashHolder, Arrays.asList(referenceIssue1, referenceIssue2), Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4));
+
+ assertThat(result.matching(newIssue1)).isNull();
+ assertThat(result.matching(newIssue2)).isNull();
+ assertThat(result.matching(newIssue3)).isSameAs(referenceIssue1);
+ assertThat(result.matching(newIssue4)).isSameAs(referenceIssue2);
+ }
+
+ /**
+ * SONAR-3072
+ */
+ @Test
+ public void should_track_issues_based_on_blocks_recognition_on_example2() throws Exception {
+ initLastHashes("example2-v1", "example2-v2");
+
+ PreviousIssue referenceIssue1 = newReferenceIssue("SystemPrintln", 5, "squid", "AvoidCycle", null);
+
+ DefaultIssue newIssue1 = newDefaultIssue("SystemPrintln", 6, RuleKey.of("squid", "AvoidCycle"), null);
+ DefaultIssue newIssue2 = newDefaultIssue("SystemPrintln", 10, RuleKey.of("squid", "AvoidCycle"), null);
+ DefaultIssue newIssue3 = newDefaultIssue("SystemPrintln", 14, RuleKey.of("squid", "AvoidCycle"), null);
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(
+ Arrays.asList(newIssue1, newIssue2, newIssue3),
+ Arrays.asList(referenceIssue1),
+ sourceHashHolder, result);
+
+ assertThat(result.matching(newIssue1)).isNull();
+ assertThat(result.matching(newIssue2)).isSameAs(referenceIssue1);
+ assertThat(result.matching(newIssue3)).isNull();
+ }
+
+ @Test
+ public void should_track_issues_based_on_blocks_recognition_on_example3() throws Exception {
+ initLastHashes("example3-v1", "example3-v2");
+
+ PreviousIssue referenceIssue1 = newReferenceIssue("Avoid unused local variables such as 'j'.", 6, "squid", "AvoidCycle", "63c11570fc0a76434156be5f8138fa03");
+ PreviousIssue referenceIssue2 = newReferenceIssue("Avoid unused private methods such as 'myMethod()'.", 13, "squid", "NullDeref", "ef23288705d1ef1e512448ace287586e");
+ PreviousIssue referenceIssue3 = newReferenceIssue("Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty.", 9, "pmd",
+ "UnusedLocalVariable", "ed5cdd046fda82727d6fedd1d8e3a310");
+
+ // New issue
+ DefaultIssue newIssue1 = newDefaultIssue("Avoid unused local variables such as 'msg'.", 18, RuleKey.of("squid", "AvoidCycle"), "a24254126be2bf1a9b9a8db43f633733");
+ // Same as referenceIssue2
+ DefaultIssue newIssue2 = newDefaultIssue("Avoid unused private methods such as 'myMethod()'.", 13, RuleKey.of("squid", "NullDeref"), "ef23288705d1ef1e512448ace287586e");
+ // Same as referenceIssue3
+ DefaultIssue newIssue3 = newDefaultIssue("Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty.", 9,
+ RuleKey.of("pmd", "UnusedLocalVariable"), "ed5cdd046fda82727d6fedd1d8e3a310");
+ // New issue
+ DefaultIssue newIssue4 = newDefaultIssue("Method 'newViolation' is not designed for extension - needs to be abstract, final or empty.", 17,
+ RuleKey.of("pmd", "UnusedLocalVariable"), "7d58ac9040c27e4ca2f11a0269e251e2");
+ // Same as referenceIssue1
+ DefaultIssue newIssue5 = newDefaultIssue("Avoid unused local variables such as 'j'.", 6, RuleKey.of("squid", "AvoidCycle"), "4432a2675ec3e1620daefe38386b51ef");
+
+ IssueTrackingResult result = new IssueTrackingResult();
+ tracking.mapIssues(
+ Arrays.asList(newIssue1, newIssue2, newIssue3, newIssue4, newIssue5),
+ Arrays.asList(referenceIssue1, referenceIssue2, referenceIssue3),
+ sourceHashHolder, result);
+
+ assertThat(result.matching(newIssue1)).isNull();
+ assertThat(result.matching(newIssue2)).isSameAs(referenceIssue2);
+ assertThat(result.matching(newIssue3)).isSameAs(referenceIssue3);
+ assertThat(result.matching(newIssue4)).isNull();
+ assertThat(result.matching(newIssue5)).isSameAs(referenceIssue1);
+ }
+
+ @Test
+ public void dont_load_checksum_if_no_new_issue() throws Exception {
+ sourceHashHolder = mock(SourceHashHolder.class);
+
+ PreviousIssue referenceIssue = newReferenceIssue("2 branches need to be covered", null, "squid", "AvoidCycle", null);
+
+ tracking.track(sourceHashHolder, newArrayList(referenceIssue), Collections.<DefaultIssue>emptyList());
+
+ verifyZeroInteractions(lastSnapshots, sourceHashHolder);
+ }
+
+ private static String load(String name) throws IOException {
+ return Resources.toString(IssueTrackingTest.class.getResource("IssueTrackingTest/" + name + ".txt"), Charsets.UTF_8);
+ }
+
+ private DefaultIssue newDefaultIssue(String message, Integer line, RuleKey ruleKey, String checksum) {
+ return new DefaultIssue().setMessage(message).setLine(line).setRuleKey(ruleKey).setChecksum(checksum).setStatus(Issue.STATUS_OPEN);
+ }
+
+ private PreviousIssueFromDb newReferenceIssue(String message, Integer lineId, String ruleRepo, String ruleKey, String lineChecksum) {
+ IssueDto referenceIssue = new IssueDto();
+ Long id = violationId++;
+ referenceIssue.setId(id);
+ referenceIssue.setKee(Long.toString(id));
+ referenceIssue.setLine(lineId);
+ referenceIssue.setMessage(message);
+ referenceIssue.setRuleKey(ruleRepo, ruleKey);
+ referenceIssue.setChecksum(lineChecksum);
+ referenceIssue.setResolution(null);
+ referenceIssue.setStatus(Issue.STATUS_OPEN);
+ return new PreviousIssueFromDb(referenceIssue);
+ }
+
+ private void initLastHashes(String reference, String newSource) throws IOException {
+ DefaultInputFile inputFile = mock(DefaultInputFile.class);
+ byte[][] hashes = computeHashes(load(newSource));
+ when(inputFile.lineHashes()).thenReturn(hashes);
+ when(inputFile.key()).thenReturn("foo:Action.java");
+ when(lastSnapshots.getLineHashes("foo:Action.java")).thenReturn(computeHexHashes(load(reference)));
+ sourceHashHolder = new SourceHashHolder(inputFile, lastSnapshots);
+ }
+
+ private byte[][] computeHashes(String source) {
+ String[] lines = source.split("\n");
+ byte[][] hashes = new byte[lines.length][];
+ for (int i = 0; i < lines.length; i++) {
+ hashes[i] = DigestUtils.md5(lines[i].replaceAll("[\t ]", ""));
+ }
+ return hashes;
+ }
+
+ private String[] computeHexHashes(String source) {
+ String[] lines = source.split("\n");
+ String[] hashes = new String[lines.length];
+ for (int i = 0; i < lines.length; i++) {
+ hashes[i] = DigestUtils.md5Hex(lines[i].replaceAll("[\t ]", ""));
+ }
+ return hashes;
+ }
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java
new file mode 100644
index 00000000000..25abe186d2e
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/RollingFileHashesTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.issue.tracking;
+
+import org.junit.Test;
+
+import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class RollingFileHashesTest {
+
+ @Test
+ public void test_equals() {
+ RollingFileHashes a = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line0"), md5Hex("line1"), md5Hex("line2")}), 1);
+ RollingFileHashes b = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line0"), md5Hex("line1"), md5Hex("line2"), md5Hex("line3")}), 1);
+
+ assertThat(a.getHash(1) == b.getHash(1)).isTrue();
+ assertThat(a.getHash(2) == b.getHash(2)).isTrue();
+ assertThat(a.getHash(3) == b.getHash(3)).isFalse();
+
+ RollingFileHashes c = RollingFileHashes.create(FileHashes.create(new String[] {md5Hex("line-1"), md5Hex("line0"), md5Hex("line1"), md5Hex("line2"), md5Hex("line3")}), 1);
+ assertThat(a.getHash(1) == c.getHash(2)).isFalse();
+ assertThat(a.getHash(2) == c.getHash(3)).isTrue();
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java
new file mode 100644
index 00000000000..6ac6645bbae
--- /dev/null
+++ b/sonar-batch/src/test/java/org/sonar/batch/issue/tracking/SourceHashHolderTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.issue.tracking;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.api.batch.fs.InputFile;
+import org.sonar.api.batch.fs.internal.DefaultInputFile;
+import org.sonar.batch.scan.LastLineHashes;
+
+import static org.apache.commons.codec.digest.DigestUtils.md5;
+import static org.apache.commons.codec.digest.DigestUtils.md5Hex;
+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 SourceHashHolderTest {
+
+ SourceHashHolder sourceHashHolder;
+
+ LastLineHashes lastSnapshots;
+ DefaultInputFile file;
+
+ @Before
+ public void setUp() {
+ lastSnapshots = mock(LastLineHashes.class);
+ file = mock(DefaultInputFile.class);
+
+ sourceHashHolder = new SourceHashHolder(file, lastSnapshots);
+ }
+
+ @Test
+ public void should_lazy_load_line_hashes() {
+ final String source = "source";
+ when(file.lineHashes()).thenReturn(new byte[][] {md5(source), null});
+
+ assertThat(sourceHashHolder.getHashedSource().getHash(1)).isEqualTo(md5Hex(source));
+ assertThat(sourceHashHolder.getHashedSource().getHash(2)).isEqualTo("");
+ verify(file).lineHashes();
+ verify(file).key();
+ verify(file).status();
+
+ assertThat(sourceHashHolder.getHashedSource().getHash(1)).isEqualTo(md5Hex(source));
+ Mockito.verifyNoMoreInteractions(file);
+ }
+
+ @Test
+ public void should_lazy_load_reference_hashes_when_status_changed() {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ when(file.lineHashes()).thenReturn(new byte[][] {md5(source)});
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.CHANGED);
+ when(lastSnapshots.getLineHashes(key)).thenReturn(new String[] {md5Hex(source)});
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ verify(lastSnapshots).getLineHashes(key);
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+ @Test
+ public void should_not_load_reference_hashes_when_status_same() {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ when(file.lineHashes()).thenReturn(new byte[][] {md5(source)});
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.SAME);
+
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ assertThat(sourceHashHolder.getHashedReference().getHash(1)).isEqualTo(md5Hex(source));
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+ @Test
+ public void no_reference_hashes_when_status_added() {
+ final String source = "source";
+ String key = "foo:src/Foo.java";
+ when(file.lineHashes()).thenReturn(new byte[][] {md5(source)});
+ when(file.key()).thenReturn(key);
+ when(file.status()).thenReturn(InputFile.Status.ADDED);
+
+ assertThat(sourceHashHolder.getHashedReference()).isNull();
+ Mockito.verifyNoMoreInteractions(lastSnapshots);
+ }
+
+}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java
index 89f926e296f..a6bed59f2fd 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/IssuesMediumTest.java
@@ -66,7 +66,7 @@ public class IssuesMediumTest {
.newScanTask(new File(projectDir, "sonar-project.properties"))
.start();
- assertThat(result.issues()).hasSize(26);
+ assertThat(result.issues()).hasSize(14);
}
@Test
@@ -78,7 +78,7 @@ public class IssuesMediumTest {
.property("sonar.xoo.internalKey", "OneIssuePerLine.internal")
.start();
- assertThat(result.issues()).hasSize(26 /* 26 lines */+ 3 /* 3 files */);
+ assertThat(result.issues()).hasSize(14 /* 8 + 6 lines */+ 2 /* 2 files */);
}
@Test
@@ -103,7 +103,7 @@ public class IssuesMediumTest {
.property("sonar.issue.ignore.allfile.1.fileRegexp", "object")
.start();
- assertThat(result.issues()).hasSize(20);
+ assertThat(result.issues()).hasSize(8);
}
@Test
diff --git a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/ReportsMediumTest.java b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/ReportsMediumTest.java
index 23f6b90f2c4..04c588ad63f 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/ReportsMediumTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/mediumtest/issues/ReportsMediumTest.java
@@ -20,6 +20,7 @@
package org.sonar.batch.mediumtest.issues;
import com.google.common.collect.ImmutableMap;
+import org.apache.commons.codec.digest.DigestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -27,6 +28,7 @@ import org.junit.rules.TemporaryFolder;
import org.sonar.batch.mediumtest.BatchMediumTester;
import org.sonar.batch.mediumtest.TaskResult;
import org.sonar.batch.protocol.input.ActiveRule;
+import org.sonar.batch.protocol.input.issues.PreviousIssue;
import org.sonar.xoo.XooPlugin;
import java.io.File;
@@ -43,6 +45,13 @@ public class ReportsMediumTest {
.addDefaultQProfile("xoo", "Sonar Way")
.activateRule(new ActiveRule("xoo", "OneIssuePerLine", "One issue per line", "MAJOR", "OneIssuePerLine.internal", "xoo"))
.bootstrapProperties(ImmutableMap.of("sonar.analysis.mode", "sensor"))
+ .addPreviousIssue(new PreviousIssue().setKey("xyz")
+ .setComponentKey("sample:xources/hello/HelloJava.xoo")
+ .setRuleKey("xoo", "OneIssuePerLine")
+ .setLine(1)
+ .setOverriddenSeverity("MAJOR")
+ .setChecksum(DigestUtils.md5Hex("packagehello;"))
+ .setStatus("OPEN"))
.build();
@Before
@@ -64,7 +73,7 @@ public class ReportsMediumTest {
.property("sonar.issuesReport.console.enable", "true")
.start();
- assertThat(result.issues()).hasSize(26);
+ assertThat(result.issues()).hasSize(14);
}
}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/referential/DefaultProjectReferentialsLoaderTest.java b/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultProjectReferentialsLoaderTest.java
index 2a66a09cc59..108151f5aac 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/referential/DefaultProjectReferentialsLoaderTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/repository/DefaultProjectReferentialsLoaderTest.java
@@ -17,7 +17,9 @@
* 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.referential;
+package org.sonar.batch.repository;
+
+import org.sonar.batch.repository.DefaultProjectReferentialsLoader;
import com.google.common.collect.Maps;
import org.junit.Before;
@@ -29,7 +31,6 @@ import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.ServerClient;
import org.sonar.batch.bootstrap.TaskProperties;
import org.sonar.batch.rule.ModuleQProfiles;
-
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java
index 6a8e64d6ccf..2c03cd472f4 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/ModuleSettingsTest.java
@@ -29,7 +29,7 @@ import org.sonar.api.config.PropertyDefinitions;
import org.sonar.api.utils.MessageException;
import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.GlobalSettings;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import java.util.List;
@@ -42,12 +42,12 @@ public class ModuleSettingsTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
- ProjectReferentials projectRef;
+ ProjectRepository projectRef;
private AnalysisMode mode;
@Before
public void before() {
- projectRef = new ProjectReferentials();
+ projectRef = new ProjectRepository();
mode = mock(AnalysisMode.class);
}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java
index ca6a0aa84e5..3326102185e 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectScanContainerTest.java
@@ -19,6 +19,8 @@
*/
package org.sonar.batch.scan;
+import org.sonar.batch.repository.ProjectRepositoriesLoader;
+
import org.junit.Before;
import org.junit.Test;
import org.sonar.api.BatchExtension;
@@ -41,8 +43,7 @@ import org.sonar.batch.bootstrap.GlobalSettings;
import org.sonar.batch.bootstrap.TaskProperties;
import org.sonar.batch.profiling.PhasesSumUpTimeProfiler;
import org.sonar.batch.protocol.input.GlobalReferentials;
-import org.sonar.batch.protocol.input.ProjectReferentials;
-import org.sonar.batch.referential.ProjectReferentialsLoader;
+import org.sonar.batch.protocol.input.ProjectRepository;
import org.sonar.batch.scan.maven.MavenPluginExecutor;
import java.util.Collections;
@@ -72,10 +73,10 @@ public class ProjectScanContainerTest {
GlobalReferentials globalRef = new GlobalReferentials();
settings = new GlobalSettings(bootstrapProperties, new PropertyDefinitions(), globalRef, analysisMode);
parentContainer.add(settings);
- ProjectReferentialsLoader projectReferentialsLoader = new ProjectReferentialsLoader() {
+ ProjectRepositoriesLoader projectReferentialsLoader = new ProjectRepositoriesLoader() {
@Override
- public ProjectReferentials load(ProjectReactor reactor, TaskProperties taskProperties) {
- return new ProjectReferentials();
+ public ProjectRepository load(ProjectReactor reactor, TaskProperties taskProperties) {
+ return new ProjectRepository();
}
};
parentContainer.add(projectReferentialsLoader);
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java
index c75f7aafb3d..69db0207a8b 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/ProjectSettingsTest.java
@@ -33,7 +33,7 @@ import org.sonar.batch.bootstrap.AnalysisMode;
import org.sonar.batch.bootstrap.BootstrapProperties;
import org.sonar.batch.bootstrap.GlobalSettings;
import org.sonar.batch.protocol.input.GlobalReferentials;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import java.util.Collections;
@@ -46,7 +46,7 @@ public class ProjectSettingsTest {
@Rule
public ExpectedException thrown = ExpectedException.none();
- ProjectReferentials projectRef;
+ ProjectRepository projectRef;
ProjectDefinition project = ProjectDefinition.create().setKey("struts");
GlobalSettings bootstrapProps;
@@ -54,7 +54,7 @@ public class ProjectSettingsTest {
@Before
public void prepare() {
- projectRef = new ProjectReferentials();
+ projectRef = new ProjectRepository();
mode = mock(AnalysisMode.class);
bootstrapProps = new GlobalSettings(new BootstrapProperties(Collections.<String, String>emptyMap()), new PropertyDefinitions(), new GlobalReferentials(), mode);
}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/FileMetadataTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/FileMetadataTest.java
index e6c73e80fd7..7b01635b67e 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/FileMetadataTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/FileMetadataTest.java
@@ -77,9 +77,9 @@ public class FileMetadataTest {
assertThat(metadata.lines).isEqualTo(4);
assertThat(metadata.hash).isEqualTo(md5Hex("föo\nbàr\n\u1D11Ebaßz\n"));
assertThat(metadata.originalLineOffsets).containsOnly(0, 5, 10, 18);
- assertThat(metadata.lineHashes[0]).containsOnly(md5("föo"));
- assertThat(metadata.lineHashes[1]).containsOnly(md5("bàr"));
- assertThat(metadata.lineHashes[2]).containsOnly(md5("\u1D11Ebaßz"));
+ assertThat(metadata.lineHashes[0]).containsExactly(md5("föo"));
+ assertThat(metadata.lineHashes[1]).containsExactly(md5("bàr"));
+ assertThat(metadata.lineHashes[2]).containsExactly(md5("\u1D11Ebaßz"));
assertThat(metadata.lineHashes[3]).isNull();
}
@@ -92,9 +92,9 @@ public class FileMetadataTest {
assertThat(metadata.lines).isEqualTo(4);
assertThat(metadata.hash).isEqualTo(md5Hex("föo\nbàr\n\u1D11Ebaßz\n"));
assertThat(metadata.originalLineOffsets).containsOnly(0, 5, 10, 18);
- assertThat(metadata.lineHashes[0]).containsOnly(md5("föo"));
- assertThat(metadata.lineHashes[1]).containsOnly(md5("bàr"));
- assertThat(metadata.lineHashes[2]).containsOnly(md5("\u1D11Ebaßz"));
+ assertThat(metadata.lineHashes[0]).containsExactly(md5("föo"));
+ assertThat(metadata.lineHashes[1]).containsExactly(md5("bàr"));
+ assertThat(metadata.lineHashes[2]).containsExactly(md5("\u1D11Ebaßz"));
assertThat(metadata.lineHashes[3]).isNull();
}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java
index 5b1badcd0a5..ddc9d1d376b 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionFactoryTest.java
@@ -20,7 +20,7 @@
package org.sonar.batch.scan.filesystem;
import org.junit.Test;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -28,7 +28,7 @@ import static org.mockito.Mockito.mock;
public class StatusDetectionFactoryTest {
@Test
public void testCreate() throws Exception {
- StatusDetectionFactory factory = new StatusDetectionFactory(mock(ProjectReferentials.class));
+ StatusDetectionFactory factory = new StatusDetectionFactory(mock(ProjectRepository.class));
StatusDetection detection = factory.create();
assertThat(detection).isNotNull();
}
diff --git a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java
index df3a94ce132..ca3282fc393 100644
--- a/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java
+++ b/sonar-batch/src/test/java/org/sonar/batch/scan/filesystem/StatusDetectionTest.java
@@ -22,14 +22,14 @@ package org.sonar.batch.scan.filesystem;
import org.junit.Test;
import org.sonar.api.batch.fs.InputFile;
import org.sonar.batch.protocol.input.FileData;
-import org.sonar.batch.protocol.input.ProjectReferentials;
+import org.sonar.batch.protocol.input.ProjectRepository;
import static org.assertj.core.api.Assertions.assertThat;
public class StatusDetectionTest {
@Test
public void detect_status() throws Exception {
- ProjectReferentials ref = new ProjectReferentials();
+ ProjectRepository ref = new ProjectRepository();
ref.addFileData("foo", "src/Foo.java", new FileData("ABCDE", true, null, null, null));
ref.addFileData("foo", "src/Bar.java", new FileData("FGHIJ", true, null, null, null));
StatusDetection statusDetection = new StatusDetection(ref);
diff --git a/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures b/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures
index 388d08b58a8..9eaf8ba2549 100644
--- a/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures
+++ b/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/HelloJava.xoo.measures
@@ -1,3 +1,2 @@
-lines:8
ncloc:3
complexity:1
diff --git a/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures b/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures
index c47948fc955..d2c8386aed1 100644
--- a/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures
+++ b/sonar-batch/src/test/resources/mediumtest/xoo/sample/xources/hello/helloscala.xoo.measures
@@ -1,3 +1,2 @@
-lines:5
ncloc:5
complexity:2
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v1.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v1.txt
new file mode 100644
index 00000000000..1920333ddb6
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v1.txt
@@ -0,0 +1,12 @@
+package example1;
+
+public class Toto {
+
+ public void doSomething() {
+ // doSomething
+ }
+
+ public void doSomethingElse() {
+ // doSomethingElse
+ }
+}
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v2.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v2.txt
new file mode 100644
index 00000000000..231532452b2
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example1-v2.txt
@@ -0,0 +1,22 @@
+package example1;
+
+public class Toto {
+
+ public Toto(){}
+
+ public void doSomethingNew() {
+ // doSomethingNew
+ }
+
+ public void doSomethingElseNew() {
+ // doSomethingElseNew
+ }
+
+ public void doSomething() {
+ // doSomething
+ }
+
+ public void doSomethingElse() {
+ // doSomethingElse
+ }
+}
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v1.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v1.txt
new file mode 100644
index 00000000000..a920afe459b
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v1.txt
@@ -0,0 +1,7 @@
+package example2;
+
+public class Toto {
+ void method1() {
+ System.out.println("toto");
+ }
+}
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v2.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v2.txt
new file mode 100644
index 00000000000..c5c8250cf65
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example2-v2.txt
@@ -0,0 +1,16 @@
+package example2;
+
+public class Toto {
+
+ void method2() {
+ System.out.println("toto");
+ }
+
+ void method1() {
+ System.out.println("toto");
+ }
+
+ void method3() {
+ System.out.println("toto");
+ }
+}
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v1.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v1.txt
new file mode 100644
index 00000000000..facdcbc008c
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v1.txt
@@ -0,0 +1,16 @@
+package sample;
+
+public class Sample {
+
+ public Sample(int i) {
+ int j = i+1; // violation: unused local variable
+ }
+
+ public boolean avoidUtilityClass() {
+ return true;
+ }
+
+ private String myMethod() { // violation : unused private method
+ return "hello";
+ }
+}
diff --git a/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v2.txt b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v2.txt
new file mode 100644
index 00000000000..91db843fc4d
--- /dev/null
+++ b/sonar-batch/src/test/resources/org/sonar/batch/issue/tracking/IssueTrackingTest/example3-v2.txt
@@ -0,0 +1,20 @@
+package sample;
+
+public class Sample {
+
+ public Sample(int i) {
+ int j = i+1; // still the same violation: unused local variable
+ }
+
+ public boolean avoidUtilityClass() {
+ return true;
+ }
+
+ private String myMethod() { // violation "unused private method" is fixed because it's called in newViolation
+ return "hello";
+ }
+
+ public void newViolation() {
+ String msg = myMethod(); // new violation : msg is an unused variable
+ }
+}