aboutsummaryrefslogtreecommitdiffstats
path: root/server
diff options
context:
space:
mode:
authorEric Giffon <eric.giffon@sonarsource.com>2024-01-24 14:01:34 +0100
committersonartech <sonartech@sonarsource.com>2024-01-31 20:03:36 +0000
commit6381e5a67c6363e66d1ef7b4d6d5ed6919894d87 (patch)
tree4b33ed7517cbc95e7d94c6ce92de8db6c4c8292b /server
parent552a7239202d6111e2957d75e6674a3277348d38 (diff)
downloadsonarqube-6381e5a67c6363e66d1ef7b4d6d5ed6919894d87.tar.gz
sonarqube-6381e5a67c6363e66d1ef7b4d6d5ed6919894d87.zip
SONAR-21455 Live update of impact measures
Diffstat (limited to 'server')
-rw-r--r--server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java39
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java4
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueImpactGroupDto.java58
-rw-r--r--server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java2
-rw-r--r--server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml14
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java50
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java18
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java4
-rw-r--r--server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java42
-rw-r--r--server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java123
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;