import org.sonar.db.DbSession;
import org.sonar.db.DbTester;
import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.AnticipatedTransitionDto;
import org.sonar.db.issue.IssueChangeDto;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueMapper;
entry("inserts", "0"), entry("updates", "1"), entry("merged", "0"));
}
+ @Test
+ public void when_anticipatedTransitionIsPresent_ItShouldBeDeleted() {
+ periodHolder.setPeriod(new Period(NewCodePeriodType.REFERENCE_BRANCH.name(), "master", null));
+ RuleDto rule = RuleTesting.newRule(RuleKey.of("xoo", "S01"));
+ db.rules().insert(rule);
+ ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
+ ComponentDto file = db.components().insertComponent(newFileDto(project));
+ session.commit();
+ String issueKey = "ISSUE-4";
+
+ DefaultIssue newIssue = new DefaultIssue()
+ .setKey(issueKey)
+ .setType(RuleType.CODE_SMELL)
+ .setRuleKey(rule.getKey())
+ .setComponentUuid(file.uuid())
+ .setComponentKey(file.getKey())
+ .setProjectUuid(project.uuid())
+ .setProjectKey(project.getKey())
+ .setSeverity(BLOCKER)
+ .setStatus(STATUS_OPEN)
+ .setCreationDate(new Date(NOW))
+ .setNew(true)
+ .setIsOnChangedLine(true)
+ .setType(RuleType.BUG);
+
+ AnticipatedTransitionDto atDto = db.anticipatedTransitions().createForIssue(newIssue, "test_uuid", file.name());
+ newIssue.setAnticipatedTransitionUuid(atDto.getUuid());
+
+ var defaultIssueCacheAppender = protoIssueCache.newAppender();
+ defaultIssueCacheAppender.append(newIssue).close();
+
+ TestComputationStepContext context = new TestComputationStepContext();
+ underTest.execute(context);
+
+ IssueDto result = dbClient.issueDao().selectOrFailByKey(session, issueKey);
+ assertThat(result.getKey()).isEqualTo(issueKey);
+ assertThat(result.getRuleKey()).isEqualTo(rule.getKey());
+ assertThat(result.getComponentUuid()).isEqualTo(file.uuid());
+ assertThat(result.getProjectUuid()).isEqualTo(project.uuid());
+ assertThat(result.getSeverity()).isEqualTo(BLOCKER);
+ assertThat(result.getStatus()).isEqualTo(STATUS_OPEN);
+ assertThat(result.getType()).isEqualTo(RuleType.BUG.getDbConstant());
+ assertThat(context.getStatistics().getAll()).contains(
+ entry("inserts", "1"), entry("updates", "0"), entry("merged", "0"));
+ assertThat(result.isNewCodeReferenceIssue()).isTrue();
+
+ assertThat(db.anticipatedTransitions().selectByProjectUuid(project.uuid())).isEmpty();
+ }
}
public interface AnticipatedTransitionRepository {
Collection<AnticipatedTransition> getAnticipatedTransitionByComponent(Component component);
-
}
private Collection<AnticipatedTransition> getAnticipatedTransitions(List<AnticipatedTransitionDto> anticipatedTransitionDtos) {
return anticipatedTransitionDtos
.stream()
- .map(this::getAnticipatedTransition)
+ .map(AnticipatedTransitionRepositoryImpl::getAnticipatedTransition)
.toList();
}
- private AnticipatedTransition getAnticipatedTransition(AnticipatedTransitionDto transitionDto) {
+ private static AnticipatedTransition getAnticipatedTransition(AnticipatedTransitionDto transitionDto) {
return new AnticipatedTransition(
+ transitionDto.getUuid(),
transitionDto.getProjectUuid(),
transitionDto.getUserUuid(),
RuleKey.parse(transitionDto.getRuleKey()),
transitionDto.getComment()
);
}
-
}
private void performAnticipatedTransition(DefaultIssue issue, AnticipatedTransition anticipatedTransition) {
issue.setBeingClosed(true);
- issue.setAnticipatedTransitions(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";
import org.sonar.db.BatchSession;
import org.sonar.db.DbClient;
import org.sonar.db.DbSession;
+import org.sonar.db.issue.AnticipatedTransitionMapper;
import org.sonar.db.issue.IssueChangeMapper;
import org.sonar.db.issue.IssueDto;
import org.sonar.db.issue.IssueMapper;
IssueMapper mapper = dbSession.getMapper(IssueMapper.class);
IssueChangeMapper changeMapper = dbSession.getMapper(IssueChangeMapper.class);
+ AnticipatedTransitionMapper anticipatedTransitionMapper = dbSession.getMapper(AnticipatedTransitionMapper.class);
while (issues.hasNext()) {
DefaultIssue issue = issues.next();
if (issue.isNew() || issue.isCopied()) {
addedIssues.add(issue);
if (addedIssues.size() >= ISSUE_BATCHING_SIZE) {
- persistNewIssues(statistics, addedIssues, mapper, changeMapper);
+ persistNewIssues(statistics, addedIssues, mapper, changeMapper, anticipatedTransitionMapper);
addedIssues.clear();
}
} else if (issue.isChanged()) {
}
}
}
- persistNewIssues(statistics, addedIssues, mapper, changeMapper);
+ persistNewIssues(statistics, addedIssues, mapper, changeMapper, anticipatedTransitionMapper);
persistUpdatedIssues(statistics, updatedIssues, mapper, changeMapper);
persistNoLongerNewIssues(statistics, noLongerNewIssues, mapper);
persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, mapper);
}
}
- private void persistNewIssues(IssueStatistics statistics, List<DefaultIssue> addedIssues, IssueMapper mapper, IssueChangeMapper changeMapper) {
- if (addedIssues.isEmpty()) {
- return;
- }
+ private void persistNewIssues(IssueStatistics statistics, List<DefaultIssue> addedIssues,
+ IssueMapper mapper, IssueChangeMapper changeMapper, AnticipatedTransitionMapper anticipatedTransitionMapper) {
- long now = system2.now();
- addedIssues.forEach(i -> {
- String ruleUuid = ruleRepository.getByKey(i.ruleKey()).getUuid();
- IssueDto dto = IssueDto.toDtoForComputationInsert(i, ruleUuid, now);
+ final long now = system2.now();
+
+ addedIssues.forEach(addedIssue -> {
+ String ruleUuid = ruleRepository.getByKey(addedIssue.ruleKey()).getUuid();
+ IssueDto dto = IssueDto.toDtoForComputationInsert(addedIssue, ruleUuid, now);
mapper.insert(dto);
- if (isOnBranchUsingReferenceBranch() && i.isOnChangedLine()) {
+ if (isOnBranchUsingReferenceBranch() && addedIssue.isOnChangedLine()) {
mapper.insertAsNewCodeOnReferenceBranch(NewCodeReferenceIssueDto.fromIssueDto(dto, now, uuidFactory));
}
statistics.inserts++;
+ issueStorage.insertChanges(changeMapper, addedIssue, uuidFactory);
+ addedIssue.getAnticipatedTransitionUuid().ifPresent(anticipatedTransitionMapper::delete);
});
-
- addedIssues.forEach(i -> issueStorage.insertChanges(changeMapper, i, uuidFactory));
}
private void persistUpdatedIssues(IssueStatistics statistics, List<DefaultIssue> updatedIssues, IssueMapper mapper, IssueChangeMapper changeMapper) {
defaultIssue.setSelectedAt(next.hasSelectedAt() ? next.getSelectedAt() : null);
defaultIssue.setQuickFixAvailable(next.getQuickFixAvailable());
defaultIssue.setIsNoLongerNewCodeReferenceIssue(next.getIsNoLongerNewCodeReferenceIssue());
+ if (next.hasAnticipatedTransitionUuid()) {
+ defaultIssue.setAnticipatedTransitionUuid(next.getAnticipatedTransitionUuid());
+ }
for (IssueCache.FieldDiffs protoFieldDiffs : next.getChangesList()) {
defaultIssue.addChange(toDefaultIssueChanges(protoFieldDiffs));
ofNullable(defaultIssue.selectedAt()).ifPresent(builder::setSelectedAt);
builder.setQuickFixAvailable(defaultIssue.isQuickFixAvailable());
builder.setIsNoLongerNewCodeReferenceIssue(defaultIssue.isNoLongerNewCodeReferenceIssue());
+ defaultIssue.getAnticipatedTransitionUuid().ifPresent(builder::setAnticipatedTransitionUuid);
for (FieldDiffs fieldDiffs : defaultIssue.changes()) {
builder.addChanges(toProtoIssueChanges(fieldDiffs));
optional sonarqube.db.issues.MessageFormattings messageFormattings = 45;
optional string codeVariants = 46;
optional string assigneeLogin = 47;
+ optional string anticipatedTransitionUuid = 48;
}
message Comment {
underTest.onIssue(component, issue);
assertThat(issue.isBeingClosed()).isTrue();
- assertThat(issue.hasAnticipatedTransitions()).isTrue();
+ assertThat(issue.getAnticipatedTransitionUuid()).isPresent();
verify(issueLifecycle).doManualTransition(issue, "wontfix", "admin");
verify(issueLifecycle).addComment(issue, "doing the transition in an anticipated way", "admin");
}
underTest.onIssue(component, issue);
assertThat(issue.isBeingClosed()).isFalse();
- assertThat(issue.hasAnticipatedTransitions()).isFalse();
+ assertThat(issue.getAnticipatedTransitionUuid()).isNotPresent();
verifyNoInteractions(issueLifecycle);
}
underTest.onIssue(component, issue);
assertThat(issue.isBeingClosed()).isTrue();
- assertThat(issue.hasAnticipatedTransitions()).isTrue();
+ assertThat(issue.getAnticipatedTransitionUuid()).isPresent();
verify(issueLifecycle).doManualTransition(issue, "wontfix", "admin");
verify(issueLifecycle).addComment(issue, "Automatically transitioned from SonarLint", "admin");
}
}
private Collection<AnticipatedTransition> getAnticipatedTransitions(String projecKey, String fileName) {
- return Stream.of(new AnticipatedTransition(projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", "wontfix", "doing the transition in an anticipated way")).collect(Collectors.toList());
+ return Stream.of(new AnticipatedTransition("atuuid", projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", "wontfix", "doing the transition in an anticipated way")).collect(Collectors.toList());
}
private Collection<AnticipatedTransition> getAnticipatedTransitionsWithEmptyComment(String projecKey, String fileName) {
- return Stream.of(new AnticipatedTransition(projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", "wontfix", null)).collect(Collectors.toList());
+ return Stream.of(new AnticipatedTransition("atuuid", projecKey, "admin", RuleKey.parse("repo:id"), "issue message", fileName, 1, "abcdefghi", "wontfix", null)).collect(Collectors.toList());
}
private Component getComponent(Component.Type type) {
from anticipated_transitions at
where at.project_uuid=#{projectUuid,jdbcType=VARCHAR} and at.file_path=#{filePath,jdbcType=VARCHAR}
</select>
-
</mapper>
import org.sonar.core.util.UuidFactory;
import org.sonar.db.alm.integration.pat.AlmPatsDbTester;
import org.sonar.db.almsettings.AlmSettingsDbTester;
+import org.sonar.db.anticipatedtransition.AnticipatedTransitionDbTester;
import org.sonar.db.audit.AuditDbTester;
import org.sonar.db.audit.AuditPersister;
import org.sonar.db.audit.NoOpAuditPersister;
private final AlmSettingsDbTester almSettingsDbTester;
private final AlmPatsDbTester almPatsDbtester;
private final AuditDbTester auditDbTester;
+ private final AnticipatedTransitionDbTester anticipatedTransitionDbTester;
private DbTester(System2 system2, @Nullable String schemaPath, AuditPersister auditPersister, MyBatisConfExtension... confExtensions) {
super(TestDbImpl.create(schemaPath, confExtensions));
this.almSettingsDbTester = new AlmSettingsDbTester(this);
this.almPatsDbtester = new AlmPatsDbTester(this);
this.auditDbTester = new AuditDbTester(this);
+ this.anticipatedTransitionDbTester = new AnticipatedTransitionDbTester(this);
}
public static DbTester create() {
return auditDbTester;
}
+ public AnticipatedTransitionDbTester anticipatedTransitions() {
+ return anticipatedTransitionDbTester;
+ }
+
@Override
protected void after() {
if (session != null) {
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.db.anticipatedtransition;
+
+import java.time.Instant;
+import java.util.List;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.issue.AnticipatedTransitionDto;
+import org.sonar.db.issue.AnticipatedTransitionMapper;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+
+public class AnticipatedTransitionDbTester {
+
+ private final DbTester db;
+
+ public AnticipatedTransitionDbTester(DbTester db) {
+ this.db = db;
+ }
+
+ public AnticipatedTransitionDto createForIssue(DefaultIssue issue, String userUuid, String filePath) {
+ var dto = new AnticipatedTransitionDto(
+ "uuid_" + randomAlphabetic(5),
+ issue.projectUuid(),
+ userUuid,
+ "wontfix",
+ "comment for transition",
+ issue.getLine(),
+ issue.getMessage(),
+ null,
+ issue.getRuleKey().rule(),
+ filePath,
+ Instant.now().toEpochMilli());
+ db.getDbClient().anticipatedTransitionDao().insert(db.getSession(), dto);
+ db.commit();
+ return dto;
+ }
+
+ public List<AnticipatedTransitionDto> selectByProjectUuid(String projectUuid) {
+ try (DbSession session = db.getDbClient().openSession(false)) {
+ AnticipatedTransitionMapper mapper = session.getMapper(AnticipatedTransitionMapper.class);
+ return mapper.selectByProjectUuid(projectUuid);
+ }
+ }
+}
db.getDbClient().issueDao().insertAsNewCodeOnReferenceBranch(db.getSession(), IssueTesting.newCodeReferenceIssue(issue));
db.commit();
}
-
}
changeDto.setProjectUuid(issue.projectUuid());
mapper.insert(changeDto);
}
- } else if ((!issue.isNew() || issue.hasAnticipatedTransitions()) && diffs != null) {
+ } else if ((!issue.isNew() || issue.getAnticipatedTransitionUuid().isPresent()) && diffs != null) {
IssueChangeDto changeDto = IssueChangeDto.of(issue.key(), diffs, issue.projectUuid());
changeDto.setUuid(uuidFactory.create());
changeDto.setProjectUuid(issue.projectUuid());
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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.server.issue;
+
+import java.util.Date;
+import org.junit.Test;
+import org.mockito.MockedStatic;
+import org.sonar.api.rule.RuleKey;
+import org.sonar.api.rules.RuleType;
+import org.sonar.api.utils.DateUtils;
+import org.sonar.api.utils.Duration;
+import org.sonar.core.issue.DefaultIssue;
+import org.sonar.core.issue.DefaultIssueComment;
+import org.sonar.core.issue.FieldDiffs;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.db.issue.IssueChangeDto;
+import org.sonar.db.issue.IssueChangeMapper;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class IssueStorageTest {
+ private final IssueStorage underTest = new IssueStorage();
+ private final UuidFactory uuidFactory = mock(UuidFactory.class);
+ private final IssueChangeMapper issueChangeMapper = mock(IssueChangeMapper.class);
+
+ @Test
+ public void when_newIssueWithAnticipatedTransitionInserted_twoChangelogCreated() {
+ when(uuidFactory.create()).thenReturn("uuid");
+
+ String issueKey = "ABCDE";
+ String commentText = "comment for new issue";
+ DefaultIssueComment comment = DefaultIssueComment.create(issueKey, "user_uuid", commentText);
+ comment.setKey("FGHIJ");
+ Date date = DateUtils.parseDateTime("2013-05-18T12:00:00+0000");
+
+ DefaultIssue issue = new DefaultIssue()
+ .setKey(issueKey)
+ .setType(RuleType.BUG)
+ .setNew(true)
+ .setRuleKey(RuleKey.of("keyRepo", "r:2145"))
+ .setProjectUuid("projectUuid")
+ .setComponentUuid("fileUuid")
+ .setLine(5000)
+ .setEffort(Duration.create(10L))
+ .setResolution("wontfix")
+ .setStatus("CLOSED")
+ .setSeverity("BLOCKER")
+ .addComment(comment)
+ .setCreationDate(date)
+ .setUpdateDate(date)
+ .setCloseDate(date)
+ .setCurrentChange(new FieldDiffs())
+ .setAnticipatedTransitionUuid("anticipatedTransitionUuid");
+
+ IssueChangeDto mockCreated = mock(IssueChangeDto.class);
+ IssueChangeDto mockAnticipatedTransition = mock(IssueChangeDto.class);
+ try (MockedStatic<IssueChangeDto> issueChangeDtoMockedStatic = mockStatic(IssueChangeDto.class)) {
+ issueChangeDtoMockedStatic.when(() -> IssueChangeDto.of(any(DefaultIssueComment.class), anyString()))
+ .thenReturn(mockCreated);
+ issueChangeDtoMockedStatic.when(() -> IssueChangeDto.of(anyString(), any(FieldDiffs.class), anyString()))
+ .thenReturn(mockAnticipatedTransition);
+ underTest.insertChanges(issueChangeMapper, issue, uuidFactory);
+ }
+ verify(issueChangeMapper, times(2)).insert(any(IssueChangeDto.class));
+ verify(issueChangeMapper).insert(mockCreated);
+ verify(issueChangeMapper).insert(mockAnticipatedTransition);
+ }
+}
private static List<AnticipatedTransition> mapBodyToAnticipatedTransitions(List<GsonAnticipatedTransition> anticipatedTransitions, String userUuid, String projectKey) {
return anticipatedTransitions.stream()
.map(anticipatedTransition -> new AnticipatedTransition(
+ null,
projectKey,
userUuid,
RuleKey.parse(anticipatedTransition.ruleKey()),
private AnticipatedTransition populateAnticipatedTransition() {
return new AnticipatedTransition(
+ null,
PROJECT_KEY,
USER_UUID,
RuleKey.of("repo", "squid:S0001"),
"transition1",
"comment1");
}
-}
\ No newline at end of file
+}
private List<AnticipatedTransition> transitionsExpectedFromTestFile() {
return List.of(
new AnticipatedTransition(
+ null,
PROJECT_KEY,
USER_UUID,
RuleKey.parse("squid:S0001"),
"wontfix",
"comment1"),
new AnticipatedTransition(
+ null,
PROJECT_KEY,
USER_UUID,
RuleKey.parse("squid:S0002"),
public class AnticipatedTransition implements Trackable {
+ private final String uuid;
private final String projectKey;
private final String transition;
private final String userUuid;
private final RuleKey ruleKey;
public AnticipatedTransition(
+ @Nullable String uuid,
String projectKey,
String userUuid,
@Nullable RuleKey ruleKey,
@Nullable String lineHash,
String transition,
@Nullable String comment) {
+ this.uuid = uuid;
this.projectKey = projectKey;
this.transition = transition;
this.userUuid = userUuid;
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
+
+ public String getUuid() {
+ return uuid;
+ }
}
private String ruleDescriptionContextKey = null;
- private boolean anticipatedTransitions = false;
+ private String anticipatedTransitionUuid = null;
@Override
public String key() {
return this;
}
- public boolean hasAnticipatedTransitions() {
- return anticipatedTransitions;
+ public Optional<String> getAnticipatedTransitionUuid() {
+ return Optional.ofNullable(anticipatedTransitionUuid);
}
- public DefaultIssue setAnticipatedTransitions(boolean anticipatedTransitions) {
- this.anticipatedTransitions = anticipatedTransitions;
+ public DefaultIssue setAnticipatedTransitionUuid(@Nullable String anticipatedTransitionUuid) {
+ this.anticipatedTransitionUuid = anticipatedTransitionUuid;
return this;
}
Assertions.assertThat(anticipatedTransition.getMessage()).isEqualTo(anticipatedTransition2.getMessage());
Assertions.assertThat(anticipatedTransition.getLineHash()).isEqualTo(anticipatedTransition2.getLineHash());
Assertions.assertThat(anticipatedTransition.getRuleKey()).isEqualTo(anticipatedTransition2.getRuleKey());
+ Assertions.assertThat(anticipatedTransition.getUuid()).isEqualTo(anticipatedTransition2.getUuid());
}
private AnticipatedTransition getAnticipatedTransition() {
return new AnticipatedTransition(
+ null,
"projectKey",
"userUuid",
RuleKey.parse("rule:key"),
@Test
public void issueByDefault_shouldNotHaveAppliedAnticipatedTransitions() {
DefaultIssue defaultIssue = new DefaultIssue();
- assertThat(defaultIssue.hasAnticipatedTransitions()).isFalse();
+ assertThat(defaultIssue.getAnticipatedTransitionUuid()).isNotPresent();
}
@Test
public void anticipatedTransitions_WhenSetTrue_shouldReturnTrue() {
DefaultIssue defaultIssue = new DefaultIssue();
- defaultIssue.setAnticipatedTransitions(true);
- assertThat(defaultIssue.hasAnticipatedTransitions()).isTrue();
+ defaultIssue.setAnticipatedTransitionUuid("uuid");
+ assertThat(defaultIssue.getAnticipatedTransitionUuid()).isPresent();
}
}
}
private AnticipatedTransition getAnticipatedTransition(Integer line, String message, String hash, String ruleKey) {
- return new AnticipatedTransition("projectKey",
+ return new AnticipatedTransition(
+ null,
+ "projectKey",
"userUuid",
RuleKey.parse(ruleKey),
message,