diff options
author | Duarte Meneses <duarte.meneses@sonarsource.com> | 2022-05-25 16:22:34 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-06-10 08:15:07 +0000 |
commit | 928f60ac3377552bce626154e3b23139fafd22b6 (patch) | |
tree | 4b3cb1571bda164c7e89ebc8891e068219b81708 /server | |
parent | e61c5f5e2d1e71d9f3dc5c562ad9be9fb1dd6c87 (diff) | |
download | sonarqube-928f60ac3377552bce626154e3b23139fafd22b6.tar.gz sonarqube-928f60ac3377552bce626154e3b23139fafd22b6.zip |
SONAR-11401 Performance hotspot when changing state of issue
Diffstat (limited to 'server')
35 files changed, 2141 insertions, 1190 deletions
diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java index 08ad6fcecdb..98655ebf25c 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentDao.java @@ -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()) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java index 18bb66f79a4..f517bddea89 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/component/ComponentMapper.java @@ -69,6 +69,8 @@ public interface ComponentMapper { List<ComponentDto> selectDescendants(@Param("query") ComponentTreeQuery query, @Param("baseUuid") String baseUuid, @Param("baseUuidPath") String baseUuidPath); + List<ComponentDto> selectChildren(@Param("uuidPaths") Set<String> uuidPaths); + /** * Returns all enabled projects (Scope {@link org.sonar.api.resources.Scopes#PROJECT} and qualifier * {@link org.sonar.api.resources.Qualifiers#PROJECT}) no matter if they are ghost project, provisioned projects or diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java new file mode 100644 index 00000000000..bdd7889c9cc --- /dev/null +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/HotspotGroupDto.java @@ -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; + } +} diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java index fd64c2c90b6..454a2420d16 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueDao.java @@ -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) { diff --git a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java index ba55992a0ab..bd3db65bb67 100644 --- a/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java +++ b/server/sonar-db-dao/src/main/java/org/sonar/db/issue/IssueMapper.java @@ -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, diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml index c93b10d9771..efacf0809b4 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/component/ComponentMapper.xml @@ -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"/> diff --git a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml index 5851f05ab2c..b56fde8abcb 100644 --- a/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml +++ b/server/sonar-db-dao/src/main/resources/org/sonar/db/issue/IssueMapper.xml @@ -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, diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java index e47fd35065b..d0dd5e840a9 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/component/ComponentDaoTest.java @@ -1670,6 +1670,33 @@ public class ComponentDaoTest { } @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. ComponentDto project = newPrivateProjectDto(PROJECT_UUID); diff --git a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java index 393798a7012..33690a61a55 100644 --- a/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java +++ b/server/sonar-db-dao/src/test/java/org/sonar/db/issue/IssueDaoTest.java @@ -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); @@ -394,6 +403,74 @@ public class IssueDaoTest { } @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))) .isEmpty(); @@ -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")); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java index 494c9bd4828..fa0cd8ae6b3 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/issue/ws/IssueUpdater.java @@ -68,15 +68,12 @@ public class IssueUpdater { this.notificationSerializer = notificationSerializer; } - public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, - IssueChangeContext context, boolean refreshMeasures) { + public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, boolean refreshMeasures) { BranchDto branch = getBranch(dbSession, issue, issue.projectUuid()); return saveIssueAndPreloadSearchResponseData(dbSession, issue, context, refreshMeasures, branch); } - public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, - IssueChangeContext context, boolean refreshMeasures, BranchDto branch) { - + public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, boolean refreshMeasures, BranchDto branch) { Optional<RuleDto> rule = getRuleByKey(dbSession, issue.getRuleKey()); ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid()); ComponentDto component = getComponent(dbSession, issue, issue.componentUuid()); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java new file mode 100644 index 00000000000..47e9be39c4f --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndex.java @@ -0,0 +1,54 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import java.util.Set; +import org.sonar.db.component.ComponentDto; + +/** + * Provides all components needed for the computation of live measures. + * The components needed for the computation are: + * 1) Components for which issues were modified + * 2) All ancestors of 1), up to the root + * 3) All immediate children of 1) and 2). The measures in these components won't be recomputed, + * but their measures are needed to recompute the measures for components in 1) and 2). + */ +public interface ComponentIndex { + /** + * Immediate children of a component that are relevant for the computation + */ + List<ComponentDto> getChildren(ComponentDto component); + + /** + * Uuids of all components relevant for the computation + */ + Set<String> getAllUuids(); + + /** + * All components that need the measures recalculated, sorted depth first. It corresponds to the points 1) and 2) in the list mentioned in the javadoc of this class. + */ + List<ComponentDto> getSortedTree(); + + /** + * Branch being recomputed + */ + ComponentDto getBranch(); +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java new file mode 100644 index 00000000000..40b80e15c7b --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexFactory.java @@ -0,0 +1,39 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; + +public class ComponentIndexFactory { + private final DbClient dbClient; + + public ComponentIndexFactory(DbClient dbClient) { + this.dbClient = dbClient; + } + + public ComponentIndex create(DbSession dbSession, List<ComponentDto> touchedComponents) { + ComponentIndexImpl idx = new ComponentIndexImpl(dbClient); + idx.load(dbSession, touchedComponents); + return idx; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java new file mode 100644 index 00000000000..7b5da77499e --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/ComponentIndexImpl.java @@ -0,0 +1,110 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; + +import static java.util.Collections.emptyList; + +public class ComponentIndexImpl implements ComponentIndex { + private final DbClient dbClient; + private ComponentDto branchComponent; + private List<ComponentDto> sortedComponentsToRoot; + private Map<String, List<ComponentDto>> children; + + public ComponentIndexImpl(DbClient dbClient) { + this.dbClient = dbClient; + } + + /** + * Loads all the components required for the calculation of the new values of the live measures based on what components were modified: + * - All components between the touched components and the roots of their component trees + * - All immediate children of those components + */ + public void load(DbSession dbSession, List<ComponentDto> touchedComponents) { + sortedComponentsToRoot = loadTreeOfComponents(dbSession, touchedComponents); + branchComponent = findBranchComponent(sortedComponentsToRoot); + children = new HashMap<>(); + List<ComponentDto> childComponents = loadChildren(dbSession, sortedComponentsToRoot); + for (ComponentDto c : childComponents) { + List<String> uuidPathAsList = c.getUuidPathAsList(); + String parentUuid = uuidPathAsList.get(uuidPathAsList.size() - 1); + children.computeIfAbsent(parentUuid, uuid -> new LinkedList<>()).add(c); + } + } + + private static ComponentDto findBranchComponent(Collection<ComponentDto> components) { + return components.stream().filter(ComponentDto::isRootProject).findFirst() + .orElseThrow(() -> new IllegalStateException("No project found in " + components)); + } + + private List<ComponentDto> loadChildren(DbSession dbSession, Collection<ComponentDto> components) { + return dbClient.componentDao().selectChildren(dbSession, components); + } + + private List<ComponentDto> loadTreeOfComponents(DbSession dbSession, List<ComponentDto> touchedComponents) { + Set<String> componentUuids = new HashSet<>(); + for (ComponentDto component : touchedComponents) { + componentUuids.add(component.uuid()); + // ancestors, excluding self + componentUuids.addAll(component.getUuidPathAsList()); + } + return dbClient.componentDao().selectByUuids(dbSession, componentUuids).stream() + .sorted(Comparator.comparing(ComponentDto::getUuidPath).reversed()) + .collect(Collectors.toList()); + } + + @Override + public List<ComponentDto> getChildren(ComponentDto component) { + return children.getOrDefault(component.uuid(), emptyList()); + } + + @Override + public Set<String> getAllUuids() { + Set<String> all = new HashSet<>(); + sortedComponentsToRoot.forEach(c -> all.add(c.uuid())); + for (Collection<ComponentDto> l : children.values()) { + for (ComponentDto c : l) { + all.add(c.uuid()); + } + } + return all; + } + + @Override + public List<ComponentDto> getSortedTree() { + return sortedComponentsToRoot; + } + + @Override public ComponentDto getBranch() { + return branchComponent; + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java new file mode 100644 index 00000000000..8f624cb71e1 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotMeasureUpdater.java @@ -0,0 +1,60 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.Optional; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.db.DbClient; +import org.sonar.db.DbSession; +import org.sonar.db.component.ComponentDto; + +import static org.sonar.server.security.SecurityReviewRating.computePercent; +import static org.sonar.server.security.SecurityReviewRating.computeRating; + +public class HotspotMeasureUpdater { + private final DbClient dbClient; + + public HotspotMeasureUpdater(DbClient dbClient) { + this.dbClient = dbClient; + } + public void apply(DbSession dbSession, MeasureMatrix matrix, ComponentIndex components, boolean useLeakFormulas, long beginningOfLeak) { + HotspotsCounter hotspotsCounter = new HotspotsCounter(dbClient.issueDao().selectBranchHotspotsCount(dbSession, components.getBranch().uuid(), beginningOfLeak)); + ComponentDto branch = components.getBranch(); + + long reviewed = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_REVIEWED, false); + long toReview = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false); + matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, reviewed); + matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, toReview); + Optional<Double> percent = computePercent(toReview, reviewed); + percent.ifPresent(p -> matrix.setValue(branch, CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY, p)); + matrix.setValue(branch, CoreMetrics.SECURITY_REVIEW_RATING_KEY, computeRating(percent.orElse(null))); + + if (useLeakFormulas) { + reviewed = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_REVIEWED, true); + toReview = hotspotsCounter.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true); + matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY, reviewed); + matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY, toReview); + percent = computePercent(toReview, reviewed); + percent.ifPresent(p -> matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY, p)); + matrix.setLeakValue(branch, CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY, computeRating(percent.orElse(null))); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java new file mode 100644 index 00000000000..9db99df66e3 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/HotspotsCounter.java @@ -0,0 +1,63 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import javax.annotation.Nullable; +import org.sonar.db.issue.HotspotGroupDto; + +public class HotspotsCounter { + private final Map<String, Count> hotspotsByStatus = new HashMap<>(); + + HotspotsCounter(Collection<HotspotGroupDto> groups) { + for (HotspotGroupDto group : groups) { + if (group.getStatus() != null) { + hotspotsByStatus + .computeIfAbsent(group.getStatus(), k -> new Count()) + .add(group); + } + } + } + + public long countHotspotsByStatus(String status, boolean onlyInLeak) { + return value(hotspotsByStatus.get(status), onlyInLeak); + } + + private static long value(@Nullable Count count, boolean onlyInLeak) { + if (count == null) { + return 0; + } + return onlyInLeak ? count.leak : count.absolute; + } + + private static class Count { + private long absolute = 0L; + private long leak = 0L; + + void add(HotspotGroupDto group) { + absolute += group.getCount(); + if (group.isInLeak()) { + leak += group.getCount(); + } + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java deleted file mode 100644 index 0dddb833568..00000000000 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImpl.java +++ /dev/null @@ -1,240 +0,0 @@ -/* - * SonarQube - * Copyright (C) 2009-2022 SonarSource SA - * mailto:info AT sonarsource DOT com - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.server.measure.live; - -import java.util.List; -import java.util.Optional; -import java.util.Set; -import org.sonar.api.issue.Issue; -import org.sonar.api.measures.CoreMetrics; -import org.sonar.api.measures.Metric; -import org.sonar.api.rule.Severity; -import org.sonar.api.rules.RuleType; -import org.sonar.server.measure.Rating; - -import static java.util.Arrays.asList; -import static org.sonar.server.measure.Rating.RATING_BY_SEVERITY; -import static org.sonar.server.security.SecurityReviewRating.computePercent; -import static org.sonar.server.security.SecurityReviewRating.computeRating; - -public class IssueMetricFormulaFactoryImpl implements IssueMetricFormulaFactory { - - private static final List<IssueMetricFormula> FORMULAS = asList( - new IssueMetricFormula(CoreMetrics.CODE_SMELLS, false, - (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))), - - new IssueMetricFormula(CoreMetrics.BUGS, false, - (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))), - - new IssueMetricFormula(CoreMetrics.VULNERABILITIES, false, - (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))), - - new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS, false, - (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, false))), - - new IssueMetricFormula(CoreMetrics.VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolved(false))), - - new IssueMetricFormula(CoreMetrics.BLOCKER_VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))), - - new IssueMetricFormula(CoreMetrics.CRITICAL_VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))), - - new IssueMetricFormula(CoreMetrics.MAJOR_VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))), - - new IssueMetricFormula(CoreMetrics.MINOR_VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))), - - new IssueMetricFormula(CoreMetrics.INFO_VIOLATIONS, false, - (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))), - - new IssueMetricFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false, - (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))), - - new IssueMetricFormula(CoreMetrics.WONT_FIX_ISSUES, false, - (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))), - - new IssueMetricFormula(CoreMetrics.OPEN_ISSUES, false, - (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))), - - new IssueMetricFormula(CoreMetrics.REOPENED_ISSUES, false, - (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))), - - new IssueMetricFormula(CoreMetrics.CONFIRMED_ISSUES, false, - (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))), - - new IssueMetricFormula(CoreMetrics.TECHNICAL_DEBT, false, - (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))), - - new IssueMetricFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false, - (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))), - - new IssueMetricFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false, - (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))), - - new IssueMetricFormula(CoreMetrics.SQALE_DEBT_RATIO, false, - (context, issues) -> context.setValue(100.0 * debtDensity(context)), - asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), - - new IssueMetricFormula(CoreMetrics.SQALE_RATING, false, - (context, issues) -> context - .setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))), - asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), - - new IssueMetricFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false, - (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), - - new IssueMetricFormula(CoreMetrics.RELIABILITY_RATING, false, - (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))), - - new IssueMetricFormula(CoreMetrics.SECURITY_RATING, false, - (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))), - - new IssueMetricFormula(CoreMetrics.SECURITY_REVIEW_RATING, false, - (context, issues) -> { - Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false)); - context.setValue(computeRating(percent.orElse(null))); - }), - - new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false, - (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false)) - .ifPresent(context::setValue)), - - new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS, false, - (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))), - - new IssueMetricFormula(CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false, - (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false))), - - new IssueMetricFormula(CoreMetrics.NEW_CODE_SMELLS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))), - - new IssueMetricFormula(CoreMetrics.NEW_BUGS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))), - - new IssueMetricFormula(CoreMetrics.NEW_VULNERABILITIES, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, true))), - - new IssueMetricFormula(CoreMetrics.NEW_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolved(true))), - - new IssueMetricFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))), - - new IssueMetricFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))), - - new IssueMetricFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))), - - new IssueMetricFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))), - - new IssueMetricFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true, - (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))), - - new IssueMetricFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true, - (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))), - - new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true, - (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true, - (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))), - - new IssueMetricFormula(CoreMetrics.NEW_RELIABILITY_RATING, true, - (context, issues) -> { - String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO); - context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity)); - }), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_RATING, true, - (context, issues) -> { - String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO); - context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity)); - }), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_REVIEW_RATING, true, - (context, issues) -> { - Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true)); - context.setLeakValue(computeRating(percent.orElse(null))); - }), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED, true, - (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true)) - .ifPresent(context::setLeakValue)), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true, - (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))), - - new IssueMetricFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true, - (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true))), - - new IssueMetricFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true, - (context, issues) -> context.setLeakValue(100.0 * newDebtDensity(context)), - asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)), - - new IssueMetricFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true, - (context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity( - newDebtDensity(context))), - asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST))); - - private static final Set<Metric> FORMULA_METRICS = IssueMetricFormulaFactory.extractMetrics(FORMULAS); - - private static double debtDensity(IssueMetricFormula.Context context) { - double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0), 0.0); - Optional<Double> devCost = context.getValue(CoreMetrics.DEVELOPMENT_COST); - if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) { - return debt / devCost.get(); - } - return 0.0; - } - - private static double newDebtDensity(IssueMetricFormula.Context context) { - double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0), 0.0); - Optional<Double> devCost = context.getLeakValue(CoreMetrics.NEW_DEVELOPMENT_COST); - if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) { - return debt / devCost.get(); - } - return 0.0; - } - - private static double effortToReachMaintainabilityRatingA(IssueMetricFormula.Context context) { - double developmentCost = context.getValue(CoreMetrics.DEVELOPMENT_COST).orElse(0.0); - double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0); - double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost; - return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0; - } - - @Override - public List<IssueMetricFormula> getFormulas() { - return FORMULAS; - } - - @Override - public Set<Metric> getFormulaMetrics() { - return FORMULA_METRICS; - } -} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java index b7b024a62ef..957cfbf0705 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureComputerImpl.java @@ -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); - } - } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java index a69881f63dd..595b3c627ed 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureModule.java @@ -25,8 +25,11 @@ public class LiveMeasureModule extends Module { @Override protected void configureModule() { add( - IssueMetricFormulaFactoryImpl.class, + MeasureUpdateFormulaFactoryImpl.class, + ComponentIndexFactory.class, + LiveMeasureTreeUpdaterImpl.class, LiveMeasureComputerImpl.class, + HotspotMeasureUpdater.class, LiveQualityGateComputerImpl.class); } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java new file mode 100644 index 00000000000..cce7799db52 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdater.java @@ -0,0 +1,29 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import org.sonar.api.config.Configuration; +import org.sonar.db.DbSession; +import org.sonar.db.component.BranchDto; +import org.sonar.db.component.SnapshotDto; + +public interface LiveMeasureTreeUpdater { + void update(DbSession dbSession, SnapshotDto lastAnalysis, Configuration config, ComponentIndex components, BranchDto branch, MeasureMatrix measures); +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java new file mode 100644 index 00000000000..f362cf0b55d --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImpl.java @@ -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); + } + } +} diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java index dd40744a261..4e8a8c44398 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/LiveQualityGateComputerImpl.java @@ -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(); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java index 435120efc2d..62d4e80649c 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureMatrix.java @@ -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()); } } } diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java index d347531e778..420a5a94598 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormula.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormula.java @@ -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); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java index 4d3fa8bf100..00c00830580 100644 --- a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/IssueMetricFormulaFactory.java +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactory.java @@ -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()); diff --git a/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java new file mode 100644 index 00000000000..02a0ee5a0f8 --- /dev/null +++ b/server/sonar-webserver-webapi/src/main/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImpl.java @@ -0,0 +1,299 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.Set; +import java.util.function.BiConsumer; +import org.sonar.api.issue.Issue; +import org.sonar.api.measures.CoreMetrics; +import org.sonar.api.measures.Metric; +import org.sonar.api.rule.Severity; +import org.sonar.api.rules.RuleType; +import org.sonar.server.measure.Rating; + +import static java.util.Arrays.asList; +import static org.sonar.api.measures.CoreMetrics.CODE_SMELLS; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS; +import static org.sonar.server.measure.Rating.RATING_BY_SEVERITY; +import static org.sonar.server.security.SecurityReviewRating.computePercent; +import static org.sonar.server.security.SecurityReviewRating.computeRating; + +public class MeasureUpdateFormulaFactoryImpl implements MeasureUpdateFormulaFactory { + private static final List<MeasureUpdateFormula> FORMULAS = asList( + new MeasureUpdateFormula(CODE_SMELLS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, false))), + + new MeasureUpdateFormula(CoreMetrics.BUGS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.BUG, false))), + + new MeasureUpdateFormula(CoreMetrics.VULNERABILITIES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, false))), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, false))), + + new MeasureUpdateFormula(CoreMetrics.VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolved(false))), + + new MeasureUpdateFormula(CoreMetrics.BLOCKER_VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, false))), + + new MeasureUpdateFormula(CoreMetrics.CRITICAL_VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, false))), + + new MeasureUpdateFormula(CoreMetrics.MAJOR_VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MAJOR, false))), + + new MeasureUpdateFormula(CoreMetrics.MINOR_VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.MINOR, false))), + + new MeasureUpdateFormula(CoreMetrics.INFO_VIOLATIONS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countUnresolvedBySeverity(Severity.INFO, false))), + + new MeasureUpdateFormula(CoreMetrics.FALSE_POSITIVE_ISSUES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_FALSE_POSITIVE, false))), + + new MeasureUpdateFormula(CoreMetrics.WONT_FIX_ISSUES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countByResolution(Issue.RESOLUTION_WONT_FIX, false))), + + new MeasureUpdateFormula(CoreMetrics.OPEN_ISSUES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_OPEN, false))), + + new MeasureUpdateFormula(CoreMetrics.REOPENED_ISSUES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_REOPENED, false))), + + new MeasureUpdateFormula(CoreMetrics.CONFIRMED_ISSUES, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countByStatus(Issue.STATUS_CONFIRMED, false))), + + new MeasureUpdateFormula(CoreMetrics.TECHNICAL_DEBT, false, new AddChildren(), + (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, false))), + + new MeasureUpdateFormula(CoreMetrics.RELIABILITY_REMEDIATION_EFFORT, false, new AddChildren(), + (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.BUG, false))), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_REMEDIATION_EFFORT, false, new AddChildren(), + (context, issues) -> context.setValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, false))), + + new MeasureUpdateFormula(CoreMetrics.SQALE_DEBT_RATIO, false, + (context, formula) -> context.setValue(100.0 * debtDensity(context)), + (context, issues) -> context.setValue(100.0 * debtDensity(context)), + asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), + + new MeasureUpdateFormula(CoreMetrics.SQALE_RATING, false, + (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))), + (context, issues) -> context.setValue(context.getDebtRatingGrid().getRatingForDensity(debtDensity(context))), + asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), + + new MeasureUpdateFormula(CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A, false, + (context, formula) -> context.setValue(effortToReachMaintainabilityRatingA(context)), + (context, issues) -> context.setValue(effortToReachMaintainabilityRatingA(context)), asList(CoreMetrics.TECHNICAL_DEBT, CoreMetrics.DEVELOPMENT_COST)), + + new MeasureUpdateFormula(CoreMetrics.RELIABILITY_RATING, false, new MaxRatingChildren(), + (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.BUG, false).orElse(Severity.INFO)))), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_RATING, false, new MaxRatingChildren(), + (context, issues) -> context.setValue(RATING_BY_SEVERITY.get(issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, false).orElse(Severity.INFO)))), + + new MeasureUpdateFormula(SECURITY_HOTSPOTS_REVIEWED_STATUS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false))), + + new MeasureUpdateFormula(SECURITY_HOTSPOTS_TO_REVIEW_STATUS, false, new AddChildren(), + (context, issues) -> context.setValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false))), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_HOTSPOTS_REVIEWED, false, + (context, formula) -> { + Optional<Double> percent = computePercent( + context.getValue(SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D).longValue(), + context.getValue(SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D).longValue()); + percent.ifPresent(context::setValue); + }, + (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false)) + .ifPresent(context::setValue)), + + new MeasureUpdateFormula(CoreMetrics.SECURITY_REVIEW_RATING, false, + (context, formula) -> context.setValue(computeRating(context.getValue(SECURITY_HOTSPOTS_REVIEWED).orElse(null))), + (context, issues) -> { + Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, false), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, false)); + context.setValue(computeRating(percent.orElse(null))); + }), + + new MeasureUpdateFormula(CoreMetrics.NEW_CODE_SMELLS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.CODE_SMELL, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_BUGS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.BUG, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_VULNERABILITIES, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.VULNERABILITY, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_HOTSPOTS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedByType(RuleType.SECURITY_HOTSPOT, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolved(true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_BLOCKER_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.BLOCKER, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_CRITICAL_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.CRITICAL, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_MAJOR_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MAJOR, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_MINOR_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.MINOR, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_INFO_VIOLATIONS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countUnresolvedBySeverity(Severity.INFO, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_TECHNICAL_DEBT, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.CODE_SMELL, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_RELIABILITY_REMEDIATION_EFFORT, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.BUG, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_REMEDIATION_EFFORT, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.sumEffortOfUnresolved(RuleType.VULNERABILITY, true))), + + new MeasureUpdateFormula(CoreMetrics.NEW_RELIABILITY_RATING, true, new MaxRatingChildren(), + (context, issues) -> { + String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.BUG, true).orElse(Severity.INFO); + context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity)); + }), + + new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_RATING, true, new MaxRatingChildren(), + (context, issues) -> { + String highestSeverity = issues.getHighestSeverityOfUnresolved(RuleType.VULNERABILITY, true).orElse(Severity.INFO); + context.setLeakValue(RATING_BY_SEVERITY.get(highestSeverity)); + }), + + new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true))), + + new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS, true, new AddChildren(), + (context, issues) -> context.setLeakValue(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true))), + + new MeasureUpdateFormula(NEW_SECURITY_HOTSPOTS_REVIEWED, true, + (context, formula) -> { + Optional<Double> percent = computePercent( + context.getLeakValue(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS).orElse(0D).longValue(), + context.getLeakValue(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS).orElse(0D).longValue()); + percent.ifPresent(context::setLeakValue); + }, + (context, issues) -> computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true)) + .ifPresent(context::setLeakValue)), + + new MeasureUpdateFormula(CoreMetrics.NEW_SECURITY_REVIEW_RATING, true, + (context, formula) -> context.setLeakValue(computeRating(context.getLeakValue(NEW_SECURITY_HOTSPOTS_REVIEWED).orElse(null))), + (context, issues) -> { + Optional<Double> percent = computePercent(issues.countHotspotsByStatus(Issue.STATUS_TO_REVIEW, true), issues.countHotspotsByStatus(Issue.STATUS_REVIEWED, true)); + context.setLeakValue(computeRating(percent.orElse(null))); + }), + + new MeasureUpdateFormula(CoreMetrics.NEW_SQALE_DEBT_RATIO, true, + (context, formula) -> context.setLeakValue(100.0D * newDebtDensity(context)), + (context, issues) -> context.setLeakValue(100.0D * newDebtDensity(context)), + asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST)), + + new MeasureUpdateFormula(CoreMetrics.NEW_MAINTAINABILITY_RATING, true, + (context, formula) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))), + (context, issues) -> context.setLeakValue(context.getDebtRatingGrid().getRatingForDensity(newDebtDensity(context))), + asList(CoreMetrics.NEW_TECHNICAL_DEBT, CoreMetrics.NEW_DEVELOPMENT_COST))); + + private static final Set<Metric> FORMULA_METRICS = MeasureUpdateFormulaFactory.extractMetrics(FORMULAS); + + private static double debtDensity(MeasureUpdateFormula.Context context) { + double debt = Math.max(context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0D), 0.0D); + Optional<Double> devCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble); + if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) { + return debt / devCost.get(); + } + return 0.0D; + } + + private static double newDebtDensity(MeasureUpdateFormula.Context context) { + double debt = Math.max(context.getLeakValue(CoreMetrics.NEW_TECHNICAL_DEBT).orElse(0.0D), 0.0D); + Optional<Double> devCost = context.getText(CoreMetrics.NEW_DEVELOPMENT_COST).map(Double::parseDouble); + if (devCost.isPresent() && Double.doubleToRawLongBits(devCost.get()) > 0L) { + return debt / devCost.get(); + } + return 0.0D; + } + + private static double effortToReachMaintainabilityRatingA(MeasureUpdateFormula.Context context) { + double developmentCost = context.getText(CoreMetrics.DEVELOPMENT_COST).map(Double::parseDouble).orElse(0.0D); + double effort = context.getValue(CoreMetrics.TECHNICAL_DEBT).orElse(0.0D); + double upperGradeCost = context.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCost; + return upperGradeCost < effort ? (effort - upperGradeCost) : 0.0D; + } + + static class AddChildren implements BiConsumer<MeasureUpdateFormula.Context, MeasureUpdateFormula> { + @Override + public void accept(MeasureUpdateFormula.Context context, MeasureUpdateFormula formula) { + double sum; + if (formula.isOnLeak()) { + sum = context.getChildrenLeakValues().stream().mapToDouble(x -> x).sum(); + context.setLeakValue(context.getLeakValue(formula.getMetric()).orElse(0D) + sum); + } else { + sum = context.getChildrenValues().stream().mapToDouble(x -> x).sum(); + context.setValue(context.getValue(formula.getMetric()).orElse(0D) + sum); + } + } + } + + private static class MaxRatingChildren implements BiConsumer<MeasureUpdateFormula.Context, MeasureUpdateFormula> { + @Override + public void accept(MeasureUpdateFormula.Context context, MeasureUpdateFormula formula) { + OptionalInt max; + if (formula.isOnLeak()) { + max = context.getChildrenLeakValues().stream().mapToInt(Double::intValue).max(); + if (max.isPresent()) { + int currentRating = context.getLeakValue(formula.getMetric()).map(Double::intValue).orElse(Rating.A.getIndex()); + context.setLeakValue(Rating.valueOf(Math.max(currentRating, max.getAsInt()))); + } + } else { + max = context.getChildrenValues().stream().mapToInt(Double::intValue).max(); + if (max.isPresent()) { + int currentRating = context.getValue(formula.getMetric()).map(Double::intValue).orElse(Rating.A.getIndex()); + context.setValue(Rating.valueOf(Math.max(currentRating, max.getAsInt()))); + } + } + } + } + + @Override + public List<MeasureUpdateFormula> getFormulas() { + return FORMULAS; + } + + @Override + public Set<Metric> getFormulaMetrics() { + return FORMULA_METRICS; + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java new file mode 100644 index 00000000000..6187aab0417 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexFactoryTest.java @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import org.junit.Test; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentIndexFactoryTest { + public DbTester db = DbTester.create(); + private final ComponentIndexFactory factory = new ComponentIndexFactory(db.getDbClient()); + + @Test + public void creates_and_loads_instance() { + ComponentDto project = db.components().insertPrivateProject(); + ComponentIndex index = factory.create(db.getSession(), List.of(project)); + + assertThat(index.getAllUuids()).containsOnly(project.uuid()); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java new file mode 100644 index 00000000000..88666f2659f --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/ComponentIndexImplTest.java @@ -0,0 +1,96 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ComponentIndexImplTest { + @Rule + public DbTester db = DbTester.create(); + private final ComponentIndexImpl componentIndex = new ComponentIndexImpl(db.getDbClient()); + + private ComponentDto project; + private ComponentDto dir1; + private ComponentDto dir2; + private ComponentDto file11; + private ComponentDto file12; + private ComponentDto file21; + + private ComponentDto branch; + private ComponentDto branchDir1; + private ComponentDto branchDir2; + private ComponentDto branchFile11; + private ComponentDto branchFile12; + private ComponentDto branchFile21; + + @Before + public void setUp() { + project = db.components().insertPrivateProject(); + dir1 = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java")); + dir2 = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java2")); + file11 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir1)); + file12 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir1)); + file21 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir2)); + + branch = db.components().insertProjectBranch(project); + branchDir1 = db.components().insertComponent(ComponentTesting.newDirectory(branch, "src/main/java")); + branchDir2 = db.components().insertComponent(ComponentTesting.newDirectory(branch, "src/main/java2")); + branchFile11 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir1)); + branchFile12 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir1)); + branchFile21 = db.components().insertComponent(ComponentTesting.newFileDto(branch, branchDir2)); + } + + @Test + public void loads_all_necessary_components() { + componentIndex.load(db.getSession(), List.of(file11)); + assertThat(componentIndex.getSortedTree()).containsExactly(file11, dir1, project); + assertThat(componentIndex.getBranch()).isEqualTo(project); + assertThat(componentIndex.getAllUuids()).containsOnly(project.uuid(), dir1.uuid(), dir2.uuid(), file11.uuid(), file12.uuid()); + assertThat(componentIndex.getChildren(dir1)).containsOnly(file11, file12); + } + + + @Test + public void loads_all_necessary_components_for_root() { + componentIndex.load(db.getSession(), List.of(project)); + assertThat(componentIndex.getSortedTree()).containsExactly(project); + assertThat(componentIndex.getBranch()).isEqualTo(project); + assertThat(componentIndex.getAllUuids()).containsOnly(project.uuid(), dir1.uuid(), dir2.uuid()); + assertThat(componentIndex.getChildren(dir1)).isEmpty(); + assertThat(componentIndex.getChildren(project)).containsOnly(dir1, dir2); + } + + @Test + public void loads_all_necessary_components_from_branch() { + componentIndex.load(db.getSession(), List.of(branchDir1)); + assertThat(componentIndex.getSortedTree()).containsExactly(branchDir1, branch); + assertThat(componentIndex.getBranch()).isEqualTo(branch); + assertThat(componentIndex.getAllUuids()).containsOnly(branch.uuid(), branchDir1.uuid(), branchDir2.uuid(), branchFile11.uuid(), branchFile12.uuid()); + assertThat(componentIndex.getChildren(branchDir1)).containsOnly(branchFile11, branchFile12); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java new file mode 100644 index 00000000000..64a876b115c --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotMeasureUpdaterTest.java @@ -0,0 +1,119 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.Date; +import java.util.List; +import org.assertj.core.data.Offset; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.sonar.db.DbTester; +import org.sonar.db.component.ComponentDto; +import org.sonar.db.component.ComponentTesting; +import org.sonar.db.metric.MetricDto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.NEW_SECURITY_REVIEW_RATING_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY; +import static org.sonar.api.measures.CoreMetrics.SECURITY_REVIEW_RATING_KEY; + +public class HotspotMeasureUpdaterTest { + @Rule + public DbTester db = DbTester.create(); + + private final HotspotMeasureUpdater hotspotMeasureUpdater = new HotspotMeasureUpdater(db.getDbClient()); + private ComponentIndexImpl componentIndex; + private MeasureMatrix matrix; + private ComponentDto project; + private ComponentDto dir; + private ComponentDto file1; + private ComponentDto file2; + + @Before + public void setUp() { + // insert project and file structure + project = db.components().insertPrivateProject(); + dir = db.components().insertComponent(ComponentTesting.newDirectory(project, "src/main/java")); + file1 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir)); + file2 = db.components().insertComponent(ComponentTesting.newFileDto(project, dir)); + + // other needed data + matrix = new MeasureMatrix(List.of(project, dir, file1, file2), insertHotspotMetrics(), List.of()); + componentIndex = new ComponentIndexImpl(db.getDbClient()); + } + + private List<MetricDto> insertHotspotMetrics() { + return List.of( + db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)), + db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)), + db.measures().insertMetric(m -> m.setKey(SECURITY_HOTSPOTS_REVIEWED_KEY)), + db.measures().insertMetric(m -> m.setKey(SECURITY_REVIEW_RATING_KEY)), + + db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)), + db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)), + db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_HOTSPOTS_REVIEWED_KEY)), + db.measures().insertMetric(m -> m.setKey(NEW_SECURITY_REVIEW_RATING_KEY)) + ); + } + + @Test + public void should_count_hotspots_for_root() { + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(999)).setStatus("TO_REVIEW")); + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(999)).setStatus("REVIEWED")); + + db.issues().insertHotspot(project, dir, i -> i.setIssueCreationDate(new Date(1001)).setStatus("TO_REVIEW")); + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1002)).setStatus("REVIEWED")); + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1003)).setStatus("REVIEWED")); + + componentIndex.load(db.getSession(), List.of(file1)); + hotspotMeasureUpdater.apply(db.getSession(), matrix, componentIndex, true, 1000L); + + assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).get().getValue()).isEqualTo(3d); + assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).get().getValue()).isEqualTo(2d); + assertThat(matrix.getMeasure(project, SECURITY_REVIEW_RATING_KEY).get().getDataAsString()).isEqualTo("C"); + assertThat(matrix.getMeasure(project, SECURITY_HOTSPOTS_REVIEWED_KEY).get().getValue()).isEqualTo(60d); + + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY).get().getVariation()).isEqualTo(2d); + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY).get().getVariation()).isEqualTo(1d); + assertThat(matrix.getMeasure(project, NEW_SECURITY_REVIEW_RATING_KEY).get().getVariation()).isEqualTo(3); + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY).get().getVariation()).isCloseTo(66d, Offset.offset(1d)); + } + + @Test + public void dont_create_leak_measures_if_no_leak_period() { + db.issues().insertHotspot(project, dir, i -> i.setIssueCreationDate(new Date(1001)).setStatus("TO_REVIEW")); + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1002)).setStatus("REVIEWED")); + db.issues().insertHotspot(project, file1, i -> i.setIssueCreationDate(new Date(1003)).setStatus("REVIEWED")); + + componentIndex.load(db.getSession(), List.of(file1)); + hotspotMeasureUpdater.apply(db.getSession(), matrix, componentIndex, false, 1000L); + + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_STATUS_KEY)).isEmpty(); + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_TO_REVIEW_STATUS_KEY)).isEmpty(); + assertThat(matrix.getMeasure(project, NEW_SECURITY_REVIEW_RATING_KEY)).isEmpty(); + assertThat(matrix.getMeasure(project, NEW_SECURITY_HOTSPOTS_REVIEWED_KEY)).isEmpty(); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java new file mode 100644 index 00000000000..6ffedfbc48a --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/HotspotsCounterTest.java @@ -0,0 +1,49 @@ +/* + * SonarQube + * Copyright (C) 2009-2022 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.server.measure.live; + +import java.util.List; +import org.junit.Test; +import org.sonar.db.issue.HotspotGroupDto; + +import static java.util.Collections.emptyList; +import static org.assertj.core.api.Assertions.assertThat; + +public class HotspotsCounterTest { + @Test + public void counts_hotspots() { + HotspotGroupDto group1 = new HotspotGroupDto().setCount(3).setStatus("TO_REVIEW").setInLeak(false); + HotspotGroupDto group2 = new HotspotGroupDto().setCount(2).setStatus("REVIEWED").setInLeak(false); + HotspotGroupDto group3 = new HotspotGroupDto().setCount(1).setStatus("TO_REVIEW").setInLeak(true); + HotspotGroupDto group4 = new HotspotGroupDto().setCount(1).setStatus("REVIEWED").setInLeak(true); + + HotspotsCounter counter = new HotspotsCounter(List.of(group1, group2, group3, group4)); + assertThat(counter.countHotspotsByStatus("TO_REVIEW", true)).isEqualTo(1); + assertThat(counter.countHotspotsByStatus("REVIEWED", true)).isEqualTo(1); + assertThat(counter.countHotspotsByStatus("TO_REVIEW", false)).isEqualTo(4); + assertThat(counter.countHotspotsByStatus("REVIEWED", false)).isEqualTo(3); + } + + @Test + public void count_empty_hotspots() { + HotspotsCounter counter = new HotspotsCounter(emptyList()); + assertThat(counter.countHotspotsByStatus("TO_REVIEW", true)).isZero(); + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java index 947ffbf49d4..3c43901f342 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureComputerImplTest.java @@ -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(); - } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java index b8ec7cc9df3..b615830425d 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureModuleTest.java @@ -29,6 +29,6 @@ public class LiveMeasureModuleTest { public void verify_count_of_added_components() { ListContainer container = new ListContainer(); new LiveMeasureModule().configure(container); - assertThat(container.getAddedObjects()).hasSize(3); + assertThat(container.getAddedObjects()).isNotEmpty(); } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java new file mode 100644 index 00000000000..2f0413de2e9 --- /dev/null +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/LiveMeasureTreeUpdaterImplTest.java @@ -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); + } + } +} diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java index 1b706dc432c..e5869c20a74 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureMatrixTest.java @@ -42,7 +42,6 @@ public class MeasureMatrixTest { private static final MetricDto METRIC_1 = newMetricDto().setUuid("100"); private static final MetricDto METRIC_2 = newMetricDto().setUuid("200"); - @Test public void getMetric() { Collection<MetricDto> metrics = asList(METRIC_1, METRIC_2); @@ -128,7 +127,7 @@ public class MeasureMatrixTest { assertThat(underTest.getChanged()).hasSize(1); verifyValue(underTest, PROJECT, metric, 3.56); - verifyVariation(underTest, PROJECT, metric, 3.56 - (3.14 - 1.14)); + verifyVariation(underTest, PROJECT, metric, 1.14); } @Test @@ -138,10 +137,11 @@ public class MeasureMatrixTest { MeasureMatrix underTest = new MeasureMatrix(asList(PROJECT), asList(metric), asList(measure)); underTest.setValue(PROJECT, metric.getKey(), 3.569); + underTest.setLeakValue(PROJECT, metric.getKey(), 3.569); assertThat(underTest.getChanged()).hasSize(1); verifyValue(underTest, PROJECT, metric, 3.57); - verifyVariation(underTest, PROJECT, metric, 1.57); + verifyVariation(underTest, PROJECT, metric, 3.57); } @Test diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java index cf8ca2a30ef..bf7548dbb6e 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/IssueMetricFormulaFactoryImplTest.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/MeasureUpdateFormulaFactoryImplTest.java @@ -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,22 +42,84 @@ 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); with(newGroup(), newGroup().setCount(4)).assertThatValueIs(CoreMetrics.VIOLATIONS, 5); @@ -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; + } + } } diff --git a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java index bdf21241b6f..81da23c33ef 100644 --- a/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestIssueMetricFormulaFactory.java +++ b/server/sonar-webserver-webapi/src/test/java/org/sonar/server/measure/live/TestMeasureUpdateFormulaFactory.java @@ -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); } } |