]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11401 Calculate hotspot measures
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Tue, 7 Jun 2022 21:08:57 +0000 (16:08 -0500)
committersonartech <sonartech@sonarsource.com>
Fri, 10 Jun 2022 08:15:07 +0000 (08:15 +0000)
14 files changed:
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
server/sonar-server-common/src/main/java/org/sonar/server/security/SecurityReviewRating.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.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/HotspotMeasureUpdaterTest.java [deleted file]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java

index 98655ebf25cf795b57c367907b592d14831f508f..afaba3af0e2844b73051cb7e3ea6b961e5f3b034 100644 (file)
@@ -224,9 +224,9 @@ public class ComponentDao implements Dao {
     return mapper(dbSession).selectDescendants(query, componentOpt.get().uuid(), query.getUuidPath(component));
   }
 
-  public List<ComponentDto> selectChildren(DbSession dbSession, Collection<ComponentDto> components) {
+  public List<ComponentDto> selectChildren(DbSession dbSession, String branchUuid, Collection<ComponentDto> components) {
     Set<String> uuidPaths = components.stream().map(c -> c.getUuidPath() + c.uuid() + ".").collect(Collectors.toSet());
-    return mapper(dbSession).selectChildren(uuidPaths);
+    return mapper(dbSession).selectChildren(branchUuid, uuidPaths);
   }
 
   public ComponentDto selectOrFailByKey(DbSession session, String key) {
index f517bddea89b510c70c967a9a6c2375444e191a6..2d827bee828fccbb1a6754e6bcfa98ab3b76cff8 100644 (file)
@@ -69,7 +69,7 @@ public interface ComponentMapper {
 
   List<ComponentDto> selectDescendants(@Param("query") ComponentTreeQuery query, @Param("baseUuid") String baseUuid, @Param("baseUuidPath") String baseUuidPath);
 
-  List<ComponentDto> selectChildren(@Param("uuidPaths") Set<String> uuidPaths);
+  List<ComponentDto> selectChildren(@Param("branchUuid") String branchUuid, @Param("uuidPaths") Set<String> uuidPaths);
 
   /**
    * Returns all enabled projects (Scope {@link org.sonar.api.resources.Scopes#PROJECT} and qualifier
index efacf0809b4ad3d10b5ef954e70aa2af50ad17c6..05d2e777fe1c641a12994775d5c5be4daa08f4b4 100644 (file)
     select
       <include refid="componentColumns"/>
     from components p
-    where p.uuid_path in
+    where
+    p.project_uuid = #{branchUuid,jdbcType=VARCHAR}
+    and p.uuid_path in
     <foreach collection="uuidPaths" item="uuidPath" open="(" close=")" separator=",">
         #{uuidPath,jdbcType=VARCHAR}
     </foreach>
index d0dd5e840a93eb1865ff0fecf926fa2ac64d9db0..c7c7d77f643c63f001fac5556064b3fe0e7777e1 100644 (file)
@@ -1684,16 +1684,16 @@ public class ComponentDaoTest {
     db.commit();
 
     // test children of root
-    assertThat(underTest.selectChildren(dbSession, List.of(project))).extracting("uuid").containsOnly(FILE_1_UUID, MODULE_UUID);
+    assertThat(underTest.selectChildren(dbSession, project.uuid(), List.of(project))).extracting("uuid").containsOnly(FILE_1_UUID, MODULE_UUID);
 
     // test children of intermediate component (module here)
-    assertThat(underTest.selectChildren(dbSession, List.of(module))).extracting("uuid").containsOnly(FILE_2_UUID, FILE_3_UUID);
+    assertThat(underTest.selectChildren(dbSession, project.uuid(), List.of(module))).extracting("uuid").containsOnly(FILE_2_UUID, FILE_3_UUID);
 
     // test children of leaf component (file here)
-    assertThat(underTest.selectChildren(dbSession, List.of(fileInProject))).isEmpty();
+    assertThat(underTest.selectChildren(dbSession, project.uuid(), List.of(fileInProject))).isEmpty();
 
     // test children of 2 components
-    assertThat(underTest.selectChildren(dbSession, List.of(project, module))).extracting("uuid").containsOnly(FILE_1_UUID, MODULE_UUID, FILE_2_UUID, FILE_3_UUID);
+    assertThat(underTest.selectChildren(dbSession, project.uuid(), List.of(project, module))).extracting("uuid").containsOnly(FILE_1_UUID, MODULE_UUID, FILE_2_UUID, FILE_3_UUID);
   }
 
   @Test
index 463c0b1790194fe958c2ff72e6a5f09b054c1723..35ce1e1b0a9a6857aeb3a77133c58c82bda05726 100644 (file)
@@ -40,17 +40,17 @@ public class SecurityReviewRating {
     if (total == 0) {
       return Optional.empty();
     }
-    return Optional.of(hotspotsReviewed * 100.0 / total);
+    return Optional.of(hotspotsReviewed * 100.0D / total);
   }
 
   public static Rating computeRating(@Nullable Double percent) {
-    if (percent == null || percent >= 80.0) {
+    if (percent == null || percent >= 80.0D) {
       return A;
-    } else if (percent >= 70.0) {
+    } else if (percent >= 70.0D) {
       return B;
-    } else if (percent >= 50.0) {
+    } else if (percent >= 50.0D) {
       return C;
-    } else if (percent >= 30.0) {
+    } else if (percent >= 30.0D) {
       return D;
     }
     return E;
index 7b5da77499e7eb226492b1581b6bfbefd8eda8ec..1bc345b18b1d696dc06a37413294037d8e0c45b8 100644 (file)
@@ -53,7 +53,7 @@ public class ComponentIndexImpl implements ComponentIndex {
     sortedComponentsToRoot = loadTreeOfComponents(dbSession, touchedComponents);
     branchComponent = findBranchComponent(sortedComponentsToRoot);
     children = new HashMap<>();
-    List<ComponentDto> childComponents = loadChildren(dbSession, sortedComponentsToRoot);
+    List<ComponentDto> childComponents = loadChildren(dbSession, branchComponent.uuid(), sortedComponentsToRoot);
     for (ComponentDto c : childComponents) {
       List<String> uuidPathAsList = c.getUuidPathAsList();
       String parentUuid = uuidPathAsList.get(uuidPathAsList.size() - 1);
@@ -66,8 +66,8 @@ public class ComponentIndexImpl implements ComponentIndex {
       .orElseThrow(() -> new IllegalStateException("No project found in " + components));
   }
 
-  private List<ComponentDto> loadChildren(DbSession dbSession, Collection<ComponentDto> components) {
-    return dbClient.componentDao().selectChildren(dbSession, components);
+  private List<ComponentDto> loadChildren(DbSession dbSession, String branchUuid, Collection<ComponentDto> components) {
+    return dbClient.componentDao().selectChildren(dbSession, branchUuid, components);
   }
 
   private List<ComponentDto> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) {
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java
deleted file mode 100644 (file)
index 8f624cb..0000000
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.server.measure.live;
-
-import java.util.Optional;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.component.ComponentDto;
-
-import static org.sonar.server.security.SecurityReviewRating.computePercent;
-import static org.sonar.server.security.SecurityReviewRating.computeRating;
-
-public class HotspotMeasureUpdater {
-  private final DbClient dbClient;
-
-  public HotspotMeasureUpdater(DbClient dbClient) {
-    this.dbClient = dbClient;
-  }
-  public void apply(DbSession dbSession, MeasureMatrix matrix, ComponentIndex components, boolean useLeakFormulas, long beginningOfLeak) {
-    HotspotsCounter hotspotsCounter = new HotspotsCounter(dbClient.issueDao().selectBranchHotspotsCount(dbSession, components.getBranch().uuid(), beginningOfLeak));
-    ComponentDto branch = components.getBranch();
-
-    long reviewed = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_REVIEWED, false);
-    long toReview = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false);
-    matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, reviewed);
-    matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, toReview);
-    Optional<Double> percent = computePercent(toReview, reviewed);
-    percent.ifPresent(p -> matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY, p));
-    matrix.setValue(branch, CoreMetrics.SECURITY_REVIEW_RATING_KEY, computeRating(percent.orElse(null)));
-
-    if (useLeakFormulas) {
-      reviewed = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_REVIEWED, true);
-      toReview = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true);
-      matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, reviewed);
-      matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, toReview);
-      percent = computePercent(toReview, reviewed);
-      percent.ifPresent(p -> matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, p));
-      matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY, computeRating(percent.orElse(null)));
-    }
-  }
-}
index 595b3c627ed06301d0034020bc16e35753b65ae0..3bd50d0463e76272e6d37a78df66e9b6b8885e27 100644 (file)
@@ -29,7 +29,6 @@ public class LiveMeasureModule extends Module {
       ComponentIndexFactory.class,
       LiveMeasureTreeUpdaterImpl.class,
       LiveMeasureComputerImpl.class,
-      HotspotMeasureUpdater.class,
       LiveQualityGateComputerImpl.class);
   }
 }
index f362cf0b55d1195f0a2d2afa8d008dd248f6bb4d..58a850337573c9a5964bd86235c709b403998e19 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.measure.live;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.measures.Metric;
@@ -36,17 +37,23 @@ import org.sonar.server.measure.DebtRatingGrid;
 import org.sonar.server.measure.Rating;
 
 import static com.google.common.base.Preconditions.checkState;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
 import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 
 public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater {
   private final DbClient dbClient;
   private final MeasureUpdateFormulaFactory formulaFactory;
-  private final HotspotMeasureUpdater hotspotMeasureUpdater;
 
-  public LiveMeasureTreeUpdaterImpl(DbClient dbClient, MeasureUpdateFormulaFactory formulaFactory, HotspotMeasureUpdater hotspotMeasureUpdater) {
+  public LiveMeasureTreeUpdaterImpl(DbClient dbClient, MeasureUpdateFormulaFactory formulaFactory) {
     this.dbClient = dbClient;
     this.formulaFactory = formulaFactory;
-    this.hotspotMeasureUpdater = hotspotMeasureUpdater;
   }
 
   @Override
@@ -59,12 +66,6 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater {
 
     // 2. aggregate new measures up the component tree
     updateMatrixWithHierarchy(measures, components, config, shouldUseLeakFormulas);
-
-    // 3. Count hotspots at root level
-    // this is only necessary because the count of reviewed and to_review hotspots is only saved for the root (not for all components).
-    // For that reason, we can't incrementally generate the new counts up the tree. To have the correct numbers for the root component, we
-    // run this extra step that set the hotspots measures to the root based on the total count of hotspots.
-    hotspotMeasureUpdater.apply(dbSession, measures, components, shouldUseLeakFormulas, beginningOfLeak);
   }
 
   private void updateMatrixWithHierarchy(MeasureMatrix matrix, ComponentIndex components, Configuration config, boolean useLeakFormulas) {
@@ -135,7 +136,7 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater {
       this.debtRatingGrid = debtRatingGrid;
     }
 
-    private void change(ComponentDto component, MeasureUpdateFormula formula) {
+    void change(ComponentDto component, MeasureUpdateFormula formula) {
       this.currentComponent = component;
       this.currentFormula = formula;
     }
@@ -149,6 +150,65 @@ public class LiveMeasureTreeUpdaterImpl implements LiveMeasureTreeUpdater {
         .collect(Collectors.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'.
+     */
+    @Override
+    public long getChildrenHotspotsReviewed() {
+      return getChildrenHotspotsReviewed(LiveMeasureDto::getValue, SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, SECURITY_HOTSPOTS_REVIEWED_KEY, SECURITY_HOTSPOTS_KEY);
+    }
+
+    /**
+     * Some child components may not have the measure 'SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY'. We assume that 'SECURITY_HOTSPOTS_KEY' has the same value.
+     */
+    @Override
+    public long getChildrenHotspotsToReview() {
+      return componentIndex.getChildren(currentComponent)
+        .stream()
+        .map(c -> matrix.getMeasure(c, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).or(() -> matrix.getMeasure(c, SECURITY_HOTSPOTS_KEY)))
+        .mapToLong(lmOpt -> lmOpt.flatMap(lm -> Optional.ofNullable(lm.getValue())).orElse(0D).longValue())
+        .sum();
+    }
+
+    @Override
+    public long getChildrenNewHotspotsReviewed() {
+      return getChildrenHotspotsReviewed(LiveMeasureDto::getVariation, NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, NEW_SECURITY_HOTSPOTS_KEY);
+    }
+
+    /**
+     * Some child components may not have the measure 'NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY'. We assume that 'NEW_SECURITY_HOTSPOTS_KEY' has the same value.
+     */
+    @Override
+    public long getChildrenNewHotspotsToReview() {
+      return componentIndex.getChildren(currentComponent)
+        .stream()
+        .map(c -> matrix.getMeasure(c, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).or(() -> matrix.getMeasure(c, NEW_SECURITY_HOTSPOTS_KEY)))
+        .mapToLong(lmOpt -> lmOpt.flatMap(lm -> Optional.ofNullable(lm.getVariation())).orElse(0D).longValue())
+        .sum();
+    }
+
+    private long getChildrenHotspotsReviewed(Function<LiveMeasureDto, Double> valueFunc, String metricKey, String percMetricKey, String hotspotsMetricKey) {
+      return componentIndex.getChildren(currentComponent)
+        .stream()
+        .mapToLong(c -> getHotspotsReviewed(c, valueFunc, metricKey, percMetricKey, hotspotsMetricKey))
+        .sum();
+    }
+
+    private long getHotspotsReviewed(ComponentDto c, Function<LiveMeasureDto, Double> valueFunc, String metricKey, String percMetricKey, String hotspotsMetricKey) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(c, metricKey);
+      return measure.map(lm -> Optional.ofNullable(valueFunc.apply(lm)).orElse(0D).longValue())
+        .orElseGet(() -> matrix.getMeasure(c, percMetricKey)
+          .flatMap(percentage -> matrix.getMeasure(c, hotspotsMetricKey)
+            .map(hotspots -> {
+              double perc = Optional.ofNullable(valueFunc.apply(percentage)).orElse(0D) / 100D;
+              double toReview = Optional.ofNullable(valueFunc.apply(hotspots)).orElse(0D);
+              double reviewed = (toReview * perc) / (1D - perc);
+              return Math.round(reviewed);
+            }))
+          .orElse(0L));
+    }
+
     public List<Double> getChildrenLeakValues() {
       List<ComponentDto> children = componentIndex.getChildren(currentComponent);
       return children.stream()
index 420a5a94598b71fe0413d9e2c2d4296e26a9a498..143f397e6cdba7983d04b0f7a220787c93adc652 100644 (file)
@@ -23,6 +23,7 @@ import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.BiConsumer;
+import java.util.stream.DoubleStream;
 import org.sonar.api.measures.Metric;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.server.measure.DebtRatingGrid;
@@ -79,6 +80,14 @@ class MeasureUpdateFormula {
   interface Context {
     List<Double> getChildrenValues();
 
+    long getChildrenHotspotsReviewed();
+
+    long getChildrenHotspotsToReview();
+
+    long getChildrenNewHotspotsReviewed();
+
+    long getChildrenNewHotspotsToReview();
+
     List<Double> getChildrenLeakValues();
 
     ComponentDto getComponent();
index 02a0ee5a0f8dd9705e007dfae28d5a281d9c825d..21f4e843b2755419e695bca5c93b955c12bc50ba 100644 (file)
@@ -119,10 +119,12 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
     new MeasureUpdateFormula(CoreMetrics.SECURITY_RATING, false, new MaxRatingChildren(),
       (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))),
 
-    new MeasureUpdateFormula(SECURITY_HOTSPOTS_REVIEWED_STATUS, false, new AddChildren(),
+    new MeasureUpdateFormula(SECURITY_HOTSPOTS_REVIEWED_STATUS, false,
+      (context, formula) -> context.setValue(context.getValue(SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D) + context.getChildrenHotspotsReviewed()),
       (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))),
 
-    new MeasureUpdateFormula(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false, new AddChildren(),
+    new MeasureUpdateFormula(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false,
+      (context, formula) -> context.setValue(context.getValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D) + context.getChildrenHotspotsToReview()),
       (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false))),
 
     new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false,
@@ -193,10 +195,12 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
         context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
       }),
 
-    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true, new AddChildren(),
+    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true,
+      (context, formula) -> context.setLeakValue(context.getLeakValue(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D) + context.getChildrenNewHotspotsReviewed()),
       (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))),
 
-    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true, new AddChildren(),
+    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true,
+      (context, formula) -> context.setLeakValue(context.getLeakValue(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D) + context.getChildrenNewHotspotsToReview()),
       (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true))),
 
     new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED, true,
@@ -239,7 +243,7 @@ public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFact
 
   private static double newDebtDensity(MeasureUpdateFormula.Context context) {
     double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0D), 0.0D);
-    Optional<Double> devCost = context.getText(CoreMetrics.NEW_DEVELOPMENT_COST).map(Double::parseDouble);
+    Optional<Double> devCost = context.getLeakValue(CoreMetrics.NEW_DEVELOPMENT_COST);
     if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
       return debt / devCost.get();
     }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java
deleted file mode 100644 (file)
index 64a876b..0000000
+++ /dev/null
@@ -1,119 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2022 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.server.measure.live;
-
-import java.util.Date;
-import java.util.List;
-import org.assertj.core.data.Offset;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.sonar.db.DbTester;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
-import org.sonar.db.metric.MetricDto;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
-import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY;
-import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY;
-import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
-import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
-import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY;
-
-public class HotspotMeasureUpdaterTest {
-  @Rule
-  public DbTester db = DbTester.create();
-
-  private final HotspotMeasureUpdater hotspotMeasureUpdater = new HotspotMeasureUpdater(db.getDbClient());
-  private ComponentIndexImpl componentIndex;
-  private MeasureMatrix matrix;
-  private ComponentDto project;
-  private ComponentDto dir;
-  private ComponentDto file1;
-  private ComponentDto file2;
-
-  @Before
-  public void setUp() {
-    // insert project and file structure
-    project = db.components().insertPrivateProject();
-    dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java"));
-    file1 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir));
-    file2 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir));
-
-    // other needed data
-    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), insertHotspotMetrics(), List.of());
-    componentIndex = new ComponentIndexImpl(db.getDbClient());
-  }
-
-  private List<MetricDto> insertHotspotMetrics() {
-    return List.of(
-      db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)),
-      db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)),
-      db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_REVIEWED_KEY)),
-      db.measures().insertMetric(m -> m.setKey(SECURITY_REVIEW_RATING_KEY)),
-
-      db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)),
-      db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)),
-      db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY)),
-      db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_REVIEW_RATING_KEY))
-    );
-  }
-
-  @Test
-  public void should_count_hotspots_for_root() {
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(999)).setStatus("TO_REVIEW"));
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(999)).setStatus("REVIEWED"));
-
-    db.issues().insertHotspot(project, dir, i -> i.setIssueCreationDate(new Date(1001)).setStatus("TO_REVIEW"));
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1002)).setStatus("REVIEWED"));
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1003)).setStatus("REVIEWED"));
-
-    componentIndex.load(db.getSession(), List.of(file1));
-    hotspotMeasureUpdater.apply(db.getSession(), matrix, componentIndex, true, 1000L);
-
-    assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).get().getValue()).isEqualTo(3d);
-    assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).get().getValue()).isEqualTo(2d);
-    assertThat(matrix.getMeasure(project, SECURITY_REVIEW_RATING_KEY).get().getDataAsString()).isEqualTo("C");
-    assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_REVIEWED_KEY).get().getValue()).isEqualTo(60d);
-
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).get().getVariation()).isEqualTo(2d);
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).get().getVariation()).isEqualTo(1d);
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_REVIEW_RATING_KEY).get().getVariation()).isEqualTo(3);
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY).get().getVariation()).isCloseTo(66d, Offset.offset(1d));
-  }
-
-  @Test
-  public void dont_create_leak_measures_if_no_leak_period() {
-    db.issues().insertHotspot(project, dir, i -> i.setIssueCreationDate(new Date(1001)).setStatus("TO_REVIEW"));
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1002)).setStatus("REVIEWED"));
-    db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1003)).setStatus("REVIEWED"));
-
-    componentIndex.load(db.getSession(), List.of(file1));
-    hotspotMeasureUpdater.apply(db.getSession(), matrix, componentIndex, false, 1000L);
-
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)).isEmpty();
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)).isEmpty();
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_REVIEW_RATING_KEY)).isEmpty();
-    assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY)).isEmpty();
-  }
-}
index 2f0413de2e9f8649467fc3b3c0b2b75a60e1d119..ab72d88b1617d0e832127d2a19be2957fd2f1176 100644 (file)
@@ -28,6 +28,7 @@ import org.junit.Test;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.config.internal.MapSettings;
 import org.sonar.api.measures.Metric;
+import org.sonar.api.rules.RuleType;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.BranchDto;
 import org.sonar.db.component.BranchType;
@@ -40,19 +41,24 @@ import org.sonar.db.metric.MetricDto;
 import org.sonar.db.newcodeperiod.NewCodePeriodType;
 import org.sonar.db.rule.RuleDto;
 
+import static java.util.Collections.emptyList;
+import static java.util.Collections.emptySet;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
 import static org.sonar.api.CoreProperties.RATING_GRID;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY;
+import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY;
 
 public class LiveMeasureTreeUpdaterImplTest {
   @Rule
   public DbTester db = DbTester.create();
 
   private final Configuration config = new MapSettings().setProperty(RATING_GRID, "0.05,0.1,0.2,0.5").asConfig();
-  private final HotspotMeasureUpdater hotspotMeasureUpdater = mock(HotspotMeasureUpdater.class);
   private LiveMeasureTreeUpdaterImpl treeUpdater;
   private ComponentIndexImpl componentIndex;
   private MeasureMatrix matrix;
@@ -84,7 +90,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void should_aggregate_values_up_the_hierarchy() {
     snapshot = db.components().insertSnapshot(project);
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new AggregateValuesFormula(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new AggregateValuesFormula());
 
     componentIndex.load(db.getSession(), List.of(file1));
     List<LiveMeasureDto> initialValues = List.of(
@@ -106,7 +112,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void should_set_values_up_the_hierarchy() {
     snapshot = db.components().insertSnapshot(project);
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new SetValuesFormula(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new SetValuesFormula());
 
     componentIndex.load(db.getSession(), List.of(file1));
     treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
@@ -121,7 +127,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void dont_use_leak_formulas_if_no_period() {
     snapshot = db.components().insertSnapshot(project, s -> s.setPeriodDate(null));
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak());
 
     componentIndex.load(db.getSession(), List.of(file1));
     treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
@@ -133,7 +139,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   public void use_leak_formulas_if_pr() {
     snapshot = db.components().insertSnapshot(project, s -> s.setPeriodDate(null));
     branch.setBranchType(BranchType.PULL_REQUEST);
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak());
 
     componentIndex.load(db.getSession(), List.of(file1));
     treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
@@ -144,7 +150,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void calculate_new_metrics_if_using_new_code_branch_reference() {
     snapshot = db.components().insertSnapshot(project, s -> s.setPeriodMode(NewCodePeriodType.REFERENCE_BRANCH.name()));
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak());
 
     componentIndex.load(db.getSession(), List.of(file1));
     treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
@@ -155,9 +161,9 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void issue_counter_based_on_new_code_branch_reference() {
     snapshot = db.components().insertSnapshot(project, s -> s.setPeriodMode(NewCodePeriodType.REFERENCE_BRANCH.name()));
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak());
 
-    RuleDto rule = db.rules().insert();
+    RuleDto rule = db.rules().insert(r -> r.setType(RuleType.BUG));
     IssueDto issue1 = db.issues().insertIssue(rule, project, file1);
     IssueDto issue2 = db.issues().insertIssue(rule, project, file1);
     db.issues().insertNewCodeReferenceIssue(issue1);
@@ -170,7 +176,7 @@ public class LiveMeasureTreeUpdaterImplTest {
   @Test
   public void issue_counter_uses_begin_of_leak() {
     snapshot = db.components().insertSnapshot(project, s -> s.setPeriodDate(1000L));
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak());
 
     db.issues().insertIssue(i -> i.setIssueCreationDate(new Date(999)).setComponentUuid(file1.uuid()));
     db.issues().insertIssue(i -> i.setIssueCreationDate(new Date(1001)).setComponentUuid(file1.uuid()));
@@ -183,13 +189,64 @@ public class LiveMeasureTreeUpdaterImplTest {
   }
 
   @Test
-  public void calls_hotspot_updater() {
-    snapshot = db.components().insertSnapshot(project, s -> s.setPeriodDate(1000L));
+  public void context_calculates_hotspot_counts_from_percentage() {
+    List<MetricDto> metrics = List.of(new MetricDto().setKey(SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY),
+      new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY));
+    componentIndex.load(db.getSession(), List.of(file1));
+    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of());
+
+    LiveMeasureTreeUpdaterImpl.FormulaContextImpl context = new LiveMeasureTreeUpdaterImpl.FormulaContextImpl(matrix, componentIndex, null);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_KEY, 4d);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_REVIEWED_KEY, 33d);
+
+    matrix.setValue(file2, SECURITY_HOTSPOTS_KEY, 2d);
+    matrix.setValue(file2, SECURITY_HOTSPOTS_REVIEWED_KEY, 50d);
+
+    context.change(dir, null);
+    assertThat(context.getChildrenHotspotsToReview()).isEqualTo(6);
+    assertThat(context.getChildrenHotspotsReviewed()).isEqualTo(4);
+  }
 
+  @Test
+  public void context_calculates_new_hotspot_counts_from_percentage() {
+    List<MetricDto> metrics = List.of(new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY),
+      new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY));
     componentIndex.load(db.getSession(), List.of(file1));
-    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new CountUnresolvedInLeak(), hotspotMeasureUpdater);
-    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
-    verify(hotspotMeasureUpdater).apply(eq(db.getSession()), any(), eq(componentIndex), eq(true), eq(1000L));
+    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of());
+
+    LiveMeasureTreeUpdaterImpl.FormulaContextImpl context = new LiveMeasureTreeUpdaterImpl.FormulaContextImpl(matrix, componentIndex, null);
+    matrix.setLeakValue(file1, NEW_SECURITY_HOTSPOTS_KEY, 4d);
+    matrix.setLeakValue(file1, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, 33d);
+
+    matrix.setLeakValue(file2, NEW_SECURITY_HOTSPOTS_KEY, 2d);
+    matrix.setLeakValue(file2, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, 50d);
+
+    context.change(dir, null);
+    assertThat(context.getChildrenNewHotspotsToReview()).isEqualTo(6);
+    assertThat(context.getChildrenNewHotspotsReviewed()).isEqualTo(4);
+  }
+
+  @Test
+  public void context_returns_hotspots_counts_from_measures() {
+    List<MetricDto> metrics = List.of(new MetricDto().setKey(SECURITY_HOTSPOTS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_KEY),
+      new MetricDto().setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY), new MetricDto().setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY));
+    componentIndex.load(db.getSession(), List.of(file1));
+    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), metrics, List.of());
+
+    LiveMeasureTreeUpdaterImpl.FormulaContextImpl context = new LiveMeasureTreeUpdaterImpl.FormulaContextImpl(matrix, componentIndex, null);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, 5D);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, 5D);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_KEY, 6d);
+    matrix.setValue(file1, SECURITY_HOTSPOTS_REVIEWED_KEY, 33d);
+
+    matrix.setValue(file2, SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, 5D);
+    matrix.setValue(file2, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, 5D);
+    matrix.setValue(file2, SECURITY_HOTSPOTS_KEY, 4d);
+    matrix.setValue(file2, SECURITY_HOTSPOTS_REVIEWED_KEY, 50d);
+
+    context.change(dir, null);
+    assertThat(context.getChildrenHotspotsToReview()).isEqualTo(10);
+    assertThat(context.getChildrenHotspotsReviewed()).isEqualTo(10);
   }
 
   private class AggregateValuesFormula implements MeasureUpdateFormulaFactory {
@@ -205,6 +262,18 @@ public class LiveMeasureTreeUpdaterImplTest {
     }
   }
 
+  private class NoOpFormula implements MeasureUpdateFormulaFactory {
+
+    @Override
+    public List<MeasureUpdateFormula> getFormulas() {
+      return emptyList();
+    }
+
+    @Override public Set<Metric> getFormulaMetrics() {
+      return emptySet();
+    }
+  }
+
   private class SetValuesFormula implements MeasureUpdateFormulaFactory {
     @Override
     public List<MeasureUpdateFormula> getFormulas() {
index bf7548dbb6edf5a57e23f01b5924727ae95457ee..c68cc2c88130fde93715e052cc3764f281544c52 100644 (file)
@@ -102,6 +102,26 @@ public class MeasureUpdateFormulaFactoryImplTest {
 
   @Test
   public void hierarchy_combining_other_metrics() {
+    new HierarchyTester(SECURITY_HOTSPOTS_TO_REVIEW_STATUS)
+      .withValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1d)
+      .withChildrenHotspotsCounts(10, 10, 2, 10)
+      .expectedResult(3d);
+
+    new HierarchyTester(SECURITY_HOTSPOTS_REVIEWED_STATUS)
+      .withValue(SECURITY_HOTSPOTS_REVIEWED_STATUS, 1d)
+      .withChildrenHotspotsCounts(2, 10, 10, 10)
+      .expectedResult(3d);
+
+    new HierarchyTester(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS)
+      .withValue(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1d)
+      .withChildrenHotspotsCounts(10, 10, 10, 2)
+      .expectedResult(3d);
+
+    new HierarchyTester(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS)
+      .withValue(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 1d)
+      .withChildrenHotspotsCounts(10, 2, 10, 10)
+      .expectedResult(3d);
+
     new HierarchyTester(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED)
       .withValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1d)
       .withValue(SECURITY_HOTSPOTS_REVIEWED_STATUS, 1d)
@@ -818,44 +838,44 @@ public class MeasureUpdateFormulaFactoryImplTest {
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
 
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "160")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 160.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 12.5)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.C);
 
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "10")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 10.0D)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 200.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.E);
 
     // A is 5% --> min debt is exactly 200*0.05=10
-    with(CoreMetrics.NEW_DEVELOPMENT_COST, "200")
+    withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 200.0)
       .andLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 10.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 5.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
 
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "0")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
 
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "80")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0);
 
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "0")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
 
     // bug, debt can't be negative
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "80")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
 
     // bug, cost can't be negative
     withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
-      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "-80")
+      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, -80.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
       .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
   }
@@ -992,11 +1012,33 @@ public class MeasureUpdateFormulaFactoryImplTest {
       this.initialValues = initialValues;
     }
 
-    @Override public List<Double> getChildrenValues() {
+    @Override
+    public List<Double> getChildrenValues() {
       return initialValues.childrenValues;
     }
 
-    @Override public List<Double> getChildrenLeakValues() {
+    @Override
+    public long getChildrenHotspotsReviewed() {
+      return initialValues.childrenHotspotsReviewed;
+    }
+
+    @Override
+    public long getChildrenHotspotsToReview() {
+      return initialValues.childrenHotspotsToReview;
+    }
+
+    @Override
+    public long getChildrenNewHotspotsReviewed() {
+      return initialValues.childrenNewHotspotsReviewed;
+    }
+
+    @Override
+    public long getChildrenNewHotspotsToReview() {
+      return initialValues.childrenNewHotspotsToReview;
+    }
+
+    @Override
+    public List<Double> getChildrenLeakValues() {
       return initialValues.childrenLeakValues;
     }
 
@@ -1067,6 +1109,11 @@ public class MeasureUpdateFormulaFactoryImplTest {
     private final List<Double> childrenValues = new ArrayList<>();
     private final List<Double> childrenLeakValues = new ArrayList<>();
     private final Map<Metric, String> text = new HashMap<>();
+    private long childrenHotspotsReviewed = 0;
+    private long childrenNewHotspotsReviewed = 0;
+    private long childrenHotspotsToReview = 0;
+    private long childrenNewHotspotsToReview = 0;
+
   }
 
   private class HierarchyTester {
@@ -1089,6 +1136,15 @@ public class MeasureUpdateFormulaFactoryImplTest {
       return this;
     }
 
+    public HierarchyTester withChildrenHotspotsCounts(long childrenHotspotsReviewed, long childrenNewHotspotsReviewed, long childrenHotspotsToReview,
+      long childrenNewHotspotsToReview) {
+      this.initialValues.childrenHotspotsReviewed = childrenHotspotsReviewed;
+      this.initialValues.childrenNewHotspotsReviewed = childrenNewHotspotsReviewed;
+      this.initialValues.childrenHotspotsToReview = childrenHotspotsToReview;
+      this.initialValues.childrenNewHotspotsToReview = childrenNewHotspotsToReview;
+      return this;
+    }
+
     public HierarchyTester withValue(Double value) {
       return withValue(metric, value);
     }