Browse Source

SONAR-11401 Performance hotspot when changing state of issue

tags/9.5.0.56709
Duarte Meneses 2 years ago
parent
commit
928f60ac33
35 changed files with 2141 additions and 1190 deletions
  1. 6
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java
  2. 2
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java
  3. 53
    0
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java
  4. 6
    2
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java
  5. 3
    3
      server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java
  6. 11
    0
      server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml
  7. 90
    29
      server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml
  8. 27
    0
      server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java
  9. 96
    112
      server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java
  10. 2
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java
  11. 54
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java
  12. 39
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java
  13. 110
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java
  14. 60
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java
  15. 63
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java
  16. 0
    240
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java
  17. 39
    178
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java
  18. 4
    1
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java
  19. 29
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java
  20. 217
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java
  21. 2
    4
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java
  22. 29
    63
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java
  23. 24
    5
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java
  24. 3
    3
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java
  25. 299
    0
      server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java
  26. 40
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java
  27. 96
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java
  28. 119
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java
  29. 49
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java
  30. 73
    444
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java
  31. 1
    1
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java
  32. 233
    0
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java
  33. 3
    3
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java
  34. 254
    92
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java
  35. 5
    5
      server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java

+ 6
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java View 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()) {

+ 2
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java View 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

+ 53
- 0
server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java View File

@@ -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;
}
}

+ 6
- 2
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java View 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) {

+ 3
- 3
server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java View 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,

+ 11
- 0
server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml View File

@@ -375,6 +375,17 @@
</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"/>

+ 90
- 29
server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml View File

@@ -347,7 +347,67 @@
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
@@ -356,50 +416,50 @@
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>
@@ -471,6 +531,7 @@
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,

+ 27
- 0
server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java View 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.

+ 96
- 112
server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java View 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"));

+ 2
- 5
server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java View 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());

+ 54
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java View File

@@ -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();
}

+ 39
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java View File

@@ -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;
}
}

+ 110
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java View File

@@ -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;
}
}

+ 60
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java View File

@@ -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)));
}
}
}

+ 63
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java View File

@@ -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();
}
}
}
}

+ 0
- 240
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java View File

@@ -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;
}
}

+ 39
- 178
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java View 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);
}
}
}

+ 4
- 1
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java View 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);
}
}

+ 29
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java View File

@@ -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);
}

+ 217
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java View File

@@ -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);
}
}
}

+ 2
- 4
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java View 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();

+ 29
- 63
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java View File

@@ -20,17 +20,19 @@
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());
}
}
}

server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java → server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java View File

@@ -20,6 +20,7 @@
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;
@@ -29,20 +30,28 @@ import org.sonar.server.measure.Rating;

import static java.util.Collections.emptyList;

class IssueMetricFormula {
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;

IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula) {
this(metric, onLeak, formula, emptyList());
/**
* @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());
}

IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula, Collection<Metric> dependentMetrics) {
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;
}
@@ -63,7 +72,15 @@ class IssueMetricFormula {
formula.accept(context, issues);
}

void computeHierarchy(Context context) {
hierarchyFormula.accept(context, this);
}

interface Context {
List<Double> getChildrenValues();

List<Double> getChildrenLeakValues();

ComponentDto getComponent();

DebtRatingGrid getDebtRatingGrid();
@@ -72,10 +89,12 @@ class IssueMetricFormula {
* 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()}).
* (see {@link MeasureUpdateFormula#getDependentMetrics()}).
*/
Optional<Double> getValue(Metric metric);

Optional<String> getText(Metric metrc);

Optional<Double> getLeakValue(Metric metric);

void setValue(double value);

server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java → server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java View File

@@ -27,12 +27,12 @@ import org.sonar.api.measures.Metric;
import org.sonar.api.server.ServerSide;

@ServerSide
public interface IssueMetricFormulaFactory {
List<IssueMetricFormula> getFormulas();
public interface MeasureUpdateFormulaFactory {
List<MeasureUpdateFormula> getFormulas();

Set<Metric> getFormulaMetrics();

static Set<Metric> extractMetrics(List<IssueMetricFormula> formulas) {
static Set<Metric> extractMetrics(List<MeasureUpdateFormula> formulas) {
return formulas.stream()
.flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream()))
.collect(Collectors.toSet());

+ 299
- 0
server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java View File

@@ -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;
}
}

+ 40
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java View File

@@ -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());
}
}

+ 96
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java View File

@@ -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);
}
}

+ 119
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java View File

@@ -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();
}
}

+ 49
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java View File

@@ -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();
}
}

+ 73
- 444
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java View File

@@ -19,15 +19,9 @@
*/
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();
}
}

+ 1
- 1
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java View 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();
}
}

+ 233
- 0
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java View File

@@ -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);
}
}
}

+ 3
- 3
server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java View 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

server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java → server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java View File

@@ -19,12 +19,16 @@
*/
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;
@@ -38,21 +42,83 @@ 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 IssueMetricFormulaFactoryImplTest {
public class MeasureUpdateFormulaFactoryImplTest {

private IssueMetricFormulaFactoryImpl underTest = new IssueMetricFormulaFactoryImpl();
private final MeasureUpdateFormulaFactoryImpl underTest = new MeasureUpdateFormulaFactoryImpl();

@Test
public void getFormulaMetrics_include_the_dependent_metrics() {
for (IssueMetricFormula formula : underTest.getFormulas()) {
for (MeasureUpdateFormula formula : underTest.getFormulas()) {
assertThat(underTest.getFormulaMetrics()).contains(formula.getMetric());
for (Metric dependentMetric : formula.getDependentMetrics()) {
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);
@@ -77,7 +143,7 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.BUG).setCount(7),
// not bugs
newGroup(RuleType.CODE_SMELL).setCount(11))
.assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
.assertThatValueIs(CoreMetrics.BUGS, 3 + 5);
}

@Test
@@ -90,7 +156,7 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.CODE_SMELL).setCount(7),
// not code smells
newGroup(RuleType.BUG).setCount(11))
.assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
.assertThatValueIs(CoreMetrics.CODE_SMELLS, 3 + 5);
}

@Test
@@ -103,7 +169,7 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.VULNERABILITY).setCount(7),
// not vulnerabilities
newGroup(RuleType.BUG).setCount(11))
.assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
.assertThatValueIs(CoreMetrics.VULNERABILITIES, 3 + 5);
}

@Test
@@ -116,7 +182,7 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.SECURITY_HOTSPOT).setCount(7),
// not hotspots
newGroup(RuleType.BUG).setCount(11))
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
.assertThatValueIs(CoreMetrics.SECURITY_HOTSPOTS, 3 + 5);
}

@Test
@@ -124,10 +190,10 @@ public class IssueMetricFormulaFactoryImplTest {
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);
.assertThatValueIs(SECURITY_REVIEW_RATING, Rating.B);

withNoIssues()
.assertThatValueIs(CoreMetrics.SECURITY_REVIEW_RATING, Rating.A);
.assertThatValueIs(SECURITY_REVIEW_RATING, Rating.A);
}

@Test
@@ -135,10 +201,10 @@ public class IssueMetricFormulaFactoryImplTest {
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);
.assertThatValueIs(SECURITY_HOTSPOTS_REVIEWED, 75.0);

withNoIssues()
.assertNoValue(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED);
.assertNoValue(SECURITY_HOTSPOTS_REVIEWED);
}

@Test
@@ -185,11 +251,11 @@ public class IssueMetricFormulaFactoryImplTest {
newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.INFO).setCount(17),
newResolvedGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setCount(19),
newResolvedGroup(RuleType.SECURITY_HOTSPOT).setSeverity(Severity.INFO).setCount(21))
.assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 11 + 13)
.assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 7)
.assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 3 + 5)
.assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
.assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
.assertThatValueIs(CoreMetrics.BLOCKER_VIOLATIONS, 11 + 13)
.assertThatValueIs(CoreMetrics.CRITICAL_VIOLATIONS, 7)
.assertThatValueIs(CoreMetrics.MAJOR_VIOLATIONS, 3 + 5)
.assertThatValueIs(CoreMetrics.MINOR_VIOLATIONS, 0)
.assertThatValueIs(CoreMetrics.INFO_VIOLATIONS, 0);
}

@Test
@@ -209,8 +275,8 @@ public class IssueMetricFormulaFactoryImplTest {
// 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);
.assertThatValueIs(CoreMetrics.FALSE_POSITIVE_ISSUES, 5)
.assertThatValueIs(CoreMetrics.WONT_FIX_ISSUES, 7 + 11);
}

@Test
@@ -229,9 +295,9 @@ public class IssueMetricFormulaFactoryImplTest {
// exclude security hotspot
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_OPEN).setCount(12),
newResolvedGroup(Issue.RESOLUTION_FALSE_POSITIVE, Issue.STATUS_CLOSED).setCount(13))
.assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 3 + 5)
.assertThatValueIs(CoreMetrics.OPEN_ISSUES, 9 + 11)
.assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 7);
.assertThatValueIs(CoreMetrics.CONFIRMED_ISSUES, 3 + 5)
.assertThatValueIs(CoreMetrics.OPEN_ISSUES, 9 + 11)
.assertThatValueIs(CoreMetrics.REOPENED_ISSUES, 7);
}

@Test
@@ -248,7 +314,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.BUG).setEffort(7.0),
// exclude resolved
newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0))
.assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
.assertThatValueIs(CoreMetrics.TECHNICAL_DEBT, 3.0 + 5.0);
}

@Test
@@ -262,7 +328,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setEffort(7.0),
// exclude resolved
newResolvedGroup(RuleType.BUG).setEffort(17.0))
.assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
.assertThatValueIs(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, 3.0 + 5.0);
}

@Test
@@ -276,7 +342,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setEffort(7.0),
// exclude resolved
newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0))
.assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
.assertThatValueIs(CoreMetrics.SECURITY_REMEDIATION_EFFORT, 3.0 + 5.0);
}

@Test
@@ -286,10 +352,10 @@ public class IssueMetricFormulaFactoryImplTest {
.assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);

// technical_debt not computed
with(CoreMetrics.DEVELOPMENT_COST, 0)
with(CoreMetrics.DEVELOPMENT_COST, "0")
.assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
.assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
with(CoreMetrics.DEVELOPMENT_COST, 20)
with(CoreMetrics.DEVELOPMENT_COST, "20")
.assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0)
.assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);

@@ -303,49 +369,49 @@ public class IssueMetricFormulaFactoryImplTest {

// input measures are available
with(CoreMetrics.TECHNICAL_DEBT, 20.0)
.and(CoreMetrics.DEVELOPMENT_COST, 0.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)
.and(CoreMetrics.DEVELOPMENT_COST, 160.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)
.and(CoreMetrics.DEVELOPMENT_COST, 10.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.0)
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)
.and(CoreMetrics.DEVELOPMENT_COST, 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)
.and(CoreMetrics.DEVELOPMENT_COST, 80.0)
.andText(CoreMetrics.DEVELOPMENT_COST, "80")
.assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0);

with(CoreMetrics.TECHNICAL_DEBT, -20.0)
.and(CoreMetrics.DEVELOPMENT_COST, 0.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)
.and(CoreMetrics.DEVELOPMENT_COST, 80.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)
.and(CoreMetrics.DEVELOPMENT_COST, -80.0)
.andText(CoreMetrics.DEVELOPMENT_COST, "-80")
.assertThatValueIs(CoreMetrics.SQALE_DEBT_RATIO, 0.0)
.assertThatValueIs(CoreMetrics.SQALE_RATING, Rating.A);
}
@@ -369,25 +435,25 @@ public class IssueMetricFormulaFactoryImplTest {
.assertThatValueIs(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, 20.0);

// B to A
with(CoreMetrics.DEVELOPMENT_COST, 200.0)
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.0)
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.0)
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.0)
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 !
@@ -404,14 +470,14 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setCount(5),
// excluded, not a bug
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3))
// highest severity of bugs is CRITICAL --> D
.assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.D);
// highest severity of bugs is CRITICAL --> D
.assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.D);

with(
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
newGroup(RuleType.VULNERABILITY).setSeverity(Severity.CRITICAL).setCount(5))
// no bugs --> A
.assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
// no bugs --> A
.assertThatValueIs(CoreMetrics.RELIABILITY_RATING, Rating.A);
}

@Test
@@ -424,14 +490,14 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.VULNERABILITY).setSeverity(Severity.MINOR).setCount(5),
// excluded, not a vulnerability
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setCount(3))
// highest severity of vulnerabilities is CRITICAL --> D
.assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.D);
// highest severity of vulnerabilities is CRITICAL --> D
.assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.D);

with(
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setCount(3),
newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setCount(5))
// no vulnerabilities --> A
.assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
// no vulnerabilities --> A
.assertThatValueIs(CoreMetrics.SECURITY_RATING, Rating.A);
}

@Test
@@ -445,7 +511,8 @@ public class IssueMetricFormulaFactoryImplTest {
// not bugs
newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(9),
newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
.assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_BUGS, 5 + 7);

}

@Test
@@ -459,7 +526,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not code smells
newGroup(RuleType.BUG).setInLeak(true).setCount(9),
newGroup(RuleType.VULNERABILITY).setInLeak(true).setCount(11))
.assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_CODE_SMELLS, 5 + 7);
}

@Test
@@ -473,7 +540,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not vulnerabilities
newGroup(RuleType.BUG).setInLeak(true).setCount(9),
newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
.assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_VULNERABILITIES, 5 + 7);
}

@Test
@@ -487,7 +554,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not hotspots
newGroup(RuleType.BUG).setInLeak(true).setCount(9),
newGroup(RuleType.CODE_SMELL).setInLeak(true).setCount(11))
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS, 5 + 7);
}

@Test
@@ -502,7 +569,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.BUG).setInLeak(false).setCount(11),
newGroup(RuleType.CODE_SMELL).setInLeak(false).setCount(13),
newGroup(RuleType.VULNERABILITY).setInLeak(false).setCount(17))
.assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 5 + 7 + 9);
.assertThatLeakValueIs(CoreMetrics.NEW_VIOLATIONS, 5 + 7 + 9);
}

@Test
@@ -519,7 +586,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not in leak
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(11),
newGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(false).setCount(13))
.assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 3 + 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_BLOCKER_VIOLATIONS, 3 + 5 + 7);
}

@Test
@@ -536,7 +603,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not in leak
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(11),
newGroup(RuleType.BUG).setSeverity(Severity.CRITICAL).setInLeak(false).setCount(13))
.assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 3 + 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_CRITICAL_VIOLATIONS, 3 + 5 + 7);
}

@Test
@@ -553,7 +620,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not in leak
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MAJOR).setInLeak(false).setCount(11),
newGroup(RuleType.BUG).setSeverity(Severity.MAJOR).setInLeak(false).setCount(13))
.assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 3 + 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_MAJOR_VIOLATIONS, 3 + 5 + 7);
}

@Test
@@ -570,7 +637,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not in leak
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.MINOR).setInLeak(false).setCount(11),
newGroup(RuleType.BUG).setSeverity(Severity.MINOR).setInLeak(false).setCount(13))
.assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 3 + 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_MINOR_VIOLATIONS, 3 + 5 + 7);
}

@Test
@@ -587,7 +654,7 @@ public class IssueMetricFormulaFactoryImplTest {
// not in leak
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.INFO).setInLeak(false).setCount(11),
newGroup(RuleType.BUG).setSeverity(Severity.INFO).setInLeak(false).setCount(13))
.assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 3 + 5 + 7);
.assertThatLeakValueIs(CoreMetrics.NEW_INFO_VIOLATIONS, 3 + 5 + 7);
}

@Test
@@ -603,7 +670,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.BUG).setEffort(7.0).setInLeak(true),
// exclude resolved
newResolvedGroup(RuleType.CODE_SMELL).setEffort(17.0).setInLeak(true))
.assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0);
.assertThatLeakValueIs(CoreMetrics.NEW_TECHNICAL_DEBT, 3.0);
}

@Test
@@ -618,7 +685,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true),
// exclude resolved
newResolvedGroup(RuleType.BUG).setEffort(17.0).setInLeak(true))
.assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 3.0);
.assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, 3.0);
}

@Test
@@ -633,7 +700,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setEffort(7.0).setInLeak(true),
// exclude resolved
newResolvedGroup(RuleType.VULNERABILITY).setEffort(17.0).setInLeak(true))
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 3.0);
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, 3.0);
}

@Test
@@ -649,8 +716,8 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true),
// exclude resolved
newResolvedGroup(RuleType.BUG).setSeverity(Severity.BLOCKER).setInLeak(true))
// highest severity of bugs on leak period is minor -> B
.assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.B);
// highest severity of bugs on leak period is minor -> B
.assertThatLeakValueIs(CoreMetrics.NEW_RELIABILITY_RATING, Rating.B);
}

@Test
@@ -666,8 +733,8 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.CODE_SMELL).setSeverity(Severity.BLOCKER).setInLeak(true),
// exclude resolved
newResolvedGroup(RuleType.VULNERABILITY).setSeverity(Severity.BLOCKER).setInLeak(true))
// highest severity of bugs on leak period is minor -> B
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.B);
// highest severity of bugs on leak period is minor -> B
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_RATING, Rating.B);
}

@Test
@@ -677,10 +744,10 @@ public class IssueMetricFormulaFactoryImplTest {
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);
.assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.B);

withNoIssues()
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_REVIEW_RATING, Rating.A);
.assertThatLeakValueIs(NEW_SECURITY_REVIEW_RATING, Rating.A);
}

@Test
@@ -690,7 +757,7 @@ public class IssueMetricFormulaFactoryImplTest {
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(1).setInLeak(true),
// not in leak
newGroup(RuleType.SECURITY_HOTSPOT).setStatus(Issue.STATUS_TO_REVIEW).setCount(5).setInLeak(false))
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, 75.0);
.assertThatLeakValueIs(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, 75.0);

withNoIssues()
.assertNoLeakValue(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED);
@@ -751,44 +818,44 @@ public class IssueMetricFormulaFactoryImplTest {
.assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);

withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, 20.0)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 160.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)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 10.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
withLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 200.0)
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)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 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)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.0)
.andText(CoreMetrics.NEW_DEVELOPMENT_COST, "80")
.assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0);

withLeak(CoreMetrics.NEW_TECHNICAL_DEBT, -20.0)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 0.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)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, 80.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)
.andLeak(CoreMetrics.NEW_DEVELOPMENT_COST, -80.0)
.andText(CoreMetrics.NEW_DEVELOPMENT_COST, "-80")
.assertThatLeakValueIs(CoreMetrics.NEW_SQALE_DEBT_RATIO, 0.0)
.assertThatLeakValueIs(CoreMetrics.NEW_MAINTAINABILITY_RATING, Rating.A);
}
@@ -805,26 +872,34 @@ public class IssueMetricFormulaFactoryImplTest {
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 Map<Metric, Double> values = new HashMap<>();
private final Map<Metric, Double> leakValues = new HashMap<>();
private final InitialValues initialValues = new InitialValues();

private Verifier(IssueGroupDto[] groups) {
this.groups = groups;
}

Verifier and(Metric metric, double value) {
this.values.put(metric, value);
this.initialValues.values.put(metric, value);
return this;
}

Verifier andLeak(Metric metric, double value) {
this.leakValues.put(metric, value);
this.initialValues.leakValues.put(metric, value);
return this;
}

Verifier andText(Metric metric, String value) {
this.initialValues.text.put(metric, value);
return this;
}

@@ -865,12 +940,12 @@ public class IssueMetricFormulaFactoryImplTest {
}

private TestContext run(Metric metric, boolean expectLeakFormula) {
IssueMetricFormula formula = underTest.getFormulas().stream()
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(), values, leakValues);
TestContext context = new TestContext(formula.getDependentMetrics(), initialValues);
formula.compute(context, newIssueCounter(groups));
return context;
}
@@ -904,19 +979,25 @@ public class IssueMetricFormulaFactoryImplTest {
return newGroup().setResolution(resolution).setStatus(status);
}

private static class TestContext implements IssueMetricFormula.Context {
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 final Map<Metric, Double> values;
private final Map<Metric, Double> leakValues;

private TestContext(Collection<Metric> dependentMetrics, Map<Metric, Double> values, Map<Metric, Double> leakValues) {
private TestContext(Collection<Metric> dependentMetrics, InitialValues initialValues) {
this.dependentMetrics = new HashSet<>(dependentMetrics);
this.values = values;
this.leakValues = leakValues;
this.initialValues = initialValues;
}

@Override public List<Double> getChildrenValues() {
return initialValues.childrenValues;
}

@Override public List<Double> getChildrenLeakValues() {
return initialValues.childrenLeakValues;
}

@Override
@@ -934,8 +1015,16 @@ public class IssueMetricFormulaFactoryImplTest {
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));
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();
}
@@ -945,8 +1034,8 @@ public class IssueMetricFormulaFactoryImplTest {
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));
if (initialValues.leakValues.containsKey(metric)) {
return Optional.of(initialValues.leakValues.get(metric));
}
return Optional.empty();
}
@@ -971,4 +1060,77 @@ public class IssueMetricFormulaFactoryImplTest {
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;
}
}
}

server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java → server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java View File

@@ -23,21 +23,21 @@ import java.util.List;
import java.util.Set;
import org.sonar.api.measures.Metric;

class TestIssueMetricFormulaFactory implements IssueMetricFormulaFactory {
class TestMeasureUpdateFormulaFactory implements MeasureUpdateFormulaFactory {

private final List<IssueMetricFormula> formulas;
private final List<MeasureUpdateFormula> formulas;

TestIssueMetricFormulaFactory(List<IssueMetricFormula> formulas) {
TestMeasureUpdateFormulaFactory(List<MeasureUpdateFormula> formulas) {
this.formulas = formulas;
}

@Override
public List<IssueMetricFormula> getFormulas() {
public List<MeasureUpdateFormula> getFormulas() {
return formulas;
}

@Override
public Set<Metric> getFormulaMetrics() {
return IssueMetricFormulaFactory.extractMetrics(formulas);
return MeasureUpdateFormulaFactory.extractMetrics(formulas);
}
}

Loading…
Cancel
Save