@@ -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()) { |
@@ -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 |
@@ -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; | |||
} | |||
} |
@@ -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) { |
@@ -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, |
@@ -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"/> |
@@ -347,7 +347,67 @@ | |||
i.issue_type <> 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 >= 0"> | |||
(i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 0"> | |||
CASE WHEN n.uuid is null THEN false ELSE true END as inLeak | |||
</if> | |||
from issues i | |||
<if test="leakPeriodBeginningDate < 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 >= 0"> | |||
case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 0"> | |||
case when n.uuid is null then 0 else 1 end as inLeak | |||
</if> | |||
from issues i | |||
<if test="leakPeriodBeginningDate < 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 >= 0"> | |||
case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 0"> | |||
case when n.uuid is null then 0 else 1 end as inLeak | |||
</if> | |||
from issues i | |||
<if test="leakPeriodBeginningDate < 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 >= 0"> | |||
(i.issue_creation_date > #{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 < 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 >= 0"> | |||
<if test="leakPeriodBeginningDate >= 0"> | |||
case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 0"> | |||
case when n.uuid is null then 0 else 1 end as inLeak | |||
</if> | |||
</if> | |||
<if test="leakPeriodBeginningDate < 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 < 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 >= 0"> | |||
case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 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 >= 0"> | |||
case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
</if> | |||
<if test="leakPeriodBeginningDate < 0"> | |||
case when n.uuid is null then 0 else 1 end as inLeak | |||
</if> | |||
from issues i | |||
<if test="leakPeriodBeginningDate < 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, |
@@ -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. |
@@ -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")); |
@@ -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()); |
@@ -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(); | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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))); | |||
} | |||
} | |||
} |
@@ -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(); | |||
} | |||
} | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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(); |
@@ -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()); | |||
} | |||
} | |||
} |
@@ -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); |
@@ -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()); |
@@ -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; | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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 |
@@ -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; | |||
} | |||
} | |||
} |
@@ -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); | |||
} | |||
} |