@@ -23,6 +23,7 @@ import org.sonar.api.batch.fs.FilePredicates; | |||
import org.sonar.api.batch.fs.FileSystem; | |||
import org.sonar.api.batch.fs.InputFile; | |||
import org.sonar.api.batch.fs.InputFile.Type; | |||
import org.sonar.api.batch.fs.TextRange; | |||
import org.sonar.api.batch.sensor.Sensor; | |||
import org.sonar.api.batch.sensor.SensorContext; | |||
import org.sonar.api.batch.sensor.SensorDescriptor; | |||
@@ -56,15 +57,20 @@ public class OneBugIssuePerLineSensor implements Sensor { | |||
} | |||
} | |||
private void createIssues(InputFile file, SensorContext context, String repo) { | |||
private static void createIssues(InputFile file, SensorContext context, String repo) { | |||
RuleKey ruleKey = RuleKey.of(repo, RULE_KEY); | |||
for (int line = 1; line <= file.lines(); line++) { | |||
TextRange text = file.selectLine(line); | |||
// do not count empty lines, which can be a pain with end-of-file return | |||
if (text.end().lineOffset() == 0) { | |||
continue; | |||
} | |||
NewIssue newIssue = context.newIssue(); | |||
newIssue | |||
.forRule(ruleKey) | |||
.at(newIssue.newLocation() | |||
.on(file) | |||
.at(file.selectLine(line)) | |||
.at(text) | |||
.message("This bug issue is generated on each line")) | |||
.save(); | |||
} |
@@ -137,6 +137,7 @@ import org.sonar.server.plugins.ServerExtensionInstaller; | |||
import org.sonar.server.plugins.privileged.PrivilegedPluginsBootstraper; | |||
import org.sonar.server.plugins.privileged.PrivilegedPluginsStopper; | |||
import org.sonar.server.property.InternalPropertiesImpl; | |||
import org.sonar.server.qualitygate.QualityGateModule; | |||
import org.sonar.server.qualityprofile.index.ActiveRuleIndexer; | |||
import org.sonar.server.rule.CommonRuleDefinitionsImpl; | |||
import org.sonar.server.rule.DefaultRuleFinder; | |||
@@ -430,6 +431,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer { | |||
// webhooks | |||
WebhookModule.class, | |||
QualityGateModule.class, | |||
// cleaning | |||
CeCleaningModule.class); | |||
@@ -91,7 +91,8 @@ public class ComputeEngineContainerImplTest { | |||
assertThat(picoContainer.getComponentAdapters()) | |||
.hasSize( | |||
CONTAINER_ITSELF | |||
+ 77 // level 4 | |||
+ 78 // level 4 | |||
+ 21 // content of QualityGateModule | |||
+ 6 // content of CeConfigurationModule | |||
+ 4 // content of CeQueueModule | |||
+ 4 // content of CeHttpModule |
@@ -29,9 +29,11 @@ import javax.annotation.Nullable; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.apache.commons.lang.builder.ToStringBuilder; | |||
import org.sonar.api.resources.Scopes; | |||
import org.sonar.db.WildcardPosition; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.lang.String.format; | |||
import static org.sonar.db.DaoDatabaseUtils.buildLikeValue; | |||
import static org.sonar.db.component.ComponentValidator.checkComponentKey; | |||
import static org.sonar.db.component.ComponentValidator.checkComponentName; | |||
import static org.sonar.db.component.DbTagsReader.readDbTags; | |||
@@ -157,6 +159,10 @@ public class ComponentDto { | |||
return parent.getUuidPath() + parent.uuid() + UUID_PATH_SEPARATOR; | |||
} | |||
public String getUuidPathLikeIncludingSelf() { | |||
return buildLikeValue(formatUuidPathFromParent(this), WildcardPosition.AFTER); | |||
} | |||
public Long getId() { | |||
return id; | |||
} | |||
@@ -196,7 +202,7 @@ public class ComponentDto { | |||
/** | |||
* List of ancestor UUIDs, ordered by depth in tree. | |||
*/ | |||
List<String> getUuidPathAsList() { | |||
public List<String> getUuidPathAsList() { | |||
return UUID_PATH_SPLITTER.splitToList(uuidPath); | |||
} | |||
@@ -107,6 +107,10 @@ 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 void insert(DbSession session, IssueDto dto) { | |||
mapper(session).insert(dto); | |||
} |
@@ -0,0 +1,98 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
public class IssueGroupDto { | |||
private int ruleType; | |||
private String severity; | |||
@Nullable | |||
private String resolution; | |||
private String status; | |||
private double effort; | |||
private long count; | |||
private boolean inLeak; | |||
public int getRuleType() { | |||
return ruleType; | |||
} | |||
public String getSeverity() { | |||
return severity; | |||
} | |||
@CheckForNull | |||
public String getResolution() { | |||
return resolution; | |||
} | |||
public String getStatus() { | |||
return status; | |||
} | |||
public double getEffort() { | |||
return effort; | |||
} | |||
public long getCount() { | |||
return count; | |||
} | |||
public boolean isInLeak() { | |||
return inLeak; | |||
} | |||
public IssueGroupDto setRuleType(int ruleType) { | |||
this.ruleType = ruleType; | |||
return this; | |||
} | |||
public IssueGroupDto setSeverity(String severity) { | |||
this.severity = severity; | |||
return this; | |||
} | |||
public IssueGroupDto setResolution(@Nullable String resolution) { | |||
this.resolution = resolution; | |||
return this; | |||
} | |||
public IssueGroupDto setStatus(String status) { | |||
this.status = status; | |||
return this; | |||
} | |||
public IssueGroupDto setEffort(double effort) { | |||
this.effort = effort; | |||
return this; | |||
} | |||
public IssueGroupDto setCount(long count) { | |||
this.count = count; | |||
return this; | |||
} | |||
public IssueGroupDto setInLeak(boolean inLeak) { | |||
this.inLeak = inLeak; | |||
return this; | |||
} | |||
} |
@@ -19,10 +19,12 @@ | |||
*/ | |||
package org.sonar.db.issue; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Set; | |||
import org.apache.ibatis.annotations.Param; | |||
import org.apache.ibatis.session.ResultHandler; | |||
import org.sonar.db.component.ComponentDto; | |||
public interface IssueMapper { | |||
@@ -46,4 +48,8 @@ public interface IssueMapper { | |||
@Param("projectUuid") String projectUuid, | |||
@Param("likeModuleUuidPath") String likeModuleUuidPath, | |||
ResultHandler<IssueDto> handler); | |||
Collection<IssueGroupDto> selectIssueGroupsByBaseComponent( | |||
@Param("baseComponent") ComponentDto baseComponent, | |||
@Param("leakPeriodBeginningDate") long leakPeriodBeginningDate); | |||
} |
@@ -42,7 +42,7 @@ public class LiveMeasureDao implements Dao { | |||
this.system2 = system2; | |||
} | |||
public List<LiveMeasureDto> selectByComponentUuids(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) { | |||
public List<LiveMeasureDto> selectByComponentUuidsAndMetricIds(DbSession dbSession, Collection<String> largeComponentUuids, Collection<Integer> metricIds) { | |||
if (largeComponentUuids.isEmpty() || metricIds.isEmpty()) { | |||
return Collections.emptyList(); | |||
} | |||
@@ -90,6 +90,9 @@ public class LiveMeasureDao implements Dao { | |||
} | |||
} | |||
/** | |||
* Delete the rows that do NOT have the specified marker | |||
*/ | |||
public void deleteByProjectUuidExcludingMarker(DbSession dbSession, String projectUuid, String marker) { | |||
mapper(dbSession).deleteByProjectUuidExcludingMarker(projectUuid, marker); | |||
} |
@@ -47,7 +47,7 @@ public class MetricDao implements Dao { | |||
return mapper(session).selectByKey(key); | |||
} | |||
public List<MetricDto> selectByKeys(final DbSession session, List<String> keys) { | |||
public List<MetricDto> selectByKeys(final DbSession session, Collection<String> keys) { | |||
return executeLargeInputs(keys, mapper(session)::selectByKeys); | |||
} | |||
@@ -39,12 +39,11 @@ public class WebhookDeliveryDto extends WebhookDeliveryLiteDto<WebhookDeliveryDt | |||
return this; | |||
} | |||
@CheckForNull | |||
public String getPayload() { | |||
return payload; | |||
} | |||
public WebhookDeliveryDto setPayload(@Nullable String s) { | |||
public WebhookDeliveryDto setPayload(String s) { | |||
this.payload = s; | |||
return this; | |||
} |
@@ -246,5 +246,41 @@ | |||
p.module_uuid_path like #{likeModuleUuidPath, jdbcType=VARCHAR} escape '/' and | |||
i.status <> 'CLOSED' | |||
</select> | |||
<select id="selectIssueGroupsByBaseComponent" 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", (i.issue_creation_date >= #{leakPeriodBeginningDate,jdbcType=BIGINT}) as inLeak | |||
from issues i | |||
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid | |||
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}) | |||
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 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, case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
from issues i | |||
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid | |||
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}) | |||
) 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 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, case when i.issue_creation_date > #{leakPeriodBeginningDate,jdbcType=BIGINT} then 1 else 0 end as inLeak | |||
from issues i | |||
inner join projects p on p.uuid = i.component_uuid and p.project_uuid = i.project_uuid | |||
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}) | |||
) i2 | |||
group by i2.issue_type, i2.severity, i2.resolution, i2.status, i2.inLeak | |||
</select> | |||
</mapper> | |||
@@ -22,9 +22,10 @@ package org.sonar.db.component; | |||
import org.junit.Test; | |||
import org.sonar.api.resources.Qualifiers; | |||
import org.sonar.api.resources.Scopes; | |||
import org.sonar.db.organization.OrganizationTesting; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.db.organization.OrganizationTesting.newOrganizationDto; | |||
public class ComponentDtoTest { | |||
@@ -89,13 +90,27 @@ public class ComponentDtoTest { | |||
} | |||
@Test | |||
public void test_formatUuidPathFromParent() { | |||
ComponentDto parent = ComponentTesting.newPrivateProjectDto(OrganizationTesting.newOrganizationDto(), "123").setUuidPath(ComponentDto.UUID_PATH_OF_ROOT); | |||
public void formatUuidPathFromParent() { | |||
ComponentDto parent = ComponentTesting.newPrivateProjectDto(newOrganizationDto(), "123").setUuidPath(ComponentDto.UUID_PATH_OF_ROOT); | |||
assertThat(ComponentDto.formatUuidPathFromParent(parent)).isEqualTo(".123."); | |||
} | |||
@Test | |||
public void test_Name() { | |||
public void getUuidPathLikeIncludingSelf() { | |||
OrganizationDto organizationDto = newOrganizationDto(); | |||
ComponentDto project = ComponentTesting.newPrivateProjectDto(organizationDto).setUuidPath(ComponentDto.UUID_PATH_OF_ROOT); | |||
assertThat(project.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + ".%"); | |||
ComponentDto module = ComponentTesting.newModuleDto(project); | |||
assertThat(module.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + "." + module.uuid() + ".%"); | |||
ComponentDto file = ComponentTesting.newFileDto(module); | |||
assertThat(file.getUuidPathLikeIncludingSelf()).isEqualTo("." + project.uuid() + "." + module.uuid() + "." + file.uuid() + ".%"); | |||
} | |||
@Test | |||
public void getUuidPathAsList() { | |||
ComponentDto root = new ComponentDto().setUuidPath(ComponentDto.UUID_PATH_OF_ROOT); | |||
assertThat(root.getUuidPathAsList()).isEmpty(); | |||
@@ -104,7 +119,7 @@ public class ComponentDtoTest { | |||
} | |||
@Test | |||
public void test_getKey_and_getBranch() { | |||
public void getKey_and_getBranch() { | |||
ComponentDto underTest = new ComponentDto().setDbKey("my_key:BRANCH:my_branch"); | |||
assertThat(underTest.getKey()).isEqualTo("my_key"); | |||
assertThat(underTest.getBranch()).isEqualTo("my_branch"); |
@@ -21,6 +21,7 @@ package org.sonar.db.issue; | |||
import java.util.ArrayList; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
import java.util.List; | |||
import org.apache.ibatis.session.ResultContext; | |||
@@ -30,6 +31,7 @@ import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import org.sonar.api.issue.Issue; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.RowNotFoundException; | |||
@@ -234,6 +236,59 @@ public class IssueDaoTest { | |||
assertThat(fp.getIssueCreationDate()).isNotNull(); | |||
} | |||
@Test | |||
public void test_selectGroupsOfComponentTreeOnLeak_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); | |||
assertThat(groups).isEmpty(); | |||
} | |||
@Test | |||
public void selectGroupsOfComponentTreeOnLeak_on_file() { | |||
ComponentDto project = db.components().insertPublicProject(); | |||
ComponentDto file = db.components().insertComponent(ComponentTesting.newFileDto(project)); | |||
RuleDefinitionDto rule = db.rules().insert(); | |||
IssueDto fpBug = db.issues().insert(rule, project, file, | |||
i -> i.setStatus("RESOLVED").setResolution("FALSE-POSITIVE").setSeverity("MAJOR").setType(RuleType.BUG).setIssueCreationTime(1_500L)); | |||
IssueDto criticalBug1 = db.issues().insert(rule, project, file, | |||
i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_600L)); | |||
IssueDto criticalBug2 = db.issues().insert(rule, project, file, | |||
i -> i.setStatus("OPEN").setResolution(null).setSeverity("CRITICAL").setType(RuleType.BUG).setIssueCreationTime(1_700L)); | |||
// closed issues are ignored | |||
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); | |||
assertThat(result.stream().mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3); | |||
assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.BUG.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3); | |||
assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.CODE_SMELL.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
assertThat(result.stream().filter(g -> g.getRuleType()==RuleType.VULNERABILITY.getDbConstant()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
assertThat(result.stream().filter(g -> g.getSeverity().equals("CRITICAL")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2); | |||
assertThat(result.stream().filter(g -> g.getSeverity().equals("MAJOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1); | |||
assertThat(result.stream().filter(g -> g.getSeverity().equals("MINOR")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
assertThat(result.stream().filter(g -> g.getStatus().equals("OPEN")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(2); | |||
assertThat(result.stream().filter(g -> g.getStatus().equals("RESOLVED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1); | |||
assertThat(result.stream().filter(g -> g.getStatus().equals("CLOSED")).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
assertThat(result.stream().filter(g -> "FALSE-POSITIVE" .equals(g.getResolution())).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(1); | |||
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(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
// test leak | |||
result = underTest.selectIssueGroupsByBaseComponent(db.getSession(), file, 999_999_999L); | |||
assertThat(result.stream().filter(g -> g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(0); | |||
assertThat(result.stream().filter(g -> !g.isInLeak()).mapToLong(IssueGroupDto::getCount).sum()).isEqualTo(3); | |||
} | |||
private static IssueDto newIssueDto(String key) { | |||
IssueDto dto = new IssueDto(); | |||
dto.setComponent(new ComponentDto().setDbKey("struts:Action").setId(123L).setUuid("component-uuid")); |
@@ -19,59 +19,115 @@ | |||
*/ | |||
package org.sonar.db.measure; | |||
import java.nio.charset.StandardCharsets; | |||
import java.util.ArrayList; | |||
import java.util.List; | |||
import org.assertj.core.groups.Tuple; | |||
import org.junit.Before; | |||
import org.junit.Rule; | |||
import org.junit.Test; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.DbTester; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import static java.util.Arrays.asList; | |||
import static java.util.Collections.emptyList; | |||
import static java.util.Collections.singleton; | |||
import static java.util.Collections.singletonList; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.core.groups.Tuple.tuple; | |||
import static org.sonar.db.component.ComponentTesting.newFileDto; | |||
import static org.sonar.db.component.ComponentTesting.newPrivateProjectDto; | |||
import static org.sonar.db.measure.MeasureTesting.newLiveMeasure; | |||
public class LiveMeasureDaoTest { | |||
private static final int A_METRIC_ID = 42; | |||
@Rule | |||
public DbTester db = DbTester.create(System2.INSTANCE); | |||
private LiveMeasureDao underTest = db.getDbClient().liveMeasureDao(); | |||
private MetricDto metric; | |||
@Before | |||
public void setUp() throws Exception { | |||
metric = db.measures().insertMetric(); | |||
} | |||
@Test | |||
public void test_selectByComponentUuids() { | |||
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(A_METRIC_ID); | |||
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(A_METRIC_ID); | |||
public void test_selectByComponentUuidsAndMetricIds() { | |||
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId()); | |||
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId()); | |||
underTest.insert(db.getSession(), measure1); | |||
underTest.insert(db.getSession(), measure2); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(A_METRIC_ID)); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), | |||
asList(measure1.getComponentUuid(), measure2.getComponentUuid()), singletonList(metric.getId())); | |||
assertThat(selected) | |||
.extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString) | |||
.containsExactlyInAnyOrder( | |||
Tuple.tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()), | |||
Tuple.tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString())); | |||
tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()), | |||
tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString())); | |||
assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), emptyList(), singletonList(metric.getId()))).isEmpty(); | |||
assertThat(underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty(); | |||
} | |||
@Test | |||
public void selectByComponentUuids_returns_empty_list_if_metric_does_not_match() { | |||
LiveMeasureDto measure = newLiveMeasure().setMetricId(10); | |||
public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_metric_does_not_match() { | |||
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId()); | |||
underTest.insert(db.getSession(), measure); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(222)); | |||
int otherMetricId = metric.getId() + 100; | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(measure.getComponentUuid()), singletonList(otherMetricId)); | |||
assertThat(selected).isEmpty(); | |||
} | |||
@Test | |||
public void selectByComponentUuids_returns_empty_list_if_component_does_not_match() { | |||
public void selectByComponentUuidsAndMetricIds_returns_empty_list_if_component_does_not_match() { | |||
LiveMeasureDto measure = newLiveMeasure(); | |||
underTest.insert(db.getSession(), measure); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId())); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList("_missing_"), singletonList(measure.getMetricId())); | |||
assertThat(selected).isEmpty(); | |||
} | |||
@Test | |||
public void test_selectByComponentUuidsAndMetricKeys() { | |||
LiveMeasureDto measure1 = newLiveMeasure().setMetricId(metric.getId()); | |||
LiveMeasureDto measure2 = newLiveMeasure().setMetricId(metric.getId()); | |||
underTest.insert(db.getSession(), measure1); | |||
underTest.insert(db.getSession(), measure2); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), asList(measure1.getComponentUuid(), measure2.getComponentUuid()), | |||
singletonList(metric.getKey())); | |||
assertThat(selected) | |||
.extracting(LiveMeasureDto::getComponentUuid, LiveMeasureDto::getProjectUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getDataAsString) | |||
.containsExactlyInAnyOrder( | |||
tuple(measure1.getComponentUuid(), measure1.getProjectUuid(), measure1.getMetricId(), measure1.getValue(), measure1.getDataAsString()), | |||
tuple(measure2.getComponentUuid(), measure2.getProjectUuid(), measure2.getMetricId(), measure2.getValue(), measure2.getDataAsString())); | |||
assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), emptyList(), singletonList(metric.getKey()))).isEmpty(); | |||
assertThat(underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure1.getComponentUuid()), emptyList())).isEmpty(); | |||
} | |||
@Test | |||
public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_metric_does_not_match() { | |||
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId()); | |||
underTest.insert(db.getSession(), measure); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList(measure.getComponentUuid()), singletonList("_other_")); | |||
assertThat(selected).isEmpty(); | |||
} | |||
@Test | |||
public void selectByComponentUuidsAndMetricKeys_returns_empty_list_if_component_does_not_match() { | |||
LiveMeasureDto measure = newLiveMeasure().setMetricId(metric.getId()); | |||
underTest.insert(db.getSession(), measure); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricKeys(db.getSession(), singletonList("_missing_"), singletonList(metric.getKey())); | |||
assertThat(selected).isEmpty(); | |||
} | |||
@@ -96,6 +152,68 @@ public class LiveMeasureDaoTest { | |||
.isEqualToComparingFieldByField(stored); | |||
} | |||
@Test | |||
public void selectTreeByQuery() { | |||
List<LiveMeasureDto> results = new ArrayList<>(); | |||
MetricDto metric = db.measures().insertMetric(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14)); | |||
underTest.selectTreeByQuery(db.getSession(), project, | |||
MeasureTreeQuery.builder() | |||
.setMetricIds(singleton(metric.getId())) | |||
.setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(), | |||
context -> results.add(context.getResultObject())); | |||
assertThat(results).hasSize(1); | |||
LiveMeasureDto result = results.get(0); | |||
assertThat(result.getComponentUuid()).isEqualTo(file.uuid()); | |||
assertThat(result.getMetricId()).isEqualTo(metric.getId()); | |||
assertThat(result.getValue()).isEqualTo(3.14); | |||
} | |||
@Test | |||
public void selectTreeByQuery_with_empty_results() { | |||
List<LiveMeasureDto> results = new ArrayList<>(); | |||
underTest.selectTreeByQuery(db.getSession(), newPrivateProjectDto(db.getDefaultOrganization()), | |||
MeasureTreeQuery.builder().setStrategy(MeasureTreeQuery.Strategy.LEAVES).build(), | |||
context -> results.add(context.getResultObject())); | |||
assertThat(results).isEmpty(); | |||
} | |||
@Test | |||
public void selectMeasure_map_fields() { | |||
MetricDto metric = db.measures().insertMetric(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
underTest.insert(db.getSession(), newLiveMeasure(file, metric).setValue(3.14).setVariation(0.1).setData("text_value")); | |||
LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found")); | |||
assertThat(result).as("Fail to map fields of %s", result.toString()).extracting( | |||
LiveMeasureDto::getProjectUuid, LiveMeasureDto::getComponentUuid, LiveMeasureDto::getMetricId, LiveMeasureDto::getValue, LiveMeasureDto::getVariation, | |||
LiveMeasureDto::getDataAsString, LiveMeasureDto::getTextValue) | |||
.contains(project.uuid(), file.uuid(), metric.getId(), 3.14, 0.1, "text_value", "text_value"); | |||
} | |||
@Test | |||
public void insert_data() { | |||
byte[] data = "text_value".getBytes(StandardCharsets.UTF_8); | |||
MetricDto metric = db.measures().insertMetric(); | |||
ComponentDto project = db.components().insertPrivateProject(); | |||
ComponentDto file = db.components().insertComponent(newFileDto(project)); | |||
LiveMeasureDto measure = newLiveMeasure(file, metric).setData(data); | |||
underTest.insert(db.getSession(), measure); | |||
LiveMeasureDto result = underTest.selectMeasure(db.getSession(), file.uuid(), metric.getKey()).orElseThrow(() -> new IllegalArgumentException("Measure not found")); | |||
assertThat(new String(result.getData(), StandardCharsets.UTF_8)).isEqualTo("text_value"); | |||
assertThat(result.getDataAsString()).isEqualTo("text_value"); | |||
} | |||
@Test | |||
public void test_insertOrUpdate() { | |||
// insert | |||
@@ -139,7 +257,7 @@ public class LiveMeasureDaoTest { | |||
} | |||
private void verifyPersisted(LiveMeasureDto dto) { | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuids(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId())); | |||
List<LiveMeasureDto> selected = underTest.selectByComponentUuidsAndMetricIds(db.getSession(), singletonList(dto.getComponentUuid()), singletonList(dto.getMetricId())); | |||
assertThat(selected).hasSize(1); | |||
assertThat(selected.get(0)).isEqualToComparingFieldByField(dto); | |||
} |
@@ -412,8 +412,8 @@ public class PurgeDaoTest { | |||
underTest.deleteProject(dbSession, project1.uuid()); | |||
assertThat(dbClient.liveMeasureDao().selectByComponentUuids(dbSession, asList(project1.uuid(), module1.uuid()), asList(metric.getId()))).isEmpty(); | |||
assertThat(dbClient.liveMeasureDao().selectByComponentUuids(dbSession, asList(project2.uuid(), module2.uuid()), asList(metric.getId()))).hasSize(2); | |||
assertThat(dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, asList(project1.uuid(), module1.uuid()), asList(metric.getId()))).isEmpty(); | |||
assertThat(dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, asList(project2.uuid(), module2.uuid()), asList(metric.getId()))).hasSize(2); | |||
} | |||
private void verifyNoEffect(ComponentDto firstRoot, ComponentDto... otherRoots) { |
@@ -84,9 +84,10 @@ public class ComponentIndexer implements ProjectIndexer, NeedAuthorizationIndexe | |||
@Override | |||
public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, Cause cause) { | |||
switch (cause) { | |||
case MEASURE_CHANGE: | |||
case PROJECT_TAGS_UPDATE: | |||
case PERMISSION_CHANGE: | |||
// tags and permissions are not part of type components/component | |||
// measures, tags and permissions are not part of type components/component | |||
return emptyList(); | |||
case PROJECT_CREATION: |
@@ -190,7 +190,7 @@ public class AppAction implements ComponentsWsAction { | |||
List<MetricDto> metrics = dbClient.metricDao().selectByKeys(dbSession, METRIC_KEYS); | |||
Map<Integer, MetricDto> metricsById = Maps.uniqueIndex(metrics, MetricDto::getId); | |||
List<LiveMeasureDto> measures = dbClient.liveMeasureDao() | |||
.selectByComponentUuids(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet()); | |||
.selectByComponentUuidsAndMetricIds(dbSession, Collections.singletonList(component.uuid()), metricsById.keySet()); | |||
return Maps.uniqueIndex(measures, m -> metricsById.get(m.getMetricId()).getKey()); | |||
} | |||
@@ -163,7 +163,7 @@ public class PostProjectAnalysisTasksExecutor implements ComputationStepExecutor | |||
@CheckForNull | |||
private QualityGateImpl createQualityGate() { | |||
com.google.common.base.Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate(); | |||
Optional<org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate> qualityGateOptional = this.qualityGateHolder.getQualityGate(); | |||
if (qualityGateOptional.isPresent()) { | |||
org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGate qualityGate = qualityGateOptional.get(); | |||
@@ -20,7 +20,7 @@ | |||
package org.sonar.server.computation.task.projectanalysis.formula.counter; | |||
import javax.annotation.Nullable; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
/** | |||
* Convenience class wrapping a rating to compute the value and know it is has ever been set. |
@@ -19,14 +19,12 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import org.sonar.server.computation.task.projectanalysis.component.Component; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
public interface MutableQualityGateHolder extends QualityGateHolder { | |||
/** | |||
* Sets the quality gate. | |||
* Settings a quality gate more than once is not allowed and it can never be set to {@code null}. | |||
* | |||
* @param qualityGate a {@link Component}, can not be {@code null} | |||
* Setting a quality gate more than once is not allowed and it can never be set to {@code null}. | |||
* | |||
* @throws NullPointerException if {@code qualityGate} is {@code null} | |||
* @throws IllegalStateException if the holder has already been initialized | |||
@@ -34,9 +32,11 @@ public interface MutableQualityGateHolder extends QualityGateHolder { | |||
void setQualityGate(QualityGate qualityGate); | |||
/** | |||
* Sets that there is no quality gate for the project of the currently processed {@link ReportQueue.Item}. | |||
* Sets the evaluation of quality gate. | |||
* Setting more than once is not allowed and it can never be set to {@code null}. | |||
* | |||
* @throws NullPointerException if {@code qualityGate} is {@code null} | |||
* @throws IllegalStateException if the holder has already been initialized | |||
*/ | |||
void setNoQualityGate(); | |||
void setEvaluation(EvaluatedQualityGate evaluation); | |||
} |
@@ -19,7 +19,8 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import com.google.common.base.Optional; | |||
import java.util.Optional; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
public interface QualityGateHolder { | |||
/** | |||
@@ -28,4 +29,11 @@ public interface QualityGateHolder { | |||
* @throws IllegalStateException if the holder has not been initialized (ie. we don't know yet what is the QualityGate) | |||
*/ | |||
Optional<QualityGate> getQualityGate(); | |||
/** | |||
* Evaluation of quality gate, including status and condition details. | |||
* | |||
* @throws IllegalStateException if the holder has not been initialized (ie. gate has not been evaluated yet) | |||
*/ | |||
Optional<EvaluatedQualityGate> getEvaluation(); | |||
} |
@@ -19,44 +19,43 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import com.google.common.base.Optional; | |||
import javax.annotation.CheckForNull; | |||
import java.util.Optional; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import static com.google.common.base.Optional.absent; | |||
import static com.google.common.base.Optional.of; | |||
import static com.google.common.base.Preconditions.checkState; | |||
import static java.util.Objects.requireNonNull; | |||
public class QualityGateHolderImpl implements MutableQualityGateHolder { | |||
private boolean initialized = false; | |||
@CheckForNull | |||
private Optional<QualityGate> qualityGate; | |||
private QualityGate qualityGate; | |||
private EvaluatedQualityGate evaluation; | |||
@Override | |||
public void setQualityGate(QualityGate qualityGate) { | |||
public void setQualityGate(QualityGate g) { | |||
// fail fast | |||
requireNonNull(qualityGate); | |||
checkNotInitialized(); | |||
requireNonNull(g); | |||
checkState(qualityGate == null, "QualityGateHolder can be initialized only once"); | |||
this.initialized = true; | |||
this.qualityGate = of(qualityGate); | |||
this.qualityGate = g; | |||
} | |||
@Override | |||
public void setNoQualityGate() { | |||
checkNotInitialized(); | |||
this.initialized = true; | |||
this.qualityGate = absent(); | |||
public Optional<QualityGate> getQualityGate() { | |||
checkState(qualityGate != null, "QualityGate has not been set yet"); | |||
return Optional.of(qualityGate); | |||
} | |||
private void checkNotInitialized() { | |||
checkState(!initialized, "QualityGateHolder can be initialized only once"); | |||
@Override | |||
public void setEvaluation(EvaluatedQualityGate g) { | |||
// fail fast | |||
requireNonNull(g); | |||
checkState(evaluation == null, "QualityGateHolder evaluation can be initialized only once"); | |||
this.evaluation = g; | |||
} | |||
@Override | |||
public Optional<QualityGate> getQualityGate() { | |||
checkState(initialized, "QualityGate has not been set yet"); | |||
return qualityGate; | |||
public Optional<EvaluatedQualityGate> getEvaluation() { | |||
checkState(evaluation != null, "Evaluation of QualityGate has not been set yet"); | |||
return Optional.of(evaluation); | |||
} | |||
} |
@@ -30,8 +30,9 @@ public interface QualityGateService { | |||
Optional<QualityGate> findById(long id); | |||
/** | |||
* Retrieve the {@link QualityGate} from the database with the specified uuid, if it exists. | |||
* Retrieve the {@link QualityGate} from the database with the specified uuid. | |||
* @throws IllegalStateException if database is corrupted and default gate can't be found. | |||
*/ | |||
Optional<QualityGate> findDefaultQualityGate(Organization organizationDto); | |||
QualityGate findDefaultQualityGate(Organization organizationDto); | |||
} |
@@ -57,13 +57,13 @@ public class QualityGateServiceImpl implements QualityGateService { | |||
} | |||
@Override | |||
public Optional<QualityGate> findDefaultQualityGate(Organization organization) { | |||
public QualityGate findDefaultQualityGate(Organization organization) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
QualityGateDto qualityGateDto = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization.toDto(), organization.getDefaultQualityGateUuid()); | |||
if (qualityGateDto == null) { | |||
return Optional.empty(); | |||
throw new IllegalStateException("The default Quality gate is missing on organization " + organization.getKey()); | |||
} | |||
return Optional.of(toQualityGate(dbSession, qualityGateDto)); | |||
return toQualityGate(dbSession, qualityGateDto); | |||
} | |||
} | |||
@@ -21,20 +21,34 @@ package org.sonar.server.computation.task.projectanalysis.qualitymodel; | |||
import com.google.common.annotations.VisibleForTesting; | |||
import java.util.Arrays; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.utils.MessageException; | |||
import static java.lang.String.format; | |||
import static java.util.Arrays.stream; | |||
import static org.sonar.api.CoreProperties.RATING_GRID; | |||
import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES; | |||
public class RatingGrid { | |||
public class DebtRatingGrid { | |||
private final double[] gridValues; | |||
RatingGrid(double[] gridValues) { | |||
public DebtRatingGrid(double[] gridValues) { | |||
this.gridValues = Arrays.copyOf(gridValues, gridValues.length); | |||
} | |||
Rating getRatingForDensity(double density) { | |||
public DebtRatingGrid(Configuration config) { | |||
try { | |||
String[] grades = config.getStringArray(RATING_GRID); | |||
gridValues = new double[4]; | |||
for (int i = 0; i < 4; i++) { | |||
gridValues[i] = Double.parseDouble(grades[i]); | |||
} | |||
} catch (Exception e) { | |||
throw new IllegalArgumentException("The rating grid is incorrect. Expected something similar to '" | |||
+ RATING_GRID_DEF_VALUES + "' and got '" + config.get(RATING_GRID).orElse("") + "'", e); | |||
} | |||
} | |||
public Rating getRatingForDensity(double density) { | |||
for (Rating rating : Rating.values()) { | |||
double lowerBound = getGradeLowerBound(rating); | |||
if (density >= lowerBound) { | |||
@@ -44,7 +58,7 @@ public class RatingGrid { | |||
throw MessageException.of("The rating density value should be between 0 and " + Double.MAX_VALUE + " and got " + density); | |||
} | |||
double getGradeLowerBound(Rating rating) { | |||
public double getGradeLowerBound(Rating rating) { | |||
if (rating.getIndex() > 1) { | |||
return gridValues[rating.getIndex() - 2]; | |||
} | |||
@@ -56,30 +70,4 @@ public class RatingGrid { | |||
return gridValues; | |||
} | |||
public enum Rating { | |||
E(5), | |||
D(4), | |||
C(3), | |||
B(2), | |||
A(1); | |||
private final int index; | |||
Rating(int index) { | |||
this.index = index; | |||
} | |||
public int getIndex() { | |||
return index; | |||
} | |||
public static Rating valueOf(int index) { | |||
return stream(Rating.values()).filter(r -> r.getIndex() == index).findFirst().orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index))); | |||
} | |||
public static boolean isValidRating(String value) { | |||
return stream(Rating.values()).anyMatch(r -> r.name().equals(value)); | |||
} | |||
} | |||
} |
@@ -29,7 +29,6 @@ import org.sonar.server.computation.task.projectanalysis.measure.Measure; | |||
import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository; | |||
import org.sonar.server.computation.task.projectanalysis.metric.Metric; | |||
import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import static org.sonar.api.measures.CoreMetrics.DEVELOPMENT_COST_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.EFFORT_TO_REACH_MAINTAINABILITY_RATING_A_KEY; | |||
@@ -50,7 +49,6 @@ import static org.sonar.server.computation.task.projectanalysis.measure.Measure. | |||
public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<MaintainabilityMeasuresVisitor.Counter> { | |||
private final MeasureRepository measureRepository; | |||
private final RatingSettings ratingSettings; | |||
private final RatingGrid ratingGrid; | |||
private final Metric nclocMetric; | |||
private final Metric developmentCostMetric; | |||
@@ -64,7 +62,6 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main | |||
super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE); | |||
this.measureRepository = measureRepository; | |||
this.ratingSettings = ratingSettings; | |||
this.ratingGrid = ratingSettings.getRatingGrid(); | |||
// Input metrics | |||
this.nclocMetric = metricRepository.getByKey(NCLOC_KEY); | |||
@@ -134,7 +131,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main | |||
} | |||
private void addMaintainabilityRatingMeasure(Component component, double density) { | |||
Rating rating = ratingGrid.getRatingForDensity(density); | |||
Rating rating = ratingSettings.getDebtRatingGrid().getRatingForDensity(density); | |||
measureRepository.add(component, maintainabilityRatingMetric, newMeasureBuilder().create(rating.getIndex(), rating.name())); | |||
} | |||
@@ -142,7 +139,7 @@ public class MaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<Main | |||
long developmentCostValue = path.current().devCosts; | |||
Optional<Measure> effortMeasure = measureRepository.getRawMeasure(component, maintainabilityRemediationEffortMetric); | |||
long effort = effortMeasure.isPresent() ? effortMeasure.get().getLongValue() : 0L; | |||
long upperGradeCost = ((Double) (ratingGrid.getGradeLowerBound(Rating.B) * developmentCostValue)).longValue(); | |||
long upperGradeCost = ((Double) (ratingSettings.getDebtRatingGrid().getGradeLowerBound(Rating.B) * developmentCostValue)).longValue(); | |||
long effortToRatingA = upperGradeCost < effort ? (effort - upperGradeCost) : 0L; | |||
measureRepository.add(component, effortToMaintainabilityRatingAMetric, Measure.newMeasureBuilder().create(effortToRatingA)); | |||
} |
@@ -42,6 +42,7 @@ import org.sonar.server.computation.task.projectanalysis.scm.ScmInfo; | |||
import org.sonar.server.computation.task.projectanalysis.scm.ScmInfoRepository; | |||
import static org.sonar.api.measures.CoreMetrics.NCLOC_DATA_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_DEVELOPMENT_COST_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_MAINTAINABILITY_RATING_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_SQALE_DEBT_RATIO_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.NEW_TECHNICAL_DEBT_KEY; | |||
@@ -64,22 +65,21 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N | |||
private final MeasureRepository measureRepository; | |||
private final PeriodHolder periodHolder; | |||
private final RatingSettings ratingSettings; | |||
private final RatingGrid ratingGrid; | |||
private final Metric newDebtMetric; | |||
private final Metric nclocDataMetric; | |||
private final Metric newDevelopmentCostMetric; | |||
private final Metric newDebtRatioMetric; | |||
private final Metric newMaintainabilityRatingMetric; | |||
public NewMaintainabilityMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ScmInfoRepository scmInfoRepository, | |||
PeriodHolder periodHolder, RatingSettings ratingSettings) { | |||
PeriodHolder periodHolder, RatingSettings ratingSettings) { | |||
super(CrawlerDepthLimit.FILE, POST_ORDER, CounterFactory.INSTANCE); | |||
this.measureRepository = measureRepository; | |||
this.scmInfoRepository = scmInfoRepository; | |||
this.periodHolder = periodHolder; | |||
this.ratingSettings = ratingSettings; | |||
this.ratingGrid = ratingSettings.getRatingGrid(); | |||
// computed by NewDebtAggregator which is executed by IntegrateIssuesVisitor | |||
this.newDebtMetric = metricRepository.getByKey(NEW_TECHNICAL_DEBT_KEY); | |||
@@ -87,6 +87,7 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N | |||
this.nclocDataMetric = metricRepository.getByKey(NCLOC_DATA_KEY); | |||
// output metrics | |||
this.newDevelopmentCostMetric = metricRepository.getByKey(NEW_DEVELOPMENT_COST_KEY); | |||
this.newDebtRatioMetric = metricRepository.getByKey(NEW_SQALE_DEBT_RATIO_KEY); | |||
this.newMaintainabilityRatingMetric = metricRepository.getByKey(NEW_MAINTAINABILITY_RATING_KEY); | |||
} | |||
@@ -121,7 +122,9 @@ public class NewMaintainabilityMeasuresVisitor extends PathAwareVisitorAdapter<N | |||
} | |||
double density = computeDensity(path.current()); | |||
double newDebtRatio = 100.0 * density; | |||
double newMaintainability = ratingGrid.getRatingForDensity(density).getIndex(); | |||
double newMaintainability = ratingSettings.getDebtRatingGrid().getRatingForDensity(density).getIndex(); | |||
long newDevelopmentCost = path.current().getDevCost().getValue(); | |||
measureRepository.add(component, this.newDevelopmentCostMetric, newMeasureBuilder().setVariation(newDevelopmentCost).createNoValue()); | |||
measureRepository.add(component, this.newDebtRatioMetric, newMeasureBuilder().setVariation(newDebtRatio).createNoValue()); | |||
measureRepository.add(component, this.newMaintainabilityRatingMetric, newMeasureBuilder().setVariation(newMaintainability).createNoValue()); | |||
} |
@@ -47,12 +47,12 @@ import static org.sonar.api.utils.DateUtils.truncateToSeconds; | |||
import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER; | |||
import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.LEAVES; | |||
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E; | |||
/** | |||
* Compute following measures : |
@@ -0,0 +1,63 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.computation.task.projectanalysis.qualitymodel; | |||
import com.google.common.collect.ImmutableMap; | |||
import java.util.Map; | |||
import static java.lang.String.format; | |||
import static java.util.Arrays.stream; | |||
import static org.sonar.api.rule.Severity.BLOCKER; | |||
import static org.sonar.api.rule.Severity.CRITICAL; | |||
import static org.sonar.api.rule.Severity.INFO; | |||
import static org.sonar.api.rule.Severity.MAJOR; | |||
import static org.sonar.api.rule.Severity.MINOR; | |||
public enum Rating { | |||
E(5), | |||
D(4), | |||
C(3), | |||
B(2), | |||
A(1); | |||
private final int index; | |||
Rating(int index) { | |||
this.index = index; | |||
} | |||
public int getIndex() { | |||
return index; | |||
} | |||
public static Rating valueOf(int index) { | |||
return stream(Rating.values()) | |||
.filter(r -> r.getIndex() == index) | |||
.findFirst() | |||
.orElseThrow(() -> new IllegalArgumentException(format("Unknown value '%s'", index))); | |||
} | |||
public static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of( | |||
BLOCKER, E, | |||
CRITICAL, D, | |||
MAJOR, C, | |||
MINOR, B, | |||
INFO, A); | |||
} |
@@ -24,6 +24,7 @@ import java.util.Map; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.utils.MessageException; | |||
@@ -33,45 +34,22 @@ import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS; | |||
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY; | |||
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_MAN_DAYS_KEY; | |||
import static org.sonar.api.CoreProperties.LANGUAGE_SPECIFIC_PARAMETERS_SIZE_METRIC_KEY; | |||
import static org.sonar.api.CoreProperties.RATING_GRID; | |||
import static org.sonar.api.CoreProperties.RATING_GRID_DEF_VALUES; | |||
@ComputeEngineSide | |||
public class RatingSettings { | |||
private final Configuration config; | |||
private final DebtRatingGrid ratingGrid; | |||
private final long defaultDevCost; | |||
private final Map<String, LanguageSpecificConfiguration> languageSpecificConfigurationByLanguageKey; | |||
public RatingSettings(Configuration config) { | |||
this.config = config; | |||
this.languageSpecificConfigurationByLanguageKey = buildLanguageSpecificConfigurationByLanguageKey(config); | |||
ratingGrid = new DebtRatingGrid(config); | |||
defaultDevCost = initDefaultDevelopmentCost(config); | |||
languageSpecificConfigurationByLanguageKey = initLanguageSpecificConfigurationByLanguageKey(config); | |||
} | |||
private static Map<String, LanguageSpecificConfiguration> buildLanguageSpecificConfigurationByLanguageKey(Configuration config) { | |||
ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder(); | |||
String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS); | |||
for (String languageConfigIndex : languageConfigIndexes) { | |||
String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY; | |||
String languageKey = config.get(languagePropertyKey) | |||
.orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " + | |||
"Contact your administrator to update this configuration in the global administration section of SonarQube.")); | |||
builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex)); | |||
} | |||
return builder.build(); | |||
} | |||
public RatingGrid getRatingGrid() { | |||
try { | |||
String[] ratingGrades = config.getStringArray(RATING_GRID); | |||
double[] grid = new double[4]; | |||
for (int i = 0; i < 4; i++) { | |||
grid[i] = Double.parseDouble(ratingGrades[i]); | |||
} | |||
return new RatingGrid(grid); | |||
} catch (Exception e) { | |||
throw new IllegalArgumentException("The rating grid is incorrect. Expected something similar to '" | |||
+ RATING_GRID_DEF_VALUES + "' and got '" | |||
+ config.get(RATING_GRID).get() + "'", e); | |||
} | |||
public DebtRatingGrid getDebtRatingGrid() { | |||
return ratingGrid; | |||
} | |||
public long getDevCost(@Nullable String languageKey) { | |||
@@ -86,10 +64,28 @@ public class RatingSettings { | |||
} | |||
} | |||
return getDefaultDevelopmentCost(); | |||
return defaultDevCost; | |||
} | |||
private long getDefaultDevelopmentCost() { | |||
@CheckForNull | |||
private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) { | |||
return languageSpecificConfigurationByLanguageKey.get(languageKey); | |||
} | |||
private static Map<String, LanguageSpecificConfiguration> initLanguageSpecificConfigurationByLanguageKey(Configuration config) { | |||
ImmutableMap.Builder<String, LanguageSpecificConfiguration> builder = ImmutableMap.builder(); | |||
String[] languageConfigIndexes = config.getStringArray(LANGUAGE_SPECIFIC_PARAMETERS); | |||
for (String languageConfigIndex : languageConfigIndexes) { | |||
String languagePropertyKey = LANGUAGE_SPECIFIC_PARAMETERS + "." + languageConfigIndex + "." + LANGUAGE_SPECIFIC_PARAMETERS_LANGUAGE_KEY; | |||
String languageKey = config.get(languagePropertyKey) | |||
.orElseThrow(() -> MessageException.of("Technical debt configuration is corrupted. At least one language specific parameter has no Language key. " + | |||
"Contact your administrator to update this configuration in the global administration section of SonarQube.")); | |||
builder.put(languageKey, LanguageSpecificConfiguration.create(config, languageConfigIndex)); | |||
} | |||
return builder.build(); | |||
} | |||
private static long initDefaultDevelopmentCost(Configuration config) { | |||
try { | |||
return Long.parseLong(config.get(DEVELOPMENT_COST).get()); | |||
} catch (NumberFormatException e) { | |||
@@ -98,11 +94,6 @@ public class RatingSettings { | |||
} | |||
} | |||
@CheckForNull | |||
private LanguageSpecificConfiguration getSpecificParametersForLanguage(String languageKey) { | |||
return languageSpecificConfigurationByLanguageKey.get(languageKey); | |||
} | |||
@Immutable | |||
private static class LanguageSpecificConfiguration { | |||
private final String language; |
@@ -21,35 +21,23 @@ package org.sonar.server.computation.task.projectanalysis.qualitymodel; | |||
import com.google.common.collect.ImmutableMap; | |||
import java.util.Map; | |||
import org.sonar.api.ce.measure.Issue; | |||
import org.sonar.api.measures.CoreMetrics; | |||
import org.sonar.server.computation.task.projectanalysis.component.Component; | |||
import org.sonar.server.computation.task.projectanalysis.component.PathAwareVisitorAdapter; | |||
import org.sonar.server.computation.task.projectanalysis.formula.counter.RatingValue; | |||
import org.sonar.server.computation.task.projectanalysis.issue.ComponentIssuesRepository; | |||
import org.sonar.server.computation.task.projectanalysis.measure.Measure; | |||
import org.sonar.server.computation.task.projectanalysis.measure.MeasureRepository; | |||
import org.sonar.server.computation.task.projectanalysis.metric.Metric; | |||
import org.sonar.server.computation.task.projectanalysis.metric.MetricRepository; | |||
import static org.sonar.api.measures.CoreMetrics.RELIABILITY_RATING_KEY; | |||
import static org.sonar.api.measures.CoreMetrics.SECURITY_RATING_KEY; | |||
import static org.sonar.api.rule.Severity.BLOCKER; | |||
import static org.sonar.api.rule.Severity.CRITICAL; | |||
import static org.sonar.api.rule.Severity.INFO; | |||
import static org.sonar.api.rule.Severity.MAJOR; | |||
import static org.sonar.api.rule.Severity.MINOR; | |||
import static org.sonar.api.rules.RuleType.BUG; | |||
import static org.sonar.api.rules.RuleType.VULNERABILITY; | |||
import static org.sonar.server.computation.task.projectanalysis.component.ComponentVisitor.Order.POST_ORDER; | |||
import static org.sonar.server.computation.task.projectanalysis.component.CrawlerDepthLimit.FILE; | |||
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY; | |||
/** | |||
* Compute following measures for projects and descendants: | |||
@@ -58,20 +46,8 @@ import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rat | |||
*/ | |||
public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisitorAdapter<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> { | |||
private static final Map<String, Rating> RATING_BY_SEVERITY = ImmutableMap.of( | |||
BLOCKER, E, | |||
CRITICAL, D, | |||
MAJOR, C, | |||
MINOR, B, | |||
INFO, A); | |||
private final MeasureRepository measureRepository; | |||
private final ComponentIssuesRepository componentIssuesRepository; | |||
// Output metrics | |||
private final Metric reliabilityRatingMetric; | |||
private final Metric securityRatingMetric; | |||
private final Map<String, Metric> metricsByKey; | |||
public ReliabilityAndSecurityRatingMeasuresVisitor(MetricRepository metricRepository, MeasureRepository measureRepository, ComponentIssuesRepository componentIssuesRepository) { | |||
@@ -80,8 +56,8 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito | |||
this.componentIssuesRepository = componentIssuesRepository; | |||
// Output metrics | |||
this.reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY); | |||
this.securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY); | |||
Metric reliabilityRatingMetric = metricRepository.getByKey(RELIABILITY_RATING_KEY); | |||
Metric securityRatingMetric = metricRepository.getByKey(SECURITY_RATING_KEY); | |||
this.metricsByKey = ImmutableMap.of( | |||
RELIABILITY_RATING_KEY, reliabilityRatingMetric, | |||
@@ -94,13 +70,13 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito | |||
} | |||
@Override | |||
public void visitDirectory(Component directory, Path<Counter> path) { | |||
computeAndSaveMeasures(directory, path); | |||
public void visitModule(Component module, Path<Counter> path) { | |||
computeAndSaveMeasures(module, path); | |||
} | |||
@Override | |||
public void visitModule(Component module, Path<Counter> path) { | |||
computeAndSaveMeasures(module, path); | |||
public void visitDirectory(Component directory, Path<Counter> path) { | |||
computeAndSaveMeasures(directory, path); | |||
} | |||
@Override | |||
@@ -110,27 +86,27 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito | |||
private void computeAndSaveMeasures(Component component, Path<Counter> path) { | |||
processIssues(component, path); | |||
path.current().ratingValueByMetric.entrySet().forEach( | |||
entry -> measureRepository.add(component, metricsByKey.get(entry.getKey()), createRatingMeasure(entry.getValue().getValue()))); | |||
addToParent(path); | |||
path.current().ratingValueByMetric.forEach((key, value) -> { | |||
Rating rating = value.getValue(); | |||
measureRepository.add(component, metricsByKey.get(key), newMeasureBuilder().create(rating.getIndex(), rating.name())); | |||
}); | |||
if (!path.isRoot()) { | |||
path.parent().add(path.current()); | |||
} | |||
} | |||
private void processIssues(Component component, Path<Counter> path) { | |||
componentIssuesRepository.getIssues(component) | |||
.stream() | |||
.filter(issue -> issue.resolution() == null) | |||
.filter(issue -> issue.type().equals(BUG) || issue.type().equals(VULNERABILITY)) | |||
.forEach(path.current()::processIssue); | |||
} | |||
private static void addToParent(Path<Counter> path) { | |||
if (!path.isRoot()) { | |||
path.parent().add(path.current()); | |||
} | |||
} | |||
private static Measure createRatingMeasure(Rating rating) { | |||
return newMeasureBuilder().create(rating.getIndex(), rating.name()); | |||
.forEach(issue -> { | |||
Rating rating = RATING_BY_SEVERITY.get(issue.severity()); | |||
if (issue.type().equals(BUG)) { | |||
path.current().ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating); | |||
} else if (issue.type().equals(VULNERABILITY)) { | |||
path.current().ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating); | |||
} | |||
}); | |||
} | |||
static final class Counter { | |||
@@ -143,17 +119,9 @@ public class ReliabilityAndSecurityRatingMeasuresVisitor extends PathAwareVisito | |||
} | |||
void add(Counter otherCounter) { | |||
ratingValueByMetric.entrySet().forEach(e -> e.getValue().increment(otherCounter.ratingValueByMetric.get(e.getKey()))); | |||
ratingValueByMetric.forEach((key, value) -> value.increment(otherCounter.ratingValueByMetric.get(key))); | |||
} | |||
void processIssue(Issue issue) { | |||
Rating rating = RATING_BY_SEVERITY.get(issue.severity()); | |||
if (issue.type().equals(BUG)) { | |||
ratingValueByMetric.get(RELIABILITY_RATING_KEY).increment(rating); | |||
} else if (issue.type().equals(VULNERABILITY)) { | |||
ratingValueByMetric.get(SECURITY_RATING_KEY).increment(rating); | |||
} | |||
} | |||
} | |||
private static final class CounterFactory extends PathAwareVisitorAdapter.SimpleStackElementFactory<ReliabilityAndSecurityRatingMeasuresVisitor.Counter> { |
@@ -59,15 +59,11 @@ public class LoadQualityGateStep implements ComputationStep { | |||
qualityGate = getProjectQualityGate(); | |||
if (!qualityGate.isPresent()) { | |||
// No QG defined for the project, let's retrieve the QG on the organization | |||
qualityGate = getOrganizationDefaultQualityGate(); | |||
qualityGate = Optional.of(getOrganizationDefaultQualityGate()); | |||
} | |||
} | |||
if (qualityGate.isPresent()) { | |||
qualityGateHolder.setQualityGate(qualityGate.get()); | |||
} else { | |||
qualityGateHolder.setNoQualityGate(); | |||
} | |||
qualityGateHolder.setQualityGate(qualityGate.orElseThrow(() -> new IllegalStateException("Quality gate not present"))); | |||
} | |||
private Optional<QualityGate> getShortLivingBranchQualityGate() { | |||
@@ -100,7 +96,7 @@ public class LoadQualityGateStep implements ComputationStep { | |||
} | |||
} | |||
private Optional<QualityGate> getOrganizationDefaultQualityGate() { | |||
private QualityGate getOrganizationDefaultQualityGate() { | |||
return qualityGateService.findDefaultQualityGate(analysisMetadataHolder.getOrganization()); | |||
} | |||
@@ -20,7 +20,6 @@ | |||
package org.sonar.server.computation.task.projectanalysis.step; | |||
import com.google.common.base.Function; | |||
import com.google.common.base.Optional; | |||
import com.google.common.collect.ImmutableMap; | |||
import com.google.common.collect.Multimap; | |||
import com.google.common.collect.Ordering; | |||
@@ -28,6 +27,7 @@ import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.Nonnull; | |||
import javax.annotation.Nullable; | |||
@@ -177,7 +177,7 @@ public class QualityGateMeasuresStep implements ComputationStep { | |||
boolean ignoredConditions = false; | |||
for (Map.Entry<Metric, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) { | |||
Metric metric = entry.getKey(); | |||
Optional<Measure> measure = measureRepository.getRawMeasure(project, metric); | |||
com.google.common.base.Optional<Measure> measure = measureRepository.getRawMeasure(project, metric); | |||
if (!measure.isPresent()) { | |||
continue; | |||
} |
@@ -24,6 +24,7 @@ import java.util.Optional; | |||
import java.util.Set; | |||
import org.sonar.api.ce.posttask.PostProjectAnalysisTask; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.server.computation.task.projectanalysis.component.ConfigurationRepository; | |||
import org.sonar.server.qualitygate.Condition; | |||
@@ -80,7 +81,7 @@ public class WebhookPostTask implements PostProjectAnalysisTask { | |||
}) | |||
.collect(MoreCollectors.toSet()); | |||
return builder.setQualityGate(new org.sonar.server.qualitygate.QualityGate(qg.getId(), qg.getName(), conditions)) | |||
.setStatus(EvaluatedQualityGate.Status.valueOf(qg.getStatus().name())) | |||
.setStatus(Metric.Level.valueOf(qg.getStatus().name())) | |||
.build(); | |||
}) | |||
.orElse(null); |
@@ -40,7 +40,8 @@ public interface ProjectIndexer extends ResilientIndexer { | |||
PROJECT_DELETION, | |||
PROJECT_KEY_UPDATE, | |||
PROJECT_TAGS_UPDATE, | |||
PERMISSION_CHANGE | |||
PERMISSION_CHANGE, | |||
MEASURE_CHANGE | |||
} | |||
/** |
@@ -60,6 +60,11 @@ public abstract class AbstractChangeTagsAction extends Action { | |||
protected abstract Collection<String> getTagsToSet(Context context, Collection<String> tagsFromParams); | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return false; | |||
} | |||
private Set<String> parseTags(Map<String, Object> properties) { | |||
Set<String> result = new HashSet<>(); | |||
String tagsString = (String) properties.get(TAGS_PARAMETER); |
@@ -75,6 +75,8 @@ public abstract class Action { | |||
public abstract boolean execute(Map<String, Object> properties, Context context); | |||
public abstract boolean shouldRefreshMeasures(); | |||
public interface Context { | |||
DefaultIssue issue(); | |||
@@ -89,6 +89,11 @@ public class AssignAction extends Action { | |||
return isAssigneeMemberOfIssueOrganization(assignee, properties, context) && issueFieldsSetter.assign(context.issue(), assignee, context.issueChangeContext()); | |||
} | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return false; | |||
} | |||
private static boolean isAssigneeMemberOfIssueOrganization(@Nullable UserDto assignee, Map<String, Object> properties, Context context) { | |||
return assignee == null || ((Set<String>) properties.get(ASSIGNEE_ORGANIZATIONS)).contains(context.project().getOrganizationUuid()); | |||
} |
@@ -51,6 +51,11 @@ public class CommentAction extends Action { | |||
return true; | |||
} | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return false; | |||
} | |||
private static String comment(Map<String, Object> properties) { | |||
String param = (String) properties.get(COMMENT_PROPERTY); | |||
if (Strings.isNullOrEmpty(param)) { |
@@ -0,0 +1,39 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.issue; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
@ServerSide | |||
public interface IssueChangePostProcessor { | |||
/** | |||
* Refresh measures, quality gate status and send webhooks | |||
* | |||
* @param components the components of changed issues | |||
*/ | |||
void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components); | |||
} |
@@ -0,0 +1,46 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.issue; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.measure.live.LiveMeasureComputer; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners; | |||
public class IssueChangePostProcessorImpl implements IssueChangePostProcessor { | |||
private final LiveMeasureComputer liveMeasureComputer; | |||
private final QGChangeEventListeners qualityGateListeners; | |||
public IssueChangePostProcessorImpl(LiveMeasureComputer liveMeasureComputer, QGChangeEventListeners qualityGateListeners) { | |||
this.liveMeasureComputer = liveMeasureComputer; | |||
this.qualityGateListeners = qualityGateListeners; | |||
} | |||
@Override | |||
public void process(DbSession dbSession, List<DefaultIssue> changedIssues, Collection<ComponentDto> components) { | |||
List<QGChangeEvent> gateChangeEvents = liveMeasureComputer.refresh(dbSession, components); | |||
qualityGateListeners.broadcastOnIssueChange(changedIssues, gateChangeEvents); | |||
} | |||
} |
@@ -19,12 +19,14 @@ | |||
*/ | |||
package org.sonar.server.issue; | |||
import java.util.List; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rule.RuleStatus; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueChangeContext; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
@@ -42,38 +44,48 @@ public class IssueUpdater { | |||
private final DbClient dbClient; | |||
private final IssueStorage issueStorage; | |||
private final NotificationManager notificationService; | |||
private final IssueChangePostProcessor issueChangePostProcessor; | |||
public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService) { | |||
public IssueUpdater(DbClient dbClient, IssueStorage issueStorage, NotificationManager notificationService, | |||
IssueChangePostProcessor issueChangePostProcessor) { | |||
this.dbClient = dbClient; | |||
this.issueStorage = issueStorage; | |||
this.notificationService = notificationService; | |||
this.issueChangePostProcessor = issueChangePostProcessor; | |||
} | |||
/** | |||
* Same as {@link #saveIssue(DbSession, DefaultIssue, IssueChangeContext, String)} but populates the specified | |||
* {@link SearchResponseData} with the DTOs (rule and components) retrieved from DB to save the issue. | |||
*/ | |||
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) { | |||
Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey()); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid()); | |||
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid()); | |||
IssueDto issueDto = saveIssue(session, issue, context, comment, rule, project, component); | |||
public SearchResponseData saveIssueAndPreloadSearchResponseData(DbSession dbSession, DefaultIssue issue, IssueChangeContext context, | |||
@Nullable String comment, boolean refreshMeasures) { | |||
Optional<RuleDefinitionDto> rule = getRuleByKey(dbSession, issue.getRuleKey()); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.projectUuid()); | |||
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(dbSession, issue.componentUuid()); | |||
IssueDto issueDto = doSaveIssue(dbSession, issue, context, comment, rule, project, component); | |||
SearchResponseData result = new SearchResponseData(issueDto); | |||
rule.ifPresent(r -> result.setRules(singletonList(r))); | |||
result.addComponents(singleton(project)); | |||
result.addComponents(singleton(component)); | |||
if (refreshMeasures) { | |||
List<DefaultIssue> changedIssues = result.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(result.getIssues().size())); | |||
issueChangePostProcessor.process(dbSession, changedIssues, singleton(component)); | |||
} | |||
SearchResponseData preloadedSearchResponseData = new SearchResponseData(issueDto); | |||
rule.ifPresent(r -> preloadedSearchResponseData.setRules(singletonList(r))); | |||
preloadedSearchResponseData.addComponents(singleton(project)); | |||
preloadedSearchResponseData.addComponents(singleton(component)); | |||
return preloadedSearchResponseData; | |||
return result; | |||
} | |||
public IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment) { | |||
Optional<RuleDefinitionDto> rule = getRuleByKey(session, issue.getRuleKey()); | |||
ComponentDto project = dbClient.componentDao().selectOrFailByUuid(session, issue.projectUuid()); | |||
ComponentDto component = dbClient.componentDao().selectOrFailByUuid(session, issue.componentUuid()); | |||
return saveIssue(session, issue, context, comment, rule, project, component); | |||
return doSaveIssue(session, issue, context, comment, rule, project, component); | |||
} | |||
private IssueDto saveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment, | |||
private IssueDto doSaveIssue(DbSession session, DefaultIssue issue, IssueChangeContext context, @Nullable String comment, | |||
Optional<RuleDefinitionDto> rule, ComponentDto project, ComponentDto component) { | |||
IssueDto issueDto = issueStorage.save(session, issue); | |||
notificationService.scheduleForSending(new IssueChangeNotification() | |||
@@ -87,7 +99,7 @@ public class IssueUpdater { | |||
} | |||
private Optional<RuleDefinitionDto> getRuleByKey(DbSession session, RuleKey ruleKey) { | |||
Optional<RuleDefinitionDto> rule = Optional.ofNullable(dbClient.ruleDao().selectDefinitionByKey(session, ruleKey).orElse(null)); | |||
Optional<RuleDefinitionDto> rule = dbClient.ruleDao().selectDefinitionByKey(session, ruleKey); | |||
return (rule.isPresent() && rule.get().getStatus() != RuleStatus.REMOVED) ? rule : Optional.empty(); | |||
} | |||
@@ -61,6 +61,11 @@ public class SetSeverityAction extends Action { | |||
return issueUpdater.setManualSeverity(context.issue(), verifySeverityParameter(properties), context.issueChangeContext()); | |||
} | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return true; | |||
} | |||
private static String verifySeverityParameter(Map<String, Object> properties) { | |||
String param = (String) properties.get(SEVERITY_PARAMETER); | |||
checkArgument(!isNullOrEmpty(param), "Missing parameter : '%s'", SEVERITY_PARAMETER); |
@@ -61,6 +61,11 @@ public class SetTypeAction extends Action { | |||
return issueUpdater.setType(context.issue(), RuleType.valueOf(type), context.issueChangeContext()); | |||
} | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return true; | |||
} | |||
private static String verifyTypeParameter(Map<String, Object> properties) { | |||
String type = (String) properties.get(TYPE_PARAMETER); | |||
checkArgument(!isNullOrEmpty(type), "Missing parameter : '%s'", TYPE_PARAMETER); |
@@ -56,6 +56,11 @@ public class TransitionAction extends Action { | |||
return canExecuteTransition(issue, transition) && transitionService.doTransition(context.issue(), context.issueChangeContext(), transition(properties)); | |||
} | |||
@Override | |||
public boolean shouldRefreshMeasures() { | |||
return true; | |||
} | |||
private boolean canExecuteTransition(DefaultIssue issue, String transitionKey) { | |||
return transitionService.listTransitions(issue) | |||
.stream() |
@@ -108,10 +108,11 @@ public class IssueIndexer implements ProjectIndexer, NeedAuthorizationIndexer { | |||
switch (cause) { | |||
case PROJECT_CREATION: | |||
// nothing to do, issues do not exist at project creation | |||
case MEASURE_CHANGE: | |||
case PROJECT_KEY_UPDATE: | |||
case PROJECT_TAGS_UPDATE: | |||
case PERMISSION_CHANGE: | |||
// nothing to do, permissions, project key and tags are not used in type issues/issue | |||
// nothing to do. Measures, permissions, project key and tags are not used in type issues/issue | |||
return emptyList(); | |||
case PROJECT_DELETION: |
@@ -98,7 +98,7 @@ public class AddCommentAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
DefaultIssue defaultIssue = issueDto.toDefaultIssue(); | |||
issueFieldsSetter.addComment(defaultIssue, wsRequest.getText(), context); | |||
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, wsRequest.getText()); | |||
SearchResponseData preloadedSearchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, defaultIssue, context, wsRequest.getText(), false); | |||
responseWriter.write(defaultIssue.key(), preloadedSearchResponseData, request, response); | |||
} | |||
} |
@@ -67,7 +67,7 @@ public class AssignAction implements IssuesWsAction { | |||
private final OperationResponseWriter responseWriter; | |||
public AssignAction(System2 system2, UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, | |||
OperationResponseWriter responseWriter) { | |||
OperationResponseWriter responseWriter) { | |||
this.system2 = system2; | |||
this.userSession = userSession; | |||
this.dbClient = dbClient; | |||
@@ -121,7 +121,7 @@ public class AssignAction implements IssuesWsAction { | |||
} | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
if (issueFieldsSetter.assign(issue, user, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, null); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(dbSession, issue, context, null, false); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -28,9 +28,8 @@ import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Consumer; | |||
import java.util.function.Function; | |||
import java.util.function.Predicate; | |||
import java.util.stream.Stream; | |||
import java.util.stream.Collectors; | |||
import org.sonar.api.issue.DefaultTransitions; | |||
import org.sonar.api.rule.RuleKey; | |||
import org.sonar.api.rule.Severity; | |||
@@ -54,20 +53,15 @@ import org.sonar.db.rule.RuleDefinitionDto; | |||
import org.sonar.server.issue.Action; | |||
import org.sonar.server.issue.AddTagsAction; | |||
import org.sonar.server.issue.AssignAction; | |||
import org.sonar.server.issue.IssueChangePostProcessor; | |||
import org.sonar.server.issue.IssueStorage; | |||
import org.sonar.server.issue.RemoveTagsAction; | |||
import org.sonar.server.issue.SetTypeAction; | |||
import org.sonar.server.issue.TransitionAction; | |||
import org.sonar.server.issue.notification.IssueChangeNotification; | |||
import org.sonar.server.notification.NotificationManager; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners; | |||
import org.sonar.server.user.UserSession; | |||
import org.sonarqube.ws.Issues; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static com.google.common.collect.ImmutableList.copyOf; | |||
import static com.google.common.collect.ImmutableMap.of; | |||
import static java.lang.String.format; | |||
import static java.util.function.Function.identity; | |||
@@ -111,20 +105,18 @@ public class BulkChangeAction implements IssuesWsAction { | |||
private final IssueStorage issueStorage; | |||
private final NotificationManager notificationService; | |||
private final List<Action> actions; | |||
private final QGChangeEventFactory qgChangeEventFactory; | |||
private final QGChangeEventListeners qgChangeEventListeners; | |||
private final IssueChangePostProcessor issueChangePostProcessor; | |||
public BulkChangeAction(System2 system2, UserSession userSession, DbClient dbClient, IssueStorage issueStorage, | |||
NotificationManager notificationService, List<Action> actions, | |||
QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) { | |||
IssueChangePostProcessor issueChangePostProcessor) { | |||
this.system2 = system2; | |||
this.userSession = userSession; | |||
this.dbClient = dbClient; | |||
this.issueStorage = issueStorage; | |||
this.notificationService = notificationService; | |||
this.actions = actions; | |||
this.qgChangeEventFactory = qgChangeEventFactory; | |||
this.qgChangeEventListeners = qgChangeEventListeners; | |||
this.issueChangePostProcessor = issueChangePostProcessor; | |||
} | |||
@Override | |||
@@ -187,53 +179,41 @@ public class BulkChangeAction implements IssuesWsAction { | |||
public void handle(Request request, Response response) throws Exception { | |||
userSession.checkLoggedIn(); | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Issues.BulkChangeWsResponse wsResponse = Stream.of(request) | |||
.map(loadData(dbSession)) | |||
.map(executeBulkChange()) | |||
.map(toWsResponse()) | |||
.collect(MoreCollectors.toOneElement()); | |||
writeProtobuf(wsResponse, request, response); | |||
BulkChangeResult result = executeBulkChange(dbSession, request); | |||
writeProtobuf(toWsResponse(result), request, response); | |||
} | |||
} | |||
private Function<Request, BulkChangeData> loadData(DbSession dbSession) { | |||
return request -> new BulkChangeData(dbSession, request); | |||
} | |||
private BulkChangeResult executeBulkChange(DbSession dbSession, Request request) { | |||
BulkChangeData bulkChangeData = new BulkChangeData(dbSession, request); | |||
BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size()); | |||
IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
private Function<BulkChangeData, BulkChangeResult> executeBulkChange() { | |||
return bulkChangeData -> { | |||
BulkChangeResult result = new BulkChangeResult(bulkChangeData.issues.size()); | |||
IssueChangeContext issueChangeContext = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
List<DefaultIssue> items = bulkChangeData.issues.stream() | |||
.filter(bulkChange(issueChangeContext, bulkChangeData, result)) | |||
.collect(MoreCollectors.toList()); | |||
issueStorage.save(items); | |||
List<DefaultIssue> items = bulkChangeData.issues.stream() | |||
.filter(bulkChange(issueChangeContext, bulkChangeData, result)) | |||
.collect(MoreCollectors.toList()); | |||
issueStorage.save(items); | |||
items.forEach(sendNotification(issueChangeContext, bulkChangeData)); | |||
buildWebhookIssueChange(bulkChangeData.propertiesByActions) | |||
.ifPresent(issueChange -> { | |||
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( | |||
bulkChangeData.issues.stream().filter(i -> result.success.contains(i.key())).collect(MoreCollectors.toList()), | |||
copyOf(bulkChangeData.componentsByUuid.values())); | |||
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, issueChange, issueChangeContext); | |||
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents); | |||
}); | |||
return result; | |||
}; | |||
refreshLiveMeasures(dbSession, bulkChangeData, result); | |||
items.forEach(sendNotification(issueChangeContext, bulkChangeData)); | |||
return result; | |||
} | |||
private static Optional<QGChangeEventFactory.IssueChange> buildWebhookIssueChange(Map<String, Map<String, Object>> propertiesByActions) { | |||
RuleType ruleType = Optional.ofNullable(propertiesByActions.get(SetTypeAction.SET_TYPE_KEY)) | |||
.map(t -> (String) t.get(SetTypeAction.TYPE_PARAMETER)) | |||
.map(RuleType::valueOf) | |||
.orElse(null); | |||
String transitionKey = Optional.ofNullable(propertiesByActions.get(TransitionAction.DO_TRANSITION_KEY)) | |||
.map(t -> (String) t.get(TransitionAction.TRANSITION_PARAMETER)) | |||
.orElse(null); | |||
if (ruleType == null && transitionKey == null) { | |||
return Optional.empty(); | |||
private void refreshLiveMeasures(DbSession dbSession, BulkChangeData data, BulkChangeResult result) { | |||
if (!data.shouldRefreshMeasures()) { | |||
return; | |||
} | |||
return Optional.of(new QGChangeEventFactory.IssueChange(ruleType, transitionKey)); | |||
Set<String> touchedComponentUuids = result.success.stream() | |||
.map(DefaultIssue::componentUuid) | |||
.collect(Collectors.toSet()); | |||
List<ComponentDto> touchedComponents = touchedComponentUuids.stream() | |||
.map(data.componentsByUuid::get) | |||
.collect(MoreCollectors.toList(touchedComponentUuids.size())); | |||
List<DefaultIssue> changedIssues = data.issues.stream().filter(result.success::contains).collect(MoreCollectors.toList()); | |||
issueChangePostProcessor.process(dbSession, changedIssues, touchedComponents); | |||
} | |||
private static Predicate<DefaultIssue> bulkChange(IssueChangeContext issueChangeContext, BulkChangeData bulkChangeData, BulkChangeResult result) { | |||
@@ -241,7 +221,7 @@ public class BulkChangeAction implements IssuesWsAction { | |||
ActionContext actionContext = new ActionContext(issue, issueChangeContext, bulkChangeData.projectsByUuid.get(issue.projectUuid())); | |||
bulkChangeData.getActionsWithoutComment().forEach(applyAction(actionContext, bulkChangeData, result)); | |||
addCommentIfNeeded(actionContext, bulkChangeData); | |||
return result.success.contains(issue.key()); | |||
return result.success.contains(issue); | |||
}; | |||
} | |||
@@ -277,12 +257,12 @@ public class BulkChangeAction implements IssuesWsAction { | |||
}; | |||
} | |||
private static Function<BulkChangeResult, Issues.BulkChangeWsResponse> toWsResponse() { | |||
return bulkChangeResult -> Issues.BulkChangeWsResponse.newBuilder() | |||
.setTotal(bulkChangeResult.getTotal()) | |||
.setSuccess(bulkChangeResult.getSuccess()) | |||
.setIgnored((long) bulkChangeResult.getTotal() - (bulkChangeResult.getSuccess() + bulkChangeResult.getFailures())) | |||
.setFailures(bulkChangeResult.getFailures()) | |||
private static Issues.BulkChangeWsResponse toWsResponse(BulkChangeResult result) { | |||
return Issues.BulkChangeWsResponse.newBuilder() | |||
.setTotal(result.countTotal()) | |||
.setSuccess(result.countSuccess()) | |||
.setIgnored((long) result.countTotal() - (result.countSuccess() + result.countFailures())) | |||
.setFailures(result.countFailures()) | |||
.build(); | |||
} | |||
@@ -391,11 +371,15 @@ public class BulkChangeAction implements IssuesWsAction { | |||
long actionsDefined = actions.stream().filter(action -> !action.equals(COMMENT_KEY)).count(); | |||
checkArgument(actionsDefined > 0, "At least one action must be provided"); | |||
} | |||
private boolean shouldRefreshMeasures() { | |||
return availableActions.stream().anyMatch(Action::shouldRefreshMeasures); | |||
} | |||
} | |||
private static class BulkChangeResult { | |||
private final int total; | |||
private Set<String> success = new HashSet<>(); | |||
private final Set<DefaultIssue> success = new HashSet<>(); | |||
private int failures = 0; | |||
BulkChangeResult(int total) { | |||
@@ -403,22 +387,22 @@ public class BulkChangeAction implements IssuesWsAction { | |||
} | |||
void increaseSuccess(DefaultIssue issue) { | |||
this.success.add(issue.key()); | |||
this.success.add(issue); | |||
} | |||
void increaseFailure() { | |||
this.failures++; | |||
} | |||
public int getTotal() { | |||
int countTotal() { | |||
return total; | |||
} | |||
public int getSuccess() { | |||
int countSuccess() { | |||
return success.size(); | |||
} | |||
public int getFailures() { | |||
int countFailures() { | |||
return failures; | |||
} | |||
} |
@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws; | |||
import com.google.common.io.Resources; | |||
import java.util.Date; | |||
import java.util.List; | |||
import org.sonar.api.issue.DefaultTransitions; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
@@ -37,13 +36,8 @@ import org.sonar.db.issue.IssueDto; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.issue.IssueUpdater; | |||
import org.sonar.server.issue.TransitionService; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners; | |||
import org.sonar.server.user.UserSession; | |||
import static com.google.common.collect.ImmutableList.copyOf; | |||
import static org.sonar.core.util.stream.MoreCollectors.toList; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_DO_TRANSITION; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_TRANSITION; | |||
@@ -57,11 +51,9 @@ public class DoTransitionAction implements IssuesWsAction { | |||
private final TransitionService transitionService; | |||
private final OperationResponseWriter responseWriter; | |||
private final System2 system2; | |||
private final QGChangeEventFactory qgChangeEventFactory; | |||
private final QGChangeEventListeners qgChangeEventListeners; | |||
public DoTransitionAction(DbClient dbClient, UserSession userSession, IssueFinder issueFinder, IssueUpdater issueUpdater, TransitionService transitionService, | |||
OperationResponseWriter responseWriter, System2 system2, QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) { | |||
OperationResponseWriter responseWriter, System2 system2) { | |||
this.dbClient = dbClient; | |||
this.userSession = userSession; | |||
this.issueFinder = issueFinder; | |||
@@ -69,8 +61,6 @@ public class DoTransitionAction implements IssuesWsAction { | |||
this.transitionService = transitionService; | |||
this.responseWriter = responseWriter; | |||
this.system2 = system2; | |||
this.qgChangeEventFactory = qgChangeEventFactory; | |||
this.qgChangeEventListeners = qgChangeEventListeners; | |||
} | |||
@Override | |||
@@ -112,13 +102,7 @@ public class DoTransitionAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
transitionService.checkTransitionPermission(transitionKey, defaultIssue); | |||
if (transitionService.doTransition(defaultIssue, context, transitionKey)) { | |||
SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null); | |||
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( | |||
searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(toList(searchResponseData.getIssues().size())), | |||
copyOf(searchResponseData.getComponents())); | |||
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(transitionKey), context); | |||
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents); | |||
return searchResponseData; | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, defaultIssue, context, null, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -28,8 +28,6 @@ import org.sonar.server.issue.ServerIssueStorage; | |||
import org.sonar.server.issue.TransitionService; | |||
import org.sonar.server.issue.workflow.FunctionExecutor; | |||
import org.sonar.server.issue.workflow.IssueWorkflow; | |||
import org.sonar.server.qualitygate.LiveQualityGateFactoryImpl; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactoryImpl; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListenersImpl; | |||
import org.sonar.server.settings.ProjectConfigurationLoaderImpl; | |||
import org.sonar.server.webhook.WebhookQGChangeEventListener; | |||
@@ -68,8 +66,6 @@ public class IssueWsModule extends Module { | |||
ChangelogAction.class, | |||
BulkChangeAction.class, | |||
ProjectConfigurationLoaderImpl.class, | |||
LiveQualityGateFactoryImpl.class, | |||
QGChangeEventFactoryImpl.class, | |||
WebhookQGChangeEventListener.class, | |||
QGChangeEventListenersImpl.class); | |||
} |
@@ -107,7 +107,7 @@ public class SetSeverityAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin()); | |||
if (issueFieldsSetter.setManualSeverity(issue, severity, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -105,7 +105,7 @@ public class SetTagsAction implements IssuesWsAction { | |||
DefaultIssue issue = issueDto.toDefaultIssue(); | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(), userSession.getLogin()); | |||
if (issueFieldsSetter.setTags(issue, tags, context)) { | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null); | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, false); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -21,7 +21,6 @@ package org.sonar.server.issue.ws; | |||
import com.google.common.io.Resources; | |||
import java.util.Date; | |||
import java.util.List; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.server.ws.Change; | |||
import org.sonar.api.server.ws.Request; | |||
@@ -31,19 +30,14 @@ import org.sonar.api.utils.System2; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueChangeContext; | |||
import org.sonar.core.util.Uuids; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.issue.IssueDto; | |||
import org.sonar.server.issue.IssueFieldsSetter; | |||
import org.sonar.server.issue.IssueFinder; | |||
import org.sonar.server.issue.IssueUpdater; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventFactory; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListeners; | |||
import org.sonar.server.user.UserSession; | |||
import static com.google.common.collect.ImmutableList.copyOf; | |||
import static org.sonar.api.web.UserRole.ISSUE_ADMIN; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.ACTION_SET_TYPE; | |||
import static org.sonarqube.ws.client.issue.IssuesWsParameters.PARAM_ISSUE; | |||
@@ -58,12 +52,9 @@ public class SetTypeAction implements IssuesWsAction { | |||
private final IssueUpdater issueUpdater; | |||
private final OperationResponseWriter responseWriter; | |||
private final System2 system2; | |||
private final QGChangeEventFactory qgChangeEventFactory; | |||
private final QGChangeEventListeners qgChangeEventListeners; | |||
public SetTypeAction(UserSession userSession, DbClient dbClient, IssueFinder issueFinder, IssueFieldsSetter issueFieldsSetter, IssueUpdater issueUpdater, | |||
OperationResponseWriter responseWriter, System2 system2, | |||
QGChangeEventFactory qgChangeEventFactory, QGChangeEventListeners qgChangeEventListeners) { | |||
OperationResponseWriter responseWriter, System2 system2) { | |||
this.userSession = userSession; | |||
this.dbClient = dbClient; | |||
this.issueFinder = issueFinder; | |||
@@ -71,8 +62,6 @@ public class SetTypeAction implements IssuesWsAction { | |||
this.issueUpdater = issueUpdater; | |||
this.responseWriter = responseWriter; | |||
this.system2 = system2; | |||
this.qgChangeEventFactory = qgChangeEventFactory; | |||
this.qgChangeEventListeners = qgChangeEventListeners; | |||
} | |||
@Override | |||
@@ -121,13 +110,7 @@ public class SetTypeAction implements IssuesWsAction { | |||
IssueChangeContext context = IssueChangeContext.createUser(new Date(system2.now()), userSession.getLogin()); | |||
if (issueFieldsSetter.setType(issue, ruleType, context)) { | |||
SearchResponseData searchResponseData = issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null); | |||
QGChangeEventFactory.IssueChangeData issueChangeData = new QGChangeEventFactory.IssueChangeData( | |||
searchResponseData.getIssues().stream().map(IssueDto::toDefaultIssue).collect(MoreCollectors.toList(searchResponseData.getIssues().size())), | |||
copyOf(searchResponseData.getComponents())); | |||
List<QGChangeEvent> qgChangeEvents = qgChangeEventFactory.from(issueChangeData, new QGChangeEventFactory.IssueChange(ruleType), context); | |||
qgChangeEventListeners.broadcastOnIssueChange(issueChangeData, qgChangeEvents); | |||
return searchResponseData; | |||
return issueUpdater.saveIssueAndPreloadSearchResponseData(session, issue, context, null, true); | |||
} | |||
return new SearchResponseData(issueDto); | |||
} |
@@ -78,8 +78,8 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization | |||
} | |||
@Override | |||
public void indexOnAnalysis(String branchUuid) { | |||
doIndex(Size.REGULAR, branchUuid); | |||
public void indexOnAnalysis(String projectUuid) { | |||
doIndex(Size.REGULAR, projectUuid); | |||
} | |||
@Override | |||
@@ -88,7 +88,7 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization | |||
case PERMISSION_CHANGE: | |||
// nothing to do, permissions are not used in type projectmeasures/projectmeasure | |||
return Collections.emptyList(); | |||
case MEASURE_CHANGE: | |||
case PROJECT_KEY_UPDATE: | |||
// project must be re-indexed because key is used in this index | |||
case PROJECT_CREATION: | |||
@@ -106,6 +106,17 @@ public class ProjectMeasuresIndexer implements ProjectIndexer, NeedAuthorization | |||
} | |||
} | |||
public IndexingResult commitAndIndex(DbSession dbSession, Collection<String> projectUuids) { | |||
List<EsQueueDto> items = projectUuids.stream() | |||
.map(projectUuid -> EsQueueDto.create(INDEX_TYPE_PROJECT_MEASURES.format(), projectUuid, null, projectUuid)) | |||
.collect(MoreCollectors.toArrayList(projectUuids.size())); | |||
dbClient.esQueueDao().insert(dbSession, items); | |||
dbSession.commit(); | |||
return index(dbSession, items); | |||
} | |||
@Override | |||
public IndexingResult index(DbSession dbSession, Collection<EsQueueDto> items) { | |||
if (items.isEmpty()) { |
@@ -0,0 +1,153 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.EnumMap; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.rule.Severity; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.db.issue.IssueGroupDto; | |||
import org.sonar.db.rule.SeverityUtil; | |||
class IssueCounter { | |||
private final Map<RuleType, HighestSeverity> highestSeverityOfUnresolved = new EnumMap<>(RuleType.class); | |||
private final Map<RuleType, Effort> effortOfUnresolved = new EnumMap<>(RuleType.class); | |||
private final Map<String, Count> unresolvedBySeverity = new HashMap<>(); | |||
private final Map<RuleType, Count> unresolvedByType = new EnumMap<>(RuleType.class); | |||
private final Map<String, Count> byResolution = new HashMap<>(); | |||
private final Map<String, Count> byStatus = new HashMap<>(); | |||
private final Count unresolved = new Count(); | |||
IssueCounter(Collection<IssueGroupDto> groups) { | |||
for (IssueGroupDto group : groups) { | |||
RuleType ruleType = RuleType.valueOf(group.getRuleType()); | |||
if (group.getResolution() == null) { | |||
highestSeverityOfUnresolved | |||
.computeIfAbsent(ruleType, k -> new HighestSeverity()) | |||
.add(group); | |||
effortOfUnresolved | |||
.computeIfAbsent(ruleType, k -> new Effort()) | |||
.add(group); | |||
unresolvedBySeverity | |||
.computeIfAbsent(group.getSeverity(), k -> new Count()) | |||
.add(group); | |||
unresolvedByType | |||
.computeIfAbsent(ruleType, k -> new Count()) | |||
.add(group); | |||
unresolved.add(group); | |||
} else { | |||
byResolution | |||
.computeIfAbsent(group.getResolution(), k -> new Count()) | |||
.add(group); | |||
} | |||
if (group.getStatus() != null) { | |||
byStatus | |||
.computeIfAbsent(group.getStatus(), k -> new Count()) | |||
.add(group); | |||
} | |||
} | |||
} | |||
public Optional<String> getHighestSeverityOfUnresolved(RuleType ruleType, boolean onlyInLeak) { | |||
return Optional.ofNullable(highestSeverityOfUnresolved.get(ruleType)) | |||
.map(hs -> hs.severity(onlyInLeak)); | |||
} | |||
public double sumEffortOfUnresolved(RuleType type, boolean onlyInLeak) { | |||
Effort effort = effortOfUnresolved.get(type); | |||
if (effort == null) { | |||
return 0.0; | |||
} | |||
return onlyInLeak ? effort.leak : effort.absolute; | |||
} | |||
public long countUnresolvedBySeverity(String severity, boolean onlyInLeak) { | |||
return value(unresolvedBySeverity.get(severity), onlyInLeak); | |||
} | |||
public long countByResolution(String resolution, boolean onlyInLeak) { | |||
return value(byResolution.get(resolution), onlyInLeak); | |||
} | |||
public long countUnresolvedByType(RuleType type, boolean onlyInLeak) { | |||
return value(unresolvedByType.get(type), onlyInLeak); | |||
} | |||
public long countByStatus(String status, boolean onlyInLeak) { | |||
return value(byStatus.get(status), onlyInLeak); | |||
} | |||
public long countUnresolved(boolean onlyInLeak) { | |||
return value(unresolved, 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(IssueGroupDto group) { | |||
absolute += group.getCount(); | |||
if (group.isInLeak()) { | |||
leak += group.getCount(); | |||
} | |||
} | |||
} | |||
private static class Effort { | |||
private double absolute = 0.0; | |||
private double leak = 0.0; | |||
void add(IssueGroupDto group) { | |||
absolute += group.getEffort(); | |||
if (group.isInLeak()) { | |||
leak += group.getEffort(); | |||
} | |||
} | |||
} | |||
private static class HighestSeverity { | |||
private int absolute = SeverityUtil.getOrdinalFromSeverity(Severity.INFO); | |||
private int leak = SeverityUtil.getOrdinalFromSeverity(Severity.INFO); | |||
void add(IssueGroupDto group) { | |||
int severity = SeverityUtil.getOrdinalFromSeverity(group.getSeverity()); | |||
absolute = Math.max(severity, absolute); | |||
if (group.isInLeak()) { | |||
leak = Math.max(severity, leak); | |||
} | |||
} | |||
String severity(boolean inLeak) { | |||
return SeverityUtil.getSeverityFromOrdinal(inLeak ? leak : absolute); | |||
} | |||
} | |||
} |
@@ -0,0 +1,89 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.measure.live; | |||
import java.util.Collection; | |||
import java.util.Optional; | |||
import java.util.function.BiConsumer; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import static java.util.Collections.emptyList; | |||
class IssueMetricFormula { | |||
private final Metric metric; | |||
private final boolean onLeak; | |||
private final BiConsumer<Context, IssueCounter> formula; | |||
private final Collection<Metric> dependentMetrics; | |||
IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula) { | |||
this(metric, onLeak, formula, emptyList()); | |||
} | |||
IssueMetricFormula(Metric metric, boolean onLeak, BiConsumer<Context, IssueCounter> formula, Collection<Metric> dependentMetrics) { | |||
this.metric = metric; | |||
this.onLeak = onLeak; | |||
this.formula = formula; | |||
this.dependentMetrics = dependentMetrics; | |||
} | |||
Metric getMetric() { | |||
return metric; | |||
} | |||
boolean isOnLeak() { | |||
return onLeak; | |||
} | |||
Collection<Metric> getDependentMetrics() { | |||
return dependentMetrics; | |||
} | |||
void compute(Context context, IssueCounter issues) { | |||
formula.accept(context, issues); | |||
} | |||
interface Context { | |||
ComponentDto getComponent(); | |||
DebtRatingGrid getDebtRatingGrid(); | |||
/** | |||
* Value that was just refreshed, otherwise value as computed | |||
* during last analysis. | |||
* The metric must be declared in the formula dependencies | |||
* (see {@link IssueMetricFormula#getDependentMetrics()}). | |||
*/ | |||
Optional<Double> getValue(Metric metric); | |||
Optional<Double> getLeakValue(Metric metric); | |||
void setValue(double value); | |||
void setValue(Rating value); | |||
void setLeakValue(double value); | |||
void setLeakValue(Rating value); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.measure.live; | |||
import java.util.List; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.api.server.ServerSide; | |||
@ServerSide | |||
public interface IssueMetricFormulaFactory { | |||
List<IssueMetricFormula> getFormulas(); | |||
Set<Metric> getFormulaMetrics(); | |||
static Set<Metric> extractMetrics(List<IssueMetricFormula> formulas) { | |||
return formulas.stream() | |||
.flatMap(f -> Stream.concat(Stream.of(f.getMetric()), f.getDependentMetrics().stream())) | |||
.collect(Collectors.toSet()); | |||
} | |||
} |
@@ -0,0 +1,200 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.computation.task.projectanalysis.qualitymodel.Rating; | |||
import static java.util.Arrays.asList; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.RATING_BY_SEVERITY; | |||
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.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.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_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_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 0d; | |||
} | |||
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 0d; | |||
} | |||
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; | |||
} | |||
} |
@@ -0,0 +1,43 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.measure.live; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
/** | |||
* Refresh and persist the measures of some files, directories, modules | |||
* or projects. Measures include status of quality gate. | |||
* | |||
* Touching a file updates the related directory, module and project. | |||
* Status of Quality gate is refreshed but webhooks are not triggered. | |||
* | |||
* Branches are supported. | |||
*/ | |||
@ServerSide | |||
public interface LiveMeasureComputer { | |||
List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components); | |||
} |
@@ -0,0 +1,242 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 SonarSource SA | |||
* mailto:info AT sonarsource DOT com | |||
* | |||
* This program is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU Lesser General Public | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or (at your option) any later version. | |||
* | |||
* This program is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Lesser General Public License | |||
* along with this program; if not, write to the Free Software Foundation, | |||
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. | |||
*/ | |||
package org.sonar.server.measure.live; | |||
import java.util.ArrayList; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
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.ComponentDto; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.db.measure.LiveMeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.DebtRatingGrid; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import org.sonar.server.es.ProjectIndexer; | |||
import org.sonar.server.es.ProjectIndexers; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import org.sonar.server.qualitygate.QualityGate; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.settings.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.stream.Collectors.groupingBy; | |||
import static org.sonar.core.util.stream.MoreCollectors.toArrayList; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
public class LiveMeasureComputerImpl implements LiveMeasureComputer { | |||
private final DbClient dbClient; | |||
private final IssueMetricFormulaFactory formulaFactory; | |||
private final LiveQualityGateComputer qGateComputer; | |||
private final ProjectConfigurationLoader projectConfigurationLoader; | |||
private final ProjectIndexers projectIndexer; | |||
public LiveMeasureComputerImpl(DbClient dbClient, IssueMetricFormulaFactory formulaFactory, | |||
LiveQualityGateComputer qGateComputer, ProjectConfigurationLoader projectConfigurationLoader, ProjectIndexers projectIndexer) { | |||
this.dbClient = dbClient; | |||
this.formulaFactory = formulaFactory; | |||
this.qGateComputer = qGateComputer; | |||
this.projectConfigurationLoader = projectConfigurationLoader; | |||
this.projectIndexer = projectIndexer; | |||
} | |||
@Override | |||
public List<QGChangeEvent> refresh(DbSession dbSession, Collection<ComponentDto> components) { | |||
if (components.isEmpty()) { | |||
return emptyList(); | |||
} | |||
List<QGChangeEvent> result = new ArrayList<>(); | |||
Map<String, List<ComponentDto>> componentsByProjectUuid = components.stream().collect(groupingBy(ComponentDto::projectUuid)); | |||
for (List<ComponentDto> groupedComponents : componentsByProjectUuid.values()) { | |||
Optional<QGChangeEvent> qgChangeEvent = refreshComponentsOnSameProject(dbSession, groupedComponents); | |||
qgChangeEvent.ifPresent(result::add); | |||
} | |||
return result; | |||
} | |||
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 project = findProject(components); | |||
OrganizationDto organization = loadOrganization(dbSession, project); | |||
BranchDto branch = loadBranch(dbSession, project); | |||
Optional<SnapshotDto> lastAnalysis = dbClient.snapshotDao().selectLastAnalysisByRootComponentUuid(dbSession, project.uuid()); | |||
if (!lastAnalysis.isPresent()) { | |||
return Optional.empty(); | |||
} | |||
Optional<Long> beginningOfLeakPeriod = lastAnalysis.map(SnapshotDto::getPeriodDate); | |||
QualityGate qualityGate = qGateComputer.loadQualityGate(dbSession, organization, project, branch); | |||
Collection<String> metricKeys = getKeysOfAllInvolvedMetrics(qualityGate); | |||
Map<Integer, MetricDto> metricsPerId = dbClient.metricDao().selectByKeys(dbSession, metricKeys).stream() | |||
.collect(uniqueIndex(MetricDto::getId)); | |||
List<String> componentUuids = components.stream().map(ComponentDto::uuid).collect(toArrayList(components.size())); | |||
List<LiveMeasureDto> dbMeasures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, componentUuids, metricsPerId.keySet()); | |||
Configuration config = projectConfigurationLoader.loadProjectConfiguration(dbSession, project); | |||
DebtRatingGrid debtRatingGrid = new DebtRatingGrid(config); | |||
MeasureMatrix matrix = new MeasureMatrix(components, metricsPerId.values(), dbMeasures); | |||
components.forEach(c -> { | |||
IssueCounter issueCounter = new IssueCounter(dbClient.issueDao().selectIssueGroupsByBaseComponent(dbSession, c, beginningOfLeakPeriod.orElse(Long.MAX_VALUE))); | |||
FormulaContextImpl context = new FormulaContextImpl(matrix, debtRatingGrid); | |||
for (IssueMetricFormula formula : formulaFactory.getFormulas()) { | |||
// exclude leak formulas when leak period is not defined | |||
if (beginningOfLeakPeriod.isPresent() || !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); | |||
} | |||
} | |||
} | |||
}); | |||
EvaluatedQualityGate evaluatedQualityGate = qGateComputer.refreshGateStatus(project, qualityGate, matrix); | |||
// persist the measures that have been created or updated | |||
matrix.getChanged().forEach(m -> dbClient.liveMeasureDao().insertOrUpdate(dbSession, m, null)); | |||
projectIndexer.commitAndIndex(dbSession, singleton(project), ProjectIndexer.Cause.MEASURE_CHANGE); | |||
return Optional.of(new QGChangeEvent(project, branch, lastAnalysis.get(), config, () -> Optional.of(evaluatedQualityGate))); | |||
} | |||
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()); | |||
} | |||
// 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()) { | |||
metricKeys.add(metric.getKey()); | |||
} | |||
metricKeys.addAll(qGateComputer.getMetricsRelatedTo(gate)); | |||
return metricKeys; | |||
} | |||
private static ComponentDto findProject(Collection<ComponentDto> components) { | |||
return components.stream().filter(ComponentDto::isRootProject).findFirst() | |||
.orElseThrow(() -> new IllegalStateException("No project found in " + components)); | |||
} | |||
private BranchDto loadBranch(DbSession dbSession, ComponentDto project) { | |||
return dbClient.branchDao().selectByUuid(dbSession, project.uuid()) | |||
.orElseThrow(() -> new IllegalStateException("Branch not found: " + project.uuid())); | |||
} | |||
private OrganizationDto loadOrganization(DbSession dbSession, ComponentDto project) { | |||
String organizationUuid = project.getOrganizationUuid(); | |||
return dbClient.organizationDao().selectByUuid(dbSession, organizationUuid) | |||
.orElseThrow(() -> new IllegalStateException("No organization with UUID " + organizationUuid)); | |||
} | |||
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); | |||
} | |||
} | |||
} |
@@ -0,0 +1,32 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.core.platform.Module; | |||
public class LiveMeasureModule extends Module { | |||
@Override | |||
protected void configureModule() { | |||
add( | |||
IssueMetricFormulaFactoryImpl.class, | |||
LiveMeasureComputerImpl.class, | |||
LiveQualityGateComputerImpl.class); | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.Set; | |||
import org.sonar.api.server.ServerSide; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.BranchDto; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import org.sonar.server.qualitygate.QualityGate; | |||
@ServerSide | |||
public interface LiveQualityGateComputer { | |||
QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch); | |||
EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix); | |||
Set<String> getMetricsRelatedTo(QualityGate gate); | |||
} |
@@ -0,0 +1,151 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.HashSet; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.OptionalDouble; | |||
import java.util.Set; | |||
import org.sonar.api.measures.CoreMetrics; | |||
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.measure.LiveMeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.server.qualitygate.Condition; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import org.sonar.server.qualitygate.QualityGate; | |||
import org.sonar.server.qualitygate.QualityGateConverter; | |||
import org.sonar.server.qualitygate.QualityGateEvaluator; | |||
import org.sonar.server.qualitygate.QualityGateFinder; | |||
import org.sonar.server.qualitygate.ShortLivingBranchQualityGate; | |||
import static org.sonar.core.util.stream.MoreCollectors.toHashSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
public class LiveQualityGateComputerImpl implements LiveQualityGateComputer { | |||
private final DbClient dbClient; | |||
private final QualityGateFinder qGateFinder; | |||
private final QualityGateEvaluator evaluator; | |||
public LiveQualityGateComputerImpl(DbClient dbClient, QualityGateFinder qGateFinder, QualityGateEvaluator evaluator) { | |||
this.dbClient = dbClient; | |||
this.qGateFinder = qGateFinder; | |||
this.evaluator = evaluator; | |||
} | |||
@Override | |||
public QualityGate loadQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto project, BranchDto branch) { | |||
if (branch.getBranchType() == BranchType.SHORT) { | |||
return ShortLivingBranchQualityGate.GATE; | |||
} | |||
ComponentDto mainProject = project.getMainBranchProjectUuid() == null ? project : dbClient.componentDao().selectOrFailByKey(dbSession, project.getKey()); | |||
QualityGateDto gateDto = qGateFinder.getQualityGate(dbSession, organization, mainProject).getQualityGate(); | |||
Collection<QualityGateConditionDto> conditionDtos = dbClient.gateConditionDao().selectForQualityGate(dbSession, gateDto.getId()); | |||
Set<Integer> metricIds = conditionDtos.stream().map(c -> (int) c.getMetricId()) | |||
.collect(toHashSet(conditionDtos.size())); | |||
Map<Integer, MetricDto> metricsById = dbClient.metricDao().selectByIds(dbSession, metricIds).stream() | |||
.collect(uniqueIndex(MetricDto::getId)); | |||
Set<Condition> conditions = conditionDtos.stream().map(conditionDto -> { | |||
String metricKey = metricsById.get((int) conditionDto.getMetricId()).getKey(); | |||
Condition.Operator operator = Condition.Operator.fromDbValue(conditionDto.getOperator()); | |||
boolean onLeak = Objects.equals(conditionDto.getPeriod(), 1); | |||
return new Condition(metricKey, operator, conditionDto.getErrorThreshold(), conditionDto.getWarningThreshold(), onLeak); | |||
}).collect(toHashSet(conditionDtos.size())); | |||
return new QualityGate(String.valueOf(gateDto.getId()), gateDto.getName(), conditions); | |||
} | |||
@Override | |||
public EvaluatedQualityGate refreshGateStatus(ComponentDto project, QualityGate gate, MeasureMatrix measureMatrix) { | |||
QualityGateEvaluator.Measures measures = metricKey -> { | |||
Optional<LiveMeasureDto> liveMeasureDto = measureMatrix.getMeasure(project, metricKey); | |||
if (!liveMeasureDto.isPresent()) { | |||
return Optional.empty(); | |||
} | |||
MetricDto metric = measureMatrix.getMetric(liveMeasureDto.get().getMetricId()); | |||
return Optional.of(new LiveMeasure(liveMeasureDto.get(), metric)); | |||
}; | |||
EvaluatedQualityGate evaluatedGate = evaluator.evaluate(gate, measures); | |||
measureMatrix.setValue(project, CoreMetrics.ALERT_STATUS_KEY, evaluatedGate.getStatus().name()); | |||
measureMatrix.setValue(project, CoreMetrics.QUALITY_GATE_DETAILS_KEY, QualityGateConverter.toJson(evaluatedGate)); | |||
return evaluatedGate; | |||
} | |||
@Override | |||
public Set<String> getMetricsRelatedTo(QualityGate gate) { | |||
Set<String> metricKeys = new HashSet<>(); | |||
metricKeys.add(CoreMetrics.ALERT_STATUS_KEY); | |||
metricKeys.add(CoreMetrics.QUALITY_GATE_DETAILS_KEY); | |||
metricKeys.addAll(evaluator.getMetricKeys(gate)); | |||
return metricKeys; | |||
} | |||
private static class LiveMeasure implements QualityGateEvaluator.Measure { | |||
private final LiveMeasureDto dto; | |||
private final MetricDto metric; | |||
LiveMeasure(LiveMeasureDto dto, MetricDto metric) { | |||
this.dto = dto; | |||
this.metric = metric; | |||
} | |||
@Override | |||
public Metric.ValueType getType() { | |||
return Metric.ValueType.valueOf(metric.getValueType()); | |||
} | |||
@Override | |||
public OptionalDouble getValue() { | |||
if (dto.getValue() == null) { | |||
return OptionalDouble.empty(); | |||
} | |||
return OptionalDouble.of(dto.getValue()); | |||
} | |||
@Override | |||
public Optional<String> getStringValue() { | |||
return Optional.ofNullable(dto.getTextValue()); | |||
} | |||
@Override | |||
public OptionalDouble getLeakValue() { | |||
if (dto.getVariation() == null) { | |||
return OptionalDouble.empty(); | |||
} | |||
return OptionalDouble.of(dto.getVariation()); | |||
} | |||
} | |||
} |
@@ -0,0 +1,203 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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 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.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.Function; | |||
import java.util.stream.Stream; | |||
import javax.annotation.Nullable; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.measure.LiveMeasureDto; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
import static java.util.Objects.requireNonNull; | |||
/** | |||
* Keep the measures in memory during refresh of live measures: | |||
* <ul> | |||
* <li>the values of last analysis, restricted to the needed metrics</li> | |||
* <li>the refreshed values</li> | |||
* </ul> | |||
*/ | |||
class MeasureMatrix { | |||
// component uuid -> metric key -> measure | |||
private final Table<String, String, MeasureCell> table; | |||
private final Map<String, MetricDto> metricsByKeys = new HashMap<>(); | |||
private final Map<Integer, MetricDto> metricsByIds = new HashMap<>(); | |||
MeasureMatrix(Collection<ComponentDto> components, Collection<MetricDto> metrics, List<LiveMeasureDto> dbMeasures) { | |||
for (MetricDto metric : metrics) { | |||
this.metricsByKeys.put(metric.getKey(), metric); | |||
this.metricsByIds.put(metric.getId(), metric); | |||
} | |||
this.table = ArrayTable.create(Collections2.transform(components, ComponentDto::uuid), metricsByKeys.keySet()); | |||
for (LiveMeasureDto dbMeasure : dbMeasures) { | |||
table.put(dbMeasure.getComponentUuid(), metricsByIds.get(dbMeasure.getMetricId()).getKey(), new MeasureCell(dbMeasure, false)); | |||
} | |||
} | |||
MetricDto getMetric(int id) { | |||
return requireNonNull(metricsByIds.get(id), () -> String.format("Metric with id %d not found", id)); | |||
} | |||
private MetricDto getMetric(String key) { | |||
return requireNonNull(metricsByKeys.get(key), () -> String.format("Metric with key %s not found", key)); | |||
} | |||
Optional<LiveMeasureDto> getMeasure(ComponentDto component, String metricKey) { | |||
checkArgument(table.containsColumn(metricKey), "Metric with key %s is not registered", metricKey); | |||
MeasureCell cell = table.get(component.uuid(), metricKey); | |||
return cell == null ? Optional.empty() : Optional.of(cell.measure); | |||
} | |||
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; | |||
}); | |||
} | |||
void setValue(ComponentDto component, String metricKey, Rating value) { | |||
changeCell(component, metricKey, m -> { | |||
Double initialValue = m.getValue(); | |||
if (initialValue != null && Double.compare(initialValue, (double) 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; | |||
}); | |||
} | |||
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; | |||
}); | |||
} | |||
void setLeakValue(ComponentDto component, String metricKey, Rating variation) { | |||
setLeakValue(component, metricKey, (double) variation.getIndex()); | |||
} | |||
Stream<LiveMeasureDto> getChanged() { | |||
return table.values() | |||
.stream() | |||
.filter(Objects::nonNull) | |||
.filter(MeasureCell::isChanged) | |||
.map(MeasureCell::getMeasure); | |||
} | |||
private void changeCell(ComponentDto component, String metricKey, Function<LiveMeasureDto, Boolean> changer) { | |||
MeasureCell cell = table.get(component.uuid(), metricKey); | |||
if (cell == null) { | |||
LiveMeasureDto measure = new LiveMeasureDto() | |||
.setComponentUuid(component.uuid()) | |||
.setProjectUuid(component.projectUuid()) | |||
.setMetricId(metricsByKeys.get(metricKey).getId()); | |||
cell = new MeasureCell(measure, true); | |||
table.put(component.uuid(), metricKey, cell); | |||
changer.apply(cell.getMeasure()); | |||
} else if (changer.apply(cell.getMeasure())) { | |||
cell.setChanged(true); | |||
} | |||
} | |||
/** | |||
* Round a measure value by applying the scale defined on the metric. | |||
* Example: scale(0.1234) returns 0.12 if metric scale is 2 | |||
*/ | |||
private static double scale(MetricDto metric, double value) { | |||
if (metric.getDecimalScale() == null) { | |||
return value; | |||
} | |||
BigDecimal bd = BigDecimal.valueOf(value); | |||
return bd.setScale(metric.getDecimalScale(), RoundingMode.HALF_UP).doubleValue(); | |||
} | |||
private static class MeasureCell { | |||
private final LiveMeasureDto measure; | |||
private boolean changed; | |||
private MeasureCell(LiveMeasureDto measure, boolean changed) { | |||
this.measure = measure; | |||
this.changed = changed; | |||
} | |||
public LiveMeasureDto getMeasure() { | |||
return measure; | |||
} | |||
public boolean isChanged() { | |||
return changed; | |||
} | |||
public void setChanged(boolean b) { | |||
this.changed = b; | |||
} | |||
} | |||
} |
@@ -0,0 +1,23 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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. | |||
*/ | |||
@ParametersAreNonnullByDefault | |||
package org.sonar.server.measure.live; | |||
import javax.annotation.ParametersAreNonnullByDefault; |
@@ -216,7 +216,7 @@ public class ComponentAction implements MeasuresWsAction { | |||
private List<LiveMeasureDto> searchMeasures(DbSession dbSession, ComponentDto component, List<MetricDto> metrics) { | |||
List<Integer> metricIds = Lists.transform(metrics, MetricDto::getId); | |||
List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuids(dbSession, singletonList(component.uuid()), metricIds); | |||
List<LiveMeasureDto> measures = dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, singletonList(component.uuid()), metricIds); | |||
addBestValuesToMeasures(measures, component, metrics); | |||
return measures; | |||
} |
@@ -162,7 +162,7 @@ public class SearchAction implements MeasuresWsAction { | |||
} | |||
private List<LiveMeasureDto> searchMeasures() { | |||
return dbClient.liveMeasureDao().selectByComponentUuids(dbSession, | |||
return dbClient.liveMeasureDao().selectByComponentUuidsAndMetricIds(dbSession, | |||
projects.stream().map(ComponentDto::uuid).collect(MoreCollectors.toArrayList(projects.size())), | |||
metrics.stream().map(MetricDto::getId).collect(MoreCollectors.toArrayList(metrics.size()))); | |||
} |
@@ -45,7 +45,6 @@ import org.sonar.server.permission.index.PermissionIndexerDao.Dto; | |||
import static java.util.Collections.emptyList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toArrayList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.server.es.DefaultIndexSettings.REFRESH_IMMEDIATE; | |||
/** | |||
* Populates the types "authorization" of each index requiring project | |||
@@ -100,9 +99,10 @@ public class PermissionIndexer implements ProjectIndexer { | |||
@Override | |||
public Collection<EsQueueDto> prepareForRecovery(DbSession dbSession, Collection<String> projectUuids, ProjectIndexer.Cause cause) { | |||
switch (cause) { | |||
case MEASURE_CHANGE: | |||
case PROJECT_KEY_UPDATE: | |||
case PROJECT_TAGS_UPDATE: | |||
// nothing to change, project key and tags are not part of this index | |||
// nothing to change. Measures, project key and tags are not part of this index | |||
return emptyList(); | |||
case PROJECT_CREATION: |
@@ -69,6 +69,7 @@ import org.sonar.server.health.NodeHealthModule; | |||
import org.sonar.server.issue.AddTagsAction; | |||
import org.sonar.server.issue.AssignAction; | |||
import org.sonar.server.issue.CommentAction; | |||
import org.sonar.server.issue.IssueChangePostProcessorImpl; | |||
import org.sonar.server.issue.RemoveTagsAction; | |||
import org.sonar.server.issue.SetSeverityAction; | |||
import org.sonar.server.issue.SetTypeAction; | |||
@@ -88,6 +89,7 @@ import org.sonar.server.issue.ws.IssueWsModule; | |||
import org.sonar.server.language.ws.LanguageWs; | |||
import org.sonar.server.measure.custom.ws.CustomMeasuresWsModule; | |||
import org.sonar.server.measure.index.ProjectsEsModule; | |||
import org.sonar.server.measure.live.LiveMeasureModule; | |||
import org.sonar.server.measure.ws.MeasuresWsModule; | |||
import org.sonar.server.measure.ws.TimeMachineWs; | |||
import org.sonar.server.metric.CoreCustomMetrics; | |||
@@ -409,6 +411,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
ComponentIndexDefinition.class, | |||
ComponentIndex.class, | |||
ComponentIndexer.class, | |||
LiveMeasureModule.class, | |||
FavoriteModule.class, | |||
@@ -444,6 +447,7 @@ public class PlatformLevel4 extends PlatformLevel { | |||
TransitionAction.class, | |||
AddTagsAction.class, | |||
RemoveTagsAction.class, | |||
IssueChangePostProcessorImpl.class, | |||
// technical debt | |||
DebtModelPluginRepository.class, |
@@ -21,10 +21,13 @@ package org.sonar.server.qualitygate; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.stream.Stream; | |||
import javax.annotation.CheckForNull; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import static com.google.common.base.Strings.emptyToNull; | |||
import static java.util.Objects.requireNonNull; | |||
@Immutable | |||
@@ -44,8 +47,8 @@ public class Condition { | |||
this.metricKey = requireNonNull(metricKey, "metricKey can't be null"); | |||
this.operator = requireNonNull(operator, "operator can't be null"); | |||
this.onLeakPeriod = onLeakPeriod; | |||
this.errorThreshold = errorThreshold; | |||
this.warningThreshold = warningThreshold; | |||
this.errorThreshold = emptyToNull(errorThreshold); | |||
this.warningThreshold = emptyToNull(warningThreshold); | |||
} | |||
public String getMetricKey() { | |||
@@ -108,7 +111,10 @@ public class Condition { | |||
} | |||
public enum Operator { | |||
EQUALS("EQ"), NOT_EQUALS("NE"), GREATER_THAN("GT"), LESS_THAN("LT"); | |||
EQUALS(QualityGateConditionDto.OPERATOR_EQUALS), | |||
NOT_EQUALS(QualityGateConditionDto.OPERATOR_NOT_EQUALS), | |||
GREATER_THAN(QualityGateConditionDto.OPERATOR_GREATER_THAN), | |||
LESS_THAN(QualityGateConditionDto.OPERATOR_LESS_THAN); | |||
private final String dbValue; | |||
@@ -119,5 +125,12 @@ public class Condition { | |||
public String getDbValue() { | |||
return dbValue; | |||
} | |||
public static Operator fromDbValue(String s) { | |||
return Stream.of(values()) | |||
.filter(o -> o.getDbValue().equals(s)) | |||
.findFirst() | |||
.orElseThrow(() -> new IllegalArgumentException("Unsupported operator db value: " + s)); | |||
} | |||
} | |||
} |
@@ -0,0 +1,181 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate; | |||
import java.util.EnumSet; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.measures.Metric.ValueType; | |||
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; | |||
import static java.util.Optional.of; | |||
import static org.sonar.api.measures.Metric.ValueType.BOOL; | |||
import static org.sonar.api.measures.Metric.ValueType.FLOAT; | |||
import static org.sonar.api.measures.Metric.ValueType.INT; | |||
import static org.sonar.api.measures.Metric.ValueType.MILLISEC; | |||
import static org.sonar.api.measures.Metric.ValueType.PERCENT; | |||
import static org.sonar.api.measures.Metric.ValueType.RATING; | |||
import static org.sonar.api.measures.Metric.ValueType.WORK_DUR; | |||
class ConditionEvaluator { | |||
private static final Set<ValueType> NUMERICAL_TYPES = EnumSet.of(BOOL, INT, RATING, FLOAT, MILLISEC, PERCENT, WORK_DUR); | |||
private ConditionEvaluator() { | |||
// prevent instantiation | |||
} | |||
/** | |||
* Evaluates the condition for the specified measure | |||
*/ | |||
static EvaluatedCondition evaluate(Condition condition, QualityGateEvaluator.Measures measures) { | |||
Optional<QualityGateEvaluator.Measure> measure = measures.get(condition.getMetricKey()); | |||
if (!measure.isPresent()) { | |||
return new EvaluatedCondition(condition, EvaluationStatus.OK, null); | |||
} | |||
Optional<Comparable> value = getMeasureValue(condition, measure.get()); | |||
if (!value.isPresent()) { | |||
return new EvaluatedCondition(condition, EvaluationStatus.OK, null); | |||
} | |||
ValueType type = measure.get().getType(); | |||
return evaluateCondition(condition, type, value.get(), true) | |||
.orElseGet(() -> evaluateCondition(condition, type, value.get(), false) | |||
.orElseGet(() -> new EvaluatedCondition(condition, EvaluationStatus.OK, value.get().toString()))); | |||
} | |||
/** | |||
* Evaluates the error or warning condition. Returns empty if threshold or measure value is not defined. | |||
*/ | |||
private static Optional<EvaluatedCondition> evaluateCondition(Condition condition, ValueType type, Comparable value, boolean error) { | |||
Optional<Comparable> threshold = getThreshold(condition, type, error); | |||
if (!threshold.isPresent()) { | |||
return Optional.empty(); | |||
} | |||
if (reachThreshold(value, threshold.get(), condition)) { | |||
EvaluationStatus status = error ? EvaluationStatus.ERROR : EvaluationStatus.WARN; | |||
return of(new EvaluatedCondition(condition, status, value.toString())); | |||
} | |||
return Optional.empty(); | |||
} | |||
private static Optional<Comparable> getThreshold(Condition condition, ValueType valueType, boolean error) { | |||
Optional<String> valString = error ? condition.getErrorThreshold() : condition.getWarningThreshold(); | |||
return valString.map(s -> { | |||
try { | |||
switch (valueType) { | |||
case BOOL: | |||
return parseInteger(s) == 1; | |||
case INT: | |||
case RATING: | |||
return parseInteger(s); | |||
case MILLISEC: | |||
case WORK_DUR: | |||
return Long.parseLong(s); | |||
case FLOAT: | |||
case PERCENT: | |||
return Double.parseDouble(s); | |||
case STRING: | |||
case LEVEL: | |||
return s; | |||
default: | |||
throw new IllegalArgumentException(String.format("Unsupported value type %s. Cannot convert condition value", valueType)); | |||
} | |||
} catch (NumberFormatException badValueFormat) { | |||
throw new IllegalArgumentException(String.format( | |||
"Quality Gate: unable to parse threshold '%s' to compare against %s", s, condition.getMetricKey())); | |||
} | |||
}); | |||
} | |||
private static Optional<Comparable> getMeasureValue(Condition condition, QualityGateEvaluator.Measure measure) { | |||
if (condition.isOnLeakPeriod()) { | |||
return Optional.ofNullable(getLeakValue(measure)); | |||
} | |||
return Optional.ofNullable(getValue(measure)); | |||
} | |||
@CheckForNull | |||
private static Comparable getValue(QualityGateEvaluator.Measure measure) { | |||
if (NUMERICAL_TYPES.contains(measure.getType())) { | |||
return measure.getValue().isPresent() ? getNumericValue(measure.getType(), measure.getValue().getAsDouble()) : null; | |||
} | |||
switch (measure.getType()) { | |||
case LEVEL: | |||
case STRING: | |||
case DISTRIB: | |||
return measure.getStringValue().orElse(null); | |||
default: | |||
throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType()); | |||
} | |||
} | |||
@CheckForNull | |||
private static Comparable getLeakValue(QualityGateEvaluator.Measure measure) { | |||
if (NUMERICAL_TYPES.contains(measure.getType())) { | |||
return measure.getLeakValue().isPresent() ? getNumericValue(measure.getType(), measure.getLeakValue().getAsDouble()) : null; | |||
} | |||
throw new IllegalArgumentException("Condition on leak period is not allowed for type " + measure.getType()); | |||
} | |||
private static Comparable getNumericValue(ValueType type, double value) { | |||
switch (type) { | |||
case BOOL: | |||
return Double.compare(value, 1.0) == 1; | |||
case INT: | |||
case RATING: | |||
return (int) value; | |||
case FLOAT: | |||
case PERCENT: | |||
return value; | |||
case MILLISEC: | |||
case WORK_DUR: | |||
return (long) value; | |||
default: | |||
throw new IllegalArgumentException("Condition on numeric value is not allowed for type " + type); | |||
} | |||
} | |||
private static int parseInteger(String value) { | |||
return value.contains(".") ? Integer.parseInt(value.substring(0, value.indexOf('.'))) : Integer.parseInt(value); | |||
} | |||
private static boolean reachThreshold(Comparable measureValue, Comparable threshold, Condition condition) { | |||
int comparison = measureValue.compareTo(threshold); | |||
switch (condition.getOperator()) { | |||
case EQUALS: | |||
return comparison == 0; | |||
case NOT_EQUALS: | |||
return comparison != 0; | |||
case GREATER_THAN: | |||
return comparison > 0; | |||
case LESS_THAN: | |||
return comparison < 0; | |||
default: | |||
throw new IllegalArgumentException(String.format("Unsupported operator '%s'", condition.getOperator())); | |||
} | |||
} | |||
} |
@@ -30,6 +30,7 @@ import static java.util.Objects.requireNonNull; | |||
public class EvaluatedCondition { | |||
private final Condition condition; | |||
private final EvaluationStatus status; | |||
@Nullable | |||
private final String value; | |||
public EvaluatedCondition(Condition condition, EvaluationStatus status, @Nullable String value) { | |||
@@ -74,7 +75,7 @@ public class EvaluatedCondition { | |||
return "EvaluatedCondition{" + | |||
"condition=" + condition + | |||
", status=" + status + | |||
", value=" + (value == null ? null : '\'' + value + '\'') + | |||
", value=" + (value == null ? null : ('\'' + value + '\'')) + | |||
'}'; | |||
} | |||
@@ -27,6 +27,7 @@ import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import javax.annotation.Nullable; | |||
import javax.annotation.concurrent.Immutable; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
@@ -35,20 +36,22 @@ import static java.util.Objects.requireNonNull; | |||
@Immutable | |||
public class EvaluatedQualityGate { | |||
private final QualityGate qualityGate; | |||
private final Status status; | |||
private final Metric.Level status; | |||
private final Set<EvaluatedCondition> evaluatedConditions; | |||
private final boolean ignoredConditionsOnSmallChangeset; | |||
private EvaluatedQualityGate(QualityGate qualityGate, Status status, Set<EvaluatedCondition> evaluatedConditions) { | |||
this.qualityGate = qualityGate; | |||
this.status = status; | |||
private EvaluatedQualityGate(QualityGate qualityGate, Metric.Level status, Set<EvaluatedCondition> evaluatedConditions, boolean ignoredConditionsOnSmallChangeset) { | |||
this.qualityGate = requireNonNull(qualityGate, "qualityGate can't be null"); | |||
this.status = requireNonNull(status, "status can't be null"); | |||
this.evaluatedConditions = evaluatedConditions; | |||
this.ignoredConditionsOnSmallChangeset = ignoredConditionsOnSmallChangeset; | |||
} | |||
public QualityGate getQualityGate() { | |||
return qualityGate; | |||
} | |||
public Status getStatus() { | |||
public Metric.Level getStatus() { | |||
return status; | |||
} | |||
@@ -56,6 +59,10 @@ public class EvaluatedQualityGate { | |||
return evaluatedConditions; | |||
} | |||
public boolean hasIgnoredConditionsOnSmallChangeset() { | |||
return ignoredConditionsOnSmallChangeset; | |||
} | |||
public static Builder newBuilder() { | |||
return new Builder(); | |||
} | |||
@@ -90,20 +97,26 @@ public class EvaluatedQualityGate { | |||
public static final class Builder { | |||
private QualityGate qualityGate; | |||
private Status status; | |||
private Metric.Level status; | |||
private final Map<Condition, EvaluatedCondition> evaluatedConditions = new HashMap<>(); | |||
private boolean ignoredConditionsOnSmallChangeset = false; | |||
private Builder() { | |||
// use static factory method | |||
} | |||
public Builder setQualityGate(QualityGate qualityGate) { | |||
this.qualityGate = checkQualityGate(qualityGate); | |||
this.qualityGate = qualityGate; | |||
return this; | |||
} | |||
public Builder setStatus(Metric.Level status) { | |||
this.status = status; | |||
return this; | |||
} | |||
public Builder setStatus(Status status) { | |||
this.status = checkStatus(status); | |||
public Builder setIgnoredConditionsOnSmallChangeset(boolean b) { | |||
this.ignoredConditionsOnSmallChangeset = b; | |||
return this; | |||
} | |||
@@ -112,18 +125,21 @@ public class EvaluatedQualityGate { | |||
return this; | |||
} | |||
public Builder addCondition(EvaluatedCondition c) { | |||
evaluatedConditions.put(c.getCondition(), c); | |||
return this; | |||
} | |||
public Set<EvaluatedCondition> getEvaluatedConditions() { | |||
return ImmutableSet.copyOf(evaluatedConditions.values()); | |||
} | |||
public EvaluatedQualityGate build() { | |||
checkQualityGate(this.qualityGate); | |||
checkStatus(this.status); | |||
return new EvaluatedQualityGate( | |||
this.qualityGate, | |||
this.status, | |||
checkEvaluatedConditions(qualityGate, evaluatedConditions)); | |||
checkEvaluatedConditions(qualityGate, evaluatedConditions), | |||
ignoredConditionsOnSmallChangeset); | |||
} | |||
private static Set<EvaluatedCondition> checkEvaluatedConditions(QualityGate qualityGate, Map<Condition, EvaluatedCondition> evaluatedConditions) { | |||
@@ -141,19 +157,5 @@ public class EvaluatedQualityGate { | |||
return ImmutableSet.copyOf(evaluatedConditions.values()); | |||
} | |||
private static QualityGate checkQualityGate(@Nullable QualityGate qualityGate) { | |||
return requireNonNull(qualityGate, "qualityGate can't be null"); | |||
} | |||
private static Status checkStatus(@Nullable Status status) { | |||
return requireNonNull(status, "status can't be null"); | |||
} | |||
} | |||
public enum Status { | |||
OK, | |||
WARN, | |||
ERROR | |||
} | |||
} |
@@ -1,134 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate; | |||
import java.util.LinkedHashMap; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.elasticsearch.action.search.SearchResponse; | |||
import org.sonar.api.measures.CoreMetrics; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.utils.System2; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import org.sonar.server.es.Facets; | |||
import org.sonar.server.es.SearchOptions; | |||
import org.sonar.server.issue.IssueQuery; | |||
import org.sonar.server.issue.index.IssueIndex; | |||
import org.sonar.server.rule.index.RuleIndex; | |||
import static java.lang.String.format; | |||
import static java.lang.String.valueOf; | |||
import static java.util.Collections.singletonList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
public class LiveQualityGateFactoryImpl implements LiveQualityGateFactory { | |||
private final IssueIndex issueIndex; | |||
private final System2 system2; | |||
public LiveQualityGateFactoryImpl(IssueIndex issueIndex, System2 system2) { | |||
this.issueIndex = issueIndex; | |||
this.system2 = system2; | |||
} | |||
@Override | |||
public EvaluatedQualityGate buildForShortLivedBranch(ComponentDto componentDto) { | |||
return createQualityGate(componentDto, issueIndex); | |||
} | |||
private EvaluatedQualityGate createQualityGate(ComponentDto project, IssueIndex issueIndex) { | |||
SearchResponse searchResponse = issueIndex.search(IssueQuery.builder() | |||
.projectUuids(singletonList(project.getMainBranchProjectUuid())) | |||
.branchUuid(project.uuid()) | |||
.mainBranch(false) | |||
.resolved(false) | |||
.checkAuthorization(false) | |||
.build(), | |||
new SearchOptions().addFacets(RuleIndex.FACET_TYPES)); | |||
LinkedHashMap<String, Long> typeFacet = new Facets(searchResponse, system2.getDefaultTimeZone()) | |||
.get(RuleIndex.FACET_TYPES); | |||
EvaluatedQualityGate.Builder builder = EvaluatedQualityGate.newBuilder(); | |||
Set<Condition> conditions = ShortLivingBranchQualityGate.CONDITIONS.stream() | |||
.map(c -> { | |||
long measure = getMeasure(typeFacet, c); | |||
EvaluatedCondition.EvaluationStatus status = measure > 0 ? EvaluatedCondition.EvaluationStatus.ERROR : EvaluatedCondition.EvaluationStatus.OK; | |||
Condition condition = new Condition(c.getMetricKey(), toOperator(c), c.getErrorThreshold(), c.getWarnThreshold(), c.isOnLeak()); | |||
builder.addCondition(condition, status, valueOf(measure)); | |||
return condition; | |||
}) | |||
.collect(toSet(ShortLivingBranchQualityGate.CONDITIONS.size())); | |||
builder | |||
.setQualityGate( | |||
new org.sonar.server.qualitygate.QualityGate( | |||
valueOf(ShortLivingBranchQualityGate.ID), | |||
ShortLivingBranchQualityGate.NAME, | |||
conditions)) | |||
.setStatus(qgStatusFrom(builder.getEvaluatedConditions())); | |||
return builder.build(); | |||
} | |||
private static Condition.Operator toOperator(ShortLivingBranchQualityGate.Condition c) { | |||
String operator = c.getOperator(); | |||
switch (operator) { | |||
case QualityGateConditionDto.OPERATOR_GREATER_THAN: | |||
return Condition.Operator.GREATER_THAN; | |||
case QualityGateConditionDto.OPERATOR_LESS_THAN: | |||
return Condition.Operator.LESS_THAN; | |||
case QualityGateConditionDto.OPERATOR_EQUALS: | |||
return Condition.Operator.EQUALS; | |||
case QualityGateConditionDto.OPERATOR_NOT_EQUALS: | |||
return Condition.Operator.NOT_EQUALS; | |||
default: | |||
throw new IllegalArgumentException(format("Unsupported Condition operator '%s'", operator)); | |||
} | |||
} | |||
private static EvaluatedQualityGate.Status qgStatusFrom(Set<EvaluatedCondition> conditions) { | |||
if (conditions.stream().anyMatch(c -> c.getStatus() == EvaluatedCondition.EvaluationStatus.ERROR)) { | |||
return EvaluatedQualityGate.Status.ERROR; | |||
} | |||
return EvaluatedQualityGate.Status.OK; | |||
} | |||
private static long getMeasure(LinkedHashMap<String, Long> typeFacet, ShortLivingBranchQualityGate.Condition c) { | |||
String metricKey = c.getMetricKey(); | |||
switch (metricKey) { | |||
case CoreMetrics.BUGS_KEY: | |||
return getValueForRuleType(typeFacet, RuleType.BUG); | |||
case CoreMetrics.VULNERABILITIES_KEY: | |||
return getValueForRuleType(typeFacet, RuleType.VULNERABILITY); | |||
case CoreMetrics.CODE_SMELLS_KEY: | |||
return getValueForRuleType(typeFacet, RuleType.CODE_SMELL); | |||
default: | |||
throw new IllegalArgumentException(format("Unsupported metric key '%s' in hardcoded quality gate", metricKey)); | |||
} | |||
} | |||
private static long getValueForRuleType(Map<String, Long> facet, RuleType ruleType) { | |||
Long res = facet.get(ruleType.name()); | |||
if (res == null) { | |||
return 0L; | |||
} | |||
return res; | |||
} | |||
} |
@@ -34,7 +34,7 @@ import org.sonar.db.DbSession; | |||
import org.sonar.db.metric.MetricDto; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import org.sonar.server.exceptions.NotFoundException; | |||
import static com.google.common.base.Strings.isNullOrEmpty; | |||
@@ -44,7 +44,7 @@ import static java.util.Arrays.stream; | |||
import static org.sonar.api.measures.Metric.ValueType.RATING; | |||
import static org.sonar.api.measures.Metric.ValueType.valueOf; | |||
import static org.sonar.db.qualitygate.QualityGateConditionDto.isOperatorAllowed; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E; | |||
import static org.sonar.server.qualitygate.ValidRatingMetrics.isCoreRatingMetric; | |||
import static org.sonar.server.ws.WsUtils.checkRequest; | |||
@@ -0,0 +1,61 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate; | |||
import com.google.gson.JsonArray; | |||
import com.google.gson.JsonObject; | |||
public class QualityGateConverter { | |||
private static final String FIELD_LEVEL = "level"; | |||
private static final String FIELD_IGNORED_CONDITIONS = "ignoredConditions"; | |||
private QualityGateConverter() { | |||
// prevent instantiation | |||
} | |||
public static String toJson(EvaluatedQualityGate gate) { | |||
JsonObject details = new JsonObject(); | |||
details.addProperty(FIELD_LEVEL, gate.getStatus().name()); | |||
JsonArray conditionResults = new JsonArray(); | |||
for (EvaluatedCondition condition : gate.getEvaluatedConditions()) { | |||
conditionResults.add(toJson(condition)); | |||
} | |||
details.add("conditions", conditionResults); | |||
details.addProperty(FIELD_IGNORED_CONDITIONS, gate.hasIgnoredConditionsOnSmallChangeset()); | |||
return details.toString(); | |||
} | |||
private static JsonObject toJson(EvaluatedCondition evaluatedCondition) { | |||
Condition condition = evaluatedCondition.getCondition(); | |||
JsonObject result = new JsonObject(); | |||
result.addProperty("metric", condition.getMetricKey()); | |||
result.addProperty("op", condition.getOperator().getDbValue()); | |||
if (condition.isOnLeakPeriod()) { | |||
result.addProperty("period", 1); | |||
} | |||
condition.getWarningThreshold().ifPresent(t -> result.addProperty("warning", t)); | |||
condition.getErrorThreshold().ifPresent(t -> result.addProperty("error", t)); | |||
evaluatedCondition.getValue().ifPresent(v -> result.addProperty("actual", v)); | |||
result.addProperty(FIELD_LEVEL, evaluatedCondition.getStatus().name()); | |||
return result; | |||
} | |||
} |
@@ -0,0 +1,60 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate; | |||
import java.util.Optional; | |||
import java.util.OptionalDouble; | |||
import java.util.Set; | |||
import org.sonar.api.ce.ComputeEngineSide; | |||
import org.sonar.api.measures.Metric; | |||
import org.sonar.api.server.ServerSide; | |||
@ComputeEngineSide | |||
@ServerSide | |||
public interface QualityGateEvaluator { | |||
/** | |||
* @param measures must provide the measures related to the metrics | |||
* defined by {@link #getMetricKeys(QualityGate)} | |||
*/ | |||
EvaluatedQualityGate evaluate(QualityGate gate, Measures measures); | |||
/** | |||
* Keys of the metrics involved in the computation of gate status. | |||
* It may include metrics that are not part of conditions, | |||
* for instance "new_lines" for the circuit-breaker on | |||
* small changesets. | |||
*/ | |||
Set<String> getMetricKeys(QualityGate gate); | |||
interface Measures { | |||
Optional<Measure> get(String metricKey); | |||
} | |||
interface Measure { | |||
Metric.ValueType getType(); | |||
OptionalDouble getValue(); | |||
Optional<String> getStringValue(); | |||
OptionalDouble getLeakValue(); | |||
} | |||
} |
@@ -0,0 +1,133 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Multimap; | |||
import java.util.Collection; | |||
import java.util.HashSet; | |||
import java.util.Map; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.function.Function; | |||
import org.sonar.api.measures.CoreMetrics; | |||
import org.sonar.api.measures.Metric.Level; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
import org.sonar.server.qualitygate.EvaluatedCondition.EvaluationStatus; | |||
import static java.util.Objects.requireNonNull; | |||
import static org.sonar.core.util.stream.MoreCollectors.toEnumSet; | |||
public class QualityGateEvaluatorImpl implements QualityGateEvaluator { | |||
private static final int MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS = 20; | |||
/** | |||
* Some metrics will be ignored on very small change sets. | |||
*/ | |||
private static final Set<String> METRICS_TO_IGNORE_ON_SMALL_CHANGESETS = ImmutableSet.of( | |||
CoreMetrics.NEW_COVERAGE_KEY, | |||
CoreMetrics.NEW_LINE_COVERAGE_KEY, | |||
CoreMetrics.NEW_BRANCH_COVERAGE_KEY, | |||
CoreMetrics.NEW_DUPLICATED_LINES_DENSITY_KEY, | |||
CoreMetrics.NEW_DUPLICATED_LINES_KEY, | |||
CoreMetrics.NEW_BLOCKS_DUPLICATED_KEY); | |||
@Override | |||
public EvaluatedQualityGate evaluate(QualityGate gate, Measures measures) { | |||
EvaluatedQualityGate.Builder result = EvaluatedQualityGate.newBuilder() | |||
.setQualityGate(gate); | |||
boolean isSmallChangeset = isSmallChangeset(measures); | |||
Multimap<String, Condition> conditionsPerMetric = gate.getConditions().stream() | |||
.collect(MoreCollectors.index(Condition::getMetricKey, Function.identity())); | |||
for (Map.Entry<String, Collection<Condition>> entry : conditionsPerMetric.asMap().entrySet()) { | |||
String metricKey = entry.getKey(); | |||
Collection<Condition> conditionsOnSameMetric = entry.getValue(); | |||
EvaluatedCondition evaluation = evaluateConditionsOnMetric(conditionsOnSameMetric, measures); | |||
if (isSmallChangeset && evaluation.getStatus() != EvaluationStatus.OK && METRICS_TO_IGNORE_ON_SMALL_CHANGESETS.contains(metricKey)) { | |||
result.addCondition(new EvaluatedCondition(evaluation.getCondition(), EvaluationStatus.OK, evaluation.getValue().orElse(null))); | |||
result.setIgnoredConditionsOnSmallChangeset(true); | |||
} else { | |||
result.addCondition(evaluation); | |||
} | |||
} | |||
result.setStatus(overallStatusOf(result.getEvaluatedConditions())); | |||
return result.build(); | |||
} | |||
@Override | |||
public Set<String> getMetricKeys(QualityGate gate) { | |||
Set<String> metricKeys = new HashSet<>(); | |||
metricKeys.add(CoreMetrics.NEW_LINES_KEY); | |||
for (Condition condition : gate.getConditions()) { | |||
metricKeys.add(condition.getMetricKey()); | |||
} | |||
return metricKeys; | |||
} | |||
private static boolean isSmallChangeset(Measures measures) { | |||
Optional<Measure> newLines = measures.get(CoreMetrics.NEW_LINES_KEY); | |||
return newLines.isPresent() && | |||
newLines.get().getLeakValue().isPresent() && | |||
newLines.get().getLeakValue().getAsDouble() < MAXIMUM_NEW_LINES_FOR_SMALL_CHANGESETS; | |||
} | |||
private static EvaluatedCondition evaluateConditionsOnMetric(Collection<Condition> conditionsOnSameMetric, Measures measures) { | |||
EvaluatedCondition leakEvaluation = null; | |||
EvaluatedCondition absoluteEvaluation = null; | |||
for (Condition condition : conditionsOnSameMetric) { | |||
if (condition.isOnLeakPeriod()) { | |||
leakEvaluation = ConditionEvaluator.evaluate(condition, measures); | |||
} else { | |||
absoluteEvaluation = ConditionEvaluator.evaluate(condition, measures); | |||
} | |||
} | |||
if (leakEvaluation == null) { | |||
return requireNonNull(absoluteEvaluation, "Evaluation of absolute value can't be null on conditions " + conditionsOnSameMetric); | |||
} | |||
if (absoluteEvaluation == null) { | |||
return requireNonNull(leakEvaluation, "Evaluation of leak value can't be null on conditions " + conditionsOnSameMetric); | |||
} | |||
// both conditions are present. Take the worse one. In case of equality, take | |||
// the one on the leak period | |||
if (absoluteEvaluation.getStatus().compareTo(leakEvaluation.getStatus()) > 0) { | |||
return absoluteEvaluation; | |||
} | |||
return leakEvaluation; | |||
} | |||
private static Level overallStatusOf(Set<EvaluatedCondition> conditions) { | |||
Set<EvaluationStatus> statuses = conditions.stream().map(EvaluatedCondition::getStatus).collect(toEnumSet(EvaluationStatus.class)); | |||
if (statuses.contains(EvaluationStatus.ERROR)) { | |||
return Level.ERROR; | |||
} | |||
if (statuses.contains(EvaluationStatus.WARN)) { | |||
return Level.WARN; | |||
} | |||
return Level.OK; | |||
} | |||
} |
@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate; | |||
import java.util.Optional; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.organization.OrganizationDto; | |||
import org.sonar.db.qualitygate.QGateWithOrgDto; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
@@ -44,16 +45,15 @@ public class QualityGateFinder { | |||
* | |||
* It will first try to get the quality gate explicitly defined on a project, if none it will try to return default quality gate ofI the organization | |||
*/ | |||
public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, long componentId) { | |||
Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, componentId); | |||
public QualityGateData getQualityGate(DbSession dbSession, OrganizationDto organization, ComponentDto component) { | |||
Optional<Long> qualityGateId = dbClient.projectQgateAssociationDao().selectQGateIdByComponentId(dbSession, component.getId()); | |||
if (qualityGateId.isPresent()) { | |||
QualityGateDto qualityGate = checkFound(dbClient.qualityGateDao().selectById(dbSession, qualityGateId.get()), "No quality gate has been found for id %s", qualityGateId); | |||
return new QualityGateData(qualityGate, false); | |||
} else { | |||
QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid()); | |||
checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid()); | |||
return new QualityGateData(defaultQualityGate, true); | |||
} | |||
QualityGateDto defaultQualityGate = dbClient.qualityGateDao().selectByOrganizationAndUuid(dbSession, organization, organization.getDefaultQualityGateUuid()); | |||
checkState(defaultQualityGate != null, "Unable to find the quality gate [%s] for organization [%s]", organization.getDefaultQualityGateUuid(), organization.getUuid()); | |||
return new QualityGateData(defaultQualityGate, true); | |||
} | |||
public QGateWithOrgDto getByOrganizationAndId(DbSession dbSession, OrganizationDto organization, long qualityGateId) { |
@@ -45,6 +45,7 @@ public class QualityGateModule extends Module { | |||
QualityGateUpdater.class, | |||
QualityGateConditionsUpdater.class, | |||
QualityGateFinder.class, | |||
QualityGateEvaluatorImpl.class, | |||
// WS | |||
QualityGatesWsSupport.class, | |||
QualityGatesWs.class, |
@@ -39,7 +39,7 @@ import org.sonar.db.qualitygate.QualityGateConditionDao; | |||
import org.sonar.db.qualitygate.QualityGateConditionDto; | |||
import org.sonar.db.qualitygate.QualityGateDao; | |||
import org.sonar.db.qualitygate.QualityGateDto; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import static java.util.Arrays.asList; | |||
import static java.util.stream.Collectors.toMap; | |||
@@ -57,7 +57,7 @@ public class RegisterQualityGates implements Startable { | |||
private static final String BUILTIN_QUALITY_GATE_NAME = "Sonar way"; | |||
private static final int LEAK_PERIOD = 1; | |||
private static final String A_RATING = Integer.toString(RatingGrid.Rating.A.getIndex()); | |||
private static final String A_RATING = Integer.toString(Rating.A.getIndex()); | |||
private static final List<QualityGateCondition> QUALITY_GATE_CONDITIONS = asList( | |||
new QualityGateCondition().setMetricKey(NEW_SECURITY_RATING_KEY).setOperator(OPERATOR_GREATER_THAN).setPeriod(LEAK_PERIOD).setErrorThreshold(A_RATING), |
@@ -20,6 +20,7 @@ | |||
package org.sonar.server.qualitygate; | |||
import com.google.common.collect.ImmutableList; | |||
import com.google.common.collect.ImmutableSet; | |||
import java.util.List; | |||
import javax.annotation.CheckForNull; | |||
import org.sonar.api.measures.CoreMetrics; | |||
@@ -37,6 +38,11 @@ public final class ShortLivingBranchQualityGate { | |||
new Condition(CoreMetrics.VULNERABILITIES_KEY, OPERATOR_GREATER_THAN, "0", false), | |||
new Condition(CoreMetrics.CODE_SMELLS_KEY, OPERATOR_GREATER_THAN, "0", false)); | |||
public static final QualityGate GATE = new QualityGate(String.valueOf(ID), NAME, ImmutableSet.of( | |||
new org.sonar.server.qualitygate.Condition(CoreMetrics.BUGS_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false), | |||
new org.sonar.server.qualitygate.Condition(CoreMetrics.VULNERABILITIES_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false), | |||
new org.sonar.server.qualitygate.Condition(CoreMetrics.CODE_SMELLS_KEY, org.sonar.server.qualitygate.Condition.Operator.GREATER_THAN, "0", null, false))); | |||
private ShortLivingBranchQualityGate() { | |||
// prevents instantiation | |||
} |
@@ -1,138 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate.changeevent; | |||
import com.google.common.collect.ImmutableList; | |||
import java.util.List; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueChangeContext; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.server.issue.ws.SearchResponseData; | |||
import static com.google.common.base.Preconditions.checkArgument; | |||
public interface QGChangeEventFactory { | |||
/** | |||
* Will call webhooks once for any short living branch which has at least one issue in {@link SearchResponseData} and | |||
* if change described in {@link IssueChange} can alter the status of the short living branch. | |||
*/ | |||
List<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context); | |||
final class IssueChange { | |||
private final RuleType ruleType; | |||
private final String transitionKey; | |||
public IssueChange(RuleType ruleType) { | |||
this(ruleType, null); | |||
} | |||
public IssueChange(String transitionKey) { | |||
this(null, transitionKey); | |||
} | |||
public IssueChange(@Nullable RuleType ruleType, @Nullable String transitionKey) { | |||
checkArgument(ruleType != null || transitionKey != null, "At least one of ruleType and transitionKey must be non null"); | |||
this.ruleType = ruleType; | |||
this.transitionKey = transitionKey; | |||
} | |||
public Optional<RuleType> getRuleType() { | |||
return Optional.ofNullable(ruleType); | |||
} | |||
public Optional<String> getTransitionKey() { | |||
return Optional.ofNullable(transitionKey); | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
IssueChange that = (IssueChange) o; | |||
return ruleType == that.ruleType && | |||
Objects.equals(transitionKey, that.transitionKey); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(ruleType, transitionKey); | |||
} | |||
@Override | |||
public String toString() { | |||
return "IssueChange{" + | |||
"ruleType=" + ruleType + | |||
", transitionKey='" + transitionKey + '\'' + | |||
'}'; | |||
} | |||
} | |||
final class IssueChangeData { | |||
private final List<DefaultIssue> issues; | |||
private final List<ComponentDto> components; | |||
public IssueChangeData(List<DefaultIssue> issues, List<ComponentDto> components) { | |||
this.issues = ImmutableList.copyOf(issues); | |||
this.components = ImmutableList.copyOf(components); | |||
} | |||
public List<DefaultIssue> getIssues() { | |||
return issues; | |||
} | |||
public List<ComponentDto> getComponents() { | |||
return components; | |||
} | |||
@Override | |||
public boolean equals(Object o) { | |||
if (this == o) { | |||
return true; | |||
} | |||
if (o == null || getClass() != o.getClass()) { | |||
return false; | |||
} | |||
IssueChangeData that = (IssueChangeData) o; | |||
return Objects.equals(issues, that.issues) && | |||
Objects.equals(components, that.components); | |||
} | |||
@Override | |||
public int hashCode() { | |||
return Objects.hash(issues, components); | |||
} | |||
@Override | |||
public String toString() { | |||
return "IssueChangeData{" + | |||
"issues=" + issues + | |||
", components=" + components + | |||
'}'; | |||
} | |||
} | |||
} |
@@ -1,156 +0,0 @@ | |||
/* | |||
* SonarQube | |||
* Copyright (C) 2009-2018 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.qualitygate.changeevent; | |||
import com.google.common.collect.ImmutableSet; | |||
import com.google.common.collect.Sets; | |||
import java.util.List; | |||
import java.util.Map; | |||
import java.util.Objects; | |||
import java.util.Optional; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import java.util.stream.Stream; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.api.issue.DefaultTransitions; | |||
import org.sonar.core.issue.DefaultIssue; | |||
import org.sonar.core.issue.IssueChangeContext; | |||
import org.sonar.core.util.stream.MoreCollectors; | |||
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.server.qualitygate.LiveQualityGateFactory; | |||
import org.sonar.server.settings.ProjectConfigurationLoader; | |||
import static java.util.Collections.emptyList; | |||
import static org.sonar.core.util.stream.MoreCollectors.toSet; | |||
import static org.sonar.core.util.stream.MoreCollectors.uniqueIndex; | |||
public class QGChangeEventFactoryImpl implements QGChangeEventFactory { | |||
private static final Set<String> MEANINGFUL_TRANSITIONS = ImmutableSet.of( | |||
DefaultTransitions.RESOLVE, DefaultTransitions.FALSE_POSITIVE, DefaultTransitions.WONT_FIX, DefaultTransitions.REOPEN); | |||
private final DbClient dbClient; | |||
private final ProjectConfigurationLoader projectConfigurationLoader; | |||
private final LiveQualityGateFactory liveQualityGateFactory; | |||
public QGChangeEventFactoryImpl(DbClient dbClient, ProjectConfigurationLoader projectConfigurationLoader, | |||
LiveQualityGateFactory liveQualityGateFactory) { | |||
this.dbClient = dbClient; | |||
this.projectConfigurationLoader = projectConfigurationLoader; | |||
this.liveQualityGateFactory = liveQualityGateFactory; | |||
} | |||
@Override | |||
public List<QGChangeEvent> from(IssueChangeData issueChangeData, IssueChange issueChange, IssueChangeContext context) { | |||
if (isEmpty(issueChangeData) || !isUserChangeContext(context) || !isRelevant(issueChange)) { | |||
return emptyList(); | |||
} | |||
return from(issueChangeData); | |||
} | |||
private static boolean isRelevant(IssueChange issueChange) { | |||
return issueChange.getTransitionKey().map(QGChangeEventFactoryImpl::isMeaningfulTransition).orElse(true); | |||
} | |||
private static boolean isEmpty(IssueChangeData issueChangeData) { | |||
return issueChangeData.getIssues().isEmpty(); | |||
} | |||
private static boolean isUserChangeContext(IssueChangeContext context) { | |||
return context.login() != null; | |||
} | |||
private static boolean isMeaningfulTransition(String transitionKey) { | |||
return MEANINGFUL_TRANSITIONS.contains(transitionKey); | |||
} | |||
private List<QGChangeEvent> from(IssueChangeData issueChangeData) { | |||
try (DbSession dbSession = dbClient.openSession(false)) { | |||
Map<String, ComponentDto> branchesByUuid = getBranchComponents(dbSession, issueChangeData); | |||
if (branchesByUuid.isEmpty()) { | |||
return emptyList(); | |||
} | |||
Set<String> branchProjectUuids = branchesByUuid.values().stream() | |||
.map(ComponentDto::uuid) | |||
.collect(toSet(branchesByUuid.size())); | |||
Set<BranchDto> shortBranches = dbClient.branchDao().selectByUuids(dbSession, branchProjectUuids) | |||
.stream() | |||
.filter(branchDto -> branchDto.getBranchType() == BranchType.SHORT) | |||
.collect(toSet(branchesByUuid.size())); | |||
if (shortBranches.isEmpty()) { | |||
return emptyList(); | |||
} | |||
Map<String, Configuration> configurationByUuid = projectConfigurationLoader.loadProjectConfigurations(dbSession, | |||
shortBranches.stream().map(shortBranch -> branchesByUuid.get(shortBranch.getUuid())).collect(Collectors.toSet())); | |||
Set<String> shortBranchesComponentUuids = shortBranches.stream().map(BranchDto::getUuid).collect(toSet(shortBranches.size())); | |||
Map<String, SnapshotDto> analysisByProjectUuid = dbClient.snapshotDao().selectLastAnalysesByRootComponentUuids( | |||
dbSession, | |||
shortBranchesComponentUuids) | |||
.stream() | |||
.collect(uniqueIndex(SnapshotDto::getComponentUuid)); | |||
return shortBranches | |||
.stream() | |||
.map(shortBranch -> { | |||
ComponentDto branch = branchesByUuid.get(shortBranch.getUuid()); | |||
SnapshotDto analysis = analysisByProjectUuid.get(shortBranch.getUuid()); | |||
if (branch != null && analysis != null) { | |||
Configuration configuration = configurationByUuid.get(shortBranch.getUuid()); | |||
return new QGChangeEvent(branch, shortBranch, analysis, configuration, | |||
() -> Optional.of(liveQualityGateFactory.buildForShortLivedBranch(branch))); | |||
} | |||
return null; | |||
}) | |||
.filter(Objects::nonNull) | |||
.collect(MoreCollectors.toList(shortBranches.size())); | |||
} | |||
} | |||
private Map<String, ComponentDto> getBranchComponents(DbSession dbSession, IssueChangeData issueChangeData) { | |||
Set<String> projectUuids = issueChangeData.getIssues().stream() | |||
.map(DefaultIssue::projectUuid) | |||
.collect(toSet()); | |||
Set<String> missingProjectUuids = ImmutableSet.copyOf(Sets.difference( | |||
projectUuids, | |||
issueChangeData.getComponents() | |||
.stream() | |||
.map(ComponentDto::uuid) | |||
.collect(Collectors.toSet()))); | |||
if (missingProjectUuids.isEmpty()) { | |||
return issueChangeData.getComponents() | |||
.stream() | |||
.filter(c -> projectUuids.contains(c.uuid())) | |||
.filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) | |||
.collect(uniqueIndex(ComponentDto::uuid)); | |||
} | |||
return Stream.concat( | |||
issueChangeData.getComponents().stream().filter(c -> projectUuids.contains(c.uuid())), | |||
dbClient.componentDao().selectByUuids(dbSession, missingProjectUuids).stream()) | |||
.filter(componentDto -> componentDto.getMainBranchProjectUuid() != null) | |||
.collect(uniqueIndex(ComponentDto::uuid)); | |||
} | |||
} |
@@ -20,8 +20,10 @@ | |||
package org.sonar.server.qualitygate.changeevent; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import org.sonar.core.issue.DefaultIssue; | |||
public interface QGChangeEventListeners { | |||
void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> qgChangeEvents); | |||
void broadcastOnIssueChange(List<DefaultIssue> changedIssues, Collection<QGChangeEvent> qgChangeEvents); | |||
} |
@@ -22,6 +22,7 @@ package org.sonar.server.qualitygate.changeevent; | |||
import com.google.common.collect.Multimap; | |||
import java.util.Arrays; | |||
import java.util.Collection; | |||
import java.util.List; | |||
import java.util.Set; | |||
import org.sonar.api.rules.RuleType; | |||
import org.sonar.api.utils.log.Logger; | |||
@@ -57,15 +58,15 @@ public class QGChangeEventListenersImpl implements QGChangeEventListeners { | |||
} | |||
@Override | |||
public void broadcastOnIssueChange(QGChangeEventFactory.IssueChangeData issueChangeData, Collection<QGChangeEvent> changeEvents) { | |||
if (listeners.length == 0 || issueChangeData.getComponents().isEmpty() || issueChangeData.getIssues().isEmpty() || changeEvents.isEmpty()) { | |||
public void broadcastOnIssueChange(List<DefaultIssue> issues, Collection<QGChangeEvent> changeEvents) { | |||
if (listeners.length == 0 || issues.isEmpty() || changeEvents.isEmpty()) { | |||
return; | |||
} | |||
try { | |||
Multimap<String, QGChangeEvent> eventsByComponentUuid = changeEvents.stream() | |||
.collect(MoreCollectors.index(t -> t.getProject().uuid())); | |||
Multimap<String, DefaultIssue> issueByComponentUuid = issueChangeData.getIssues().stream() | |||
Multimap<String, DefaultIssue> issueByComponentUuid = issues.stream() | |||
.collect(MoreCollectors.index(DefaultIssue::projectUuid)); | |||
issueByComponentUuid.asMap() |
@@ -99,7 +99,7 @@ public class GetByProjectAction implements QualityGatesWsAction { | |||
throw insufficientPrivilegesException(); | |||
} | |||
QualityGateData data = qualityGateFinder.getQualityGate(dbSession, organization, project.getId()); | |||
QualityGateData data = qualityGateFinder.getQualityGate(dbSession, organization, project); | |||
writeProtobuf(buildResponse(data), request, response); | |||
} |
@@ -19,12 +19,16 @@ | |||
*/ | |||
package org.sonar.server.settings; | |||
import java.util.Collections; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import org.sonar.api.config.Configuration; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.ComponentDto; | |||
import static java.lang.String.format; | |||
import static java.util.Objects.requireNonNull; | |||
public interface ProjectConfigurationLoader { | |||
/** | |||
* Loads configuration for the specified components. | |||
@@ -37,4 +41,9 @@ public interface ProjectConfigurationLoader { | |||
* Any component is accepted but SQ only supports specific properties for projects and branches. | |||
*/ | |||
Map<String, Configuration> loadProjectConfigurations(DbSession dbSession, Set<ComponentDto> projects); | |||
default Configuration loadProjectConfiguration(DbSession dbSession, ComponentDto project) { | |||
Map<String, Configuration> configurations = loadProjectConfigurations(dbSession, Collections.singleton(project)); | |||
return requireNonNull(configurations.get(project.uuid()), () -> format("Configuration for project '%s' is not found", project.getKey())); | |||
} | |||
} |
@@ -93,12 +93,11 @@ public class TestIndexer implements ProjectIndexer { | |||
switch (cause) { | |||
case PROJECT_CREATION: | |||
// no tests at that time | |||
return emptyList(); | |||
case MEASURE_CHANGE: | |||
case PROJECT_KEY_UPDATE: | |||
case PROJECT_TAGS_UPDATE: | |||
case PERMISSION_CHANGE: | |||
// project key, tags and permissions are not part of tests/test | |||
// Measures, project key, tags and permissions are not part of tests/test | |||
return emptyList(); | |||
case PROJECT_DELETION: |
@@ -207,7 +207,7 @@ public class ComponentAction implements NavigationWsAction { | |||
} | |||
private void writeQualityGate(JsonWriter json, DbSession session, OrganizationDto organization, ComponentDto component) { | |||
QualityGateFinder.QualityGateData qualityGateData = qualityGateFinder.getQualityGate(session, organization, component.getId()); | |||
QualityGateFinder.QualityGateData qualityGateData = qualityGateFinder.getQualityGate(session, organization, component); | |||
QualityGateDto qualityGateDto = qualityGateData.getQualityGate(); | |||
json.name("qualityGate").beginObject() | |||
.prop("key", qualityGateDto.getId()) |
@@ -22,6 +22,7 @@ package org.sonar.server.webhook; | |||
import java.util.Map; | |||
import java.util.Set; | |||
import java.util.stream.Collectors; | |||
import org.apache.commons.lang.StringUtils; | |||
import org.sonar.db.DbClient; | |||
import org.sonar.db.DbSession; | |||
import org.sonar.db.component.AnalysisPropertyDto; | |||
@@ -30,6 +31,7 @@ import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.component.SnapshotDto; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEvent; | |||
import org.sonar.server.qualitygate.changeevent.QGChangeEventListener; | |||
import org.sonar.server.webhook.Branch.Type; | |||
public class WebhookQGChangeEventListener implements QGChangeEventListener { | |||
private final WebHooks webhooks; | |||
@@ -61,17 +63,18 @@ public class WebhookQGChangeEventListener implements QGChangeEventListener { | |||
} | |||
private WebhookPayload buildWebHookPayload(DbSession dbSession, QGChangeEvent event) { | |||
ComponentDto branch = event.getProject(); | |||
BranchDto shortBranch = event.getBranch(); | |||
ComponentDto project = event.getProject(); | |||
BranchDto branch = event.getBranch(); | |||
SnapshotDto analysis = event.getAnalysis(); | |||
Map<String, String> analysisProperties = dbClient.analysisPropertiesDao().selectBySnapshotUuid(dbSession, analysis.getUuid()) | |||
.stream() | |||
.collect(Collectors.toMap(AnalysisPropertyDto::getKey, AnalysisPropertyDto::getValue)); | |||
String projectUuid = StringUtils.defaultString(project.getMainBranchProjectUuid(), project.projectUuid()); | |||
ProjectAnalysis projectAnalysis = new ProjectAnalysis( | |||
new Project(branch.getMainBranchProjectUuid(), branch.getKey(), branch.name()), | |||
new Project(projectUuid, project.getKey(), project.name()), | |||
null, | |||
new Analysis(analysis.getUuid(), analysis.getCreatedAt()), | |||
new Branch(false, shortBranch.getKey(), Branch.Type.SHORT), | |||
new Branch(branch.isMain(), branch.getKey(), Type.valueOf(branch.getBranchType().name())), | |||
event.getQualityGateSupplier().get().orElse(null), | |||
null, | |||
analysisProperties); |
@@ -19,6 +19,7 @@ | |||
*/ | |||
package org.sonar.server.webhook.ws; | |||
import org.sonar.core.util.Protobuf; | |||
import org.sonar.db.component.ComponentDto; | |||
import org.sonar.db.webhook.WebhookDeliveryDto; | |||
import org.sonar.db.webhook.WebhookDeliveryLiteDto; | |||
@@ -39,23 +40,17 @@ class WebhookWsSupport { | |||
.setName(dto.getName()) | |||
.setUrl(dto.getUrl()) | |||
.setSuccess(dto.isSuccess()) | |||
.setCeTaskId(dto.getCeTaskUuid()) | |||
.setComponentKey(component.getDbKey()); | |||
if (dto.getHttpStatus() != null) { | |||
builder.setHttpStatus(dto.getHttpStatus()); | |||
} | |||
if (dto.getDurationMs() != null) { | |||
builder.setDurationMs(dto.getDurationMs()); | |||
} | |||
Protobuf.setNullable(dto.getCeTaskUuid(), builder::setCeTaskId); | |||
Protobuf.setNullable(dto.getHttpStatus(), builder::setHttpStatus); | |||
Protobuf.setNullable(dto.getDurationMs(), builder::setDurationMs); | |||
return builder; | |||
} | |||
static Webhooks.Delivery.Builder copyDtoToProtobuf(ComponentDto component, WebhookDeliveryDto dto, Webhooks.Delivery.Builder builder) { | |||
copyDtoToProtobuf(component, (WebhookDeliveryLiteDto) dto, builder); | |||
builder.setPayload(dto.getPayload()); | |||
if (dto.getErrorStacktrace() != null) { | |||
builder.setErrorStacktrace(dto.getErrorStacktrace()); | |||
} | |||
Protobuf.setNullable(dto.getErrorStacktrace(), builder::setErrorStacktrace); | |||
return builder; | |||
} | |||
} |
@@ -20,13 +20,13 @@ | |||
package org.sonar.server.computation.task.projectanalysis.formula.counter; | |||
import org.junit.Test; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.D; | |||
public class RatingValueTest { | |||
@@ -28,8 +28,8 @@ import org.sonar.server.computation.task.projectanalysis.metric.MetricImpl; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.computation.task.projectanalysis.measure.Measure.newMeasureBuilder; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B; | |||
public class BestValueOptimizationTest { | |||
@@ -19,8 +19,9 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import com.google.common.base.Optional; | |||
import java.util.Optional; | |||
import org.junit.rules.ExternalResource; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
public class MutableQualityGateHolderRule extends ExternalResource implements MutableQualityGateHolder { | |||
private MutableQualityGateHolder delegate = new QualityGateHolderImpl(); | |||
@@ -31,13 +32,18 @@ public class MutableQualityGateHolderRule extends ExternalResource implements Mu | |||
} | |||
@Override | |||
public void setNoQualityGate() { | |||
delegate.setNoQualityGate(); | |||
public Optional<QualityGate> getQualityGate() { | |||
return delegate.getQualityGate(); | |||
} | |||
@Override | |||
public Optional<QualityGate> getQualityGate() { | |||
return delegate.getQualityGate(); | |||
public void setEvaluation(EvaluatedQualityGate evaluation) { | |||
delegate.setEvaluation(evaluation); | |||
} | |||
@Override | |||
public Optional<EvaluatedQualityGate> getEvaluation() { | |||
return delegate.getEvaluation(); | |||
} | |||
@Override |
@@ -19,15 +19,16 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import java.util.Collections; | |||
import org.junit.Test; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import static java.util.Collections.emptyList; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.assertj.guava.api.Assertions.assertThat; | |||
import static org.mockito.Mockito.mock; | |||
public class QualityGateHolderImplTest { | |||
public static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", Collections.<Condition>emptyList()); | |||
private static final QualityGate QUALITY_GATE = new QualityGate(4612, "name", emptyList()); | |||
@Test(expected = IllegalStateException.class) | |||
public void getQualityGate_throws_ISE_if_QualityGate_not_set() { | |||
@@ -56,13 +57,33 @@ public class QualityGateHolderImplTest { | |||
assertThat(holder.getQualityGate().get()).isSameAs(QUALITY_GATE); | |||
} | |||
@Test(expected = IllegalStateException.class) | |||
public void getEvaluation_throws_ISE_if_QualityGate_not_set() { | |||
new QualityGateHolderImpl().getEvaluation(); | |||
} | |||
@Test(expected = NullPointerException.class) | |||
public void setEvaluation_throws_NPE_if_argument_is_null() { | |||
new QualityGateHolderImpl().setEvaluation(null); | |||
} | |||
@Test(expected = IllegalStateException.class) | |||
public void setEvaluation_throws_ISE_if_called_twice() { | |||
QualityGateHolderImpl holder = new QualityGateHolderImpl(); | |||
EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class); | |||
holder.setEvaluation(evaluation); | |||
holder.setEvaluation(evaluation); | |||
} | |||
@Test | |||
public void getQualityGate_returns_absent_if_holder_initialized_with_setNoQualityGate() { | |||
public void getEvaluation_returns_QualityGate_set_by_setQualityGate() { | |||
QualityGateHolderImpl holder = new QualityGateHolderImpl(); | |||
holder.setNoQualityGate(); | |||
EvaluatedQualityGate evaluation = mock(EvaluatedQualityGate.class); | |||
holder.setEvaluation(evaluation); | |||
assertThat(holder.getQualityGate()).isAbsent(); | |||
assertThat(holder.getEvaluation().get()).isSameAs(evaluation); | |||
} | |||
} |
@@ -19,17 +19,21 @@ | |||
*/ | |||
package org.sonar.server.computation.task.projectanalysis.qualitygate; | |||
import com.google.common.base.Optional; | |||
import java.util.Optional; | |||
import javax.annotation.Nullable; | |||
import org.junit.rules.ExternalResource; | |||
import org.sonar.server.qualitygate.EvaluatedQualityGate; | |||
import static com.google.common.base.Preconditions.checkState; | |||
public class QualityGateHolderRule extends ExternalResource implements QualityGateHolder { | |||
@Nullable | |||
private Optional<QualityGate> qualityGate; | |||
@Nullable | |||
private Optional<EvaluatedQualityGate> evaluation; | |||
public void setQualityGate(@Nullable QualityGate qualityGate) { | |||
this.qualityGate = Optional.fromNullable(qualityGate); | |||
this.qualityGate = Optional.ofNullable(qualityGate); | |||
} | |||
@Override | |||
@@ -38,6 +42,16 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa | |||
return qualityGate; | |||
} | |||
public void setEvaluation(@Nullable EvaluatedQualityGate e) { | |||
this.evaluation = Optional.ofNullable(e); | |||
} | |||
@Override | |||
public Optional<EvaluatedQualityGate> getEvaluation() { | |||
checkState(evaluation != null, "EvaluatedQualityGate has not been initialized"); | |||
return evaluation; | |||
} | |||
@Override | |||
protected void after() { | |||
reset(); | |||
@@ -45,5 +59,6 @@ public class QualityGateHolderRule extends ExternalResource implements QualityGa | |||
public void reset() { | |||
this.qualityGate = null; | |||
this.evaluation = null; | |||
} | |||
} |
@@ -25,16 +25,16 @@ import org.junit.Test; | |||
import org.junit.rules.ExpectedException; | |||
import static org.assertj.core.api.Assertions.assertThat; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.RatingGrid.Rating.E; | |||
public class RatingGridTest { | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.A; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.B; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.C; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.D; | |||
import static org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating.E; | |||
private RatingGrid ratingGrid; | |||
public class DebtRatingGridTest { | |||
private DebtRatingGrid ratingGrid; | |||
@Rule | |||
public ExpectedException throwable = ExpectedException.none(); | |||
@@ -42,7 +42,7 @@ public class RatingGridTest { | |||
@Before | |||
public void setUp() { | |||
double[] gridValues = new double[] {0.1, 0.2, 0.5, 1}; | |||
ratingGrid = new RatingGrid(gridValues); | |||
ratingGrid = new DebtRatingGrid(gridValues); | |||
} | |||
@Test |