diff options
author | Eric Giffon <eric.giffon@sonarsource.com> | 2024-01-24 14:01:34 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2024-01-31 20:03:36 +0000 |
commit | 6381e5a67c6363e66d1ef7b4d6d5ed6919894d87 (patch) | |
tree | 4b33ed7517cbc95e7d94c6ce92de8db6c4c8292b /server | |
parent | 552a7239202d6111e2957d75e6674a3277348d38 (diff) | |
download | sonarqube-6381e5a67c6363e66d1ef7b4d6d5ed6919894d87.tar.gz sonarqube-6381e5a67c6363e66d1ef7b4d6d5ed6919894d87.zip |
SONAR-21455 Live update of impact measures
Diffstat (limited to 'server')
10 files changed, 340 insertions, 14 deletions
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 7ae2f4eb5fa..f32cd93dca0 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 @@ -34,6 +34,7 @@ import org.jetbrains.annotations.NotNull; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.sonar.api.issue.Issue; import org.sonar.api.issue.impact.Severity; import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rule.RuleKey; @@ -566,6 +567,44 @@ public class IssueDaoIT { } @Test + public void selectIssueImpactGroupsByComponent_shouldReturnImpactGroups() { + ComponentDto project = db.components().insertPublicProject().getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project)); + RuleDto rule = db.rules().insert(); + db.issues().insert(rule, project, file, + i -> i.replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, LOW)))); + db.issues().insert(rule, project, file, + i -> i.replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, HIGH)))); + db.issues().insert(rule, project, file, + i -> i.replaceAllImpacts(List.of(createImpact(SECURITY, HIGH)))); + // closed issues are ignored + db.issues().insert(rule, project, file, + i -> i.setStatus(Issue.STATUS_CLOSED).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH)))); + + Collection<IssueImpactGroupDto> result = underTest.selectIssueImpactGroupsByComponent(db.getSession(), file); + + assertThat(result).hasSize(3); + assertThat(result.stream().mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(5); + + assertThat(result.stream().filter(g -> MAINTAINABILITY == g.getSoftwareQuality()).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(2); + assertThat(result.stream().filter(g -> SECURITY == g.getSoftwareQuality()).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(3); + assertThat(result.stream().filter(g -> SECURITY == g.getSoftwareQuality() && HIGH == g.getSeverity()).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(3); + assertThat(result.stream().filter(g -> HIGH == g.getSeverity()).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(4); + assertThat(result.stream().filter(g -> LOW == g.getSeverity()).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(1); + assertThat(result.stream().noneMatch(g -> RELIABILITY == g.getSoftwareQuality())).isTrue(); + } + + @Test + public void selectIssueImpactGroupsByComponent_whenComponentWithNoIssues_shouldReturnEmpty() { + ComponentDto project = db.components().insertPublicProject().getMainBranchComponent(); + ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project)); + + Collection<IssueImpactGroupDto> groups = underTest.selectIssueImpactGroupsByComponent(db.getSession(), file); + + assertThat(groups).isEmpty(); + } + + @Test public void selectByKey_givenOneIssueNewOnReferenceBranch_selectOneIssueWithNewOnReferenceBranch() { underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1) .setMessage("the message") 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 8d3a9454bbd..d4708ab2fbd 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 @@ -90,6 +90,10 @@ public class IssueDao implements Dao { return mapper(dbSession).selectIssueGroupsByComponent(component, leakPeriodBeginningDate); } + public Collection<IssueImpactGroupDto> selectIssueImpactGroupsByComponent(DbSession dbSession, ComponentDto component) { + return mapper(dbSession).selectIssueImpactGroupsByComponent(component); + } + public Cursor<IndexedIssueDto> scrollIssuesForIndexation(DbSession dbSession, @Nullable @Param("branchUuid") String branchUuid, @Nullable @Param("issueKeys") Collection<String> issueKeys) { return mapper(dbSession).scrollIssuesForIndexation(branchUuid, issueKeys); diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueImpactGroupDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueImpactGroupDto.java new file mode 100644 index 00000000000..28e8a8451c6 --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueImpactGroupDto.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2024 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.issue; + +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; + +public class IssueImpactGroupDto { + + private SoftwareQuality softwareQuality; + private Severity severity; + private long count; + + public IssueImpactGroupDto() { + // nothing to do + } + + public SoftwareQuality getSoftwareQuality() { + return softwareQuality; + } + + public void setSoftwareQuality(SoftwareQuality softwareQuality) { + this.softwareQuality = softwareQuality; + } + + public Severity getSeverity() { + return severity; + } + + public void setSeverity(Severity severity) { + this.severity = severity; + } + + public long getCount() { + return count; + } + + public void setCount(long count) { + this.count = count; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index c2cec03fd74..21f43b1773f 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -74,6 +74,8 @@ public interface IssueMapper { Collection<IssueGroupDto> selectIssueGroupsByComponent(@Param("component") ComponentDto component, @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate); + Collection<IssueImpactGroupDto> selectIssueImpactGroupsByComponent(@Param("component") ComponentDto component); + List<IssueDto> selectByBranch(@Param("keys") Set<String> keys, @Nullable @Param("changedSince") Long changedSince); List<String> selectRecentlyClosedIssues(@Param("queryParams") IssueQueryParams issueQueryParams); diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index 7a1773cb4d3..ae88f48bff8 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -535,6 +535,20 @@ group by i2.issue_type, i2.severity, i2.hasHighImpactSeverity, i2.resolution, i2.status, i2.inLeak </sql> + <select id="selectIssueImpactGroupsByComponent" resultType="org.sonar.db.issue.IssueImpactGroupDto" parameterType="map"> + select + ii.software_quality as softwareQuality, + ii.severity as severity, + count(i.kee) as "count" + from issues i + left join issues_impacts ii on i.kee = ii.issue_key + where 1=1 + and i.status in ('OPEN', 'REOPENED', 'CONFIRMED') + and i.issue_type != 4 + and i.component_uuid = #{component.uuid,jdbcType=VARCHAR} + group by ii.software_quality, ii.severity + </select> + <select id="selectIssueKeysByComponentUuid" parameterType="string" resultType="string"> select i.kee diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java index a4fc50a9b7f..f3343ccacfe 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java @@ -19,6 +19,8 @@ */ package org.sonar.server.measure.live; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.util.Collection; import java.util.EnumMap; import java.util.HashMap; @@ -26,11 +28,14 @@ import java.util.Map; import java.util.Optional; import javax.annotation.Nullable; import org.sonar.api.issue.IssueStatus; -import org.sonar.api.rule.Severity; +import org.sonar.api.issue.impact.Severity; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.rules.RuleType; import org.sonar.db.issue.IssueGroupDto; +import org.sonar.db.issue.IssueImpactGroupDto; import org.sonar.db.rule.SeverityUtil; +import static org.sonar.api.rule.Severity.INFO; import static org.sonar.api.rules.RuleType.SECURITY_HOTSPOT; class IssueCounter { @@ -44,8 +49,10 @@ class IssueCounter { private final Map<String, Count> hotspotsByStatus = new HashMap<>(); private final Count unresolved = new Count(); private final Count highImpactAccepted = new Count(); + private final Map<SoftwareQuality, Map<Severity, Count>> bySoftwareQualityAndSeverity = new EnumMap<>(SoftwareQuality.class); + private final Gson gson = new GsonBuilder().create(); - IssueCounter(Collection<IssueGroupDto> groups) { + IssueCounter(Collection<IssueGroupDto> groups, Collection<IssueImpactGroupDto> impactGroups) { for (IssueGroupDto group : groups) { if (RuleType.valueOf(group.getRuleType()).equals(SECURITY_HOTSPOT)) { processHotspotGroup(group); @@ -53,6 +60,9 @@ class IssueCounter { processGroup(group); } } + for (IssueImpactGroupDto group : impactGroups) { + processImpactGroup(group); + } } private void processHotspotGroup(IssueGroupDto group) { @@ -99,6 +109,15 @@ class IssueCounter { } } + private void processImpactGroup(IssueImpactGroupDto group) { + if (group.getSoftwareQuality() != null && group.getSeverity() != null) { + bySoftwareQualityAndSeverity + .computeIfAbsent(group.getSoftwareQuality(), k -> new EnumMap<>(Severity.class)) + .computeIfAbsent(group.getSeverity(), k -> new Count()) + .add(group); + } + } + public Optional<String> getHighestSeverityOfUnresolved(RuleType ruleType, boolean onlyInLeak) { return Optional.ofNullable(highestSeverityOfUnresolved.get(ruleType)) .map(hs -> hs.severity(onlyInLeak)); @@ -147,6 +166,25 @@ class IssueCounter { return onlyInLeak ? count.leak : count.absolute; } + public String getBySoftwareQuality(SoftwareQuality softwareQuality) { + Map<Severity, Count> severityToCount = bySoftwareQualityAndSeverity.get(softwareQuality); + + Map<String, Long> impactMap = new HashMap<>(); + if (severityToCount != null) { + impactMap.put("total", severityToCount.values().stream().mapToLong(count -> count.absolute).sum()); + for (Severity severity : Severity.values()) { + impactMap.put(severity.name(), Optional.ofNullable(severityToCount.get(severity)).map(count -> count.absolute).orElse(0L)); + } + } else { + impactMap.put("total", 0L); + for (Severity severity : Severity.values()) { + impactMap.put(severity.name(), 0L); + } + } + + return gson.toJson(impactMap); + } + private static class Count { private long absolute = 0L; private long leak = 0L; @@ -157,6 +195,10 @@ class IssueCounter { leak += group.getCount(); } } + + public void add(IssueImpactGroupDto group) { + absolute += group.getCount(); + } } private static class Effort { @@ -172,8 +214,8 @@ class IssueCounter { } private static class HighestSeverity { - private int absolute = SeverityUtil.getOrdinalFromSeverity(Severity.INFO); - private int leak = SeverityUtil.getOrdinalFromSeverity(Severity.INFO); + private int absolute = SeverityUtil.getOrdinalFromSeverity(INFO); + private int leak = SeverityUtil.getOrdinalFromSeverity(INFO); void add(IssueGroupDto group) { int severity = SeverityUtil.getOrdinalFromSeverity(group.getSeverity()); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java index aca5d92de17..4ddf4b42829 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java @@ -88,7 +88,8 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { FormulaContextImpl context = new FormulaContextImpl(matrix, components, debtRatingGrid); components.getSortedTree().forEach(c -> { - IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByComponent(dbSession, c, beginningOfLeak)); + IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByComponent(dbSession, c, beginningOfLeak), + dbClient.issueDao().selectIssueImpactGroupsByComponent(dbSession, c)); for (MeasureUpdateFormula formula : formulaFactory.getFormulas()) { // use formulas when the leak period is defined, it's a PR, or the formula is not about the leak period if (useLeakFormulas || !formula.isOnLeak()) { @@ -149,6 +150,15 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { .toList(); } + public List<String> getChildrenTextValues() { + List<ComponentDto> children = componentIndex.getChildren(currentComponent); + return children.stream() + .flatMap(c -> matrix.getMeasure(c, currentFormula.getMetric().getKey()).stream()) + .map(LiveMeasureDto::getTextValue) + .filter(Objects::nonNull) + .toList(); + } + /** * Some child components may not have the measures 'SECURITY_HOTSPOTS_TO_REVIEW_STATUS' and 'SECURITY_HOTSPOTS_REVIEWED_STATUS' saved for them, * so we may need to calculate them based on 'SECURITY_HOTSPOTS_REVIEWED' and 'SECURITY_HOTSPOTS'. @@ -243,5 +253,11 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater { String metricKey = currentFormula.getMetric().getKey(); matrix.setValue(currentComponent, metricKey, value); } + + @Override + public void setValue(String value) { + String metricKey = currentFormula.getMetric().getKey(); + matrix.setValue(currentComponent, metricKey, value); + } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java index 68099ab5bb9..e00d5c2948b 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java @@ -79,6 +79,8 @@ class MeasureUpdateFormula { interface Context { List<Double> getChildrenValues(); + List<String> getChildrenTextValues(); + long getChildrenHotspotsReviewed(); long getChildrenHotspotsToReview(); @@ -104,5 +106,7 @@ class MeasureUpdateFormula { void setValue(double value); void setValue(Rating value); + + void setValue(String value); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java index 06954a8bd84..df159b8fcb1 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java @@ -19,12 +19,20 @@ */ package org.sonar.server.measure.live; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.OptionalInt; import java.util.Set; import java.util.function.BiConsumer; +import java.util.stream.Collectors; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric; import org.sonar.api.rule.Severity; @@ -57,6 +65,15 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS, false, new AddChildren(), (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, false))), + new MeasureUpdateFormula(CoreMetrics.RELIABILITY_ISSUES, false, new ImpactAddChildren(), + (context, issues) -> context.setValue(issues.getBySoftwareQuality(SoftwareQuality.RELIABILITY))), + + new MeasureUpdateFormula(CoreMetrics.MAINTAINABILITY_ISSUES, false, new ImpactAddChildren(), + (context, issues) -> context.setValue(issues.getBySoftwareQuality(SoftwareQuality.MAINTAINABILITY))), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_ISSUES, false, new ImpactAddChildren(), + (context, issues) -> context.setValue(issues.getBySoftwareQuality(SoftwareQuality.SECURITY))), + new MeasureUpdateFormula(CoreMetrics.VIOLATIONS, false, new AddChildren(), (context, issues) -> context.setValue(issues.countUnresolved(false))), @@ -238,6 +255,9 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact private static final Set<Metric> FORMULA_METRICS = MeasureUpdateFormulaFactory.extractMetrics(FORMULAS); + private static final Gson GSON = new GsonBuilder().create(); + private static final Type MAP_TYPE = new TypeToken<Map<String, Long>>() {}.getType(); + private static double debtDensity(MeasureUpdateFormula.Context context) { double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0D), 0.0D); Optional<Double> devCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble); @@ -282,6 +302,28 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact } } + private static class ImpactAddChildren implements BiConsumer<MeasureUpdateFormula.Context, MeasureUpdateFormula> { + @Override + public void accept(MeasureUpdateFormula.Context context, MeasureUpdateFormula formula) { + List<Map<String, Long>> measures = context.getChildrenTextValues().stream() + .map(ImpactAddChildren::toMap) + .collect(Collectors.toList()); + context.getText(formula.getMetric()).ifPresent(value -> measures.add(toMap(value))); + + Map<String, Long> newValue = new HashMap<>(); + newValue.put("total", measures.stream().mapToLong(map -> map.get("total")).sum()); + for (org.sonar.api.issue.impact.Severity severity : org.sonar.api.issue.impact.Severity.values()) { + newValue.put(severity.name(), measures.stream().mapToLong(map -> map.get(severity.name())).sum()); + } + + context.setValue(GSON.toJson(newValue)); + } + + private static Map<String, Long> toMap(String value) { + return GSON.fromJson(value, MAP_TYPE); + } + } + @Override public List<MeasureUpdateFormula> getFormulas() { return FORMULAS; diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java index 9ca8ddc1774..7eb37a04a37 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java @@ -19,6 +19,8 @@ */ package org.sonar.server.measure.live; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -31,17 +33,25 @@ import java.util.Set; import javax.annotation.Nullable; import org.junit.Test; import org.sonar.api.issue.Issue; +import org.sonar.api.issue.impact.SoftwareQuality; import org.sonar.api.measures.CoreMetrics; import org.sonar.api.measures.Metric; import org.sonar.api.rule.Severity; import org.sonar.api.rules.RuleType; import org.sonar.db.component.ComponentDto; import org.sonar.db.issue.IssueGroupDto; +import org.sonar.db.issue.IssueImpactGroupDto; import org.sonar.server.measure.DebtRatingGrid; import org.sonar.server.measure.Rating; import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.issue.impact.Severity.HIGH; +import static org.sonar.api.issue.impact.Severity.LOW; +import static org.sonar.api.issue.impact.Severity.MEDIUM; +import static org.sonar.api.issue.impact.SoftwareQuality.MAINTAINABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.RELIABILITY; +import static org.sonar.api.issue.impact.SoftwareQuality.SECURITY; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS; import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS; @@ -50,9 +60,11 @@ import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED; import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS; import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS; import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING; +import static org.sonar.test.JsonAssert.assertJson; public class MeasureUpdateFormulaFactoryImplTest { + public static final Gson GSON = new GsonBuilder().create(); private final MeasureUpdateFormulaFactoryImpl underTest = new MeasureUpdateFormulaFactoryImpl(); @Test @@ -919,10 +931,52 @@ public class MeasureUpdateFormulaFactoryImplTest { .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A); } + @Test + public void computeHierarchy_shouldComputeImpactMeasures() { + new HierarchyTester(CoreMetrics.RELIABILITY_ISSUES) + .withValue(impactMeasureToJson(6, 1, 2, 3)) + .withChildrenValues(impactMeasureToJson(6, 1, 2, 3), impactMeasureToJson(10, 5, 3, 2)) + .expectedJsonResult(impactMeasureToJson(22, 7, 7, 8)); + + new HierarchyTester(CoreMetrics.RELIABILITY_ISSUES) + .withValue(impactMeasureToJson(6, 1, 2, 3)) + .expectedJsonResult(impactMeasureToJson(6, 1, 2, 3)); + } + + @Test + public void compute_shouldComputeImpactMeasures() { + with( + newImpactGroup(RELIABILITY, HIGH, 3), + newImpactGroup(RELIABILITY, MEDIUM, 4), + newImpactGroup(RELIABILITY, LOW, 1), + newImpactGroup(MAINTAINABILITY, MEDIUM, 10), + newImpactGroup(MAINTAINABILITY, LOW, 11), + newImpactGroup(SECURITY, HIGH, 3)) + .assertThatJsonValueIs(CoreMetrics.RELIABILITY_ISSUES, impactMeasureToJson(8, 3, 4, 1)) + .assertThatJsonValueIs(CoreMetrics.MAINTAINABILITY_ISSUES, impactMeasureToJson(21, 0, 10, 11)) + .assertThatJsonValueIs(CoreMetrics.SECURITY_ISSUES, impactMeasureToJson(3, 3, 0, 0)); + } + + @Test + public void compute_whenNoIssues_shouldComputeImpactMeasures() { + withNoIssues() + .assertThatJsonValueIs(CoreMetrics.RELIABILITY_ISSUES, impactMeasureToJson(0, 0, 0, 0)) + .assertThatJsonValueIs(CoreMetrics.MAINTAINABILITY_ISSUES, impactMeasureToJson(0, 0, 0, 0)) + .assertThatJsonValueIs(CoreMetrics.SECURITY_ISSUES, impactMeasureToJson(0, 0, 0, 0)); + } + + private static String impactMeasureToJson(long total, long high, long medium, long low) { + return GSON.toJson(Map.of("total", total, "HIGH", high, "MEDIUM", medium, "LOW", low)); + } + private Verifier with(IssueGroupDto... groups) { return new Verifier(groups); } + private Verifier with(IssueImpactGroupDto... groups) { + return new Verifier(groups); + } + private Verifier withNoIssues() { return new Verifier(new IssueGroupDto[0]); } @@ -936,20 +990,25 @@ public class MeasureUpdateFormulaFactoryImplTest { } private class Verifier { - private final IssueGroupDto[] groups; + private IssueGroupDto[] groups = {}; + private IssueImpactGroupDto[] impactGroups = {}; private final InitialValues initialValues = new InitialValues(); private Verifier(IssueGroupDto[] groups) { this.groups = groups; } + private Verifier(IssueImpactGroupDto[] impactGroups) { + this.impactGroups = impactGroups; + } + Verifier and(Metric metric, double value) { this.initialValues.values.put(metric, value); return this; } Verifier andText(Metric metric, String value) { - this.initialValues.text.put(metric, value); + this.initialValues.textValues.put(metric, value); return this; } @@ -959,6 +1018,12 @@ public class MeasureUpdateFormulaFactoryImplTest { return this; } + Verifier assertThatJsonValueIs(Metric metric, String expectedValue) { + TestContext context = run(metric, false); + assertJson(context.stringValue).isSimilarTo(expectedValue); + return this; + } + Verifier assertThatLeakValueIs(Metric metric, double expectedValue) { TestContext context = run(metric, true); assertThat(context.doubleValue).isNotNull().isEqualTo(expectedValue); @@ -996,13 +1061,13 @@ public class MeasureUpdateFormulaFactoryImplTest { .get(); assertThat(formula.isOnLeak()).isEqualTo(expectLeakFormula); TestContext context = new TestContext(formula.getDependentMetrics(), initialValues); - formula.compute(context, newIssueCounter(groups)); + formula.compute(context, newIssueCounter(groups, impactGroups)); return context; } } - private static IssueCounter newIssueCounter(IssueGroupDto... issues) { - return new IssueCounter(asList(issues)); + private static IssueCounter newIssueCounter(IssueGroupDto[] groups, IssueImpactGroupDto[] impactGroups) { + return new IssueCounter(asList(groups), asList(impactGroups)); } private static IssueGroupDto newGroup() { @@ -1021,6 +1086,14 @@ public class MeasureUpdateFormulaFactoryImplTest { return dto; } + private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, long count) { + IssueImpactGroupDto dto = new IssueImpactGroupDto(); + dto.setSoftwareQuality(softwareQuality); + dto.setSeverity(severity); + dto.setCount(count); + return dto; + } + private static IssueGroupDto newResolvedGroup(RuleType ruleType) { return newGroup(ruleType).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setStatus(Issue.STATUS_CLOSED); } @@ -1034,6 +1107,7 @@ public class MeasureUpdateFormulaFactoryImplTest { private final InitialValues initialValues; private Double doubleValue; private Rating ratingValue; + private String stringValue; private TestContext(Collection<Metric> dependentMetrics, InitialValues initialValues) { this.dependentMetrics = new HashSet<>(dependentMetrics); @@ -1046,6 +1120,11 @@ public class MeasureUpdateFormulaFactoryImplTest { } @Override + public List<String> getChildrenTextValues() { + return initialValues.childrenTextValues; + } + + @Override public long getChildrenHotspotsReviewed() { return initialValues.childrenHotspotsReviewed; } @@ -1088,8 +1167,8 @@ public class MeasureUpdateFormulaFactoryImplTest { @Override public Optional<String> getText(Metric metric) { - if (initialValues.text.containsKey(metric)) { - return Optional.of(initialValues.text.get(metric)); + if (initialValues.textValues.containsKey(metric)) { + return Optional.of(initialValues.textValues.get(metric)); } return Optional.empty(); } @@ -1103,12 +1182,18 @@ public class MeasureUpdateFormulaFactoryImplTest { public void setValue(Rating value) { this.ratingValue = value; } + + @Override + public void setValue(String value) { + this.stringValue = value; + } } private class InitialValues { private final Map<Metric, Double> values = new HashMap<>(); private final List<Double> childrenValues = new ArrayList<>(); - private final Map<Metric, String> text = new HashMap<>(); + private final Map<Metric, String> textValues = new HashMap<>(); + private final List<String> childrenTextValues = new ArrayList<>(); private long childrenHotspotsReviewed = 0; private long childrenNewHotspotsReviewed = 0; private long childrenHotspotsToReview = 0; @@ -1132,6 +1217,11 @@ public class MeasureUpdateFormulaFactoryImplTest { return this; } + public HierarchyTester withValue(Metric metric, String value) { + this.initialValues.textValues.put(metric, value); + return this; + } + public HierarchyTester withChildrenHotspotsCounts(long childrenHotspotsReviewed, long childrenNewHotspotsReviewed, long childrenHotspotsToReview, long childrenNewHotspotsToReview) { this.initialValues.childrenHotspotsReviewed = childrenHotspotsReviewed; @@ -1145,17 +1235,32 @@ public class MeasureUpdateFormulaFactoryImplTest { return withValue(metric, value); } + public HierarchyTester withValue(String value) { + return withValue(metric, value); + } + public HierarchyTester withChildrenValues(Double... values) { this.initialValues.childrenValues.addAll(asList(values)); return this; } + public HierarchyTester withChildrenValues(String... values) { + this.initialValues.childrenTextValues.addAll(asList(values)); + return this; + } + public HierarchyTester expectedResult(@Nullable Double expected) { TestContext ctx = run(); assertThat(ctx.doubleValue).isEqualTo(expected); return this; } + public HierarchyTester expectedJsonResult(@Nullable String expected) { + TestContext ctx = run(); + assertJson(ctx.stringValue).isSimilarTo(expected); + return this; + } + public HierarchyTester expectedRating(@Nullable Rating rating) { TestContext ctx = run(); assertThat(ctx.ratingValue).isEqualTo(rating); @@ -1166,7 +1271,7 @@ public class MeasureUpdateFormulaFactoryImplTest { List<Metric> deps = new LinkedList<>(formula.getDependentMetrics()); deps.add(formula.getMetric()); deps.addAll(initialValues.values.keySet()); - deps.addAll(initialValues.text.keySet()); + deps.addAll(initialValues.textValues.keySet()); TestContext context = new TestContext(deps, initialValues); formula.computeHierarchy(context); return context; |