diff options
author | Julien HENRY <julien.henry@sonarsource.com> | 2015-01-21 11:22:44 +0100 |
---|---|---|
committer | Julien HENRY <julien.henry@sonarsource.com> | 2015-01-23 09:59:47 +0100 |
commit | 1340ee7da7a1688ebb059812504e117d041e0124 (patch) | |
tree | 171deefeac6057e876106131ac333bd4914d9253 /sonar-batch | |
parent | 97ca9e16fc27e19f021558502fdce23fe9a77460 (diff) | |
download | sonarqube-1340ee7da7a1688ebb059812504e117d041e0124.tar.gz sonarqube-1340ee7da7a1688ebb059812504e117d041e0124.zip |
SONAR-6012 Local issue tracking
Diffstat (limited to 'sonar-batch')
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 + } +} |