]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21455 Live update of impact measures
authorEric Giffon <eric.giffon@sonarsource.com>
Wed, 24 Jan 2024 13:01:34 +0000 (14:01 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 31 Jan 2024 20:03:36 +0000 (20:03 +0000)
server/sonar-db-dao/src/it/java/org/sonar/db/issue/IssueDaoIT.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueImpactGroupDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueCounter.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java

index 7ae2f4eb5faf4c6ba0be35e42376dbec56f27749..f32cd93dca0fde62829c99a42692eed119c3eb45 100644 (file)
@@ -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;
@@ -565,6 +566,44 @@ public class IssueDaoIT {
     assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isOne();
   }
 
+  @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)
index 8d3a9454bbdb29e0f2653d26b235cf394f52cbab..d4708ab2fbda4f0334ec22232726a1b7dba1e2a1 100644 (file)
@@ -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 (file)
index 0000000..28e8a84
--- /dev/null
@@ -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;
+  }
+}
index c2cec03fd74191a6d52e75d13687dd91ede2bece..21f43b1773f3b2e4f5c283175ec48b5062039035 100644 (file)
@@ -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);
index 7a1773cb4d31030ad77052e07c383b23f96b959a..ae88f48bff89207c5a39c69dd7b2dd96440718b8 100644 (file)
     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
index a4fc50a9b7f53874a7f566e9eb9d12efbe93790a..f3343ccacfe9427cd8f243d65fa5d9e8491b7842 100644 (file)
@@ -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());
index aca5d92de179f4d3691d85100a9eef978385cdce..4ddf4b428299469da187cdc1802676f0134b3686 100644 (file)
@@ -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);
+    }
   }
 }
index 68099ab5bb99ee73d02274f6f82e56b23a9361af..e00d5c2948be7e02c0e0df081b097eecf9367ed9 100644 (file)
@@ -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);
   }
 }
index 06954a8bd8470b640e2856fb4e0e4ebd0a2940c9..df159b8fcb16800e07a532da4afb9d8f5f5b4de3 100644 (file)
  */
 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;
index 9ca8ddc1774a02dbeaac4e6daa21daae132602e2..7eb37a04a37109754b7e05c6eb30d0a781179cd0 100644 (file)
@@ -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);
@@ -1045,6 +1119,11 @@ public class MeasureUpdateFormulaFactoryImplTest {
       return initialValues.childrenValues;
     }
 
+    @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;