From 6c53090d740ad446bf52e37318e3b6b58e94a76d Mon Sep 17 00:00:00 2001 From: =?utf8?q?L=C3=A9o=20Geoffroy?= Date: Wed, 9 Aug 2023 14:18:17 +0200 Subject: [PATCH] SONAR-20021 persist impacts from report to Database --- .../step/PersistIssuesStepIT.java | 10 +++- .../projectanalysis/issue/IssueLifecycle.java | 1 + .../ce/task/projectanalysis/issue/Rule.java | 5 ++ .../task/projectanalysis/issue/RuleImpl.java | 12 +++++ .../issue/RuleRepositoryImpl.java | 8 ++++ .../issue/TrackerRawInputFactory.java | 16 ++++++- .../issue/UpdateConflictResolver.java | 7 +-- .../step/PersistIssuesStep.java | 46 ++++++++++--------- .../util/cache/ProtobufIssueDiskCache.java | 13 +++++- .../src/main/protobuf/issue_cache.proto | 6 +++ .../task/projectanalysis/issue/DumbRule.java | 17 ++++++- .../issue/IssueLifecycleTest.java | 7 ++- .../issue/TrackerRawInputFactoryTest.java | 41 +++++++++++++++-- .../issue/UpdateConflictResolverTest.java | 11 +++-- .../cache/ProtobufIssueDiskCacheTest.java | 33 +++++++++++++ .../java/org/sonar/db/issue/IssueDaoIT.java | 28 +++++++++++ .../org/sonar/db/issue/IssueMapperIT.java | 4 +- .../org/sonar/db/purge/PurgeCommandsIT.java | 2 +- .../java/org/sonar/db/purge/PurgeDaoIT.java | 2 +- .../java/org/sonar/db/issue/IssueDao.java | 16 ++++++- .../java/org/sonar/db/issue/IssueDto.java | 16 ++++++- .../java/org/sonar/db/issue/IssueTesting.java | 3 ++ .../java/org/sonar/db/issue/IssueDtoTest.java | 14 ++++-- .../sonar/server/issue/IssueFieldsSetter.java | 22 +++++++-- .../server/issue/IssueFieldsSetterTest.java | 14 ++++++ .../org/sonar/core/issue/DefaultIssue.java | 16 ++++++- .../sonar/core/issue/DefaultIssueTest.java | 21 +++++++++ 27 files changed, 338 insertions(+), 53 deletions(-) diff --git a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepIT.java b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepIT.java index 77e630aaa39..ae12303dc0a 100644 --- a/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepIT.java +++ b/server/sonar-ce-task-projectanalysis/src/it/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStepIT.java @@ -28,6 +28,8 @@ import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.mockito.ArgumentCaptor; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; @@ -51,8 +53,8 @@ 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.IssueDao; import org.sonar.db.issue.IssueDto; -import org.sonar.db.issue.IssueMapper; import org.sonar.db.newcodeperiod.NewCodePeriodType; import org.sonar.db.rule.RuleDto; import org.sonar.db.rule.RuleTesting; @@ -61,6 +63,7 @@ import org.sonar.server.issue.IssueStorage; import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.tuple; import static org.assertj.core.data.MapEntry.entry; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -314,7 +317,7 @@ public class PersistIssuesStepIT extends BaseStepTest { underTest.execute(context); ArgumentCaptor issueDtoCaptor = ArgumentCaptor.forClass(IssueDto.class); - verify(conflictResolver).resolve(eq(defaultIssue), issueDtoCaptor.capture(), any(IssueMapper.class)); + verify(conflictResolver).resolve(eq(defaultIssue), issueDtoCaptor.capture(), any(IssueDao.class), any(DbSession.class)); assertThat(issueDtoCaptor.getValue().getKey()).isEqualTo(issue.getKey()); assertThat(context.getStatistics().getAll()).contains( entry("inserts", "0"), entry("updates", "1"), entry("merged", "1")); @@ -343,6 +346,7 @@ public class PersistIssuesStepIT extends BaseStepTest { .setCreationDate(new Date(NOW)) .setNew(true) .setIsOnChangedLine(true) + .addImpact(SoftwareQuality.SECURITY, Severity.MEDIUM) .setType(RuleType.BUG)).close(); TestComputationStepContext context = new TestComputationStepContext(); @@ -356,6 +360,8 @@ public class PersistIssuesStepIT extends BaseStepTest { assertThat(result.getSeverity()).isEqualTo(BLOCKER); assertThat(result.getStatus()).isEqualTo(STATUS_OPEN); assertThat(result.getType()).isEqualTo(RuleType.BUG.getDbConstant()); + assertThat(result.getImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactly(tuple(SoftwareQuality.SECURITY, Severity.MEDIUM)); assertThat(context.getStatistics().getAll()).contains( entry("inserts", "1"), entry("updates", "0"), entry("merged", "0")); assertThat(result.isNewCodeReferenceIssue()).isTrue(); diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java index 517dffec253..39bffffc47e 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycle.java @@ -203,6 +203,7 @@ public class IssueLifecycle { updater.setPastGap(raw, base.gap(), changeContext); updater.setPastEffort(raw, base.effort(), changeContext); updater.setCodeVariants(raw, requireNonNull(base.codeVariants()), changeContext); + updater.setImpacts(raw, base.impacts(), changeContext); } public void doAutomaticTransition(DefaultIssue issue) { diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java index 4b6d801d5ca..f59c3a04d56 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/Rule.java @@ -19,8 +19,11 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rules.RuleType; @@ -64,4 +67,6 @@ public interface Rule { String getSeverity(); Set getSecurityStandards(); + + Map getDefaultImpacts(); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java index aa46996a315..265e5c51033 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleImpl.java @@ -20,11 +20,15 @@ package org.sonar.ce.task.projectanalysis.issue; import com.google.common.base.MoreObjects; +import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.CheckForNull; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rules.RuleType; @@ -52,6 +56,7 @@ public class RuleImpl implements Rule { private final String defaultRuleDescription; private final String severity; private final Set securityStandards; + private final Map defaultImpacts; public RuleImpl(RuleDto dto) { this.uuid = dto.getUuid(); @@ -68,6 +73,8 @@ public class RuleImpl implements Rule { this.defaultRuleDescription = getNonNullDefaultRuleDescription(dto); this.severity = Optional.ofNullable(dto.getSeverityString()).orElse(dto.getAdHocSeverity()); this.securityStandards = dto.getSecurityStandards(); + this.defaultImpacts = dto.getDefaultImpacts().stream() + .collect(Collectors.toMap(i -> i.getSoftwareQuality(), i -> i.getSeverity())); } private static String getNonNullDefaultRuleDescription(RuleDto dto) { @@ -137,6 +144,11 @@ public class RuleImpl implements Rule { return securityStandards; } + @Override + public Map getDefaultImpacts() { + return defaultImpacts; + } + @Override public boolean equals(@Nullable Object o) { if (this == o) { diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java index 6f5dd4d2f54..aa7ad4e89fc 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/RuleRepositoryImpl.java @@ -27,6 +27,8 @@ import java.util.Optional; import java.util.Set; import java.util.function.Supplier; import javax.annotation.CheckForNull; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rules.RuleType; @@ -226,5 +228,11 @@ public class RuleRepositoryImpl implements RuleRepository { public Set getSecurityStandards() { return Collections.emptySet(); } + + @Override + public Map getDefaultImpacts() { + //TODO external issues + return Collections.emptyMap(); + } } } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java index 7996cafb8c0..17fcfd71b47 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactory.java @@ -25,12 +25,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; import javax.annotation.Nullable; import org.jetbrains.annotations.NotNull; +import org.slf4j.LoggerFactory; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; -import org.slf4j.LoggerFactory; import org.sonar.ce.task.projectanalysis.batch.BatchReportReader; import org.sonar.ce.task.projectanalysis.component.Component; import org.sonar.ce.task.projectanalysis.component.TreeRootHolder; @@ -192,6 +194,8 @@ public class TrackerRawInputFactory { issue.setQuickFixAvailable(reportIssue.getQuickFixAvailable()); issue.setRuleDescriptionContextKey(reportIssue.hasRuleDescriptionContextKey() ? reportIssue.getRuleDescriptionContextKey() : null); issue.setCodeVariants(reportIssue.getCodeVariantsList()); + + issue.replaceImpacts(convertImpacts(issue.ruleKey(), reportIssue.getOverridenImpactsList())); return issue; } @@ -328,6 +332,16 @@ public class TrackerRawInputFactory { } } + private Map convertImpacts(RuleKey ruleKey, List overridenImpactsList) { + if (overridenImpactsList.isEmpty()) { + return Collections.emptyMap(); + } + Rule rule = ruleRepository.getByKey(ruleKey); + return overridenImpactsList.stream() + .filter(i -> rule.getDefaultImpacts().containsKey(SoftwareQuality.valueOf(i.getSoftwareQuality()))) + .collect(Collectors.toMap(i -> SoftwareQuality.valueOf(i.getSoftwareQuality()), i -> org.sonar.api.issue.impact.Severity.valueOf(i.getSeverity()))); + } + private static DbIssues.MessageFormattings convertMessageFormattings(List msgFormattings) { DbIssues.MessageFormattings.Builder builder = DbIssues.MessageFormattings.newBuilder(); msgFormattings.stream() diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolver.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolver.java index 2d3dc830a46..16220799ee2 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolver.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolver.java @@ -23,8 +23,9 @@ import com.google.common.annotations.VisibleForTesting; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.DbSession; +import org.sonar.db.issue.IssueDao; import org.sonar.db.issue.IssueDto; -import org.sonar.db.issue.IssueMapper; /** * Support concurrent modifications on issues made by analysis and users at the same time @@ -34,10 +35,10 @@ public class UpdateConflictResolver { private static final Logger LOG = LoggerFactory.getLogger(UpdateConflictResolver.class); - public void resolve(DefaultIssue issue, IssueDto dbIssue, IssueMapper mapper) { + public void resolve(DefaultIssue issue, IssueDto dbIssue, IssueDao issueDao, DbSession dbSession) { LOG.debug("Resolve conflict on issue {}", issue.key()); mergeFields(dbIssue, issue); - mapper.update(IssueDto.toDtoForUpdate(issue, System.currentTimeMillis())); + issueDao.update(dbSession, IssueDto.toDtoForUpdate(issue, System.currentTimeMillis())); } @VisibleForTesting diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStep.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStep.java index 363b88507d4..9a7fea9364e 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStep.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/step/PersistIssuesStep.java @@ -38,6 +38,7 @@ 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.IssueDao; import org.sonar.db.issue.IssueDto; import org.sonar.db.issue.IssueMapper; import org.sonar.db.issue.NewCodeReferenceIssueDto; @@ -83,8 +84,7 @@ public class PersistIssuesStep implements ComputationStep { List updatedIssues = new ArrayList<>(ISSUE_BATCHING_SIZE); List noLongerNewIssues = new ArrayList<>(ISSUE_BATCHING_SIZE); List newCodeIssuesToMigrate = new ArrayList<>(ISSUE_BATCHING_SIZE); - - IssueMapper mapper = dbSession.getMapper(IssueMapper.class); + IssueDao issueDao = dbClient.issueDao(); IssueChangeMapper changeMapper = dbSession.getMapper(IssueChangeMapper.class); AnticipatedTransitionMapper anticipatedTransitionMapper = dbSession.getMapper(AnticipatedTransitionMapper.class); while (issues.hasNext()) { @@ -92,33 +92,34 @@ public class PersistIssuesStep implements ComputationStep { if (issue.isNew() || issue.isCopied()) { addedIssues.add(issue); if (addedIssues.size() >= ISSUE_BATCHING_SIZE) { - persistNewIssues(statistics, addedIssues, mapper, changeMapper, anticipatedTransitionMapper); + persistNewIssues(statistics, addedIssues, issueDao, changeMapper, anticipatedTransitionMapper, dbSession); addedIssues.clear(); } } else if (issue.isChanged()) { updatedIssues.add(issue); if (updatedIssues.size() >= ISSUE_BATCHING_SIZE) { - persistUpdatedIssues(statistics, updatedIssues, mapper, changeMapper); + persistUpdatedIssues(statistics, updatedIssues, issueDao, changeMapper, dbSession); updatedIssues.clear(); } } else if (isOnBranchUsingReferenceBranch() && issue.isNoLongerNewCodeReferenceIssue()) { noLongerNewIssues.add(issue); if (noLongerNewIssues.size() >= ISSUE_BATCHING_SIZE) { - persistNoLongerNewIssues(statistics, noLongerNewIssues, mapper); + persistNoLongerNewIssues(statistics, noLongerNewIssues, issueDao, dbSession); noLongerNewIssues.clear(); } } else if (isOnBranchUsingReferenceBranch() && issue.isToBeMigratedAsNewCodeReferenceIssue()) { newCodeIssuesToMigrate.add(issue); if (newCodeIssuesToMigrate.size() >= ISSUE_BATCHING_SIZE) { - persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, mapper); + persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, issueDao, dbSession); newCodeIssuesToMigrate.clear(); } } } - persistNewIssues(statistics, addedIssues, mapper, changeMapper, anticipatedTransitionMapper); - persistUpdatedIssues(statistics, updatedIssues, mapper, changeMapper); - persistNoLongerNewIssues(statistics, noLongerNewIssues, mapper); - persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, mapper); + + persistNewIssues(statistics, addedIssues, issueDao, changeMapper, anticipatedTransitionMapper, dbSession); + persistUpdatedIssues(statistics, updatedIssues, issueDao, changeMapper, dbSession); + persistNoLongerNewIssues(statistics, noLongerNewIssues, issueDao, dbSession); + persistNewCodeIssuesToMigrate(statistics, newCodeIssuesToMigrate, issueDao, dbSession); flushSession(dbSession); } finally { statistics.dumpTo(context); @@ -126,16 +127,16 @@ public class PersistIssuesStep implements ComputationStep { } private void persistNewIssues(IssueStatistics statistics, List addedIssues, - IssueMapper mapper, IssueChangeMapper changeMapper, AnticipatedTransitionMapper anticipatedTransitionMapper) { + IssueDao issueDao, IssueChangeMapper changeMapper, AnticipatedTransitionMapper anticipatedTransitionMapper, DbSession dbSession) { 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); + issueDao.insert(dbSession, dto); if (isOnBranchUsingReferenceBranch() && addedIssue.isOnChangedLine()) { - mapper.insertAsNewCodeOnReferenceBranch(NewCodeReferenceIssueDto.fromIssueDto(dto, now, uuidFactory)); + issueDao.insertAsNewCodeOnReferenceBranch(dbSession, NewCodeReferenceIssueDto.fromIssueDto(dto, now, uuidFactory)); } statistics.inserts++; issueStorage.insertChanges(changeMapper, addedIssue, uuidFactory); @@ -143,7 +144,8 @@ public class PersistIssuesStep implements ComputationStep { }); } - private void persistUpdatedIssues(IssueStatistics statistics, List updatedIssues, IssueMapper mapper, IssueChangeMapper changeMapper) { + private void persistUpdatedIssues(IssueStatistics statistics, List updatedIssues, IssueDao + issueDao, IssueChangeMapper changeMapper, DbSession dbSession) { if (updatedIssues.isEmpty()) { return; } @@ -151,19 +153,19 @@ public class PersistIssuesStep implements ComputationStep { long now = system2.now(); updatedIssues.forEach(i -> { IssueDto dto = IssueDto.toDtoForUpdate(i, now); - mapper.updateIfBeforeSelectedDate(dto); + issueDao.updateIfBeforeSelectedDate(dbSession, dto); statistics.updates++; }); // retrieve those of the updatedIssues which have not been updated and apply conflictResolver on them List updatedIssueKeys = updatedIssues.stream().map(DefaultIssue::key).toList(); - List conflictIssueKeys = mapper.selectByKeysIfNotUpdatedAt(updatedIssueKeys, now); + List conflictIssueKeys = issueDao.selectByKeysIfNotUpdatedAt(dbSession, updatedIssueKeys, now); if (!conflictIssueKeys.isEmpty()) { Map issuesByKeys = updatedIssues.stream().collect(Collectors.toMap(DefaultIssue::key, Function.identity())); conflictIssueKeys .forEach(dbIssue -> { DefaultIssue updatedIssue = issuesByKeys.get(dbIssue.getKey()); - conflictResolver.resolve(updatedIssue, dbIssue, mapper); + conflictResolver.resolve(updatedIssue, dbIssue, issueDao, dbSession); statistics.merged++; }); } @@ -171,26 +173,28 @@ public class PersistIssuesStep implements ComputationStep { updatedIssues.forEach(i -> issueStorage.insertChanges(changeMapper, i, uuidFactory)); } - private static void persistNoLongerNewIssues(IssueStatistics statistics, List noLongerNewIssues, IssueMapper mapper) { + private static void persistNoLongerNewIssues(IssueStatistics + statistics, List noLongerNewIssues, IssueDao issueDao, DbSession dbSession) { if (noLongerNewIssues.isEmpty()) { return; } noLongerNewIssues.forEach(i -> { - mapper.deleteAsNewCodeOnReferenceBranch(i.key()); + issueDao.deleteAsNewCodeOnReferenceBranch(dbSession, i.key()); statistics.updates++; }); } - private void persistNewCodeIssuesToMigrate(IssueStatistics statistics, List newCodeIssuesToMigrate, IssueMapper mapper) { + private void persistNewCodeIssuesToMigrate(IssueStatistics + statistics, List newCodeIssuesToMigrate, IssueDao issueDao, DbSession dbSession) { if (newCodeIssuesToMigrate.isEmpty()) { return; } long now = system2.now(); newCodeIssuesToMigrate.forEach(i -> { - mapper.insertAsNewCodeOnReferenceBranch(NewCodeReferenceIssueDto.fromIssueKey(i.key(), now, uuidFactory)); + issueDao.insertAsNewCodeOnReferenceBranch(dbSession, NewCodeReferenceIssueDto.fromIssueKey(i.key(), now, uuidFactory)); statistics.updates++; }); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java index 69653021a60..4e015401a3b 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java +++ b/server/sonar-ce-task-projectanalysis/src/main/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCache.java @@ -32,6 +32,8 @@ import java.util.Collections; import java.util.Date; import java.util.Map; import javax.annotation.CheckForNull; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; @@ -140,10 +142,12 @@ public class ProtobufIssueDiskCache implements DiskCache { defaultIssue.setAnticipatedTransitionUuid(next.getAnticipatedTransitionUuid()); } + for (IssueCache.Impact impact : next.getImpactsList()) { + defaultIssue.addImpact(SoftwareQuality.valueOf(impact.getSoftwareQuality()), Severity.valueOf(impact.getSeverity())); + } for (IssueCache.FieldDiffs protoFieldDiffs : next.getChangesList()) { defaultIssue.addChange(toDefaultIssueChanges(protoFieldDiffs)); } - return defaultIssue; } @@ -194,6 +198,13 @@ public class ProtobufIssueDiskCache implements DiskCache { builder.setIsNoLongerNewCodeReferenceIssue(defaultIssue.isNoLongerNewCodeReferenceIssue()); defaultIssue.getAnticipatedTransitionUuid().ifPresent(builder::setAnticipatedTransitionUuid); + + for (Map.Entry impact : defaultIssue.impacts().entrySet()) { + builder.addImpacts(IssueCache.Impact.newBuilder() + .setSoftwareQuality(impact.getKey().name()) + .setSeverity(impact.getValue().name()) + .build()); + } for (FieldDiffs fieldDiffs : defaultIssue.changes()) { builder.addChanges(toProtoIssueChanges(fieldDiffs)); } diff --git a/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto b/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto index c75e6f58509..77eabd0a214 100644 --- a/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto +++ b/server/sonar-ce-task-projectanalysis/src/main/protobuf/issue_cache.proto @@ -84,6 +84,7 @@ message Issue { optional string codeVariants = 46; optional string assigneeLogin = 47; optional string anticipatedTransitionUuid = 48; + repeated Impact impacts = 49; } message Comment { @@ -107,3 +108,8 @@ message Diff { optional string oldValue = 1; optional string newValue = 2; } + +message Impact { + required string software_quality = 1; + required string severity = 2; +} diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java index 532ad64422a..1e551f5dbeb 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/DumbRule.java @@ -19,10 +19,14 @@ */ package org.sonar.ce.task.projectanalysis.issue; +import java.util.EnumMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nullable; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.RuleStatus; import org.sonar.api.rules.RuleType; @@ -42,7 +46,8 @@ public class DumbRule implements Rule { private String pluginKey; private boolean isExternal; private boolean isAdHoc; - private Set securityStandards = new HashSet<>(); + private final Set securityStandards = new HashSet<>(); + private final Map defaultImpacts = new EnumMap<>(SoftwareQuality.class); public DumbRule(RuleKey key) { this.key = key; @@ -111,6 +116,11 @@ public class DumbRule implements Rule { return securityStandards; } + @Override + public Map getDefaultImpacts() { + return defaultImpacts; + } + @Override public boolean isExternal() { return isExternal; @@ -171,4 +181,9 @@ public class DumbRule implements Rule { return this; } + public DumbRule addDefaultImpact(SoftwareQuality softwareQuality, Severity severity) { + defaultImpacts.put(softwareQuality, severity); + return this; + } + } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java index e995540494e..8c925bc46fb 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/IssueLifecycleTest.java @@ -23,6 +23,8 @@ import java.util.Date; import java.util.Set; import org.junit.Rule; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; import org.sonar.ce.task.projectanalysis.analysis.AnalysisMetadataHolderRule; @@ -386,6 +388,7 @@ public class IssueLifecycleTest { .setRuleKey(XOO_X1) .setRuleDescriptionContextKey("spring") .setCodeVariants(Set.of("foo", "bar")) + .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH) .setCreationDate(parseDate("2015-10-01")) .setUpdateDate(parseDate("2015-10-02")) .setCloseDate(parseDate("2015-10-03")); @@ -425,6 +428,7 @@ public class IssueLifecycleTest { .setGap(15d) .setRuleDescriptionContextKey("hibernate") .setCodeVariants(Set.of("donut")) + .addImpact(SoftwareQuality.RELIABILITY, Severity.LOW) .setEffort(Duration.create(15L)) .setManualSeverity(false) .setLocations(issueLocations) @@ -455,7 +459,8 @@ public class IssueLifecycleTest { .containsOnly(entry("foo", new FieldDiffs.Diff<>("bar", "donut"))); assertThat(raw.changes().get(1).diffs()) .containsOnly(entry("file", new FieldDiffs.Diff<>("A", "B"))); - + assertThat(raw.impacts()) + .containsEntry(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); verify(updater).setPastSeverity(raw, BLOCKER, issueChangeContext); verify(updater).setPastLine(raw, 10); verify(updater).setRuleDescriptionContextKey(raw, "hibernate"); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java index 67647062a1d..a40e033b5f6 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/TrackerRawInputFactoryTest.java @@ -25,11 +25,14 @@ import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Optional; +import java.util.function.Consumer; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; @@ -123,11 +126,15 @@ public class TrackerRawInputFactoryTest { public void load_issues_from_report() { RuleKey ruleKey = RuleKey.of("java", "S001"); markRuleAsActive(ruleKey); - + registerRule(ruleKey, "name", r -> r.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() .setTextRange(newTextRange(2)) .setMsg("the message") .addMsgFormatting(ScannerReport.MessageFormatting.newBuilder().setStart(0).setEnd(3).setType(CODE).build()) + .addOverridenImpacts(ScannerReport.Impact.newBuilder() + .setSoftwareQuality(SoftwareQuality.MAINTAINABILITY.name()) + .setSeverity(org.sonar.api.issue.impact.Severity.HIGH.name()) + .build()) .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) .setSeverity(Constants.Severity.BLOCKER) @@ -162,6 +169,7 @@ public class TrackerRawInputFactoryTest { assertInitializedIssue(issue); assertThat(issue.effort()).isNull(); assertThat(issue.getRuleDescriptionContextKey()).isEmpty(); + assertThat(issue.impacts()).containsExactlyEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.HIGH)); } @Test @@ -231,11 +239,35 @@ public class TrackerRawInputFactoryTest { .containsOnly(Optional.of(TEST_CONTEXT_KEY)); } + @Test + public void create_whenImpactIsNotDefinedAtRuleLevel_shouldDiscardImpacts() { + RuleKey ruleKey = RuleKey.of("java", "S001"); + markRuleAsActive(ruleKey); + registerRule(ruleKey, "name", r -> r.addDefaultImpact(SoftwareQuality.MAINTAINABILITY, org.sonar.api.issue.impact.Severity.LOW)); + ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() + .setTextRange(newTextRange(2)) + .setMsg("the message") + .setRuleRepository(ruleKey.repository()) + .setRuleKey(ruleKey.rule()) + .addOverridenImpacts(ScannerReport.Impact.newBuilder() + .setSoftwareQuality(SoftwareQuality.SECURITY.name()) + .setSeverity(org.sonar.api.issue.impact.Severity.LOW.name()).build()) + .build(); + reportReader.putIssues(FILE.getReportAttributes().getRef(), singletonList(reportIssue)); + Input input = underTest.create(FILE); + + Collection issues = input.getIssues(); + assertThat(issues) + .hasSize(1); + assertThat(issues.iterator().next().impacts()).isEmpty(); + } + @Test public void set_rule_name_as_message_when_issue_message_from_report_is_empty() { RuleKey ruleKey = RuleKey.of("java", "S001"); markRuleAsActive(ruleKey); - registerRule(ruleKey, "Rule 1"); + registerRule(ruleKey, "Rule 1", r -> { + }); ScannerReport.Issue reportIssue = ScannerReport.Issue.newBuilder() .setRuleRepository(ruleKey.repository()) .setRuleKey(ruleKey.rule()) @@ -350,7 +382,7 @@ public class TrackerRawInputFactoryTest { @DataProvider public static Object[][] ruleTypeAndStatusByIssueType() { - return new Object[][] { + return new Object[][]{ {IssueType.CODE_SMELL, RuleType.CODE_SMELL, STATUS_OPEN}, {IssueType.BUG, RuleType.BUG, STATUS_OPEN}, {IssueType.VULNERABILITY, RuleType.VULNERABILITY, STATUS_OPEN}, @@ -474,9 +506,10 @@ public class TrackerRawInputFactoryTest { activeRulesHolder.put(new ActiveRule(ruleKey, Severity.CRITICAL, emptyMap(), 1_000L, null, "qp1")); } - private void registerRule(RuleKey ruleKey, String name) { + private void registerRule(RuleKey ruleKey, String name, Consumer dumbRulePopulator) { DumbRule dumbRule = new DumbRule(ruleKey); dumbRule.setName(name); + dumbRulePopulator.accept(dumbRule); ruleRepository.add(dumbRule); } } diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolverTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolverTest.java index 88919c1cb57..19ebc222006 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolverTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/issue/UpdateConflictResolverTest.java @@ -26,10 +26,12 @@ import org.sonar.api.rule.RuleKey; import org.sonar.api.rule.Severity; import org.sonar.api.utils.DateUtils; import org.sonar.core.issue.DefaultIssue; +import org.sonar.db.DbSession; +import org.sonar.db.issue.IssueDao; import org.sonar.db.issue.IssueDto; -import org.sonar.db.issue.IssueMapper; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.sonar.api.issue.Issue.STATUS_OPEN; @@ -49,7 +51,8 @@ public class UpdateConflictResolverTest { .setStatus(STATUS_OPEN); // Issue as seen and changed by end-user - IssueMapper mapper = mock(IssueMapper.class); + IssueDao issueDao = mock(IssueDao.class); + DbSession dbSession = mock(DbSession.class); IssueDto issueDto = new IssueDto() .setKee("ABCDE") .setType(CODE_SMELL) @@ -63,10 +66,10 @@ public class UpdateConflictResolverTest { // field changed by user .setAssigneeUuid("arthur-uuid"); - new UpdateConflictResolver().resolve(issue, issueDto, mapper); + new UpdateConflictResolver().resolve(issue, issueDto, issueDao, dbSession); ArgumentCaptor argument = ArgumentCaptor.forClass(IssueDto.class); - verify(mapper).update(argument.capture()); + verify(issueDao).update(any(), argument.capture()); IssueDto updatedIssue = argument.getValue(); assertThat(updatedIssue.getKee()).isEqualTo("ABCDE"); assertThat(updatedIssue.getAssigneeUuid()).isEqualTo("arthur-uuid"); diff --git a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCacheTest.java b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCacheTest.java index d55cb439f50..340bb259769 100644 --- a/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCacheTest.java +++ b/server/sonar-ce-task-projectanalysis/src/test/java/org/sonar/ce/task/projectanalysis/util/cache/ProtobufIssueDiskCacheTest.java @@ -20,7 +20,10 @@ package org.sonar.ce.task.projectanalysis.util.cache; import java.util.Date; +import java.util.Map; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.core.issue.DefaultIssue; @@ -52,6 +55,18 @@ public class ProtobufIssueDiskCacheTest { assertThat(defaultIssue.getRuleDescriptionContextKey()).isEmpty(); } + @Test + public void toDefaultIssue_whenImpactIsSet_shouldSetItInDefaultIssue() { + IssueCache.Issue issue = prepareIssueWithCompulsoryFields() + .addImpacts(toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH)) + .addImpacts(toImpact(SoftwareQuality.RELIABILITY, Severity.LOW)) + .build(); + + DefaultIssue defaultIssue = ProtobufIssueDiskCache.toDefaultIssue(issue); + + assertThat(defaultIssue.impacts()).containsExactlyInAnyOrderEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, SoftwareQuality.RELIABILITY, Severity.LOW)); + } + @Test public void toProto_whenRuleDescriptionContextKeySet_shouldCopyToIssueProto() { DefaultIssue defaultIssue = createDefaultIssueWithMandatoryFields(); @@ -73,6 +88,24 @@ public class ProtobufIssueDiskCacheTest { assertThat(issue.hasRuleDescriptionContextKey()).isFalse(); } + @Test + public void toProto_whenRuleDescriptionContextKeyIsSet_shouldCopyToIssueProto() { + DefaultIssue defaultIssue = createDefaultIssueWithMandatoryFields(); + defaultIssue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); + defaultIssue.addImpact(SoftwareQuality.RELIABILITY, Severity.LOW); + + IssueCache.Issue issue = ProtobufIssueDiskCache.toProto(IssueCache.Issue.newBuilder(), defaultIssue); + + assertThat(issue.getImpactsList()).containsExactly( + toImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH), + toImpact(SoftwareQuality.RELIABILITY, Severity.LOW) + ); + } + + private IssueCache.Impact toImpact(SoftwareQuality softwareQuality, Severity severity) { + return IssueCache.Impact.newBuilder().setSoftwareQuality(softwareQuality.name()).setSeverity(severity.name()).build(); + } + private static DefaultIssue createDefaultIssueWithMandatoryFields() { DefaultIssue defaultIssue = new DefaultIssue(); defaultIssue.setKey("test_issue:key"); diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java index 74a267e7b07..d9f8ac85ee9 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java @@ -37,6 +37,7 @@ import org.sonar.api.rule.RuleKey; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.System2; import org.sonar.core.util.UuidFactoryFast; +import org.sonar.core.util.Uuids; import org.sonar.db.DbSession; import org.sonar.db.DbTester; import org.sonar.db.Pagination; @@ -786,6 +787,33 @@ public class IssueDaoIT { assertThat(results.stream().toList()).containsExactlyInAnyOrderElementsOf(expectedKeys); } + @Test + public void updateIfBeforeSelectedDate_whenCalledWithBeforeSelectDate_shouldUpdateImpacts() { + prepareTables(); + IssueDto issueDto = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1) + .setSelectedAt(1_440_000_000_000L) + .replaceAllImpacts(List.of(new ImpactDto().setUuid(Uuids.createFast()).setSoftwareQuality(SoftwareQuality.RELIABILITY).setSeverity(Severity.LOW))); + + underTest.updateIfBeforeSelectedDate(db.getSession(), issueDto); + + assertThat(underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1).getImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactly(tuple(SoftwareQuality.RELIABILITY, Severity.LOW)); + + } + + @Test + public void updateIfBeforeSelectedDate_whenCalledWithAfterSelectDate_shouldNotUpdateImpacts() { + prepareTables(); + IssueDto issueDto = underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1) + .setSelectedAt(1_400_000_000_000L) + .replaceAllImpacts(List.of(new ImpactDto().setUuid(Uuids.createFast()).setSoftwareQuality(SoftwareQuality.RELIABILITY).setSeverity(Severity.LOW))); + + underTest.updateIfBeforeSelectedDate(db.getSession(), issueDto); + + assertThat(underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1).getImpacts()).extracting(i -> i.getSoftwareQuality(), i -> i.getSeverity()) + .containsExactlyInAnyOrder(tuple(SoftwareQuality.RELIABILITY, Severity.MEDIUM), tuple(SoftwareQuality.SECURITY, Severity.LOW)); + } + private static IssueDto createIssueWithKey(String issueKey) { return createIssueWithKey(issueKey, PROJECT_UUID, FILE_UUID); } diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueMapperIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueMapperIT.java index 0572e11d34a..06a8d73a67c 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueMapperIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueMapperIT.java @@ -192,7 +192,7 @@ public class IssueMapperIT { .setCodeVariants(Set.of("variant2", "variant3")); // selected after last update -> ok - dto.setSelectedAt(1500000000000L); + dto.setSelectedAt(1_500_000_000_000L); int count = underTest.updateIfBeforeSelectedDate(dto); assertThat(count).isOne(); @@ -227,7 +227,7 @@ public class IssueMapperIT { .setCodeVariants(Set.of("variant2", "variant3")); // selected before last update -> ko - dto.setSelectedAt(1400000000000L); + dto.setSelectedAt(1_400_000_000_000L); int count = underTest.updateIfBeforeSelectedDate(dto); assertThat(count).isZero(); diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeCommandsIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeCommandsIT.java index 937dc4d348b..b89a854e009 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeCommandsIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeCommandsIT.java @@ -582,7 +582,7 @@ public class PurgeCommandsIT { IntStream.range(0, count).forEach(i -> { IssueDto issue = dbTester.issues().insertIssue(t -> t.setRule(rule).setProject(projectOrView).setComponent(projectOrView) .addImpact(new ImpactDto().setUuid(UuidFactoryFast.getInstance().create()) - .setSoftwareQuality(SoftwareQuality.MAINTAINABILITY) + .setSoftwareQuality(SoftwareQuality.SECURITY) .setSeverity(Severity.HIGH))); issueKeys.add("'" + issue.getKey() + "'"); }); diff --git a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java index 9d0e6ccb808..5fa203103b0 100644 --- a/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java +++ b/server/sonar-db-dao/src/it/java/org/sonar/db/purge/PurgeDaoIT.java @@ -1216,7 +1216,7 @@ public class PurgeDaoIT { ComponentDto dir = db.components().insertComponent(newDirectory(mainBranch, "path")); ComponentDto file = db.components().insertComponent(newFileDto(mainBranch, dir)); - IssueDto issue1 = db.issues().insert(rule, mainBranch, file); + IssueDto issue1 = db.issues().insert(rule, mainBranch, file, issue -> issue.replaceAllImpacts(Collections.emptyList())); IssueDto orphanIssue = db.issues().insert(rule, mainBranch, file, issue -> issue.setComponentUuid("nonExisting")); IssueChangeDto orphanIssueChange = db.issues().insertChange(orphanIssue); db.issues().insertNewCodeReferenceIssue(orphanIssue); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index 905c0aa70c9..6efd896f392 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -98,10 +98,10 @@ public class IssueDao implements Dao { private static void updateIssueImpacts(IssueDto issueDto, IssueMapper mapper) { mapper.deleteIssueImpacts(issueDto.getKey()); - insertInsertIssueImpacts(issueDto, mapper); + insertIssueImpact(issueDto, mapper); } - private static void insertInsertIssueImpacts(IssueDto issueDto, IssueMapper mapper) { + private static void insertIssueImpact(IssueDto issueDto, IssueMapper mapper) { issueDto.getImpacts() .forEach(impact -> mapper.insertIssueImpact(issueDto.getKey(), impact)); } @@ -115,6 +115,18 @@ public class IssueDao implements Dao { public void update(DbSession session, IssueDto dto) { mapper(session).update(dto); + updateIssueImpacts(dto, mapper(session)); + } + + public void updateIfBeforeSelectedDate(DbSession session, IssueDto dto) { + int updatedRows = mapper(session).updateIfBeforeSelectedDate(dto); + if (updatedRows != 0) { + updateIssueImpacts(dto, mapper(session)); + } + } + + public List selectByKeysIfNotUpdatedAt(DbSession session, List keys, long updatedAt) { + return mapper(session).selectByKeysIfNotUpdatedAt(keys, updatedAt); } public void insertAsNewCodeOnReferenceBranch(DbSession session, NewCodeReferenceIssueDto dto) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java index af25c60b5ee..8d16aba747d 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDto.java @@ -38,6 +38,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nullable; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; +import org.jetbrains.annotations.NotNull; import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; @@ -45,6 +46,7 @@ import org.sonar.api.rules.CleanCodeAttribute; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; +import org.sonar.core.util.Uuids; import org.sonar.db.component.ComponentDto; import org.sonar.db.protobuf.DbIssues; import org.sonar.db.rule.RuleDto; @@ -154,12 +156,21 @@ public final class IssueDto implements Serializable { .setQuickFixAvailable(issue.isQuickFixAvailable()) .setIsNewCodeReferenceIssue(issue.isNewCodeReferenceIssue()) .setCodeVariants(issue.codeVariants()) - + .replaceAllImpacts(mapToImpactDto(issue.impacts())) // technical dates .setCreatedAt(now) .setUpdatedAt(now); } + @NotNull + private static Set mapToImpactDto(Map impacts) { + return impacts.entrySet().stream().map(e -> new ImpactDto() + .setUuid(Uuids.createFast()) + .setSoftwareQuality(e.getKey()) + .setSeverity(e.getValue())) + .collect(Collectors.toSet()); + } + /** * On server side, we need component keys and uuid */ @@ -202,7 +213,7 @@ public final class IssueDto implements Serializable { .setQuickFixAvailable(issue.isQuickFixAvailable()) .setIsNewCodeReferenceIssue(issue.isNewCodeReferenceIssue()) .setCodeVariants(issue.codeVariants()) - + .replaceAllImpacts(mapToImpactDto(issue.impacts())) // technical date .setUpdatedAt(now); } @@ -865,6 +876,7 @@ public final class IssueDto implements Serializable { issue.setQuickFixAvailable(quickFixAvailable); issue.setIsNewCodeReferenceIssue(isNewCodeReferenceIssue); issue.setCodeVariants(getCodeVariants()); + impacts.forEach(i -> issue.addImpact(i.getSoftwareQuality(), i.getSeverity())); return issue; } } diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java index 201fbff439d..86ff8712eb5 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueTesting.java @@ -25,6 +25,7 @@ import java.util.function.Function; import java.util.stream.Stream; import org.apache.commons.lang.math.RandomUtils; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.resources.Qualifiers; import org.sonar.api.rule.Severity; import org.sonar.core.util.UuidFactoryFast; @@ -68,6 +69,8 @@ public class IssueTesting { .setStatus(Issue.STATUS_OPEN) .setResolution(null) .setSeverity(Severity.ALL.get(nextInt(Severity.ALL.size()))) + //TODO map to correct impact + .addImpact(new ImpactDto().setUuid(Uuids.createFast()).setSoftwareQuality(SoftwareQuality.MAINTAINABILITY).setSeverity(org.sonar.api.issue.impact.Severity.HIGH)) .setEffort((long) RandomUtils.nextInt(10)) .setAssigneeUuid("assignee-uuid_" + randomAlphabetic(26)) .setAuthorLogin("author_" + randomAlphabetic(5)) diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java index 66f510cb5a2..5bb26690d80 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDtoTest.java @@ -83,7 +83,8 @@ public class IssueDtoTest { .setIssueCreationDate(createdAt) .setIssueUpdateDate(updatedAt) .setIssueCloseDate(closedAt) - .setRuleDescriptionContextKey(TEST_CONTEXT_KEY); + .setRuleDescriptionContextKey(TEST_CONTEXT_KEY) + .addImpact(new ImpactDto().setSoftwareQuality(SoftwareQuality.MAINTAINABILITY).setSeverity(Severity.HIGH)); DefaultIssue expected = new DefaultIssue() .setKey("100") @@ -112,7 +113,8 @@ public class IssueDtoTest { .setIsNewCodeReferenceIssue(false) .setRuleDescriptionContextKey(TEST_CONTEXT_KEY) .setCodeVariants(Set.of()) - .setTags(Set.of()); + .setTags(Set.of()) + .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); DefaultIssue issue = dto.toDefaultIssue(); @@ -297,6 +299,8 @@ public class IssueDtoTest { assertThat(issueDto.isQuickFixAvailable()).isTrue(); assertThat(issueDto.isNewCodeReferenceIssue()).isTrue(); assertThat(issueDto.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY); + assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactlyInAnyOrder(tuple(SoftwareQuality.MAINTAINABILITY, Severity.HIGH), tuple(SoftwareQuality.RELIABILITY, Severity.LOW)); } @Test @@ -327,6 +331,8 @@ public class IssueDtoTest { assertThat(issueDto.isQuickFixAvailable()).isTrue(); assertThat(issueDto.isNewCodeReferenceIssue()).isTrue(); assertThat(issueDto.getOptionalRuleDescriptionContextKey()).contains(TEST_CONTEXT_KEY); + assertThat(issueDto.getImpacts()).extracting(ImpactDto::getSoftwareQuality, ImpactDto::getSeverity) + .containsExactlyInAnyOrder(tuple(SoftwareQuality.MAINTAINABILITY, Severity.HIGH), tuple(SoftwareQuality.RELIABILITY, Severity.LOW)); } private DefaultIssue createExampleDefaultIssue(Date dateNow) { @@ -358,7 +364,9 @@ public class IssueDtoTest { .setQuickFixAvailable(true) .setIsNewCodeReferenceIssue(true) .setRuleDescriptionContextKey(TEST_CONTEXT_KEY) - .setCodeVariants(List.of("variant1", "variant2")); + .setCodeVariants(List.of("variant1", "variant2")) + .addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH) + .addImpact(SoftwareQuality.RELIABILITY, Severity.LOW); return defaultIssue; } diff --git a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java index b6d8b7443d9..0aeaa11cadd 100644 --- a/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java +++ b/server/sonar-server-common/src/main/java/org/sonar/server/issue/IssueFieldsSetter.java @@ -23,12 +23,17 @@ import com.google.common.base.Joiner; import java.time.temporal.ChronoUnit; import java.util.Collection; import java.util.Date; +import java.util.EnumMap; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nullable; import org.sonar.api.ce.ComputeEngineSide; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rules.RuleType; import org.sonar.api.server.ServerSide; import org.sonar.api.server.rule.RuleTagFormat; @@ -319,8 +324,8 @@ public class IssueFieldsSetter { for (int i = 0; i < l1c.getMessageFormattingCount(); i++) { if (l1c.getMessageFormatting(i).getStart() != l2.getMessageFormatting(i).getStart() - || l1c.getMessageFormatting(i).getEnd() != l2.getMessageFormatting(i).getEnd() - || l1c.getMessageFormatting(i).getType() != l2.getMessageFormatting(i).getType()) { + || l1c.getMessageFormatting(i).getEnd() != l2.getMessageFormatting(i).getEnd() + || l1c.getMessageFormatting(i).getType() != l2.getMessageFormatting(i).getType()) { return false; } } @@ -333,7 +338,7 @@ public class IssueFieldsSetter { issue.setMessage(previousMessage); issue.setMessageFormattings(previousMessageFormattings); boolean changed = setMessage(issue, currentMessage, context); - return setMessageFormattings(issue, currentMessageFormattings, context) || changed; + return setMessageFormattings(issue, currentMessageFormattings, context) || changed; } public void addComment(DefaultIssue issue, String text, IssueChangeContext context) { @@ -426,6 +431,17 @@ public class IssueFieldsSetter { return false; } + public boolean setImpacts(DefaultIssue issue, Map previousImpacts, IssueChangeContext context) { + Map currentImpacts = new EnumMap<>(issue.impacts()); + if (!previousImpacts.equals(currentImpacts)) { + issue.replaceImpacts(currentImpacts); + issue.setUpdateDate(context.date()); + issue.setChanged(true); + return true; + } + return false; + } + private static Set getNewCodeVariants(DefaultIssue issue) { Set issueCodeVariants = issue.codeVariants(); if (issueCodeVariants == null) { diff --git a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java index 362c7844d67..3003da52cea 100644 --- a/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java +++ b/server/sonar-server-common/src/test/java/org/sonar/server/issue/IssueFieldsSetterTest.java @@ -24,10 +24,13 @@ import java.util.Calendar; import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.Set; import org.apache.commons.lang.time.DateUtils; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rules.RuleType; import org.sonar.api.utils.Duration; import org.sonar.core.issue.DefaultIssue; @@ -529,6 +532,17 @@ public class IssueFieldsSetterTest { assertThat(issue.mustSendNotifications()).isTrue(); } + @Test + public void setImpacts_whenImpactAdded_shouldBeUpdated() { + Map currentImpacts = Map.of(SoftwareQuality.RELIABILITY, Severity.LOW); + Map newImpacts = Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); + + issue.replaceImpacts(newImpacts); + boolean updated = underTest.setImpacts(issue, currentImpacts, context); + assertThat(updated).isTrue(); + assertThat(issue.impacts()).isEqualTo(newImpacts); + } + @Test public void setCodeVariants_whenCodeVariantsUnchanged_shouldNotBeUpdated() { Set currentCodeVariants = new HashSet<>(Arrays.asList("linux", "windows")); 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 index b26765e4c71..581def46912 100644 --- a/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java +++ b/sonar-core/src/main/java/org/sonar/core/issue/DefaultIssue.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; +import java.util.EnumMap; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -133,6 +134,8 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. private String anticipatedTransitionUuid = null; + private Map impacts = new EnumMap<>(SoftwareQuality.class); + @Override public String key() { return key; @@ -150,7 +153,18 @@ public class DefaultIssue implements Issue, Trackable, org.sonar.api.ce.measure. @Override public Map impacts() { - return Collections.emptyMap(); + return impacts; + } + + public DefaultIssue replaceImpacts(Map impacts) { + this.impacts.clear(); + this.impacts.putAll(impacts); + return this; + } + + public DefaultIssue addImpact(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity) { + impacts.put(softwareQuality, severity); + return this; } public DefaultIssue setType(RuleType type) { diff --git a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java index 9d1309c7850..a09c91c03dc 100644 --- a/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java +++ b/sonar-core/src/test/java/org/sonar/core/issue/DefaultIssueTest.java @@ -20,8 +20,11 @@ package org.sonar.core.issue; import java.util.List; +import java.util.Map; import org.apache.commons.lang.StringUtils; import org.junit.Test; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.utils.Duration; import static org.assertj.core.api.Assertions.assertThat; @@ -288,5 +291,23 @@ public class DefaultIssueTest { DefaultIssue defaultIssue = new DefaultIssue(); defaultIssue.setAnticipatedTransitionUuid("uuid"); assertThat(defaultIssue.getAnticipatedTransitionUuid()).isPresent(); + + } + + public void getImpacts_whenAddingNewImpacts_shouldReturnListOfImpacts() { + issue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); + issue.addImpact(SoftwareQuality.RELIABILITY, Severity.LOW); + + assertThat(issue.impacts()).containsExactlyEntriesOf(Map.of(SoftwareQuality.MAINTAINABILITY, Severity.HIGH, SoftwareQuality.RELIABILITY, Severity.LOW)); + } + + @Test + public void replaceImpacts_shouldreplaceExistingImpacts() { + issue.addImpact(SoftwareQuality.MAINTAINABILITY, Severity.HIGH); + issue.addImpact(SoftwareQuality.RELIABILITY, Severity.LOW); + + issue.replaceImpacts(Map.of(SoftwareQuality.SECURITY, Severity.LOW)); + + assertThat(issue.impacts()).containsExactlyEntriesOf(Map.of(SoftwareQuality.SECURITY, Severity.LOW)); } } -- 2.39.5