]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11401 Performance hotspot when changing state of issue
authorDuarte Meneses <duarte.meneses@sonarsource.com>
Wed, 25 May 2022 14:22:34 +0000 (16:22 +0200)
committersonartech <sonartech@sonarsource.com>
Fri, 10 Jun 2022 08:15:07 +0000 (08:15 +0000)
39 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/java/org/sonar/db/issue/HotspotGroupDto.java [new file with mode: 0644]
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java [deleted file]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
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/LiveMeasureTreeUpdater.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java [deleted file]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java [new file with mode: 0644]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java [deleted file]
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java [new file with mode: 0644]

index 08ad6fcecdbc46d9f18d3f8174f6f977637ee521..98655ebf25cf795b57c367907b592d14831f508f 100644 (file)
@@ -29,6 +29,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.apache.ibatis.session.ResultHandler;
@@ -223,6 +224,11 @@ 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) {
+    Set<String> uuidPaths = components.stream().map(c -> c.getUuidPath() + c.uuid() + ".").collect(Collectors.toSet());
+    return mapper(dbSession).selectChildren(uuidPaths);
+  }
+
   public ComponentDto selectOrFailByKey(DbSession session, String key) {
     Optional<ComponentDto> component = selectByKey(session, key);
     if (!component.isPresent()) {
index 18bb66f79a4bc3a281596499672f0bada5048945..f517bddea89b510c70c967a9a6c2375444e191a6 100644 (file)
@@ -69,6 +69,8 @@ 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);
+
   /**
    * Returns all enabled projects (Scope {@link org.sonar.api.resources.Scopes#PROJECT} and qualifier
    * {@link org.sonar.api.resources.Qualifiers#PROJECT}) no matter if they are ghost project, provisioned projects or
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java
new file mode 100644 (file)
index 0000000..bdd7889
--- /dev/null
@@ -0,0 +1,53 @@
+/*
+ * 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.db.issue;
+
+public class HotspotGroupDto {
+  private String status;
+  private long count;
+  private boolean inLeak;
+
+  public String getStatus() {
+    return status;
+  }
+
+  public HotspotGroupDto setStatus(String status) {
+    this.status = status;
+    return this;
+  }
+
+  public long getCount() {
+    return count;
+  }
+
+  public HotspotGroupDto setCount(long count) {
+    this.count = count;
+    return this;
+  }
+
+  public boolean isInLeak() {
+    return inLeak;
+  }
+
+  public HotspotGroupDto setInLeak(boolean inLeak) {
+    this.inLeak = inLeak;
+    return this;
+  }
+}
index fd64c2c90b6a4ef8511a5b18923abe908d95ee2c..454a2420d16b47d9585b34b62fbd44800131e31e 100644 (file)
@@ -87,8 +87,12 @@ public class IssueDao implements Dao {
     return executeLargeInputs(componentUuids, mapper(dbSession)::selectOpenByComponentUuids);
   }
 
-  public Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(DbSession dbSession, ComponentDto baseComponent, long leakPeriodBeginningDate) {
-    return mapper(dbSession).selectIssueGroupsByBaseComponent(baseComponent, leakPeriodBeginningDate);
+  public Collection<HotspotGroupDto> selectBranchHotspotsCount(DbSession dbSession, String branchUuid, long leakPeriodBeginningDate) {
+    return mapper(dbSession).selectBranchHotspotsCount(branchUuid, leakPeriodBeginningDate);
+  }
+
+  public Collection<IssueGroupDto> selectIssueGroupsByComponent(DbSession dbSession, ComponentDto component, long leakPeriodBeginningDate) {
+    return mapper(dbSession).selectIssueGroupsByComponent(component, leakPeriodBeginningDate);
   }
 
   public void insert(DbSession session, IssueDto dto) {
index ba55992a0ab7aa00dac357f570a9210a81508478..bd3db65bb67cf065c8310df9caf638790c90a7fa 100644 (file)
@@ -64,9 +64,9 @@ public interface IssueMapper {
 
   List<IssueDto> selectNonClosedByModuleOrProject(@Param("projectUuid") String projectUuid, @Param("likeModuleUuidPath") String likeModuleUuidPath);
 
-  Collection<IssueGroupDto> selectIssueGroupsByBaseComponent(
-    @Param("baseComponent") ComponentDto baseComponent,
-    @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
+  Collection<HotspotGroupDto> selectBranchHotspotsCount(@Param("rootUuid") String rootUuid, @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
+
+  Collection<IssueGroupDto> selectIssueGroupsByComponent(@Param("component") ComponentDto component, @Param("leakPeriodBeginningDate") long leakPeriodBeginningDate);
 
 
   List<IssueDto> selectByBranch(@Param("queryParams") IssueQueryParams issueQueryParams,
index c93b10d97714f7e151338ceee30c9b3db7c5e52a..efacf0809b4ad3d10b5ef954e70aa2af50ad17c6 100644 (file)
     </if>
   </sql>
 
+  <select id="selectChildren" resultType="Component">
+    select
+      <include refid="componentColumns"/>
+    from components p
+    where p.uuid_path in
+    <foreach collection="uuidPaths" item="uuidPath" open="(" close=")" separator=",">
+        #{uuidPath,jdbcType=VARCHAR}
+    </foreach>
+    and p.enabled = ${_true}
+  </select>
+
   <select id="selectDescendants" resultType="Component">
     select
       <include refid="componentColumns"/>
index 5851f05ab2cf2fb1979264c3cbb3d2df55d6f5b3..b56fde8abcb85dc54b952801fe94132f894cac89 100644 (file)
     i.issue_type &lt;&gt; 4
   </select>
 
-  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map">
+  <select id="selectBranchHotspotsCount" resultType="org.sonar.db.issue.HotspotGroupDto" parameterType="map">
+    select i.status as status, count(i.status) as "count",
+    <if test="leakPeriodBeginningDate &gt;= 0">
+      (i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak
+    </if>
+    <if test="leakPeriodBeginningDate &lt; 0">
+      CASE WHEN n.uuid is null THEN false ELSE true END as inLeak
+    </if>
+    from issues i
+    <if test="leakPeriodBeginningDate &lt; 0">
+      left join new_code_reference_issues n on n.issue_key = i.kee
+    </if>
+    where i.project_uuid = #{rootUuid,jdbcType=VARCHAR}
+    and i.status !='CLOSED'
+    and i.issue_type = 4
+    group by i.status, inLeak
+  </select>
+
+  <select id="selectBranchHotspotsCount" resultType="org.sonar.db.issue.HotspotGroupDto" parameterType="map" databaseId="oracle">
+    select i2.status as status, count(i2.status) as "count", i2.inLeak as inLeak
+    from (
+      select i.status,
+      <if test="leakPeriodBeginningDate &gt;= 0">
+       case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
+      </if>
+      <if test="leakPeriodBeginningDate &lt; 0">
+        case when n.uuid is null then 0 else 1 end as inLeak
+      </if>
+      from issues i
+      <if test="leakPeriodBeginningDate &lt; 0">
+        left join new_code_reference_issues n on n.issue_key = i.kee
+      </if>
+      where i.project_uuid = #{rootUuid,jdbcType=VARCHAR}
+      and i.status !='CLOSED'
+      and i.issue_type = 4
+    ) i2
+    group by i2.status, i2.inLeak
+  </select>
+
+  <select id="selectBranchHotspotsCount" resultType="org.sonar.db.issue.HotspotGroupDto" parameterType="map" databaseId="mssql">
+    select i2.status as status, count(i2.status) as "count", i2.inLeak as inLeak
+    from (
+      select i.status,
+      <if test="leakPeriodBeginningDate &gt;= 0">
+        case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
+      </if>
+      <if test="leakPeriodBeginningDate &lt; 0">
+        case when n.uuid is null then 0 else 1 end as inLeak
+      </if>
+      from issues i
+      <if test="leakPeriodBeginningDate &lt; 0">
+        left join new_code_reference_issues n on n.issue_key = i.kee
+      </if>
+      where i.project_uuid = #{rootUuid,jdbcType=VARCHAR}
+      and i.status !='CLOSED'
+      and i.issue_type = 4
+    ) i2
+    group by i2.status, i2.inLeak
+  </select>
+
+  <select id="selectIssueGroupsByComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map">
     select i.issue_type as ruleType, i.severity as severity, i.resolution as resolution, i.status as status, sum(i.effort) as effort, count(i.issue_type) as "count",
     <if test="leakPeriodBeginningDate &gt;= 0">
       (i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak
       CASE WHEN n.uuid is null THEN false ELSE true END as inLeak
     </if>
     from issues i
-    inner join components p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
-    left join new_code_reference_issues n on n.issue_key = i.kee
+    <if test="leakPeriodBeginningDate &lt; 0">
+      left join new_code_reference_issues n on n.issue_key = i.kee
+    </if>
     where i.status !='CLOSED'
-    and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
-    and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+    and i.component_uuid = #{component.uuid,jdbcType=VARCHAR}
     group by i.issue_type, i.severity, i.resolution, i.status, inLeak
   </select>
 
-  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="oracle">
+  <select id="selectIssueGroupsByComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="oracle">
     select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
     from (
       select i.issue_type, i.severity, i.resolution, i.status, i.effort,
-    <if test="leakPeriodBeginningDate &gt;= 0">
+      <if test="leakPeriodBeginningDate &gt;= 0">
         case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
-    </if>
-    <if test="leakPeriodBeginningDate &lt; 0">
-      case when n.uuid is null then 0 else 1 end as inLeak
-    </if>
+      </if>
+      <if test="leakPeriodBeginningDate &lt; 0">
+        case when n.uuid is null then 0 else 1 end as inLeak
+      </if>
       from issues i
-      inner join components p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
-      left join new_code_reference_issues n on n.issue_key = i.kee
+      <if test="leakPeriodBeginningDate &lt; 0">
+        left join new_code_reference_issues n on n.issue_key = i.kee
+      </if>
       where i.status !='CLOSED'
-      and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
-      and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+      and i.component_uuid = #{component.uuid,jdbcType=VARCHAR}
     ) i2
     group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
   </select>
 
-  <select id="selectIssueGroupsByBaseComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="mssql">
+  <select id="selectIssueGroupsByComponent" resultType="org.sonar.db.issue.IssueGroupDto" parameterType="map" databaseId="mssql">
     select i2.issue_type as ruleType, i2.severity as severity, i2.resolution as resolution, i2.status as status, sum(i2.effort) as effort, count(i2.issue_type) as "count", i2.inLeak as inLeak
     from (
-    select i.issue_type, i.severity, i.resolution, i.status, i.effort,
-    <if test="leakPeriodBeginningDate &gt;= 0">
-    case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
-    </if>
-    <if test="leakPeriodBeginningDate &lt; 0">
-      case when n.uuid is null then 0 else 1 end as inLeak
-    </if>
-    from issues i
-    inner join components p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid
-    left join new_code_reference_issues n on n.issue_key = i.kee
-    where i.status !='CLOSED'
-    and i.project_uuid = #{baseComponent.projectUuid,jdbcType=VARCHAR}
-    and (p.uuid_path like #{baseComponent.uuidPathLikeIncludingSelf,jdbcType=VARCHAR} escape '/' or p.uuid = #{baseComponent.uuid,jdbcType=VARCHAR})
+      select i.issue_type, i.severity, i.resolution, i.status, i.effort,
+      <if test="leakPeriodBeginningDate &gt;= 0">
+      case when i.issue_creation_date &gt; #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak
+      </if>
+      <if test="leakPeriodBeginningDate &lt; 0">
+        case when n.uuid is null then 0 else 1 end as inLeak
+      </if>
+      from issues i
+      <if test="leakPeriodBeginningDate &lt; 0">
+        left join new_code_reference_issues n on n.issue_key = i.kee
+      </if>
+      where i.status !='CLOSED'
+      and i.component_uuid = #{component.uuid,jdbcType=VARCHAR}
     ) i2
     group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak
   </select>
     left join new_code_reference_issues n on i.kee = n.issue_key
   </select>
 
+
   <sql id="selectByBranchColumnsFinal">
     result.kee as kee,
     result.ruleUuid as ruleUuid,
index e47fd35065bdeb19d4da2539e5c85cbf8fbd3191..d0dd5e840a93eb1865ff0fecf926fa2ac64d9db0 100644 (file)
@@ -1669,6 +1669,33 @@ public class ComponentDaoTest {
     assertThat(ancestors).extracting("uuid").containsExactly(PROJECT_UUID, MODULE_UUID);
   }
 
+  @Test
+  public void select_children() {
+    ComponentDto project = newPrivateProjectDto(PROJECT_UUID);
+    db.components().insertProjectAndSnapshot(project);
+    ComponentDto module = newModuleDto(MODULE_UUID, project);
+    db.components().insertComponent(module);
+    ComponentDto fileInProject = newFileDto(project, null, FILE_1_UUID).setDbKey("file-key-1").setName("File One");
+    db.components().insertComponent(fileInProject);
+    ComponentDto file1InModule = newFileDto(module, null, FILE_2_UUID).setDbKey("file-key-2").setName("File Two");
+    db.components().insertComponent(file1InModule);
+    ComponentDto file2InModule = newFileDto(module, null, FILE_3_UUID).setDbKey("file-key-3").setName("File Three");
+    db.components().insertComponent(file2InModule);
+    db.commit();
+
+    // test children of root
+    assertThat(underTest.selectChildren(dbSession, 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);
+
+    // test children of leaf component (file here)
+    assertThat(underTest.selectChildren(dbSession, 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);
+  }
+
   @Test
   public void select_descendants_with_children_strategy() {
     // project has 2 children: module and file 1. Other files are part of module.
index 393798a70124f77699790d3030c23fc67d62b074..33690a61a5592a25ae6eb245892860c608eec9aa 100644 (file)
@@ -60,7 +60,6 @@ public class IssueDaoTest {
   private static final RuleDto RULE = RuleTesting.newXooX1();
   private static final String ISSUE_KEY1 = "I1";
   private static final String ISSUE_KEY2 = "I2";
-  private static final String DEFAULT_BRANCH_NAME = "master";
 
   private static final RuleType[] RULE_TYPES_EXCEPT_HOTSPOT = Stream.of(RuleType.values())
     .filter(r -> r != RuleType.SECURITY_HOTSPOT)
@@ -167,11 +166,11 @@ public class IssueDaoTest {
 
     assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), file.uuid()))
       .extracting(IssueDto::getKey)
-      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
+      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile}).map(IssueDto::getKey).toArray(String[]::new));
 
     assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), project.uuid()))
       .extracting(IssueDto::getKey)
-      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
 
     assertThat(underTest.selectNonClosedByComponentUuidExcludingExternalsAndSecurityHotspots(db.getSession(), "does_not_exist")).isEmpty();
   }
@@ -199,11 +198,11 @@ public class IssueDaoTest {
     assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), project))
       .extracting(IssueDto::getKey)
       .containsExactlyInAnyOrder(
-        Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
+        Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule, openIssueOnProject}).map(IssueDto::getKey).toArray(String[]::new));
 
     assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), module))
       .extracting(IssueDto::getKey)
-      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[]{openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
+      .containsExactlyInAnyOrder(Arrays.stream(new IssueDto[] {openIssue1OnFile, openIssue2OnFile, openIssueOnModule}).map(IssueDto::getKey).toArray(String[]::new));
 
     ComponentDto notPersisted = ComponentTesting.newPrivateProjectDto();
     assertThat(underTest.selectNonClosedByModuleOrProjectExcludingExternalsAndSecurityHotspots(db.getSession(), notPersisted)).isEmpty();
@@ -261,11 +260,21 @@ public class IssueDaoTest {
   }
 
   @Test
-  public void test_selectGroupsOfComponentTreeOnLeak_on_component_without_issues() {
+  public void test_selectIssueGroupsByComponent_on_component_without_issues() {
     ComponentDto project = db.components().insertPublicProject();
     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
 
-    Collection<IssueGroupDto> groups = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);
+    Collection<IssueGroupDto> groups = underTest.selectIssueGroupsByComponent(db.getSession(), file, 1_000L);
+
+    assertThat(groups).isEmpty();
+  }
+
+  @Test
+  public void test_selectBranchHotspotsCount_on_component_without_issues() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+
+    Collection<HotspotGroupDto> groups = underTest.selectBranchHotspotsCount(db.getSession(), project.uuid(), 1_000L);
 
     assertThat(groups).isEmpty();
   }
@@ -303,7 +312,7 @@ public class IssueDaoTest {
   }
 
   @Test
-  public void selectGroupsOfComponentTreeOnLeak_on_file() {
+  public void selectIssueGroupsByComponent_on_file() {
     ComponentDto project = db.components().insertPublicProject();
     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
     RuleDto rule = db.rules().insert();
@@ -317,7 +326,7 @@ public class IssueDaoTest {
     IssueDto closed = db.issues().insert(rule, project, file,
       i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
 
-    Collection<IssueGroupDto> result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 1_000L);
+    Collection<IssueGroupDto> result = underTest.selectIssueGroupsByComponent(db.getSession(), file, 1_000L);
 
     assertThat(result.stream().mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
 
@@ -336,17 +345,17 @@ public class IssueDaoTest {
     assertThat(result.stream().filter(g -> "FALSE-POSITIVE".equals(g.getResolution())).mapToLong(IssueGroupDto::getCount).sum()).isOne();
     assertThat(result.stream().filter(g -> g.getResolution() == null).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2);
 
-    assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
+    assertThat(result.stream().filter(IssueGroupDto::isInLeak).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
     assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isZero();
 
     // test leak
-    result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 999_999_999L);
-    assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isZero();
+    result = underTest.selectIssueGroupsByComponent(db.getSession(), file, 999_999_999L);
+    assertThat(result.stream().filter(IssueGroupDto::isInLeak).mapToLong(IssueGroupDto::getCount).sum()).isZero();
     assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
 
     // test leak using exact creation time of criticalBug2 issue
-    result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, criticalBug2.getIssueCreationTime());
-    assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isZero();
+    result = underTest.selectIssueGroupsByComponent(db.getSession(), file, criticalBug2.getIssueCreationTime());
+    assertThat(result.stream().filter(IssueGroupDto::isInLeak).mapToLong(IssueGroupDto::getCount).sum()).isZero();
     assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3);
   }
 
@@ -356,21 +365,21 @@ public class IssueDaoTest {
     ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
     RuleDto rule = db.rules().insert();
     IssueDto fpBug = db.issues().insert(rule, project, file,
-        i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG));
+      i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG));
     IssueDto criticalBug1 = db.issues().insert(rule, project, file,
-        i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
     IssueDto criticalBug2 = db.issues().insert(rule, project, file,
-        i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
 
     db.issues().insert(rule, project, file,
-        i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG));
 
     //two issues part of new code period on reference branch
     db.issues().insertNewCodeReferenceIssue(fpBug);
     db.issues().insertNewCodeReferenceIssue(criticalBug1);
     db.issues().insertNewCodeReferenceIssue(criticalBug2);
 
-    Collection<IssueGroupDto> result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, -1);
+    Collection<IssueGroupDto> result = underTest.selectIssueGroupsByComponent(db.getSession(), file, -1);
 
     assertThat(result.stream().mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(4);
 
@@ -393,6 +402,74 @@ public class IssueDaoTest {
     assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isOne();
   }
 
+  @Test
+  public void selectBranchHotspotsCount_on_project() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+    RuleDto rule = db.rules().insert();
+    IssueDto i1 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("REVIEWED").setResolution("SAFE").setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_500L));
+    IssueDto i2 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("TO_REVIEW").setResolution(null).setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_600L));
+    IssueDto i3 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("TO_REVIEW").setResolution(null).setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_700L));
+
+    // closed issues or other types are ignored
+    IssueDto closed = db.issues().insert(rule, project, file,
+      i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+    IssueDto bug = db.issues().insert(rule, project, file,
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+
+    Collection<HotspotGroupDto> result = underTest.selectBranchHotspotsCount(db.getSession(), project.uuid(), 1_000L);
+
+    assertThat(result.stream().mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(3);
+
+    assertThat(result.stream().filter(g -> g.getStatus().equals("TO_REVIEW")).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(2);
+    assertThat(result.stream().filter(g -> g.getStatus().equals("REVIEWED")).mapToLong(HotspotGroupDto::getCount).sum()).isOne();
+    assertThat(result.stream().filter(g -> g.getStatus().equals("CLOSED")).mapToLong(HotspotGroupDto::getCount).sum()).isZero();
+
+    assertThat(result.stream().filter(HotspotGroupDto::isInLeak).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(3);
+    assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(HotspotGroupDto::getCount).sum()).isZero();
+
+    // test leak
+    result = underTest.selectBranchHotspotsCount(db.getSession(), project.uuid(), 999_999_999L);
+    assertThat(result.stream().filter(HotspotGroupDto::isInLeak).mapToLong(HotspotGroupDto::getCount).sum()).isZero();
+    assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(3);
+  }
+
+  @Test
+  public void selectBranchHotspotsCount_on_project_with_reference_branch() {
+    ComponentDto project = db.components().insertPublicProject();
+    ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project));
+    RuleDto rule = db.rules().insert();
+    IssueDto i1 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("REVIEWED").setResolution("SAFE").setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_500L));
+    IssueDto i2 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("TO_REVIEW").setResolution(null).setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_600L));
+    IssueDto i3 = db.issues().insert(rule, project, file,
+      i -> i.setStatus("TO_REVIEW").setResolution(null).setSeverity("CRITICAL").setType(RuleType.SECURITY_HOTSPOT).setIssueCreationTime(1_700L));
+
+    // closed issues or other types are ignored
+    IssueDto closed = db.issues().insert(rule, project, file,
+      i -> i.setStatus("CLOSED").setResolution("REMOVED").setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+    IssueDto bug = db.issues().insert(rule, project, file,
+      i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L));
+
+    db.issues().insertNewCodeReferenceIssue(i1);
+    db.issues().insertNewCodeReferenceIssue(bug);
+
+    Collection<HotspotGroupDto> result = underTest.selectBranchHotspotsCount(db.getSession(), project.uuid(), -1);
+
+    assertThat(result.stream().mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(3);
+
+    assertThat(result.stream().filter(g -> g.getStatus().equals("TO_REVIEW")).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(2);
+    assertThat(result.stream().filter(g -> g.getStatus().equals("REVIEWED")).mapToLong(HotspotGroupDto::getCount).sum()).isOne();
+    assertThat(result.stream().filter(g -> g.getStatus().equals("CLOSED")).mapToLong(HotspotGroupDto::getCount).sum()).isZero();
+
+    assertThat(result.stream().filter(HotspotGroupDto::isInLeak).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(1);
+    assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(HotspotGroupDto::getCount).sum()).isEqualTo(2);
+  }
+
   @Test
   public void selectModuleAndDirComponentUuidsOfOpenIssuesForProjectUuid() {
     assertThat(underTest.selectModuleAndDirComponentUuidsOfOpenIssuesForProjectUuid(db.getSession(), randomAlphabetic(12)))
@@ -515,99 +592,6 @@ public class IssueDaoTest {
     assertThat(underTest.selectOrFailByKey(db.getSession(), ISSUE_KEY1).isNewCodeReferenceIssue()).isFalse();
   }
 
-  @Test
-  public void selectByBranch_givenOneIssueOnTheRightBranchAndOneOnTheWrongOne_returnOneIssue() {
-    prepareIssuesComponent();
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setProjectUuid(PROJECT_UUID));
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setProjectUuid("another-branch-uuid"));
-    db.getSession().commit();
-
-    List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
-      new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, false, null),
-      1);
-
-    assertThat(issueDtos).hasSize(1);
-    assertThat(issueDtos.get(0).getKey()).isEqualTo(ISSUE_KEY1);
-  }
-
-  @Test
-  public void selectByBranch_ordersResultByCreationDate() {
-    prepareIssuesComponent();
-
-    int times = 1;
-    for (;times <= 1001; times++) {
-      underTest.insert(db.getSession(), newIssueDto(String.valueOf(times))
-        .setIssueCreationTime(Long.valueOf(times))
-        .setCreatedAt(times)
-        .setRuleUuid(RULE.getUuid())
-        .setComponentUuid(FILE_UUID)
-        .setProjectUuid(PROJECT_UUID));
-    }
-    // updating time's value to the last actual value that was used for creating an issue
-    times--;
-    db.getSession().commit();
-
-    List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
-      new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, false, null),
-      2);
-
-    assertThat(issueDtos).hasSize(1);
-    assertThat(issueDtos.get(0).getKey()).isEqualTo(String.valueOf(times));
-  }
-
-  @Test
-  public void selectByBranch_openIssueNotReturnedWhenResolvedOnlySet() {
-    prepareIssuesComponent();
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setStatus(Issue.STATUS_OPEN)
-      .setProjectUuid(PROJECT_UUID));
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setStatus(Issue.STATUS_RESOLVED)
-      .setProjectUuid(PROJECT_UUID));
-    db.getSession().commit();
-
-    List<IssueDto> issueDtos = underTest.selectByBranch(db.getSession(),
-      new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, true, null),
-      1);
-
-    assertThat(issueDtos).hasSize(1);
-    assertThat(issueDtos.get(0).getKey()).isEqualTo(ISSUE_KEY2);
-  }
-
-  @Test
-  public void selectRecentlyClosedIssues_doNotReturnIssuesOlderThanTimestamp() {
-    prepareIssuesComponent();
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY1)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setStatus(Issue.STATUS_CLOSED)
-      .setIssueUpdateTime(10_000L)
-      .setProjectUuid(PROJECT_UUID));
-    underTest.insert(db.getSession(), newIssueDto(ISSUE_KEY2)
-      .setRuleUuid(RULE.getUuid())
-      .setComponentUuid(FILE_UUID)
-      .setStatus(Issue.STATUS_CLOSED)
-      .setIssueUpdateTime(5_000L)
-      .setProjectUuid(PROJECT_UUID));
-    db.getSession().commit();
-
-    List<String> issueUuids = underTest.selectRecentlyClosedIssues(db.getSession(),
-      new IssueQueryParams(PROJECT_UUID, DEFAULT_BRANCH_NAME, null, null, true, 8_000L));
-
-    assertThat(issueUuids).hasSize(1);
-    assertThat(issueUuids.get(0)).isEqualTo(ISSUE_KEY1);
-  }
-
   private static IssueDto newIssueDto(String key) {
     IssueDto dto = new IssueDto();
     dto.setComponent(new ComponentDto().setDbKey("struts:Action").setUuid("component-uuid"));
index 494c9bd4828d01365ddfc06fd6669b10d23925ea..fa0cd8ae6b321bd7fa390867a57b2b5a42f3fa4d 100644 (file)
@@ -68,15 +68,12 @@ public class IssueUpdater {
     this.notificationSerializer = notificationSerializer;
   }
 
-  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue,
-    IssueChangeContext context, boolean refreshMeasures) {
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, boolean refreshMeasures) {
     BranchDto branch = getBranch(dbSession, issue, issue.projectUuid());
     return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, refreshMeasures, branch);
   }
 
-  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue,
-    IssueChangeContext context, boolean refreshMeasures, BranchDto branch) {
-
+  public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, boolean refreshMeasures, BranchDto branch) {
     Optional<RuleDto> rule = getRuleByKey(dbSession, issue.getRuleKey());
     ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid());
     ComponentDto component = getComponent(dbSession, issue, issue.componentUuid());
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java
new file mode 100644 (file)
index 0000000..47e9be3
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * 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.List;
+import java.util.Set;
+import org.sonar.db.component.ComponentDto;
+
+/**
+ * Provides all components needed for the computation of live measures.
+ * The components needed for the computation are:
+ * 1) Components for which issues were modified
+ * 2) All ancestors of 1), up to the root
+ * 3) All immediate children of 1) and 2). The measures in these components won't be recomputed,
+ * but their measures are needed to recompute the measures for components in 1) and 2).
+ */
+public interface ComponentIndex {
+  /**
+   * Immediate children of a component that are relevant for the computation
+   */
+  List<ComponentDto> getChildren(ComponentDto component);
+
+  /**
+   * Uuids of all components relevant for the computation
+   */
+  Set<String> getAllUuids();
+
+  /**
+   * All components that need the measures recalculated, sorted depth first. It corresponds to the points 1) and 2) in the list mentioned in the javadoc of this class.
+   */
+  List<ComponentDto> getSortedTree();
+
+  /**
+   * Branch being recomputed
+   */
+  ComponentDto getBranch();
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java
new file mode 100644 (file)
index 0000000..40b80e1
--- /dev/null
@@ -0,0 +1,39 @@
+/*
+ * 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.List;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+
+public class ComponentIndexFactory {
+  private final DbClient dbClient;
+
+  public ComponentIndexFactory(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  public ComponentIndex create(DbSession dbSession, List<ComponentDto> touchedComponents) {
+    ComponentIndexImpl idx = new ComponentIndexImpl(dbClient);
+    idx.load(dbSession, touchedComponents);
+    return idx;
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java
new file mode 100644 (file)
index 0000000..7b5da77
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * 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.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.ComponentDto;
+
+import static java.util.Collections.emptyList;
+
+public class ComponentIndexImpl implements ComponentIndex {
+  private final DbClient dbClient;
+  private ComponentDto branchComponent;
+  private List<ComponentDto> sortedComponentsToRoot;
+  private Map<String, List<ComponentDto>> children;
+
+  public ComponentIndexImpl(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  /**
+   * Loads all the components required for the calculation of the new values of the live measures based on what components were modified:
+   * - All components between the touched components and the roots of their component trees
+   * - All immediate children of those components
+   */
+  public void load(DbSession dbSession, List<ComponentDto> touchedComponents) {
+    sortedComponentsToRoot = loadTreeOfComponents(dbSession, touchedComponents);
+    branchComponent = findBranchComponent(sortedComponentsToRoot);
+    children = new HashMap<>();
+    List<ComponentDto> childComponents = loadChildren(dbSession, sortedComponentsToRoot);
+    for (ComponentDto c : childComponents) {
+      List<String> uuidPathAsList = c.getUuidPathAsList();
+      String parentUuid = uuidPathAsList.get(uuidPathAsList.size() - 1);
+      children.computeIfAbsent(parentUuid, uuid -> new LinkedList<>()).add(c);
+    }
+  }
+
+  private static ComponentDto findBranchComponent(Collection<ComponentDto> components) {
+    return components.stream().filter(ComponentDto::isRootProject).findFirst()
+      .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> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) {
+    Set<String> componentUuids = new HashSet<>();
+    for (ComponentDto component : touchedComponents) {
+      componentUuids.add(component.uuid());
+      // ancestors, excluding self
+      componentUuids.addAll(component.getUuidPathAsList());
+    }
+    return dbClient.componentDao().selectByUuids(dbSession, componentUuids).stream()
+      .sorted(Comparator.comparing(ComponentDto::getUuidPath).reversed())
+      .collect(Collectors.toList());
+  }
+
+  @Override
+  public List<ComponentDto> getChildren(ComponentDto component) {
+    return children.getOrDefault(component.uuid(), emptyList());
+  }
+
+  @Override
+  public Set<String> getAllUuids() {
+    Set<String> all = new HashSet<>();
+    sortedComponentsToRoot.forEach(c -> all.add(c.uuid()));
+    for (Collection<ComponentDto> l : children.values()) {
+      for (ComponentDto c : l) {
+        all.add(c.uuid());
+      }
+    }
+    return all;
+  }
+
+  @Override
+  public List<ComponentDto> getSortedTree() {
+    return sortedComponentsToRoot;
+  }
+
+  @Override public ComponentDto getBranch() {
+    return branchComponent;
+  }
+}
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
new file mode 100644 (file)
index 0000000..8f624cb
--- /dev/null
@@ -0,0 +1,60 @@
+/*
+ * 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)));
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java
new file mode 100644 (file)
index 0000000..9db99df
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.Nullable;
+import org.sonar.db.issue.HotspotGroupDto;
+
+public class HotspotsCounter {
+  private final Map<String, Count> hotspotsByStatus = new HashMap<>();
+
+  HotspotsCounter(Collection<HotspotGroupDto> groups) {
+    for (HotspotGroupDto group : groups) {
+      if (group.getStatus() != null) {
+        hotspotsByStatus
+          .computeIfAbsent(group.getStatus(), k -> new Count())
+          .add(group);
+      }
+    }
+  }
+
+  public long countHotspotsByStatus(String status, boolean onlyInLeak) {
+    return value(hotspotsByStatus.get(status), onlyInLeak);
+  }
+
+  private static long value(@Nullable Count count, boolean onlyInLeak) {
+    if (count == null) {
+      return 0;
+    }
+    return onlyInLeak ? count.leak : count.absolute;
+  }
+
+  private static class Count {
+    private long absolute = 0L;
+    private long leak = 0L;
+
+    void add(HotspotGroupDto group) {
+      absolute += group.getCount();
+      if (group.isInLeak()) {
+        leak += group.getCount();
+      }
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java
deleted file mode 100644 (file)
index d347531..0000000
+++ /dev/null
@@ -1,89 +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.Collection;
-import java.util.Optional;
-import java.util.function.BiConsumer;
-import org.sonar.api.measures.Metric;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.server.measure.DebtRatingGrid;
-import org.sonar.server.measure.Rating;
-
-import static java.util.Collections.emptyList;
-
-class IssueMetricFormula {
-
-  private final Metric metric;
-  private final boolean onLeak;
-  private final BiConsumer<Context, IssueCounter> formula;
-  private final Collection<Metric> dependentMetrics;
-
-  IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula) {
-    this(metric, onLeak, formula, emptyList());
-  }
-
-  IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula, Collection<Metric> dependentMetrics) {
-    this.metric = metric;
-    this.onLeak = onLeak;
-    this.formula = formula;
-    this.dependentMetrics = dependentMetrics;
-  }
-
-  Metric getMetric() {
-    return metric;
-  }
-
-  boolean isOnLeak() {
-    return onLeak;
-  }
-
-  Collection<Metric> getDependentMetrics() {
-    return dependentMetrics;
-  }
-
-  void compute(Context context, IssueCounter issues) {
-    formula.accept(context, issues);
-  }
-
-  interface Context {
-    ComponentDto getComponent();
-
-    DebtRatingGrid getDebtRatingGrid();
-
-    /**
-     * Value that was just refreshed, otherwise value as computed
-     * during last analysis.
-     * The metric must be declared in the formula dependencies
-     * (see {@link IssueMetricFormula#getDependentMetrics()}).
-     */
-    Optional<Double> getValue(Metric metric);
-
-    Optional<Double> getLeakValue(Metric metric);
-
-    void setValue(double value);
-
-    void setValue(Rating value);
-
-    void setLeakValue(double value);
-
-    void setLeakValue(Rating value);
-  }
-}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java
deleted file mode 100644 (file)
index 4d3fa8b..0000000
+++ /dev/null
@@ -1,40 +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.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-import org.sonar.api.measures.Metric;
-import org.sonar.api.server.ServerSide;
-
-@ServerSide
-public interface IssueMetricFormulaFactory {
-  List<IssueMetricFormula> getFormulas();
-
-  Set<Metric> getFormulaMetrics();
-
-  static Set<Metric> extractMetrics(List<IssueMetricFormula> formulas) {
-    return formulas.stream()
-      .flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream()))
-      .collect(Collectors.toSet());
-  }
-}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java
deleted file mode 100644 (file)
index 0dddb83..0000000
+++ /dev/null
@@ -1,240 +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.List;
-import java.util.Optional;
-import java.util.Set;
-import org.sonar.api.issue.Issue;
-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.server.measure.Rating;
-
-import static java.util.Arrays.asList;
-import static org.sonar.server.measure.Rating.RATING_BY_SEVERITY;
-import static org.sonar.server.security.SecurityReviewRating.computePercent;
-import static org.sonar.server.security.SecurityReviewRating.computeRating;
-
-public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory {
-
-  private static final List<IssueMetricFormula> FORMULAS = asList(
-    new IssueMetricFormula(CoreMetrics.CODE_SMELLS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))),
-
-    new IssueMetricFormula(CoreMetrics.BUGS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))),
-
-    new IssueMetricFormula(CoreMetrics.VULNERABILITIES, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, false))),
-
-    new IssueMetricFormula(CoreMetrics.VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolved(false))),
-
-    new IssueMetricFormula(CoreMetrics.BLOCKER_VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))),
-
-    new IssueMetricFormula(CoreMetrics.CRITICAL_VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))),
-
-    new IssueMetricFormula(CoreMetrics.MAJOR_VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))),
-
-    new IssueMetricFormula(CoreMetrics.MINOR_VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))),
-
-    new IssueMetricFormula(CoreMetrics.INFO_VIOLATIONS, false,
-      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))),
-
-    new IssueMetricFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false,
-      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))),
-
-    new IssueMetricFormula(CoreMetrics.WONT_FIX_ISSUES, false,
-      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))),
-
-    new IssueMetricFormula(CoreMetrics.OPEN_ISSUES, false,
-      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))),
-
-    new IssueMetricFormula(CoreMetrics.REOPENED_ISSUES, false,
-      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))),
-
-    new IssueMetricFormula(CoreMetrics.CONFIRMED_ISSUES, false,
-      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))),
-
-    new IssueMetricFormula(CoreMetrics.TECHNICAL_DEBT, false,
-      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))),
-
-    new IssueMetricFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false,
-      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false,
-      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))),
-
-    new IssueMetricFormula(CoreMetrics.SQALE_DEBT_RATIO, false,
-      (context, issues) -> context.setValue(100.0 * debtDensity(context)),
-      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
-
-    new IssueMetricFormula(CoreMetrics.SQALE_RATING, false,
-      (context, issues) -> context
-        .setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
-      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
-
-    new IssueMetricFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false,
-      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
-
-    new IssueMetricFormula(CoreMetrics.RELIABILITY_RATING, false,
-      (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_RATING, false,
-      (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_REVIEW_RATING, false,
-      (context, issues) -> {
-        Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false));
-        context.setValue(computeRating(percent.orElse(null)));
-      }),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false,
-      (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))
-        .ifPresent(context::setValue)),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, false,
-      (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))),
-
-    new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false,
-      (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_BUGS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_VULNERABILITIES, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolved(true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true,
-      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true,
-      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true,
-      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true,
-      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_RATING, true,
-      (context, issues) -> {
-        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO);
-        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
-      }),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_RATING, true,
-      (context, issues) -> {
-        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO);
-        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
-      }),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REVIEW_RATING, true,
-      (context, issues) -> {
-        Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true));
-        context.setLeakValue(computeRating(percent.orElse(null)));
-      }),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, true,
-      (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))
-        .ifPresent(context::setLeakValue)),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true,
-      (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true,
-      (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true))),
-
-    new IssueMetricFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true,
-      (context, issues) -> context.setLeakValue(100.0 * newDebtDensity(context)),
-      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),
-
-    new IssueMetricFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true,
-      (context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(
-        newDebtDensity(context))),
-      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)));
-
-  private static final Set<Metric> FORMULA_METRICS = IssueMetricFormulaFactory.extractMetrics(FORMULAS);
-
-  private static double debtDensity(IssueMetricFormula.Context context) {
-    double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0), 0.0);
-    Optional<Double> devCost = context.getValue(CoreMetrics.DEVELOPMENT_COST);
-    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
-      return debt / devCost.get();
-    }
-    return 0.0;
-  }
-
-  private static double newDebtDensity(IssueMetricFormula.Context context) {
-    double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0), 0.0);
-    Optional<Double> devCost = context.getLeakValue(CoreMetrics.NEW_DEVELOPMENT_COST);
-    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
-      return debt / devCost.get();
-    }
-    return 0.0;
-  }
-
-  private static double effortToReachMaintainabilityRatingA(IssueMetricFormula.Context context) {
-    double developmentCost = context.getValue(CoreMetrics.DEVELOPMENT_COST).orElse(0.0);
-    double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0);
-    double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost;
-    return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0;
-  }
-
-  @Override
-  public List<IssueMetricFormula> getFormulas() {
-    return FORMULAS;
-  }
-
-  @Override
-  public Set<Metric> getFormulaMetrics() {
-    return FORMULA_METRICS;
-  }
-}
index b7b024a62ef55badf2bd0fec935321aebeff658f..957cfbf0705dcb0fbc174771ed139231e8bf6f60 100644 (file)
@@ -24,7 +24,6 @@ import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import javax.annotation.CheckForNull;
@@ -34,7 +33,6 @@ import org.sonar.api.utils.log.Loggers;
 import org.sonar.db.DbClient;
 import org.sonar.db.DbSession;
 import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
 import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.measure.LiveMeasureComparator;
@@ -43,38 +41,36 @@ import org.sonar.db.metric.MetricDto;
 import org.sonar.db.project.ProjectDto;
 import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.es.ProjectIndexers;
-import org.sonar.server.measure.DebtRatingGrid;
-import org.sonar.server.measure.Rating;
 import org.sonar.server.qualitygate.EvaluatedQualityGate;
 import org.sonar.server.qualitygate.QualityGate;
 import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
 import org.sonar.server.setting.ProjectConfigurationLoader;
 
-import static com.google.common.base.Preconditions.checkState;
 import static java.util.Collections.emptyList;
 import static java.util.Collections.singleton;
-import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.groupingBy;
 import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
-import static org.sonar.core.util.stream.MoreCollectors.toArrayList;
 import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex;
-import static org.sonar.db.newcodeperiod.NewCodePeriodType.REFERENCE_BRANCH;
 
 public class LiveMeasureComputerImpl implements LiveMeasureComputer {
 
   private final DbClient dbClient;
-  private final IssueMetricFormulaFactory formulaFactory;
+  private final MeasureUpdateFormulaFactory formulaFactory;
+  private final ComponentIndexFactory componentIndexFactory;
   private final LiveQualityGateComputer qGateComputer;
   private final ProjectConfigurationLoader projectConfigurationLoader;
   private final ProjectIndexers projectIndexer;
+  private final LiveMeasureTreeUpdater treeUpdater;
 
-  public LiveMeasureComputerImpl(DbClient dbClient, IssueMetricFormulaFactory formulaFactory,
-    LiveQualityGateComputer qGateComputer, ProjectConfigurationLoader projectConfigurationLoader, ProjectIndexers projectIndexer) {
+  public LiveMeasureComputerImpl(DbClient dbClient, MeasureUpdateFormulaFactory formulaFactory, ComponentIndexFactory componentIndexFactory,
+    LiveQualityGateComputer qGateComputer, ProjectConfigurationLoader projectConfigurationLoader, ProjectIndexers projectIndexer, LiveMeasureTreeUpdater treeUpdater) {
     this.dbClient = dbClient;
     this.formulaFactory = formulaFactory;
+    this.componentIndexFactory = componentIndexFactory;
     this.qGateComputer = qGateComputer;
     this.projectConfigurationLoader = projectConfigurationLoader;
     this.projectIndexer = projectIndexer;
+    this.treeUpdater = treeUpdater;
   }
 
   @Override
@@ -93,133 +89,65 @@ public class LiveMeasureComputerImpl implements LiveMeasureComputer {
   }
 
   private Optional<QGChangeEvent> refreshComponentsOnSameProject(DbSession dbSession, List<ComponentDto> touchedComponents) {
-    // load all the components to be refreshed, including their ancestors
-    List<ComponentDto> components = loadTreeOfComponents(dbSession, touchedComponents);
-    ComponentDto branchComponent = findBranchComponent(components);
-    BranchDto branch = loadBranch(dbSession, branchComponent);
-    ProjectDto project = loadProject(dbSession, branch.getProjectUuid());
-    Optional<SnapshotDto> lastAnalysisResult = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchComponent.uuid());
-    if (lastAnalysisResult.isEmpty()) {
+    ComponentIndex components = componentIndexFactory.create(dbSession, touchedComponents);
+    ComponentDto branchComponent = components.getBranch();
+    Optional<SnapshotDto> lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, branchComponent.uuid());
+    if (lastAnalysis.isEmpty()) {
       return Optional.empty();
     }
 
-    var lastAnalysis = lastAnalysisResult.get();
-
-    QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, project, branch);
-    Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);
-
-    List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, metricKeys);
-    Map<String, MetricDto> metricsPerId = metrics.stream()
-      .collect(uniqueIndex(MetricDto::getUuid));
-    List<String> componentUuids = components.stream().map(ComponentDto::uuid).collect(toArrayList(components.size()));
-    List<LiveMeasureDto> dbMeasures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricUuids(dbSession, componentUuids, metricsPerId.keySet());
-    // previous status must be load now as MeasureMatrix mutate the LiveMeasureDto which are passed to it
-    Metric.Level previousStatus = loadPreviousStatus(metrics, dbMeasures);
-
+    BranchDto branch = loadBranch(dbSession, branchComponent);
     Configuration config = projectConfigurationLoader.loadProjectConfiguration(dbSession, branchComponent);
-    DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config);
-
-    MeasureMatrix matrix = new MeasureMatrix(components, metricsPerId.values(), dbMeasures);
-    FormulaContextImpl context = new FormulaContextImpl(matrix, debtRatingGrid);
-    long beginningOfLeak = getBeginningOfLeakPeriod(lastAnalysis, branch);
+    ProjectDto project = loadProject(dbSession, branch.getProjectUuid());
+    QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, project, branch);
+    MeasureMatrix matrix = loadMeasureMatrix(dbSession, components.getAllUuids(), qualityGate);
 
-    components.forEach(c -> {
-      IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByBaseComponent(dbSession, c, beginningOfLeak));
-      for (IssueMetricFormula formula : formulaFactory.getFormulas()) {
-        // use formulas when the leak period is defined, it's a PR, or the formula is not about the leak period
-        if (shouldUseLeakFormulas(lastAnalysis, branch) || !formula.isOnLeak()) {
-          context.change(c, formula);
-          try {
-            formula.compute(context, issueCounter);
-          } catch (RuntimeException e) {
-            throw new IllegalStateException("Fail to compute " + formula.getMetric().getKey() + " on " + context.getComponent().getDbKey(), e);
-          }
-        }
-      }
-    });
+    treeUpdater.update(dbSession, lastAnalysis.get(), config, components, branch, matrix);
 
+    Metric.Level previousStatus = loadPreviousStatus(dbSession, branchComponent);
     EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(branchComponent, qualityGate, matrix, config);
+    persistAndIndex(dbSession, matrix, branchComponent);
 
-    // persist the measures that have been created or updated
-    matrix.getChanged().sorted(LiveMeasureComparator.INSTANCE)
-      .forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m));
-    projectIndexer.commitAndIndexComponents(dbSession, singleton(branchComponent), ProjectIndexer.Cause.MEASURE_CHANGE);
-
-    return Optional.of(
-      new QGChangeEvent(project, branch, lastAnalysis, config, previousStatus, () -> Optional.of(evaluatedQualityGate)));
-  }
-
-  private static long getBeginningOfLeakPeriod(SnapshotDto lastAnalysis, BranchDto branch) {
-    if (isPR(branch)) {
-      return 0L;
-    } else if (REFERENCE_BRANCH.name().equals(lastAnalysis.getPeriodMode())) {
-      return -1;
-    }
-    return ofNullable(lastAnalysis.getPeriodDate())
-        .orElse(Long.MAX_VALUE);
-
+    return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, previousStatus, () -> Optional.of(evaluatedQualityGate)));
   }
 
-  private static boolean isPR(BranchDto branch) {
-    return branch.getBranchType() == BranchType.PULL_REQUEST;
+  private MeasureMatrix loadMeasureMatrix(DbSession dbSession, Set<String> componentUuids, QualityGate qualityGate) {
+    Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate);
+    Map<String, MetricDto> metricsPerUuid = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream().collect(uniqueIndex(MetricDto::getUuid));
+    List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricUuids(dbSession, componentUuids, metricsPerUuid.keySet());
+    return new MeasureMatrix(componentUuids, metricsPerUuid.values(), measures);
   }
 
-  private static boolean shouldUseLeakFormulas(SnapshotDto lastAnalysis, BranchDto branch) {
-    return lastAnalysis.getPeriodDate() != null || isPR(branch) || REFERENCE_BRANCH.name().equals(lastAnalysis.getPeriodMode());
+  private void persistAndIndex(DbSession dbSession, MeasureMatrix matrix, ComponentDto branchComponent) {
+    // persist the measures that have been created or updated
+    matrix.getChanged().sorted(LiveMeasureComparator.INSTANCE).forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m));
+    projectIndexer.commitAndIndexComponents(dbSession, singleton(branchComponent), ProjectIndexer.Cause.MEASURE_CHANGE);
   }
 
   @CheckForNull
-  private static Metric.Level loadPreviousStatus(List<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) {
-    MetricDto alertStatusMetric = metrics.stream()
-      .filter(m -> ALERT_STATUS_KEY.equals(m.getKey()))
-      .findAny()
-      .orElseThrow(() -> new IllegalStateException(String.format("Metric with key %s is not registered", ALERT_STATUS_KEY)));
-    return dbMeasures.stream()
-      .filter(m -> m.getMetricUuid().equals(alertStatusMetric.getUuid()))
-      .map(LiveMeasureDto::getTextValue)
-      .filter(Objects::nonNull)
-      .map(m -> {
-        try {
-          return Metric.Level.valueOf(m);
-        } catch (IllegalArgumentException e) {
-          Loggers.get(LiveMeasureComputerImpl.class)
-            .trace("Failed to parse value of metric '{}'", m, e);
-          return null;
-        }
-      })
-      .filter(Objects::nonNull)
-      .findAny()
-      .orElse(null);
-  }
+  private Metric.Level loadPreviousStatus(DbSession dbSession, ComponentDto branchComponent) {
+    Optional<LiveMeasureDto> measure = dbClient.liveMeasureDao().selectMeasure(dbSession, branchComponent.uuid(), ALERT_STATUS_KEY);
+    if (measure.isEmpty()) {
+      return null;
+    }
 
-  private List<ComponentDto> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) {
-    Set<String> componentUuids = new HashSet<>();
-    for (ComponentDto component : touchedComponents) {
-      componentUuids.add(component.uuid());
-      // ancestors, excluding self
-      componentUuids.addAll(component.getUuidPathAsList());
+    try {
+      return Metric.Level.valueOf(measure.get().getTextValue());
+    } catch (IllegalArgumentException e) {
+      Loggers.get(LiveMeasureComputerImpl.class).trace("Failed to parse value of metric '{}'", ALERT_STATUS_KEY, e);
+      return null;
     }
-    // Contrary to the formulas in Compute Engine,
-    // measures do not aggregate values of descendant components.
-    // As a consequence nodes do not need to be sorted. Formulas can be applied
-    // on components in any order.
-    return dbClient.componentDao().selectByUuids(dbSession, componentUuids);
   }
 
   private Set<String> getKeysOfAllInvolvedMetrics(QualityGate gate) {
     Set<String> metricKeys = new HashSet<>();
-    for (Metric metric : formulaFactory.getFormulaMetrics()) {
+    for (Metric<?> metric : formulaFactory.getFormulaMetrics()) {
       metricKeys.add(metric.getKey());
     }
     metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate));
     return metricKeys;
   }
 
-  private static ComponentDto findBranchComponent(Collection<ComponentDto> components) {
-    return components.stream().filter(ComponentDto::isRootProject).findFirst()
-      .orElseThrow(() -> new IllegalStateException("No project found in " + components));
-  }
-
   private BranchDto loadBranch(DbSession dbSession, ComponentDto branchComponent) {
     return dbClient.branchDao().selectByUuid(dbSession, branchComponent.uuid())
       .orElseThrow(() -> new IllegalStateException("Branch not found: " + branchComponent.uuid()));
@@ -229,71 +157,4 @@ public class LiveMeasureComputerImpl implements LiveMeasureComputer {
     return dbClient.projectDao().selectByUuid(dbSession, uuid)
       .orElseThrow(() -> new IllegalStateException("Project not found: " + uuid));
   }
-
-  private static class FormulaContextImpl implements IssueMetricFormula.Context {
-    private final MeasureMatrix matrix;
-    private final DebtRatingGrid debtRatingGrid;
-    private ComponentDto currentComponent;
-    private IssueMetricFormula currentFormula;
-
-    private FormulaContextImpl(MeasureMatrix matrix, DebtRatingGrid debtRatingGrid) {
-      this.matrix = matrix;
-      this.debtRatingGrid = debtRatingGrid;
-    }
-
-    private void change(ComponentDto component, IssueMetricFormula formula) {
-      this.currentComponent = component;
-      this.currentFormula = formula;
-    }
-
-    @Override
-    public ComponentDto getComponent() {
-      return currentComponent;
-    }
-
-    @Override
-    public DebtRatingGrid getDebtRatingGrid() {
-      return debtRatingGrid;
-    }
-
-    @Override
-    public Optional<Double> getValue(Metric metric) {
-      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
-      return measure.map(LiveMeasureDto::getValue);
-    }
-
-    @Override
-    public Optional<Double> getLeakValue(Metric metric) {
-      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
-      return measure.map(LiveMeasureDto::getVariation);
-    }
-
-    @Override
-    public void setValue(double value) {
-      String metricKey = currentFormula.getMetric().getKey();
-      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
-      matrix.setValue(currentComponent, metricKey, value);
-    }
-
-    @Override
-    public void setLeakValue(double value) {
-      String metricKey = currentFormula.getMetric().getKey();
-      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
-      matrix.setLeakValue(currentComponent, metricKey, value);
-    }
-
-    @Override
-    public void setValue(Rating value) {
-      String metricKey = currentFormula.getMetric().getKey();
-      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
-      matrix.setValue(currentComponent, metricKey, value);
-    }
-
-    @Override
-    public void setLeakValue(Rating value) {
-      String metricKey = currentFormula.getMetric().getKey();
-      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
-      matrix.setLeakValue(currentComponent, metricKey, value);
-    }
-  }
 }
index a69881f63dda8990cea3b4b0085ddbf6ab675b3b..595b3c627ed06301d0034020bc16e35753b65ae0 100644 (file)
@@ -25,8 +25,11 @@ public class LiveMeasureModule extends Module {
   @Override
   protected void configureModule() {
     add(
-      IssueMetricFormulaFactoryImpl.class,
+      MeasureUpdateFormulaFactoryImpl.class,
+      ComponentIndexFactory.class,
+      LiveMeasureTreeUpdaterImpl.class,
       LiveMeasureComputerImpl.class,
+      HotspotMeasureUpdater.class,
       LiveQualityGateComputerImpl.class);
   }
 }
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java
new file mode 100644 (file)
index 0000000..cce7799
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * 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 org.sonar.api.config.Configuration;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.SnapshotDto;
+
+public interface LiveMeasureTreeUpdater {
+  void update(DbSession dbSession, SnapshotDto lastAnalysis, Configuration config, ComponentIndex components, BranchDto branch, MeasureMatrix measures);
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java
new file mode 100644 (file)
index 0000000..f362cf0
--- /dev/null
@@ -0,0 +1,217 @@
+/*
+ * 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.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.server.measure.DebtRatingGrid;
+import org.sonar.server.measure.Rating;
+
+import static com.google.common.base.Preconditions.checkState;
+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) {
+    this.dbClient = dbClient;
+    this.formulaFactory = formulaFactory;
+    this.hotspotMeasureUpdater = hotspotMeasureUpdater;
+  }
+
+  @Override
+  public void update(DbSession dbSession, SnapshotDto lastAnalysis, Configuration config, ComponentIndex components, BranchDto branch, MeasureMatrix measures) {
+    long beginningOfLeak = getBeginningOfLeakPeriod(lastAnalysis, branch);
+    boolean shouldUseLeakFormulas = shouldUseLeakFormulas(lastAnalysis, branch);
+
+    // 1. set new measure from issues to each component from touched components to the root
+    updateMatrixWithIssues(dbSession, measures, components, config, shouldUseLeakFormulas, beginningOfLeak);
+
+    // 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) {
+    DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config);
+    FormulaContextImpl context = new FormulaContextImpl(matrix, components, debtRatingGrid);
+    components.getSortedTree().forEach(c -> {
+      for (MeasureUpdateFormula formula : formulaFactory.getFormulas()) {
+        if (useLeakFormulas || !formula.isOnLeak()) {
+          context.change(c, formula);
+          try {
+            formula.computeHierarchy(context);
+          } catch (RuntimeException e) {
+            throw new IllegalStateException("Fail to compute " + formula.getMetric().getKey() + " on " + context.getComponent().getDbKey(), e);
+          }
+        }
+      }
+    });
+  }
+
+  private void updateMatrixWithIssues(DbSession dbSession, MeasureMatrix matrix, ComponentIndex components, Configuration config, boolean useLeakFormulas, long beginningOfLeak) {
+    DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config);
+    FormulaContextImpl context = new FormulaContextImpl(matrix, components, debtRatingGrid);
+
+    components.getSortedTree().forEach(c -> {
+      IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByComponent(dbSession, c, beginningOfLeak));
+      for (MeasureUpdateFormula formula : formulaFactory.getFormulas()) {
+        // use formulas when the leak period is defined, it's a PR, or the formula is not about the leak period
+        if (useLeakFormulas || !formula.isOnLeak()) {
+          context.change(c, formula);
+          try {
+            formula.compute(context, issueCounter);
+          } catch (RuntimeException e) {
+            throw new IllegalStateException("Fail to compute " + formula.getMetric().getKey() + " on " + context.getComponent().getDbKey(), e);
+          }
+        }
+      }
+    });
+  }
+
+  private static long getBeginningOfLeakPeriod(SnapshotDto lastAnalysis, BranchDto branch) {
+    if (isPR(branch)) {
+      return 0L;
+    } else if (REFERENCE_BRANCH.name().equals(lastAnalysis.getPeriodMode())) {
+      return -1;
+    } else {
+      return Optional.ofNullable(lastAnalysis.getPeriodDate()).orElse(Long.MAX_VALUE);
+    }
+  }
+
+  private static boolean isPR(BranchDto branch) {
+    return branch.getBranchType() == BranchType.PULL_REQUEST;
+  }
+
+  private static boolean shouldUseLeakFormulas(SnapshotDto lastAnalysis, BranchDto branch) {
+    return lastAnalysis.getPeriodDate() != null || isPR(branch) || REFERENCE_BRANCH.name().equals(lastAnalysis.getPeriodMode());
+  }
+
+  public static class FormulaContextImpl implements MeasureUpdateFormula.Context {
+    private final MeasureMatrix matrix;
+    private final ComponentIndex componentIndex;
+    private final DebtRatingGrid debtRatingGrid;
+    private ComponentDto currentComponent;
+    private MeasureUpdateFormula currentFormula;
+
+    public FormulaContextImpl(MeasureMatrix matrix, ComponentIndex componentIndex, DebtRatingGrid debtRatingGrid) {
+      this.matrix = matrix;
+      this.componentIndex = componentIndex;
+      this.debtRatingGrid = debtRatingGrid;
+    }
+
+    private void change(ComponentDto component, MeasureUpdateFormula formula) {
+      this.currentComponent = component;
+      this.currentFormula = formula;
+    }
+
+    public List<Double> getChildrenValues() {
+      List<ComponentDto> children = componentIndex.getChildren(currentComponent);
+      return children.stream()
+        .flatMap(c -> matrix.getMeasure(c, currentFormula.getMetric().getKey()).stream())
+        .map(LiveMeasureDto::getValue)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+    }
+
+    public List<Double> getChildrenLeakValues() {
+      List<ComponentDto> children = componentIndex.getChildren(currentComponent);
+      return children.stream()
+        .flatMap(c -> matrix.getMeasure(c, currentFormula.getMetric().getKey()).stream())
+        .map(LiveMeasureDto::getVariation)
+        .filter(Objects::nonNull)
+        .collect(Collectors.toList());
+    }
+
+    @Override
+    public ComponentDto getComponent() {
+      return currentComponent;
+    }
+
+    @Override
+    public DebtRatingGrid getDebtRatingGrid() {
+      return debtRatingGrid;
+    }
+
+    @Override
+    public Optional<Double> getValue(Metric metric) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
+      return measure.map(LiveMeasureDto::getValue);
+    }
+
+    @Override
+    public Optional<String> getText(Metric metric) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
+      return measure.map(LiveMeasureDto::getTextValue);
+    }
+
+    @Override
+    public Optional<Double> getLeakValue(Metric metric) {
+      Optional<LiveMeasureDto> measure = matrix.getMeasure(currentComponent, metric.getKey());
+      return measure.map(LiveMeasureDto::getVariation);
+    }
+
+    @Override
+    public void setValue(double value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
+      matrix.setValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setLeakValue(double value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
+      matrix.setLeakValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setValue(Rating value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(!currentFormula.isOnLeak(), "Formula of metric %s accepts only leak values", metricKey);
+      matrix.setValue(currentComponent, metricKey, value);
+    }
+
+    @Override
+    public void setLeakValue(Rating value) {
+      String metricKey = currentFormula.getMetric().getKey();
+      checkState(currentFormula.isOnLeak(), "Formula of metric %s does not accept leak values", metricKey);
+      matrix.setLeakValue(currentComponent, metricKey, value);
+    }
+  }
+}
index dd40744a261d28448a96f90205905a5a5cd11a12..4e8a8c44398b5da8d6f3bc35c7475cf028d533c4 100644 (file)
@@ -65,10 +65,8 @@ public class LiveQualityGateComputerImpl implements LiveQualityGateComputer {
   public QualityGate loadQualityGate(DbSession dbSession, ProjectDto project, BranchDto branch) {
     QualityGateData qg = qGateFinder.getEffectiveQualityGate(dbSession, project);
     Collection<QualityGateConditionDto> conditionDtos = dbClient.gateConditionDao().selectForQualityGate(dbSession, qg.getUuid());
-    Set<String> metricUuids = conditionDtos.stream().map(QualityGateConditionDto::getMetricUuid)
-      .collect(toHashSet(conditionDtos.size()));
-    Map<String, MetricDto> metricsByUuid = dbClient.metricDao().selectByUuids(dbSession, metricUuids).stream()
-      .collect(uniqueIndex(MetricDto::getUuid));
+    Set<String> metricUuids = conditionDtos.stream().map(QualityGateConditionDto::getMetricUuid).collect(toHashSet(conditionDtos.size()));
+    Map<String, MetricDto> metricsByUuid = dbClient.metricDao().selectByUuids(dbSession, metricUuids).stream().collect(uniqueIndex(MetricDto::getUuid));
 
     Stream<Condition> conditions = conditionDtos.stream().map(conditionDto -> {
       String metricKey = metricsByUuid.get(conditionDto.getMetricUuid()).getKey();
index 435120efc2da9c26519787437b139f42450847a2..62d4e80649c88ec69f596673bba81e5db5b76932 100644 (file)
 package org.sonar.server.measure.live;
 
 import com.google.common.collect.ArrayTable;
-import com.google.common.collect.Collections2;
 import com.google.common.collect.Table;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import java.util.function.Predicate;
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import javax.annotation.Nullable;
 import org.sonar.db.component.ComponentDto;
@@ -49,7 +51,6 @@ import static java.util.Objects.requireNonNull;
  * </ul>
  */
 class MeasureMatrix {
-
   // component uuid -> metric key -> measure
   private final Table<String, String, MeasureCell> table;
 
@@ -57,13 +58,18 @@ class MeasureMatrix {
   private final Map<String, MetricDto> metricsByUuids = new HashMap<>();
 
   MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) {
+    this(components.stream().map(ComponentDto::uuid).collect(Collectors.toSet()), metrics, dbMeasures);
+  }
+
+  MeasureMatrix(Set<String> componentUuids, Collection<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) {
     for (MetricDto metric : metrics) {
       this.metricsByKeys.put(metric.getKey(), metric);
       this.metricsByUuids.put(metric.getUuid(), metric);
     }
-    this.table = ArrayTable.create(Collections2.transform(components, ComponentDto::uuid), metricsByKeys.keySet());
+    this.table = ArrayTable.create(componentUuids, metricsByKeys.keySet());
+
     for (LiveMeasureDto dbMeasure : dbMeasures) {
-      table.put(dbMeasure.getComponentUuid(), metricsByUuids.get(dbMeasure.getMetricUuid()).getKey(), new MeasureCell(dbMeasure, false));
+      table.put(dbMeasure.getComponentUuid(), metricsByUuids.get(dbMeasure.getMetricUuid()).getKey(), new MeasureCell(dbMeasure));
     }
   }
 
@@ -82,62 +88,22 @@ class MeasureMatrix {
   }
 
   void setValue(ComponentDto component, String metricKey, double value) {
-    changeCell(component, metricKey, m -> {
-      MetricDto metric = getMetric(metricKey);
-      double newValue = scale(metric, value);
-
-      Double initialValue = m.getValue();
-      if (initialValue != null && Double.compare(initialValue, newValue) == 0) {
-        return false;
-      }
-      m.setValue(newValue);
-      Double initialVariation = m.getVariation();
-      if (initialValue != null && initialVariation != null) {
-        double leakInitialValue = initialValue - initialVariation;
-        m.setVariation(scale(metric, value - leakInitialValue));
-      }
-      return true;
-    });
+    changeCell(component, metricKey, m -> m.setValue(scale(getMetric(metricKey), value)));
   }
 
   void setValue(ComponentDto component, String metricKey, Rating value) {
     changeCell(component, metricKey, m -> {
-      Double initialValue = m.getValue();
-      if (initialValue != null && Double.compare(initialValue, value.getIndex()) == 0) {
-        return false;
-      }
       m.setData(value.name());
       m.setValue((double) value.getIndex());
-
-      Double initialVariation = m.getVariation();
-      if (initialValue != null && initialVariation != null) {
-        double leakInitialValue = initialValue - initialVariation;
-        m.setVariation(value.getIndex() - leakInitialValue);
-      }
-      return true;
     });
   }
 
   void setValue(ComponentDto component, String metricKey, @Nullable String data) {
-    changeCell(component, metricKey, m -> {
-      if (Objects.equals(m.getDataAsString(), data)) {
-        return false;
-      }
-      m.setData(data);
-      return true;
-    });
+    changeCell(component, metricKey, m -> m.setData(data));
   }
 
   void setLeakValue(ComponentDto component, String metricKey, double variation) {
-    changeCell(component, metricKey, c -> {
-      double newVariation = scale(getMetric(metricKey), variation);
-      if (c.getVariation() != null && Double.compare(c.getVariation(), newVariation) == 0) {
-        return false;
-      }
-      MetricDto metric = metricsByKeys.get(metricKey);
-      c.setVariation(scale(metric, variation));
-      return true;
-    });
+    changeCell(component, metricKey, c -> c.setVariation(scale(metricsByKeys.get(metricKey), variation)));
   }
 
   void setLeakValue(ComponentDto component, String metricKey, Rating variation) {
@@ -145,26 +111,23 @@ class MeasureMatrix {
   }
 
   Stream<LiveMeasureDto> getChanged() {
-    return table.values()
-      .stream()
+    return table.values().stream()
       .filter(Objects::nonNull)
       .filter(MeasureCell::isChanged)
       .map(MeasureCell::getMeasure);
   }
 
-  private void changeCell(ComponentDto component, String metricKey, Predicate<LiveMeasureDto> changer) {
+  private void changeCell(ComponentDto component, String metricKey, Consumer<LiveMeasureDto> changer) {
     MeasureCell cell = table.get(component.uuid(), metricKey);
     if (cell == null) {
       LiveMeasureDto measure = new LiveMeasureDto()
         .setComponentUuid(component.uuid())
         .setProjectUuid(component.projectUuid())
         .setMetricUuid(metricsByKeys.get(metricKey).getUuid());
-      cell = new MeasureCell(measure, true);
+      cell = new MeasureCell(measure);
       table.put(component.uuid(), metricKey, cell);
-      changer.test(cell.getMeasure());
-    } else if (changer.test(cell.getMeasure())) {
-      cell.setChanged(true);
     }
+    changer.accept(cell.getMeasure());
   }
 
   /**
@@ -181,11 +144,17 @@ class MeasureMatrix {
 
   private static class MeasureCell {
     private final LiveMeasureDto measure;
-    private boolean changed;
+    private final Double initialVariation;
+    private final Double initialValue;
+    private final byte[] initialData;
+    private final String initialTextValue;
 
-    private MeasureCell(LiveMeasureDto measure, boolean changed) {
+    private MeasureCell(LiveMeasureDto measure) {
       this.measure = measure;
-      this.changed = changed;
+      this.initialValue = measure.getValue();
+      this.initialVariation = measure.getVariation();
+      this.initialData = measure.getData();
+      this.initialTextValue = measure.getTextValue();
     }
 
     public LiveMeasureDto getMeasure() {
@@ -193,11 +162,8 @@ class MeasureMatrix {
     }
 
     public boolean isChanged() {
-      return changed;
-    }
-
-    public void setChanged(boolean b) {
-      this.changed = b;
+      return !Objects.equals(initialValue, measure.getValue()) || !Objects.equals(initialVariation, measure.getVariation())
+        || !Arrays.equals(initialData, measure.getData()) || !Objects.equals(initialTextValue, measure.getTextValue());
     }
   }
 }
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java
new file mode 100644 (file)
index 0000000..420a5a9
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * 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.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.function.BiConsumer;
+import org.sonar.api.measures.Metric;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.server.measure.DebtRatingGrid;
+import org.sonar.server.measure.Rating;
+
+import static java.util.Collections.emptyList;
+
+class MeasureUpdateFormula {
+
+  private final Metric metric;
+  private final boolean onLeak;
+  private final BiConsumer<Context, MeasureUpdateFormula> hierarchyFormula;
+  private final BiConsumer<Context, IssueCounter> formula;
+  private final Collection<Metric> dependentMetrics;
+
+  /**
+   * @param hierarchyFormula Called in a second pass through all the components, after 'formula' is called. Used to calculate the aggregate values for each component.
+   *                         For many metrics, we sum the value of the children to the value of the component
+   * @param formula          Used to calculate new values for a metric for each component, based on the issue counts
+   */
+  MeasureUpdateFormula(Metric metric, boolean onLeak, BiConsumer<Context, MeasureUpdateFormula> hierarchyFormula, BiConsumer<Context, IssueCounter> formula) {
+    this(metric, onLeak, hierarchyFormula, formula, emptyList());
+  }
+
+  MeasureUpdateFormula(Metric metric, boolean onLeak, BiConsumer<Context, MeasureUpdateFormula> hierarchyFormula, BiConsumer<Context, IssueCounter> formula,
+    Collection<Metric> dependentMetrics) {
+    this.metric = metric;
+    this.onLeak = onLeak;
+    this.hierarchyFormula = hierarchyFormula;
+    this.formula = formula;
+    this.dependentMetrics = dependentMetrics;
+  }
+
+  Metric getMetric() {
+    return metric;
+  }
+
+  boolean isOnLeak() {
+    return onLeak;
+  }
+
+  Collection<Metric> getDependentMetrics() {
+    return dependentMetrics;
+  }
+
+  void compute(Context context, IssueCounter issues) {
+    formula.accept(context, issues);
+  }
+
+  void computeHierarchy(Context context) {
+    hierarchyFormula.accept(context, this);
+  }
+
+  interface Context {
+    List<Double> getChildrenValues();
+
+    List<Double> getChildrenLeakValues();
+
+    ComponentDto getComponent();
+
+    DebtRatingGrid getDebtRatingGrid();
+
+    /**
+     * Value that was just refreshed, otherwise value as computed
+     * during last analysis.
+     * The metric must be declared in the formula dependencies
+     * (see {@link MeasureUpdateFormula#getDependentMetrics()}).
+     */
+    Optional<Double> getValue(Metric metric);
+
+    Optional<String> getText(Metric metrc);
+
+    Optional<Double> getLeakValue(Metric metric);
+
+    void setValue(double value);
+
+    void setValue(Rating value);
+
+    void setLeakValue(double value);
+
+    void setLeakValue(Rating value);
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java
new file mode 100644 (file)
index 0000000..00c0083
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.server.ServerSide;
+
+@ServerSide
+public interface MeasureUpdateFormulaFactory {
+  List<MeasureUpdateFormula> getFormulas();
+
+  Set<Metric> getFormulaMetrics();
+
+  static Set<Metric> extractMetrics(List<MeasureUpdateFormula> formulas) {
+    return formulas.stream()
+      .flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream()))
+      .collect(Collectors.toSet());
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java
new file mode 100644 (file)
index 0000000..02a0ee5
--- /dev/null
@@ -0,0 +1,299 @@
+/*
+ * 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.List;
+import java.util.Optional;
+import java.util.OptionalInt;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import org.sonar.api.issue.Issue;
+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.server.measure.Rating;
+
+import static java.util.Arrays.asList;
+import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS;
+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;
+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.server.measure.Rating.RATING_BY_SEVERITY;
+import static org.sonar.server.security.SecurityReviewRating.computePercent;
+import static org.sonar.server.security.SecurityReviewRating.computeRating;
+
+public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFactory {
+  private static final List<MeasureUpdateFormula> FORMULAS = asList(
+    new MeasureUpdateFormula(CODE_SMELLS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.BUGS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.VULNERABILITIES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolved(false))),
+
+    new MeasureUpdateFormula(CoreMetrics.BLOCKER_VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.CRITICAL_VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.MAJOR_VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.MINOR_VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.INFO_VIOLATIONS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.WONT_FIX_ISSUES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.OPEN_ISSUES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.REOPENED_ISSUES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.CONFIRMED_ISSUES, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.TECHNICAL_DEBT, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.SQALE_DEBT_RATIO, false,
+      (context, formula) -> context.setValue(100.0 * debtDensity(context)),
+      (context, issues) -> context.setValue(100.0 * debtDensity(context)),
+      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(CoreMetrics.SQALE_RATING, false,
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
+      (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))),
+      asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false,
+      (context, formula) -> context.setValue(effortToReachMaintainabilityRatingA(context)),
+      (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(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)))),
+
+    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(),
+      (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))),
+
+    new MeasureUpdateFormula(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false, new AddChildren(),
+      (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false))),
+
+    new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false,
+      (context, formula) -> {
+        Optional<Double> percent = computePercent(
+          context.getValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D).longValue(),
+          context.getValue(SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D).longValue());
+        percent.ifPresent(context::setValue);
+      },
+      (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))
+        .ifPresent(context::setValue)),
+
+    new MeasureUpdateFormula(CoreMetrics.SECURITY_REVIEW_RATING, false,
+      (context, formula) -> context.setValue(computeRating(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(computeRating(percent.orElse(null)));
+      }),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_CODE_SMELLS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_BUGS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_VULNERABILITIES, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolved(true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_RELIABILITY_RATING, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO);
+        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
+      }),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_RATING, true, new MaxRatingChildren(),
+      (context, issues) -> {
+        String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO);
+        context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity));
+      }),
+
+    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))),
+
+    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true, new AddChildren(),
+      (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true))),
+
+    new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED, true,
+      (context, formula) -> {
+        Optional<Double> percent = computePercent(
+          context.getLeakValue(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D).longValue(),
+          context.getLeakValue(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D).longValue());
+        percent.ifPresent(context::setLeakValue);
+      },
+      (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))
+        .ifPresent(context::setLeakValue)),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_REVIEW_RATING, true,
+      (context, formula) -> context.setLeakValue(computeRating(context.getLeakValue(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.setLeakValue(computeRating(percent.orElse(null)));
+      }),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true,
+      (context, formula) -> context.setLeakValue(100.0D * newDebtDensity(context)),
+      (context, issues) -> context.setLeakValue(100.0D * newDebtDensity(context)),
+      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)),
+
+    new MeasureUpdateFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true,
+      (context, formula) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))),
+      (context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))),
+      asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)));
+
+  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);
+    Optional<Double> devCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble);
+    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
+      return debt / devCost.get();
+    }
+    return 0.0D;
+  }
+
+  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);
+    if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) {
+      return debt / devCost.get();
+    }
+    return 0.0D;
+  }
+
+  private static double effortToReachMaintainabilityRatingA(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 upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost;
+    return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0D;
+  }
+
+  static class AddChildren implements BiConsumer<MeasureUpdateFormula.Context, MeasureUpdateFormula> {
+    @Override
+    public void accept(MeasureUpdateFormula.Context context, MeasureUpdateFormula formula) {
+      double sum;
+      if (formula.isOnLeak()) {
+        sum = context.getChildrenLeakValues().stream().mapToDouble(x -> x).sum();
+        context.setLeakValue(context.getLeakValue(formula.getMetric()).orElse(0D) + sum);
+      } else {
+        sum = context.getChildrenValues().stream().mapToDouble(x -> x).sum();
+        context.setValue(context.getValue(formula.getMetric()).orElse(0D) + sum);
+      }
+    }
+  }
+
+  private static class MaxRatingChildren implements BiConsumer<MeasureUpdateFormula.Context, MeasureUpdateFormula> {
+    @Override
+    public void accept(MeasureUpdateFormula.Context context, MeasureUpdateFormula formula) {
+      OptionalInt max;
+      if (formula.isOnLeak()) {
+        max = context.getChildrenLeakValues().stream().mapToInt(Double::intValue).max();
+        if (max.isPresent()) {
+          int currentRating = context.getLeakValue(formula.getMetric()).map(Double::intValue).orElse(Rating.A.getIndex());
+          context.setLeakValue(Rating.valueOf(Math.max(currentRating, max.getAsInt())));
+        }
+      } else {
+        max = context.getChildrenValues().stream().mapToInt(Double::intValue).max();
+        if (max.isPresent()) {
+          int currentRating = context.getValue(formula.getMetric()).map(Double::intValue).orElse(Rating.A.getIndex());
+          context.setValue(Rating.valueOf(Math.max(currentRating, max.getAsInt())));
+        }
+      }
+    }
+  }
+
+  @Override
+  public List<MeasureUpdateFormula> getFormulas() {
+    return FORMULAS;
+  }
+
+  @Override
+  public Set<Metric> getFormulaMetrics() {
+    return FORMULA_METRICS;
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java
new file mode 100644 (file)
index 0000000..6187aab
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * 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.List;
+import org.junit.Test;
+import org.sonar.db.DbTester;
+import org.sonar.db.component.ComponentDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ComponentIndexFactoryTest {
+  public DbTester db = DbTester.create();
+  private final ComponentIndexFactory factory = new ComponentIndexFactory(db.getDbClient());
+
+  @Test
+  public void creates_and_loads_instance() {
+    ComponentDto project = db.components().insertPrivateProject();
+    ComponentIndex index = factory.create(db.getSession(), List.of(project));
+
+    assertThat(index.getAllUuids()).containsOnly(project.uuid());
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java
new file mode 100644 (file)
index 0000000..88666f2
--- /dev/null
@@ -0,0 +1,96 @@
+/*
+ * 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.List;
+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 static org.assertj.core.api.Assertions.assertThat;
+
+public class ComponentIndexImplTest {
+  @Rule
+  public DbTester db = DbTester.create();
+  private final ComponentIndexImpl componentIndex = new ComponentIndexImpl(db.getDbClient());
+
+  private ComponentDto project;
+  private ComponentDto dir1;
+  private ComponentDto dir2;
+  private ComponentDto file11;
+  private ComponentDto file12;
+  private ComponentDto file21;
+
+  private ComponentDto branch;
+  private ComponentDto branchDir1;
+  private ComponentDto branchDir2;
+  private ComponentDto branchFile11;
+  private ComponentDto branchFile12;
+  private ComponentDto branchFile21;
+
+  @Before
+  public void setUp() {
+    project = db.components().insertPrivateProject();
+    dir1 = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java"));
+    dir2 = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java2"));
+    file11 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir1));
+    file12 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir1));
+    file21 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir2));
+
+    branch = db.components().insertProjectBranch(project);
+    branchDir1 = db.components().insertComponent(ComponentTesting.newDirectory(branch, "src/main/java"));
+    branchDir2 = db.components().insertComponent(ComponentTesting.newDirectory(branch, "src/main/java2"));
+    branchFile11 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir1));
+    branchFile12 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir1));
+    branchFile21 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir2));
+  }
+
+  @Test
+  public void loads_all_necessary_components() {
+    componentIndex.load(db.getSession(), List.of(file11));
+    assertThat(componentIndex.getSortedTree()).containsExactly(file11, dir1, project);
+    assertThat(componentIndex.getBranch()).isEqualTo(project);
+    assertThat(componentIndex.getAllUuids()).containsOnly(project.uuid(), dir1.uuid(), dir2.uuid(), file11.uuid(), file12.uuid());
+    assertThat(componentIndex.getChildren(dir1)).containsOnly(file11, file12);
+  }
+
+
+  @Test
+  public void loads_all_necessary_components_for_root() {
+    componentIndex.load(db.getSession(), List.of(project));
+    assertThat(componentIndex.getSortedTree()).containsExactly(project);
+    assertThat(componentIndex.getBranch()).isEqualTo(project);
+    assertThat(componentIndex.getAllUuids()).containsOnly(project.uuid(), dir1.uuid(), dir2.uuid());
+    assertThat(componentIndex.getChildren(dir1)).isEmpty();
+    assertThat(componentIndex.getChildren(project)).containsOnly(dir1, dir2);
+  }
+
+  @Test
+  public void loads_all_necessary_components_from_branch() {
+    componentIndex.load(db.getSession(), List.of(branchDir1));
+    assertThat(componentIndex.getSortedTree()).containsExactly(branchDir1, branch);
+    assertThat(componentIndex.getBranch()).isEqualTo(branch);
+    assertThat(componentIndex.getAllUuids()).containsOnly(branch.uuid(), branchDir1.uuid(), branchDir2.uuid(), branchFile11.uuid(), branchFile12.uuid());
+    assertThat(componentIndex.getChildren(branchDir1)).containsOnly(branchFile11, branchFile12);
+  }
+}
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
new file mode 100644 (file)
index 0000000..64a876b
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * 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();
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java
new file mode 100644 (file)
index 0000000..6ffedfb
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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.List;
+import org.junit.Test;
+import org.sonar.db.issue.HotspotGroupDto;
+
+import static java.util.Collections.emptyList;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class HotspotsCounterTest {
+  @Test
+  public void counts_hotspots() {
+    HotspotGroupDto group1 = new HotspotGroupDto().setCount(3).setStatus("TO_REVIEW").setInLeak(false);
+    HotspotGroupDto group2 = new HotspotGroupDto().setCount(2).setStatus("REVIEWED").setInLeak(false);
+    HotspotGroupDto group3 = new HotspotGroupDto().setCount(1).setStatus("TO_REVIEW").setInLeak(true);
+    HotspotGroupDto group4 = new HotspotGroupDto().setCount(1).setStatus("REVIEWED").setInLeak(true);
+
+    HotspotsCounter counter = new HotspotsCounter(List.of(group1, group2, group3, group4));
+    assertThat(counter.countHotspotsByStatus("TO_REVIEW", true)).isEqualTo(1);
+    assertThat(counter.countHotspotsByStatus("REVIEWED", true)).isEqualTo(1);
+    assertThat(counter.countHotspotsByStatus("TO_REVIEW", false)).isEqualTo(4);
+    assertThat(counter.countHotspotsByStatus("REVIEWED", false)).isEqualTo(3);
+  }
+
+  @Test
+  public void count_empty_hotspots() {
+    HotspotsCounter counter = new HotspotsCounter(emptyList());
+    assertThat(counter.countHotspotsByStatus("TO_REVIEW", true)).isZero();
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java
deleted file mode 100644 (file)
index cf8ca2a..0000000
+++ /dev/null
@@ -1,974 +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.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import org.junit.Test;
-import org.sonar.api.issue.Issue;
-import org.sonar.api.measures.CoreMetrics;
-import org.sonar.api.measures.Metric;
-import org.sonar.api.rule.Severity;
-import org.sonar.api.rules.RuleType;
-import org.sonar.db.component.ComponentDto;
-import org.sonar.db.issue.IssueGroupDto;
-import org.sonar.server.measure.DebtRatingGrid;
-import org.sonar.server.measure.Rating;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class IssueMetricFormulaFactoryImplTest {
-
-  private IssueMetricFormulaFactoryImpl underTest = new IssueMetricFormulaFactoryImpl();
-
-  @Test
-  public void getFormulaMetrics_include_the_dependent_metrics() {
-    for (IssueMetricFormula formula : underTest.getFormulas()) {
-      assertThat(underTest.getFormulaMetrics()).contains(formula.getMetric());
-      for (Metric dependentMetric : formula.getDependentMetrics()) {
-        assertThat(underTest.getFormulaMetrics()).contains(dependentMetric);
-      }
-    }
-  }
-
-  @Test
-  public void test_violations() {
-    withNoIssues().assertThatValueIs(CoreMetrics.VIOLATIONS, 0);
-    with(newGroup(), newGroup().setCount(4)).assertThatValueIs(CoreMetrics.VIOLATIONS, 5);
-
-    // exclude resolved
-    IssueGroupDto resolved = newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED);
-    with(newGroup(), newGroup(), resolved).assertThatValueIs(CoreMetrics.VIOLATIONS, 2);
-
-    // include issues on leak
-    IssueGroupDto onLeak = newGroup().setCount(11).setInLeak(true);
-    with(newGroup(), newGroup(), onLeak).assertThatValueIs(CoreMetrics.VIOLATIONS, 1 + 1 + 11);
-  }
-
-  @Test
-  public void test_bugs() {
-    withNoIssues().assertThatValueIs(CoreMetrics.BUGS, 0);
-    with(
-      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5),
-      // exclude resolved
-      newResolvedGroup(RuleType.BUG).setCount(7),
-      // not bugs
-      newGroup(RuleType.CODE_SMELL).setCount(11))
-        .assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
-  }
-
-  @Test
-  public void test_code_smells() {
-    withNoIssues().assertThatValueIs(CoreMetrics.CODE_SMELLS, 0);
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setCount(5),
-      // exclude resolved
-      newResolvedGroup(RuleType.CODE_SMELL).setCount(7),
-      // not code smells
-      newGroup(RuleType.BUG).setCount(11))
-        .assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
-  }
-
-  @Test
-  public void test_vulnerabilities() {
-    withNoIssues().assertThatValueIs(CoreMetrics.VULNERABILITIES, 0);
-    with(
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5),
-      // exclude resolved
-      newResolvedGroup(RuleType.VULNERABILITY).setCount(7),
-      // not vulnerabilities
-      newGroup(RuleType.BUG).setCount(11))
-        .assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
-  }
-
-  @Test
-  public void test_security_hotspots() {
-    withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 0);
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.CRITICAL).setCount(5),
-      // exclude resolved
-      newResolvedGroup(RuleType.SECURITY_HOTSPOT).setCount(7),
-      // not hotspots
-      newGroup(RuleType.BUG).setCount(11))
-        .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
-  }
-
-  @Test
-  public void test_security_review_rating() {
-    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_REVIEW_RATING, Rating.B);
-
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.A);
-  }
-
-  @Test
-  public void test_security_hotspots_reviewed() {
-    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, 75.0);
-
-    withNoIssues()
-      .assertNoValue(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED);
-  }
-
-  @Test
-  public void test_security_hotspots_reviewed_status() {
-    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);
-
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
-  }
-
-  @Test
-  public void test_security_hotspots_to_review_status() {
-    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);
-
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
-  }
-
-  @Test
-  public void count_unresolved_by_severity() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 0)
-      .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 0)
-      .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 0)
-      .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
-      .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(5),
-      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(7),
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(11),
-      // exclude security hotspot
-      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.CRITICAL).setCount(15),
-      // include leak
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(13),
-      // exclude resolved
-      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);
-  }
-
-  @Test
-  public void count_resolved() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 0)
-      .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 0);
-
-    with(
-      newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED).setCount(3),
-      newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(5),
-      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.MAJOR).setCount(7),
-      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.BLOCKER).setCount(11),
-      newResolvedGroup(Issue.RESOLUTION_REMOVED, Issue.STATUS_CLOSED).setCount(13),
-      // exclude security hotspot
-      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_RESOLVED).setCount(15).setRuleType(RuleType.SECURITY_HOTSPOT.getDbConstant()),
-      // exclude unresolved
-      newGroup(RuleType.VULNERABILITY).setCount(17),
-      newGroup(RuleType.BUG).setCount(19))
-        .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5)
-        .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 7 + 11);
-  }
-
-  @Test
-  public void count_by_status() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 0)
-      .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 0)
-      .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 0);
-
-    with(
-      newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.BLOCKER).setCount(3),
-      newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.INFO).setCount(5),
-      newGroup().setStatus(Issue.STATUS_REOPENED).setCount(7),
-      newGroup(RuleType.CODE_SMELL).setStatus(Issue.STATUS_OPEN).setCount(9),
-      newGroup(RuleType.BUG).setStatus(Issue.STATUS_OPEN).setCount(11),
-      // 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);
-  }
-
-  @Test
-  public void test_technical_debt() {
-    withNoIssues().assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(false),
-      newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(true),
-      // exclude security hotspot
-      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(9).setInLeak(true),
-      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(11).setInLeak(false),
-      // not code smells
-      newGroup(RuleType.BUG).setEffort(7.0),
-      // exclude resolved
-      newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0))
-        .assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
-  }
-
-  @Test
-  public void test_reliability_remediation_effort() {
-    withNoIssues().assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 0);
-
-    with(
-      newGroup(RuleType.BUG).setEffort(3.0),
-      newGroup(RuleType.BUG).setEffort(5.0).setSeverity(Severity.BLOCKER),
-      // not bugs
-      newGroup(RuleType.CODE_SMELL).setEffort(7.0),
-      // exclude resolved
-      newResolvedGroup(RuleType.BUG).setEffort(17.0))
-        .assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
-  }
-
-  @Test
-  public void test_security_remediation_effort() {
-    withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 0);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setEffort(3.0),
-      newGroup(RuleType.VULNERABILITY).setEffort(5.0).setSeverity(Severity.BLOCKER),
-      // not vulnerability
-      newGroup(RuleType.CODE_SMELL).setEffort(7.0),
-      // exclude resolved
-      newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0))
-        .assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
-  }
-
-  @Test
-  public void test_sqale_debt_ratio_and_sqale_rating() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    // technical_debt not computed
-    with(CoreMetrics.DEVELOPMENT_COST, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-    with(CoreMetrics.DEVELOPMENT_COST, 20)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, 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);
-
-    // input measures are available
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 160.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 12.5)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.C);
-
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 10.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 200.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.E);
-
-    // A is 5% --> min debt is exactly 200*0.05=10
-    with(CoreMetrics.DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.TECHNICAL_DEBT, 10.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 5.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 80.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0);
-
-    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    // bug, debt can't be negative
-    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, 80.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-
-    // bug, cost can't be negative
-    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
-      .and(CoreMetrics.DEVELOPMENT_COST, -80.0)
-      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
-      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
-  }
-
-  @Test
-  public void test_effort_to_reach_maintainability_rating_A() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
-
-    // technical_debt not computed
-    with(CoreMetrics.DEVELOPMENT_COST, 0.0)
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
-    with(CoreMetrics.DEVELOPMENT_COST, 20.0)
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 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)
-      // development cost is considered as zero, so the effort is to reach... zero
-      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 20.0);
-
-    // B to A
-    with(CoreMetrics.DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.TECHNICAL_DEBT, 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));
-
-    // E to A
-    with(CoreMetrics.DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.TECHNICAL_DEBT, 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));
-
-    // already A
-    with(CoreMetrics.DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.TECHNICAL_DEBT, 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);
-
-    // exactly lower range of B
-    with(CoreMetrics.DEVELOPMENT_COST, 200.0)
-      .and(CoreMetrics.TECHNICAL_DEBT, 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);
-  }
-
-  @Test
-  public void test_reliability_rating() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
-
-    with(
-      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(1),
-      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);
-
-    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);
-  }
-
-  @Test
-  public void test_security_rating() {
-    withNoIssues()
-      .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(1),
-      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);
-
-    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);
-  }
-
-  @Test
-  public void test_new_bugs() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 0.0);
-
-    with(
-      newGroup(RuleType.BUG).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
-      newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
-      // not bugs
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(9),
-      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
-        .assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);
-  }
-
-  @Test
-  public void test_new_code_smells() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
-      // not code smells
-      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
-      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
-        .assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
-  }
-
-  @Test
-  public void test_new_vulnerabilities() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 0.0);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
-      // not vulnerabilities
-      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
-        .assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
-  }
-
-  @Test
-  public void test_new_security_hotspots() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 0.0);
-
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
-      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
-      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
-      // not hotspots
-      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
-        .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
-  }
-
-  @Test
-  public void test_new_violations() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.BUG).setInLeak(true).setCount(5),
-      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(7),
-      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(9),
-      // not in leak
-      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);
-  }
-
-  @Test
-  public void test_new_blocker_violations() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(7),
-      // not blocker
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
-      // 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);
-  }
-
-  @Test
-  public void test_new_critical_violations() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(7),
-      // not CRITICAL
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(9),
-      // 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);
-  }
-
-  @Test
-  public void test_new_major_violations() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(true).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setInLeak(true).setCount(7),
-      // not MAJOR
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
-      // 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);
-  }
-
-  @Test
-  public void test_new_minor_violations() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(true).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(true).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setInLeak(true).setCount(7),
-      // not MINOR
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
-      // 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);
-  }
-
-  @Test
-  public void test_new_info_violations() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(true).setCount(3),
-      newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(true).setCount(5),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setInLeak(true).setCount(7),
-      // not INFO
-      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
-      // 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);
-  }
-
-  @Test
-  public void test_new_technical_debt() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0);
-
-    with(
-      newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(true),
-      // not in leak
-      newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(false),
-      // not code smells
-      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(9.0).setInLeak(true),
-      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);
-  }
-
-  @Test
-  public void test_new_reliability_remediation_effort() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 0.0);
-
-    with(
-      newGroup(RuleType.BUG).setEffort(3.0).setInLeak(true),
-      // not in leak
-      newGroup(RuleType.BUG).setEffort(5.0).setInLeak(false),
-      // not bugs
-      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);
-  }
-
-  @Test
-  public void test_new_security_remediation_effort() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 0.0);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setEffort(3.0).setInLeak(true),
-      // not in leak
-      newGroup(RuleType.VULNERABILITY).setEffort(5.0).setInLeak(false),
-      // not vulnerability
-      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);
-  }
-
-  @Test
-  public void test_new_reliability_rating() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.A);
-
-    with(
-      newGroup(RuleType.BUG).setSeverity(Severity.INFO).setCount(3).setInLeak(true),
-      newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(1).setInLeak(true),
-      // not in leak
-      newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false),
-      // not bug
-      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);
-  }
-
-  @Test
-  public void test_new_security_rating() {
-    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.A);
-
-    with(
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(3).setInLeak(true),
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(1).setInLeak(true),
-      // not in leak
-      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(false),
-      // not vulnerability
-      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);
-  }
-
-  @Test
-  public void test_new_security_review_rating() {
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).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(CoreMetrics.NEW_SECURITY_REVIEW_RATING, Rating.B);
-
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REVIEW_RATING, Rating.A);
-  }
-
-  @Test
-  public void test_new_security_hotspots_reviewed() {
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
-      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);
-
-    withNoIssues()
-      .assertNoLeakValue(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED);
-  }
-
-  @Test
-  public void test_new_security_hotspots_reviewed_status() {
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
-      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);
-
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
-  }
-
-  @Test
-  public void test_new_security_hotspots_to_review_status() {
-    with(
-      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
-      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);
-
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
-  }
-
-  @Test
-  public void test_new_sqale_debt_ratio_and_new_maintainability_rating() {
-    withNoIssues()
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-
-    // technical_debt not computed
-    withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-    withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 20)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-
-    // development_cost not computed
-    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-
-    // input measures are available
-    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.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, 20.0)
-      .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)
-      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 10.0)
-      .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
-    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)
-      .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)
-      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0);
-
-    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.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)
-      .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)
-      .andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, -80.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
-      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
-  }
-
-  private Verifier with(IssueGroupDto... groups) {
-    return new Verifier(groups);
-  }
-
-  private Verifier withNoIssues() {
-    return new Verifier(new IssueGroupDto[0]);
-  }
-
-  private Verifier with(Metric metric, double value) {
-    return new Verifier(new IssueGroupDto[0]).and(metric, value);
-  }
-
-  private Verifier withLeak(Metric metric, double leakValue) {
-    return new Verifier(new IssueGroupDto[0]).andLeak(metric, leakValue);
-  }
-
-  private class Verifier {
-    private final IssueGroupDto[] groups;
-    private final Map<Metric, Double> values = new HashMap<>();
-    private final Map<Metric, Double> leakValues = new HashMap<>();
-
-    private Verifier(IssueGroupDto[] groups) {
-      this.groups = groups;
-    }
-
-    Verifier and(Metric metric, double value) {
-      this.values.put(metric, value);
-      return this;
-    }
-
-    Verifier andLeak(Metric metric, double value) {
-      this.leakValues.put(metric, value);
-      return this;
-    }
-
-    Verifier assertThatValueIs(Metric metric, double expectedValue) {
-      TestContext context = run(metric, false);
-      assertThat(context.doubleValue).isNotNull().isEqualTo(expectedValue);
-      return this;
-    }
-
-    Verifier assertThatLeakValueIs(Metric metric, double expectedValue) {
-      TestContext context = run(metric, true);
-      assertThat(context.doubleLeakValue).isNotNull().isEqualTo(expectedValue);
-      return this;
-    }
-
-    Verifier assertThatLeakValueIs(Metric metric, Rating expectedRating) {
-      TestContext context = run(metric, true);
-      assertThat(context.ratingLeakValue).isNotNull().isEqualTo(expectedRating);
-      return this;
-    }
-
-    Verifier assertNoLeakValue(Metric metric) {
-      TestContext context = run(metric, true);
-      assertThat(context.ratingLeakValue).isNull();
-      return this;
-    }
-
-    Verifier assertThatValueIs(Metric metric, Rating expectedValue) {
-      TestContext context = run(metric, false);
-      assertThat(context.ratingValue).isNotNull().isEqualTo(expectedValue);
-      return this;
-    }
-
-    Verifier assertNoValue(Metric metric) {
-      TestContext context = run(metric, false);
-      assertThat(context.ratingValue).isNull();
-      return this;
-    }
-
-    private TestContext run(Metric metric, boolean expectLeakFormula) {
-      IssueMetricFormula formula = underTest.getFormulas().stream()
-        .filter(f -> f.getMetric().getKey().equals(metric.getKey()))
-        .findFirst()
-        .get();
-      assertThat(formula.isOnLeak()).isEqualTo(expectLeakFormula);
-      TestContext context = new TestContext(formula.getDependentMetrics(), values, leakValues);
-      formula.compute(context, newIssueCounter(groups));
-      return context;
-    }
-  }
-
-  private static IssueCounter newIssueCounter(IssueGroupDto... issues) {
-    return new IssueCounter(asList(issues));
-  }
-
-  private static IssueGroupDto newGroup() {
-    return newGroup(RuleType.CODE_SMELL);
-  }
-
-  private static IssueGroupDto newGroup(RuleType ruleType) {
-    IssueGroupDto dto = new IssueGroupDto();
-    // set non-null fields
-    dto.setRuleType(ruleType.getDbConstant());
-    dto.setCount(1);
-    dto.setEffort(0.0);
-    dto.setSeverity(Severity.INFO);
-    dto.setStatus(Issue.STATUS_OPEN);
-    dto.setInLeak(false);
-    return dto;
-  }
-
-  private static IssueGroupDto newResolvedGroup(RuleType ruleType) {
-    return newGroup(ruleType).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setStatus(Issue.STATUS_CLOSED);
-  }
-
-  private static IssueGroupDto newResolvedGroup(String resolution, String status) {
-    return newGroup().setResolution(resolution).setStatus(status);
-  }
-
-  private static class TestContext implements IssueMetricFormula.Context {
-    private final Set<Metric> dependentMetrics;
-    private Double doubleValue;
-    private Rating ratingValue;
-    private Double doubleLeakValue;
-    private Rating ratingLeakValue;
-    private final Map<Metric, Double> values;
-    private final Map<Metric, Double> leakValues;
-
-    private TestContext(Collection<Metric> dependentMetrics, Map<Metric, Double> values, Map<Metric, Double> leakValues) {
-      this.dependentMetrics = new HashSet<>(dependentMetrics);
-      this.values = values;
-      this.leakValues = leakValues;
-    }
-
-    @Override
-    public ComponentDto getComponent() {
-      throw new UnsupportedOperationException();
-    }
-
-    @Override
-    public DebtRatingGrid getDebtRatingGrid() {
-      return new DebtRatingGrid(new double[] {0.05, 0.1, 0.2, 0.5});
-    }
-
-    @Override
-    public Optional<Double> getValue(Metric metric) {
-      if (!dependentMetrics.contains(metric)) {
-        throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency");
-      }
-      if (values.containsKey(metric)) {
-        return Optional.of(values.get(metric));
-      }
-      return Optional.empty();
-    }
-
-    @Override
-    public Optional<Double> getLeakValue(Metric metric) {
-      if (!dependentMetrics.contains(metric)) {
-        throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency");
-      }
-      if (leakValues.containsKey(metric)) {
-        return Optional.of(leakValues.get(metric));
-      }
-      return Optional.empty();
-    }
-
-    @Override
-    public void setValue(double value) {
-      this.doubleValue = value;
-    }
-
-    @Override
-    public void setValue(Rating value) {
-      this.ratingValue = value;
-    }
-
-    @Override
-    public void setLeakValue(double value) {
-      this.doubleLeakValue = value;
-    }
-
-    @Override
-    public void setLeakValue(Rating value) {
-      this.ratingLeakValue = value;
-    }
-  }
-}
index 947ffbf49d413dc2ccc8feeec1d9f69379b6b0f6..3c43901f3422d0f44f0f23455d194a3daeacc76c 100644 (file)
  */
 package org.sonar.server.measure.live;
 
-import com.tngtech.java.junit.dataprovider.DataProvider;
 import com.tngtech.java.junit.dataprovider.DataProviderRunner;
-import com.tngtech.java.junit.dataprovider.UseDataProvider;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
+import java.util.Set;
 import javax.annotation.Nullable;
 import org.junit.Before;
 import org.junit.Rule;
@@ -36,7 +30,6 @@ import org.junit.runner.RunWith;
 import org.sonar.api.config.Configuration;
 import org.sonar.api.config.PropertyDefinitions;
 import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.measures.CoreMetrics;
 import org.sonar.api.measures.Metric;
 import org.sonar.api.resources.Qualifiers;
 import org.sonar.api.utils.System2;
@@ -44,35 +37,23 @@ import org.sonar.core.config.CorePropertyDefinitions;
 import org.sonar.db.DbSession;
 import org.sonar.db.DbTester;
 import org.sonar.db.component.BranchDto;
-import org.sonar.db.component.BranchType;
 import org.sonar.db.component.ComponentDto;
-import org.sonar.db.component.ComponentTesting;
-import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.component.SnapshotDto;
 import org.sonar.db.metric.MetricDto;
-import org.sonar.db.newcodeperiod.NewCodePeriodType;
-import org.sonar.db.project.ProjectDto;
-import org.sonar.server.es.ProjectIndexer;
 import org.sonar.server.es.TestProjectIndexers;
-import org.sonar.server.measure.Rating;
 import org.sonar.server.qualitygate.EvaluatedQualityGate;
 import org.sonar.server.qualitygate.QualityGate;
 import org.sonar.server.qualitygate.changeevent.QGChangeEvent;
 import org.sonar.server.setting.ProjectConfigurationLoader;
 import org.sonar.server.setting.TestProjectConfigurationLoader;
 
-import static java.util.Arrays.asList;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.argThat;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.same;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
-import static org.sonar.api.resources.Qualifiers.ORDERED_BOTTOM_UP;
+import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY;
 
 @RunWith(DataProviderRunner.class)
 public class LiveMeasureComputerImplTest {
@@ -81,471 +62,119 @@ public class LiveMeasureComputerImplTest {
   public DbTester db = DbTester.create();
 
   private final TestProjectIndexers projectIndexer = new TestProjectIndexers();
-  private MetricDto intMetric;
-  private MetricDto ratingMetric;
-  private MetricDto alertStatusMetric;
+  private MetricDto metric1;
+  private MetricDto metric2;
   private ComponentDto project;
-  private ProjectDto projectDto;
-  private ComponentDto dir;
-  private ComponentDto file1;
-  private ComponentDto file2;
-  private ComponentDto prBranch;
-  private ComponentDto prBranchFile;
-  private ComponentDto branch;
-  private ComponentDto branchFile;
+
   private final LiveQualityGateComputer qGateComputer = mock(LiveQualityGateComputer.class);
   private final QualityGate qualityGate = mock(QualityGate.class);
   private final EvaluatedQualityGate newQualityGate = mock(EvaluatedQualityGate.class);
+  private final Configuration configuration = new MapSettings(new PropertyDefinitions(System2.INSTANCE, CorePropertyDefinitions.all())).asConfig();
+  private final ProjectConfigurationLoader configurationLoader = new TestProjectConfigurationLoader(configuration);
+  private final MeasureUpdateFormulaFactory measureUpdateFormulaFactory = mock(MeasureUpdateFormulaFactory.class);
+  private final ComponentIndexFactory componentIndexFactory = mock(ComponentIndexFactory.class);
+  private final ComponentIndex componentIndex = mock(ComponentIndex.class);
+  private final FakeLiveMeasureTreeUpdater treeUpdater = new FakeLiveMeasureTreeUpdater();
+  private final LiveMeasureComputerImpl liveMeasureComputer = new LiveMeasureComputerImpl(db.getDbClient(), measureUpdateFormulaFactory, componentIndexFactory,
+    qGateComputer, configurationLoader, projectIndexer, treeUpdater);
+  private BranchDto branch;
 
   @Before
   public void setUp() {
-    intMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.INT.name()));
-    ratingMetric = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.RATING.name()));
-    alertStatusMetric = db.measures().insertMetric(m -> m.setKey(CoreMetrics.ALERT_STATUS_KEY));
-    project = db.components().insertPublicProject();
-    projectDto = db.components().getProjectDto(project);
-    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));
-
-    prBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.PULL_REQUEST));
-    prBranchFile = db.components().insertComponent(ComponentTesting.newFileDto(prBranch));
-
-    branch = db.components().insertProjectBranch(project);
-    branchFile = db.components().insertComponent(ComponentTesting.newFileDto(branch));
-  }
-
-  @Test
-  public void compute_and_insert_measures_if_they_do_not_exist_yet() {
-    markProjectAsAnalyzed(project);
-
-    List<QGChangeEvent> result = run(asList(file1, file2), newQualifierBasedIntFormula(), newRatingConstantFormula(Rating.C));
-
-    // 2 measures per component have been created
-    // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(8);
-    assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatRatingMeasureHasValue(file1, Rating.C);
-    assertThatIntMeasureHasValue(file2, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatRatingMeasureHasValue(file2, Rating.C);
-    assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
-    assertThatRatingMeasureHasValue(dir, Rating.C);
-    assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
-    assertThatRatingMeasureHasValue(project, Rating.C);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void compute_and_update_measures_if_they_already_exist() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0));
-    db.measures().insertLiveMeasure(dir, intMetric, m -> m.setValue(42.0));
-    db.measures().insertLiveMeasure(file1, intMetric, m -> m.setValue(42.0));
-    db.measures().insertLiveMeasure(file2, intMetric, m -> m.setValue(42.0));
-
-    // generates values 1, 2, 3
-    List<QGChangeEvent> result = run(file1, newQualifierBasedIntFormula());
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
-    assertThatProjectChanged(result, project);
-
-    // Numeric value depends on qualifier (see newQualifierBasedIntFormula())
-    assertThatIntMeasureHasValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatIntMeasureHasValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
-    assertThatIntMeasureHasValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
-    // untouched
-    assertThatIntMeasureHasValue(file2, 42.0);
-  }
-
-  @Test
-  public void variation_is_refreshed_when_int_value_is_changed() {
-    markProjectAsAnalyzed(project);
-    // value is:
-    // 42 on last analysis
-    // 42-12=30 on beginning of leak period
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setValue(42.0).setVariation(12.0));
-
-    // new value is 44, so variation on leak period is 44-30=14
-    List<QGChangeEvent> result = run(file1, newIntConstantFormula(44.0));
-
-    LiveMeasureDto measure = assertThatIntMeasureHasValue(project, 44.0);
-    assertThat(measure.getVariation()).isEqualTo(14.0);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void variation_is_refreshed_when_rating_value_is_changed() {
-    markProjectAsAnalyzed(project);
-    // value is:
-    // B on last analysis
-    // D on beginning of leak period --> variation is -2
-    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
-
-    // new value is C, so variation on leak period is D to C = -1
-    List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.C));
-
-    LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.C);
-    assertThat(measure.getVariation()).isEqualTo(-1.0);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void variation_does_not_change_if_rating_value_does_not_change() {
-    markProjectAsAnalyzed(project);
-    // value is:
-    // B on last analysis
-    // D on beginning of leak period --> variation is -2
-    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.B.getIndex()).setData("B").setVariation(-2.0));
-
-    // new value is still B, so variation on leak period is still -2
-    List<QGChangeEvent> result = run(file1, newRatingConstantFormula(Rating.B));
-
-    LiveMeasureDto measure = assertThatRatingMeasureHasValue(project, Rating.B);
-    assertThat(measure.getVariation()).isEqualTo(-2.0);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void refresh_leak_measures() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
-    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setVariation((double) Rating.E.getIndex()));
-    db.measures().insertLiveMeasure(dir, intMetric, m -> m.setVariation(42.0).setValue(null));
-    db.measures().insertLiveMeasure(dir, ratingMetric, m -> m.setVariation((double) Rating.D.getIndex()));
-    db.measures().insertLiveMeasure(file1, intMetric, m -> m.setVariation(42.0).setValue(null));
-    db.measures().insertLiveMeasure(file1, ratingMetric, m -> m.setVariation((double) Rating.C.getIndex()));
-
-    // generates values 1, 2, 3 on leak measures
-    List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(6);
-
-    // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
-    assertThatIntMeasureHasLeakValue(file1, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatRatingMeasureHasLeakValue(file1, Rating.B);
-    assertThatIntMeasureHasLeakValue(dir, ORDERED_BOTTOM_UP.indexOf(Qualifiers.DIRECTORY));
-    assertThatRatingMeasureHasLeakValue(dir, Rating.B);
-    assertThatIntMeasureHasLeakValue(project, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
-    assertThatRatingMeasureHasLeakValue(project, Rating.B);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void refresh_after_first_analysis() {
-    markProjectAsAnalyzed(project, null);
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(null).setValue(42.0));
-    db.measures().insertLiveMeasure(project, ratingMetric, m -> m.setValue((double) Rating.E.getIndex()).setData(Rating.E.name()));
-    db.measures().insertLiveMeasure(dir, intMetric, m -> m.setVariation(null).setValue(42.0));
-    db.measures().insertLiveMeasure(dir, ratingMetric, m -> m.setValue((double) Rating.D.getIndex()).setData(Rating.D.name()));
-    db.measures().insertLiveMeasure(file1, intMetric, m -> m.setVariation(null).setValue(42.0));
-    db.measures().insertLiveMeasure(file1, ratingMetric, m -> m.setValue((double) Rating.C.getIndex()).setData(Rating.C.name()));
-
-    List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula(), newIntConstantFormula(1337));
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(6);
-
-    assertThatIntMeasureHasValue(file1, 1337);
-    assertThatRatingMeasureHasValue(file1, Rating.C);
-    assertThatIntMeasureHasValue(dir, 1337);
-    assertThatRatingMeasureHasValue(dir, Rating.D);
-    assertThatIntMeasureHasValue(project, 1337);
-    assertThatRatingMeasureHasValue(project, Rating.E);
-    assertThatProjectChanged(result, project);
-  }
-
-  @Test
-  public void calculate_new_metrics_if_it_is_pr_or_branch() {
-    markProjectAsAnalyzed(prBranch, null);
-    db.measures().insertLiveMeasure(prBranch, intMetric, m -> m.setVariation(42.0).setValue(null));
-    db.measures().insertLiveMeasure(prBranchFile, intMetric, m -> m.setVariation(42.0).setValue(null));
-
-    // generates values 1, 2, 3 on leak measures
-    List<QGChangeEvent> result = run(prBranchFile, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
-
-    // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
-    assertThatIntMeasureHasLeakValue(prBranchFile, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatRatingMeasureHasLeakValue(prBranchFile, Rating.B);
-    assertThatIntMeasureHasLeakValue(prBranch, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
-    assertThatRatingMeasureHasLeakValue(prBranch, Rating.B);
-    assertThatProjectChanged(result, prBranch);
-  }
-
-  @Test
-  public void calculate_new_metrics_if_it_is_branch_using_new_code_reference() {
-    markProjectAsAnalyzed(branch, null, NewCodePeriodType.REFERENCE_BRANCH);
-    db.measures().insertLiveMeasure(branch, intMetric, m -> m.setVariation(42.0).setValue(null));
-    db.measures().insertLiveMeasure(branchFile, intMetric, m -> m.setVariation(42.0).setValue(null));
-
-    // generates values 1, 2, 3 on leak measures
-    List<QGChangeEvent> result = run(branchFile, newQualifierBasedIntLeakFormula(), newRatingLeakFormula(Rating.B));
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(4);
-
-    // Numeric value depends on qualifier (see newQualifierBasedIntLeakFormula())
-    assertThatIntMeasureHasLeakValue(branchFile, ORDERED_BOTTOM_UP.indexOf(Qualifiers.FILE));
-    assertThatRatingMeasureHasLeakValue(branchFile, Rating.B);
-    assertThatIntMeasureHasLeakValue(branch, ORDERED_BOTTOM_UP.indexOf(Qualifiers.PROJECT));
-    assertThatRatingMeasureHasLeakValue(branch, Rating.B);
-    assertThatProjectChanged(result, branch);
-  }
-
-  @Test
-  public void do_nothing_if_project_has_not_been_analyzed() {
-    // project has no snapshots
-    List<QGChangeEvent> result = run(file1, newIncrementalFormula());
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isZero();
-    assertThatProjectNotChanged(result, project);
-  }
-
-  @Test
-  public void do_nothing_if_input_components_are_empty() {
-    List<QGChangeEvent> result = run(emptyList(), newIncrementalFormula());
-
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isZero();
-    assertThatProjectNotChanged(result, project);
-  }
-
-  @Test
-  public void refresh_multiple_projects_at_the_same_time() {
-    markProjectAsAnalyzed(project);
-    ComponentDto project2 = db.components().insertPublicProject();
-    ComponentDto fileInProject2 = db.components().insertComponent(ComponentTesting.newFileDto(project2));
-    markProjectAsAnalyzed(project2);
-
-    List<QGChangeEvent> result = run(asList(file1, fileInProject2), newQualifierBasedIntFormula());
-
-    // generated values depend on position of qualifier in Qualifiers.ORDERED_BOTTOM_UP (see formula)
-    assertThatIntMeasureHasValue(file1, 0);
-    assertThatIntMeasureHasValue(dir, 2);
-    assertThatIntMeasureHasValue(project, 4);
-    assertThatIntMeasureHasValue(fileInProject2, 0);
-    assertThatIntMeasureHasValue(project2, 4);
-
-    // no other measures generated
-    assertThat(db.countRowsOfTable(db.getSession(), "live_measures")).isEqualTo(5);
-    assertThatProjectChanged(result, project, project2);
-  }
-
-  @Test
-  public void refresh_multiple_branches_at_the_same_time() {
-    // FIXME
-  }
-
-  @Test
-  public void event_contains_no_previousStatus_if_measure_does_not_exist() {
-    markProjectAsAnalyzed(project);
-
-    List<QGChangeEvent> result = run(file1);
-
-    assertThat(result)
-      .extracting(QGChangeEvent::getPreviousStatus)
-      .containsExactly(Optional.empty());
-  }
-
-  @Test
-  public void event_contains_no_previousStatus_if_measure_exists_and_has_no_value() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData((String) null));
+    metric1 = db.measures().insertMetric();
+    metric2 = db.measures().insertMetric();
 
-    List<QGChangeEvent> result = run(file1);
+    project = db.components().insertPublicProject();
+    branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.uuid()).get();
+    db.measures().insertLiveMeasure(project, metric2, lm -> lm.setValue(1d));
 
-    assertThat(result)
-      .extracting(QGChangeEvent::getPreviousStatus)
-      .containsExactly(Optional.empty());
+    when(componentIndexFactory.create(any(), any())).thenReturn(componentIndex);
+    when(measureUpdateFormulaFactory.getFormulaMetrics()).thenReturn(Set.of(toMetric(metric1), toMetric(metric2)));
+    when(componentIndex.getBranch()).thenReturn(project);
   }
 
   @Test
-  public void event_contains_no_previousStatus_if_measure_exists_and_is_empty() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(""));
+  public void loads_measure_matrix_and_calls_tree_updater() {
+    SnapshotDto snapshot = markProjectAsAnalyzed(project);
+    when(componentIndex.getAllUuids()).thenReturn(Set.of(project.uuid()));
 
-    List<QGChangeEvent> result = run(file1);
+    liveMeasureComputer.refresh(db.getSession(), List.of(project));
 
-    assertThat(result)
-      .extracting(QGChangeEvent::getPreviousStatus)
-      .containsExactly(Optional.empty());
-  }
+    // tree updater was called
+    assertThat(treeUpdater.getMeasureMatrix()).isNotNull();
 
-  @Test
-  public void event_contains_no_previousStatus_if_measure_exists_and_is_not_a_level() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData("fooBar"));
+    // measure matrix was loaded with formula's metrics and measures
+    assertThat(treeUpdater.getMeasureMatrix().getMetricByUuid(metric2.getUuid())).isNotNull();
+    assertThat(treeUpdater.getMeasureMatrix().getMeasure(project, metric2.getKey()).get().getValue()).isEqualTo(1d);
 
-    List<QGChangeEvent> result = run(file1);
-
-    assertThat(result)
-      .extracting(QGChangeEvent::getPreviousStatus)
-      .containsExactly(Optional.empty());
+    // new measures were persisted
+    assertThat(db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), project.uuid(), metric1.getKey()).get().getValue()).isEqualTo(2d);
   }
 
   @Test
-  @UseDataProvider("metricLevels")
-  public void event_contains_previousStatus_if_measure_exists(Metric.Level level) {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(level.name()));
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
-
-    List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula());
+  public void refreshes_quality_gate() {
+    SnapshotDto snapshot = markProjectAsAnalyzed(project);
+    when(componentIndex.getAllUuids()).thenReturn(Set.of(project.uuid()));
+    when(qGateComputer.loadQualityGate(db.getSession(), db.components().getProjectDto(project), branch)).thenReturn(qualityGate);
 
-    assertThat(result)
-      .extracting(QGChangeEvent::getPreviousStatus)
-      .containsExactly(Optional.of(level));
-  }
+    liveMeasureComputer.refresh(db.getSession(), List.of(project));
 
-  @DataProvider
-  public static Object[][] metricLevels() {
-    return Arrays.stream(Metric.Level.values())
-      .map(l -> new Object[] {l})
-      .toArray(Object[][]::new);
+    verify(qGateComputer).refreshGateStatus(eq(project), eq(qualityGate), any(MeasureMatrix.class), eq(configuration));
   }
 
   @Test
-  public void event_contains_newQualityGate_computed_by_LiveQualityGateComputer() {
-    markProjectAsAnalyzed(project);
-    db.measures().insertLiveMeasure(project, alertStatusMetric, m -> m.setData(Metric.Level.ERROR.name()));
-    db.measures().insertLiveMeasure(project, intMetric, m -> m.setVariation(42.0).setValue(null));
-    BranchDto branch = db.getDbClient().branchDao().selectByBranchKey(db.getSession(), project.projectUuid(), "master")
-      .orElseThrow(() -> new IllegalStateException("Can't find master branch"));
-
-    List<QGChangeEvent> result = run(file1, newQualifierBasedIntLeakFormula());
-
-    assertThat(result)
-      .extracting(QGChangeEvent::getQualityGateSupplier)
-      .extracting(Supplier::get)
-      .containsExactly(Optional.of(newQualityGate));
-    verify(qGateComputer).loadQualityGate(any(DbSession.class), argThat(p -> p.getUuid().equals(projectDto.getUuid())), eq(branch));
-    verify(qGateComputer).getMetricsRelatedTo(qualityGate);
-    verify(qGateComputer).refreshGateStatus(eq(project), same(qualityGate), any(MeasureMatrix.class), any(Configuration.class));
+  public void return_if_no_analysis_found() {
+    liveMeasureComputer.refresh(db.getSession(), List.of(project));
+    assertThat(treeUpdater.getMeasureMatrix()).isNull();
   }
 
   @Test
-  public void exception_describes_context_when_a_formula_fails() {
-    markProjectAsAnalyzed(project);
-    Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
-
-    assertThatThrownBy(() -> {
-      run(project, new IssueMetricFormula(metric, false, (context, issueCounter) -> {
-        throw new NullPointerException("BOOM");
-      }));
-    })
-      .isInstanceOf(IllegalStateException.class)
-      .hasMessage("Fail to compute " + metric.getKey() + " on " + project.getDbKey());
-  }
-
-  private List<QGChangeEvent> run(ComponentDto component, IssueMetricFormula... formulas) {
-    return run(singleton(component), formulas);
-  }
-
-  private List<QGChangeEvent> run(Collection<ComponentDto> components, IssueMetricFormula... formulas) {
-    IssueMetricFormulaFactory formulaFactory = new TestIssueMetricFormulaFactory(asList(formulas));
+  public void returns_qgate_event() {
+    SnapshotDto snapshot = markProjectAsAnalyzed(project);
+    when(componentIndex.getAllUuids()).thenReturn(Set.of(project.uuid()));
 
-    when(qGateComputer.loadQualityGate(any(DbSession.class), any(ProjectDto.class), any(BranchDto.class)))
-      .thenReturn(qualityGate);
-    when(qGateComputer.getMetricsRelatedTo(qualityGate)).thenReturn(singleton(CoreMetrics.ALERT_STATUS_KEY));
-    when(qGateComputer.refreshGateStatus(eq(project), same(qualityGate), any(MeasureMatrix.class), any(Configuration.class)))
-      .thenReturn(newQualityGate);
-    MapSettings settings = new MapSettings(new PropertyDefinitions(System2.INSTANCE, CorePropertyDefinitions.all()));
-    ProjectConfigurationLoader configurationLoader = new TestProjectConfigurationLoader(settings.asConfig());
+    MetricDto alertStatusMetric = db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY));
+    db.measures().insertLiveMeasure(project, alertStatusMetric, lm -> lm.setData("OK"));
 
-    LiveMeasureComputerImpl underTest = new LiveMeasureComputerImpl(db.getDbClient(), formulaFactory, qGateComputer, configurationLoader, projectIndexer);
+    when(qGateComputer.loadQualityGate(db.getSession(), db.components().getProjectDto(project), branch)).thenReturn(qualityGate);
+    when(qGateComputer.refreshGateStatus(eq(project), eq(qualityGate), any(MeasureMatrix.class), eq(configuration))).thenReturn(newQualityGate);
 
-    return underTest.refresh(db.getSession(), components);
-  }
+    List<QGChangeEvent> qgChangeEvents = liveMeasureComputer.refresh(db.getSession(), List.of(project));
 
-  private void markProjectAsAnalyzed(ComponentDto p) {
-    markProjectAsAnalyzed(p, 1_490_000_000L);
+    assertThat(qgChangeEvents).hasSize(1);
+    assertThat(qgChangeEvents.get(0).getBranch()).isEqualTo(branch);
+    assertThat(qgChangeEvents.get(0).getAnalysis()).isEqualTo(snapshot);
+    assertThat(qgChangeEvents.get(0).getProject()).isEqualTo(db.components().getProjectDto(project));
+    assertThat(qgChangeEvents.get(0).getPreviousStatus()).contains(Metric.Level.OK);
+    assertThat(qgChangeEvents.get(0).getProjectConfiguration()).isEqualTo(configuration);
+    assertThat(qgChangeEvents.get(0).getQualityGateSupplier().get()).contains(newQualityGate);
   }
 
-  private void markProjectAsAnalyzed(ComponentDto p, @Nullable Long periodDate) {
-    assertThat(p.qualifier()).isEqualTo(Qualifiers.PROJECT);
-    markProjectAsAnalyzed(p, periodDate, null);
+  private SnapshotDto markProjectAsAnalyzed(ComponentDto p) {
+    return markProjectAsAnalyzed(p, 1_490_000_000L);
   }
 
-  private void markProjectAsAnalyzed(ComponentDto p, @Nullable Long periodDate, @Nullable NewCodePeriodType type) {
+  private SnapshotDto markProjectAsAnalyzed(ComponentDto p, @Nullable Long periodDate) {
     assertThat(p.qualifier()).isEqualTo(Qualifiers.PROJECT);
-    db.components().insertSnapshot(p, s -> s.setPeriodDate(periodDate).setPeriodMode(type != null ? type.name() : null));
-  }
-
-  private LiveMeasureDto assertThatIntMeasureHasValue(ComponentDto component, double expectedValue) {
-    LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get();
-    assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
-    assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
-    assertThat(measure.getMetricUuid()).isEqualTo(intMetric.getUuid());
-    assertThat(measure.getValue()).isEqualTo(expectedValue);
-    return measure;
-  }
-
-  private LiveMeasureDto assertThatRatingMeasureHasValue(ComponentDto component, Rating expectedRating) {
-    LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get();
-    assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
-    assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
-    assertThat(measure.getMetricUuid()).isEqualTo(ratingMetric.getUuid());
-    assertThat(measure.getValue()).isEqualTo(expectedRating.getIndex());
-    assertThat(measure.getDataAsString()).isEqualTo(expectedRating.name());
-    return measure;
-  }
-
-  private void assertThatIntMeasureHasLeakValue(ComponentDto component, double expectedValue) {
-    LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), intMetric.getKey()).get();
-    assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
-    assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
-    assertThat(measure.getMetricUuid()).isEqualTo(intMetric.getUuid());
-    assertThat(measure.getValue()).isNull();
-    assertThat(measure.getVariation()).isEqualTo(expectedValue);
-  }
-
-  private void assertThatRatingMeasureHasLeakValue(ComponentDto component, Rating expectedValue) {
-    LiveMeasureDto measure = db.getDbClient().liveMeasureDao().selectMeasure(db.getSession(), component.uuid(), ratingMetric.getKey()).get();
-    assertThat(measure.getComponentUuid()).isEqualTo(component.uuid());
-    assertThat(measure.getProjectUuid()).isEqualTo(component.projectUuid());
-    assertThat(measure.getMetricUuid()).isEqualTo(ratingMetric.getUuid());
-    assertThat(measure.getVariation()).isEqualTo(expectedValue.getIndex());
-  }
-
-  private IssueMetricFormula newIncrementalFormula() {
-    Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
-    AtomicInteger counter = new AtomicInteger();
-    return new IssueMetricFormula(metric, false, (ctx, issues) -> ctx.setValue(counter.incrementAndGet()));
-  }
-
-  private IssueMetricFormula newIntConstantFormula(double constant) {
-    Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
-    return new IssueMetricFormula(metric, false, (ctx, issues) -> ctx.setValue(constant));
+    return db.components().insertSnapshot(p, s -> s.setPeriodDate(periodDate));
   }
 
-  private IssueMetricFormula newRatingConstantFormula(Rating constant) {
-    Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create();
-    return new IssueMetricFormula(metric, false, (ctx, issues) -> ctx.setValue(constant));
+  private static Metric<?> toMetric(MetricDto metric) {
+    return new Metric.Builder(metric.getKey(), metric.getShortName(), Metric.ValueType.valueOf(metric.getValueType())).create();
   }
 
-  private IssueMetricFormula newRatingLeakFormula(Rating rating) {
-    Metric metric = new Metric.Builder(ratingMetric.getKey(), ratingMetric.getShortName(), Metric.ValueType.valueOf(ratingMetric.getValueType())).create();
-    return new IssueMetricFormula(metric, true, (ctx, issues) -> ctx.setLeakValue(rating));
-  }
-
-  private IssueMetricFormula newQualifierBasedIntFormula() {
-    Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
-    return new IssueMetricFormula(metric, false, (ctx, issues) -> ctx.setValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier())));
-  }
+  private class FakeLiveMeasureTreeUpdater implements LiveMeasureTreeUpdater {
+    private MeasureMatrix measureMatrix;
 
-  private IssueMetricFormula newQualifierBasedIntLeakFormula() {
-    Metric metric = new Metric.Builder(intMetric.getKey(), intMetric.getShortName(), Metric.ValueType.valueOf(intMetric.getValueType())).create();
-    return new IssueMetricFormula(metric, true, (ctx, issues) -> ctx.setLeakValue(ORDERED_BOTTOM_UP.indexOf(ctx.getComponent().qualifier())));
-  }
-
-  private void assertThatProjectChanged(List<QGChangeEvent> events, ComponentDto... projects) {
-    for (ComponentDto p : projects) {
-      assertThat(projectIndexer.hasBeenCalled(p.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isTrue();
+    @Override
+    public void update(DbSession dbSession, SnapshotDto lastAnalysis, Configuration config, ComponentIndex components, BranchDto branch, MeasureMatrix measures) {
+      this.measureMatrix = measures;
+      measures.setValue(project, metric1.getKey(), 2d);
     }
 
-    assertThat(events).extracting(e -> e.getBranch().getUuid())
-      .containsExactlyInAnyOrder(Arrays.stream(projects).map(ComponentDto::uuid).toArray(String[]::new));
+    public MeasureMatrix getMeasureMatrix() {
+      return measureMatrix;
+    }
   }
 
-  private void assertThatProjectNotChanged(List<QGChangeEvent> events, ComponentDto project) {
-    assertThat(projectIndexer.hasBeenCalled(project.uuid(), ProjectIndexer.Cause.MEASURE_CHANGE)).isFalse();
-    assertThat(events).isEmpty();
-  }
 }
index b8ec7cc9df37e5dfcf16ef972a4e150489825d5d..b615830425d68edee6ee3db4ea0f73e9535ffdd1 100644 (file)
@@ -29,6 +29,6 @@ public class LiveMeasureModuleTest {
   public void verify_count_of_added_components() {
     ListContainer container = new ListContainer();
     new LiveMeasureModule().configure(container);
-    assertThat(container.getAddedObjects()).hasSize(3);
+    assertThat(container.getAddedObjects()).isNotEmpty();
   }
 }
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java
new file mode 100644 (file)
index 0000000..2f0413d
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * 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 java.util.Set;
+import org.junit.Before;
+import org.junit.Rule;
+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.db.DbTester;
+import org.sonar.db.component.BranchDto;
+import org.sonar.db.component.BranchType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.component.ComponentTesting;
+import org.sonar.db.component.SnapshotDto;
+import org.sonar.db.issue.IssueDto;
+import org.sonar.db.measure.LiveMeasureDto;
+import org.sonar.db.metric.MetricDto;
+import org.sonar.db.newcodeperiod.NewCodePeriodType;
+import org.sonar.db.rule.RuleDto;
+
+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;
+
+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;
+  private MetricDto metricDto;
+  private Metric metric;
+  private ComponentDto project;
+  private BranchDto branch;
+  private ComponentDto dir;
+  private ComponentDto file1;
+  private ComponentDto file2;
+  private SnapshotDto snapshot;
+
+  @Before
+  public void setUp() {
+    // insert project and file structure
+    project = db.components().insertPrivateProject();
+    branch = db.getDbClient().branchDao().selectByUuid(db.getSession(), project.uuid()).get();
+    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
+    metricDto = db.measures().insertMetric(m -> m.setValueType(Metric.ValueType.INT.name()));
+    metric = new Metric.Builder(metricDto.getKey(), metricDto.getShortName(), Metric.ValueType.valueOf(metricDto.getValueType())).create();
+    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), List.of());
+    componentIndex = new ComponentIndexImpl(db.getDbClient());
+  }
+
+  @Test
+  public void should_aggregate_values_up_the_hierarchy() {
+    snapshot = db.components().insertSnapshot(project);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new AggregateValuesFormula(), hotspotMeasureUpdater);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    List<LiveMeasureDto> initialValues = List.of(
+      new LiveMeasureDto().setComponentUuid(file1.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()),
+      new LiveMeasureDto().setComponentUuid(file2.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()),
+      new LiveMeasureDto().setComponentUuid(dir.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid()),
+      new LiveMeasureDto().setComponentUuid(project.uuid()).setValue(1d).setMetricUuid(metricDto.getUuid())
+    );
+    matrix = new MeasureMatrix(List.of(project, dir, file1, file2), List.of(metricDto), initialValues);
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid());
+    assertThat(matrix.getMeasure(project, metric.getKey()).get().getValue()).isEqualTo(4d);
+    assertThat(matrix.getMeasure(dir, metric.getKey()).get().getValue()).isEqualTo(3d);
+    assertThat(matrix.getMeasure(file1, metric.getKey()).get().getValue()).isEqualTo(1d);
+    assertThat(matrix.getMeasure(file2, metric.getKey()).get().getValue()).isEqualTo(1d);
+  }
+
+  @Test
+  public void should_set_values_up_the_hierarchy() {
+    snapshot = db.components().insertSnapshot(project);
+    treeUpdater = new LiveMeasureTreeUpdaterImpl(db.getDbClient(), new SetValuesFormula(), hotspotMeasureUpdater);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid());
+    assertThat(matrix.getMeasure(project, metric.getKey()).get().getValue()).isEqualTo(1d);
+    assertThat(matrix.getMeasure(dir, metric.getKey()).get().getValue()).isEqualTo(1d);
+    assertThat(matrix.getMeasure(file1, metric.getKey()).get().getValue()).isEqualTo(1d);
+    assertThat(matrix.getMeasure(file2, metric.getKey())).isEmpty();
+  }
+
+  @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);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).isEmpty();
+  }
+
+  @Test
+  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);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid());
+  }
+
+  @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);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getChanged()).extracting(LiveMeasureDto::getComponentUuid).containsOnly(project.uuid(), dir.uuid(), file1.uuid());
+  }
+
+  @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);
+
+    RuleDto rule = db.rules().insert();
+    IssueDto issue1 = db.issues().insertIssue(rule, project, file1);
+    IssueDto issue2 = db.issues().insertIssue(rule, project, file1);
+    db.issues().insertNewCodeReferenceIssue(issue1);
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+    assertThat(matrix.getMeasure(file1, metric.getKey()).get().getVariation()).isEqualTo(1d);
+  }
+
+  @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);
+
+    db.issues().insertIssue(i -> i.setIssueCreationDate(new Date(999)).setComponentUuid(file1.uuid()));
+    db.issues().insertIssue(i -> i.setIssueCreationDate(new Date(1001)).setComponentUuid(file1.uuid()));
+    db.issues().insertIssue(i -> i.setIssueCreationDate(new Date(1002)).setComponentUuid(file1.uuid()));
+
+    componentIndex.load(db.getSession(), List.of(file1));
+    treeUpdater.update(db.getSession(), snapshot, config, componentIndex, branch, matrix);
+
+    assertThat(matrix.getMeasure(file1, metric.getKey()).get().getVariation()).isEqualTo(2d);
+  }
+
+  @Test
+  public void calls_hotspot_updater() {
+    snapshot = db.components().insertSnapshot(project, s -> s.setPeriodDate(1000L));
+
+    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));
+  }
+
+  private class AggregateValuesFormula implements MeasureUpdateFormulaFactory {
+    @Override
+    public List<MeasureUpdateFormula> getFormulas() {
+      return List.of(new MeasureUpdateFormula(metric, false, new MeasureUpdateFormulaFactoryImpl.AddChildren(), (c, i) -> {
+      }));
+    }
+
+    @Override
+    public Set<Metric> getFormulaMetrics() {
+      return Set.of(metric);
+    }
+  }
+
+  private class SetValuesFormula implements MeasureUpdateFormulaFactory {
+    @Override
+    public List<MeasureUpdateFormula> getFormulas() {
+      return List.of(new MeasureUpdateFormula(metric, false, (c, m) -> {
+      }, (c, i) -> c.setValue(1d)));
+    }
+
+    @Override
+    public Set<Metric> getFormulaMetrics() {
+      return Set.of(metric);
+    }
+  }
+
+  private class CountUnresolvedInLeak implements MeasureUpdateFormulaFactory {
+    @Override
+    public List<MeasureUpdateFormula> getFormulas() {
+      return List.of(new MeasureUpdateFormula(metric, true, (c, m) -> {
+      }, (c, i) -> c.setLeakValue(i.countUnresolved(true))));
+    }
+
+    @Override
+    public Set<Metric> getFormulaMetrics() {
+      return Set.of(metric);
+    }
+  }
+}
index 1b706dc432c0f6c7df4644d22d95739182a3d60e..e5869c20a74578f5643ea25fb066e4aa2b1351d7 100644 (file)
@@ -42,7 +42,6 @@ public class MeasureMatrixTest {
   private static final MetricDto METRIC_1 = newMetricDto().setUuid("100");
   private static final MetricDto METRIC_2 = newMetricDto().setUuid("200");
 
-
   @Test
   public void getMetric() {
     Collection<MetricDto> metrics = asList(METRIC_1, METRIC_2);
@@ -128,7 +127,7 @@ public class MeasureMatrixTest {
 
     assertThat(underTest.getChanged()).hasSize(1);
     verifyValue(underTest, PROJECT, metric, 3.56);
-    verifyVariation(underTest, PROJECT, metric, 3.56 - (3.14 - 1.14));
+    verifyVariation(underTest, PROJECT, metric, 1.14);
   }
 
   @Test
@@ -138,10 +137,11 @@ public class MeasureMatrixTest {
     MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure));
 
     underTest.setValue(PROJECT, metric.getKey(), 3.569);
+    underTest.setLeakValue(PROJECT, metric.getKey(), 3.569);
 
     assertThat(underTest.getChanged()).hasSize(1);
     verifyValue(underTest, PROJECT, metric, 3.57);
-    verifyVariation(underTest, PROJECT, metric, 1.57);
+    verifyVariation(underTest, PROJECT, metric, 3.57);
   }
 
   @Test
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java
new file mode 100644 (file)
index 0000000..bf7548d
--- /dev/null
@@ -0,0 +1,1136 @@
+/*
+ * 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.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.junit.Test;
+import org.sonar.api.issue.Issue;
+import org.sonar.api.measures.CoreMetrics;
+import org.sonar.api.measures.Metric;
+import org.sonar.api.rule.Severity;
+import org.sonar.api.rules.RuleType;
+import org.sonar.db.component.ComponentDto;
+import org.sonar.db.issue.IssueGroupDto;
+import org.sonar.server.measure.DebtRatingGrid;
+import org.sonar.server.measure.Rating;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.api.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;
+import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_REVIEW_RATING;
+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;
+
+public class MeasureUpdateFormulaFactoryImplTest {
+
+  private final MeasureUpdateFormulaFactoryImpl underTest = new MeasureUpdateFormulaFactoryImpl();
+
+  @Test
+  public void getFormulaMetrics_include_the_dependent_metrics() {
+    for (MeasureUpdateFormula formula : underTest.getFormulas()) {
+      assertThat(underTest.getFormulaMetrics()).contains(formula.getMetric());
+      for (Metric<?> dependentMetric : formula.getDependentMetrics()) {
+        assertThat(underTest.getFormulaMetrics()).contains(dependentMetric);
+      }
+    }
+  }
+
+  @Test
+  public void hierarchy_adding_numbers() {
+    new HierarchyTester(CoreMetrics.VIOLATIONS)
+      .withValue(1d)
+      .withChildrenValues(2d, 3d)
+      .expectedResult(6d);
+
+    new HierarchyTester(CoreMetrics.BUGS)
+      .withValue(0d)
+      .withChildrenValues(2d, 3d)
+      .expectedResult(5d);
+
+    new HierarchyTester(CoreMetrics.NEW_BUGS)
+      .withValue(1d)
+      .expectedResult(1d);
+  }
+
+  @Test
+  public void hierarchy_highest_rating() {
+    new HierarchyTester(CoreMetrics.RELIABILITY_RATING)
+      .withValue(1d)
+      .withChildrenValues(2d, 3d)
+      .expectedRating(Rating.C);
+
+    // if no children, no need to set a value
+    new HierarchyTester(CoreMetrics.SECURITY_RATING)
+      .withValue(1d)
+      .expectedResult(null);
+
+    new HierarchyTester(CoreMetrics.NEW_RELIABILITY_RATING)
+      .withValue(5d)
+      .withChildrenValues(2d, 3d)
+      .expectedRating(Rating.E);
+  }
+
+  @Test
+  public void hierarchy_combining_other_metrics() {
+    new HierarchyTester(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED)
+      .withValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1d)
+      .withValue(SECURITY_HOTSPOTS_REVIEWED_STATUS, 1d)
+      .expectedResult(50d);
+    new HierarchyTester(CoreMetrics.SECURITY_REVIEW_RATING)
+      .withValue(SECURITY_HOTSPOTS_REVIEWED, 100d)
+      .expectedRating(Rating.A);
+
+    new HierarchyTester(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED)
+      .withValue(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 1d)
+      .withValue(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 1d)
+      .expectedResult(50d);
+    new HierarchyTester(CoreMetrics.NEW_SECURITY_REVIEW_RATING)
+      .withValue(NEW_SECURITY_HOTSPOTS_REVIEWED, 0d)
+      .expectedRating(Rating.E);
+  }
+
+  @Test
+  public void test_violations() {
+    withNoIssues().assertThatValueIs(CoreMetrics.VIOLATIONS, 0);
+    with(newGroup(), newGroup().setCount(4)).assertThatValueIs(CoreMetrics.VIOLATIONS, 5);
+
+    // exclude resolved
+    IssueGroupDto resolved = newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED);
+    with(newGroup(), newGroup(), resolved).assertThatValueIs(CoreMetrics.VIOLATIONS, 2);
+
+    // include issues on leak
+    IssueGroupDto onLeak = newGroup().setCount(11).setInLeak(true);
+    with(newGroup(), newGroup(), onLeak).assertThatValueIs(CoreMetrics.VIOLATIONS, 1 + 1 + 11);
+  }
+
+  @Test
+  public void test_bugs() {
+    withNoIssues().assertThatValueIs(CoreMetrics.BUGS, 0);
+    with(
+      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5),
+      // exclude resolved
+      newResolvedGroup(RuleType.BUG).setCount(7),
+      // not bugs
+      newGroup(RuleType.CODE_SMELL).setCount(11))
+      .assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
+  }
+
+  @Test
+  public void test_code_smells() {
+    withNoIssues().assertThatValueIs(CoreMetrics.CODE_SMELLS, 0);
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setCount(5),
+      // exclude resolved
+      newResolvedGroup(RuleType.CODE_SMELL).setCount(7),
+      // not code smells
+      newGroup(RuleType.BUG).setCount(11))
+      .assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
+  }
+
+  @Test
+  public void test_vulnerabilities() {
+    withNoIssues().assertThatValueIs(CoreMetrics.VULNERABILITIES, 0);
+    with(
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5),
+      // exclude resolved
+      newResolvedGroup(RuleType.VULNERABILITY).setCount(7),
+      // not vulnerabilities
+      newGroup(RuleType.BUG).setCount(11))
+      .assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
+  }
+
+  @Test
+  public void test_security_hotspots() {
+    withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 0);
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.CRITICAL).setCount(5),
+      // exclude resolved
+      newResolvedGroup(RuleType.SECURITY_HOTSPOT).setCount(7),
+      // not hotspots
+      newGroup(RuleType.BUG).setCount(11))
+      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
+  }
+
+  @Test
+  public void test_security_review_rating() {
+    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);
+
+    withNoIssues()
+      .assertThatValueIs(SECURITY_REVIEW_RATING, Rating.A);
+  }
+
+  @Test
+  public void test_security_hotspots_reviewed() {
+    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);
+
+    withNoIssues()
+      .assertNoValue(SECURITY_HOTSPOTS_REVIEWED);
+  }
+
+  @Test
+  public void test_security_hotspots_reviewed_status() {
+    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);
+
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
+  }
+
+  @Test
+  public void test_security_hotspots_to_review_status() {
+    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);
+
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
+  }
+
+  @Test
+  public void count_unresolved_by_severity() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 0)
+      .assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 0)
+      .assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 0)
+      .assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
+      .assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(5),
+      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(7),
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(11),
+      // exclude security hotspot
+      newGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.CRITICAL).setCount(15),
+      // include leak
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(13),
+      // exclude resolved
+      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);
+  }
+
+  @Test
+  public void count_resolved() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 0)
+      .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 0);
+
+    with(
+      newResolvedGroup(Issue.RESOLUTION_FIXED, Issue.STATUS_RESOLVED).setCount(3),
+      newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(5),
+      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.MAJOR).setCount(7),
+      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_CLOSED).setSeverity(Severity.BLOCKER).setCount(11),
+      newResolvedGroup(Issue.RESOLUTION_REMOVED, Issue.STATUS_CLOSED).setCount(13),
+      // exclude security hotspot
+      newResolvedGroup(Issue.RESOLUTION_WONT_FIX, Issue.STATUS_RESOLVED).setCount(15).setRuleType(RuleType.SECURITY_HOTSPOT.getDbConstant()),
+      // exclude unresolved
+      newGroup(RuleType.VULNERABILITY).setCount(17),
+      newGroup(RuleType.BUG).setCount(19))
+      .assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5)
+      .assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 7 + 11);
+  }
+
+  @Test
+  public void count_by_status() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 0)
+      .assertThatValueIs(CoreMetrics.OPEN_ISSUES, 0)
+      .assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 0);
+
+    with(
+      newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.BLOCKER).setCount(3),
+      newGroup().setStatus(Issue.STATUS_CONFIRMED).setSeverity(Severity.INFO).setCount(5),
+      newGroup().setStatus(Issue.STATUS_REOPENED).setCount(7),
+      newGroup(RuleType.CODE_SMELL).setStatus(Issue.STATUS_OPEN).setCount(9),
+      newGroup(RuleType.BUG).setStatus(Issue.STATUS_OPEN).setCount(11),
+      // 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);
+  }
+
+  @Test
+  public void test_technical_debt() {
+    withNoIssues().assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(false),
+      newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(true),
+      // exclude security hotspot
+      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(9).setInLeak(true),
+      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(11).setInLeak(false),
+      // not code smells
+      newGroup(RuleType.BUG).setEffort(7.0),
+      // exclude resolved
+      newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0))
+      .assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
+  }
+
+  @Test
+  public void test_reliability_remediation_effort() {
+    withNoIssues().assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 0);
+
+    with(
+      newGroup(RuleType.BUG).setEffort(3.0),
+      newGroup(RuleType.BUG).setEffort(5.0).setSeverity(Severity.BLOCKER),
+      // not bugs
+      newGroup(RuleType.CODE_SMELL).setEffort(7.0),
+      // exclude resolved
+      newResolvedGroup(RuleType.BUG).setEffort(17.0))
+      .assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
+  }
+
+  @Test
+  public void test_security_remediation_effort() {
+    withNoIssues().assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 0);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setEffort(3.0),
+      newGroup(RuleType.VULNERABILITY).setEffort(5.0).setSeverity(Severity.BLOCKER),
+      // not vulnerability
+      newGroup(RuleType.CODE_SMELL).setEffort(7.0),
+      // exclude resolved
+      newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0))
+      .assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
+  }
+
+  @Test
+  public void test_sqale_debt_ratio_and_sqale_rating() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+
+    // technical_debt not computed
+    with(CoreMetrics.DEVELOPMENT_COST, "0")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+    with(CoreMetrics.DEVELOPMENT_COST, "20")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, 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);
+
+    // input measures are available
+    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "0")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+
+    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "160")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 12.5)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.C);
+
+    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "10")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 200.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.E);
+
+    // 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);
+
+    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "0")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+
+    with(CoreMetrics.TECHNICAL_DEBT, 0.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "80")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0);
+
+    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "0")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+
+    // bug, debt can't be negative
+    with(CoreMetrics.TECHNICAL_DEBT, -20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "80")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+
+    // bug, cost can't be negative
+    with(CoreMetrics.TECHNICAL_DEBT, 20.0)
+      .andText(CoreMetrics.DEVELOPMENT_COST, "-80")
+      .assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
+      .assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
+  }
+
+  @Test
+  public void test_effort_to_reach_maintainability_rating_A() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+
+    // technical_debt not computed
+    with(CoreMetrics.DEVELOPMENT_COST, 0.0)
+      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 0.0);
+    with(CoreMetrics.DEVELOPMENT_COST, 20.0)
+      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 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)
+      // development cost is considered as zero, so the effort is to reach... zero
+      .assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 20.0);
+
+    // B to A
+    with(CoreMetrics.DEVELOPMENT_COST, "200")
+      .and(CoreMetrics.TECHNICAL_DEBT, 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));
+
+    // E to A
+    with(CoreMetrics.DEVELOPMENT_COST, "200")
+      .and(CoreMetrics.TECHNICAL_DEBT, 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));
+
+    // already A
+    with(CoreMetrics.DEVELOPMENT_COST, "200")
+      .and(CoreMetrics.TECHNICAL_DEBT, 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);
+
+    // exactly lower range of B
+    with(CoreMetrics.DEVELOPMENT_COST, "200")
+      .and(CoreMetrics.TECHNICAL_DEBT, 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);
+  }
+
+  @Test
+  public void test_reliability_rating() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(1),
+      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);
+
+    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);
+  }
+
+  @Test
+  public void test_security_rating() {
+    withNoIssues()
+      .assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(1),
+      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);
+
+    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);
+  }
+
+  @Test
+  public void test_new_bugs() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 0.0);
+
+    with(
+      newGroup(RuleType.BUG).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
+      newGroup(RuleType.BUG).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
+      // not bugs
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(9),
+      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
+      .assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);
+
+  }
+
+  @Test
+  public void test_new_code_smells() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
+      // not code smells
+      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
+      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
+      .assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
+  }
+
+  @Test
+  public void test_new_vulnerabilities() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 0.0);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
+      // not vulnerabilities
+      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
+      .assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
+  }
+
+  @Test
+  public void test_new_security_hotspots() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 0.0);
+
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(false).setSeverity(Severity.MAJOR).setCount(3),
+      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(true).setSeverity(Severity.CRITICAL).setCount(5),
+      newGroup(RuleType.SECURITY_HOTSPOT).setInLeak(true).setSeverity(Severity.MINOR).setCount(7),
+      // not hotspots
+      newGroup(RuleType.BUG).setInLeak(true).setCount(9),
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
+      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
+  }
+
+  @Test
+  public void test_new_violations() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.BUG).setInLeak(true).setCount(5),
+      newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(7),
+      newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(9),
+      // not in leak
+      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);
+  }
+
+  @Test
+  public void test_new_blocker_violations() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true).setCount(7),
+      // not blocker
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
+      // 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);
+  }
+
+  @Test
+  public void test_new_critical_violations() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(7),
+      // not CRITICAL
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(9),
+      // 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);
+  }
+
+  @Test
+  public void test_new_major_violations() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(true).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(true).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MAJOR).setInLeak(true).setCount(7),
+      // not MAJOR
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
+      // 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);
+  }
+
+  @Test
+  public void test_new_minor_violations() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(true).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(true).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setInLeak(true).setCount(7),
+      // not MINOR
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
+      // 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);
+  }
+
+  @Test
+  public void test_new_info_violations() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(true).setCount(3),
+      newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(true).setCount(5),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setInLeak(true).setCount(7),
+      // not INFO
+      newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(true).setCount(9),
+      // 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);
+  }
+
+  @Test
+  public void test_new_technical_debt() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 0.0);
+
+    with(
+      newGroup(RuleType.CODE_SMELL).setEffort(3.0).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.CODE_SMELL).setEffort(5.0).setInLeak(false),
+      // not code smells
+      newGroup(RuleType.SECURITY_HOTSPOT).setEffort(9.0).setInLeak(true),
+      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);
+  }
+
+  @Test
+  public void test_new_reliability_remediation_effort() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 0.0);
+
+    with(
+      newGroup(RuleType.BUG).setEffort(3.0).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.BUG).setEffort(5.0).setInLeak(false),
+      // not bugs
+      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);
+  }
+
+  @Test
+  public void test_new_security_remediation_effort() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 0.0);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setEffort(3.0).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.VULNERABILITY).setEffort(5.0).setInLeak(false),
+      // not vulnerability
+      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);
+  }
+
+  @Test
+  public void test_new_reliability_rating() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.BUG).setSeverity(Severity.INFO).setCount(3).setInLeak(true),
+      newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(1).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false),
+      // not bug
+      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);
+  }
+
+  @Test
+  public void test_new_security_rating() {
+    withNoIssues().assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.A);
+
+    with(
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(3).setInLeak(true),
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(1).setInLeak(true),
+      // not in leak
+      newGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(false),
+      // not vulnerability
+      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);
+  }
+
+  @Test
+  public void test_new_security_review_rating() {
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).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.B);
+
+    withNoIssues()
+      .assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.A);
+  }
+
+  @Test
+  public void test_new_security_hotspots_reviewed() {
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
+      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);
+
+    withNoIssues()
+      .assertNoLeakValue(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED);
+  }
+
+  @Test
+  public void test_new_security_hotspots_reviewed_status() {
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
+      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);
+
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, 0.0);
+  }
+
+  @Test
+  public void test_new_security_hotspots_to_review_status() {
+    with(
+      newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_REVIEWED).setCount(3).setInLeak(true),
+      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);
+
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, 0.0);
+  }
+
+  @Test
+  public void test_new_sqale_debt_ratio_and_new_maintainability_rating() {
+    withNoIssues()
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+
+    // technical_debt not computed
+    withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+    withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 20)
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+
+    // development_cost not computed
+    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20)
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+
+    // input measures are available
+    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.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, 20.0)
+      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "160")
+      .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")
+      .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")
+      .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")
+      .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")
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0);
+
+    withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
+      .andText(CoreMetrics.NEW_DEVELOPMENT_COST, "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")
+      .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")
+      .assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
+      .assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
+  }
+
+  private Verifier with(IssueGroupDto... groups) {
+    return new Verifier(groups);
+  }
+
+  private Verifier withNoIssues() {
+    return new Verifier(new IssueGroupDto[0]);
+  }
+
+  private Verifier with(Metric metric, double value) {
+    return new Verifier(new IssueGroupDto[0]).and(metric, value);
+  }
+
+  private Verifier with(Metric metric, String value) {
+    return new Verifier(new IssueGroupDto[0]).andText(metric, value);
+  }
+
+  private Verifier withLeak(Metric metric, double leakValue) {
+    return new Verifier(new IssueGroupDto[0]).andLeak(metric, leakValue);
+  }
+
+  private class Verifier {
+    private final IssueGroupDto[] groups;
+    private final InitialValues initialValues = new InitialValues();
+
+    private Verifier(IssueGroupDto[] groups) {
+      this.groups = groups;
+    }
+
+    Verifier and(Metric metric, double value) {
+      this.initialValues.values.put(metric, value);
+      return this;
+    }
+
+    Verifier andLeak(Metric metric, double value) {
+      this.initialValues.leakValues.put(metric, value);
+      return this;
+    }
+
+    Verifier andText(Metric metric, String value) {
+      this.initialValues.text.put(metric, value);
+      return this;
+    }
+
+    Verifier assertThatValueIs(Metric metric, double expectedValue) {
+      TestContext context = run(metric, false);
+      assertThat(context.doubleValue).isNotNull().isEqualTo(expectedValue);
+      return this;
+    }
+
+    Verifier assertThatLeakValueIs(Metric metric, double expectedValue) {
+      TestContext context = run(metric, true);
+      assertThat(context.doubleLeakValue).isNotNull().isEqualTo(expectedValue);
+      return this;
+    }
+
+    Verifier assertThatLeakValueIs(Metric metric, Rating expectedRating) {
+      TestContext context = run(metric, true);
+      assertThat(context.ratingLeakValue).isNotNull().isEqualTo(expectedRating);
+      return this;
+    }
+
+    Verifier assertNoLeakValue(Metric metric) {
+      TestContext context = run(metric, true);
+      assertThat(context.ratingLeakValue).isNull();
+      return this;
+    }
+
+    Verifier assertThatValueIs(Metric metric, Rating expectedValue) {
+      TestContext context = run(metric, false);
+      assertThat(context.ratingValue).isNotNull().isEqualTo(expectedValue);
+      return this;
+    }
+
+    Verifier assertNoValue(Metric metric) {
+      TestContext context = run(metric, false);
+      assertThat(context.ratingValue).isNull();
+      return this;
+    }
+
+    private TestContext run(Metric metric, boolean expectLeakFormula) {
+      MeasureUpdateFormula formula = underTest.getFormulas().stream()
+        .filter(f -> f.getMetric().getKey().equals(metric.getKey()))
+        .findFirst()
+        .get();
+      assertThat(formula.isOnLeak()).isEqualTo(expectLeakFormula);
+      TestContext context = new TestContext(formula.getDependentMetrics(), initialValues);
+      formula.compute(context, newIssueCounter(groups));
+      return context;
+    }
+  }
+
+  private static IssueCounter newIssueCounter(IssueGroupDto... issues) {
+    return new IssueCounter(asList(issues));
+  }
+
+  private static IssueGroupDto newGroup() {
+    return newGroup(RuleType.CODE_SMELL);
+  }
+
+  private static IssueGroupDto newGroup(RuleType ruleType) {
+    IssueGroupDto dto = new IssueGroupDto();
+    // set non-null fields
+    dto.setRuleType(ruleType.getDbConstant());
+    dto.setCount(1);
+    dto.setEffort(0.0);
+    dto.setSeverity(Severity.INFO);
+    dto.setStatus(Issue.STATUS_OPEN);
+    dto.setInLeak(false);
+    return dto;
+  }
+
+  private static IssueGroupDto newResolvedGroup(RuleType ruleType) {
+    return newGroup(ruleType).setResolution(Issue.RESOLUTION_FALSE_POSITIVE).setStatus(Issue.STATUS_CLOSED);
+  }
+
+  private static IssueGroupDto newResolvedGroup(String resolution, String status) {
+    return newGroup().setResolution(resolution).setStatus(status);
+  }
+
+  private static class TestContext implements MeasureUpdateFormula.Context {
+    private final Set<Metric> dependentMetrics;
+    private final InitialValues initialValues;
+    private Double doubleValue;
+    private Rating ratingValue;
+    private Double doubleLeakValue;
+    private Rating ratingLeakValue;
+
+    private TestContext(Collection<Metric> dependentMetrics, InitialValues initialValues) {
+      this.dependentMetrics = new HashSet<>(dependentMetrics);
+      this.initialValues = initialValues;
+    }
+
+    @Override public List<Double> getChildrenValues() {
+      return initialValues.childrenValues;
+    }
+
+    @Override public List<Double> getChildrenLeakValues() {
+      return initialValues.childrenLeakValues;
+    }
+
+    @Override
+    public ComponentDto getComponent() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public DebtRatingGrid getDebtRatingGrid() {
+      return new DebtRatingGrid(new double[] {0.05, 0.1, 0.2, 0.5});
+    }
+
+    @Override
+    public Optional<Double> getValue(Metric metric) {
+      if (!dependentMetrics.contains(metric)) {
+        throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency");
+      }
+      if (initialValues.values.containsKey(metric)) {
+        return Optional.of(initialValues.values.get(metric));
+      }
+      return Optional.empty();
+    }
+
+    @Override
+    public Optional<String> getText(Metric metric) {
+      if (initialValues.text.containsKey(metric)) {
+        return Optional.of(initialValues.text.get(metric));
+      }
+      return Optional.empty();
+    }
+
+    @Override
+    public Optional<Double> getLeakValue(Metric metric) {
+      if (!dependentMetrics.contains(metric)) {
+        throw new IllegalStateException("Metric " + metric.getKey() + " is not declared as a dependency");
+      }
+      if (initialValues.leakValues.containsKey(metric)) {
+        return Optional.of(initialValues.leakValues.get(metric));
+      }
+      return Optional.empty();
+    }
+
+    @Override
+    public void setValue(double value) {
+      this.doubleValue = value;
+    }
+
+    @Override
+    public void setValue(Rating value) {
+      this.ratingValue = value;
+    }
+
+    @Override
+    public void setLeakValue(double value) {
+      this.doubleLeakValue = value;
+    }
+
+    @Override
+    public void setLeakValue(Rating value) {
+      this.ratingLeakValue = value;
+    }
+  }
+
+  private class InitialValues {
+    private final Map<Metric, Double> values = new HashMap<>();
+    private final Map<Metric, Double> leakValues = new HashMap<>();
+    private final List<Double> childrenValues = new ArrayList<>();
+    private final List<Double> childrenLeakValues = new ArrayList<>();
+    private final Map<Metric, String> text = new HashMap<>();
+  }
+
+  private class HierarchyTester {
+    private final Metric metric;
+    private final InitialValues initialValues;
+    private final MeasureUpdateFormula formula;
+
+    public HierarchyTester(Metric metric) {
+      this.metric = metric;
+      this.initialValues = new InitialValues();
+      this.formula = underTest.getFormulas().stream().filter(f -> f.getMetric().equals(metric)).findAny().get();
+    }
+
+    public HierarchyTester withValue(Metric metric, Double value) {
+      if (formula.isOnLeak()) {
+        this.initialValues.leakValues.put(metric, value);
+      } else {
+        this.initialValues.values.put(metric, value);
+      }
+      return this;
+    }
+
+    public HierarchyTester withValue(Double value) {
+      return withValue(metric, value);
+    }
+
+    public HierarchyTester withChildrenValues(Double... values) {
+      if (formula.isOnLeak()) {
+        this.initialValues.childrenLeakValues.addAll(asList(values));
+      } else {
+        this.initialValues.childrenValues.addAll(asList(values));
+      }
+      return this;
+    }
+
+    public HierarchyTester expectedResult(@Nullable Double expected) {
+      TestContext ctx = run();
+      if (formula.isOnLeak()) {
+        assertThat(ctx.doubleLeakValue).isEqualTo(expected);
+      } else {
+        assertThat(ctx.doubleValue).isEqualTo(expected);
+      }
+      return this;
+    }
+
+    public HierarchyTester expectedRating(@Nullable Rating rating) {
+      TestContext ctx = run();
+      if (formula.isOnLeak()) {
+        assertThat(ctx.ratingLeakValue).isEqualTo(rating);
+      } else {
+        assertThat(ctx.ratingValue).isEqualTo(rating);
+      }
+      return this;
+    }
+
+    private TestContext run() {
+      List<Metric> deps = new LinkedList<>(formula.getDependentMetrics());
+      deps.add(formula.getMetric());
+      deps.addAll(initialValues.values.keySet());
+      deps.addAll(initialValues.leakValues.keySet());
+      deps.addAll(initialValues.text.keySet());
+      TestContext context = new TestContext(deps, initialValues);
+      formula.computeHierarchy(context);
+      return context;
+    }
+  }
+}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java
deleted file mode 100644 (file)
index bdf2124..0000000
+++ /dev/null
@@ -1,43 +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.List;
-import java.util.Set;
-import org.sonar.api.measures.Metric;
-
-class TestIssueMetricFormulaFactory implements IssueMetricFormulaFactory {
-
-  private final List<IssueMetricFormula> formulas;
-
-  TestIssueMetricFormulaFactory(List<IssueMetricFormula> formulas) {
-    this.formulas = formulas;
-  }
-
-  @Override
-  public List<IssueMetricFormula> getFormulas() {
-    return formulas;
-  }
-
-  @Override
-  public Set<Metric> getFormulaMetrics() {
-    return IssueMetricFormulaFactory.extractMetrics(formulas);
-  }
-}
diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java
new file mode 100644 (file)
index 0000000..81da23c
--- /dev/null
@@ -0,0 +1,43 @@
+/*
+ * 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.List;
+import java.util.Set;
+import org.sonar.api.measures.Metric;
+
+class TestMeasureUpdateFormulaFactory implements MeasureUpdateFormulaFactory {
+
+  private final List<MeasureUpdateFormula> formulas;
+
+  TestMeasureUpdateFormulaFactory(List<MeasureUpdateFormula> formulas) {
+    this.formulas = formulas;
+  }
+
+  @Override
+  public List<MeasureUpdateFormula> getFormulas() {
+    return formulas;
+  }
+
+  @Override
+  public Set<Metric> getFormulaMetrics() {
+    return MeasureUpdateFormulaFactory.extractMetrics(formulas);
+  }
+}