diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-06-03 10:31:16 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2015-07-02 16:06:08 +0200 |
commit | bf4118d6a9ceb9ad24274cdc6537d4a607121815 (patch) | |
tree | 8f758ccb7a205da3eae96b05b74f79cade8ceae0 /sonar-core | |
parent | 2f948758eebec934beb54701792cf2d558319251 (diff) | |
download | sonarqube-bf4118d6a9ceb9ad24274cdc6537d4a607121815.tar.gz sonarqube-bf4118d6a9ceb9ad24274cdc6537d4a607121815.zip |
SONAR-6623 extract issue tracking algorithm from batch
Diffstat (limited to 'sonar-core')
50 files changed, 2696 insertions, 228 deletions
diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java new file mode 100644 index 00000000000..9439aad99c5 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -0,0 +1,613 @@ +/* + * 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.core.issue; + +import com.google.common.base.Objects; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Maps; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.builder.ToStringBuilder; +import org.apache.commons.lang.builder.ToStringStyle; +import org.apache.commons.lang.time.DateUtils; +import org.sonar.api.issue.Issue; +import org.sonar.api.issue.IssueComment; +import org.sonar.api.rule.RuleKey; +import org.sonar.api.rule.Severity; +import org.sonar.api.utils.Duration; +import org.sonar.core.issue.tracking.Trackable; + +/** + * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING. + * + * @since 3.6 + */ +public class DefaultIssue implements Issue, Trackable { + + private String key; + + private String componentUuid; + private String componentKey; + + private String moduleUuid; + private String moduleUuidPath; + + private String projectUuid; + private String projectKey; + + private RuleKey ruleKey; + private String language; + private String severity; + private boolean manualSeverity = false; + private String message; + private Integer line; + private Double effortToFix; + private Duration debt; + private String status; + private String resolution; + private String reporter; + private String assignee; + private String checksum; + private Map<String, String> attributes = null; + private String authorLogin = null; + private String actionPlanKey; + private List<IssueComment> comments = null; + private Set<String> tags = null; + + // FUNCTIONAL DATES + private Date creationDate; + private Date updateDate; + private Date closeDate; + + // FOLLOWING FIELDS ARE AVAILABLE ONLY DURING SCAN + + // Current changes + private FieldDiffs currentChange = null; + + // all changes + private List<FieldDiffs> changes = null; + + // true if the the issue did not exist in the previous scan. + private boolean isNew = true; + + // True if the the issue did exist in the previous scan but not in the current one. That means + // that this issue should be closed. + private boolean beingClosed = false; + + private boolean onDisabledRule = false; + + // true if some fields have been changed since the previous scan + private boolean isChanged = false; + + // true if notifications have to be sent + private boolean sendNotifications = false; + + // Date when issue was loaded from db (only when isNew=false) + private Long selectedAt; + + @Override + public String key() { + return key; + } + + public DefaultIssue setKey(String key) { + this.key = key; + return this; + } + + /** + * Can be null on Views or Devs + */ + @Override + @CheckForNull + public String componentUuid() { + return componentUuid; + } + + public DefaultIssue setComponentUuid(@CheckForNull String componentUuid) { + this.componentUuid = componentUuid; + return this; + } + + @Override + public String componentKey() { + return componentKey; + } + + public DefaultIssue setComponentKey(String s) { + this.componentKey = s; + return this; + } + + @CheckForNull + public String moduleUuid() { + return moduleUuid; + } + + public DefaultIssue setModuleUuid(@Nullable String moduleUuid) { + this.moduleUuid = moduleUuid; + return this; + } + + @CheckForNull + public String moduleUuidPath() { + return moduleUuidPath; + } + + public DefaultIssue setModuleUuidPath(@Nullable String moduleUuidPath) { + this.moduleUuidPath = moduleUuidPath; + return this; + } + + /** + * Can be null on Views or Devs + */ + @Override + @CheckForNull + public String projectUuid() { + return projectUuid; + } + + public DefaultIssue setProjectUuid(@Nullable String projectUuid) { + this.projectUuid = projectUuid; + return this; + } + + @Override + public String projectKey() { + return projectKey; + } + + public DefaultIssue setProjectKey(String projectKey) { + this.projectKey = projectKey; + return this; + } + + @Override + public RuleKey ruleKey() { + return ruleKey; + } + + public DefaultIssue setRuleKey(RuleKey k) { + this.ruleKey = k; + return this; + } + + @Override + public String language() { + return language; + } + + public DefaultIssue setLanguage(String l) { + this.language = l; + return this; + } + + @Override + public String severity() { + return severity; + } + + public DefaultIssue setSeverity(@Nullable String s) { + Preconditions.checkArgument(s == null || Severity.ALL.contains(s), "Not a valid severity: " + s); + this.severity = s; + return this; + } + + public boolean manualSeverity() { + return manualSeverity; + } + + public DefaultIssue setManualSeverity(boolean b) { + this.manualSeverity = b; + return this; + } + + @Override + @CheckForNull + public String message() { + return message; + } + + public DefaultIssue setMessage(@Nullable String s) { + this.message = StringUtils.abbreviate(StringUtils.trim(s), MESSAGE_MAX_SIZE); + return this; + } + + @Override + @CheckForNull + public Integer line() { + return line; + } + + public DefaultIssue setLine(@Nullable Integer l) { + Preconditions.checkArgument(l == null || l > 0, "Line must be null or greater than zero (got " + l + ")"); + this.line = l; + return this; + } + + @Override + @CheckForNull + public Double effortToFix() { + return effortToFix; + } + + public DefaultIssue setEffortToFix(@Nullable Double d) { + Preconditions.checkArgument(d == null || d >= 0, "Effort to fix must be greater than or equal 0 (got " + d + ")"); + this.effortToFix = d; + return this; + } + + /** + * Elapsed time to fix the issue + */ + @Override + @CheckForNull + public Duration debt() { + return debt; + } + + @CheckForNull + public Long debtInMinutes() { + return debt != null ? debt.toMinutes() : null; + } + + public DefaultIssue setDebt(@Nullable Duration t) { + this.debt = t; + return this; + } + + @Override + public String status() { + return status; + } + + public DefaultIssue setStatus(String s) { + Preconditions.checkArgument(!Strings.isNullOrEmpty(s), "Status must be set"); + this.status = s; + return this; + } + + @Override + @CheckForNull + public String resolution() { + return resolution; + } + + public DefaultIssue setResolution(@Nullable String s) { + this.resolution = s; + return this; + } + + @Override + @CheckForNull + public String reporter() { + return reporter; + } + + public DefaultIssue setReporter(@Nullable String s) { + this.reporter = s; + return this; + } + + @Override + @CheckForNull + public String assignee() { + return assignee; + } + + public DefaultIssue setAssignee(@Nullable String s) { + this.assignee = s; + return this; + } + + @Override + public Date creationDate() { + return creationDate; + } + + public DefaultIssue setCreationDate(Date d) { + // d is not marked as Nullable but we still allow null parameter for unit testing. + this.creationDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null); + return this; + } + + @Override + @CheckForNull + public Date updateDate() { + return updateDate; + } + + public DefaultIssue setUpdateDate(@Nullable Date d) { + this.updateDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null); + return this; + } + + @Override + @CheckForNull + public Date closeDate() { + return closeDate; + } + + public DefaultIssue setCloseDate(@Nullable Date d) { + this.closeDate = (d != null ? DateUtils.truncate(d, Calendar.SECOND) : null); + return this; + } + + @CheckForNull + public String checksum() { + return checksum; + } + + public DefaultIssue setChecksum(@Nullable String s) { + this.checksum = s; + return this; + } + + @Override + public boolean isNew() { + return isNew; + } + + public DefaultIssue setNew(boolean b) { + isNew = b; + return this; + } + + /** + * True when one of the following conditions is true : + * <ul> + * <li>the related component has been deleted or renamed</li> + * <li>the rule has been deleted (eg. on plugin uninstall)</li> + * <li>the rule has been disabled in the Quality profile</li> + * </ul> + */ + public boolean isBeingClosed() { + return beingClosed; + } + + public DefaultIssue setBeingClosed(boolean b) { + beingClosed = b; + return this; + } + + public boolean isOnDisabledRule() { + return onDisabledRule; + } + + public DefaultIssue setOnDisabledRule(boolean b) { + onDisabledRule = b; + return this; + } + + public boolean isChanged() { + return isChanged; + } + + public DefaultIssue setChanged(boolean b) { + isChanged = b; + return this; + } + + public boolean mustSendNotifications() { + return sendNotifications; + } + + public DefaultIssue setSendNotifications(boolean b) { + sendNotifications = b; + return this; + } + + @Override + @CheckForNull + public String attribute(String key) { + return attributes == null ? null : attributes.get(key); + } + + public DefaultIssue setAttribute(String key, @Nullable String value) { + if (attributes == null) { + attributes = Maps.newHashMap(); + } + if (value == null) { + attributes.remove(key); + } else { + attributes.put(key, value); + } + return this; + } + + @Override + public Map<String, String> attributes() { + return attributes == null ? Collections.<String, String>emptyMap() : ImmutableMap.copyOf(attributes); + } + + public DefaultIssue setAttributes(@Nullable Map<String, String> map) { + if (map != null) { + if (attributes == null) { + attributes = Maps.newHashMap(); + } + attributes.putAll(map); + } + return this; + } + + @Override + @CheckForNull + public String authorLogin() { + return authorLogin; + } + + public DefaultIssue setAuthorLogin(@Nullable String s) { + this.authorLogin = s; + return this; + } + + @Override + @CheckForNull + public String actionPlanKey() { + return actionPlanKey; + } + + public DefaultIssue setActionPlanKey(@Nullable String actionPlanKey) { + this.actionPlanKey = actionPlanKey; + return this; + } + + public DefaultIssue setFieldChange(IssueChangeContext context, String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) { + if (!Objects.equal(oldValue, newValue)) { + if (currentChange == null) { + currentChange = new FieldDiffs(); + currentChange.setUserLogin(context.login()); + currentChange.setCreationDate(context.date()); + } + currentChange.setDiff(field, oldValue, newValue); + } + addChange(currentChange); + return this; + } + + public DefaultIssue setCurrentChange(FieldDiffs currentChange) { + this.currentChange = currentChange; + addChange(currentChange); + return this; + } + + @CheckForNull + public FieldDiffs currentChange() { + return currentChange; + } + + public DefaultIssue addChange(FieldDiffs change) { + if (changes == null) { + changes = new ArrayList<>(); + } + changes.add(change); + return this; + } + + public DefaultIssue setChanges(List<FieldDiffs> changes) { + this.changes = changes; + return this; + } + + public List<FieldDiffs> changes() { + if (changes == null) { + return Collections.emptyList(); + } + return ImmutableList.copyOf(changes); + } + + public DefaultIssue addComment(DefaultIssueComment comment) { + if (comments == null) { + comments = new ArrayList<>(); + } + comments.add(comment); + return this; + } + + @Override + public List<IssueComment> comments() { + if (comments == null) { + return Collections.emptyList(); + } + return ImmutableList.copyOf(comments); + } + + @CheckForNull + public Long selectedAt() { + return selectedAt; + } + + public DefaultIssue setSelectedAt(@Nullable Long d) { + this.selectedAt = d; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DefaultIssue that = (DefaultIssue) o; + return !(key != null ? !key.equals(that.key) : (that.key != null)); + } + + @Override + public int hashCode() { + return key != null ? key.hashCode() : 0; + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public Set<String> tags() { + if (tags == null) { + return ImmutableSet.of(); + } else { + return ImmutableSet.copyOf(tags); + } + } + + public DefaultIssue setTags(Collection<String> tags) { + this.tags = new LinkedHashSet<>(tags); + return this; + } + + @Override + public Integer getLine() { + return line; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String getLineHash() { + return checksum; + } + + @Override + public RuleKey getRuleKey() { + return ruleKey; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java index 3c39b5dadde..962a4591916 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueBuilder.java @@ -23,7 +23,6 @@ import com.google.common.base.Preconditions; import com.google.common.collect.Maps; import org.sonar.api.issue.Issuable; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.internal.Uuids; @@ -132,7 +131,7 @@ public class DefaultIssueBuilder implements Issuable.IssueBuilder { issue.setStatus(Issue.STATUS_OPEN); issue.setCloseDate(null); issue.setNew(true); - issue.setEndOfLife(false); + issue.setBeingClosed(false); issue.setOnDisabledRule(false); return issue; } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java new file mode 100644 index 00000000000..3a5b23b0056 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssueComment.java @@ -0,0 +1,130 @@ +/* + * 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.core.issue; + +import org.sonar.api.issue.IssueComment; +import org.sonar.api.utils.internal.Uuids; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import java.io.Serializable; +import java.util.Date; + +/** + * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING. + * + * @since 3.6 + */ +public class DefaultIssueComment implements Serializable, IssueComment { + + private String issueKey; + private String userLogin; + private Date createdAt; + private Date updatedAt; + private String key; + private String markdownText; + private boolean isNew; + + public static DefaultIssueComment create(String issueKey, @Nullable String login, String markdownText) { + DefaultIssueComment comment = new DefaultIssueComment(); + comment.setIssueKey(issueKey); + comment.setKey(Uuids.create()); + Date now = new Date(); + comment.setUserLogin(login); + comment.setMarkdownText(markdownText); + comment.setCreatedAt(now).setUpdatedAt(now); + comment.setNew(true); + return comment; + } + + @Override + public String markdownText() { + return markdownText; + } + + public DefaultIssueComment setMarkdownText(String s) { + this.markdownText = s; + return this; + } + + @Override + public String issueKey() { + return issueKey; + } + + public DefaultIssueComment setIssueKey(String s) { + this.issueKey = s; + return this; + } + + @Override + public String key() { + return key; + } + + public DefaultIssueComment setKey(String key) { + this.key = key; + return this; + } + + /** + * The user who created the comment. Null if it was automatically generated during project scan. + */ + @Override + @CheckForNull + public String userLogin() { + return userLogin; + } + + public DefaultIssueComment setUserLogin(@Nullable String userLogin) { + this.userLogin = userLogin; + return this; + } + + @Override + public Date createdAt() { + return createdAt; + } + + public DefaultIssueComment setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + return this; + } + + @Override + public Date updatedAt() { + return updatedAt; + } + + public DefaultIssueComment setUpdatedAt(@Nullable Date updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public boolean isNew() { + return isNew; + } + + public DefaultIssueComment setNew(boolean b) { + isNew = b; + return this; + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java new file mode 100644 index 00000000000..d162056a795 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/FieldDiffs.java @@ -0,0 +1,195 @@ +/* + * 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.core.issue; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.Maps; +import java.io.Serializable; +import java.util.Date; +import java.util.Map; +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +/** + * PLUGINS MUST NOT USE THIS CLASS, EXCEPT FOR UNIT TESTING. + * + * @since 3.6 + */ +public class FieldDiffs implements Serializable { + + public static final Splitter FIELDS_SPLITTER = Splitter.on(',').omitEmptyStrings(); + + private String issueKey; + private String userLogin; + private Date creationDate; + + private final Map<String, Diff> diffs = Maps.newLinkedHashMap(); + + public Map<String, Diff> diffs() { + return diffs; + } + + public Diff get(String field) { + return diffs.get(field); + } + + @CheckForNull + public String userLogin() { + return userLogin; + } + + public FieldDiffs setUserLogin(@Nullable String s) { + this.userLogin = s; + return this; + } + + public Date creationDate() { + return creationDate; + } + + public FieldDiffs setCreationDate(Date creationDate) { + this.creationDate = creationDate; + return this; + } + + public String issueKey() { + return issueKey; + } + + public FieldDiffs setIssueKey(String issueKey) { + this.issueKey = issueKey; + return this; + } + + @SuppressWarnings("unchecked") + public FieldDiffs setDiff(String field, @Nullable Serializable oldValue, @Nullable Serializable newValue) { + Diff diff = diffs.get(field); + if (diff == null) { + diff = new Diff(oldValue, newValue); + diffs.put(field, diff); + } else { + diff.setNewValue(newValue); + } + return this; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + boolean notFirst = false; + for (Map.Entry<String, Diff> entry : diffs.entrySet()) { + if (notFirst) { + sb.append(','); + } else { + notFirst = true; + } + sb.append(entry.getKey()); + sb.append('='); + sb.append(entry.getValue().toString()); + } + return sb.toString(); + } + + public static FieldDiffs parse(@Nullable String s) { + FieldDiffs diffs = new FieldDiffs(); + if (!Strings.isNullOrEmpty(s)) { + Iterable<String> fields = FIELDS_SPLITTER.split(s); + for (String field : fields) { + String[] keyValues = field.split("="); + if (keyValues.length == 2) { + String[] values = keyValues[1].split("\\|"); + String oldValue = ""; + String newValue = ""; + if(values.length == 1) { + newValue = Strings.nullToEmpty(values[0]); + } else if(values.length == 2) { + oldValue = Strings.nullToEmpty(values[0]); + newValue = Strings.nullToEmpty(values[1]); + } + diffs.setDiff(keyValues[0], oldValue, newValue); + } else { + diffs.setDiff(keyValues[0], "", ""); + } + } + } + return diffs; + } + + public static class Diff<T extends Serializable> implements Serializable { + private T oldValue; + private T newValue; + + public Diff(@Nullable T oldValue, @Nullable T newValue) { + this.oldValue = oldValue; + this.newValue = newValue; + } + + @CheckForNull + public T oldValue() { + return oldValue; + } + + @CheckForNull + public Long oldValueLong() { + return toLong(oldValue); + } + + @CheckForNull + public T newValue() { + return newValue; + } + + @CheckForNull + public Long newValueLong() { + return toLong(newValue); + } + + void setNewValue(T t) { + this.newValue = t; + } + + @CheckForNull + private static Long toLong(@Nullable Serializable value) { + if (value != null && !"".equals(value)) { + try { + return Long.valueOf((String) value); + } catch (ClassCastException e) { + return (Long) value; + } + } + return null; + } + + @Override + public String toString() { + //TODO escape , and | characters + StringBuilder sb = new StringBuilder(); + if(newValue != null) { + if(oldValue != null) { + sb.append(oldValue.toString()); + sb.append('|'); + } + sb.append(newValue.toString()); + } + return sb.toString(); + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.java new file mode 100644 index 00000000000..1df5bd5e3eb --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueChangeContext.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.core.issue; + +import javax.annotation.CheckForNull; +import javax.annotation.Nullable; + +import java.io.Serializable; +import java.util.Date; + +/** + * PLUGINS MUST NOT BE USED THIS CLASS, EXCEPT FOR UNIT TESTING. + * + * @since 3.6 + */ +public class IssueChangeContext implements Serializable { + + private final String login; + private final Date date; + private final boolean scan; + + private IssueChangeContext(@Nullable String login, Date date, boolean scan) { + this.login = login; + this.date = date; + this.scan = scan; + } + + @CheckForNull + public String login() { + return login; + } + + public Date date() { + return date; + } + + public boolean scan() { + return scan; + } + + public static IssueChangeContext createScan(Date date) { + return new IssueChangeContext(null, date, true); + } + + public static IssueChangeContext createUser(Date date, @Nullable String login) { + return new IssueChangeContext(login, date, false); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java b/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java index e91e7bbdbb4..cb3bb6079aa 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/IssueUpdater.java @@ -30,9 +30,6 @@ import org.apache.commons.lang.time.DateUtils; import org.sonar.api.batch.BatchSide; import org.sonar.api.server.ServerSide; import org.sonar.api.issue.ActionPlan; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.IssueChangeContext; import org.sonar.api.server.rule.RuleTagFormat; import org.sonar.api.user.User; import org.sonar.api.utils.Duration; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java index 1155e70c62d..a400a04c9cd 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDao.java @@ -29,8 +29,8 @@ import java.util.Map; import javax.annotation.CheckForNull; import org.apache.ibatis.session.ResultHandler; import org.sonar.api.batch.BatchSide; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.FieldDiffs; import org.sonar.api.server.ServerSide; import org.sonar.core.persistence.DaoComponent; import org.sonar.core.persistence.DbSession; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java index 18b731018f1..0d79cd47ca4 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueChangeDto.java @@ -21,8 +21,8 @@ package org.sonar.core.issue.db; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.FieldDiffs; import org.sonar.api.utils.System2; import javax.annotation.CheckForNull; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java index 1a0208b7756..c4e2e516cf2 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDao.java @@ -20,7 +20,6 @@ package org.sonar.core.issue.db; -import javax.annotation.CheckForNull; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.SqlSession; import org.sonar.api.batch.BatchSide; @@ -41,16 +40,6 @@ public class IssueDao { this.mybatis = mybatis; } - @CheckForNull - public IssueDto selectByKey(String key) { - DbSession session = mybatis.openSession(false); - try { - return mapper(session).selectByKey(key); - } finally { - MyBatis.closeQuietly(session); - } - } - public void selectNonClosedIssuesByModule(long componentId, ResultHandler handler) { SqlSession session = mybatis.openSession(false); try { diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java index 9b726f4b4e3..44baff06b8c 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueDto.java @@ -24,9 +24,10 @@ import com.google.common.base.Objects; import com.google.common.base.Preconditions; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; +import java.util.Set; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import org.sonar.api.resources.Project; import org.sonar.api.rule.RuleKey; import org.sonar.api.utils.Duration; @@ -624,7 +625,7 @@ public final class IssueDto implements Serializable { return this; } - public Collection<String> getTags() { + public Set<String> getTags() { return ImmutableSet.copyOf(TAGS_SPLITTER.split(tags == null ? "" : tags)); } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java index 569fa659449..aba2531071e 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueMapper.java @@ -20,11 +20,16 @@ package org.sonar.core.issue.db; import java.util.List; +import java.util.Set; public interface IssueMapper { IssueDto selectByKey(String key); + List<IssueDto> selectOpenByComponentUuid(String componentUuid); + + Set<String> selectComponentUuidsOfOpenIssuesForProjectUuid(String projectUuid); + List<IssueDto> selectByKeys(List<String> keys); List<IssueDto> selectByActionPlan(String actionPlan); @@ -34,5 +39,4 @@ public interface IssueMapper { int update(IssueDto issue); int updateIfBeforeSelectedDate(IssueDto issue); - } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java index c4533c9bf0d..fb443d7cc9e 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/IssueStorage.java @@ -21,9 +21,9 @@ package org.sonar.core.issue.db; import org.sonar.api.issue.Issue; import org.sonar.api.issue.IssueComment; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.FieldDiffs; import org.sonar.api.rules.Rule; import org.sonar.api.rules.RuleFinder; import org.sonar.core.persistence.BatchSession; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java index 4fbd892d499..7c6b48a1603 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/db/UpdateConflictResolver.java @@ -20,17 +20,19 @@ package org.sonar.core.issue.db; import com.google.common.annotations.VisibleForTesting; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.utils.log.Logger; +import org.sonar.api.utils.log.Loggers; +import org.sonar.core.issue.DefaultIssue; /** * Support concurrent modifications on issues made by analysis and users at the same time * See https://jira.sonarsource.com/browse/SONAR-4309 + * + * TODO move to compute engine */ public class UpdateConflictResolver { - private static final Logger LOG = LoggerFactory.getLogger(IssueStorage.class); + private static final Logger LOG = Loggers.get(UpdateConflictResolver.class); public void resolve(DefaultIssue issue, IssueMapper mapper) { LOG.debug("Resolve conflict on issue " + issue.key()); diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java new file mode 100644 index 00000000000..7a4d38671ca --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockHashSequence.java @@ -0,0 +1,90 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.core.issue.tracking; + +import java.util.List; + +public class BlockHashSequence { + + public static final int DEFAULT_HALF_BLOCK_SIZE = 5; + /** + * Hashes of blocks around lines. Line 1 is at index 0. + */ + private final int[] blockHashes; + + BlockHashSequence(LineHashSequence lineHashSequence, int halfBlockSize) { + this.blockHashes = new int[lineHashSequence.length()]; + + BlockHashFactory blockHashFactory = new BlockHashFactory(lineHashSequence.getHashes(), halfBlockSize); + for (int line = 1; line <= lineHashSequence.length(); line++) { + blockHashes[line - 1] = blockHashFactory.getHash(); + if (line - halfBlockSize > 0) { + blockHashFactory.remove(lineHashSequence.getHashForLine(line - halfBlockSize).hashCode()); + } + if (line + 1 + halfBlockSize <= lineHashSequence.length()) { + blockHashFactory.add(lineHashSequence.getHashForLine(line + 1 + halfBlockSize).hashCode()); + } else { + blockHashFactory.add(0); + } + } + } + + public static BlockHashSequence create(LineHashSequence lineHashSequence) { + return new BlockHashSequence(lineHashSequence, DEFAULT_HALF_BLOCK_SIZE); + } + + /** + * Hash of block around line. Line must be in range of valid lines. It starts with 1. + */ + public int getBlockHashForLine(int line) { + return blockHashes[line - 1]; + } + + private static class BlockHashFactory { + private static final int PRIME_BASE = 31; + + private final int power; + private int hash = 0; + + public BlockHashFactory(List<String> hashes, int halfBlockSize) { + int pow = 1; + for (int i = 0; i < halfBlockSize * 2; i++) { + pow = pow * PRIME_BASE; + } + this.power = pow; + for (int i = 1; i <= Math.min(hashes.size(), halfBlockSize + 1); i++) { + add(hashes.get(i - 1).hashCode()); + } + } + + 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-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java new file mode 100644 index 00000000000..d4361819fbb --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/BlockRecognizer.java @@ -0,0 +1,177 @@ +/* + * 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.core.issue.tracking; + +import com.google.common.collect.LinkedHashMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class BlockRecognizer<RAW extends Trackable, BASE extends Trackable> { + + /** + * If base source code is available, then detect code moves through block hashes. + * Only the issues associated to a line can be matched here. + */ + void match(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) { + BlockHashSequence baseHashSequence = baseInput.getBlockHashSequence(); + BlockHashSequence rawHashSequence = rawInput.getBlockHashSequence(); + + Multimap<Integer, RAW> rawsByLine = groupByLine(tracking.getUnmatchedRaws()); + Multimap<Integer, BASE> basesByLine = groupByLine(tracking.getUnmatchedBases()); + Map<Integer, HashOccurrence> map = new HashMap<>(); + + for (Integer line : basesByLine.keySet()) { + int hash = baseHashSequence.getBlockHashForLine(line); + HashOccurrence hashOccurrence = map.get(hash); + if (hashOccurrence == null) { + // first occurrence in base + hashOccurrence = new HashOccurrence(); + hashOccurrence.baseLine = line; + hashOccurrence.baseCount = 1; + map.put(hash, hashOccurrence); + } else { + hashOccurrence.baseCount++; + } + } + + for (Integer line : rawsByLine.keySet()) { + int hash = rawHashSequence.getBlockHashForLine(line); + HashOccurrence hashOccurrence = map.get(hash); + if (hashOccurrence != null) { + hashOccurrence.rawLine = line; + hashOccurrence.rawCount++; + } + } + + for (HashOccurrence hashOccurrence : map.values()) { + if (hashOccurrence.baseCount == 1 && hashOccurrence.rawCount == 1) { + // Guaranteed that baseLine has been moved to rawLine, so we can map all issues on baseLine to all issues on rawLine + map(rawsByLine.get(hashOccurrence.rawLine), basesByLine.get(hashOccurrence.baseLine), tracking); + basesByLine.removeAll(hashOccurrence.baseLine); + rawsByLine.removeAll(hashOccurrence.rawLine); + } + } + + // Check if remaining number of lines exceeds threshold + if (basesByLine.keySet().size() * rawsByLine.keySet().size() < 250000) { + List<LinePair> possibleLinePairs = Lists.newArrayList(); + for (Integer baseLine : basesByLine.keySet()) { + for (Integer rawLine : rawsByLine.keySet()) { + int weight = lengthOfMaximalBlock(baseInput.getLineHashSequence(), baseLine, rawInput.getLineHashSequence(), rawLine); + possibleLinePairs.add(new LinePair(baseLine, rawLine, weight)); + } + } + Collections.sort(possibleLinePairs, LinePairComparator.INSTANCE); + for (LinePair linePair : possibleLinePairs) { + // High probability that baseLine has been moved to rawLine, so we can map all Issues on baseLine to all Issues on rawLine + map(rawsByLine.get(linePair.rawLine), basesByLine.get(linePair.baseLine), tracking); + } + } + } + + /** + * @param startLineA number of line from first version of text (numbering starts from 1) + * @param startLineB number of line from second version of text (numbering starts from 1) + */ + static int lengthOfMaximalBlock(LineHashSequence hashesA, int startLineA, LineHashSequence hashesB, int startLineB) { + if (!hashesA.getHashForLine(startLineA).equals(hashesB.getHashForLine(startLineB))) { + return 0; + } + int length = 0; + int ai = startLineA; + int bi = startLineB; + while (ai <= hashesA.length() && bi <= hashesB.length() && hashesA.getHashForLine(ai).equals(hashesB.getHashForLine(bi))) { + ai++; + bi++; + length++; + } + ai = startLineA; + bi = startLineB; + while (ai > 0 && bi > 0 && hashesA.getHashForLine(ai).equals(hashesB.getHashForLine(bi))) { + ai--; + bi--; + length++; + } + // Note that position (startA, startB) was counted twice + return length - 1; + } + + private void map(Collection<RAW> raws, Collection<BASE> bases, Tracking<RAW, BASE> result) { + for (RAW raw : raws) { + for (BASE base : bases) { + if (result.containsUnmatchedBase(base) && base.getRuleKey().equals(raw.getRuleKey())) { + result.associateRawToBase(raw, base); + result.markRawAsAssociated(raw); + break; + } + } + } + } + + private static <T extends Trackable> Multimap<Integer, T> groupByLine(Collection<T> trackables) { + Multimap<Integer, T> result = LinkedHashMultimap.create(); + for (T trackable : trackables) { + Integer line = trackable.getLine(); + if (line != null) { + result.put(line, trackable); + } + } + return result; + } + + private static class LinePair { + int baseLine; + int rawLine; + int weight; + + public LinePair(int baseLine, int rawLine, int weight) { + this.baseLine = baseLine; + this.rawLine = rawLine; + this.weight = weight; + } + } + + private static class HashOccurrence { + int baseLine; + int rawLine; + int baseCount; + int rawCount; + } + + private enum LinePairComparator implements Comparator<LinePair> { + INSTANCE; + + @Override + public int compare(LinePair o1, LinePair o2) { + int weightDiff = o2.weight - o1.weight; + if (weightDiff != 0) { + return weightDiff; + } else { + return Math.abs(o1.baseLine - o1.rawLine) - Math.abs(o2.baseLine - o2.rawLine); + } + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.java new file mode 100644 index 00000000000..b0681c67c94 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Input.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.core.issue.tracking; + +import java.util.Collection; + +public interface Input<ISSUE extends Trackable> { + + LineHashSequence getLineHashSequence(); + BlockHashSequence getBlockHashSequence(); + Collection<ISSUE> getIssues(); + +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java new file mode 100644 index 00000000000..0406c631f09 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LazyInput.java @@ -0,0 +1,58 @@ +/* + * 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.core.issue.tracking; + +import java.util.Collection; +import java.util.List; + +public abstract class LazyInput<ISSUE extends Trackable> implements Input<ISSUE> { + + private List<ISSUE> issues; + private LineHashSequence lineHashSeq; + private BlockHashSequence blockHashSeq; + + @Override + public LineHashSequence getLineHashSequence() { + if (lineHashSeq == null) { + lineHashSeq = LineHashSequence.createForLines(loadSourceLines()); + } + return lineHashSeq; + } + + @Override + public BlockHashSequence getBlockHashSequence() { + if (blockHashSeq == null) { + blockHashSeq = BlockHashSequence.create(getLineHashSequence()); + } + return blockHashSeq; + } + + @Override + public Collection<ISSUE> getIssues() { + if (issues == null) { + issues = loadIssues(); + } + return issues; + } + + protected abstract Iterable<String> loadSourceLines(); + + protected abstract List<ISSUE> loadIssues(); +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java new file mode 100644 index 00000000000..b6be6b731af --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/LineHashSequence.java @@ -0,0 +1,119 @@ +/* + * 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.core.issue.tracking; + +import com.google.common.base.Strings; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; + +/** + * Sequence of hash of lines for a given file + */ +public class LineHashSequence { + + private static final int[] EMPTY_INTS = new int[0]; + + /** + * Hashes of lines. Line 1 is at index 0. No null elements. + */ + private final List<String> hashes; + private final Map<String, int[]> linesByHash; + + public LineHashSequence(List<String> hashes) { + this.hashes = hashes; + this.linesByHash = new HashMap<>(hashes.size()); + for (int line = 1; line <= hashes.size(); line++) { + String hash = hashes.get(line - 1); + int[] lines = linesByHash.get(hash); + linesByHash.put(hash, appendLineTo(line, lines)); + } + } + + /** + * Number of lines + */ + public int length() { + return hashes.size(); + } + + /** + * Checks if the line, starting with 1, is defined. + */ + public boolean hasLine(int line) { + return 0 < line && line <= hashes.size(); + } + + /** + * The lines, starting with 1, that matches the given hash. + */ + public int[] getLinesForHash(String hash) { + int[] lines = linesByHash.get(hash); + return lines == null ? EMPTY_INTS : lines; + } + + /** + * Hash of the given line, which starts with 1. Return empty string + * is the line does not exist. + */ + public String getHashForLine(int line) { + if (line > 0 && line <= hashes.size()) { + return Strings.nullToEmpty(hashes.get(line - 1)); + } + return ""; + } + + List<String> getHashes() { + return hashes; + } + + private static int[] appendLineTo(int line, @Nullable int[] to) { + int[] result; + if (to == null) { + result = new int[] {line}; + } else { + result = new int[to.length + 1]; + System.arraycopy(to, 0, result, 0, to.length); + result[result.length - 1] = line; + } + return result; + } + + public static LineHashSequence createForLines(Iterable<String> lines) { + List<String> hashes = new ArrayList<>(); + for (String line : lines) { + hashes.add(hash(line)); + } + return new LineHashSequence(hashes); + } + + // FIXME duplicates ComputeFileSourceData + private static String hash(String line) { + String reducedLine = StringUtils.replaceChars(line, "\t ", ""); + if (reducedLine.isEmpty()) { + return ""; + } + return DigestUtils.md5Hex(reducedLine); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java new file mode 100644 index 00000000000..c6e5512d19c --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Trackable.java @@ -0,0 +1,40 @@ +/* + * 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.core.issue.tracking; + +import javax.annotation.CheckForNull; +import org.sonar.api.rule.RuleKey; + +public interface Trackable { + + /** + * The line index, starting with 1. Null means that + * issue does not relate to a line (file issue for example). + */ + @CheckForNull + Integer getLine(); + + String getMessage(); + + @CheckForNull + String getLineHash(); + + RuleKey getRuleKey(); +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java new file mode 100644 index 00000000000..8691757440c --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracker.java @@ -0,0 +1,298 @@ +/* + * 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.core.issue.tracking; + +import com.google.common.base.Predicate; +import com.google.common.base.Strings; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import javax.annotation.Nonnull; +import org.sonar.api.rule.RuleKey; + +import static com.google.common.collect.FluentIterable.from; + +public class Tracker<RAW extends Trackable, BASE extends Trackable> { + + public Tracking<RAW, BASE> track(Input<RAW> rawInput, Input<BASE> baseInput) { + Tracking<RAW, BASE> tracking = new Tracking<>(rawInput, baseInput); + + // 1. match issues with same rule, same line and same line hash, but not necessarily with same message + match(tracking, LineAndLineHashKeyFactory.INSTANCE); + + // 2. detect code moves by comparing blocks of codes + detectCodeMoves(rawInput, baseInput, tracking); + + // 3. match issues with same rule, same message and same line hash + match(tracking, LineHashAndMessagekeyFactory.INSTANCE); + + // 4. match issues with same rule, same line and same message + match(tracking, LineAndMessageKeyFactory.INSTANCE); + + // 5. match issues with same rule and same line hash but different line and different message. + // See SONAR-2812 + match(tracking, LineHashKeyFactory.INSTANCE); + + // TODO what about issues on line 0 ? + relocateManualIssues(rawInput, tracking); + + return tracking; + } + + private void detectCodeMoves(Input<RAW> rawInput, Input<BASE> baseInput, Tracking<RAW, BASE> tracking) { + if (!tracking.isComplete()) { + new BlockRecognizer<RAW, BASE>().match(rawInput, baseInput, tracking); + } + } + + private void match(Tracking<RAW, BASE> tracking, SearchKeyFactory factory) { + if (tracking.isComplete()) { + return; + } + + Multimap<SearchKey, BASE> baseSearch = ArrayListMultimap.create(); + for (BASE base : tracking.getUnmatchedBases()) { + baseSearch.put(factory.create(base), base); + } + + Collection<RAW> trackedRaws = new ArrayList<>(); + for (RAW raw : tracking.getUnmatchedRaws()) { + SearchKey rawKey = factory.create(raw); + Collection<BASE> bases = baseSearch.get(rawKey); + if (!bases.isEmpty()) { + // TODO taking the first one. Could be improved if there are more than 2 issues on the same line. + // Message could be checked to take the best one. + BASE match = bases.iterator().next(); + tracking.associateRawToBase(raw, match); + baseSearch.remove(rawKey, match); + trackedRaws.add(raw); + } + } + tracking.markRawsAsAssociated(trackedRaws); + } + + private void relocateManualIssues(Input<RAW> rawInput, Tracking<RAW, BASE> tracking) { + Iterable<BASE> manualIssues = from(tracking.getUnmatchedBases()).filter(IsManual.INSTANCE); + for (BASE base : manualIssues) { + if (base.getLine() == null) { + // no need to relocate. Location is unchanged. + tracking.associateManualIssueToLine(base, 0); + } else { + String lineHash = base.getLineHash(); + if (!Strings.isNullOrEmpty(lineHash)) { + int[] rawLines = rawInput.getLineHashSequence().getLinesForHash(lineHash); + if (rawLines.length == 1) { + tracking.associateManualIssueToLine(base, rawLines[0]); + } else if (rawLines.length == 0 && base.getLine() <= rawInput.getLineHashSequence().length()) { + // still valid (???). We didn't manage to correctly detect code move, so the + // issue is kept at the same location, even if code changes + tracking.associateManualIssueToLine(base, base.getLine()); + } + } + } + } + } + + private enum IsManual implements Predicate<Trackable> { + INSTANCE; + @Override + public boolean apply(Trackable input) { + return input.getRuleKey().isManual(); + } + } + + private interface SearchKey { + } + + private interface SearchKeyFactory { + SearchKey create(Trackable trackable); + } + + private static class LineAndLineHashKey implements SearchKey { + private final RuleKey ruleKey; + private final String lineHash; + private final Integer line; + + LineAndLineHashKey(Trackable trackable) { + this.ruleKey = trackable.getRuleKey(); + this.line = trackable.getLine(); + this.lineHash = trackable.getLineHash(); + } + + @Override + public boolean equals(@Nonnull Object o) { + if (this == o) { + return true; + } + LineAndLineHashKey that = (LineAndLineHashKey) o; + // start with most discriminant field + if (!Objects.equals(line, that.line)) { + return false; + } + if (!lineHash.equals(that.lineHash)) { + return false; + } + return ruleKey.equals(that.ruleKey); + } + + @Override + public int hashCode() { + int result = ruleKey.hashCode(); + result = 31 * result + lineHash.hashCode(); + result = 31 * result + (line != null ? line.hashCode() : 0); + return result; + } + } + + private enum LineAndLineHashKeyFactory implements SearchKeyFactory { + INSTANCE; + @Override + public SearchKey create(Trackable t) { + return new LineAndLineHashKey(t); + } + } + + private static class LineHashAndMessageKey implements SearchKey { + private final RuleKey ruleKey; + private final String message, lineHash; + + LineHashAndMessageKey(Trackable trackable) { + this.ruleKey = trackable.getRuleKey(); + this.message = trackable.getMessage(); + this.lineHash = trackable.getLineHash(); + } + + @Override + public boolean equals(@Nonnull Object o) { + if (this == o) { + return true; + } + LineHashAndMessageKey that = (LineHashAndMessageKey) o; + // start with most discriminant field + if (!lineHash.equals(that.lineHash)) { + return false; + } + if (!message.equals(that.message)) { + return false; + } + return ruleKey.equals(that.ruleKey); + } + + @Override + public int hashCode() { + int result = ruleKey.hashCode(); + result = 31 * result + message.hashCode(); + result = 31 * result + lineHash.hashCode(); + return result; + } + } + + private enum LineHashAndMessagekeyFactory implements SearchKeyFactory { + INSTANCE; + @Override + public SearchKey create(Trackable t) { + return new LineHashAndMessageKey(t); + } + } + + private static class LineAndMessageKey implements SearchKey { + private final RuleKey ruleKey; + private final String message; + private final Integer line; + + LineAndMessageKey(Trackable trackable) { + this.ruleKey = trackable.getRuleKey(); + this.message = trackable.getMessage(); + this.line = trackable.getLine(); + } + + @Override + public boolean equals(@Nonnull Object o) { + if (this == o) { + return true; + } + LineAndMessageKey that = (LineAndMessageKey) o; + // start with most discriminant field + if (!Objects.equals(line, that.line)) { + return false; + } + if (!message.equals(that.message)) { + return false; + } + return ruleKey.equals(that.ruleKey); + } + + @Override + public int hashCode() { + int result = ruleKey.hashCode(); + result = 31 * result + message.hashCode(); + result = 31 * result + (line != null ? line.hashCode() : 0); + return result; + } + } + + private enum LineAndMessageKeyFactory implements SearchKeyFactory { + INSTANCE; + @Override + public SearchKey create(Trackable t) { + return new LineAndMessageKey(t); + } + } + + private static class LineHashKey implements SearchKey { + private final RuleKey ruleKey; + private final String lineHash; + + LineHashKey(Trackable trackable) { + this.ruleKey = trackable.getRuleKey(); + this.lineHash = trackable.getLineHash(); + } + + @Override + public boolean equals(@Nonnull Object o) { + if (this == o) { + return true; + } + LineAndLineHashKey that = (LineAndLineHashKey) o; + // start with most discriminant field + if (!lineHash.equals(that.lineHash)) { + return false; + } + return ruleKey.equals(that.ruleKey); + } + + @Override + public int hashCode() { + int result = ruleKey.hashCode(); + result = 31 * result + lineHash.hashCode(); + return result; + } + } + + private enum LineHashKeyFactory implements SearchKeyFactory { + INSTANCE; + @Override + public SearchKey create(Trackable t) { + return new LineHashKey(t); + } + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java new file mode 100644 index 00000000000..27f34ac3887 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/tracking/Tracking.java @@ -0,0 +1,116 @@ +/* + * 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.core.issue.tracking; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import javax.annotation.CheckForNull; + +public class Tracking<RAW extends Trackable, BASE extends Trackable> { + + /** + * Tracked issues -> a raw issue is associated to a base issue + */ + private final IdentityHashMap<RAW, BASE> rawToBase = new IdentityHashMap<>(); + + /** + * The raw issues that are not associated to a base issue. + */ + private final Set<RAW> unmatchedRaws = Collections.newSetFromMap(new IdentityHashMap<RAW, Boolean>()); + + /** + * IdentityHashSet of the base issues that are not associated to a raw issue. + */ + private final Set<BASE> unmatchedBases = Collections.newSetFromMap(new IdentityHashMap<BASE, Boolean>()); + + /** + * The manual issues that are still valid (related source code still exists). They + * are grouped by line. Lines start with 1. The key 0 references the manual + * issues that do not relate to a line. + */ + private final Multimap<Integer, BASE> openManualIssues = ArrayListMultimap.create(); + + public Tracking(Input<RAW> rawInput, Input<BASE> baseInput) { + for (RAW raw : rawInput.getIssues()) { + // Extra verification if some plugins create issues on wrong lines + Integer rawLine = raw.getLine(); + if (rawLine != null && !rawInput.getLineHashSequence().hasLine(rawLine)) { + throw new IllegalArgumentException("Issue line is not valid: " + raw); + } + this.unmatchedRaws.add(raw); + } + this.unmatchedBases.addAll(baseInput.getIssues()); + } + + public Set<RAW> getUnmatchedRaws() { + return unmatchedRaws; + } + + public Map<RAW, BASE> getMatchedRaws() { + return rawToBase; + } + + @CheckForNull + public BASE baseFor(RAW raw) { + return rawToBase.get(raw); + } + + /** + * The base issues that are not matched by a raw issue and that need to be closed. Manual + */ + public Set<BASE> getUnmatchedBases() { + return unmatchedBases; + } + + boolean containsUnmatchedBase(BASE base) { + return unmatchedBases.contains(base); + } + + void associateRawToBase(RAW raw, BASE base) { + rawToBase.put(raw, base); + unmatchedBases.remove(base); + } + + void markRawAsAssociated(RAW raw) { + unmatchedRaws.remove(raw); + } + + void markRawsAsAssociated(Collection<RAW> c) { + unmatchedRaws.removeAll(c); + } + + boolean isComplete() { + return unmatchedRaws.isEmpty() || unmatchedBases.isEmpty(); + } + + public Multimap<Integer, BASE> getOpenManualIssuesByLine() { + return openManualIssues; + } + + void associateManualIssueToLine(BASE manualIssue, int line) { + openManualIssues.put(line, manualIssue); + unmatchedBases.remove(manualIssue); + } +} diff --git a/sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java b/sonar-core/src/main/java/org/sonar/core/issue/tracking/package-info.java new file mode 100644 index 00000000000..1438e8aee5a --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/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.core.issue.tracking; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java index 51d12b7e36d..dfeac403458 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/FunctionExecutor.java @@ -20,10 +20,10 @@ package org.sonar.core.issue.workflow; import org.sonar.api.batch.BatchSide; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.api.server.ServerSide; import org.sonar.api.issue.Issue; -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; diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsEndOfLife.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsBeingClosed.java index e8d877dbd13..a9f4f210de0 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsEndOfLife.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsBeingClosed.java @@ -21,18 +21,13 @@ package org.sonar.core.issue.workflow; import org.sonar.api.issue.Issue; import org.sonar.api.issue.condition.Condition; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; -class IsEndOfLife implements Condition { - - private final boolean endOfLife; - - IsEndOfLife(boolean endOfLife) { - this.endOfLife = endOfLife; - } +enum IsBeingClosed implements Condition { + INSTANCE; @Override public boolean matches(Issue issue) { - return ((DefaultIssue) issue).isEndOfLife() == endOfLife; + return ((DefaultIssue) issue).isBeingClosed(); } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java index cd448ee8ce6..6bdda1fc117 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IsManual.java @@ -22,16 +22,11 @@ package org.sonar.core.issue.workflow; import org.sonar.api.issue.Issue; import org.sonar.api.issue.condition.Condition; -class IsManual implements Condition { - - private final boolean manual; - - IsManual(boolean manual) { - this.manual = manual; - } +enum IsManual implements Condition { + INSTANCE; @Override public boolean matches(Issue issue) { - return manual==(issue.reporter()!=null); + return issue.ruleKey().isManual(); } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java index 5131a7def4d..5b54986c84f 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/IssueWorkflow.java @@ -19,19 +19,19 @@ */ package org.sonar.core.issue.workflow; +import java.util.List; import org.picocontainer.Startable; import org.sonar.api.batch.BatchSide; -import org.sonar.api.server.ServerSide; import org.sonar.api.issue.DefaultTransitions; import org.sonar.api.issue.Issue; import org.sonar.api.issue.condition.HasResolution; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.api.issue.condition.NotCondition; +import org.sonar.api.server.ServerSide; import org.sonar.api.web.UserRole; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.core.issue.IssueUpdater; -import java.util.List; - @BatchSide @ServerSide public class IssueWorkflow implements Startable { @@ -86,7 +86,7 @@ public class IssueWorkflow implements Startable { .functions(new SetResolution(null)) .build()) .transition(Transition.builder(DefaultTransitions.REOPEN) - .conditions(new IsManual(true)) + .conditions(IsManual.INSTANCE) .from(Issue.STATUS_CLOSED).to(Issue.STATUS_REOPENED) .functions(new SetResolution(null), new SetCloseDate(false)) .build()) @@ -94,34 +94,34 @@ public class IssueWorkflow implements Startable { // resolve as false-positive .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build()) .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build()) .transition(Transition.builder(DefaultTransitions.FALSE_POSITIVE) .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_FALSE_POSITIVE), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build()) // resolve as won't fix .transition(Transition.builder(DefaultTransitions.WONT_FIX) .from(Issue.STATUS_OPEN).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build()) .transition(Transition.builder(DefaultTransitions.WONT_FIX) .from(Issue.STATUS_REOPENED).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build()) .transition(Transition.builder(DefaultTransitions.WONT_FIX) .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_RESOLVED) - .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), SetAssignee.UNASSIGN) + .functions(new SetResolution(Issue.RESOLUTION_WONT_FIX), UnsetAssignee.INSTANCE) .requiredProjectPermission(UserRole.ISSUE_ADMIN) .build() ); @@ -130,33 +130,34 @@ public class IssueWorkflow implements Startable { private void buildAutomaticTransitions(StateMachine.Builder builder) { // Close the "end of life" issues (disabled/deleted rule, deleted component) - builder.transition(Transition.builder("automaticclose") - .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) - .conditions(new IsEndOfLife(true)) - .functions(new SetEndOfLife(), new SetCloseDate(true)) - .automatic() - .build()) + builder + .transition(Transition.builder("automaticclose") + .from(Issue.STATUS_OPEN).to(Issue.STATUS_CLOSED) + .conditions(IsBeingClosed.INSTANCE) + .functions(SetClosed.INSTANCE, new SetCloseDate(true)) + .automatic() + .build()) .transition(Transition.builder("automaticclose") .from(Issue.STATUS_REOPENED).to(Issue.STATUS_CLOSED) - .conditions(new IsEndOfLife(true)) - .functions(new SetEndOfLife(), new SetCloseDate(true)) + .conditions(IsBeingClosed.INSTANCE) + .functions(SetClosed.INSTANCE, new SetCloseDate(true)) .automatic() .build()) .transition(Transition.builder("automaticclose") .from(Issue.STATUS_CONFIRMED).to(Issue.STATUS_CLOSED) - .conditions(new IsEndOfLife(true)) - .functions(new SetEndOfLife(), new SetCloseDate(true)) + .conditions(IsBeingClosed.INSTANCE) + .functions(SetClosed.INSTANCE, new SetCloseDate(true)) .automatic() .build()) .transition(Transition.builder("automaticclose") .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) - .conditions(new IsEndOfLife(true)) - .functions(new SetEndOfLife(), new SetCloseDate(true)) + .conditions(IsBeingClosed.INSTANCE) + .functions(SetClosed.INSTANCE, new SetCloseDate(true)) .automatic() .build()) .transition(Transition.builder("automaticclosemanual") .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_CLOSED) - .conditions(new IsEndOfLife(false), new IsManual(true)) + .conditions(new NotCondition(IsBeingClosed.INSTANCE), IsManual.INSTANCE) .functions(new SetCloseDate(true)) .automatic() .build()) @@ -165,7 +166,7 @@ public class IssueWorkflow implements Startable { // Manual issues are kept resolved. .transition(Transition.builder("automaticreopen") .from(Issue.STATUS_RESOLVED).to(Issue.STATUS_REOPENED) - .conditions(new IsEndOfLife(false), new HasResolution(Issue.RESOLUTION_FIXED), new IsManual(false)) + .conditions(new NotCondition(IsBeingClosed.INSTANCE), new HasResolution(Issue.RESOLUTION_FIXED), new NotCondition(IsManual.INSTANCE)) .functions(new SetResolution(null), new SetCloseDate(false)) .automatic() .build() diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java index 3d83ac27c36..36da29e0c68 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetCloseDate.java @@ -19,7 +19,7 @@ */ package org.sonar.core.issue.workflow; -public class SetCloseDate implements Function { +class SetCloseDate implements Function { private final boolean set; public SetCloseDate(boolean set) { diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetEndOfLife.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosed.java index ae45caa9390..cee06ac4181 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetEndOfLife.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetClosed.java @@ -20,13 +20,15 @@ package org.sonar.core.issue.workflow; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; + +public enum SetClosed implements Function { + INSTANCE; -public class SetEndOfLife implements Function { @Override public void execute(Context context) { DefaultIssue issue = (DefaultIssue) context.issue(); - if (!issue.isEndOfLife()) { + if (!issue.isBeingClosed()) { throw new IllegalStateException("Issue is still alive: " + issue); } if (issue.isOnDisabledRule()) { diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetAssignee.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetAssignee.java index 7a5c778ce61..b7f9da103d8 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/workflow/SetAssignee.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetAssignee.java @@ -19,21 +19,11 @@ */ package org.sonar.core.issue.workflow; -import org.sonar.api.user.User; - -import javax.annotation.Nullable; - -public class SetAssignee implements Function { - public static final SetAssignee UNASSIGN = new SetAssignee(null); - - private final User assignee; - - public SetAssignee(@Nullable User assignee) { - this.assignee = assignee; - } +enum UnsetAssignee implements Function { + INSTANCE; @Override public void execute(Context context) { - context.setAssignee(assignee); + context.setAssignee(null); } } diff --git a/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java new file mode 100644 index 00000000000..8073e179280 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/issue/workflow/UnsetLine.java @@ -0,0 +1,29 @@ +/* + * 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.core.issue.workflow; + +enum UnsetLine implements Function { + INSTANCE; + @Override + public void execute(Context context) { + context.setLine(null); + } + +} diff --git a/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml b/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml index c1bfead8323..50eb94db4d3 100644 --- a/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml +++ b/sonar-core/src/main/resources/org/sonar/core/issue/db/IssueMapper.xml @@ -148,7 +148,42 @@ where i.kee=#{kee} </select> - <select id="selectNonClosedIssuesByModule" parameterType="Long" resultType="Issue"> + <select id="selectOpenByComponentUuid" parameterType="String" resultType="Issue"> + select + i.id, + i.kee as kee, + i.rule_id as ruleId, + i.action_plan_key as actionPlanKey, + i.severity as severity, + i.manual_severity as manualSeverity, + i.message as message, + i.line as line, + i.effort_to_fix as effortToFix, + i.technical_debt as debt, + i.status as status, + i.resolution as resolution, + i.checksum as checksum, + i.reporter as reporter, + i.assignee as assignee, + i.author_login as authorLogin, + i.tags as tagsString, + i.issue_attributes as issueAttributes, + i.issue_creation_date as issueCreationTime, + i.issue_update_date as issueUpdateTime, + i.issue_close_date as issueCloseTime, + i.created_at as createdAt, + i.updated_at as updatedAt, + r.plugin_rule_key as ruleKey, + i.component_uuid as componentUuid, + i.project_uuid as projectUuid + from issues i + inner join rules r on r.id=i.rule_id + where + i.component_uuid=#{componentUuid} and + i.status <> 'CLOSED' + </select> + + <select id="selectNonClosedIssuesByModule" parameterType="long" resultType="Issue"> select i.id, i.kee as kee, @@ -186,6 +221,12 @@ where i.status <> 'CLOSED' </select> + <select id="selectComponentUuidsOfOpenIssuesForProjectUuid" parameterType="string" resultType="string"> + select distinct(i.component_uuid) + from issues i + where i.project_uuid=#{projectUuid} and i.status <> 'CLOSED' + </select> + <select id="selectByKeys" parameterType="map" resultType="Issue"> select <include refid="issueColumns"/> diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java index a84e1111836..136c5a262b5 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueBuilderTest.java @@ -21,7 +21,6 @@ package org.sonar.core.issue; import org.junit.Test; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java index d6e304a309d..ed424565037 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueChangeContextTest.java @@ -20,7 +20,6 @@ package org.sonar.core.issue; import org.junit.Test; -import org.sonar.api.issue.internal.IssueChangeContext; import java.util.Date; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java b/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java index 8a4181f47b5..c3d927cbe3c 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/IssueUpdaterTest.java @@ -22,9 +22,6 @@ package org.sonar.core.issue; import org.junit.Before; import org.junit.Test; import org.sonar.api.issue.ActionPlan; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.FieldDiffs; -import org.sonar.api.issue.internal.IssueChangeContext; import org.sonar.api.user.User; import org.sonar.api.utils.Duration; import org.sonar.core.user.DefaultUser; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java index d9de4aa528f..411048b421e 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDaoTest.java @@ -23,8 +23,8 @@ import org.apache.ibatis.executor.result.DefaultResultHandler; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.FieldDiffs; import org.sonar.api.utils.DateUtils; import org.sonar.core.persistence.AbstractDaoTestCase; import org.sonar.core.persistence.DbSession; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java index 3b383c1c974..2d3ece83f18 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueChangeDtoTest.java @@ -20,8 +20,8 @@ package org.sonar.core.issue.db; import org.junit.Test; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.FieldDiffs; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.FieldDiffs; import org.sonar.api.utils.System2; import static org.assertj.core.api.Assertions.assertThat; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java index 7958aa26674..7738ca20486 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDaoTest.java @@ -47,41 +47,6 @@ public class IssueDaoTest extends AbstractDaoTestCase { } @Test - public void should_select_by_key() { - setupData("shared", "should_select_by_key"); - - IssueDto issue = dao.selectByKey("ABCDE"); - assertThat(issue.getKee()).isEqualTo("ABCDE"); - assertThat(issue.getId()).isEqualTo(100L); - assertThat(issue.getRuleId()).isEqualTo(500); - assertThat(issue.getSeverity()).isEqualTo("BLOCKER"); - assertThat(issue.isManualSeverity()).isFalse(); - assertThat(issue.getMessage()).isNull(); - assertThat(issue.getLine()).isEqualTo(200); - assertThat(issue.getEffortToFix()).isEqualTo(4.2); - assertThat(issue.getStatus()).isEqualTo("OPEN"); - assertThat(issue.getResolution()).isEqualTo("FIXED"); - assertThat(issue.getChecksum()).isEqualTo("XXX"); - assertThat(issue.getAuthorLogin()).isEqualTo("karadoc"); - assertThat(issue.getReporter()).isEqualTo("arthur"); - assertThat(issue.getAssignee()).isEqualTo("perceval"); - assertThat(issue.getIssueAttributes()).isEqualTo("JIRA=FOO-1234"); - assertThat(issue.getIssueCreationDate()).isNotNull(); - assertThat(issue.getIssueUpdateDate()).isNotNull(); - assertThat(issue.getIssueCloseDate()).isNotNull(); - assertThat(issue.getCreatedAt()).isNotNull(); - assertThat(issue.getUpdatedAt()).isNotNull(); - assertThat(issue.getRuleRepo()).isEqualTo("squid"); - assertThat(issue.getRule()).isEqualTo("AvoidCycle"); - assertThat(issue.getComponentUuid()).isEqualTo("CDEF"); - assertThat(issue.getComponentKey()).isEqualTo("Action.java"); - assertThat(issue.getModuleUuid()).isEqualTo("BCDE"); - assertThat(issue.getModuleUuidPath()).isEqualTo(".ABCD.BCDE."); - assertThat(issue.getProjectKey()).isEqualTo("struts"); // ABCD - assertThat(issue.getProjectUuid()).isEqualTo("ABCD"); // null - } - - @Test public void select_non_closed_issues_by_module() { setupData("shared", "should_select_non_closed_issues_by_module"); diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java index edac8a2dd84..0ee3ae23416 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueDtoTest.java @@ -24,7 +24,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import org.sonar.api.utils.Duration; import org.sonar.core.rule.RuleDto; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java index 62e10059629..259f751c12e 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/IssueStorageTest.java @@ -22,9 +22,9 @@ package org.sonar.core.issue.db; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.DefaultIssueComment; -import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.DefaultIssueComment; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.Rule; import org.sonar.api.rules.RuleFinder; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java index 2dc79d30b8d..9a4d5037901 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/db/UpdateConflictResolverTest.java @@ -22,7 +22,7 @@ package org.sonar.core.issue.db; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.utils.DateUtils; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java new file mode 100644 index 00000000000..f3646150846 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockHashSequenceTest.java @@ -0,0 +1,42 @@ +/* + * 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.core.issue.tracking; + +import org.junit.Test; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class BlockHashSequenceTest { + + @Test + public void test() { + BlockHashSequence a = new BlockHashSequence(LineHashSequence.createForLines(asList("line0", "line1", "line2")), 1); + BlockHashSequence b = new BlockHashSequence(LineHashSequence.createForLines(asList("line0", "line1", "line2", "line3")), 1); + + assertThat(a.getBlockHashForLine(1)).isEqualTo(b.getBlockHashForLine(1)); + assertThat(a.getBlockHashForLine(2)).isEqualTo(b.getBlockHashForLine(2)); + assertThat(a.getBlockHashForLine(3)).isNotEqualTo(b.getBlockHashForLine(3)); + + BlockHashSequence c = new BlockHashSequence(LineHashSequence.createForLines(asList("line-1", "line0", "line1", "line2", "line3")), 1); + assertThat(a.getBlockHashForLine(1)).isNotEqualTo(c.getBlockHashForLine(2)); + assertThat(a.getBlockHashForLine(2)).isEqualTo(c.getBlockHashForLine(3)); + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java new file mode 100644 index 00000000000..d265e70d687 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/BlockRecognizerTest.java @@ -0,0 +1,56 @@ +/* + * 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.core.issue.tracking; + +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BlockRecognizerTest { + + @Test + public void lengthOfMaximalBlock() { + /** + * - line 4 of first sequence is "d" + * - line 4 of second sequence is "d" + * - in each sequence, the 3 lines before and the line after are similar -> block size is 5 + */ + assertThat(compute(seq("abcde"), seq("abcde"), 4, 4)).isEqualTo(5); + + assertThat(compute(seq("abcde"), seq("abcd"), 4, 4)).isEqualTo(4); + assertThat(compute(seq("bcde"), seq("abcde"), 4, 4)).isEqualTo(0); + assertThat(compute(seq("bcde"), seq("abcde"), 3, 4)).isEqualTo(4); + } + + private int compute(LineHashSequence seqA, LineHashSequence seqB, int ai, int bi) { + return BlockRecognizer.lengthOfMaximalBlock(seqA, ai, seqB, bi); + } + + private static LineHashSequence seq(String text) { + List<String> hashes = new ArrayList<>(); + for (int i = 0; i < text.length(); i++) { + hashes.add("" + text.charAt(i)); + } + return new LineHashSequence(hashes); + } + +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java new file mode 100644 index 00000000000..37391225a84 --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/issue/tracking/TrackerTest.java @@ -0,0 +1,456 @@ +/* + * 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.core.issue.tracking; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nullable; +import org.apache.commons.codec.digest.DigestUtils; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.sonar.api.rule.RuleKey; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; + +public class TrackerTest { + + public static final RuleKey RULE_SYSTEM_PRINT = RuleKey.of("java", "SystemPrint"); + public static final RuleKey RULE_UNUSED_LOCAL_VARIABLE = RuleKey.of("java", "UnusedLocalVariable"); + public static final RuleKey RULE_UNUSED_PRIVATE_METHOD = RuleKey.of("java", "UnusedPrivateMethod"); + public static final RuleKey RULE_NOT_DESIGNED_FOR_EXTENSION = RuleKey.of("java", "NotDesignedForExtension"); + @Rule + public ExpectedException thrown = ExpectedException.none(); + + Tracker<Issue, Issue> tracker = new Tracker<>(); + + /** + * Of course rule must match + */ + @Test + public void similar_issues_except_rule_do_not_match() { + FakeInput baseInput = new FakeInput("H1"); + baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + + FakeInput rawInput = new FakeInput("H1"); + Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isNull(); + } + + @Test + @Ignore + public void different_issues_do_not_match() { + FakeInput baseInput = new FakeInput("H1"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1"); + + FakeInput rawInput = new FakeInput("H2", "H3", "H4", "H5", "H6"); + Issue raw = rawInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "msg2"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isNull(); + assertThat(tracking.getUnmatchedBases()).containsOnly(base); + } + + @Test + public void line_hash_has_greater_priority_than_line() { + FakeInput baseInput = new FakeInput("H1", "H2", "H3"); + Issue base1 = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + Issue base2 = baseInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "msg"); + + FakeInput rawInput = new FakeInput("a", "b", "H1", "H2", "H3"); + Issue raw1 = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "msg"); + Issue raw2 = rawInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "msg"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw1)).isSameAs(base1); + assertThat(tracking.baseFor(raw2)).isSameAs(base2); + } + + /** + * SONAR-2928 + */ + @Test + public void no_lines_and_different_messages_match() { + FakeInput baseInput = new FakeInput("H1", "H2", "H3"); + Issue base = baseInput.createIssue(RULE_SYSTEM_PRINT, "msg1"); + + FakeInput rawInput = new FakeInput("H10", "H11", "H12"); + Issue raw = rawInput.createIssue(RULE_SYSTEM_PRINT, "msg2"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + @Test + public void similar_issues_except_message_match() { + FakeInput baseInput = new FakeInput("H1"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1"); + + FakeInput rawInput = new FakeInput("H1"); + Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg2"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + @Test + public void similar_issues_if_trimmed_messages_match() { + FakeInput baseInput = new FakeInput("H1"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, " message "); + + FakeInput rawInput = new FakeInput("H2"); + Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "message"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + /** + * Source code of this line was changed, but line and message still match + */ + @Test + public void similar_issues_except_line_hash_match() { + FakeInput baseInput = new FakeInput("H1"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + + FakeInput rawInput = new FakeInput("H2"); + Issue raw = rawInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + @Test + public void similar_issues_except_line_match() { + FakeInput baseInput = new FakeInput("H1", "H2"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + + FakeInput rawInput = new FakeInput("H2", "H1"); + Issue raw = rawInput.createIssueOnLine(2, RULE_SYSTEM_PRINT, "msg"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + /** + * SONAR-2812 + */ + @Test + public void only_same_line_hash_match_match() { + FakeInput baseInput = new FakeInput("H1", "H2"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg"); + + FakeInput rawInput = new FakeInput("H3", "H4", "H1"); + Issue raw = rawInput.createIssueOnLine(3, RULE_SYSTEM_PRINT, "other message"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isSameAs(base); + } + + @Test + public void do_not_fail_if_base_issue_without_line() throws Exception { + FakeInput baseInput = new FakeInput("H1", "H2"); + Issue base = baseInput.createIssueOnLine(1, RULE_SYSTEM_PRINT, "msg1"); + + FakeInput rawInput = new FakeInput("H3", "H4", "H5"); + Issue raw = rawInput.createIssue(RULE_UNUSED_LOCAL_VARIABLE, "msg2"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isNull(); + assertThat(tracking.getUnmatchedBases()).containsOnly(base); + } + + @Test + public void do_not_fail_if_raw_issue_without_line() throws Exception { + FakeInput baseInput = new FakeInput("H1", "H2"); + Issue base = baseInput.createIssue(RULE_SYSTEM_PRINT, "msg1"); + + FakeInput rawInput = new FakeInput("H3", "H4", "H5"); + Issue raw = rawInput.createIssueOnLine(1, RULE_UNUSED_LOCAL_VARIABLE, "msg2"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw)).isNull(); + assertThat(tracking.getUnmatchedBases()).containsOnly(base); + } + + @Test + public void fail_if_raw_line_does_not_exist() throws Exception { + FakeInput baseInput = new FakeInput(); + FakeInput rawInput = new FakeInput("H1").addIssue(new Issue(200, "H200", RULE_SYSTEM_PRINT, "msg")); + + thrown.expect(IllegalArgumentException.class); + thrown.expectMessage("Issue line is not valid"); + tracker.track(rawInput, baseInput); + } + + /** + * SONAR-3072 + */ + @Test + public void recognize_blocks_1() throws Exception { + FakeInput baseInput = FakeInput.createForSourceLines( + "package example1;", + "", + "public class Toto {", + "", + " public void doSomething() {", + " // doSomething", + " }", + "", + " public void doSomethingElse() {", + " // doSomethingElse", + " }", + "}" + ); + Issue base1 = baseInput.createIssueOnLine(7, RULE_SYSTEM_PRINT, "Indentation"); + Issue base2 = baseInput.createIssueOnLine(11, RULE_SYSTEM_PRINT, "Indentation"); + + FakeInput rawInput = FakeInput.createForSourceLines( + "package example1;", + "", + "public class Toto {", + "", + " public Toto(){}", + "", + " public void doSomethingNew() {", + " // doSomethingNew", + " }", + "", + " public void doSomethingElseNew() {", + " // doSomethingElseNew", + " }", + "", + " public void doSomething() {", + " // doSomething", + " }", + "", + " public void doSomethingElse() {", + " // doSomethingElse", + " }", + "}" + ); + Issue raw1 = rawInput.createIssueOnLine(9, RULE_SYSTEM_PRINT, "Indentation"); + Issue raw2 = rawInput.createIssueOnLine(13, RULE_SYSTEM_PRINT, "Indentation"); + Issue raw3 = rawInput.createIssueOnLine(17, RULE_SYSTEM_PRINT, "Indentation"); + Issue raw4 = rawInput.createIssueOnLine(21, RULE_SYSTEM_PRINT, "Indentation"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw1)).isNull(); + assertThat(tracking.baseFor(raw2)).isNull(); + assertThat(tracking.baseFor(raw3)).isSameAs(base1); + assertThat(tracking.baseFor(raw4)).isSameAs(base2); + assertThat(tracking.getUnmatchedBases()).isEmpty(); + } + + /** + * SONAR-3072 + */ + @Test + public void recognize_blocks_2() throws Exception { + FakeInput baseInput = FakeInput.createForSourceLines( + "package example2;", + "", + "public class Toto {", + " void method1() {", + " System.out.println(\"toto\");", + " }", + "}" + ); + Issue base1 = baseInput.createIssueOnLine(5, RULE_SYSTEM_PRINT, "SystemPrintln"); + + FakeInput rawInput = FakeInput.createForSourceLines( + "package example2;", + "", + "public class Toto {", + "", + " void method2() {", + " System.out.println(\"toto\");", + " }", + "", + " void method1() {", + " System.out.println(\"toto\");", + " }", + "", + " void method3() {", + " System.out.println(\"toto\");", + " }", + "}" + ); + Issue raw1 = rawInput.createIssueOnLine(6, RULE_SYSTEM_PRINT, "SystemPrintln"); + Issue raw2 = rawInput.createIssueOnLine(10, RULE_SYSTEM_PRINT, "SystemPrintln"); + Issue raw3 = rawInput.createIssueOnLine(14, RULE_SYSTEM_PRINT, "SystemPrintln"); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + assertThat(tracking.baseFor(raw1)).isNull(); + assertThat(tracking.baseFor(raw2)).isSameAs(base1); + assertThat(tracking.baseFor(raw3)).isNull(); + } + + @Test + public void recognize_blocks_3() throws Exception { + FakeInput baseInput = FakeInput.createForSourceLines( + "package sample;", + "", + "public class Sample {", + "\t", + "\tpublic Sample(int i) {", + "\t\tint j = i+1;", // UnusedLocalVariable + "\t}", + "", + "\tpublic boolean avoidUtilityClass() {", // NotDesignedForExtension + "\t\treturn true;", + "\t}", + "", + "\tprivate String myMethod() {", // UnusedPrivateMethod + "\t\treturn \"hello\";", + "\t}", + "}" + ); + Issue base1 = baseInput.createIssueOnLine(6, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'j'."); + Issue base2 = baseInput.createIssueOnLine(13, RULE_UNUSED_PRIVATE_METHOD, "Avoid unused private methods such as 'myMethod()'."); + Issue base3 = baseInput.createIssueOnLine(9, RULE_NOT_DESIGNED_FOR_EXTENSION, + "Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty."); + + FakeInput rawInput = FakeInput.createForSourceLines( + "package sample;", + "", + "public class Sample {", + "", + "\tpublic Sample(int i) {", + "\t\tint j = i+1;", // UnusedLocalVariable is still there + "\t}", + "\t", + "\tpublic boolean avoidUtilityClass() {", // NotDesignedForExtension is still there + "\t\treturn true;", + "\t}", + "\t", + "\tprivate String myMethod() {", // issue UnusedPrivateMethod is fixed because it's called at line 18 + "\t\treturn \"hello\";", + "\t}", + "", + " public void newIssue() {", + " String msg = myMethod();", // new issue UnusedLocalVariable + " }", + "}" + ); + + Issue newRaw = rawInput.createIssueOnLine(18, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'msg'."); + Issue rawSameAsBase1 = rawInput.createIssueOnLine(6, RULE_UNUSED_LOCAL_VARIABLE, "Avoid unused local variables such as 'j'."); + Issue rawSameAsBase3 = rawInput.createIssueOnLine(9, RULE_NOT_DESIGNED_FOR_EXTENSION, + "Method 'avoidUtilityClass' is not designed for extension - needs to be abstract, final or empty."); + + Tracking<Issue, Issue> tracking = tracker.track(rawInput, baseInput); + + assertThat(tracking.baseFor(newRaw)).isNull(); + assertThat(tracking.baseFor(rawSameAsBase1)).isSameAs(base1); + assertThat(tracking.baseFor(rawSameAsBase3)).isSameAs(base3); + assertThat(tracking.getUnmatchedBases()).containsOnly(base2); + } + + private static class Issue implements Trackable { + private final RuleKey ruleKey; + private final Integer line; + private final String message, lineHash; + + Issue(@Nullable Integer line, String lineHash, RuleKey ruleKey, String message) { + this.line = line; + this.lineHash = lineHash; + this.ruleKey = ruleKey; + this.message = message; + } + + @Override + public Integer getLine() { + return line; + } + + @Override + public String getMessage() { + return message; + } + + @Override + public String getLineHash() { + return lineHash; + } + + @Override + public RuleKey getRuleKey() { + return ruleKey; + } + } + + private static class FakeInput implements Input<Issue> { + private final List<Issue> issues = new ArrayList<>(); + private final List<String> lineHashes; + + public FakeInput(String... lineHashes) { + this.lineHashes = asList(lineHashes); + } + + static FakeInput createForSourceLines(String... lines) { + String[] hashes = new String[lines.length]; + for (int i = 0; i < lines.length; i++) { + hashes[i] = DigestUtils.md5Hex(lines[i].replaceAll("[\t ]", "")); + } + return new FakeInput(hashes); + } + + public Issue createIssueOnLine(int line, RuleKey ruleKey, String message) { + Issue issue = new Issue(line, lineHashes.get(line - 1), ruleKey, message); + issues.add(issue); + return issue; + } + + /** + * No line (line 0) + */ + public Issue createIssue(RuleKey ruleKey, String message) { + Issue issue = new Issue(null, "", ruleKey, message); + issues.add(issue); + return issue; + } + + public FakeInput addIssue(Issue issue) { + this.issues.add(issue); + return this; + } + + @Override + public LineHashSequence getLineHashSequence() { + return new LineHashSequence(lineHashes); + } + + @Override + public BlockHashSequence getBlockHashSequence() { + return new BlockHashSequence(getLineHashSequence(), 2); + } + + @Override + public Collection<Issue> getIssues() { + return issues; + } + } +} diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsEndOfLifeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsBeingClosedTest.java index 47ca6b3d526..04e6c29304e 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsEndOfLifeTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsBeingClosedTest.java @@ -20,24 +20,19 @@ package org.sonar.core.issue.workflow; import org.junit.Test; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.issue.workflow.IsBeingClosed.INSTANCE; + +public class IsBeingClosedTest { -public class IsEndOfLifeTest { - DefaultIssue issue = new DefaultIssue(); @Test public void should_be_end_of_life() { - IsEndOfLife condition = new IsEndOfLife(true); - assertThat(condition.matches(issue.setEndOfLife(true))).isTrue(); - assertThat(condition.matches(issue.setEndOfLife(false))).isFalse(); + DefaultIssue issue = new DefaultIssue(); + assertThat(INSTANCE.matches(issue.setBeingClosed(true))).isTrue(); + assertThat(INSTANCE.matches(issue.setBeingClosed(false))).isFalse(); } - @Test - public void should_not_be_end_of_life() { - IsEndOfLife condition = new IsEndOfLife(false); - assertThat(condition.matches(issue.setEndOfLife(true))).isFalse(); - assertThat(condition.matches(issue.setEndOfLife(false))).isTrue(); - } } diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java index 16a82274f04..78a581387c7 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IsManualTest.java @@ -20,24 +20,19 @@ package org.sonar.core.issue.workflow; import org.junit.Test; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.api.rule.RuleKey; +import org.sonar.core.issue.DefaultIssue; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.core.issue.workflow.IsManual.INSTANCE; public class IsManualTest { - DefaultIssue issue = new DefaultIssue(); @Test public void should_match() { - IsManual condition = new IsManual(true); - assertThat(condition.matches(issue.setReporter("you"))).isTrue(); - assertThat(condition.matches(issue.setReporter(null))).isFalse(); + DefaultIssue issue = new DefaultIssue(); + assertThat(INSTANCE.matches(issue.setRuleKey(RuleKey.of(RuleKey.MANUAL_REPOSITORY_KEY, "R1")))).isTrue(); + assertThat(INSTANCE.matches(issue.setRuleKey(RuleKey.of("java", "R1")))).isFalse(); } - @Test - public void should_match_dead() { - IsManual condition = new IsManual(false); - assertThat(condition.matches(issue.setReporter("you"))).isFalse(); - assertThat(condition.matches(issue.setReporter(null))).isTrue(); - } } diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java index 8cc01a72189..ad5b9bef6e8 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/IssueWorkflowTest.java @@ -25,8 +25,8 @@ import org.apache.commons.lang.time.DateUtils; import org.junit.Test; import org.sonar.api.issue.DefaultTransitions; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; -import org.sonar.api.issue.internal.IssueChangeContext; +import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.issue.IssueChangeContext; import org.sonar.api.rule.RuleKey; import org.sonar.core.issue.IssueUpdater; @@ -106,7 +106,7 @@ public class IssueWorkflowTest { public void list_no_out_transition_from_status_closed() { workflow.start(); - DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_CLOSED); + DefaultIssue issue = new DefaultIssue().setStatus(Issue.STATUS_CLOSED).setRuleKey(RuleKey.of("java", "R1 ")); List<Transition> transitions = workflow.outTransitions(issue); assertThat(transitions).isEmpty(); } @@ -149,7 +149,7 @@ public class IssueWorkflowTest { .setResolution(Issue.RESOLUTION_FIXED) .setStatus(Issue.STATUS_RESOLVED) .setNew(false) - .setEndOfLife(true); + .setBeingClosed(true); Date now = new Date(); workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); @@ -167,7 +167,7 @@ public class IssueWorkflowTest { .setResolution(null) .setStatus(Issue.STATUS_OPEN) .setNew(false) - .setEndOfLife(true); + .setBeingClosed(true); Date now = new Date(); workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); @@ -185,7 +185,7 @@ public class IssueWorkflowTest { .setResolution(null) .setStatus(Issue.STATUS_REOPENED) .setNew(false) - .setEndOfLife(true); + .setBeingClosed(true); Date now = new Date(); workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); @@ -203,7 +203,7 @@ public class IssueWorkflowTest { .setResolution(null) .setStatus(Issue.STATUS_CONFIRMED) .setNew(false) - .setEndOfLife(true); + .setBeingClosed(true); Date now = new Date(); workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(now)); assertThat(issue.resolution()).isEqualTo(Issue.RESOLUTION_FIXED); @@ -222,7 +222,7 @@ public class IssueWorkflowTest { .setResolution(Issue.RESOLUTION_FIXED) .setStatus("xxx") .setNew(false) - .setEndOfLife(true); + .setBeingClosed(true); try { workflow.doAutomaticTransition(issue, IssueChangeContext.createScan(new Date())); fail(); @@ -346,7 +346,7 @@ public class IssueWorkflowTest { .setStatus(Issue.STATUS_OPEN) .setRuleKey(RuleKey.of("manual", "Performance")) .setReporter("simon") - .setEndOfLife(true) + .setBeingClosed(true) .setOnDisabledRule(true); workflow.start(); @@ -364,7 +364,7 @@ public class IssueWorkflowTest { .setStatus(Issue.STATUS_OPEN) .setRuleKey(RuleKey.of("manual", "Performance")) .setReporter("simon") - .setEndOfLife(true) + .setBeingClosed(true) .setOnDisabledRule(false); workflow.start(); diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetEndOfLifeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetClosedTest.java index 014a8c2d087..84e46786292 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetEndOfLifeTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetClosedTest.java @@ -21,39 +21,39 @@ package org.sonar.core.issue.workflow; import org.junit.Test; import org.sonar.api.issue.Issue; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.*; +import static org.sonar.core.issue.workflow.SetClosed.INSTANCE; -public class SetEndOfLifeTest { +public class SetClosedTest { Function.Context context = mock(Function.Context.class); - SetEndOfLife function = new SetEndOfLife(); @Test public void should_resolve_as_fixed() { - Issue issue = new DefaultIssue().setEndOfLife(true).setOnDisabledRule(false); + Issue issue = new DefaultIssue().setBeingClosed(true).setOnDisabledRule(false); when(context.issue()).thenReturn(issue); - function.execute(context); + INSTANCE.execute(context); verify(context, times(1)).setResolution(Issue.RESOLUTION_FIXED); } @Test public void should_resolve_as_removed_when_rule_is_disabled() { - Issue issue = new DefaultIssue().setEndOfLife(true).setOnDisabledRule(true); + Issue issue = new DefaultIssue().setBeingClosed(true).setOnDisabledRule(true); when(context.issue()).thenReturn(issue); - function.execute(context); + INSTANCE.execute(context); verify(context, times(1)).setResolution(Issue.RESOLUTION_REMOVED); } @Test public void should_fail_if_issue_is_not_resolved() { - Issue issue = new DefaultIssue().setEndOfLife(false); + Issue issue = new DefaultIssue().setBeingClosed(false); when(context.issue()).thenReturn(issue); try { - function.execute(context); + INSTANCE.execute(context); fail(); } catch (IllegalStateException e) { assertThat(e.getMessage()).contains("Issue is still alive"); @@ -63,9 +63,9 @@ public class SetEndOfLifeTest { @Test public void line_number_must_be_unset() { - Issue issue = new DefaultIssue().setEndOfLife(true).setLine(10); + Issue issue = new DefaultIssue().setBeingClosed(true).setLine(10); when(context.issue()).thenReturn(issue); - function.execute(context); + INSTANCE.execute(context); verify(context).setLine(null); } } diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java index 4c680c54ec1..eab956c6858 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/TransitionTest.java @@ -21,7 +21,7 @@ package org.sonar.core.issue.workflow; import org.junit.Test; import org.sonar.api.issue.condition.Condition; -import org.sonar.api.issue.internal.DefaultIssue; +import org.sonar.core.issue.DefaultIssue; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; diff --git a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetAssigneeTest.java b/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetAssigneeTest.java index ebadb4bccd9..3458baa77dd 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/workflow/SetAssigneeTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/workflow/UnsetAssigneeTest.java @@ -21,25 +21,18 @@ package org.sonar.core.issue.workflow; import org.junit.Test; -import org.sonar.api.user.User; -import org.sonar.core.user.DefaultUser; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.sonar.core.issue.workflow.UnsetAssignee.INSTANCE; -public class SetAssigneeTest { - @Test - public void assign() { - User user = new DefaultUser().setLogin("eric").setName("eric"); - SetAssignee function = new SetAssignee(user); - Function.Context context = mock(Function.Context.class); - function.execute(context); - verify(context, times(1)).setAssignee(user); - } +public class UnsetAssigneeTest { @Test public void unassign() { Function.Context context = mock(Function.Context.class); - SetAssignee.UNASSIGN.execute(context); + INSTANCE.execute(context); verify(context, times(1)).setAssignee(null); } } diff --git a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml b/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml deleted file mode 100644 index 080269cef46..00000000000 --- a/sonar-core/src/test/resources/org/sonar/core/issue/db/IssueDaoTest/should_select_by_key.xml +++ /dev/null @@ -1,28 +0,0 @@ -<dataset> - - <issues - id="100" - kee="ABCDE" - component_uuid="CDEF" - project_uuid="ABCD" - rule_id="500" - severity="BLOCKER" - manual_severity="[false]" - message="[null]" - line="200" - effort_to_fix="4.2" - status="OPEN" - resolution="FIXED" - checksum="XXX" - reporter="arthur" - assignee="perceval" - author_login="karadoc" - issue_attributes="JIRA=FOO-1234" - issue_creation_date="1366063200000" - issue_update_date="1366063200000" - issue_close_date="1366063200000" - created_at="1400000000000" - updated_at="1400000000000" - /> - -</dataset> |