]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22727 Manage new metrics in the live update
authorLéo Geoffroy <leo.geoffroy@sonarsource.com>
Mon, 12 Aug 2024 13:20:00 +0000 (15:20 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:06 +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/IssueImpactGroupDto.java
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-webserver-webapi/build.gradle
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/MeasureUpdateFormulaFactoryImpl.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java

index 29c6932e53f032e8981b6f1c9c020d0b07cc1c0d..6d6a2e9bd2018065e6e329436b0274a14a75db13 100644 (file)
@@ -223,8 +223,8 @@ class IssueDaoIT {
     ComponentDto project = db.components().insertPrivateProject().getMainBranchComponent();
     RuleDto rule = db.rules().insert(r -> r.setRepositoryKey("java").setLanguage("java")
       .replaceAllDefaultImpacts(List.of(new ImpactDto()
-          .setSoftwareQuality(MAINTAINABILITY)
-          .setSeverity(MEDIUM),
+        .setSoftwareQuality(MAINTAINABILITY)
+        .setSeverity(MEDIUM),
         new ImpactDto()
           .setSoftwareQuality(RELIABILITY)
           .setSeverity(LOW))));
@@ -343,7 +343,7 @@ class IssueDaoIT {
     IntStream.range(0, statusesB.size()).forEach(i -> insertBranchIssue(branchB, fileB, rule, "B" + i, statusesB.get(i), updatedAt));
 
     List<IssueDto> branchAIssuesA1 = underTest.selectByBranch(db.getSession(), Set.of("issueA0", "issueA1", "issueA3",
-        "issueWithResolution"),
+      "issueWithResolution"),
       buildSelectByBranchQuery(branchA, false, changedSince));
 
     assertThat(branchAIssuesA1)
@@ -565,25 +565,25 @@ class IssueDaoIT {
   }
 
   @ParameterizedTest
-  @ValueSource(booleans =  {true, false})
+  @ValueSource(booleans = {true, false})
   void selectIssueImpactGroupsByComponent_shouldReturnImpactGroups(boolean inLeak) {
 
     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.setStatus(STATUS_OPEN).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, LOW))));
+      i -> i.setStatus(STATUS_OPEN).setEffort(60L).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, LOW))));
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_OPEN).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, HIGH))));
+      i -> i.setStatus(STATUS_OPEN).setEffort(60L).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, HIGH))));
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_REOPENED).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
+      i -> i.setStatus(STATUS_REOPENED).setEffort(60L).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_RESOLVED).setResolution(RESOLUTION_WONT_FIX).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
+      i -> i.setStatus(STATUS_RESOLVED).setEffort(60L).setResolution(RESOLUTION_WONT_FIX).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
     // issues in ignored status
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(Issue.STATUS_CLOSED).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
+      i -> i.setStatus(Issue.STATUS_CLOSED).setEffort(60L).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_RESOLVED).setResolution(RESOLUTION_FALSE_POSITIVE).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
+      i -> i.setStatus(STATUS_RESOLVED).setEffort(60L).setResolution(RESOLUTION_FALSE_POSITIVE).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
 
     Collection<IssueImpactGroupDto> result = underTest.selectIssueImpactGroupsByComponent(db.getSession(), file, inLeak ? 1L : Long.MAX_VALUE);
 
@@ -594,6 +594,9 @@ class IssueDaoIT {
     assertThat(result.stream().filter(g -> g.getSoftwareQuality() == SECURITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(4);
     assertThat(result.stream().filter(g -> g.getSoftwareQuality() == MAINTAINABILITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(2);
 
+    assertThat(result.stream().filter(g -> g.getSoftwareQuality() == SECURITY).mapToDouble(IssueImpactGroupDto::getEffort).sum()).isEqualTo(4 * 60);
+    assertThat(result.stream().filter(g -> g.getSoftwareQuality() == MAINTAINABILITY).mapToDouble(IssueImpactGroupDto::getEffort).sum()).isEqualTo(2 * 60);
+
     assertThat(result.stream().filter(g -> g.getSeverity() == HIGH).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(5);
     assertThat(result.stream().filter(g -> g.getSeverity() == LOW).mapToLong(IssueImpactGroupDto::getCount).sum()).isOne();
 
@@ -618,9 +621,9 @@ class IssueDaoIT {
     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
     RuleDto rule = db.rules().insert();
     IssueDto issueInNewCodePeriod = db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_OPEN).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, LOW))));
+      i -> i.setStatus(STATUS_OPEN).setEffort(1L).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH), createImpact(MAINTAINABILITY, LOW))));
     db.issues().insert(rule, project, file,
-      i -> i.setStatus(STATUS_RESOLVED).setResolution(RESOLUTION_WONT_FIX).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
+      i -> i.setStatus(STATUS_RESOLVED).setEffort(2L).setResolution(RESOLUTION_WONT_FIX).replaceAllImpacts(List.of(createImpact(SECURITY, HIGH))));
 
     db.issues().insertNewCodeReferenceIssue(issueInNewCodePeriod);
 
@@ -633,6 +636,10 @@ class IssueDaoIT {
     assertThat(result.stream().filter(g -> g.getSoftwareQuality() == SECURITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(2);
     assertThat(result.stream().filter(g -> g.getSoftwareQuality() == MAINTAINABILITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(1);
 
+    assertThat(result.stream().filter(g -> g.getSoftwareQuality() == SECURITY && g.isInLeak()).mapToDouble(IssueImpactGroupDto::getEffort).sum()).isEqualTo(1d);
+    assertThat(result.stream().filter(g -> g.getSoftwareQuality() == MAINTAINABILITY && g.isInLeak()).mapToDouble(IssueImpactGroupDto::getEffort).sum()).isEqualTo(1d);
+    assertThat(result.stream().filter(g -> g.getSoftwareQuality() == SECURITY && !g.isInLeak()).mapToDouble(IssueImpactGroupDto::getEffort).sum()).isEqualTo(2d);
+
     assertThat(result.stream().filter(g -> g.getSeverity() == HIGH).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(2);
     assertThat(result.stream().filter(g -> g.getSeverity() == LOW).mapToLong(IssueImpactGroupDto::getCount).sum()).isOne();
 
@@ -642,7 +649,6 @@ class IssueDaoIT {
 
     assertThat(result.stream().filter(g -> RESOLUTION_WONT_FIX.equals(g.getResolution())).mapToLong(IssueImpactGroupDto::getCount).sum()).isOne();
 
-
     assertThat(result.stream().filter(IssueImpactGroupDto::isInLeak).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(2);
     assertThat(result.stream().filter(g -> g.isInLeak() && g.getSoftwareQuality() == SECURITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(1);
     assertThat(result.stream().filter(g -> g.isInLeak() && g.getSoftwareQuality() == MAINTAINABILITY).mapToLong(IssueImpactGroupDto::getCount).sum()).isEqualTo(1);
@@ -1017,7 +1023,7 @@ class IssueDaoIT {
     underTest.updateIfBeforeSelectedDate(db.getSession(), issueDto);
 
     assertThat(underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1).getImpacts()).extracting(i -> i.getSoftwareQuality(),
-        i -> i.getSeverity())
+      i -> i.getSeverity())
       .containsExactlyInAnyOrder(tuple(RELIABILITY, MEDIUM), tuple(SECURITY, LOW));
   }
 
index dbf05f35843c9f2dc6e90d0cc22e0b661222be06..7326fb3072fd509f4dd8a657ec7d491ce94e57a5 100644 (file)
@@ -32,6 +32,7 @@ public class IssueImpactGroupDto {
   private Severity severity;
   private long count;
   private boolean inLeak;
+  private double effort;
 
   public IssueImpactGroupDto() {
     // nothing to do
@@ -85,4 +86,12 @@ public class IssueImpactGroupDto {
   public void setInLeak(boolean inLeak) {
     this.inLeak = inLeak;
   }
+
+  public double getEffort() {
+    return effort;
+  }
+
+  public void setEffort(double effort) {
+    this.effort = effort;
+  }
 }
index 3d70d0aeba2653c8f2cbf90b6beb169354fca507..422e50a7922fdb1a2439da6ff1377000e835d2ed 100644 (file)
       i2.software_quality as softwareQuality,
       i2.severity as severity,
       count(i2.kee) as "count",
-      i2.inLeak as inLeak
+      i2.inLeak as inLeak,
+      sum(i2.effort) as effort
       from (
         select
           i.status,
           i.kee,
           ii.software_quality,
           ii.severity,
+          i.effort,
           <if test="leakPeriodBeginningDate &gt;= 0">
             case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then ${_true} else ${_false} end as inLeak
           </if>
index f8fa9990bbef4a46de563656d748620c7a7974ec..d532a1c888cc64708d20976fa413aac4c10485f4 100644 (file)
@@ -39,6 +39,7 @@ dependencies {
   testImplementation 'com.squareup.okhttp3:mockwebserver'
   testImplementation 'jakarta.servlet:jakarta.servlet-api'
   testImplementation 'org.junit.jupiter:junit-jupiter-api'
+  testImplementation 'org.junit.jupiter:junit-jupiter-params'
   testImplementation 'org.mockito:mockito-core'
   testImplementation 'org.mockito:mockito-junit-jupiter'
   testImplementation 'org.sonarsource.api.plugin:sonar-plugin-api-test-fixtures'
@@ -53,6 +54,7 @@ dependencies {
 
   testFixturesImplementation 'org.junit.jupiter:junit-jupiter-api'
 
+
   testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
   testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
 }
index 76d7da9d86701d33790c6b5b49cf67d1d6415bf7..c7c0fbb34ecc92d425b82457cfa0e7d1b4452cca 100644 (file)
@@ -24,6 +24,7 @@ import java.util.EnumMap;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import org.sonar.api.issue.IssueStatus;
 import org.sonar.api.issue.impact.Severity;
@@ -50,6 +51,8 @@ class IssueCounter {
   private long prioritizedRuleIssues = 0;
   private final Count highImpactAccepted = new Count();
   private final Map<SoftwareQuality, Map<Severity, Count>> bySoftwareQualityAndSeverity = new EnumMap<>(SoftwareQuality.class);
+  private final Map<SoftwareQuality, Effort> effortOfUnresolvedBySoftwareQuality = new EnumMap<>(SoftwareQuality.class);
+  private final Map<SoftwareQuality, HighestImpactSeverity> highestSeverityOfUnresolvedBySoftwareQuality = new EnumMap<>(SoftwareQuality.class);
 
   IssueCounter(Collection<IssueGroupDto> groups, Collection<IssueImpactGroupDto> impactGroups) {
     for (IssueGroupDto group : groups) {
@@ -117,6 +120,16 @@ class IssueCounter {
         .add(group);
     }
 
+    if (group.getResolution() == null) {
+      effortOfUnresolvedBySoftwareQuality
+        .computeIfAbsent(group.getSoftwareQuality(), k -> new Effort())
+        .add(group);
+
+      highestSeverityOfUnresolvedBySoftwareQuality
+        .computeIfAbsent(group.getSoftwareQuality(), k -> new HighestImpactSeverity())
+        .add(group);
+    }
+
     if (Severity.HIGH == group.getSeverity() && IssueStatus.ACCEPTED == issueStatus) {
       highImpactAccepted.add(group);
     }
@@ -127,6 +140,11 @@ class IssueCounter {
       .map(hs -> hs.severity(onlyInLeak));
   }
 
+  public Optional<Severity> getHighestSeverityOfUnresolved(SoftwareQuality softwareQuality, boolean onlyInLeak) {
+    return Optional.ofNullable(highestSeverityOfUnresolvedBySoftwareQuality.get(softwareQuality))
+      .map(hs -> hs.severity(onlyInLeak));
+  }
+
   public double sumEffortOfUnresolved(RuleType type, boolean onlyInLeak) {
     Effort effort = effortOfUnresolved.get(type);
     if (effort == null) {
@@ -135,6 +153,14 @@ class IssueCounter {
     return onlyInLeak ? effort.leak : effort.absolute;
   }
 
+  public double sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality softwareQuality, boolean onlyInLeak) {
+    Effort effort = effortOfUnresolvedBySoftwareQuality.get(softwareQuality);
+    if (effort == null) {
+      return 0.0;
+    }
+    return onlyInLeak ? effort.leak : effort.absolute;
+  }
+
   public long countUnresolvedBySeverity(String severity, boolean onlyInLeak) {
     return value(unresolvedBySeverity.get(severity), onlyInLeak);
   }
@@ -220,6 +246,13 @@ class IssueCounter {
         leak += group.getEffort();
       }
     }
+
+    void add(IssueImpactGroupDto group) {
+      absolute += group.getEffort();
+      if (group.isInLeak()) {
+        leak += group.getEffort();
+      }
+    }
   }
 
   private static class HighestSeverity {
@@ -238,4 +271,32 @@ class IssueCounter {
       return SeverityUtil.getSeverityFromOrdinal(inLeak ? leak : absolute);
     }
   }
+
+  private static class HighestImpactSeverity {
+    private Severity absolute = null;
+    private Severity leak = null;
+
+    void add(IssueImpactGroupDto group) {
+      absolute = getMaxSeverity(absolute, group.getSeverity());
+      if (group.isInLeak()) {
+        leak = getMaxSeverity(leak, group.getSeverity());
+      }
+    }
+
+    Severity getMaxSeverity(@Nullable Severity currentSeverity, Severity newSeverity) {
+      if (currentSeverity == null) {
+        return newSeverity;
+      }
+      if (newSeverity.ordinal() > currentSeverity.ordinal()) {
+        return newSeverity;
+      } else {
+        return currentSeverity;
+      }
+    }
+
+    @CheckForNull
+    Severity severity(boolean inLeak) {
+      return inLeak ? leak : absolute;
+    }
+  }
 }
index 61f1b6595f4e49736d6bf06bf0f8cfd8116920ac..3f5871bd5fd3463df3c057992ad783b9f6ffb7d2 100644 (file)
@@ -41,8 +41,28 @@ import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW
 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.TECHNICAL_DEBT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REVIEW_RATING;
 import static org.sonar.server.measure.Rating.RATING_BY_SEVERITY;
+import static org.sonar.server.measure.Rating.RATING_BY_SOFTWARE_QUALITY_SEVERITY;
 import static org.sonar.server.metric.IssueCountMetrics.PRIORITIZED_RULE_ISSUES;
+import static org.sonar.server.security.SecurityReviewRating.computeAToDRating;
 import static org.sonar.server.security.SecurityReviewRating.computePercent;
 import static org.sonar.server.security.SecurityReviewRating.computeRating;
 
@@ -127,18 +147,19 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
       (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))),
 
     new MeasureUpdateFormula(CoreMetrics.SQALE_DEBT_RATIO, false, false,
-      (context, formula) -> context.setValue(100.0 * debtDensity(context)),
-      (context, issues) -> context.setValue(100.0 * debtDensity(context)),
+      (context, formula) -> context.setValue(100.0 * debtDensity(TECHNICAL_DEBT, context)),
+      (context, issues) -> context.setValue(100.0 * debtDensity(TECHNICAL_DEBT, context)),
       asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
 
     new MeasureUpdateFormula(CoreMetrics.SQALE_RATING, false, false,
-      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
-      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(TECHNICAL_DEBT, context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(TECHNICAL_DEBT, context))),
       asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
 
     new MeasureUpdateFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false, false,
-      (context, formula) -> context.setValue(effortToReachMaintainabilityRatingA(context)),
-      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+      (context, formula) -> context.setValue(effortToReachMaintainabilityRatingA(CoreMetrics.TECHNICAL_DEBT, context)),
+      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(CoreMetrics.TECHNICAL_DEBT, context)),
+      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
 
     new MeasureUpdateFormula(CoreMetrics.RELIABILITY_RATING, false, new MaxRatingChildren(),
       (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))),
@@ -251,19 +272,109 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
       }),
 
     new MeasureUpdateFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true, false,
-      (context, formula) -> context.setValue(100.0D * newDebtDensity(context)),
-      (context, issues) -> context.setValue(100.0D * newDebtDensity(context)),
+      (context, formula) -> context.setValue(100.0D * newDebtDensity(CoreMetrics.NEW_TECHNICAL_DEBT, context)),
+      (context, issues) -> context.setValue(100.0D * newDebtDensity(CoreMetrics.NEW_TECHNICAL_DEBT, context)),
       asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),
 
     new MeasureUpdateFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true, false,
-      (context, formula) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))),
-      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))),
-      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)));
+      (context, formula) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(CoreMetrics.NEW_TECHNICAL_DEBT, context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(CoreMetrics.NEW_TECHNICAL_DEBT, context))),
+      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),
+
+    // Metrics based on Software Qualities
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, false, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.MAINTAINABILITY, false))),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, false, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.RELIABILITY, false))),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, false, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.SECURITY, false))),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, true, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.MAINTAINABILITY, true))),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, true, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.RELIABILITY, true))),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, true, true, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolvedBySoftwareQuality(SoftwareQuality.SECURITY, true))),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO, false, true,
+      (context, formula) -> context.setValue(100.0 * debtDensity(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      (context, issues) -> context.setValue(100.0 * debtDensity(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      asList(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO, true, true,
+      (context, formula) -> context.setValue(100.0D * newDebtDensity(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      (context, issues) -> context.setValue(100.0D * newDebtDensity(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      asList(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, CoreMetrics.NEW_DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_MAINTAINABILITY_RATING, false, true,
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getAToDRatingForDensity(debtDensity(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getAToDRatingForDensity(debtDensity(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context))),
+      asList(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING, true, true,
+      (context, formula) -> context.setValue(context.getDebtRatingGrid().getAToDRatingForDensity(newDebtDensity(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getAToDRatingForDensity(newDebtDensity(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context))),
+      asList(NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, CoreMetrics.NEW_DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A, false, true,
+      (context, formula) -> context.setValue(effortToReachMaintainabilityRatingA(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, context)),
+      asList(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_RELIABILITY_RATING, false, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        Rating rating = issues.getHighestSeverityOfUnresolved(SoftwareQuality.RELIABILITY, false)
+          .map(RATING_BY_SOFTWARE_QUALITY_SEVERITY::get)
+          .orElse(Rating.A);
+        context.setValue(rating);
+      }),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_RELIABILITY_RATING, true, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        Rating rating = issues.getHighestSeverityOfUnresolved(SoftwareQuality.RELIABILITY, true)
+          .map(RATING_BY_SOFTWARE_QUALITY_SEVERITY::get)
+          .orElse(Rating.A);
+        context.setValue(rating);
+      }),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_SECURITY_RATING, false, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        Rating rating = issues.getHighestSeverityOfUnresolved(SoftwareQuality.SECURITY, false)
+          .map(RATING_BY_SOFTWARE_QUALITY_SEVERITY::get)
+          .orElse(Rating.A);
+        context.setValue(rating);
+      }),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_SECURITY_RATING, true, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        Rating rating = issues.getHighestSeverityOfUnresolved(SoftwareQuality.SECURITY, true)
+          .map(RATING_BY_SOFTWARE_QUALITY_SEVERITY::get)
+          .orElse(Rating.A);
+        context.setValue(rating);
+      }),
+
+    new MeasureUpdateFormula(SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, false, true,
+      (context, formula) -> context.setValue(computeAToDRating(context.getValue(SECURITY_HOTSPOTS_REVIEWED).orElse(null))),
+      (context, issues) -> {
+        Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false));
+        context.setValue(computeAToDRating(percent.orElse(null)));
+      }),
+
+    new MeasureUpdateFormula(NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, true, true,
+      (context, formula) -> context.setValue(computeAToDRating(context.getValue(NEW_SECURITY_HOTSPOTS_REVIEWED).orElse(null))),
+      (context, issues) -> {
+        Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true));
+        context.setValue(computeAToDRating(percent.orElse(null)));
+      }));
 
   private static final Set<Metric> FORMULA_METRICS = MeasureUpdateFormulaFactory.extractMetrics(FORMULAS);
 
-  private static double debtDensity(MeasureUpdateFormula.Context context) {
-    double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0D), 0.0D);
+  private static double debtDensity(Metric<?> maintainabilityRemediationEffortMetric, MeasureUpdateFormula.Context context) {
+    double debt = Math.max(context.getValue(maintainabilityRemediationEffortMetric).orElse(0.0D), 0.0D);
     Optional<Double> devCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble);
     if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
       return debt / devCost.get();
@@ -271,8 +382,8 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
     return 0.0D;
   }
 
-  private static double newDebtDensity(MeasureUpdateFormula.Context context) {
-    double debt = Math.max(context.getValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0D), 0.0D);
+  private static double newDebtDensity(Metric<?> maintainabilityRemediationEffortMetric, MeasureUpdateFormula.Context context) {
+    double debt = Math.max(context.getValue(maintainabilityRemediationEffortMetric).orElse(0.0D), 0.0D);
     Optional<Double> devCost = context.getValue(CoreMetrics.NEW_DEVELOPMENT_COST);
     if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
       return debt / devCost.get();
@@ -280,9 +391,9 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
     return 0.0D;
   }
 
-  private static double effortToReachMaintainabilityRatingA(MeasureUpdateFormula.Context context) {
+  private static double effortToReachMaintainabilityRatingA(Metric<?> maintainabilityRemediationEffortMetric, MeasureUpdateFormula.Context context) {
     double developmentCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble).orElse(0.0D);
-    double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0D);
+    double effort = context.getValue(maintainabilityRemediationEffortMetric).orElse(0.0D);
     double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost;
     return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0D;
   }
index a6f5746718e1929c5fe4c635461b2114ee6d73c9..1eb1e92a3774388218dfd403f3df1c063bafc12f 100644 (file)
@@ -30,14 +30,19 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
 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.core.metric.SoftwareQualitiesMetrics;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.issue.IssueGroupDto;
 import org.sonar.db.issue.IssueImpactGroupDto;
@@ -46,12 +51,15 @@ import org.sonar.server.measure.Rating;
 
 import static java.util.Arrays.asList;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
 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.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A;
+import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY;
 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;
@@ -60,6 +68,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.api.measures.CoreMetrics.SQALE_DEBT_RATIO;
+import static org.sonar.api.measures.CoreMetrics.SQALE_RATING;
+import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY;
+import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT;
+import static org.sonar.core.metric.SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT;
 import static org.sonar.server.metric.IssueCountMetrics.PRIORITIZED_RULE_ISSUES;
 import static org.sonar.test.JsonAssert.assertJson;
 
@@ -152,6 +165,75 @@ class MeasureUpdateFormulaFactoryImplTest {
       .expectedRating(Rating.E);
   }
 
+  @Test
+  void computeHierarchy_shouldRecomputeSoftwareQualityMetricsCombiningOtherMetrics() {
+    new HierarchyTester(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REVIEW_RATING)
+      .withValue(SECURITY_HOTSPOTS_REVIEWED, 0d)
+      .expectedRating(Rating.D);
+    new HierarchyTester(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING)
+      .withValue(NEW_SECURITY_HOTSPOTS_REVIEWED, 0d)
+      .expectedRating(Rating.D);
+    new HierarchyTester(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO)
+      .withValue(SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 10d)
+      .withValue(CoreMetrics.DEVELOPMENT_COST, "40")
+      .expectedResult(25d);
+    new HierarchyTester(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO)
+      .withValue(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 10d)
+      .withValue(CoreMetrics.NEW_DEVELOPMENT_COST, 40d)
+      .expectedResult(25d);
+
+    new HierarchyTester(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING)
+      .withValue(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 10d)
+      .withValue(CoreMetrics.DEVELOPMENT_COST, "40")
+      .expectedRating(Rating.D);
+
+    new HierarchyTester(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING)
+      .withValue(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 10d)
+      .withValue(CoreMetrics.NEW_DEVELOPMENT_COST, 40d)
+      .expectedRating(Rating.D);
+
+    new HierarchyTester(SoftwareQualitiesMetrics.EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A)
+      .withValue(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 10d)
+      .withValue(CoreMetrics.DEVELOPMENT_COST, "40")
+      .expectedResult(8d);
+  }
+
+  static List<Metric<?>> softwareQualityRatingMetric() {
+    return List.of(
+      SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING,
+      SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING,
+      SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING,
+      SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING);
+  }
+
+  @MethodSource("softwareQualityRatingMetric")
+  @ParameterizedTest
+  void computeHierarchy_shoudRecomputeSoftwareQualityMetricsBasedOnRating(Metric<?> ratingMetric) {
+    new HierarchyTester(ratingMetric)
+      .withValue(1d)
+      .withChildrenValues(2d, 3d)
+      .expectedRating(Rating.C);
+  }
+
+  static List<Metric<?>> softwareQualitySummingMetric() {
+    return List.of(
+      SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT,
+      SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT,
+      SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT,
+      SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT,
+      SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT,
+      SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT);
+  }
+
+  @MethodSource("softwareQualitySummingMetric")
+  @ParameterizedTest
+  void computeHierarchy_shoudRecomputeSoftwareQualityMetricsBasedOnSumming(Metric<?> summingMetric) {
+    new HierarchyTester(summingMetric)
+      .withValue(1d)
+      .withChildrenValues(2d, 3d)
+      .expectedResult(6d);
+  }
+
   @Test
   void test_violations() {
     withNoIssues().assertThatValueIs(CoreMetrics.VIOLATIONS, 0);
@@ -182,7 +264,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newResolvedGroup(RuleType.BUG).setCount(7),
       // not bugs
       newGroup(RuleType.CODE_SMELL).setCount(11))
-      .assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
+        .assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
   }
 
   @Test
@@ -195,7 +277,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newResolvedGroup(RuleType.CODE_SMELL).setCount(7),
       // not code smells
       newGroup(RuleType.BUG).setCount(11))
-      .assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
+        .assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
   }
 
   @Test
@@ -208,7 +290,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newResolvedGroup(RuleType.VULNERABILITY).setCount(7),
       // not vulnerabilities
       newGroup(RuleType.BUG).setCount(11))
-      .assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
+        .assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
   }
 
   @Test
@@ -221,18 +303,26 @@ class MeasureUpdateFormulaFactoryImplTest {
       newResolvedGroup(RuleType.SECURITY_HOTSPOT).setCount(7),
       // not hotspots
       newGroup(RuleType.BUG).setCount(11))
-      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
+        .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
   }
 
   @Test
-  void test_security_review_rating() {
+  void test_security_review_ratings() {
     with(
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
-      .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.B);
+        .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.B)
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.B);
 
     withNoIssues()
-      .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.A);
+      .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.A)
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(3).setInLeak(true),
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true))
+        .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.E)
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.D);
   }
 
   @Test
@@ -240,7 +330,7 @@ class MeasureUpdateFormulaFactoryImplTest {
     with(
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
-      .assertThatValueIs(SECURITY_HOTSPOTS_REVIEWED, 75.0);
+        .assertThatValueIs(SECURITY_HOTSPOTS_REVIEWED, 75.0);
 
     withNoIssues()
       .assertNoValue(SECURITY_HOTSPOTS_REVIEWED);
@@ -251,7 +341,7 @@ class MeasureUpdateFormulaFactoryImplTest {
     with(
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
-      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, 3.0);
+        .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, 3.0);
 
     withNoIssues()
       .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
@@ -262,7 +352,7 @@ class MeasureUpdateFormulaFactoryImplTest {
     with(
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3),
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1))
-      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1.0);
+        .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1.0);
 
     withNoIssues()
       .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
@@ -290,11 +380,11 @@ class MeasureUpdateFormulaFactoryImplTest {
       newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(17),
       newResolvedGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(19),
       newResolvedGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.INFO).setCount(21))
-      .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 11 + 13)
-      .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 7)
-      .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 3 + 5)
-      .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
-      .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
+        .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 11 + 13)
+        .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 7)
+        .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 3 + 5)
+        .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
+        .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
   }
 
   @Test
@@ -314,8 +404,8 @@ class MeasureUpdateFormulaFactoryImplTest {
       // exclude unresolved
       newGroup(RuleType.VULNERABILITY).setCount(17),
       newGroup(RuleType.BUG).setCount(19))
-      .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5)
-      .assertThatValueIs(CoreMetrics.ACCEPTED_ISSUES, 7 + 11);
+        .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5)
+        .assertThatValueIs(CoreMetrics.ACCEPTED_ISSUES, 7 + 11);
   }
 
   @Test
@@ -334,9 +424,9 @@ class MeasureUpdateFormulaFactoryImplTest {
       // exclude security hotspot
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN).setCount(12),
       newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(13))
-      .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 3 + 5)
-      .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 9 + 11)
-      .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 7);
+        .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 3 + 5)
+        .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 9 + 11)
+        .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 7);
   }
 
   @Test
@@ -353,7 +443,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.BUG).setEffort(7.0),
       // exclude resolved
       newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0))
-      .assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
+        .assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
   }
 
   @Test
@@ -367,7 +457,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setEffort(7.0),
       // exclude resolved
       newResolvedGroup(RuleType.BUG).setEffort(17.0))
-      .assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
+        .assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
   }
 
   @Test
@@ -381,122 +471,142 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setEffort(7.0),
       // exclude resolved
       newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0))
-      .assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
+        .assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
   }
 
-  @Test
-  void test_sqale_debt_ratio_and_sqale_rating() {
+  private static Stream<Arguments> maintainabilityMetrics() {
+    return Stream.of(
+      arguments(TECHNICAL_DEBT, SQALE_DEBT_RATIO, SQALE_RATING),
+      arguments(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO,
+        SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING));
+  }
+
+  @ParameterizedTest
+  @MethodSource("maintainabilityMetrics")
+  void test_sqale_debt_ratio_and_sqale_rating(Metric<?> maintainabilityRemediationEffortMetric, Metric<?> maintainabilityDebtRatioMetric, Metric<?> maintainabilityRatingMetric) {
     withNoIssues()
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
     // technical_debt not computed
     with(CoreMetrics.DEVELOPMENT_COST, "0")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
     with(CoreMetrics.DEVELOPMENT_COST, "20")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
     // development_cost not computed
-    with(CoreMetrics.TECHNICAL_DEBT, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-    with(CoreMetrics.TECHNICAL_DEBT, 20)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+    with(maintainabilityRemediationEffortMetric, 0)
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
+    with(maintainabilityRemediationEffortMetric, 20)
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
     // input measures are available
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+    with(maintainabilityRemediationEffortMetric, 20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "0")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+    with(maintainabilityRemediationEffortMetric, 20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "160")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 12.5)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.C);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 12.5)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.C);
 
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+    Verifier verifier = with(maintainabilityRemediationEffortMetric, 20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "10")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 200.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.E);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 200.0);
+    switch (maintainabilityRatingMetric.key()) {
+      case SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY -> verifier.assertThatValueIs(maintainabilityRatingMetric, Rating.D);
+      case SQALE_RATING_KEY -> verifier.assertThatValueIs(maintainabilityRatingMetric, Rating.E);
+      default -> throw new IllegalArgumentException("Unexpected metric: " + maintainabilityRatingMetric.key());
+    }
 
     // A is 5% --> min debt is exactly 200*0.05=10
     with(CoreMetrics.DEVELOPMENT_COST, "200")
-      .and(CoreMetrics.TECHNICAL_DEBT, 10.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 5.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .and(maintainabilityRemediationEffortMetric, 10.0)
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 5.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
+    with(maintainabilityRemediationEffortMetric, 0.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "0")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
+    with(maintainabilityRemediationEffortMetric, 0.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "80")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0);
 
-    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
+    with(maintainabilityRemediationEffortMetric, -20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "0")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
     // bug, debt can't be negative
-    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
+    with(maintainabilityRemediationEffortMetric, -20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "80")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
 
     // bug, cost can't be negative
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+    with(maintainabilityRemediationEffortMetric, 20.0)
       .andText(CoreMetrics.DEVELOPMENT_COST, "-80")
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+      .assertThatValueIs(maintainabilityDebtRatioMetric, 0.0)
+      .assertThatValueIs(maintainabilityRatingMetric, Rating.A);
   }
 
-  @Test
-  void test_effort_to_reach_maintainability_rating_A() {
+  private static Stream<Arguments> effortToReachAMetrics() {
+    return Stream.of(
+      arguments(TECHNICAL_DEBT, EFFORT_TO_REACH_MAINTAINABILITY_RATING_A),
+      arguments(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT,
+        SoftwareQualitiesMetrics.EFFORT_TO_REACH_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_A));
+  }
+
+  @ParameterizedTest
+  @MethodSource("effortToReachAMetrics")
+  void test_effort_to_reach_maintainability_rating_A(Metric<?> maintainabilityRemediationEffortMetric, Metric<?> effortToReachAMetric) {
     withNoIssues()
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+      .assertThatValueIs(effortToReachAMetric, 0.0);
 
     // technical_debt not computed
     with(CoreMetrics.DEVELOPMENT_COST, 0.0)
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+      .assertThatValueIs(effortToReachAMetric, 0.0);
     with(CoreMetrics.DEVELOPMENT_COST, 20.0)
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+      .assertThatValueIs(effortToReachAMetric, 0.0);
 
     // development_cost not computed
-    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+    with(maintainabilityRemediationEffortMetric, 0.0)
+      .assertThatValueIs(effortToReachAMetric, 0.0);
+    with(maintainabilityRemediationEffortMetric, 20.0)
       // development cost is considered as zero, so the effort is to reach... zero
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 20.0);
+      .assertThatValueIs(effortToReachAMetric, 20.0);
 
     // B to A
     with(CoreMetrics.DEVELOPMENT_COST, "200")
-      .and(CoreMetrics.TECHNICAL_DEBT, 40.0)
+      .and(maintainabilityRemediationEffortMetric, 40.0)
       // B is 5% --> goal is to reach 200*0.05=10 --> effort is 40-10=30
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 40.0 - (200.0 * 0.05));
+      .assertThatValueIs(effortToReachAMetric, 40.0 - (200.0 * 0.05));
 
     // E to A
     with(CoreMetrics.DEVELOPMENT_COST, "200")
-      .and(CoreMetrics.TECHNICAL_DEBT, 180.0)
+      .and(maintainabilityRemediationEffortMetric, 180.0)
       // B is 5% --> goal is to reach 200*0.05=10 --> effort is 180-10=170
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 180.0 - (200.0 * 0.05));
+      .assertThatValueIs(effortToReachAMetric, 180.0 - (200.0 * 0.05));
 
     // already A
     with(CoreMetrics.DEVELOPMENT_COST, "200")
-      .and(CoreMetrics.TECHNICAL_DEBT, 8.0)
+      .and(maintainabilityRemediationEffortMetric, 8.0)
       // B is 5% --> goal is to reach 200*0.05=10 --> debt is already at 8 --> effort to reach A is zero
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+      .assertThatValueIs(effortToReachAMetric, 0.0);
 
     // exactly lower range of B
     with(CoreMetrics.DEVELOPMENT_COST, "200")
-      .and(CoreMetrics.TECHNICAL_DEBT, 10.0)
+      .and(maintainabilityRemediationEffortMetric, 10.0)
       // B is 5% --> goal is to reach 200*0.05=10 --> debt is 10 --> effort to reach A is zero
       // FIXME need zero to reach A but effective rating is B !
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+      .assertThatValueIs(effortToReachAMetric, 0.0);
   }
 
   @Test
@@ -509,14 +619,50 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(5),
       // excluded, not a bug
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3))
-      // highest severity of bugs is CRITICAL --> D
-      .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.D);
+        // highest severity of bugs is CRITICAL --> D
+        .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.D);
 
     with(
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
       newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5))
-      // no bugs --> A
-      .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
+        // no bugs --> A
+        .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
+  }
+
+  @Test
+  void test_software_quality_reliability_rating() {
+    withNoIssues()
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.A);
+
+    with(
+      newImpactGroup(MAINTAINABILITY, HIGH, 1),
+      newImpactGroup(RELIABILITY, MEDIUM, 1),
+      newImpactGroup(RELIABILITY, LOW, 1),
+      newImpactGroup(SECURITY, MEDIUM, 1))
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.C);
+
+    with(
+      newImpactGroup(RELIABILITY, LOW, 1))
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.B);
+  }
+
+  @Test
+  void test_software_quality_new_reliability_rating() {
+    withNoIssues()
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.A);
+
+    with(
+      newImpactGroup(MAINTAINABILITY, HIGH, 1, true),
+      newImpactGroup(RELIABILITY, MEDIUM, 1, true),
+      newImpactGroup(RELIABILITY, LOW, 1, true),
+      newImpactGroup(RELIABILITY, HIGH, 1, false),
+      newImpactGroup(SECURITY, HIGH, 1, true))
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.C);
+
+    with(
+      newImpactGroup(RELIABILITY, LOW, 1, true),
+      newImpactGroup(RELIABILITY, MEDIUM, 1, false))
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_RATING, Rating.B);
   }
 
   @Test
@@ -529,14 +675,50 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(5),
       // excluded, not a vulnerability
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3))
-      // highest severity of vulnerabilities is CRITICAL --> D
-      .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.D);
+        // highest severity of vulnerabilities is CRITICAL --> D
+        .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.D);
 
     with(
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
       newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5))
-      // no vulnerabilities --> A
-      .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
+        // no vulnerabilities --> A
+        .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
+  }
+
+  @Test
+  void test_software_quality_security_rating() {
+    withNoIssues()
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING, Rating.A);
+
+    with(
+      newImpactGroup(MAINTAINABILITY, HIGH, 1),
+      newImpactGroup(SECURITY, MEDIUM, 1),
+      newImpactGroup(SECURITY, LOW, 1),
+      newImpactGroup(RELIABILITY, MEDIUM, 1))
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING, Rating.C);
+
+    with(
+      newImpactGroup(SECURITY, LOW, 1))
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_RATING, Rating.B);
+  }
+
+  @Test
+  void test_software_quality_new_security_rating() {
+    withNoIssues()
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING, Rating.A);
+
+    with(
+      newImpactGroup(MAINTAINABILITY, HIGH, 1, true),
+      newImpactGroup(SECURITY, MEDIUM, 1, true),
+      newImpactGroup(SECURITY, LOW, 1, true),
+      newImpactGroup(SECURITY, HIGH, 1, false),
+      newImpactGroup(RELIABILITY, HIGH, 1, true))
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING, Rating.C);
+
+    with(
+      newImpactGroup(SECURITY, LOW, 1, true),
+      newImpactGroup(SECURITY, MEDIUM, 1, false))
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_RATING, Rating.B);
   }
 
   @Test
@@ -550,7 +732,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not bugs
       newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(9),
       newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
-      .assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);
 
   }
 
@@ -565,7 +747,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not code smells
       newGroup(RuleType.BUG).setInLeak(true).setCount(9),
       newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
-      .assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
   }
 
   @Test
@@ -579,7 +761,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not vulnerabilities
       newGroup(RuleType.BUG).setInLeak(true).setCount(9),
       newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
-      .assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
   }
 
   @Test
@@ -593,7 +775,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not hotspots
       newGroup(RuleType.BUG).setInLeak(true).setCount(9),
       newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
   }
 
   @Test
@@ -608,7 +790,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.BUG).setInLeak(false).setCount(11),
       newGroup(RuleType.CODE_SMELL).setInLeak(false).setCount(13),
       newGroup(RuleType.VULNERABILITY).setInLeak(false).setCount(17))
-      .assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 5 + 7 + 9);
+        .assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 5 + 7 + 9);
   }
 
   @Test
@@ -625,7 +807,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(11),
       newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(13))
-      .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 3 + 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 3 + 5 + 7);
   }
 
   @Test
@@ -642,7 +824,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(11),
       newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(13))
-      .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 3 + 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 3 + 5 + 7);
   }
 
   @Test
@@ -659,7 +841,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(false).setCount(11),
       newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(false).setCount(13))
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 3 + 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 3 + 5 + 7);
   }
 
   @Test
@@ -676,7 +858,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(false).setCount(11),
       newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(false).setCount(13))
-      .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 3 + 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 3 + 5 + 7);
   }
 
   @Test
@@ -693,7 +875,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(false).setCount(11),
       newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(false).setCount(13))
-      .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 3 + 5 + 7);
+        .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 3 + 5 + 7);
   }
 
   @Test
@@ -709,7 +891,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       // not in leak
       newGroup(RuleType.CODE_SMELL).setResolution(Issue.RESOLUTION_WONT_FIX).setInLeak(false).setCount(5),
       newGroup(RuleType.BUG).setResolution(Issue.RESOLUTION_WONT_FIX).setInLeak(false).setCount(50))
-      .assertThatLeakValueIs(CoreMetrics.NEW_ACCEPTED_ISSUES, 4 + 40);
+        .assertThatLeakValueIs(CoreMetrics.NEW_ACCEPTED_ISSUES, 4 + 40);
   }
 
   @Test
@@ -725,7 +907,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.BUG).setEffort(7.0).setInLeak(true),
       // exclude resolved
       newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0).setInLeak(true))
-      .assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0);
   }
 
   @Test
@@ -740,7 +922,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true),
       // exclude resolved
       newResolvedGroup(RuleType.BUG).setEffort(17.0).setInLeak(true))
-      .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 3.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 3.0);
   }
 
   @Test
@@ -755,7 +937,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true),
       // exclude resolved
       newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0).setInLeak(true))
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 3.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 3.0);
   }
 
   @Test
@@ -771,8 +953,8 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true),
       // exclude resolved
       newResolvedGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true))
-      // highest severity of bugs on leak period is minor -> B
-      .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.B);
+        // highest severity of bugs on leak period is minor -> B
+        .assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.B);
   }
 
   @Test
@@ -788,8 +970,8 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true),
       // exclude resolved
       newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true))
-      // highest severity of bugs on leak period is minor -> B
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.B);
+        // highest severity of bugs on leak period is minor -> B
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.B);
   }
 
   @Test
@@ -799,10 +981,20 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
       // not in leak
       newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Issue.STATUS_TO_REVIEW).setInLeak(false))
-      .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.B);
+        .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.B)
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.B);
 
     withNoIssues()
-      .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.A);
+      .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.A)
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(3).setInLeak(true),
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Issue.STATUS_TO_REVIEW).setInLeak(false))
+        .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.E)
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REVIEW_RATING, Rating.D);
   }
 
   @Test
@@ -812,7 +1004,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
       // not in leak
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(5).setInLeak(false))
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, 75.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, 75.0);
 
     withNoIssues()
       .assertNoLeakValue(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED);
@@ -825,7 +1017,7 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
       // not in leak
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(5).setInLeak(false))
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 3.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 3.0);
 
     withNoIssues()
       .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
@@ -838,81 +1030,94 @@ class MeasureUpdateFormulaFactoryImplTest {
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
       // not in leak
       newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(5).setInLeak(false))
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1.0);
+        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1.0);
 
     withNoIssues()
       .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
   }
 
-  @Test
-  void test_new_sqale_debt_ratio_and_new_maintainability_rating() {
+  private static Stream<Arguments> newMaintainabilityMetrics() {
+    return Stream.of(
+      arguments(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_SQALE_DEBT_RATIO, CoreMetrics.NEW_MAINTAINABILITY_RATING),
+      arguments(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_DEBT_RATIO,
+        SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING));
+  }
+
+  @ParameterizedTest
+  @MethodSource("newMaintainabilityMetrics")
+  void test_new_sqale_debt_ratio_and_new_maintainability_rating(Metric<?> newMaintainabilityRemediationEffortMetric, Metric<?> newMaintainabilityDebtRatioMetric,
+    Metric<?> newMaintainabilityRatingMetric) {
     withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
     // technical_debt not computed
     with(CoreMetrics.NEW_DEVELOPMENT_COST, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
     with(CoreMetrics.NEW_DEVELOPMENT_COST, 20)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
     // development_cost not computed
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 20)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+    with(newMaintainabilityRemediationEffortMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
+    with(newMaintainabilityRemediationEffortMetric, 20)
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
     // input measures are available
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
+    with(newMaintainabilityRemediationEffortMetric, 20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
+    with(newMaintainabilityRemediationEffortMetric, 20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 160.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 12.5)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.C);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 12.5)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.C);
 
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
+    Verifier verifier = with(newMaintainabilityRemediationEffortMetric, 20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 10.0D)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 200.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.E);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 200.0);
+    switch (newMaintainabilityRatingMetric.key()) {
+      case NEW_MAINTAINABILITY_RATING_KEY -> verifier.assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.E);
+      case SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_RATING_KEY -> verifier.assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.D);
+      default -> throw new IllegalArgumentException("Unexpected metric: " + newMaintainabilityRatingMetric.key());
+    }
 
     // A is 5% --> min debt is exactly 200*0.05=10
     with(CoreMetrics.NEW_DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.NEW_TECHNICAL_DEBT, 10.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 5.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .and(newMaintainabilityRemediationEffortMetric, 10.0)
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 5.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0)
+    with(newMaintainabilityRemediationEffortMetric, 0.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0)
+    with(newMaintainabilityRemediationEffortMetric, 0.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0);
 
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
+    with(newMaintainabilityRemediationEffortMetric, -20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
     // bug, debt can't be negative
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
+    with(newMaintainabilityRemediationEffortMetric, -20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
 
     // bug, cost can't be negative
-    with(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
+    with(newMaintainabilityRemediationEffortMetric, 20.0)
       .and(CoreMetrics.NEW_DEVELOPMENT_COST, -80.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+      .assertThatLeakValueIs(newMaintainabilityDebtRatioMetric, 0.0)
+      .assertThatLeakValueIs(newMaintainabilityRatingMetric, Rating.A);
   }
 
   @Test
@@ -930,7 +1135,34 @@ class MeasureUpdateFormulaFactoryImplTest {
       newImpactGroup(SECURITY, LOW, Issue.STATUS_RESOLVED, Issue.RESOLUTION_WONT_FIX, 6),
       newImpactGroup(SECURITY, HIGH, Issue.STATUS_RESOLVED, Issue.RESOLUTION_FALSE_POSITIVE, 7),
       newImpactGroup(RELIABILITY, HIGH, Issue.STATUS_RESOLVED, Issue.RESOLUTION_WONT_FIX, 8))
-      .assertThatValueIs(CoreMetrics.HIGH_IMPACT_ACCEPTED_ISSUES, 4 + 8);
+        .assertThatValueIs(CoreMetrics.HIGH_IMPACT_ACCEPTED_ISSUES, 4 + 8);
+  }
+
+  @Test
+  void compute_shouldComputeRemediationEffortBasedOnSoftwareQuality() {
+    withNoIssues()
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 0)
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, 0)
+      .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, 0)
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 0d)
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, 0d)
+      .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, 0d);
+
+    with(
+      newImpactGroup(RELIABILITY, HIGH, 3, 1d),
+      newImpactGroup(RELIABILITY, MEDIUM, 1, 2d, true),
+      newImpactGroup(SECURITY, MEDIUM, 1, 1d),
+      newImpactGroup(MAINTAINABILITY, MEDIUM, 1, 1d),
+      newImpactGroup(MAINTAINABILITY, HIGH, 1, 2d, true),
+      newImpactGroup(SECURITY, HIGH, Issue.STATUS_RESOLVED, Issue.RESOLUTION_WONT_FIX, 4, 1d, false),
+      newImpactGroup(RELIABILITY, HIGH, Issue.STATUS_RESOLVED, Issue.RESOLUTION_WONT_FIX, 8, 1d, false),
+      newImpactGroup(MAINTAINABILITY, MEDIUM, Issue.STATUS_RESOLVED, Issue.RESOLUTION_WONT_FIX, 8, 1d, false))
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 1d + 2d)
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, 1d + 2d)
+        .assertThatValueIs(SoftwareQualitiesMetrics.SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, 1d)
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_MAINTAINABILITY_REMEDIATION_EFFORT, 2d)
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_RELIABILITY_REMEDIATION_EFFORT, 2d)
+        .assertThatLeakValueIs(SoftwareQualitiesMetrics.NEW_SOFTWARE_QUALITY_SECURITY_REMEDIATION_EFFORT, 0d);
   }
 
   @Test
@@ -954,9 +1186,9 @@ class MeasureUpdateFormulaFactoryImplTest {
       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));
+        .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
@@ -1098,18 +1330,37 @@ class MeasureUpdateFormulaFactoryImplTest {
   }
 
   private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity,
-                                                    String status, @Nullable String resolution, long count) {
+    String status, @Nullable String resolution, long count, double effort, boolean inLeak) {
     IssueImpactGroupDto dto = new IssueImpactGroupDto();
     dto.setSoftwareQuality(softwareQuality);
     dto.setSeverity(severity);
     dto.setStatus(status);
     dto.setResolution(resolution);
     dto.setCount(count);
+    dto.setEffort(effort);
+    dto.setInLeak(inLeak);
     return dto;
   }
 
+  private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity,
+    String status, @Nullable String resolution, long count) {
+    return newImpactGroup(softwareQuality, severity, status, resolution, count, 0, false);
+  }
+
   private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, long count) {
-    return newImpactGroup(softwareQuality, severity, Issue.STATUS_OPEN, null, count);
+    return newImpactGroup(softwareQuality, severity, Issue.STATUS_OPEN, null, count, 0, false);
+  }
+
+  private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, long count, boolean inLeak) {
+    return newImpactGroup(softwareQuality, severity, Issue.STATUS_OPEN, null, count, 0, inLeak);
+  }
+
+  private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, long count, double effort) {
+    return newImpactGroup(softwareQuality, severity, Issue.STATUS_OPEN, null, count, effort, false);
+  }
+
+  private static IssueImpactGroupDto newImpactGroup(SoftwareQuality softwareQuality, org.sonar.api.issue.impact.Severity severity, long count, double effort, boolean inLeak) {
+    return newImpactGroup(softwareQuality, severity, Issue.STATUS_OPEN, null, count, effort, inLeak);
   }
 
   private static IssueGroupDto newResolvedGroup(RuleType ruleType) {