*/
package org.sonar.ce.task.projectanalysis.issue;
+import java.time.Instant;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.apache.logging.log4j.util.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.ce.task.log.CeTaskMessages;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.core.issue.AnticipatedTransition;
import org.sonar.core.issue.DefaultIssue;
import org.sonar.core.issue.tracking.AnticipatedTransitionTracker;
import org.sonar.core.issue.tracking.Tracking;
+import org.sonar.db.dismissmessage.MessageType;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.FILE;
/**
*/
public class TransitionIssuesToAnticipatedStatesVisitor extends IssueVisitor {
+ private static final Logger LOGGER = LoggerFactory.getLogger(TransitionIssuesToAnticipatedStatesVisitor.class);
+ public static final String TRANSITION_ERROR_TEMPLATE = "Cannot resolve issue at line {} of {} due to: {}";
+
private Collection<AnticipatedTransition> anticipatedTransitions;
private final AnticipatedTransitionTracker<DefaultIssue, AnticipatedTransition> tracker = new AnticipatedTransitionTracker<>();
private final IssueLifecycle issueLifecycle;
+ private final CeTaskMessages ceTaskMessages;
+
private final AnticipatedTransitionRepository anticipatedTransitionRepository;
- public TransitionIssuesToAnticipatedStatesVisitor(AnticipatedTransitionRepository anticipatedTransitionRepository, IssueLifecycle issueLifecycle) {
+ public TransitionIssuesToAnticipatedStatesVisitor(AnticipatedTransitionRepository anticipatedTransitionRepository, IssueLifecycle issueLifecycle, CeTaskMessages ceTaskMessages) {
this.anticipatedTransitionRepository = anticipatedTransitionRepository;
this.issueLifecycle = issueLifecycle;
+ this.ceTaskMessages = ceTaskMessages;
}
@Override
@Override
public void onIssue(Component component, DefaultIssue issue) {
- if (issue.isNew()) {
+ if (isEligibleForAnticipatedTransitions(issue)) {
Tracking<DefaultIssue, AnticipatedTransition> tracking = tracker.track(List.of(issue), anticipatedTransitions);
Map<DefaultIssue, AnticipatedTransition> matchedRaws = tracking.getMatchedRaws();
if (matchedRaws.containsKey(issue)) {
}
}
+ private static boolean isEligibleForAnticipatedTransitions(DefaultIssue issue) {
+ return issue.isNew() && STATUS_OPEN.equals(issue.getStatus()) && null == issue.resolution();
+ }
+
private void performAnticipatedTransition(DefaultIssue issue, AnticipatedTransition anticipatedTransition) {
- issue.setBeingClosed(true);
- issue.setAnticipatedTransitionUuid(anticipatedTransition.getUuid());
- issueLifecycle.doManualTransition(issue, anticipatedTransition.getTransition(), anticipatedTransition.getUserUuid());
- String transitionComment = anticipatedTransition.getComment();
- String comment = Strings.isNotBlank(transitionComment) ? transitionComment : "Automatically transitioned from SonarLint";
- issueLifecycle.addComment(issue, comment, anticipatedTransition.getUserUuid());
+ try {
+ issueLifecycle.doManualTransition(issue, anticipatedTransition.getTransition(), anticipatedTransition.getUserUuid());
+ String transitionComment = anticipatedTransition.getComment();
+ String comment = Strings.isNotBlank(transitionComment) ? transitionComment : "Automatically transitioned from SonarLint";
+ issueLifecycle.addComment(issue, comment, anticipatedTransition.getUserUuid());
+ issue.setBeingClosed(true);
+ issue.setAnticipatedTransitionUuid(anticipatedTransition.getUuid());
+ } catch (Exception e) {
+ LOGGER.warn(TRANSITION_ERROR_TEMPLATE, issue.getLine(), issue.componentKey(), e.getMessage());
+ ceTaskMessages.add(
+ new CeTaskMessages.Message(getMessage(issue, e),
+ Instant.now().toEpochMilli(),
+ MessageType.GENERIC));
+ }
+ }
+
+ private static String getMessage(DefaultIssue issue, Exception e) {
+ final int MAX_LENGTH = 50;
+ int componentKeyLength = issue.componentKey().length();
+ String componentKey = componentKeyLength > MAX_LENGTH ? ("..." + issue.componentKey().substring(componentKeyLength - MAX_LENGTH, componentKeyLength)) : issue.componentKey();
+ return String.format(TRANSITION_ERROR_TEMPLATE.replace("{}", "%s"), issue.getLine(), componentKey, e.getMessage());
}
}
import java.util.Collections;
import java.util.stream.Collectors;
import java.util.stream.Stream;
+import org.junit.Rule;
import org.junit.Test;
+import org.slf4j.event.Level;
import org.sonar.api.rule.RuleKey;
+import org.sonar.api.testfixtures.log.LogTester;
+import org.sonar.ce.task.log.CeTaskMessages;
import org.sonar.ce.task.projectanalysis.component.Component;
import org.sonar.ce.task.projectanalysis.component.ComponentImpl;
import org.sonar.ce.task.projectanalysis.component.ProjectAttributes;
import org.sonar.core.issue.DefaultIssue;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
+import static org.sonar.api.issue.Issue.STATUS_OPEN;
+import static org.sonar.api.issue.Issue.STATUS_RESOLVED;
import static org.sonar.ce.task.projectanalysis.component.Component.Type.PROJECT;
public class TransitionIssuesToAnticipatedStatesVisitorTest {
-
+ @Rule
+ public LogTester logTester = new LogTester();
private final IssueLifecycle issueLifecycle = mock(IssueLifecycle.class);
private final AnticipatedTransitionRepository anticipatedTransitionRepository = mock(AnticipatedTransitionRepository.class);
- private final TransitionIssuesToAnticipatedStatesVisitor underTest = new TransitionIssuesToAnticipatedStatesVisitor(anticipatedTransitionRepository, issueLifecycle);
+ private final CeTaskMessages ceTaskMessages = mock(CeTaskMessages.class);
+
+ private final TransitionIssuesToAnticipatedStatesVisitor underTest = new TransitionIssuesToAnticipatedStatesVisitor(anticipatedTransitionRepository, issueLifecycle, ceTaskMessages);
@Test
public void givenMatchingAnticipatedTransitions_transitionsShouldBeAppliedToIssues() {
verify(issueLifecycle).addComment(issue, "doing the transition in an anticipated way", "admin");
}
+ @Test
+ public void givenMatchingAnticipatedTransitions_whenExceptionIsThrown_transitionsShouldNotBeAppliedAndWarningLogged() {
+ Component component = getComponent(Component.Type.FILE);
+ String exceptionMessage = "Cannot apply transition";
+
+ when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
+ doThrow(new IllegalStateException(exceptionMessage)).when(issueLifecycle).doManualTransition(any(), any(), any());
+ DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
+ issue.setComponentKey(component.getKey());
+
+ underTest.beforeComponent(component);
+ underTest.onIssue(component, issue);
+
+ assertThat(issue.isBeingClosed()).isFalse();
+ assertThat(issue.getAnticipatedTransitionUuid()).isEmpty();
+ verify(issueLifecycle).doManualTransition(issue, "wontfix", "admin");
+ verifyNoMoreInteractions(issueLifecycle);
+ assertThat(logTester.logs(Level.WARN))
+ .contains(String.format("Cannot resolve issue at line %s of %s due to: %s", issue.getLine(), issue.componentKey(), exceptionMessage));
+ verify(ceTaskMessages, times(1)).add(any());
+ }
+
+ @Test
+ public void givenMatchingAnticipatedTransitionsOnResolvedIssue_transitionsShouldNotBeAppliedToIssues() {
+ Component component = getComponent(Component.Type.FILE);
+ when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
+
+ DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
+ issue.setStatus(STATUS_RESOLVED);
+
+ underTest.beforeComponent(component);
+ underTest.onIssue(component, issue);
+
+ assertThat(issue.isBeingClosed()).isFalse();
+ assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
+ verifyNoInteractions(issueLifecycle);
+ }
+
+ @Test
+ public void givenMatchingAnticipatedTransitions_whenIssueIsNotNew_transitionsShouldNotBeAppliedToIssues() {
+ Component component = getComponent(Component.Type.FILE);
+ when(anticipatedTransitionRepository.getAnticipatedTransitionByComponent(component)).thenReturn(getAnticipatedTransitions("projectKey", "fileName"));
+
+ DefaultIssue issue = getDefaultIssue(1, "abcdefghi", "issue message");
+ issue.setNew(false);
+
+ underTest.beforeComponent(component);
+ underTest.onIssue(component, issue);
+
+ assertThat(issue.isBeingClosed()).isFalse();
+ assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
+ verifyNoInteractions(issueLifecycle);
+ }
+
@Test
public void givenNonMatchingAnticipatedTransitions_transitionsAreNotAppliedToIssues() {
Component component = getComponent(Component.Type.FILE);
private DefaultIssue getDefaultIssue(Integer line, String hash, String message) {
DefaultIssue defaultIssue = new DefaultIssue();
+ defaultIssue.setStatus(STATUS_OPEN);
+ defaultIssue.setResolution(null);
defaultIssue.setLine(line);
defaultIssue.setChecksum(hash);
defaultIssue.setMessage(message);